mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
improve agent model enabler settings
This commit is contained in:
parent
86a9748580
commit
093df5a301
4 changed files with 137 additions and 14 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue