feat(code-mode): per-session model + effort, and keep output on nav (#632)

Two improvements to the Code section:

- Fix: leaving the Code section and returning no longer drops the open
  session's output. The selected session id is persisted to localStorage
  (mirroring the terminal-height pattern) and restored on remount, so the
  right-hand chat pane re-binds instead of falling back to the empty state.

- Feature: choose the coding agent's model and reasoning effort per session.
  Choices are discovered live from the engine (the same list `/model` shows)
  via a new `codeMode:listModelOptions` IPC, cached per agent — never
  hardcoded, so they track whatever the provider currently offers. Claude
  exposes model + effort as separate axes (with explicit Opus/Sonnet/Haiku
  alias rows surfaced for clarity); Codex folds effort into the model id and
  reports no separate effort. Selections persist on the CodeSession and are
  re-applied to the ACP session each turn (best-effort), editable from both
  the new-session dialog and the session header.
This commit is contained in:
gagan 2026-06-21 08:38:49 -07:00 committed by GitHub
parent c8d801a123
commit 45188e7c1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 360 additions and 9 deletions

View file

@ -53,11 +53,34 @@ export const CodeSession = z.object({
// Where the agent works: the project path, or the worktree path.
cwd: z.string(),
worktree: CodeWorktree.optional(),
// The coding agent's own model + reasoning effort (applied to the ACP engine,
// not the Rowboat-mode LLM). Values come from CODE_AGENT_MODELS /
// CODE_AGENT_EFFORTS; unset (or 'default') leaves the engine's own default.
agentModel: z.string().optional(),
agentEffort: z.string().optional(),
createdAt: z.iso.datetime(),
lastActivityAt: z.iso.datetime().optional(),
});
export type CodeSession = z.infer<typeof CodeSession>;
// Model + effort choices for the ACP coding agents are discovered live from the
// engine (the same list `/model` shows), not hardcoded — so they always reflect
// whatever the provider currently offers. See the `codeMode:listModelOptions`
// IPC and CodeModeManager.listModelOptions. 'default' is a synthetic sentinel
// meaning "don't override the engine default".
//
// Claude exposes model and effort as two independent options; Codex folds the
// reasoning effort into the model id ("gpt-5-codex[high]") and so reports no
// separate effort list. The UI renders whatever each agent advertises.
export const CodeAgentOption = z.object({ value: z.string(), label: z.string() });
export type CodeAgentOption = z.infer<typeof CodeAgentOption>;
export const CodeAgentModelOptions = z.object({
models: z.array(CodeAgentOption),
efforts: z.array(CodeAgentOption),
});
export type CodeAgentModelOptions = z.infer<typeof CodeAgentModelOptions>;
export const GitFileState = z.enum(["modified", "added", "deleted", "untracked", "renamed"]);
export type GitFileState = z.infer<typeof GitFileState>;

View file

@ -21,7 +21,7 @@ import { BillingInfoSchema } from './billing.js';
import { EmailBlockSchema, GmailThreadSchema } from './blocks.js';
import { PermissionDecision, ApprovalPolicy, CodingAgent } from './code-mode.js';
import { NotificationSettingsSchema } from './notification-settings.js';
import { CodeProject, CodeSession, CodeSessionMode, CodeSessionStatus, GitRepoInfo, GitStatusFile } from './code-sessions.js';
import { CodeProject, CodeSession, CodeSessionMode, CodeSessionStatus, GitRepoInfo, GitStatusFile, CodeAgentModelOptions } from './code-sessions.js';
// ============================================================================
// Runtime Validation Schemas (Single Source of Truth)
@ -601,6 +601,10 @@ const ipcSchemas = {
// chat, the model is fixed once the session's run exists.
model: z.string().optional(),
provider: z.string().optional(),
// The coding agent's own model + reasoning effort (ACP engine). Unlike the
// Rowboat model these are re-applied each turn, so they stay editable.
agentModel: z.string().optional(),
agentEffort: z.string().optional(),
}),
res: z.object({
session: CodeSession,
@ -616,12 +620,18 @@ const ipcSchemas = {
'codeSession:update': {
req: z.object({
sessionId: z.string(),
patch: CodeSession.pick({ title: true, mode: true, policy: true, agent: true }).partial(),
patch: CodeSession.pick({ title: true, mode: true, policy: true, agent: true, agentModel: true, agentEffort: true }).partial(),
}),
res: z.object({
session: CodeSession,
}),
},
// Live model + effort choices for a coding agent, discovered from the engine
// (cached per agent in the main process). Mirrors what `/model` would show.
'codeMode:listModelOptions': {
req: z.object({ agent: CodingAgent }),
res: CodeAgentModelOptions,
},
'codeSession:delete': {
req: z.object({
sessionId: z.string(),