improve agent model enabler settings

This commit is contained in:
Zero Liu 2026-05-26 10:50:27 -07:00
parent 86a9748580
commit 093df5a301
No known key found for this signature in database
4 changed files with 137 additions and 14 deletions

View file

@ -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

View file

@ -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<ModelEnableListProps> = ({
}) => {
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<Record<string, boolean>>({});
const isOpen = (key: string) => searching || !collapsed[key];
const handleOpenChange = (key: string, open: boolean) =>
setCollapsed((prev) => ({ ...prev, [key]: !open }));
const renderRows = (rows: ModelEnableRow[]): React.ReactNode => (
<div className="tw-space-y-1">
{rows.map((row) => (
@ -94,10 +110,31 @@ export const ModelEnableList: React.FC<ModelEnableListProps> = ({
{groups
.filter((g) => g.rows.length > 0)
.map((group) => (
<div key={group.key}>
<div className="tw-px-2 tw-py-1.5 tw-font-medium">{group.label}</div>
<div className="tw-pl-2">{renderRows(group.rows)}</div>
</div>
<Collapsible
key={group.key}
open={isOpen(group.key)}
onOpenChange={(open) => handleOpenChange(group.key, open)}
>
<CollapsibleTrigger asChild>
<div className="tw-flex tw-w-full tw-cursor-pointer tw-items-center tw-gap-1 tw-rounded tw-px-2 tw-py-1.5 tw-text-left tw-text-ui-medium tw-font-bold hover:tw-bg-modifier-hover">
<ChevronRight
className={cn(
"tw-size-3 tw-shrink-0 tw-text-muted tw-transition-transform",
isOpen(group.key) && "tw-rotate-90"
)}
/>
<span className="tw-truncate">{group.label}</span>
{group.badge && (
<Badge variant="secondary" className="tw-shrink-0 tw-font-normal">
{group.badge}
</Badge>
)}
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="tw-pl-4">{renderRows(group.rows)}</div>
</CollapsibleContent>
</Collapsible>
))}
</div>
)}

View file

@ -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();
});
});

View file

@ -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<string, { label: string; rows: ModelEnableRow[] }>();
// BYOK/Plus providers — one group per provider.
const byProvider = new Map<string, { label: string; kind: OriginKind; rows: ModelEnableRow[] }>();
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);
}