@ferris1225/pi-subagents 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -0
- package/README.md +188 -95
- package/agents/cleaner.md +51 -0
- package/agents/explore.md +6 -4
- package/agents/reviewer.md +2 -0
- package/package.json +9 -7
- package/src/agents.ts +2 -7
- package/src/announcements.ts +12 -1
- package/src/completion.ts +7 -36
- package/src/config.ts +310 -364
- package/src/dispatch.ts +145 -275
- package/src/fixloop.ts +0 -16
- package/src/format.ts +28 -30
- package/src/index.ts +6 -3
- package/src/models.ts +89 -106
- package/src/monitor.ts +57 -101
- package/src/prompt.ts +13 -8
- package/src/rpc-run.ts +90 -21
- package/src/runtime.ts +3 -17
- package/src/session-fork.ts +0 -4
- package/src/setup.ts +437 -639
- package/src/spawn.ts +587 -557
- package/src/tools.ts +27 -35
- package/src/ui.ts +3 -7
- package/src/widget.ts +144 -0
- package/src/worktree.ts +1 -1
- package/src/trajectory.ts +0 -312
package/src/fixloop.ts
CHANGED
|
@@ -86,22 +86,6 @@ export function chainKeyFragments(result: SingleResult): string[] {
|
|
|
86
86
|
return extractKeyFragments(getResultOutput(result)).slice(0, CHAIN_SUMMARY_FRAGMENTS_MAX);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/**
|
|
90
|
-
* Compact one-line outcome for a finished chain run, retained so
|
|
91
|
-
* each round reads as what it did: a reviewer reports its verdict plus the
|
|
92
|
-
* key fragments of what it found ("fail · src/index.ts · render()"), a worker
|
|
93
|
-
* the fragments of what it changed. Failed runs and runs with nothing
|
|
94
|
-
* distinctive get no summary.
|
|
95
|
-
*/
|
|
96
|
-
export function summarizeChainResult(result: SingleResult): string | undefined {
|
|
97
|
-
if (isFailedResult(result)) return undefined;
|
|
98
|
-
const verdict = result.agent === "reviewer" ? reviewVerdict(getResultOutput(result)) : undefined;
|
|
99
|
-
if (verdict === "pass") return "pass";
|
|
100
|
-
const fragments = chainKeyFragments(result);
|
|
101
|
-
if (verdict === "fail") return fragments.length > 0 ? `fail · ${fragments.join(" · ")}` : "fail";
|
|
102
|
-
return fragments.length > 0 ? fragments.join(" · ") : undefined;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
89
|
/**
|
|
106
90
|
* Condensed, readable summary of a completed auto-fix chain: one line per step
|
|
107
91
|
* (run id, role, verdict / what changed) plus aggregate usage. Full per-step
|
package/src/format.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { AgentConfig } from "./agents.ts";
|
|
8
8
|
import { formatTaskSummary } from "./monitor.ts";
|
|
9
|
+
import { emptyUsage } from "./rpc-run.ts";
|
|
9
10
|
import {
|
|
10
11
|
getResultOutput,
|
|
11
12
|
isFailedResult,
|
|
@@ -15,14 +16,9 @@ import {
|
|
|
15
16
|
type UsageStats,
|
|
16
17
|
} from "./spawn.ts";
|
|
17
18
|
|
|
18
|
-
export function emptyUsage(): UsageStats {
|
|
19
|
-
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
19
|
export function queuedResult(agent: AgentConfig, task: string, thinking?: string): SingleResult {
|
|
23
20
|
return {
|
|
24
21
|
agent: agent.name,
|
|
25
|
-
agentSource: agent.source,
|
|
26
22
|
task,
|
|
27
23
|
exitCode: -1,
|
|
28
24
|
messages: [],
|
|
@@ -36,7 +32,6 @@ export function queuedResult(agent: AgentConfig, task: string, thinking?: string
|
|
|
36
32
|
export function failedStartResult(agentName: string, task: string, errorMessage: string): SingleResult {
|
|
37
33
|
return {
|
|
38
34
|
agent: agentName,
|
|
39
|
-
agentSource: "unknown",
|
|
40
35
|
task,
|
|
41
36
|
exitCode: 1,
|
|
42
37
|
messages: [],
|
|
@@ -61,7 +56,7 @@ export function dispatchFailedResult(agent: AgentConfig, task: string, error: un
|
|
|
61
56
|
};
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
|
|
59
|
+
function formatTokens(count: number): string {
|
|
65
60
|
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
66
61
|
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
67
62
|
return String(count);
|
|
@@ -78,33 +73,40 @@ export function formatUsage(usage: UsageStats): string {
|
|
|
78
73
|
return parts.join(" ");
|
|
79
74
|
}
|
|
80
75
|
|
|
81
|
-
export
|
|
76
|
+
export interface CompletionFormatOptions {
|
|
77
|
+
/** Include individual failed-tool errors. Reserved for explicit status lookup. */
|
|
78
|
+
failedToolDetails?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function formatCompletionBlock(
|
|
82
|
+
result: SingleResult,
|
|
83
|
+
maxResultLines: number,
|
|
84
|
+
cwd?: string,
|
|
85
|
+
options: CompletionFormatOptions = {},
|
|
86
|
+
): string {
|
|
82
87
|
const failed = isFailedResult(result);
|
|
83
88
|
const failedTools = result.failedTools ?? [];
|
|
84
89
|
const status = failed
|
|
85
90
|
? "failed"
|
|
86
|
-
: failedTools.length > 0
|
|
91
|
+
: options.failedToolDetails && failedTools.length > 0
|
|
87
92
|
? `completed with ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}`
|
|
88
93
|
: "completed";
|
|
89
94
|
const usage = formatUsage(result.usage);
|
|
90
95
|
const output = getResultOutput(result);
|
|
91
96
|
const { text, truncated } = truncateResultOutput(output, maxResultLines);
|
|
92
97
|
const fallbackNote = result.modelFallbackFrom
|
|
93
|
-
? ` (model
|
|
98
|
+
? ` (selected model ${result.modelFallbackFrom} failed → main ${result.model ?? "dynamic default"})`
|
|
94
99
|
: "";
|
|
95
100
|
const startupRetryNote = result.startupRetries
|
|
96
101
|
? ` (recovered after ${result.startupRetries} startup retr${result.startupRetries === 1 ? "y" : "ies"} — concurrent pi startup race)`
|
|
97
102
|
: "";
|
|
98
|
-
const modelRetryNote = result.modelRetries
|
|
99
|
-
? ` (recovered after ${result.modelRetries} same-model retr${result.modelRetries === 1 ? "y" : "ies"} on a transient provider error)`
|
|
100
|
-
: "";
|
|
101
103
|
const relations = [
|
|
102
104
|
result.forkedFromRunId !== undefined ? `forked from #${result.forkedFromRunId}` : undefined,
|
|
103
105
|
(result.forkChildRunIds?.length ?? 0) > 0 ? `fork children ${result.forkChildRunIds!.map((id) => `#${id}`).join(", ")}` : undefined,
|
|
104
106
|
].filter((value): value is string => Boolean(value));
|
|
105
107
|
const relationNote = relations.length > 0 ? ` · ${relations.join(" · ")}` : "";
|
|
106
108
|
const runNote = result.runId !== undefined ? ` · run #${result.runId}` : "";
|
|
107
|
-
const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}${startupRetryNote}${
|
|
109
|
+
const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}${startupRetryNote}${runNote}`, "", `Task: ${formatTaskSummary(result.task, 80, false)}`, ""];
|
|
108
110
|
if (result.isolation === "worktree") {
|
|
109
111
|
const isolation =
|
|
110
112
|
result.integrationStatus === "integrated"
|
|
@@ -125,23 +127,22 @@ export function formatCompletionBlock(result: SingleResult, maxResultLines: numb
|
|
|
125
127
|
lines.push(`Relation: ${relations.join(" · ")}`, "");
|
|
126
128
|
}
|
|
127
129
|
lines.push(text);
|
|
128
|
-
//
|
|
129
|
-
// the
|
|
130
|
-
//
|
|
131
|
-
if (
|
|
132
|
-
const shown = failedTools.slice(0, 3);
|
|
133
|
-
const more = failedTools.length - shown.length;
|
|
130
|
+
// Explicit status always exposes every retained diagnostic, including when
|
|
131
|
+
// the overall run failed or was aborted. Automatic delivery adds only a
|
|
132
|
+
// compact pointer for otherwise-clean runs.
|
|
133
|
+
if (options.failedToolDetails && failedTools.length > 0) {
|
|
134
134
|
lines.push(
|
|
135
135
|
"",
|
|
136
|
-
`⚠ ${failedTools.length} tool call${failedTools.length === 1 ? "" : "s"}
|
|
137
|
-
...
|
|
136
|
+
`⚠ ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}:`,
|
|
137
|
+
...failedTools.map((tool) => `- ${tool.toolName}: ${tool.error.trim() || "(no output)"}`),
|
|
138
138
|
);
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
} else if (!failed && failedTools.length > 0) {
|
|
140
|
+
const lookup = result.runId !== undefined ? ` · details: subagent_status #${result.runId}` : "";
|
|
141
|
+
lines.push("", `⚠ ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}${lookup}`);
|
|
141
142
|
}
|
|
142
143
|
if (truncated) {
|
|
143
144
|
// The full text lives on disk so the main agent can read it on demand.
|
|
144
|
-
lines.push("", `(output truncated to ${maxResultLines} lines; full result: ${writeResultArtifact(output, result.agent, cwd)})`);
|
|
145
|
+
lines.push("", `(output truncated to ${maxResultLines} lines; full result: ${writeResultArtifact(output, result.agent, result.projectCwd ?? cwd)})`);
|
|
145
146
|
}
|
|
146
147
|
return lines.join("\n");
|
|
147
148
|
}
|
|
@@ -153,15 +154,12 @@ export function formatCompletionBlock(result: SingleResult, maxResultLines: numb
|
|
|
153
154
|
* RESUME it in-context once a model is available, instead of re-dispatching
|
|
154
155
|
* fresh (which would re-scan everything). */
|
|
155
156
|
export function modelLevelTakeoverNote(result: SingleResult, opts?: { runId?: number }): string {
|
|
156
|
-
const
|
|
157
|
-
? `, after ${result.modelRetries} same-model retr${result.modelRetries === 1 ? "y" : "ies"} on transient errors`
|
|
158
|
-
: "";
|
|
159
|
-
const retry = result.modelFallbackFrom ? ", and the configured backup chain also failed" : "";
|
|
157
|
+
const retry = result.modelFallbackFrom ? ", and the current main model also failed" : "";
|
|
160
158
|
const sessionPreserved = Boolean(result.sessionDir && result.sessionId) && opts?.runId !== undefined;
|
|
161
159
|
const recovery = sessionPreserved
|
|
162
160
|
? ` The sub-agent's earlier work in this run is preserved. Once a model is available again, call subagent_control with { action: "resume", id: ${opts!.runId} } to CONTINUE it in-context (it keeps the same run id and does not re-scan), or execute the task in the main window with your own tools.`
|
|
163
161
|
: ` Please execute this task in the main window with your own tools; do not re-dispatch it as a sub-agent.`;
|
|
164
|
-
return `The sub-agent could not complete this task: its model was unavailable or failed (or the run stalled)${
|
|
162
|
+
return `The sub-agent could not complete this task: its model was unavailable or failed (or the run stalled)${retry}.${recovery}`;
|
|
165
163
|
}
|
|
166
164
|
|
|
167
165
|
/** Resolve a run-id request to actual ids: an exact numeric match always wins
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* The heavy lifting lives in focused modules:
|
|
6
6
|
* - dispatch.ts — the `subagent` tool (spawn, auto-fix chain, vision model)
|
|
7
7
|
* - tools.ts — subagent_control / subagent_wait / status / stop
|
|
8
|
-
* - announcements.ts — session-start recovery and
|
|
8
|
+
* - announcements.ts — session-start recovery, notices, and widget install
|
|
9
|
+
* - widget.ts — active-only TUI run status
|
|
9
10
|
* - runtime.ts — shared per-session state
|
|
10
11
|
*
|
|
11
12
|
* Also registers the `/subagents-setup` command and a `before_agent_start` hook
|
|
@@ -28,6 +29,7 @@ import { createRuntime } from "./runtime.ts";
|
|
|
28
29
|
import { runSetup } from "./setup.ts";
|
|
29
30
|
import { currentSubagentDepth } from "./spawn.ts";
|
|
30
31
|
import { registerLookupTools } from "./tools.ts";
|
|
32
|
+
import { clearActiveRunsWidget } from "./widget.ts";
|
|
31
33
|
|
|
32
34
|
export { matchRunIds };
|
|
33
35
|
|
|
@@ -57,7 +59,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
57
59
|
),
|
|
58
60
|
);
|
|
59
61
|
|
|
60
|
-
pi.on("session_shutdown", async () => {
|
|
62
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
63
|
+
clearActiveRunsWidget(ctx);
|
|
61
64
|
await runtime.shutdown();
|
|
62
65
|
});
|
|
63
66
|
|
|
@@ -65,7 +68,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
65
68
|
registerLookupTools(pi, runtime);
|
|
66
69
|
|
|
67
70
|
pi.registerCommand("subagents-setup", {
|
|
68
|
-
description: "Configure pi-subagents:
|
|
71
|
+
description: "Configure pi-subagents: agents, selected models, capability-aware thinking, vision, and runtime settings",
|
|
69
72
|
handler: async (_args, ctx) => {
|
|
70
73
|
await runSetup(ctx, configPath);
|
|
71
74
|
},
|
package/src/models.ts
CHANGED
|
@@ -1,61 +1,55 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Model-
|
|
2
|
+
* Model routing, capability-aware thinking, and setup-picker helpers.
|
|
3
3
|
*
|
|
4
|
-
* Runtime
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* only for honest availability labels; it never rewrites persisted choices.
|
|
4
|
+
* Runtime has one explicit fallback only: a configured agent/vision model hands
|
|
5
|
+
* off directly to the current main-window model. Setup lists only currently
|
|
6
|
+
* available models and derives thinking choices from Pi's model metadata.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
clampThinkingLevel,
|
|
11
|
+
getSupportedThinkingLevels,
|
|
12
|
+
type Api,
|
|
13
|
+
type Model,
|
|
14
|
+
} from "@earendil-works/pi-ai";
|
|
11
15
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { DEFAULT_THINKING_LEVEL, type ThinkingLevel } from "./config.ts";
|
|
12
17
|
|
|
13
18
|
export type ModelContext = Pick<ExtensionContext, "model" | "modelRegistry"> &
|
|
14
19
|
Partial<Pick<ExtensionContext, "scopedModels">>;
|
|
15
20
|
|
|
16
21
|
export const CURRENT_MAIN_MODEL = "__current_main_model__";
|
|
17
22
|
|
|
18
|
-
export type
|
|
19
|
-
export type ModelPickerSlot = ModelPoolSlot | "vision";
|
|
23
|
+
export type ModelPickerSlot = "agent" | "vision";
|
|
20
24
|
|
|
21
25
|
export interface ModelPickerItem {
|
|
22
26
|
value: string;
|
|
23
27
|
label: string;
|
|
24
28
|
description?: string;
|
|
25
|
-
/** Visible for diagnosis/search, but cannot be selected. */
|
|
26
|
-
disabled?: boolean;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export type ModelListEntry = Pick<
|
|
30
32
|
Model<Api>,
|
|
31
|
-
"provider" | "id" | "name" | "input" | "reasoning"
|
|
33
|
+
"provider" | "id" | "name" | "input" | "reasoning" | "thinkingLevelMap"
|
|
32
34
|
>;
|
|
33
35
|
|
|
34
|
-
export interface
|
|
35
|
-
/** Effective first candidate. Undefined means let
|
|
36
|
+
export interface ResolvedAgentModelRoute {
|
|
37
|
+
/** Effective first candidate. Undefined means let Pi use its normal default. */
|
|
36
38
|
primaryRef?: string;
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
/**
|
|
39
|
+
/** Current main-window model when it differs from the selection. */
|
|
40
|
+
mainFallbackRef?: string;
|
|
41
|
+
/** Runtime order, useful for status/tests. */
|
|
40
42
|
candidateRefs: string[];
|
|
43
|
+
/** Configured ref skipped because Pi does not currently report it available. */
|
|
44
|
+
unavailableSelectedRef?: string;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
export interface
|
|
44
|
-
|
|
45
|
-
backupRef?: string;
|
|
47
|
+
export interface AgentModelRouteInput {
|
|
48
|
+
selectedRef?: string;
|
|
46
49
|
mainRef?: string;
|
|
47
50
|
declaredDefaultRef?: string;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export interface AgentModelPoolMaps {
|
|
51
|
-
agentModels: Record<string, string>;
|
|
52
|
-
agentBackupModels: Record<string, string>;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface AgentModelPoolRow {
|
|
56
|
-
name: string;
|
|
57
|
-
primary: string;
|
|
58
|
-
backup: string;
|
|
51
|
+
/** When supplied, a configured selection outside this live set is skipped. */
|
|
52
|
+
availableRefs?: readonly string[];
|
|
59
53
|
}
|
|
60
54
|
|
|
61
55
|
function cleanModelRef(ref: string | undefined): string | undefined {
|
|
@@ -78,70 +72,84 @@ export function currentModelRef(ctx: Pick<ModelContext, "model">): string | unde
|
|
|
78
72
|
*/
|
|
79
73
|
export function availableModelsInScope(ctx: ModelContext): readonly Model<Api>[] {
|
|
80
74
|
const models = ctx.modelRegistry.getAvailable();
|
|
81
|
-
// scopedModels was added after the
|
|
82
|
-
//
|
|
75
|
+
// scopedModels was added after the original Pi minimum. Treat a missing field
|
|
76
|
+
// exactly like an empty scope and use the full live registry.
|
|
83
77
|
const scopedModels = ctx.scopedModels ?? [];
|
|
84
78
|
if (scopedModels.length === 0) return models;
|
|
85
79
|
const scopedRefs = new Set(scopedModels.map((entry) => modelRef(entry.model)));
|
|
86
80
|
return models.filter((model) => scopedRefs.has(modelRef(model)));
|
|
87
81
|
}
|
|
88
82
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return
|
|
83
|
+
export function findModelByRef(
|
|
84
|
+
models: readonly Model<Api>[],
|
|
85
|
+
ref: string | undefined,
|
|
86
|
+
): Model<Api> | undefined {
|
|
87
|
+
const normalized = cleanModelRef(ref);
|
|
88
|
+
return normalized ? models.find((model) => modelRef(model) === normalized) : undefined;
|
|
95
89
|
}
|
|
96
90
|
|
|
97
91
|
/**
|
|
98
|
-
* Resolve one agent's
|
|
92
|
+
* Resolve one agent's runtime route:
|
|
99
93
|
*
|
|
100
|
-
* configured
|
|
94
|
+
* configured selection -> current main-window model
|
|
101
95
|
*
|
|
102
|
-
* Without
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* fail normally at runtime instead of being silently repaired.
|
|
96
|
+
* Without an override, current main is primary; the agent-declared default is
|
|
97
|
+
* used only when no main model exists. A configured selection that Pi no longer
|
|
98
|
+
* reports as available is skipped immediately instead of spawning a doomed child.
|
|
106
99
|
*/
|
|
107
|
-
export function
|
|
100
|
+
export function resolveAgentModelRoute(input: AgentModelRouteInput): ResolvedAgentModelRoute {
|
|
101
|
+
const selectedRef = cleanModelRef(input.selectedRef);
|
|
108
102
|
const mainRef = cleanModelRef(input.mainRef);
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
103
|
+
const declaredDefaultRef = cleanModelRef(input.declaredDefaultRef);
|
|
104
|
+
const available = input.availableRefs
|
|
105
|
+
? new Set(input.availableRefs.map((ref) => ref.trim()).filter(Boolean))
|
|
106
|
+
: undefined;
|
|
107
|
+
const selectedAvailable = !selectedRef || !available || available.has(selectedRef);
|
|
108
|
+
const usableSelectedRef = selectedAvailable ? selectedRef : undefined;
|
|
109
|
+
const primaryRef = usableSelectedRef ?? mainRef ?? declaredDefaultRef;
|
|
110
|
+
const ordered = [primaryRef, usableSelectedRef && usableSelectedRef !== mainRef ? mainRef : undefined];
|
|
111
|
+
const candidateRefs = [...new Set(ordered.filter((ref): ref is string => Boolean(ref)))];
|
|
112
|
+
return {
|
|
113
|
+
primaryRef,
|
|
114
|
+
...(candidateRefs[1] ? { mainFallbackRef: candidateRefs[1] } : {}),
|
|
115
|
+
candidateRefs,
|
|
116
|
+
...(!selectedAvailable && selectedRef ? { unavailableSelectedRef: selectedRef } : {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The exact levels Pi exposes for this model, including `off` when supported. */
|
|
121
|
+
export function supportedThinkingLevels(model: Model<Api> | undefined): ThinkingLevel[] {
|
|
122
|
+
return model ? (getSupportedThinkingLevels(model) as ThinkingLevel[]) : [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Clamp an agent preference to the effective model's actual capability map. */
|
|
126
|
+
export function resolveThinkingLevel(
|
|
127
|
+
model: Model<Api> | undefined,
|
|
128
|
+
preferred: ThinkingLevel = DEFAULT_THINKING_LEVEL,
|
|
129
|
+
): ThinkingLevel {
|
|
130
|
+
return model ? (clampThinkingLevel(model, preferred) as ThinkingLevel) : preferred;
|
|
120
131
|
}
|
|
121
132
|
|
|
122
133
|
function modelCapabilities(model: ModelListEntry): string {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
return
|
|
134
|
+
const input = model.input.includes("image") ? "vision" : "text-only";
|
|
135
|
+
const thinking = getSupportedThinkingLevels(model as Model<Api>).join("/");
|
|
136
|
+
return `${input} · thinking: ${thinking}`;
|
|
126
137
|
}
|
|
127
138
|
|
|
128
|
-
/** Build
|
|
129
|
-
*
|
|
130
|
-
* without a configured API key/OAuth session never flood the setup picker. */
|
|
139
|
+
/** Build one searchable list for agent or vision selection. Only models Pi
|
|
140
|
+
* currently reports as available are supplied by setup. */
|
|
131
141
|
export function buildModelPickerItems(options: {
|
|
132
142
|
models: readonly ModelListEntry[];
|
|
133
|
-
availableRefs: readonly string[];
|
|
134
143
|
slot: ModelPickerSlot;
|
|
135
144
|
configuredRef?: string;
|
|
136
145
|
mainRef?: string;
|
|
137
146
|
}): ModelPickerItem[] {
|
|
138
147
|
const configuredRef = cleanModelRef(options.configuredRef);
|
|
139
148
|
const mainRef = cleanModelRef(options.mainRef);
|
|
140
|
-
const available = new Set(options.availableRefs.map((ref) => ref.trim()));
|
|
141
149
|
const byRef = new Map<string, ModelListEntry>();
|
|
142
150
|
for (const model of options.models) {
|
|
143
151
|
const ref = modelRef(model);
|
|
144
|
-
if (
|
|
152
|
+
if (!byRef.has(ref)) byRef.set(ref, model);
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
const refs = [...byRef.keys()]
|
|
@@ -152,26 +160,18 @@ export function buildModelPickerItems(options: {
|
|
|
152
160
|
return leftRank - rightRank || left.localeCompare(right);
|
|
153
161
|
});
|
|
154
162
|
|
|
155
|
-
const dynamic =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
value: CURRENT_MAIN_MODEL,
|
|
163
|
-
label: "Current main model (dynamic)",
|
|
164
|
-
description: options.slot === "vision"
|
|
165
|
-
? "Clear vision override; use the main-window model for vision tasks"
|
|
166
|
-
: "Clear primary override; use the main-window model dynamically",
|
|
167
|
-
};
|
|
168
|
-
|
|
163
|
+
const dynamic: ModelPickerItem = {
|
|
164
|
+
value: CURRENT_MAIN_MODEL,
|
|
165
|
+
label: "Current main model (dynamic)",
|
|
166
|
+
description: options.slot === "vision"
|
|
167
|
+
? "Clear vision override; use the current main model for image tasks"
|
|
168
|
+
: "Clear agent override; use the current main model dynamically",
|
|
169
|
+
};
|
|
169
170
|
const items: ModelPickerItem[] = [dynamic];
|
|
170
171
|
for (const ref of refs) {
|
|
171
172
|
const model = byRef.get(ref)!;
|
|
172
|
-
const tags = ["
|
|
173
|
-
|
|
174
|
-
if (ref === mainRef) tags.push("current main");
|
|
173
|
+
const tags = [ref === configuredRef ? "configured" : "", ref === mainRef ? "current main" : ""]
|
|
174
|
+
.filter(Boolean);
|
|
175
175
|
const name = model.name.trim() && model.name !== model.id ? model.name.trim() : undefined;
|
|
176
176
|
items.push({
|
|
177
177
|
value: ref,
|
|
@@ -182,31 +182,14 @@ export function buildModelPickerItems(options: {
|
|
|
182
182
|
return items;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
/**
|
|
186
|
-
export function
|
|
187
|
-
current:
|
|
185
|
+
/** The dynamic choice removes the persisted per-agent override. */
|
|
186
|
+
export function applyAgentModelChoice(
|
|
187
|
+
current: Record<string, string>,
|
|
188
188
|
agentName: string,
|
|
189
|
-
slot: ModelPoolSlot,
|
|
190
189
|
choice: string,
|
|
191
|
-
):
|
|
192
|
-
const next
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
};
|
|
196
|
-
const target = slot === "primary" ? next.agentModels : next.agentBackupModels;
|
|
197
|
-
if (choice === CURRENT_MAIN_MODEL) delete target[agentName];
|
|
198
|
-
else target[agentName] = choice.trim();
|
|
190
|
+
): Record<string, string> {
|
|
191
|
+
const next = { ...current };
|
|
192
|
+
if (choice === CURRENT_MAIN_MODEL) delete next[agentName];
|
|
193
|
+
else next[agentName] = choice.trim();
|
|
199
194
|
return next;
|
|
200
195
|
}
|
|
201
|
-
|
|
202
|
-
/** Pure rows used by the overview component and focused helper tests. */
|
|
203
|
-
export function buildAgentModelPoolRows(
|
|
204
|
-
agentNames: readonly string[],
|
|
205
|
-
pools: AgentModelPoolMaps,
|
|
206
|
-
): AgentModelPoolRow[] {
|
|
207
|
-
return agentNames.map((name) => ({
|
|
208
|
-
name,
|
|
209
|
-
primary: pools.agentModels[name] ?? "Main (dynamic)",
|
|
210
|
-
backup: pools.agentBackupModels[name] ?? "Main (default)",
|
|
211
|
-
}));
|
|
212
|
-
}
|