@oh-my-pi/pi-coding-agent 15.10.1 → 15.10.2
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 +67 -0
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/main.d.ts +3 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/controllers/event-controller.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +5 -2
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/tools/eval.d.ts +8 -0
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/yield.d.ts +8 -0
- package/package.json +9 -9
- package/src/cli/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +2 -4
- package/src/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -0
- package/src/commit/model-selection.ts +3 -2
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +4 -22
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings.ts +86 -41
- package/src/debug/index.ts +8 -0
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/eval/__tests__/llm-bridge.test.ts +20 -0
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/llm-bridge.ts +14 -3
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +61 -9
- package/src/main.ts +91 -65
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +2 -2
- package/src/modes/components/custom-editor.ts +143 -111
- package/src/modes/components/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- package/src/modes/components/status-line.ts +19 -4
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/user-message.ts +1 -1
- package/src/modes/controllers/event-controller.ts +26 -0
- package/src/modes/controllers/input-controller.ts +46 -7
- package/src/modes/interactive-mode.ts +107 -20
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/types.ts +3 -0
- package/src/modes/workflow.ts +10 -10
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +2 -1
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +26 -9
- package/src/session/agent-session.ts +37 -12
- package/src/session/auth-storage.ts +2 -0
- package/src/session/session-manager.ts +96 -23
- package/src/task/executor.ts +71 -36
- package/src/task/render.ts +3 -4
- package/src/tools/bash.ts +7 -0
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/eval.ts +13 -2
- package/src/tools/find.ts +7 -0
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/github-cache.ts +25 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/path-utils.ts +28 -2
- package/src/tools/plan-mode-guard.ts +52 -7
- package/src/tools/read.ts +25 -12
- package/src/tools/search.ts +38 -3
- package/src/tools/write.ts +2 -2
- package/src/tools/yield.ts +10 -1
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/enhanced-paste.ts +30 -2
- package/src/web/search/providers/codex.ts +37 -8
|
@@ -26,6 +26,16 @@ type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
|
26
26
|
|
|
27
27
|
const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Loader label shown the instant a user interrupt (Esc) is requested, kept until
|
|
31
|
+
* the agent turn fully unwinds. Esc fires the abort synchronously, but the loop
|
|
32
|
+
* only stops the spinner at `agent_end`, which it cannot reach until every
|
|
33
|
+
* in-flight tool settles its abort in `executeToolCalls` (`Promise.allSettled`).
|
|
34
|
+
* Swapping the steady "Working…" for this acknowledges the keypress instead of
|
|
35
|
+
* reading as an ignored Esc for the seconds a slow tool takes to tear down.
|
|
36
|
+
*/
|
|
37
|
+
export const INTERRUPTING_WORKING_MESSAGE = "Interrupting…";
|
|
38
|
+
|
|
29
39
|
// Events that change foreground streaming state, or that reset a turn. The TUI
|
|
30
40
|
// eager native-scrollback rebuild mode is recomputed only on these so unrelated
|
|
31
41
|
// IRC/notices/status refreshes do not toggle scrollback replay policy.
|
|
@@ -57,6 +67,7 @@ export class EventController {
|
|
|
57
67
|
#backgroundToolCallIds = new Set<string>();
|
|
58
68
|
#assistantMessageStreaming = false;
|
|
59
69
|
#agentTurnActive = false;
|
|
70
|
+
#interrupting = false;
|
|
60
71
|
#readToolCallArgs = new Map<string, Record<string, unknown>>();
|
|
61
72
|
#readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
62
73
|
#lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
|
|
@@ -167,6 +178,7 @@ export class EventController {
|
|
|
167
178
|
return true;
|
|
168
179
|
}
|
|
169
180
|
#updateWorkingMessageFromIntent(intent: unknown): void {
|
|
181
|
+
if (this.#interrupting) return;
|
|
170
182
|
// Streamed JSON can deliver non-string `_i` (object, number, boolean) before
|
|
171
183
|
// schema validation; `?.` only guards null/undefined, so guard the type too.
|
|
172
184
|
if (typeof intent !== "string") return;
|
|
@@ -176,6 +188,19 @@ export class EventController {
|
|
|
176
188
|
this.ctx.setWorkingMessage(`${trimmed}${interruptHint()}`);
|
|
177
189
|
}
|
|
178
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Acknowledge a user interrupt (Esc) immediately: switch the loader to
|
|
193
|
+
* `INTERRUPTING_WORKING_MESSAGE` and freeze intent-driven working-message
|
|
194
|
+
* updates for the rest of the turn so a late `tool_execution_start` intent
|
|
195
|
+
* cannot repaint a "Working…/<intent>" line over the acknowledgment. Reset at
|
|
196
|
+
* the next `agent_start`. No-op outside an active turn or if already set.
|
|
197
|
+
*/
|
|
198
|
+
notifyInterrupting(): void {
|
|
199
|
+
if (!this.#agentTurnActive || this.#interrupting) return;
|
|
200
|
+
this.#interrupting = true;
|
|
201
|
+
this.ctx.setWorkingMessage(INTERRUPTING_WORKING_MESSAGE);
|
|
202
|
+
}
|
|
203
|
+
|
|
179
204
|
subscribeToAgent(): void {
|
|
180
205
|
this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
|
|
181
206
|
await this.handleEvent(event);
|
|
@@ -220,6 +245,7 @@ export class EventController {
|
|
|
220
245
|
|
|
221
246
|
async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
|
|
222
247
|
this.#agentTurnActive = true;
|
|
248
|
+
this.#interrupting = false;
|
|
223
249
|
this.#lastIntent = undefined;
|
|
224
250
|
this.#readToolCallArgs.clear();
|
|
225
251
|
this.#readToolCallAssistantComponents.clear();
|
|
@@ -94,6 +94,7 @@ export class InputController {
|
|
|
94
94
|
if (this.ctx.loopModeEnabled) {
|
|
95
95
|
this.ctx.pauseLoop();
|
|
96
96
|
if (this.ctx.session.isStreaming) {
|
|
97
|
+
this.ctx.notifyInterrupting();
|
|
97
98
|
void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
98
99
|
} else {
|
|
99
100
|
this.ctx.cancelPendingSubmission();
|
|
@@ -124,6 +125,7 @@ export class InputController {
|
|
|
124
125
|
this.ctx.isPythonMode = false;
|
|
125
126
|
this.ctx.updateEditorBorderColor();
|
|
126
127
|
} else if (this.ctx.session.isStreaming) {
|
|
128
|
+
this.ctx.notifyInterrupting();
|
|
127
129
|
void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
128
130
|
} else if (!this.ctx.editor.getText().trim()) {
|
|
129
131
|
// Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
|
|
@@ -499,17 +501,44 @@ export class InputController {
|
|
|
499
501
|
}
|
|
500
502
|
|
|
501
503
|
handleCtrlZ(): void {
|
|
502
|
-
//
|
|
503
|
-
process.
|
|
504
|
+
// SIGTSTP is POSIX job-control: Windows has no equivalent and
|
|
505
|
+
// `process.kill(_, "SIGTSTP")` throws `TypeError: Unknown signal:
|
|
506
|
+
// SIGTSTP` there, taking the whole agent down via an uncaught
|
|
507
|
+
// exception (issue #2036). No-op on platforms that cannot suspend.
|
|
508
|
+
if (process.platform === "win32") {
|
|
509
|
+
this.ctx.showStatus("Suspend (Ctrl+Z) is not supported on this platform");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Capture the listener so we can detach it if the signal never
|
|
514
|
+
// fires; otherwise a failed suspend would leave a stale SIGCONT
|
|
515
|
+
// handler that fires on the next unrelated continue and tries to
|
|
516
|
+
// re-`start()` an already-running TUI.
|
|
517
|
+
const onResume = (): void => {
|
|
504
518
|
this.ctx.ui.start();
|
|
505
519
|
this.ctx.ui.requestRender(true);
|
|
506
|
-
}
|
|
520
|
+
};
|
|
521
|
+
process.once("SIGCONT", onResume);
|
|
507
522
|
|
|
508
|
-
// Stop the TUI (restore terminal to normal mode)
|
|
523
|
+
// Stop the TUI (restore terminal to normal mode) before sending the
|
|
524
|
+
// signal so the parent shell sees a sane terminal state.
|
|
509
525
|
this.ctx.ui.stop();
|
|
510
526
|
|
|
511
|
-
|
|
512
|
-
|
|
527
|
+
try {
|
|
528
|
+
// pid=0 → entire foreground process group; the shell receives
|
|
529
|
+
// SIGTSTP and parks the job.
|
|
530
|
+
process.kill(0, "SIGTSTP");
|
|
531
|
+
} catch (err) {
|
|
532
|
+
// Either the runtime refused the signal or the kernel rejected
|
|
533
|
+
// it (some sandboxes block sending to pid=0). Tear the resume
|
|
534
|
+
// hook down and bring the TUI back so the user is not stranded
|
|
535
|
+
// on a frozen prompt.
|
|
536
|
+
process.removeListener("SIGCONT", onResume);
|
|
537
|
+
this.ctx.ui.start();
|
|
538
|
+
this.ctx.ui.requestRender(true);
|
|
539
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
540
|
+
this.ctx.showError(`Failed to suspend: ${reason}`);
|
|
541
|
+
}
|
|
513
542
|
}
|
|
514
543
|
|
|
515
544
|
handleDequeue(): void {
|
|
@@ -589,7 +618,7 @@ export class InputController {
|
|
|
589
618
|
|
|
590
619
|
/** Send editor text as a follow-up message (queued behind current stream). */
|
|
591
620
|
async handleFollowUp(): Promise<void> {
|
|
592
|
-
|
|
621
|
+
let text = this.ctx.editor.getText().trim();
|
|
593
622
|
if (!text) return;
|
|
594
623
|
|
|
595
624
|
// Compaction first: while compacting, free text gets queued via
|
|
@@ -603,6 +632,16 @@ export class InputController {
|
|
|
603
632
|
return;
|
|
604
633
|
}
|
|
605
634
|
|
|
635
|
+
const slashResult = await executeBuiltinSlashCommand(text, {
|
|
636
|
+
ctx: this.ctx,
|
|
637
|
+
});
|
|
638
|
+
if (slashResult === true) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (typeof slashResult === "string") {
|
|
642
|
+
text = slashResult;
|
|
643
|
+
}
|
|
644
|
+
|
|
606
645
|
// Skill commands invoke through the custom-message path regardless of
|
|
607
646
|
// which keybinding submitted them. Enter routes them as `steer`;
|
|
608
647
|
// Ctrl+Enter (this handler) routes them as `followUp`.
|
|
@@ -50,7 +50,7 @@ import chalk from "chalk";
|
|
|
50
50
|
import { reset as resetCapabilities } from "../capability";
|
|
51
51
|
import { KeybindingsManager } from "../config/keybindings";
|
|
52
52
|
import { MODEL_ROLES, type ModelRole } from "../config/model-registry";
|
|
53
|
-
import { isSettingsInitialized, Settings, settings } from "../config/settings";
|
|
53
|
+
import { isSettingsInitialized, onStatusLineSessionAccentChanged, Settings, settings } from "../config/settings";
|
|
54
54
|
import { clearClaudePluginRootsCache } from "../discovery/helpers";
|
|
55
55
|
import type {
|
|
56
56
|
ContextUsage,
|
|
@@ -125,7 +125,7 @@ import {
|
|
|
125
125
|
import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
126
126
|
import { SessionObserverRegistry } from "./session-observer-registry";
|
|
127
127
|
import { interruptHint } from "./shared";
|
|
128
|
-
import { type ShimmerPalette, shimmerSegments, shimmerText } from "./theme/shimmer";
|
|
128
|
+
import { type ShimmerPalette, shimmerEnabled, shimmerSegments, shimmerText } from "./theme/shimmer";
|
|
129
129
|
import type { Theme } from "./theme/theme";
|
|
130
130
|
import {
|
|
131
131
|
getEditorTheme,
|
|
@@ -157,6 +157,12 @@ interface WorkingMessageAccent {
|
|
|
157
157
|
dim: string;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
interface WorkingMessageAccentCacheKey {
|
|
161
|
+
sessionName: string | undefined;
|
|
162
|
+
accentSurfaceLuminance: number | undefined;
|
|
163
|
+
sessionAccentEnabled: boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
160
166
|
function renderWorkingMessage(message: string, accent?: WorkingMessageAccent): string {
|
|
161
167
|
const palette = accent
|
|
162
168
|
? ({
|
|
@@ -301,6 +307,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
301
307
|
autoCompactionLoader: Loader | undefined = undefined;
|
|
302
308
|
retryLoader: Loader | undefined = undefined;
|
|
303
309
|
#pendingWorkingMessage: string | undefined;
|
|
310
|
+
#workingMessageAccentCacheKey?: WorkingMessageAccentCacheKey;
|
|
311
|
+
#workingMessageAccentCacheValue?: WorkingMessageAccent;
|
|
312
|
+
#workingMessageAccentCacheHasValue = false;
|
|
304
313
|
get #defaultWorkingMessage(): string {
|
|
305
314
|
return `Working…${interruptHint()}`;
|
|
306
315
|
}
|
|
@@ -638,9 +647,17 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
638
647
|
this.session.subscribe(event => {
|
|
639
648
|
void this.#handleGoalSessionEvent(event);
|
|
640
649
|
}),
|
|
650
|
+
this.sessionManager.onSessionNameChanged(() => {
|
|
651
|
+
this.#handleSessionAccentInputsChanged();
|
|
652
|
+
}),
|
|
653
|
+
onStatusLineSessionAccentChanged(() => {
|
|
654
|
+
this.#syncStatusLineSettings();
|
|
655
|
+
this.#handleSessionAccentInputsChanged();
|
|
656
|
+
}),
|
|
641
657
|
);
|
|
642
658
|
// Set up theme file watcher
|
|
643
659
|
onThemeChange(() => {
|
|
660
|
+
this.#clearWorkingMessageAccentCache();
|
|
644
661
|
clearRenderCache();
|
|
645
662
|
this.ui.invalidate();
|
|
646
663
|
this.updateEditorBorderColor();
|
|
@@ -965,9 +982,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
965
982
|
this.#goalContinuationTurnInFlight = false;
|
|
966
983
|
}
|
|
967
984
|
if (this.loadingAnimation) {
|
|
968
|
-
this
|
|
969
|
-
this.loadingAnimation = undefined;
|
|
970
|
-
this.statusContainer.clear();
|
|
985
|
+
this.#stopLoadingAnimation(true);
|
|
971
986
|
}
|
|
972
987
|
if (!submission.customType) {
|
|
973
988
|
this.pendingImages = submission.images ? [...submission.images] : [];
|
|
@@ -1005,9 +1020,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1005
1020
|
pendingSubmissionDispose?.();
|
|
1006
1021
|
this.#pendingWorkingMessage = undefined;
|
|
1007
1022
|
if (this.loadingAnimation) {
|
|
1008
|
-
this
|
|
1009
|
-
this.loadingAnimation = undefined;
|
|
1010
|
-
this.statusContainer.clear();
|
|
1023
|
+
this.#stopLoadingAnimation(true);
|
|
1011
1024
|
}
|
|
1012
1025
|
}
|
|
1013
1026
|
}
|
|
@@ -1023,6 +1036,24 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1023
1036
|
this.editor.setMaxHeight(this.#computeEditorMaxHeight());
|
|
1024
1037
|
}
|
|
1025
1038
|
|
|
1039
|
+
#syncStatusLineSettings(): void {
|
|
1040
|
+
this.statusLine.updateSettings({
|
|
1041
|
+
preset: settings.get("statusLine.preset"),
|
|
1042
|
+
leftSegments: settings.get("statusLine.leftSegments"),
|
|
1043
|
+
rightSegments: settings.get("statusLine.rightSegments"),
|
|
1044
|
+
separator: settings.get("statusLine.separator"),
|
|
1045
|
+
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
1046
|
+
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
1047
|
+
segmentOptions: settings.get("statusLine.segmentOptions"),
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
#handleSessionAccentInputsChanged(): void {
|
|
1052
|
+
this.#clearWorkingMessageAccentCache();
|
|
1053
|
+
this.statusLine.invalidate();
|
|
1054
|
+
this.updateEditorBorderColor();
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1026
1057
|
updateEditorBorderColor(): void {
|
|
1027
1058
|
if (this.isBashMode) {
|
|
1028
1059
|
this.editor.borderColor = theme.getBashModeBorderColor();
|
|
@@ -2416,8 +2447,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2416
2447
|
|
|
2417
2448
|
stop(): void {
|
|
2418
2449
|
if (this.loadingAnimation) {
|
|
2419
|
-
this
|
|
2420
|
-
this.loadingAnimation = undefined;
|
|
2450
|
+
this.#stopLoadingAnimation(false);
|
|
2421
2451
|
}
|
|
2422
2452
|
this.#cleanupMicAnimation();
|
|
2423
2453
|
this.#cancelTodoAutoClearTimer();
|
|
@@ -2581,9 +2611,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2581
2611
|
this.#pendingSubmissionDispose = undefined;
|
|
2582
2612
|
this.#pendingWorkingMessage = undefined;
|
|
2583
2613
|
if (this.loadingAnimation) {
|
|
2584
|
-
this
|
|
2585
|
-
this.loadingAnimation = undefined;
|
|
2586
|
-
this.statusContainer.clear();
|
|
2614
|
+
this.#stopLoadingAnimation(true);
|
|
2587
2615
|
}
|
|
2588
2616
|
this.#uiHelpers.showError(message);
|
|
2589
2617
|
}
|
|
@@ -2646,24 +2674,69 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2646
2674
|
this.ui.requestRender();
|
|
2647
2675
|
}
|
|
2648
2676
|
|
|
2677
|
+
#clearWorkingMessageAccentCache(): void {
|
|
2678
|
+
this.#workingMessageAccentCacheKey = undefined;
|
|
2679
|
+
this.#workingMessageAccentCacheValue = undefined;
|
|
2680
|
+
this.#workingMessageAccentCacheHasValue = false;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
#buildWorkingMessageAccentCacheKey(): WorkingMessageAccentCacheKey {
|
|
2684
|
+
const sessionAccentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
|
|
2685
|
+
return {
|
|
2686
|
+
sessionAccentEnabled,
|
|
2687
|
+
sessionName: sessionAccentEnabled ? this.sessionManager.getSessionName() : undefined,
|
|
2688
|
+
accentSurfaceLuminance: theme.accentSurfaceLuminance,
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
#workingMessageAccentCacheKeyEquals(a: WorkingMessageAccentCacheKey, b: WorkingMessageAccentCacheKey): boolean {
|
|
2693
|
+
return (
|
|
2694
|
+
a.sessionName === b.sessionName &&
|
|
2695
|
+
a.accentSurfaceLuminance === b.accentSurfaceLuminance &&
|
|
2696
|
+
a.sessionAccentEnabled === b.sessionAccentEnabled
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
#cacheWorkingMessageAccent(
|
|
2701
|
+
key: WorkingMessageAccentCacheKey,
|
|
2702
|
+
value: WorkingMessageAccent | undefined,
|
|
2703
|
+
): WorkingMessageAccent | undefined {
|
|
2704
|
+
this.#workingMessageAccentCacheKey = key;
|
|
2705
|
+
this.#workingMessageAccentCacheValue = value;
|
|
2706
|
+
this.#workingMessageAccentCacheHasValue = true;
|
|
2707
|
+
return value;
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2649
2710
|
#getWorkingMessageAccent(): WorkingMessageAccent | undefined {
|
|
2650
|
-
const
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2711
|
+
const key = this.#buildWorkingMessageAccentCacheKey();
|
|
2712
|
+
if (
|
|
2713
|
+
this.#workingMessageAccentCacheHasValue &&
|
|
2714
|
+
this.#workingMessageAccentCacheKey &&
|
|
2715
|
+
this.#workingMessageAccentCacheKeyEquals(key, this.#workingMessageAccentCacheKey)
|
|
2716
|
+
) {
|
|
2717
|
+
return this.#workingMessageAccentCacheValue;
|
|
2718
|
+
}
|
|
2719
|
+
if (!key.sessionAccentEnabled || !key.sessionName) {
|
|
2720
|
+
return this.#cacheWorkingMessageAccent(key, undefined);
|
|
2721
|
+
}
|
|
2722
|
+
const hex = getSessionAccentHex(key.sessionName, key.accentSurfaceLuminance);
|
|
2654
2723
|
const main = getSessionAccentAnsi(hex);
|
|
2655
2724
|
const dim = getSessionAccentAnsi(adjustHsv(hex, { s: 0.55, v: 0.65 }));
|
|
2656
|
-
return main && dim ? { main, dim } : undefined;
|
|
2725
|
+
return this.#cacheWorkingMessageAccent(key, main && dim ? { main, dim } : undefined);
|
|
2657
2726
|
}
|
|
2658
2727
|
|
|
2659
2728
|
ensureLoadingAnimation(): void {
|
|
2660
2729
|
if (!this.loadingAnimation) {
|
|
2730
|
+
this.#clearWorkingMessageAccentCache();
|
|
2661
2731
|
this.statusContainer.clear();
|
|
2662
2732
|
const messageColorFn = ((message: string) =>
|
|
2663
2733
|
renderWorkingMessage(message, this.#getWorkingMessageAccent())) as LoaderMessageColorFn & {
|
|
2664
|
-
animated
|
|
2734
|
+
animated?: true;
|
|
2665
2735
|
};
|
|
2666
|
-
|
|
2736
|
+
// Shimmer drives the 30fps redraw; when it is disabled the working
|
|
2737
|
+
// message is static, so leave `animated` unset and let the loader use
|
|
2738
|
+
// the spinner-only ~12.5fps cadence instead of repainting a frozen line.
|
|
2739
|
+
if (shimmerEnabled()) messageColorFn.animated = true;
|
|
2667
2740
|
this.loadingAnimation = new Loader(
|
|
2668
2741
|
this.ui,
|
|
2669
2742
|
spinner => {
|
|
@@ -2680,6 +2753,16 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2680
2753
|
this.applyPendingWorkingMessage();
|
|
2681
2754
|
}
|
|
2682
2755
|
|
|
2756
|
+
#stopLoadingAnimation(clearStatusContainer: boolean): void {
|
|
2757
|
+
if (!this.loadingAnimation) return;
|
|
2758
|
+
this.loadingAnimation.stop();
|
|
2759
|
+
this.loadingAnimation = undefined;
|
|
2760
|
+
this.#clearWorkingMessageAccentCache();
|
|
2761
|
+
if (clearStatusContainer) {
|
|
2762
|
+
this.statusContainer.clear();
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2683
2766
|
setWorkingMessage(message?: string): void {
|
|
2684
2767
|
if (message === undefined) {
|
|
2685
2768
|
this.#pendingWorkingMessage = undefined;
|
|
@@ -2707,6 +2790,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2707
2790
|
this.setWorkingMessage(message);
|
|
2708
2791
|
}
|
|
2709
2792
|
|
|
2793
|
+
notifyInterrupting(): void {
|
|
2794
|
+
this.#eventController.notifyInterrupting();
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2710
2797
|
showNewVersionNotification(newVersion: string): void {
|
|
2711
2798
|
this.#uiHelpers.showNewVersionNotification(newVersion);
|
|
2712
2799
|
}
|
|
@@ -4,7 +4,7 @@ import { highlightWorkflow } from "./workflow";
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Gradient-highlight every magic keyword ("ultrathink", "orchestrate",
|
|
7
|
-
* "
|
|
7
|
+
* "workflowz") that appears as standalone prose, skipping any occurrence inside a
|
|
8
8
|
* code block, inline code span, or XML/HTML section. Each highlighter paints its
|
|
9
9
|
* own keyword with its own gradient, so chaining is order-independent — the
|
|
10
10
|
* earlier passes only inject zero-width SGR escapes (no backticks or angle
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Markdown structure awareness for the magic-keyword affordances
|
|
3
|
-
* ("ultrathink"/"orchestrate"/"
|
|
3
|
+
* ("ultrathink"/"orchestrate"/"workflowz").
|
|
4
4
|
*
|
|
5
5
|
* Keyword detection and editor/transcript highlighting must fire only on prose
|
|
6
6
|
* the user is actually addressing to the model — never on a word that happens to
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
2
2
|
import type { Theme, ThemeColor } from "./theme";
|
|
3
3
|
|
|
4
|
+
// ─── Animation velocity ──────────────────────────────────────────────────────
|
|
5
|
+
// Band/head travel speed in border cells per second. Driving position by a fixed
|
|
6
|
+
// velocity — instead of dividing a fixed sweep duration by the (length-derived)
|
|
7
|
+
// period — makes smoothness independent of message length: at the loader's
|
|
8
|
+
// default 30fps redraw cadence the band advances ≤1 cell per frame for any
|
|
9
|
+
// string, so it never visibly steps. Sweep/round-trip durations now scale with
|
|
10
|
+
// length. Keep ≤ the animated redraw fps (loader RENDER_INTERVAL_MS = 1000/30).
|
|
11
|
+
const SHIMMER_SPEED_CELLS_PER_S = 30;
|
|
12
|
+
|
|
4
13
|
// ─── Classic sweep tunables ──────────────────────────────────────────────────
|
|
5
14
|
const CLASSIC_PADDING = 10;
|
|
6
|
-
const CLASSIC_SWEEP_MS = 1400;
|
|
7
15
|
const CLASSIC_BAND_HALF_WIDTH = 6;
|
|
8
16
|
|
|
9
17
|
// ─── KITT scanner tunables ───────────────────────────────────────────────────
|
|
10
|
-
// 1.5s round trip ≈ classic 1982 K.I.T.T. scanner cadence (~0.75s per direction).
|
|
11
|
-
const KITT_CYCLE_MS = 1500;
|
|
12
18
|
const KITT_HEAD_HALF = 0.6;
|
|
13
19
|
const KITT_TRAIL_LEN = 7;
|
|
14
20
|
|
|
@@ -103,9 +109,10 @@ function compile(theme: ShimmerTheme, palette: ShimmerPalette): CompiledPalette
|
|
|
103
109
|
/** Smooth cosine bump sweeping left → right with edge padding. */
|
|
104
110
|
function classicIntensity(time: number, index: number, length: number): number {
|
|
105
111
|
const period = length + CLASSIC_PADDING * 2;
|
|
106
|
-
//
|
|
107
|
-
// frame
|
|
108
|
-
|
|
112
|
+
// Fixed-velocity, un-floored band position: advancing at a constant
|
|
113
|
+
// cells/second (not period / fixed-sweep) keeps the per-frame step ≤1 cell at
|
|
114
|
+
// the default cadence for any length, so long messages are no steppier.
|
|
115
|
+
const pos = ((time / 1000) * SHIMMER_SPEED_CELLS_PER_S) % period;
|
|
109
116
|
const dist = Math.abs(index + CLASSIC_PADDING - pos);
|
|
110
117
|
if (dist >= CLASSIC_BAND_HALF_WIDTH) return 0;
|
|
111
118
|
return 0.5 * (1 + Math.cos((Math.PI * dist) / CLASSIC_BAND_HALF_WIDTH));
|
|
@@ -119,9 +126,13 @@ function classicIntensity(time: number, index: number, length: number): number {
|
|
|
119
126
|
function kittIntensity(time: number, index: number, length: number): number {
|
|
120
127
|
const range = length - 1;
|
|
121
128
|
if (range <= 0) return 1;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
// Fixed head velocity: a triangle ping-pong over a 2*range round trip at a
|
|
130
|
+
// constant cells/second, so the bright head advances ≤1 cell per frame at the
|
|
131
|
+
// default cadence regardless of bar length. Round-trip duration scales with length.
|
|
132
|
+
const cycleCells = 2 * range;
|
|
133
|
+
const sweep = ((time / 1000) * SHIMMER_SPEED_CELLS_PER_S) % cycleCells;
|
|
134
|
+
const goingRight = sweep < range;
|
|
135
|
+
const head = goingRight ? sweep : cycleCells - sweep;
|
|
125
136
|
const delta = index - head;
|
|
126
137
|
const abs = delta < 0 ? -delta : delta;
|
|
127
138
|
if (abs <= KITT_HEAD_HALF) return 1;
|
package/src/modes/types.ts
CHANGED
|
@@ -182,6 +182,9 @@ export interface InteractiveModeContext {
|
|
|
182
182
|
flushPendingModelSwitch(): Promise<void>;
|
|
183
183
|
setWorkingMessage(message?: string): void;
|
|
184
184
|
applyPendingWorkingMessage(): void;
|
|
185
|
+
/** Acknowledge a user interrupt (Esc) by switching the loader to an
|
|
186
|
+
* "Interrupting…" label until the agent turn unwinds. */
|
|
187
|
+
notifyInterrupting(): void;
|
|
185
188
|
ensureLoadingAnimation(): void;
|
|
186
189
|
startPendingSubmission(input: {
|
|
187
190
|
text: string;
|
package/src/modes/workflow.ts
CHANGED
|
@@ -3,25 +3,25 @@ import { createGradientHighlighter, type KeywordHighlighter } from "./gradient-h
|
|
|
3
3
|
import { keywordInProse } from "./markdown-prose";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* "
|
|
6
|
+
* "workflowz" keyword support.
|
|
7
7
|
*
|
|
8
8
|
* Typing the standalone word in the input editor paints it with a warm
|
|
9
9
|
* amber→green gradient ({@link highlightWorkflow}); submitting a message that
|
|
10
10
|
* mentions it appends a hidden {@link WORKFLOW_NOTICE} that steers the model to
|
|
11
11
|
* author a deterministic multi-subagent workflow in eval cells (agent/parallel/
|
|
12
12
|
* pipeline). Matching is whitespace-delimited and case-sensitive (lowercase
|
|
13
|
-
* only) — "
|
|
14
|
-
* "
|
|
13
|
+
* only) — "workflowz" triggers, but "workflowzed", "Workflowz", and
|
|
14
|
+
* "workflowz.ts" never do.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
// Detection: lowercase keyword
|
|
18
|
-
const WORKFLOW_WORD = /(?<!\S)
|
|
17
|
+
// Detection: lowercase keyword flanked by whitespace or a string edge. Non-global so `.test` stays stateless.
|
|
18
|
+
const WORKFLOW_WORD = /(?<!\S)workflowz(?!\S)/;
|
|
19
19
|
|
|
20
|
-
/** Hidden system notice appended after a user message that mentions "
|
|
20
|
+
/** Hidden system notice appended after a user message that mentions "workflowz". */
|
|
21
21
|
export const WORKFLOW_NOTICE: string = workflowNotice.trim();
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Whether `text` contains the standalone keyword "
|
|
24
|
+
* Whether `text` contains the standalone keyword "workflowz"
|
|
25
25
|
* (lowercase, whitespace-delimited) in prose — never inside a code block, inline
|
|
26
26
|
* code span, or XML/HTML section.
|
|
27
27
|
*/
|
|
@@ -30,13 +30,13 @@ export function containsWorkflow(text: string): boolean {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Highlight every standalone "
|
|
33
|
+
* Highlight every standalone "workflowz" in `text` for editor display
|
|
34
34
|
* with a warm amber→green gradient (hue 30..150), visually distinct from
|
|
35
35
|
* ultrathink's rainbow and orchestrate's teal→violet.
|
|
36
36
|
*/
|
|
37
37
|
export const highlightWorkflow: KeywordHighlighter = createGradientHighlighter({
|
|
38
|
-
probe: /
|
|
39
|
-
highlight: /(?<!\S)
|
|
38
|
+
probe: /workflowz/,
|
|
39
|
+
highlight: /(?<!\S)workflowz(?!\S)/g,
|
|
40
40
|
stops: 14,
|
|
41
41
|
hue: t => 30 + t * 120,
|
|
42
42
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<system-notice>
|
|
2
|
-
The user's message above contains the **
|
|
2
|
+
The user's message above contains the **workflowz** keyword: drive this task as a deterministic multi-subagent workflow. Author the orchestration as Python in the `eval` tool and fan out subagents — to be comprehensive (decompose and cover in parallel), to be confident (independent perspectives and adversarial checks before you commit), or to take on scale one context can't hold (audits, migrations, broad sweeps). This overrides any default tendency to do the whole task inline when fanning out would be more thorough.
|
|
3
3
|
|
|
4
4
|
<when>
|
|
5
5
|
Worth it when the task benefits from decomposition + parallel coverage, or from independent/adversarial cross-checking before you commit. For a quick lookup or single edit, just do it directly — don't spin up agents. Scout inline FIRST (list the files, scope the diff, find the call sites) to discover the work-list, then fan out over it — you don't need to know the shape before the *task*, only before the *fan-out*. Common shapes, each a well-scoped `eval` call you can chain across turns:
|
|
@@ -31,6 +31,15 @@ Executes bash command in shell session for terminal operations like git, bun, ca
|
|
|
31
31
|
- `async: true` only defers **reporting** of the result — it does NOT disable, extend, or detach the timeout. A daemon started with `async: true` is still killed when `timeout` elapses, regardless of how long the agent waits before reading the result.
|
|
32
32
|
- For long-running daemons (dev servers, watchers): either pass an explicit large `timeout` (up to `3600`), or fully detach the process from this shell using `nohup … &` / `setsid … &` / `disown` so it survives independent of the bash call's lifecycle.
|
|
33
33
|
{{/if}}
|
|
34
|
+
{{#if autoBackgroundEnabled}}
|
|
35
|
+
|
|
36
|
+
## Auto-background
|
|
37
|
+
|
|
38
|
+
- A foreground (non-`async`) call that has not completed within **{{autoBackgroundThresholdSeconds}}s** is automatically converted into a background job and returns a `Background job <id> started: …` notice with the buffered output so far. The command keeps running; the final result is delivered as a follow-up tool call when it completes.
|
|
39
|
+
- This is NOT a failure or a re-queue. Treat the notice as "still running, will report back" — do not retry the same command, and do not wait synchronously for it.
|
|
40
|
+
- Auto-backgrounding does NOT extend `timeout`: the job is still killed at the original deadline.
|
|
41
|
+
- If you need the result inline (e.g. piping into another command), raise `timeout` above the expected duration so it finishes before the threshold matters{{#if asyncEnabled}}, or set `async: true` up front so the contract is explicit{{/if}}.
|
|
42
|
+
{{/if}}
|
|
34
43
|
|
|
35
44
|
# Output minimizer
|
|
36
45
|
|
|
@@ -26,7 +26,7 @@ Drives real Chromium tab; full puppeteer access via JS execution.
|
|
|
26
26
|
- `tab.waitForResponse(pattern, { timeout? })` — pattern substring, `RegExp`, or `(response) => boolean`. Returns raw puppeteer `HTTPResponse` (call `.text()` / `.json()` / `.status()` / `.headers()` on it).
|
|
27
27
|
- `tab.evaluate(fn, …args)` — sugar for `page.evaluate` with abort signal already wired. Use this instead of dropping to `page.evaluate` for ad-hoc DOM reads.
|
|
28
28
|
- `tab.screenshot({ selector?, fullPage?, save?, silent? })` — captures screenshot and **auto-attaches to tool output for you to view** (unless `silent: true`). `save` is **strictly optional**: OMIT when you just want to look at page — downscaled image shown regardless, full-res capture written to temp file automatically. Pass `save` (a path) ONLY when deliberately need to keep full-res copy on disk for later use; `browser.screenshotDir` does same for every shot. NEVER invent `save` path for throwaway/temporal screenshot.
|
|
29
|
-
- `tab.extract(format = "markdown")` — Readability-extracted page content.
|
|
29
|
+
- `tab.extract(format = "markdown")` — returns Readability-extracted page content as a string (`"markdown"` or `"text"`). Throws if the page yields no readable content.
|
|
30
30
|
- Selectors accept CSS plus puppeteer query handlers: `aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`. Playwright-style `p-aria/[name="…"]`, `p-text/…` normalized.
|
|
31
31
|
- Default `tab.observe()` over `tab.screenshot()` for page state. Screenshot only when visual appearance matters.
|
|
32
32
|
</instruction>
|
|
@@ -46,8 +46,9 @@ tool.<name>(args) → unknown
|
|
|
46
46
|
Invoke any session tool by name. `args` is the tool's parameter object.
|
|
47
47
|
llm(prompt, model?="default", system?=None, schema?=None) → str | dict
|
|
48
48
|
Oneshot, stateless LLM call (no history, no tools). `model` picks a tier: "smol" (fast), "default" (this session's model), "slow" (most capable). Pass `system` for a system prompt. Pass a JSON-Schema `schema` to force structured output and get the parsed object back; otherwise returns the completion text.
|
|
49
|
-
agent(prompt, agent_type?="task", model?=None, context?=None, label?=None, schema?=None) → str | dict
|
|
49
|
+
{{#if spawns}}agent(prompt, agent_type?="task", model?=None, context?=None, label?=None, schema?=None) → str | dict
|
|
50
50
|
Run a subagent and return its final output. Defaults to the bundled "task" agent; pass `agent_type`/`agentType` for another discovered agent. Pass a JSON-Schema `schema` to force structured output and get the parsed object back.
|
|
51
|
+
{{/if}}
|
|
51
52
|
parallel(thunks) → list
|
|
52
53
|
Run thunks (callables) through a bounded pool, preserving input order. The pool is as wide as a `task` tool batch (tracks the `task.maxConcurrency` setting), so fan out as wide as the work divides — don't pre-shrink it. Barrier: returns once all finish; a thunk that throws propagates.
|
|
53
54
|
pipeline(items, ...stages) → list
|
|
@@ -18,8 +18,8 @@ Append `:<sel>` to `path`. The bare path falls back to the default mode.
|
|
|
18
18
|
- `:50` / `:50-` — read from line 50 onward.
|
|
19
19
|
- `:50-200` — lines 50–200 inclusive.
|
|
20
20
|
- `:50+150` — 150 lines starting at line 50.
|
|
21
|
-
- `:20+1` —
|
|
22
|
-
- `:5-16,960-973` — multiple ranges in one call (sorted, overlaps merged).
|
|
21
|
+
- `:20+1` — anchor on line 20 (single-range reads expand by ≤1 leading and ≤3 trailing context lines).
|
|
22
|
+
- `:5-16,960-973` — multiple ranges in one call (sorted, overlaps merged). Multi-range mode returns exact bounds with no context padding.
|
|
23
23
|
- `:raw` — verbatim text; no anchors, no summary, no line prefixes.
|
|
24
24
|
- `:2-4:raw` or `:raw:2-4` — range AND verbatim; the two compose in either order.
|
|
25
25
|
- `:conflicts` — one-line-per-block index of every unresolved git merge conflict.
|
package/src/sdk.ts
CHANGED
|
@@ -41,7 +41,9 @@ import { createApiKeyResolver } from "./config/api-key-resolver";
|
|
|
41
41
|
import { shouldEnableAppendOnlyContext } from "./config/append-only-context-mode";
|
|
42
42
|
import { ModelRegistry } from "./config/model-registry";
|
|
43
43
|
import {
|
|
44
|
+
defaultModelPerProvider,
|
|
44
45
|
formatModelString,
|
|
46
|
+
getModelMatchPreferences,
|
|
45
47
|
parseModelPattern,
|
|
46
48
|
parseModelString,
|
|
47
49
|
resolveAllowedModels,
|
|
@@ -709,6 +711,7 @@ function customToolToDefinition(tool: CustomTool): ToolDefinition {
|
|
|
709
711
|
parameters: tool.parameters,
|
|
710
712
|
hidden: tool.hidden,
|
|
711
713
|
deferrable: tool.deferrable,
|
|
714
|
+
approval: typeof tool.approval === "function" ? tool.approval.bind(tool) : tool.approval,
|
|
712
715
|
mcpServerName: tool.mcpServerName,
|
|
713
716
|
mcpToolName: tool.mcpToolName,
|
|
714
717
|
execute: (toolCallId, params, signal, onUpdate, ctx) =>
|
|
@@ -1030,9 +1033,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1030
1033
|
const hasServiceTierEntry = existingBranch.some(entry => entry.type === "service_tier_change");
|
|
1031
1034
|
|
|
1032
1035
|
const hasExplicitModel = options.model !== undefined || options.modelPattern !== undefined;
|
|
1033
|
-
const modelMatchPreferences =
|
|
1034
|
-
usageOrder: settings.getStorage()?.getModelUsageOrder(),
|
|
1035
|
-
};
|
|
1036
|
+
const modelMatchPreferences = getModelMatchPreferences(settings);
|
|
1036
1037
|
const allowedModels = await logger.time("resolveAllowedModels", () =>
|
|
1037
1038
|
resolveAllowedModels(modelRegistry, settings, modelMatchPreferences),
|
|
1038
1039
|
);
|
|
@@ -1553,9 +1554,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1553
1554
|
// Resolve deferred --model pattern now that extension models are registered.
|
|
1554
1555
|
if (!model && options.modelPattern) {
|
|
1555
1556
|
const availableModels = modelRegistry.getAll();
|
|
1556
|
-
const matchPreferences =
|
|
1557
|
-
usageOrder: settings.getStorage()?.getModelUsageOrder(),
|
|
1558
|
-
};
|
|
1557
|
+
const matchPreferences = getModelMatchPreferences(settings);
|
|
1559
1558
|
const { model: resolved } = parseModelPattern(options.modelPattern, availableModels, matchPreferences, {
|
|
1560
1559
|
modelRegistry,
|
|
1561
1560
|
});
|
|
@@ -1574,12 +1573,30 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1574
1573
|
// Re-resolve the allowed set: extension factories above may have
|
|
1575
1574
|
// registered providers/models that weren't visible at startup.
|
|
1576
1575
|
const fallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1576
|
+
// Prefer each provider's configured default model
|
|
1577
|
+
// (DEFAULT_MODEL_PER_PROVIDER) over raw catalog order. Without this the
|
|
1578
|
+
// first-run fallback picks whatever model sorts first in models.json for
|
|
1579
|
+
// the winning provider (e.g. anthropic's claude-3-5-sonnet-20240620)
|
|
1580
|
+
// instead of the intended provider default (claude-sonnet-4-6). Mirrors
|
|
1581
|
+
// findInitialModel's precedence.
|
|
1582
|
+
for (const [provider, defaultId] of Object.entries(defaultModelPerProvider)) {
|
|
1583
|
+
const preferred = fallbackCandidates.find(
|
|
1584
|
+
candidate => candidate.provider === provider && candidate.id === defaultId,
|
|
1585
|
+
);
|
|
1586
|
+
if (preferred && (await hasModelApiKey(preferred))) {
|
|
1587
|
+
model = preferred;
|
|
1580
1588
|
break;
|
|
1581
1589
|
}
|
|
1582
1590
|
}
|
|
1591
|
+
// Otherwise, first available model with a valid API key.
|
|
1592
|
+
if (!model) {
|
|
1593
|
+
for (const candidate of fallbackCandidates) {
|
|
1594
|
+
if (await hasModelApiKey(candidate)) {
|
|
1595
|
+
model = candidate;
|
|
1596
|
+
break;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1583
1600
|
if (model) {
|
|
1584
1601
|
if (modelFallbackMessage) {
|
|
1585
1602
|
modelFallbackMessage += `. Using ${model.provider}/${model.id}`;
|