@oh-my-pi/pi-coding-agent 16.2.11 → 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 +31 -0
- package/dist/cli.js +3005 -2848
- 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/lsp/client.d.ts +9 -3
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +0 -5
- 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 +12 -12
- package/src/advisor/__tests__/advisor.test.ts +1 -1
- 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/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 +32 -6
- 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/lsp/client.ts +162 -45
- 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/components/model-selector.ts +32 -186
- package/src/modes/controllers/event-controller.ts +8 -39
- package/src/modes/controllers/selector-controller.ts +0 -1
- package/src/modes/controllers/tool-args-reveal.ts +0 -12
- 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/task/index.ts +23 -27
- 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/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/inspect-image.ts +1 -1
- package/src/tools/write.ts +49 -9
- package/src/utils/commit-message-generator.ts +0 -1
- package/src/utils/image-vision-fallback.ts +2 -3
- package/src/utils/title-generator.ts +1 -1
|
@@ -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.");
|
|
@@ -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
|
}
|
|
@@ -72,11 +72,6 @@ export class EventController {
|
|
|
72
72
|
// emits one read per completion — does not break it, so a run of consecutive
|
|
73
73
|
// reads collapses into one group even across completion boundaries.
|
|
74
74
|
#lastVisibleBlockCount = 0;
|
|
75
|
-
// Bumped on each assistant message_start. Scopes the placeholder key for a
|
|
76
|
-
// not-yet-id'd streamed tool block (see the toolCall loop) so a sealed,
|
|
77
|
-
// undeleted pending entry from an aborted message can never be reused by the
|
|
78
|
-
// next message's same content index.
|
|
79
|
-
#streamTurnNonce = 0;
|
|
80
75
|
#renderedCustomMessages = new Set<string>();
|
|
81
76
|
#lastIntent: string | undefined = undefined;
|
|
82
77
|
#backgroundToolCallIds = new Set<string>();
|
|
@@ -406,7 +401,6 @@ export class EventController {
|
|
|
406
401
|
this.ctx.ui.requestRender();
|
|
407
402
|
} else if (event.message.role === "assistant") {
|
|
408
403
|
this.#lastVisibleBlockCount = 0;
|
|
409
|
-
this.#streamTurnNonce++;
|
|
410
404
|
this.ctx.streamingComponent = createAssistantMessageComponent(this.ctx);
|
|
411
405
|
this.ctx.streamingMessage = event.message;
|
|
412
406
|
this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
|
|
@@ -613,14 +607,9 @@ export class EventController {
|
|
|
613
607
|
if (this.ctx.streamingMessage.content.some(content => content.type === "toolCall")) {
|
|
614
608
|
this.ctx.streamingComponent.markTranscriptBlockFinalized();
|
|
615
609
|
}
|
|
616
|
-
for (const
|
|
610
|
+
for (const content of this.ctx.streamingMessage.content) {
|
|
617
611
|
if (content.type !== "toolCall") continue;
|
|
618
612
|
if (content.name === "read") {
|
|
619
|
-
// Read groups key by the real id (one group spans several reads),
|
|
620
|
-
// and the owned-dialect parser delivers id + args together when
|
|
621
|
-
// the call closes, so a parseable target without an id never
|
|
622
|
-
// occurs here. Defer if it somehow does.
|
|
623
|
-
if (!content.id) continue;
|
|
624
613
|
if (!readArgsHaveTarget(content.arguments)) {
|
|
625
614
|
// Args still streaming — defer until path is parseable so we can route to the
|
|
626
615
|
// read group (regular files) vs ToolExecutionComponent (internal URLs).
|
|
@@ -643,26 +632,6 @@ export class EventController {
|
|
|
643
632
|
// Internal URL read falls through to ToolExecutionComponent below.
|
|
644
633
|
}
|
|
645
634
|
|
|
646
|
-
// The owned-dialect tool parser (text-based tool calls for OAuth
|
|
647
|
-
// Anthropic / OpenAI) appends a tool block when it detects the call
|
|
648
|
-
// opening and only fills `id` + arguments once the call's text
|
|
649
|
-
// closes — so the live preview must stream while `content.id` is "".
|
|
650
|
-
// Key the preview by stable content position until the id lands,
|
|
651
|
-
// then migrate the pending entry + reveal state onto the real id
|
|
652
|
-
// (so `tool_execution_*`, which always carries the real id, matches
|
|
653
|
-
// the same component instead of orphaning a blank card). Native
|
|
654
|
-
// structured tool calls — every Gemini call, and Anthropic/OpenAI
|
|
655
|
-
// function calls — carry the id from the first frame, so
|
|
656
|
-
// `pendingKey === content.id` throughout and the migration no-ops.
|
|
657
|
-
const streamKey = `\u0000stream:${this.#streamTurnNonce}:${contentIndex}`;
|
|
658
|
-
const pendingKey = content.id || streamKey;
|
|
659
|
-
if (content.id && this.ctx.pendingTools.has(streamKey)) {
|
|
660
|
-
const migrated = this.ctx.pendingTools.get(streamKey);
|
|
661
|
-
this.ctx.pendingTools.delete(streamKey);
|
|
662
|
-
if (migrated) this.ctx.pendingTools.set(pendingKey, migrated);
|
|
663
|
-
this.#toolArgsReveal.rekey(streamKey, pendingKey);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
635
|
// Preserve the raw partial JSON only for renderers that need to surface fields before the JSON object closes.
|
|
667
636
|
// Bash uses this to show inline env assignments during streaming instead of popping them in at completion.
|
|
668
637
|
// While the JSON is still open, ToolArgsRevealController paces the
|
|
@@ -674,16 +643,16 @@ export class EventController {
|
|
|
674
643
|
const rawInput = content.customWireName !== undefined;
|
|
675
644
|
const tool = this.ctx.viewSession.getToolByName(content.name);
|
|
676
645
|
if (partialJson) {
|
|
677
|
-
renderArgs = this.#toolArgsReveal.setTarget(
|
|
646
|
+
renderArgs = this.#toolArgsReveal.setTarget(content.id, partialJson, {
|
|
678
647
|
rawInput,
|
|
679
648
|
exposeRawPartialJson: exposesRawPartialJson(content.name, rawInput, tool),
|
|
680
649
|
fullArgs: content.arguments,
|
|
681
650
|
});
|
|
682
651
|
} else {
|
|
683
|
-
this.#toolArgsReveal.finish(
|
|
652
|
+
this.#toolArgsReveal.finish(content.id);
|
|
684
653
|
renderArgs = content.arguments;
|
|
685
654
|
}
|
|
686
|
-
if (!this.ctx.pendingTools.has(
|
|
655
|
+
if (!this.ctx.pendingTools.has(content.id)) {
|
|
687
656
|
this.#resolveDisplaceablePoll(content.name);
|
|
688
657
|
this.#resetReadGroup();
|
|
689
658
|
const component = new ToolExecutionComponent(
|
|
@@ -702,13 +671,13 @@ export class EventController {
|
|
|
702
671
|
);
|
|
703
672
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
704
673
|
this.ctx.chatContainer.addChild(component);
|
|
705
|
-
this.ctx.pendingTools.set(
|
|
706
|
-
this.#toolArgsReveal.bind(
|
|
674
|
+
this.ctx.pendingTools.set(content.id, component);
|
|
675
|
+
this.#toolArgsReveal.bind(content.id, component);
|
|
707
676
|
} else {
|
|
708
|
-
const component = this.ctx.pendingTools.get(
|
|
677
|
+
const component = this.ctx.pendingTools.get(content.id);
|
|
709
678
|
if (component) {
|
|
710
679
|
component.updateArgs(renderArgs, content.id);
|
|
711
|
-
this.#toolArgsReveal.bind(
|
|
680
|
+
this.#toolArgsReveal.bind(content.id, component);
|
|
712
681
|
}
|
|
713
682
|
}
|
|
714
683
|
}
|
|
@@ -248,7 +248,6 @@ export class SelectorController {
|
|
|
248
248
|
const advisorRoleSel = resolveAdvisorRoleSelection(
|
|
249
249
|
this.ctx.settings,
|
|
250
250
|
this.ctx.session.modelRegistry.getAvailable(),
|
|
251
|
-
this.ctx.session.modelRegistry,
|
|
252
251
|
);
|
|
253
252
|
const defaultAdvisorModel = advisorRoleSel?.model;
|
|
254
253
|
const deps: AdvisorConfigDeps = {
|
|
@@ -177,18 +177,6 @@ export class ToolArgsRevealController {
|
|
|
177
177
|
if (entry) entry.component = component;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
/** Migrate a live reveal entry from a placeholder key onto the real
|
|
181
|
-
* tool-call id once the owned-dialect parser assigns it. No-op when no
|
|
182
|
-
* entry exists under `from` (smoothing disabled, or the JSON already
|
|
183
|
-
* closed and `finish` cleared it). */
|
|
184
|
-
rekey(from: string, to: string): void {
|
|
185
|
-
if (from === to) return;
|
|
186
|
-
const entry = this.#entries.get(from);
|
|
187
|
-
if (!entry) return;
|
|
188
|
-
this.#entries.delete(from);
|
|
189
|
-
this.#entries.set(to, entry);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
180
|
/** Final arguments arrived (the JSON closed): drop the reveal so the
|
|
193
181
|
* caller's final-args render wins immediately, mirroring how assistant
|
|
194
182
|
* text snaps to the full message at message_end. */
|
|
@@ -62,9 +62,9 @@ Yield protocol:
|
|
|
62
62
|
This is your only way to return a final result. For structured results, you NEVER put JSON in plain text or substitute a text summary for `result.data`.
|
|
63
63
|
|
|
64
64
|
{{#if outputSchema}}
|
|
65
|
-
Your
|
|
65
|
+
Your terminal `yield` MUST use exactly this shape — the schema fields go inside `result.data`, NEVER at the top level and NEVER as a stringified summary:
|
|
66
66
|
```ts
|
|
67
|
-
{{
|
|
67
|
+
{{renderYieldSchema outputSchema}}
|
|
68
68
|
```
|
|
69
69
|
{{/if}}
|
|
70
70
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<system-interrupt reason="tool_call_loop_detected">
|
|
2
|
+
You called `{{tool_name}}` {{count}} consecutive times with identical arguments:
|
|
3
|
+
`{{arguments_summary}}`
|
|
4
|
+
|
|
5
|
+
Last result (truncated): `{{result_summary}}`
|
|
6
|
+
|
|
7
|
+
NEVER call `{{tool_name}}` with those arguments again this turn. Use different arguments, choose another tool, or summarize findings and yield if complete.
|
|
8
|
+
</system-interrupt>
|
|
@@ -39,8 +39,8 @@ tool.<name>(args) → unknown
|
|
|
39
39
|
Invoke any session tool; `args` = its parameter object.
|
|
40
40
|
completion(prompt, model?="default", system?=None, schema?=None) → str | dict
|
|
41
41
|
Oneshot, stateless (no history/tools). `model`: "smol" fast | "default" session | "slow" most capable. `schema` (JSON-Schema) → structured output, parsed object.
|
|
42
|
-
{{#if spawns}}agent(prompt, agent?="
|
|
43
|
-
Run a subagent → final output. `agent` picks another discovered agent; `schema` as in completion(). Background via `local://` files named in the prompt. `handle` → DAG node dict { text, output, handle: "agent://<id>", id, agent } (parsed under `data` when `schema` set).
|
|
42
|
+
{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, label?=None, schema?=None, handle?=False) → str | dict
|
|
43
|
+
Run a subagent → final output. `agent` picks another discovered agent; omit it to use `{{spawnDefaultAgent}}`.{{#if spawnAllowedAgentsText}} Allowed agents: {{spawnAllowedAgentsText}}.{{/if}} `schema` as in completion(). Background via `local://` files named in the prompt. `handle` → DAG node dict { text, output, handle: "agent://<id>", id, agent } (parsed under `data` when `schema` set).
|
|
44
44
|
{{#if js}} JS: options are ONE trailing object — agent(prompt, { agent, schema, handle }).
|
|
45
45
|
{{/if}}
|
|
46
46
|
{{/if}}
|
|
@@ -10,7 +10,7 @@ Execution blocks your turn: the call only returns once the work is completely fi
|
|
|
10
10
|
- **One-pass agents:** Prefer agents that investigate **and** edit in a single pass; only spin a read-only discovery step (e.g. `explore`) when the affected files are genuinely unknown.
|
|
11
11
|
|
|
12
12
|
# Inputs
|
|
13
|
-
- `agent` (optional): The base agent type to use (e.g., `explore`, `reviewer`). Defaults to `
|
|
13
|
+
- `agent` (optional): The base agent type to use (e.g., `explore`, `reviewer`). Defaults to `{{defaultAgent}}`{{#if defaultAgentIsGeneric}} (the general-purpose worker){{/if}} — omit it for the default worker instead of passing `agent: "{{defaultAgent}}"`.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
|
|
14
14
|
{{#if batchEnabled}}
|
|
15
15
|
- `context`: Shared project state, constraints, and contracts. Applies to the entire batch; do not duplicate this background into individual tasks.
|
|
16
16
|
- `tasks[]`: Array of subagents to spawn.
|