@oh-my-pi/pi-coding-agent 16.2.9 → 16.2.12
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/CHANGELOG.md +60 -0
- package/dist/cli.js +3464 -3306
- package/dist/types/cli/bench-cli.d.ts +0 -5
- package/dist/types/cli/dry-balance-cli.d.ts +0 -5
- package/dist/types/cli/models-cli.d.ts +1 -1
- package/dist/types/config/inline-tool-descriptors-mode.d.ts +1 -2
- package/dist/types/config/model-registry.d.ts +1 -23
- package/dist/types/config/model-resolver.d.ts +10 -15
- package/dist/types/config/models-config-schema.d.ts +0 -6
- package/dist/types/config/models-config.d.ts +0 -6
- package/dist/types/config/settings-schema.d.ts +31 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +15 -0
- package/dist/types/extensibility/skills.d.ts +29 -0
- package/dist/types/lsp/client.d.ts +9 -3
- package/dist/types/modes/components/todo-reminder.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +0 -1
- package/dist/types/modes/skill-command.d.ts +1 -1
- package/dist/types/modes/types.d.ts +0 -1
- package/dist/types/stt/asr-client.d.ts +7 -3
- package/dist/types/task/spawn-policy.d.ts +17 -0
- package/dist/types/task/spawn-policy.test.d.ts +1 -0
- package/dist/types/task/types.d.ts +8 -2
- package/dist/types/tools/__tests__/eval-description.test.d.ts +1 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +2 -2
- package/dist/types/tools/browser/registry.d.ts +2 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +4 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +30 -1
- package/dist/types/tools/eval.d.ts +3 -6
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/utils/image-vision-fallback.d.ts +1 -1
- package/package.json +14 -13
- package/scripts/bundle-dist.ts +23 -4
- package/scripts/generate-docs-index.ts +116 -24
- package/src/advisor/__tests__/advisor.test.ts +1 -1
- package/src/async/job-manager.ts +27 -3
- package/src/auto-thinking/classifier.ts +1 -1
- package/src/cli/bench-cli.ts +3 -12
- package/src/cli/dry-balance-cli.ts +1 -6
- package/src/cli/grep-cli.ts +1 -1
- package/src/cli/models-cli.ts +3 -81
- package/src/commands/models.ts +1 -2
- package/src/commit/model-selection.ts +4 -4
- package/src/config/inline-tool-descriptors-mode.ts +1 -2
- package/src/config/model-discovery.ts +150 -82
- package/src/config/model-registry.ts +11 -225
- package/src/config/model-resolver.ts +23 -135
- package/src/config/models-config-schema.ts +0 -22
- package/src/config/prompt-templates.ts +20 -0
- package/src/config/settings-schema.ts +35 -1
- package/src/config/settings.ts +60 -24
- package/src/eval/__tests__/agent-bridge.test.ts +17 -3
- package/src/eval/agent-bridge.ts +7 -9
- package/src/eval/completion-bridge.ts +1 -1
- package/src/eval/py/__tests__/runner-shell-output.test.ts +157 -0
- package/src/eval/py/runner.py +169 -18
- package/src/extensibility/extensions/model-api.ts +1 -3
- package/src/extensibility/extensions/runner.ts +62 -5
- package/src/extensibility/skills.ts +77 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/lsp/client.ts +162 -45
- package/src/lsp/config.ts +17 -3
- package/src/lsp/index.ts +31 -21
- package/src/main.ts +0 -1
- package/src/mcp/transports/http.ts +19 -17
- package/src/mcp/transports/stdio.test.ts +51 -1
- package/src/mcp/transports/stdio.ts +19 -9
- package/src/memories/index.ts +0 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/modes/acp/acp-agent.ts +6 -9
- package/src/modes/components/model-selector.ts +32 -186
- package/src/modes/components/todo-reminder.ts +5 -1
- package/src/modes/controllers/event-controller.ts +1 -16
- package/src/modes/controllers/selector-controller.ts +57 -36
- package/src/modes/interactive-mode.ts +0 -6
- package/src/modes/rpc/rpc-mode.ts +5 -8
- package/src/modes/skill-command.ts +8 -20
- package/src/modes/types.ts +0 -1
- package/src/prompts/system/subagent-system-prompt.md +2 -2
- package/src/prompts/system/tool-call-loop-redirect.md +8 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/sdk.ts +2 -9
- package/src/session/agent-session.ts +89 -8
- package/src/session/session-context.ts +2 -1
- package/src/session/session-manager.ts +2 -1
- package/src/session/unexpected-stop-classifier.ts +1 -1
- package/src/stt/asr-client.ts +87 -27
- package/src/stt/downloader.ts +8 -2
- package/src/task/executor.ts +1 -3
- package/src/task/index.ts +37 -32
- package/src/task/spawn-policy.test.ts +63 -0
- package/src/task/spawn-policy.ts +58 -0
- package/src/task/types.ts +77 -6
- package/src/tools/__tests__/eval-description.test.ts +19 -0
- package/src/tools/ast-grep.ts +34 -12
- package/src/tools/browser/cmux/cmux-tab.ts +66 -24
- package/src/tools/browser/registry.ts +25 -0
- package/src/tools/browser/run-cancellation.ts +47 -0
- package/src/tools/browser/tab-supervisor.ts +120 -7
- package/src/tools/browser/tab-worker.ts +22 -10
- package/src/tools/browser.ts +1 -0
- package/src/tools/eval.ts +15 -10
- package/src/tools/grep.ts +11 -8
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/write.ts +49 -9
- package/src/utils/commit-message-generator.ts +0 -1
- package/src/utils/git.ts +22 -1
- package/src/utils/image-vision-fallback.ts +2 -3
- package/src/utils/title-generator.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import { resolveStdioSpawnCommand } from "./stdio";
|
|
3
|
+
import { resolveStdioSpawnCommand, StdioTransport } from "./stdio";
|
|
4
4
|
|
|
5
5
|
describe("resolveStdioSpawnCommand", () => {
|
|
6
6
|
it("hides Windows executable MCP servers when the host has no console", async () => {
|
|
@@ -43,3 +43,53 @@ describe("resolveStdioSpawnCommand", () => {
|
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
// Regression for #3945: request() awaited stdin.write/flush, so a child that
|
|
48
|
+
// stops draining stdin would park the async fn past the timeout timer and past
|
|
49
|
+
// `return promise`, orphaning the deferred rejection and hanging the caller
|
|
50
|
+
// forever. `sleep` is POSIX-only, so the check is scoped to non-Windows hosts.
|
|
51
|
+
describe.skipIf(process.platform === "win32")("StdioTransport request write stall", () => {
|
|
52
|
+
it("rejects with the timeout error when the child never drains stdin", async () => {
|
|
53
|
+
const timeoutMs = 400;
|
|
54
|
+
const orphaned: Error[] = [];
|
|
55
|
+
const captureOrphan = (reason: unknown) => {
|
|
56
|
+
if (reason instanceof Error) orphaned.push(reason);
|
|
57
|
+
};
|
|
58
|
+
process.on("unhandledRejection", captureOrphan);
|
|
59
|
+
|
|
60
|
+
// `sleep 60` accepts a stdin pipe but never reads it; a 1 MB payload
|
|
61
|
+
// overruns the OS pipe buffer plus any FileSink JS-side buffering, so
|
|
62
|
+
// Bun's write() returns a Promise that only settles if the child reads.
|
|
63
|
+
const transport = new StdioTransport({
|
|
64
|
+
command: "sleep",
|
|
65
|
+
args: ["60"],
|
|
66
|
+
timeout: timeoutMs,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await transport.connect();
|
|
71
|
+
|
|
72
|
+
const bigParam = "x".repeat(1024 * 1024);
|
|
73
|
+
const started = performance.now();
|
|
74
|
+
const outcome = await transport.request("tools/call", { name: "noop", arguments: { blob: bigParam } }).then(
|
|
75
|
+
() => ({ kind: "resolved" as const }),
|
|
76
|
+
(error: unknown) => ({ kind: "rejected" as const, error }),
|
|
77
|
+
);
|
|
78
|
+
const elapsedMs = performance.now() - started;
|
|
79
|
+
|
|
80
|
+
expect(outcome.kind).toBe("rejected");
|
|
81
|
+
if (outcome.kind !== "rejected") return;
|
|
82
|
+
if (!(outcome.error instanceof Error)) {
|
|
83
|
+
throw new Error(`expected Error rejection, got ${String(outcome.error)}`);
|
|
84
|
+
}
|
|
85
|
+
expect(outcome.error.message).toContain(`Request timeout after ${timeoutMs}ms`);
|
|
86
|
+
// Generous ceiling: bare rejection latency plus room for slow CI. The
|
|
87
|
+
// pre-fix behavior was an unbounded hang, not a slightly-late reject.
|
|
88
|
+
expect(elapsedMs).toBeLessThan(timeoutMs + 1500);
|
|
89
|
+
expect(orphaned).toEqual([]);
|
|
90
|
+
} finally {
|
|
91
|
+
process.off("unhandledRejection", captureOrphan);
|
|
92
|
+
await transport.close();
|
|
93
|
+
}
|
|
94
|
+
}, 8000);
|
|
95
|
+
});
|
|
@@ -575,17 +575,27 @@ export class StdioTransport implements MCPTransport {
|
|
|
575
575
|
|
|
576
576
|
const stdin = this.#process.stdin;
|
|
577
577
|
const message = `${JSON.stringify(request)}\n`;
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
// synchronous throw or as a rejected Promise (the EPIPE arrives on a
|
|
581
|
-
// processTicksAndRejections tick). Awaiting funnels both into this catch
|
|
582
|
-
// so the request rejects cleanly instead of leaving a floating rejected
|
|
583
|
-
// promise that crashes the process via the unhandledRejection handler.
|
|
584
|
-
await stdin.write(message);
|
|
585
|
-
await stdin.flush();
|
|
586
|
-
} catch (error: unknown) {
|
|
578
|
+
const failFromSend = (error: unknown) => {
|
|
579
|
+
if (settled) return;
|
|
587
580
|
cleanup();
|
|
588
581
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
582
|
+
};
|
|
583
|
+
try {
|
|
584
|
+
// Never `await` write/flush. Bun's FileSink returns a pending Promise
|
|
585
|
+
// once the OS pipe buffer fills (default ~64 KB on POSIX), and a
|
|
586
|
+
// subprocess that stops draining stdin will park those awaits forever.
|
|
587
|
+
// Awaiting here would keep the async fn stuck above `return promise`,
|
|
588
|
+
// past the timeout timer and the abort handler, orphaning the deferred
|
|
589
|
+
// rejection and hanging the caller (#3945). Route sync throws (Windows
|
|
590
|
+
// EPIPE) and async rejections (POSIX EPIPE on processTicksAndRejections)
|
|
591
|
+
// into `reject()` while leaving the returned promise free to settle
|
|
592
|
+
// from the response, timer, abort signal, or read-loop transport-close.
|
|
593
|
+
const wrote = stdin.write(message);
|
|
594
|
+
if (isThenable(wrote)) wrote.then(undefined, failFromSend);
|
|
595
|
+
const flushed = stdin.flush();
|
|
596
|
+
if (isThenable(flushed)) flushed.then(undefined, failFromSend);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
failFromSend(error);
|
|
589
599
|
}
|
|
590
600
|
|
|
591
601
|
return promise;
|
package/src/memories/index.ts
CHANGED
|
@@ -1219,7 +1219,6 @@ async function resolveMemoryModel(options: {
|
|
|
1219
1219
|
const resolved = resolveModelRoleValue(requestedModel, modelRegistry.getAll(), {
|
|
1220
1220
|
settings: session.settings,
|
|
1221
1221
|
matchPreferences: getModelMatchPreferences(session.settings),
|
|
1222
|
-
modelRegistry,
|
|
1223
1222
|
});
|
|
1224
1223
|
if (resolved.model) return resolved.model;
|
|
1225
1224
|
}
|
package/src/mnemopi/backend.ts
CHANGED
|
@@ -511,7 +511,7 @@ async function resolveMnemopiProviderOptions(
|
|
|
511
511
|
}
|
|
512
512
|
|
|
513
513
|
try {
|
|
514
|
-
const resolved = resolveRoleSelection(["tiny", "smol"], settings, modelRegistry.getAvailable()
|
|
514
|
+
const resolved = resolveRoleSelection(["tiny", "smol"], settings, modelRegistry.getAvailable());
|
|
515
515
|
const model = resolved?.model;
|
|
516
516
|
if (!model) {
|
|
517
517
|
logger.warn("Mnemopi: llmMode=smol but no tiny/smol model resolved; continuing without LLM.");
|
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
} from "../../extensibility/extensions";
|
|
54
54
|
import { runExtensionCompact } from "../../extensibility/extensions/compact-handler";
|
|
55
55
|
import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
|
|
56
|
-
import { buildSkillPromptMessage } from "../../extensibility/skills";
|
|
56
|
+
import { buildSkillPromptMessage, parseSkillInvocation } from "../../extensibility/skills";
|
|
57
57
|
import { loadSlashCommands } from "../../extensibility/slash-commands";
|
|
58
58
|
import { resolveLocalUrlToPath } from "../../internal-urls";
|
|
59
59
|
import { MCPManager } from "../../mcp/manager";
|
|
@@ -831,21 +831,18 @@ export class AcpAgent implements Agent {
|
|
|
831
831
|
}
|
|
832
832
|
|
|
833
833
|
async #tryRunSkillCommand(record: ManagedSessionRecord, text: string): Promise<boolean> {
|
|
834
|
-
if (!
|
|
834
|
+
if (!record.session.skillsSettings?.enableSkillCommands) {
|
|
835
835
|
return false;
|
|
836
836
|
}
|
|
837
|
-
|
|
837
|
+
const parsed = parseSkillInvocation(text);
|
|
838
|
+
if (!parsed) {
|
|
838
839
|
return false;
|
|
839
840
|
}
|
|
840
|
-
const
|
|
841
|
-
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
842
|
-
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
|
843
|
-
const skillName = commandName.slice("skill:".length);
|
|
844
|
-
const skill = record.session.skills.find(candidate => candidate.name === skillName);
|
|
841
|
+
const skill = record.session.skills.find(candidate => candidate.name === parsed.name);
|
|
845
842
|
if (!skill) {
|
|
846
843
|
return false;
|
|
847
844
|
}
|
|
848
|
-
const built = await buildSkillPromptMessage(skill, args);
|
|
845
|
+
const built = await buildSkillPromptMessage(skill, parsed.args);
|
|
849
846
|
await record.session.promptCustomMessage({
|
|
850
847
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
851
848
|
content: built.message,
|
|
@@ -58,21 +58,6 @@ function makeRoleBadgeToken(label: string, color: ThemeColor, assigned: RoleAssi
|
|
|
58
58
|
return `${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
function normalizeSearchText(value: string): string {
|
|
62
|
-
return value
|
|
63
|
-
.toLowerCase()
|
|
64
|
-
.replace(/[^a-z0-9]+/g, " ")
|
|
65
|
-
.trim();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function compactSearchText(value: string): string {
|
|
69
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function getAlphaSearchTokens(query: string): string[] {
|
|
73
|
-
return [...normalizeSearchText(query).matchAll(/[a-z]+/g)].map(match => match[0]).filter(token => token.length > 0);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
61
|
function computeModelRank(model: Model, roles: Record<string, RoleAssignment | undefined>): number {
|
|
77
62
|
let i = 0;
|
|
78
63
|
while (i < MODEL_ROLE_IDS.length) {
|
|
@@ -94,17 +79,6 @@ interface ModelItem {
|
|
|
94
79
|
selector: string;
|
|
95
80
|
}
|
|
96
81
|
|
|
97
|
-
interface CanonicalModelItem {
|
|
98
|
-
kind: "canonical";
|
|
99
|
-
id: string;
|
|
100
|
-
model: Model;
|
|
101
|
-
selector: string;
|
|
102
|
-
variantCount: number;
|
|
103
|
-
searchText: string;
|
|
104
|
-
normalizedSearchText: string;
|
|
105
|
-
compactSearchText: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
82
|
interface ScopedModelItem {
|
|
109
83
|
model: Model;
|
|
110
84
|
thinkingLevel?: string;
|
|
@@ -134,12 +108,8 @@ interface ProviderTabState {
|
|
|
134
108
|
providerId?: string;
|
|
135
109
|
}
|
|
136
110
|
const ALL_TAB = "ALL";
|
|
137
|
-
const CANONICAL_TAB = "CANONICAL";
|
|
138
111
|
|
|
139
|
-
const STATIC_PROVIDER_TABS: ProviderTabState[] = [
|
|
140
|
-
{ id: ALL_TAB, label: ALL_TAB },
|
|
141
|
-
{ id: CANONICAL_TAB, label: CANONICAL_TAB },
|
|
142
|
-
];
|
|
112
|
+
const STATIC_PROVIDER_TABS: ProviderTabState[] = [{ id: ALL_TAB, label: ALL_TAB }];
|
|
143
113
|
|
|
144
114
|
const MODEL_TAB_REFRESH_DEBOUNCE_MS = 120;
|
|
145
115
|
|
|
@@ -168,8 +138,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
168
138
|
#menuContainer: Container;
|
|
169
139
|
#allModels: ModelItem[] = [];
|
|
170
140
|
#filteredModels: ModelItem[] = [];
|
|
171
|
-
#canonicalModels: CanonicalModelItem[] = [];
|
|
172
|
-
#filteredCanonicalModels: CanonicalModelItem[] = [];
|
|
173
141
|
#selectedIndex: number = 0;
|
|
174
142
|
#roles = {} as Record<string, RoleAssignment | undefined>;
|
|
175
143
|
#settings = null as unknown as Settings;
|
|
@@ -297,8 +265,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
297
265
|
// Hydrate synchronously from the current registry snapshot so the first
|
|
298
266
|
// Enter after opening the selector acts on cached models instead of being
|
|
299
267
|
// dropped while the offline refresh promise is still pending. This stays
|
|
300
|
-
// on the open path, so it must remain cheap
|
|
301
|
-
// registry's one-pass getCanonicalModelSelections.
|
|
268
|
+
// on the open path, so it must remain cheap.
|
|
302
269
|
this.#syncFromRegistryState();
|
|
303
270
|
|
|
304
271
|
// Reconcile with cached discovery state in the background. A --models
|
|
@@ -342,7 +309,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
342
309
|
const resolved = resolveModelRoleValue(roleValue, allModels, {
|
|
343
310
|
settings: this.#settings,
|
|
344
311
|
matchPreferences,
|
|
345
|
-
modelRegistry: this.#modelRegistry,
|
|
346
312
|
});
|
|
347
313
|
if (resolved.model) {
|
|
348
314
|
nextRoles[role] = {
|
|
@@ -360,7 +326,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
360
326
|
const resolved = resolveModelRoleValue(`pi/${role}`, candidates, {
|
|
361
327
|
settings: this.#settings,
|
|
362
328
|
matchPreferences,
|
|
363
|
-
modelRegistry: this.#modelRegistry,
|
|
364
329
|
});
|
|
365
330
|
if (!resolved.model) continue;
|
|
366
331
|
nextRoles[role] = {
|
|
@@ -444,30 +409,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
444
409
|
});
|
|
445
410
|
}
|
|
446
411
|
|
|
447
|
-
#sortCanonicalModels(models: CanonicalModelItem[], { skipRoleRank = false }: { skipRoleRank?: boolean } = {}): void {
|
|
448
|
-
const mruOrder = this.#settings.getStorage()?.getModelUsageOrder() ?? [];
|
|
449
|
-
const mruIndex = new Map(mruOrder.map((key, i) => [key, i]));
|
|
450
|
-
|
|
451
|
-
const modelRank = (item: CanonicalModelItem) => computeModelRank(item.model, this.#roles);
|
|
452
|
-
|
|
453
|
-
models.sort((a, b) => {
|
|
454
|
-
if (!skipRoleRank) {
|
|
455
|
-
const aRank = modelRank(a);
|
|
456
|
-
const bRank = modelRank(b);
|
|
457
|
-
if (aRank !== bRank) return aRank - bRank;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const aMru = mruIndex.get(`${a.model.provider}/${a.model.id}`) ?? Number.MAX_SAFE_INTEGER;
|
|
461
|
-
const bMru = mruIndex.get(`${b.model.provider}/${b.model.id}`) ?? Number.MAX_SAFE_INTEGER;
|
|
462
|
-
if (aMru !== bMru) return aMru - bMru;
|
|
463
|
-
|
|
464
|
-
const providerCmp = a.model.provider.localeCompare(b.model.provider);
|
|
465
|
-
if (providerCmp !== 0) return providerCmp;
|
|
466
|
-
|
|
467
|
-
return a.id.localeCompare(b.id);
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
|
|
471
412
|
#loadModelsFromCurrentRegistryState(): void {
|
|
472
413
|
let models: ModelItem[];
|
|
473
414
|
if (this.#scopedModels.length > 0) {
|
|
@@ -498,8 +439,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
498
439
|
} catch (error) {
|
|
499
440
|
this.#allModels = [];
|
|
500
441
|
this.#filteredModels = [];
|
|
501
|
-
this.#canonicalModels = [];
|
|
502
|
-
this.#filteredCanonicalModels = [];
|
|
503
442
|
this.#errorMessage = error instanceof Error ? error.message : String(error);
|
|
504
443
|
return;
|
|
505
444
|
}
|
|
@@ -507,42 +446,14 @@ export class ModelSelectorComponent extends Container {
|
|
|
507
446
|
|
|
508
447
|
const candidates = models.map(item => item.model);
|
|
509
448
|
this.#loadRoleModels(candidates);
|
|
510
|
-
const canonicalSelections = this.#modelRegistry.getCanonicalModelSelections({
|
|
511
|
-
availableOnly: this.#scopedModels.length === 0,
|
|
512
|
-
candidates,
|
|
513
|
-
});
|
|
514
|
-
const canonicalModels = canonicalSelections.map(({ record, model: selectedModel }): CanonicalModelItem => {
|
|
515
|
-
const searchText = [
|
|
516
|
-
record.id,
|
|
517
|
-
record.name,
|
|
518
|
-
selectedModel.provider,
|
|
519
|
-
selectedModel.id,
|
|
520
|
-
selectedModel.name,
|
|
521
|
-
...record.variants.flatMap(variant => [variant.selector, variant.model.name]),
|
|
522
|
-
].join(" ");
|
|
523
|
-
return {
|
|
524
|
-
kind: "canonical",
|
|
525
|
-
id: record.id,
|
|
526
|
-
model: selectedModel,
|
|
527
|
-
selector: record.id,
|
|
528
|
-
variantCount: record.variants.length,
|
|
529
|
-
searchText,
|
|
530
|
-
normalizedSearchText: normalizeSearchText(searchText),
|
|
531
|
-
compactSearchText: compactSearchText(searchText),
|
|
532
|
-
};
|
|
533
|
-
});
|
|
534
449
|
|
|
535
450
|
this.#sortModels(models);
|
|
536
|
-
this.#sortCanonicalModels(canonicalModels);
|
|
537
451
|
|
|
538
452
|
this.#allModels = models;
|
|
539
453
|
this.#filteredModels = models;
|
|
540
|
-
this.#canonicalModels = canonicalModels;
|
|
541
|
-
this.#filteredCanonicalModels = canonicalModels;
|
|
542
|
-
const visibleModels = this.#isCanonicalTab() ? canonicalModels : models;
|
|
543
454
|
this.#selectedIndex = this.#coerceSelectedIndex(
|
|
544
|
-
Math.min(this.#selectedIndex, Math.max(0,
|
|
545
|
-
|
|
455
|
+
Math.min(this.#selectedIndex, Math.max(0, models.length - 1)),
|
|
456
|
+
models,
|
|
546
457
|
);
|
|
547
458
|
}
|
|
548
459
|
|
|
@@ -717,10 +628,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
717
628
|
return this.#getActiveTab().providerId;
|
|
718
629
|
}
|
|
719
630
|
|
|
720
|
-
#isCanonicalTab(): boolean {
|
|
721
|
-
return this.#getActiveTabId() === CANONICAL_TAB;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
631
|
#isModelOverCurrentContext(model: Model): boolean {
|
|
725
632
|
const contextWindow = model.contextWindow ?? 0;
|
|
726
633
|
return this.#currentContextTokens > 0 && contextWindow > 0 && this.#currentContextTokens > contextWindow;
|
|
@@ -734,7 +641,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
734
641
|
return ` ${theme.status.disabled} context>${formatNumber(model.contextWindow ?? 0).toLowerCase()}`;
|
|
735
642
|
}
|
|
736
643
|
|
|
737
|
-
#isItemDisabled(item: ModelItem
|
|
644
|
+
#isItemDisabled(item: ModelItem): boolean {
|
|
738
645
|
return this.#isModelOverContextLimit(item.model);
|
|
739
646
|
}
|
|
740
647
|
|
|
@@ -745,14 +652,11 @@ export class ModelSelectorComponent extends Container {
|
|
|
745
652
|
return this.#formatCurrentContextLimitSuffix(model);
|
|
746
653
|
}
|
|
747
654
|
|
|
748
|
-
#getVisibleItems(): ReadonlyArray<ModelItem
|
|
749
|
-
return this.#
|
|
655
|
+
#getVisibleItems(): ReadonlyArray<ModelItem> {
|
|
656
|
+
return this.#filteredModels;
|
|
750
657
|
}
|
|
751
658
|
|
|
752
|
-
#coerceSelectedIndex(
|
|
753
|
-
index: number,
|
|
754
|
-
visibleItems: ReadonlyArray<ModelItem | CanonicalModelItem> = this.#getVisibleItems(),
|
|
755
|
-
): number {
|
|
659
|
+
#coerceSelectedIndex(index: number, visibleItems: ReadonlyArray<ModelItem> = this.#getVisibleItems()): number {
|
|
756
660
|
const maxIndex = visibleItems.length - 1;
|
|
757
661
|
if (maxIndex < 0) {
|
|
758
662
|
return 0;
|
|
@@ -798,21 +702,16 @@ export class ModelSelectorComponent extends Container {
|
|
|
798
702
|
}
|
|
799
703
|
|
|
800
704
|
#filterModels(query: string): void {
|
|
801
|
-
const activeTabId = this.#getActiveTabId();
|
|
802
705
|
const activeProviderId = this.#getActiveProviderId();
|
|
803
|
-
const isCanonicalTab = activeTabId === CANONICAL_TAB;
|
|
804
706
|
|
|
805
|
-
// Start with all models or filter by provider/canonical view
|
|
806
707
|
let baseModels = this.#allModels;
|
|
807
|
-
const baseCanonicalModels = this.#canonicalModels;
|
|
808
708
|
if (activeProviderId) {
|
|
809
709
|
baseModels = this.#allModels.filter(m => m.provider === activeProviderId);
|
|
810
710
|
}
|
|
811
711
|
|
|
812
|
-
// Apply fuzzy filter if query is present
|
|
813
712
|
if (query.trim()) {
|
|
814
713
|
// If user is searching from a provider tab, auto-switch to ALL to show global provider results.
|
|
815
|
-
if (activeProviderId
|
|
714
|
+
if (activeProviderId) {
|
|
816
715
|
this.#activeTabIndex = 0;
|
|
817
716
|
if (this.#tabBar && this.#tabBar.getActiveIndex() !== 0) {
|
|
818
717
|
this.#tabBar.setActiveIndex(0);
|
|
@@ -822,55 +721,22 @@ export class ModelSelectorComponent extends Container {
|
|
|
822
721
|
baseModels = this.#allModels;
|
|
823
722
|
}
|
|
824
723
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
const substringFiltered =
|
|
835
|
-
compactQuery.length === 0
|
|
836
|
-
? alphaFiltered
|
|
837
|
-
: alphaFiltered.filter(item => item.compactSearchText.includes(compactQuery));
|
|
838
|
-
const fuzzySource =
|
|
839
|
-
substringFiltered.length > 0
|
|
840
|
-
? substringFiltered
|
|
841
|
-
: alphaFiltered.length > 0
|
|
842
|
-
? alphaFiltered
|
|
843
|
-
: baseCanonicalModels;
|
|
844
|
-
// Fuzzy provides the candidate set, but `${provider}/${id}` scoring
|
|
845
|
-
// is biased by provider-prefix length (e.g. `openai/X` beats
|
|
846
|
-
// `openai-codex/X` purely because the prefix is shorter). Re-sort by
|
|
847
|
-
// affinity — MRU then version — so the user's actually-used model
|
|
848
|
-
// wins. Role rank is skipped: when narrowing by query, a weakly
|
|
849
|
-
// matching default model should not be promoted above a stronger
|
|
850
|
-
// non-default match.
|
|
851
|
-
const fuzzyMatches = fuzzyFilter(fuzzySource, query, ({ searchText }) => searchText);
|
|
852
|
-
this.#sortCanonicalModels(fuzzyMatches, { skipRoleRank: true });
|
|
853
|
-
this.#filteredCanonicalModels = fuzzyMatches;
|
|
854
|
-
} else {
|
|
855
|
-
// Match against the displayed "provider/id" string so the user can
|
|
856
|
-
// type what they see: bare names (`mimo`, `kimi`), provider prefixes
|
|
857
|
-
// (`openrouter`), or scoped queries (`openrouter/mimo`) all flow
|
|
858
|
-
// through the same fuzzy matcher. The score is biased by provider-
|
|
859
|
-
// prefix length, so re-sort by MRU/version afterwards; skip role
|
|
860
|
-
// rank so a weakly matching default doesn't trump a stronger match.
|
|
861
|
-
const fuzzyMatches = fuzzyFilter(baseModels, query, ({ id, provider }) => `${provider}/${id}`);
|
|
862
|
-
this.#sortModels(fuzzyMatches, { skipRoleRank: true });
|
|
863
|
-
this.#filteredModels = fuzzyMatches;
|
|
864
|
-
}
|
|
724
|
+
// Match against the displayed "provider/id" string so the user can
|
|
725
|
+
// type what they see: bare names (`mimo`, `kimi`), provider prefixes
|
|
726
|
+
// (`openrouter`), or scoped queries (`openrouter/mimo`) all flow
|
|
727
|
+
// through the same fuzzy matcher. The score is biased by provider-
|
|
728
|
+
// prefix length, so re-sort by MRU/version afterwards; skip role
|
|
729
|
+
// rank so a weakly matching default doesn't trump a stronger match.
|
|
730
|
+
const fuzzyMatches = fuzzyFilter(baseModels, query, ({ id, provider }) => `${provider}/${id}`);
|
|
731
|
+
this.#sortModels(fuzzyMatches, { skipRoleRank: true });
|
|
732
|
+
this.#filteredModels = fuzzyMatches;
|
|
865
733
|
} else {
|
|
866
734
|
this.#filteredModels = baseModels;
|
|
867
|
-
this.#filteredCanonicalModels = baseCanonicalModels;
|
|
868
735
|
}
|
|
869
736
|
|
|
870
|
-
const visibleItems = isCanonicalTab ? this.#filteredCanonicalModels : this.#filteredModels;
|
|
871
737
|
this.#selectedIndex = this.#coerceSelectedIndex(
|
|
872
|
-
Math.min(this.#selectedIndex, Math.max(0,
|
|
873
|
-
|
|
738
|
+
Math.min(this.#selectedIndex, Math.max(0, this.#filteredModels.length - 1)),
|
|
739
|
+
this.#filteredModels,
|
|
874
740
|
);
|
|
875
741
|
this.#updateList();
|
|
876
742
|
}
|
|
@@ -940,8 +806,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
940
806
|
|
|
941
807
|
#updateList(): void {
|
|
942
808
|
this.#listContainer.clear();
|
|
943
|
-
const
|
|
944
|
-
const visibleItems = isCanonicalTab ? this.#filteredCanonicalModels : this.#filteredModels;
|
|
809
|
+
const visibleItems = this.#filteredModels;
|
|
945
810
|
|
|
946
811
|
const maxVisible = 10;
|
|
947
812
|
const startIndex = Math.max(
|
|
@@ -959,8 +824,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
959
824
|
for (let i = startIndex; i < endIndex; i++) {
|
|
960
825
|
const item = visibleItems[i];
|
|
961
826
|
if (!item) continue;
|
|
962
|
-
const canonicalItem = isCanonicalTab ? (item as CanonicalModelItem) : undefined;
|
|
963
|
-
const providerItem = isCanonicalTab ? undefined : (item as ModelItem);
|
|
964
827
|
|
|
965
828
|
const isSelected = i === this.#selectedIndex;
|
|
966
829
|
const isDisabled = this.#isItemDisabled(item);
|
|
@@ -988,25 +851,17 @@ export class ModelSelectorComponent extends Container {
|
|
|
988
851
|
let line = "";
|
|
989
852
|
if (isSelected) {
|
|
990
853
|
const prefix = theme.fg("accent", `${theme.nav.cursor} `);
|
|
991
|
-
if (
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
line = `${prefix}${theme.fg("accent", item.id)}${variants}${backing}${badgeText}${disabledSuffix}`;
|
|
995
|
-
} else if (showProvider) {
|
|
996
|
-
const providerPrefix = theme.fg("dim", `${providerItem?.provider ?? ""}/`);
|
|
997
|
-
line = `${prefix}${providerPrefix}${theme.fg("accent", providerItem?.id ?? item.id)}${badgeText}${disabledSuffix}`;
|
|
854
|
+
if (showProvider) {
|
|
855
|
+
const providerPrefix = theme.fg("dim", `${item.provider}/`);
|
|
856
|
+
line = `${prefix}${providerPrefix}${theme.fg("accent", item.id)}${badgeText}${disabledSuffix}`;
|
|
998
857
|
} else {
|
|
999
858
|
line = `${prefix}${theme.fg("accent", item.id)}${badgeText}${disabledSuffix}`;
|
|
1000
859
|
}
|
|
1001
860
|
} else {
|
|
1002
861
|
const prefix = " ";
|
|
1003
|
-
if (
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
line = `${prefix}${item.id}${variants}${backing}${badgeText}${disabledSuffix}`;
|
|
1007
|
-
} else if (showProvider) {
|
|
1008
|
-
const providerPrefix = theme.fg("dim", `${providerItem?.provider ?? ""}/`);
|
|
1009
|
-
line = `${prefix}${providerPrefix}${providerItem?.id ?? item.id}${badgeText}${disabledSuffix}`;
|
|
862
|
+
if (showProvider) {
|
|
863
|
+
const providerPrefix = theme.fg("dim", `${item.provider}/`);
|
|
864
|
+
line = `${prefix}${providerPrefix}${item.id}${badgeText}${disabledSuffix}`;
|
|
1010
865
|
} else {
|
|
1011
866
|
line = `${prefix}${item.id}${badgeText}${disabledSuffix}`;
|
|
1012
867
|
}
|
|
@@ -1044,9 +899,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
1044
899
|
return;
|
|
1045
900
|
}
|
|
1046
901
|
this.#listContainer.addChild(new Spacer(1));
|
|
1047
|
-
const suffix = isCanonicalTab
|
|
1048
|
-
? ` (${selected.model.provider}/${selected.model.id}, ${(selected as CanonicalModelItem).variantCount} variants)`
|
|
1049
|
-
: "";
|
|
1050
902
|
const limitWarning = this.#isItemDisabled(selected)
|
|
1051
903
|
? theme.fg(
|
|
1052
904
|
"dim",
|
|
@@ -1054,7 +906,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1054
906
|
)
|
|
1055
907
|
: "";
|
|
1056
908
|
this.#listContainer.addChild(
|
|
1057
|
-
new Text(theme.fg("muted", ` Model Name: ${selected.model.name}
|
|
909
|
+
new Text(theme.fg("muted", ` Model Name: ${selected.model.name}`) + limitWarning, 0, 0),
|
|
1058
910
|
);
|
|
1059
911
|
}
|
|
1060
912
|
}
|
|
@@ -1086,10 +938,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
1086
938
|
return foundIndex >= 0 ? foundIndex : 0;
|
|
1087
939
|
}
|
|
1088
940
|
|
|
1089
|
-
#getSelectedItem(): ModelItem |
|
|
1090
|
-
return this.#
|
|
1091
|
-
? this.#filteredCanonicalModels[this.#selectedIndex]
|
|
1092
|
-
: this.#filteredModels[this.#selectedIndex];
|
|
941
|
+
#getSelectedItem(): ModelItem | undefined {
|
|
942
|
+
return this.#filteredModels[this.#selectedIndex];
|
|
1093
943
|
}
|
|
1094
944
|
|
|
1095
945
|
#coerceMenuSelectedIndex(index: number): number {
|
|
@@ -1100,7 +950,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1100
950
|
return Math.max(0, Math.min(index, maxIndex));
|
|
1101
951
|
}
|
|
1102
952
|
|
|
1103
|
-
#moveMenuSelection(delta: number, _selectedItem: ModelItem
|
|
953
|
+
#moveMenuSelection(delta: number, _selectedItem: ModelItem, optionCount: number): void {
|
|
1104
954
|
this.#menuSelectedIndex = (this.#menuSelectedIndex + delta + optionCount) % optionCount;
|
|
1105
955
|
this.#updateMenu();
|
|
1106
956
|
}
|
|
@@ -1375,11 +1225,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1375
1225
|
}
|
|
1376
1226
|
}
|
|
1377
1227
|
|
|
1378
|
-
#handleSelect(
|
|
1379
|
-
item: ModelItem | CanonicalModelItem,
|
|
1380
|
-
role: string | null,
|
|
1381
|
-
thinkingLevel?: ConfiguredThinkingLevel,
|
|
1382
|
-
): void {
|
|
1228
|
+
#handleSelect(item: ModelItem, role: string | null, thinkingLevel?: ConfiguredThinkingLevel): void {
|
|
1383
1229
|
if (this.#isItemDisabled(item)) {
|
|
1384
1230
|
return;
|
|
1385
1231
|
}
|
|
@@ -3,7 +3,9 @@ import { theme } from "../../modes/theme/theme";
|
|
|
3
3
|
import type { TodoItem } from "../../tools/todo";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Component that renders a todo completion reminder notification
|
|
6
|
+
* Component that renders a todo completion reminder notification, committed into
|
|
7
|
+
* the transcript like a TTSR notification so it stays anchored in history rather
|
|
8
|
+
* than floating above the editor.
|
|
7
9
|
* Shows when the agent stops with incomplete todos.
|
|
8
10
|
*/
|
|
9
11
|
export class TodoReminderComponent extends Container {
|
|
@@ -16,6 +18,8 @@ export class TodoReminderComponent extends Container {
|
|
|
16
18
|
) {
|
|
17
19
|
super();
|
|
18
20
|
|
|
21
|
+
this.addChild(new Spacer(1));
|
|
22
|
+
|
|
19
23
|
this.#box = new Box(1, 1, t => theme.inverse(theme.fg("warning", t)));
|
|
20
24
|
this.#box.setIgnoreTight(true);
|
|
21
25
|
this.addChild(this.#box);
|
|
@@ -609,15 +609,6 @@ export class EventController {
|
|
|
609
609
|
}
|
|
610
610
|
for (const content of this.ctx.streamingMessage.content) {
|
|
611
611
|
if (content.type !== "toolCall") continue;
|
|
612
|
-
// Anthropic/OpenAI open a streamed tool block with an empty id (and
|
|
613
|
-
// `{}` args) before the id/arguments arrive; Gemini assembles the
|
|
614
|
-
// whole call first, so it never hits this. Keying `pendingTools` by
|
|
615
|
-
// "" would create a placeholder card, and the later real-id frame —
|
|
616
|
-
// `pendingTools.has(realId)` false — would create a SECOND card,
|
|
617
|
-
// orphaning the blank one (no `tool_execution_*` event ever carries
|
|
618
|
-
// "", so it is never matched, updated, or removed). Defer until the
|
|
619
|
-
// provider assigns the real id.
|
|
620
|
-
if (!content.id) continue;
|
|
621
612
|
if (content.name === "read") {
|
|
622
613
|
if (!readArgsHaveTarget(content.arguments)) {
|
|
623
614
|
// Args still streaming — defer until path is parseable so we can route to the
|
|
@@ -978,13 +969,9 @@ export class EventController {
|
|
|
978
969
|
}
|
|
979
970
|
// Update todo display when todo tool completes
|
|
980
971
|
if (event.toolName === "todo" && !event.isError) {
|
|
981
|
-
const hadTodoReminder = (this.ctx.todoReminderContainer?.children.length ?? 0) > 0;
|
|
982
|
-
this.ctx.todoReminderContainer?.clear();
|
|
983
972
|
const details = event.result.details as { phases?: TodoPhase[] } | undefined;
|
|
984
973
|
if (details?.phases) {
|
|
985
974
|
this.ctx.setTodos(details.phases);
|
|
986
|
-
} else if (hadTodoReminder) {
|
|
987
|
-
this.ctx.ui.requestRender();
|
|
988
975
|
}
|
|
989
976
|
} else if (event.toolName === "todo" && event.isError) {
|
|
990
977
|
const textContent = event.result.content.find(
|
|
@@ -1281,9 +1268,7 @@ export class EventController {
|
|
|
1281
1268
|
|
|
1282
1269
|
async #handleTodoReminder(event: Extract<AgentSessionEvent, { type: "todo_reminder" }>): Promise<void> {
|
|
1283
1270
|
const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts);
|
|
1284
|
-
this.ctx.
|
|
1285
|
-
this.ctx.todoReminderContainer.addChild(component);
|
|
1286
|
-
this.ctx.ui.requestRender();
|
|
1271
|
+
this.ctx.present(component);
|
|
1287
1272
|
}
|
|
1288
1273
|
|
|
1289
1274
|
async #handleTodoAutoClear(_event: Extract<AgentSessionEvent, { type: "todo_auto_clear" }>): Promise<void> {
|