From 093df5a301f5207bbc5447eaa8b57338fde9758b Mon Sep 17 00:00:00 2001 From: Zero Liu Date: Tue, 26 May 2026 10:50:27 -0700 Subject: [PATCH] improve agent model enabler settings --- designdocs/todo/AGENT_MODE_TODOS.md | 1 + src/agentMode/ui/ModelEnableList.tsx | 45 ++++++++++++++-- .../configuredModelGrouping.test.ts | 51 ++++++++++++++++++ .../v2/components/configuredModelGrouping.ts | 54 +++++++++++++++---- 4 files changed, 137 insertions(+), 14 deletions(-) diff --git a/designdocs/todo/AGENT_MODE_TODOS.md b/designdocs/todo/AGENT_MODE_TODOS.md index 95cb6a05..1cd4908b 100644 --- a/designdocs/todo/AGENT_MODE_TODOS.md +++ b/designdocs/todo/AGENT_MODE_TODOS.md @@ -10,6 +10,7 @@ - [ ] Allow users to share system prompt - [ ] Do a quick spike on the concrete behavior - [ ] Investigate opencode provider specific prompt +- [ ] P0: Test sandbox mode - [x] P0: Skills - [x] Check out cc-switch to understand how to make skills compatible cross other agents https://github.com/farion1231/cc-switch - [x] P0: Permission management diff --git a/src/agentMode/ui/ModelEnableList.tsx b/src/agentMode/ui/ModelEnableList.tsx index 8742b33e..84fe9d5e 100644 --- a/src/agentMode/ui/ModelEnableList.tsx +++ b/src/agentMode/ui/ModelEnableList.tsx @@ -1,6 +1,9 @@ +import { Badge } from "@/components/ui/badge"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { SearchBar } from "@/components/ui/SearchBar"; import { SettingSwitch } from "@/components/ui/setting-switch"; import { cn } from "@/lib/utils"; +import { ChevronRight } from "lucide-react"; import React from "react"; /** A single toggleable model row. */ @@ -23,6 +26,11 @@ export interface ModelEnableGroup { key: string; /** Group heading — a provider display name (no glyphs/avatars). */ label: string; + /** + * Origin badge (e.g. "BYOK", "Copilot Plus", "Agent Provided"). Set only when + * the list spans multiple origins, so it actually disambiguates. + */ + badge?: string; rows: ModelEnableRow[]; } @@ -56,6 +64,14 @@ export const ModelEnableList: React.FC = ({ }) => { const searching = query.trim().length > 0; + // Open by default — track only the keys the user explicitly collapsed. While + // searching, force every group open so matches are never hidden; the collapse + // intent is remembered and re-applies once the query clears. + const [collapsed, setCollapsed] = React.useState>({}); + const isOpen = (key: string) => searching || !collapsed[key]; + const handleOpenChange = (key: string, open: boolean) => + setCollapsed((prev) => ({ ...prev, [key]: !open })); + const renderRows = (rows: ModelEnableRow[]): React.ReactNode => (
{rows.map((row) => ( @@ -94,10 +110,31 @@ export const ModelEnableList: React.FC = ({ {groups .filter((g) => g.rows.length > 0) .map((group) => ( -
-
{group.label}
-
{renderRows(group.rows)}
-
+ handleOpenChange(group.key, open)} + > + +
+ + {group.label} + {group.badge && ( + + {group.badge} + + )} +
+
+ +
{renderRows(group.rows)}
+
+
))}
)} diff --git a/src/settings/v2/components/configuredModelGrouping.test.ts b/src/settings/v2/components/configuredModelGrouping.test.ts index bed09790..41b87443 100644 --- a/src/settings/v2/components/configuredModelGrouping.test.ts +++ b/src/settings/v2/components/configuredModelGrouping.test.ts @@ -317,4 +317,55 @@ describe("buildModelEnableGroups", () => { expect(groups).toHaveLength(1); expect(groups[0].label).toBe("Codex"); }); + + it("badges each group with its origin when the list mixes origins (opencode)", () => { + const plusProvider: Provider = { + providerId: "plus-1", + providerType: "anthropic", + displayName: "Copilot Plus", + origin: { kind: "copilot-plus" }, + addedAt: 0, + }; + const partition = { + byokPlusCandidates: [ + { + configuredModel: model("m-byok", "byok-1", "claude-sonnet-4-5"), + provider: byok, + enabled: true, + }, + { + configuredModel: model("m-plus", "plus-1", "gpt-5"), + provider: plusProvider, + enabled: false, + }, + ], + agentOriginCandidates: [ + { + configuredModel: model("m-oc", "oc-agent", "opencode/big-pickle"), + provider: ocAgent, + enabled: false, + }, + ], + }; + const groups = buildModelEnableGroups(partition, true, ""); + expect(groups.find((g) => g.key === "byok:byok-1")?.badge).toBe("BYOK"); + expect(groups.find((g) => g.key === "byok:plus-1")?.badge).toBe("Copilot Plus"); + expect(groups.find((g) => g.label === "opencode")?.badge).toBe("Agent Provided"); + }); + + it("omits badges when the list has a single origin (claude/codex)", () => { + const codexAgent = agentProvider("codex-agent", "codex", "Codex"); + const partition = { + byokPlusCandidates: [], + agentOriginCandidates: [ + { + configuredModel: model("m-codex", "codex-agent", "gpt-5"), + provider: codexAgent, + enabled: true, + }, + ], + }; + const groups = buildModelEnableGroups(partition, false, ""); + expect(groups[0].badge).toBeUndefined(); + }); }); diff --git a/src/settings/v2/components/configuredModelGrouping.ts b/src/settings/v2/components/configuredModelGrouping.ts index c22cdb7b..7e5581f2 100644 --- a/src/settings/v2/components/configuredModelGrouping.ts +++ b/src/settings/v2/components/configuredModelGrouping.ts @@ -104,12 +104,36 @@ export function rowMatches(row: ModelEnableRow, q: string): boolean { ); } +/** Provider-origin kind, used to label the per-group disambiguation badge. */ +type OriginKind = Provider["origin"]["kind"]; + +/** User-facing badge label for a provider's origin. */ +function originBadgeLabel(kind: OriginKind): string { + switch (kind) { + case "byok": + return "BYOK"; + case "copilot-plus": + return "Copilot Plus"; + case "agent": + return "Agent Provided"; + } +} + +/** A built group paired with the origin kind every row in it shares. */ +interface OriginGroup { + group: ModelEnableGroup; + kind: OriginKind; +} + /** * Provider-grouped rows for the shared list. BYOK/Plus candidates get one group * per provider; agent-origin candidates get wire-prefix sub-groups for opencode * (`opencode`, `openrouter`, …) or a per-provider group for claude/codex. All - * groups render the same flat, always-visible way. Groups emptied by `query` - * are dropped. + * groups render the same flat way. Groups emptied by `query` are dropped. + * + * Each group maps to a single origin, so when the list spans more than one + * origin (opencode mixes BYOK/Plus/agent) we tag every group with an origin + * badge to disambiguate; a single-origin list (claude/codex) gets none. */ export function buildModelEnableGroups( partition: CandidatePartition, @@ -117,20 +141,25 @@ export function buildModelEnableGroups( query: string ): ModelEnableGroup[] { const q = query.trim().toLowerCase(); - const out: ModelEnableGroup[] = []; + const out: OriginGroup[] = []; - // BYOK/Plus providers — one group per provider, always visible. - const byProvider = new Map(); + // BYOK/Plus providers — one group per provider. + const byProvider = new Map(); for (const candidate of partition.byokPlusCandidates) { const row = toRow(candidate); if (!rowMatches(row, q)) continue; const key = candidate.provider.providerId; const bucket = byProvider.get(key); if (bucket) bucket.rows.push(row); - else byProvider.set(key, { label: candidate.provider.displayName, rows: [row] }); + else + byProvider.set(key, { + label: candidate.provider.displayName, + kind: candidate.provider.origin.kind, + rows: [row], + }); } - for (const [key, { label, rows }] of byProvider) { - out.push({ key: `byok:${key}`, label, rows }); + for (const [key, { label, kind, rows }] of byProvider) { + out.push({ group: { key: `byok:${key}`, label, rows }, kind }); } // Agent-origin models — opencode-only sub-groups (by wire prefix) or a @@ -147,8 +176,13 @@ export function buildModelEnableGroups( else bySubGroup.set(label, { label, rows: [row] }); } for (const [label, { rows }] of bySubGroup) { - out.push({ key: `agent:${label}`, label, rows }); + out.push({ group: { key: `agent:${label}`, label, rows }, kind: "agent" }); } - return out; + // Badge each group only when the list actually mixes origins. + const mixed = new Set(out.map((o) => o.kind)).size > 1; + if (mixed) { + for (const o of out) o.group.badge = originBadgeLabel(o.kind); + } + return out.map((o) => o.group); }