feat(x): one model-selection experience — unified settings, provider lifecycle, split-view picker

Task slots (config + runtime):
- taskModels gains backgroundTask and subagent; getBackgroundTaskAgentModel
  resolves its own override instead of mirroring live-notes (migration
  materializes the old mirror rule so effective models are unchanged)
- spawn-agent precedence: explicit per-spawn args > configured subagent
  override > parent turn's model
- repo.setProvider rejects auth-derived flavors (rowboat/codex) and merges
  over existing entries so key rotation keeps contextLength/reasoningEffort

Models settings, rebuilt:
- ModelSelectionSection: ONE Assistant model picker (with an Unavailable
  badge when the saved model drops off its provider's live list) + all
  seven tasks defaulting to "Same as Assistant" with resolve subtext and a
  clear-override link. Replaces both the signed-in and BYOK screens; the
  "Auto (recommended)" sentinel and both hardcoded preferredDefaults maps
  are gone — connect-time resolution uses backend recommendations
- ProvidersSection replaces the old ModelSettings card grid: status cards
  (Connected · N models / error + Retry), an add-provider dialog
  (choose → creds → loading → first-vs-more result states → error with
  manual-model fallback), and a manage dialog (masked key + Replace,
  endpoint, Refresh models, Used-by list, disconnect with consequences
  spelled out). Rowboat and ChatGPT are cards in the same list
- connecting a provider seeds the assistant ONLY when none is configured —
  never replaces a saved choice
- models:getConfig IPC serves selections + credential-free provider
  metadata; the probe machinery (use-provider-models, liveCredentials)
  is deleted — the add flow validates credentials click-driven

Onboarding:
- screen 2 is the same ProvidersSection (onboarding variant) for BOTH
  paths: "Add more providers" after sign-in, "Connect a model provider"
  otherwise; Continue gates on an assistant model existing. The tile grid,
  per-provider key forms, and ~350 lines of duplicate state are deleted;
  one step sequence replaces the two path-dependent indicators

Picker:
- split-view browse (Popover + cmdk Command): providers left with counts
  and error dots, selected provider's models right, ←/→ switches provider,
  ↑/↓ navigates, typing collapses to a flat cross-provider list that
  matches provider NAMES as well as model ids (searching "rowboat" now
  works). Scoped/static pickers stay flat; public API unchanged

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-24 16:16:21 +05:30
parent f39daa9f5d
commit c695ddf1c1
24 changed files with 1599 additions and 1917 deletions

View file

@ -656,7 +656,7 @@ const ipcSchemas = {
}).nullable(),
res: z.object({
providers: z.array(z.object({
// Provider INSTANCE id — what ModelRef.provider / defaultSelection /
// Provider INSTANCE id — what ModelRef.provider / assistantModel /
// refreshProvider reference. One instance per flavor today, so it
// always equals the flavor key; kept distinct so a future
// multi-instance setup ("openai-work") is additive.
@ -748,6 +748,33 @@ const ipcSchemas = {
success: z.literal(true),
}),
},
// Current model selections plus credential-FREE provider metadata (the
// renderer never needs keys to render pickers or provider cards). Null
// assistantModel = not configured yet.
'models:getConfig': {
req: z.null(),
res: z.object({
// Configured BYOK provider entries, secrets stripped: enough to
// render manage/edit surfaces (masked key indicator, endpoint).
providers: z.array(z.object({
id: z.string(),
flavor: z.string(),
baseURL: z.string().optional(),
hasApiKey: z.boolean(),
})),
assistantModel: ModelRef.nullable(),
taskModels: z.object({
knowledgeGraph: ModelRef.nullable(),
meetingNotes: ModelRef.nullable(),
liveNoteAgent: ModelRef.nullable(),
autoPermissionDecision: ModelRef.nullable(),
chatTitle: ModelRef.nullable(),
backgroundTask: ModelRef.nullable(),
subagent: ModelRef.nullable(),
}),
deferBackgroundTasks: z.boolean(),
}),
},
// Partial merge of model selections into models.json. Omitted keys are
// untouched; null clears a key (a cleared task override inherits the
// assistant model again). taskModels merges per-key.
@ -760,6 +787,8 @@ const ipcSchemas = {
liveNoteAgent: ModelRef.nullable().optional(),
autoPermissionDecision: ModelRef.nullable().optional(),
chatTitle: ModelRef.nullable().optional(),
backgroundTask: ModelRef.nullable().optional(),
subagent: ModelRef.nullable().optional(),
}).optional(),
deferBackgroundTasks: z.boolean().nullable().optional(),
}),

View file

@ -41,13 +41,17 @@ export const ModelRef = z.object({
model: z.string(),
});
// The per-task model override slots. Absence = inherit the assistant model.
// The per-task model override slots. Absence = inherit the assistant model
// (except `subagent`, whose default is the PARENT turn's model — which is
// the assistant for a top-level chat).
export const TaskModels = z.object({
knowledgeGraph: ModelRef.optional(),
meetingNotes: ModelRef.optional(),
liveNoteAgent: ModelRef.optional(),
autoPermissionDecision: ModelRef.optional(),
chatTitle: ModelRef.optional(),
backgroundTask: ModelRef.optional(),
subagent: ModelRef.optional(),
});
export type TaskModelKey = keyof z.infer<typeof TaskModels>;