@oh-my-pi/pi-coding-agent 17.2.2 → 17.2.4
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 +55 -0
- package/dist/{CHANGELOG-xfpkakrn.md → CHANGELOG-bpmhv26t.md} +55 -0
- package/dist/cli.js +3294 -3294
- package/dist/types/advisor/advise-tool.d.ts +6 -0
- package/dist/types/advisor/runtime.d.ts +4 -4
- package/dist/types/capability/mcp.d.ts +9 -0
- package/dist/types/cli/models-cli.d.ts +1 -1
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/discovery/helpers.d.ts +6 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +21 -2
- package/dist/types/extensibility/extensions/loader.d.ts +6 -2
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +2 -2
- package/dist/types/launch/paths.d.ts +2 -1
- package/dist/types/mcp/request-id.d.ts +19 -0
- package/dist/types/mcp/types.d.ts +14 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/messages.d.ts +15 -0
- package/dist/types/modes/components/ask-dialog.d.ts +0 -1
- package/dist/types/modes/components/model-browser.d.ts +7 -5
- package/dist/types/modes/components/model-picker.d.ts +9 -3
- package/dist/types/modes/components/read-tool-group.d.ts +9 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +14 -5
- package/dist/types/registry/agent-registry.d.ts +4 -3
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/secrets/index.d.ts +6 -8
- package/dist/types/session/agent-session.d.ts +6 -1
- package/dist/types/session/model-controls.d.ts +0 -1
- package/dist/types/session/session-advisors.d.ts +7 -0
- package/dist/types/session/session-maintenance.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +11 -5
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/tools/browser/launch.d.ts +30 -0
- package/dist/types/tools/browser/registry.d.ts +6 -1
- package/dist/types/tools/browser/shared-daemon.d.ts +26 -0
- package/dist/types/tools/default-renderer.d.ts +3 -0
- package/package.json +12 -12
- package/scripts/legacy-pi-virtual-module.ts +12 -2
- package/src/advisor/advise-tool.ts +18 -0
- package/src/advisor/runtime.ts +6 -6
- package/src/capability/mcp.ts +7 -0
- package/src/cli/models-cli.ts +13 -16
- package/src/collab/host.ts +1 -1
- package/src/config/keybindings.ts +14 -9
- package/src/config/mcp-schema.json +5 -0
- package/src/config/model-registry.ts +7 -1
- package/src/config/settings-schema.ts +1 -1
- package/src/discovery/builtin.ts +10 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/mcp-json.ts +11 -1
- package/src/discovery/omp-extension-roots.ts +79 -20
- package/src/discovery/omp-plugins.ts +10 -1
- package/src/extensibility/extensions/loader.ts +66 -28
- package/src/extensibility/plugins/marketplace/registry.ts +4 -8
- package/src/launch/broker.ts +59 -41
- package/src/launch/paths.ts +2 -5
- package/src/main.ts +24 -22
- package/src/mcp/config.ts +1 -0
- package/src/mcp/request-id.ts +24 -0
- package/src/mcp/transports/http.ts +4 -2
- package/src/mcp/transports/sse.ts +4 -2
- package/src/mcp/transports/stdio.ts +4 -2
- package/src/mcp/types.ts +15 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/messages.ts +19 -0
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/ask-dialog.ts +65 -23
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/model-browser.ts +25 -17
- package/src/modes/components/model-picker.ts +10 -6
- package/src/modes/components/read-tool-group.ts +26 -0
- package/src/modes/components/tool-execution.ts +77 -20
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/event-controller.ts +182 -10
- package/src/modes/controllers/selector-controller.ts +27 -6
- package/src/modes/types.ts +5 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/registry/agent-lifecycle.ts +36 -11
- package/src/registry/agent-registry.ts +12 -5
- package/src/sdk.ts +68 -29
- package/src/secrets/index.ts +15 -17
- package/src/session/agent-session.ts +11 -2
- package/src/session/agent-storage.ts +6 -4
- package/src/session/history-storage.ts +4 -2
- package/src/session/messages.ts +31 -7
- package/src/session/model-controls.ts +0 -1
- package/src/session/session-advisors.ts +19 -1
- package/src/session/session-maintenance.ts +5 -2
- package/src/session/session-manager.ts +128 -76
- package/src/session/session-tools.ts +25 -23
- package/src/slash-commands/builtin-registry.ts +2 -2
- package/src/system-prompt.ts +6 -1
- package/src/task/executor.ts +8 -1
- package/src/tiny/text.ts +12 -0
- package/src/tools/bash.ts +11 -5
- package/src/tools/browser/launch.ts +54 -9
- package/src/tools/browser/registry.ts +123 -30
- package/src/tools/browser/shared-daemon.ts +190 -0
- package/src/tools/browser.ts +1 -1
- package/src/tools/default-renderer.ts +19 -4
- package/src/tools/index.ts +10 -6
- package/src/web/search/providers/anthropic.ts +3 -1
- package/src/web/search/providers/codex.ts +31 -6
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
wrapTextWithAnsi,
|
|
16
16
|
} from "@oh-my-pi/pi-tui";
|
|
17
17
|
import type {
|
|
18
|
+
ExtensionAskDialogOption,
|
|
18
19
|
ExtensionAskDialogQuestion,
|
|
19
20
|
ExtensionAskDialogResultItem,
|
|
20
21
|
ExtensionAskDialogSubmitResult,
|
|
@@ -337,6 +338,45 @@ function renderRowLabel(
|
|
|
337
338
|
return lines;
|
|
338
339
|
}
|
|
339
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Coerce untrusted dialog questions into a render-safe shape. The live ask
|
|
343
|
+
* dialog is reached from the public `askDialog` extension surface and from
|
|
344
|
+
* streamed tool args, where a question entry can arrive with a missing or
|
|
345
|
+
* non-string `question` field. The render helpers (`replaceTabs`,
|
|
346
|
+
* `renderQuestionTitle`, `questionTabLabel`) assume strings, so a malformed
|
|
347
|
+
* entry throws and takes down the whole TUI render loop. Mirrors
|
|
348
|
+
* `normalizeRenderQuestions` on the transcript path.
|
|
349
|
+
*/
|
|
350
|
+
function normalizeDialogQuestions(questions: ExtensionAskDialogQuestion[]): ExtensionAskDialogQuestion[] {
|
|
351
|
+
if (!Array.isArray(questions)) return [];
|
|
352
|
+
const out: ExtensionAskDialogQuestion[] = [];
|
|
353
|
+
for (const entry of questions) {
|
|
354
|
+
if (!entry || typeof entry !== "object") continue;
|
|
355
|
+
const q = entry as Partial<ExtensionAskDialogQuestion>;
|
|
356
|
+
const options: ExtensionAskDialogOption[] = [];
|
|
357
|
+
if (Array.isArray(q.options)) {
|
|
358
|
+
for (const opt of q.options) {
|
|
359
|
+
if (!opt || typeof opt !== "object") continue;
|
|
360
|
+
const o = opt as Partial<ExtensionAskDialogOption>;
|
|
361
|
+
options.push({
|
|
362
|
+
label: typeof o.label === "string" ? o.label : "",
|
|
363
|
+
...(typeof o.description === "string" ? { description: o.description } : {}),
|
|
364
|
+
...(typeof o.preview === "string" ? { preview: o.preview } : {}),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
out.push({
|
|
369
|
+
id: typeof q.id === "string" ? q.id : "?",
|
|
370
|
+
question: typeof q.question === "string" ? q.question : "",
|
|
371
|
+
...(typeof q.header === "string" ? { header: q.header } : {}),
|
|
372
|
+
options,
|
|
373
|
+
...(typeof q.multi === "boolean" ? { multi: q.multi } : {}),
|
|
374
|
+
...(Number.isInteger(q.recommended) ? { recommended: q.recommended } : {}),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
|
|
340
380
|
export class AskDialogComponent implements Component {
|
|
341
381
|
#states: QuestionState[];
|
|
342
382
|
#activeTabIndex = 0;
|
|
@@ -352,13 +392,15 @@ export class AskDialogComponent implements Component {
|
|
|
352
392
|
#stableHeight: { key: string; total: number } | undefined;
|
|
353
393
|
#previewCache: PreviewRenderCache = new Map();
|
|
354
394
|
#overflowLayouts = new WeakMap<ExtensionAskDialogQuestion, Set<string>>();
|
|
395
|
+
readonly #questions: ExtensionAskDialogQuestion[];
|
|
355
396
|
|
|
356
397
|
constructor(
|
|
357
|
-
|
|
398
|
+
questions: ExtensionAskDialogQuestion[],
|
|
358
399
|
private readonly callbacks: AskDialogCallbacks,
|
|
359
400
|
private readonly options: AskDialogOptions = {},
|
|
360
401
|
) {
|
|
361
|
-
this.#
|
|
402
|
+
this.#questions = normalizeDialogQuestions(questions);
|
|
403
|
+
this.#states = this.#questions.map(question => {
|
|
362
404
|
const recommended = Number.isInteger(question.recommended) ? question.recommended : 0;
|
|
363
405
|
const maxIndex = Math.max(0, question.options.length - 1);
|
|
364
406
|
return {
|
|
@@ -474,8 +516,8 @@ export class AskDialogComponent implements Component {
|
|
|
474
516
|
const tabBarRows = this.#hasSubmitTab() ? 1 : 0;
|
|
475
517
|
const mdTheme = getMarkdownTheme();
|
|
476
518
|
let needed = MIN_DIALOG_ROWS;
|
|
477
|
-
for (let index = 0; index < this
|
|
478
|
-
const question = this
|
|
519
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
520
|
+
const question = this.#questions[index];
|
|
479
521
|
const state = this.#states[index];
|
|
480
522
|
if (!question || !state) continue;
|
|
481
523
|
const headerRows = tabBarRows + renderQuestionTitle(question, width).length;
|
|
@@ -493,7 +535,7 @@ export class AskDialogComponent implements Component {
|
|
|
493
535
|
if (this.#hasSubmitTab()) {
|
|
494
536
|
// Warning line + blank, one summary line per question, blank, and
|
|
495
537
|
// the Submit row; note lines added later scroll within the body.
|
|
496
|
-
const body = 2 + this
|
|
538
|
+
const body = 2 + this.#questions.length + 2;
|
|
497
539
|
needed = Math.max(needed, chrome + tabBarRows + 1 + Math.max(MIN_BODY_ROWS, body));
|
|
498
540
|
}
|
|
499
541
|
return Math.min(needed, maxHeight);
|
|
@@ -507,11 +549,11 @@ export class AskDialogComponent implements Component {
|
|
|
507
549
|
// Multi questions confirm on the Submit tab (Enter toggles, never
|
|
508
550
|
// submits), so any multi question forces the tab even when there is
|
|
509
551
|
// only one question.
|
|
510
|
-
return this
|
|
552
|
+
return this.#questions.length > 1 || this.#questions.some(question => question.multi);
|
|
511
553
|
}
|
|
512
554
|
|
|
513
555
|
#submitTabIndex(): number {
|
|
514
|
-
return this
|
|
556
|
+
return this.#questions.length;
|
|
515
557
|
}
|
|
516
558
|
|
|
517
559
|
#isSubmitTab(): boolean {
|
|
@@ -519,7 +561,7 @@ export class AskDialogComponent implements Component {
|
|
|
519
561
|
}
|
|
520
562
|
|
|
521
563
|
#currentQuestionIndex(): number {
|
|
522
|
-
return clamp(this.#activeTabIndex, 0, Math.max(0, this
|
|
564
|
+
return clamp(this.#activeTabIndex, 0, Math.max(0, this.#questions.length - 1));
|
|
523
565
|
}
|
|
524
566
|
|
|
525
567
|
#requestRender(): void {
|
|
@@ -530,7 +572,7 @@ export class AskDialogComponent implements Component {
|
|
|
530
572
|
const lines: string[] = [];
|
|
531
573
|
if (this.#hasSubmitTab()) {
|
|
532
574
|
const tabs: Tab[] = [
|
|
533
|
-
...this
|
|
575
|
+
...this.#questions.map((question, index) => ({
|
|
534
576
|
id: String(index),
|
|
535
577
|
label: questionTabLabel(question, index),
|
|
536
578
|
})),
|
|
@@ -545,7 +587,7 @@ export class AskDialogComponent implements Component {
|
|
|
545
587
|
return lines;
|
|
546
588
|
}
|
|
547
589
|
const questionIndex = this.#currentQuestionIndex();
|
|
548
|
-
const question = this
|
|
590
|
+
const question = this.#questions[questionIndex];
|
|
549
591
|
if (!question) return lines;
|
|
550
592
|
lines.push(...renderQuestionTitle(question, width));
|
|
551
593
|
return lines;
|
|
@@ -559,7 +601,7 @@ export class AskDialogComponent implements Component {
|
|
|
559
601
|
const scroll = indicator ? ` ${indicator} scroll ·` : "";
|
|
560
602
|
return `Enter submit · ↑/↓ scroll ·${scroll} ${cancel}`;
|
|
561
603
|
}
|
|
562
|
-
const question = this
|
|
604
|
+
const question = this.#questions[this.#currentQuestionIndex()];
|
|
563
605
|
const action = question?.multi ? "Space/Enter toggle · n note" : "Enter select · n note";
|
|
564
606
|
const tabs = this.#hasSubmitTab() ? " · Tab/←/→" : "";
|
|
565
607
|
if (this.#questionCanPage && indicator) {
|
|
@@ -585,7 +627,7 @@ export class AskDialogComponent implements Component {
|
|
|
585
627
|
}
|
|
586
628
|
|
|
587
629
|
#activeQuestionState(): { question: ExtensionAskDialogQuestion; state: QuestionState } | undefined {
|
|
588
|
-
const question = this
|
|
630
|
+
const question = this.#questions[this.#currentQuestionIndex()];
|
|
589
631
|
const state = this.#states[this.#currentQuestionIndex()];
|
|
590
632
|
if (!question || !state) return undefined;
|
|
591
633
|
return { question, state };
|
|
@@ -672,18 +714,18 @@ export class AskDialogComponent implements Component {
|
|
|
672
714
|
}
|
|
673
715
|
|
|
674
716
|
#switchTab(direction: 1 | -1): void {
|
|
675
|
-
const tabCount = this
|
|
717
|
+
const tabCount = this.#questions.length + 1;
|
|
676
718
|
this.#activeTabIndex = (this.#activeTabIndex + direction + tabCount) % tabCount;
|
|
677
719
|
this.#submitScrollOffset = 0;
|
|
678
720
|
}
|
|
679
721
|
|
|
680
722
|
#advanceAfterQuestion(): void {
|
|
681
723
|
const current = this.#currentQuestionIndex();
|
|
682
|
-
if (this
|
|
724
|
+
if (this.#questions.length === 1) {
|
|
683
725
|
this.#finishSubmit();
|
|
684
726
|
return;
|
|
685
727
|
}
|
|
686
|
-
this.#activeTabIndex = current + 1 < this
|
|
728
|
+
this.#activeTabIndex = current + 1 < this.#questions.length ? current + 1 : this.#submitTabIndex();
|
|
687
729
|
this.#submitScrollOffset = 0;
|
|
688
730
|
this.#requestRender();
|
|
689
731
|
}
|
|
@@ -829,8 +871,8 @@ export class AskDialogComponent implements Component {
|
|
|
829
871
|
);
|
|
830
872
|
allLines.push("");
|
|
831
873
|
}
|
|
832
|
-
for (let index = 0; index < this
|
|
833
|
-
const question = this
|
|
874
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
875
|
+
const question = this.#questions[index];
|
|
834
876
|
const state = this.#states[index];
|
|
835
877
|
if (!question || !state) continue;
|
|
836
878
|
const label = questionTabLabel(question, index);
|
|
@@ -895,8 +937,8 @@ export class AskDialogComponent implements Component {
|
|
|
895
937
|
|
|
896
938
|
#unansweredCount(): number {
|
|
897
939
|
let count = 0;
|
|
898
|
-
for (let index = 0; index < this
|
|
899
|
-
const question = this
|
|
940
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
941
|
+
const question = this.#questions[index];
|
|
900
942
|
const state = this.#states[index];
|
|
901
943
|
if (!question || !state) continue;
|
|
902
944
|
if (state.selectedOptions.size === 0 && state.customInput === undefined) count += 1;
|
|
@@ -911,8 +953,8 @@ export class AskDialogComponent implements Component {
|
|
|
911
953
|
return;
|
|
912
954
|
}
|
|
913
955
|
this.options.onTimeout?.();
|
|
914
|
-
for (let index = 0; index < this
|
|
915
|
-
const question = this
|
|
956
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
957
|
+
const question = this.#questions[index];
|
|
916
958
|
const state = this.#states[index];
|
|
917
959
|
if (!question || !state) continue;
|
|
918
960
|
if (state.selectedOptions.size === 0 && state.customInput === undefined) {
|
|
@@ -952,8 +994,8 @@ export class AskDialogComponent implements Component {
|
|
|
952
994
|
|
|
953
995
|
#buildResults(): ExtensionAskDialogResultItem[] {
|
|
954
996
|
const results: ExtensionAskDialogResultItem[] = [];
|
|
955
|
-
for (let index = 0; index < this
|
|
956
|
-
const question = this
|
|
997
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
998
|
+
const question = this.#questions[index];
|
|
957
999
|
const state = this.#states[index];
|
|
958
1000
|
if (!question || !state) continue;
|
|
959
1001
|
const selectedOptions = question.options
|
|
@@ -56,7 +56,7 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
|
|
|
56
56
|
"app.thinking.toggle": ["ctrl+t"],
|
|
57
57
|
"app.editor.external": ["ctrl+g"],
|
|
58
58
|
"app.history.search": ["ctrl+r"],
|
|
59
|
-
"app.message.dequeue": ["alt+up"],
|
|
59
|
+
"app.message.dequeue": ["alt+up", "shift+up"],
|
|
60
60
|
"app.retry": ["alt+r"],
|
|
61
61
|
"app.clipboard.pasteImage": ["ctrl+v"],
|
|
62
62
|
"app.clipboard.pasteTextRaw": ["ctrl+shift+v", "alt+shift+v"],
|
|
@@ -314,10 +314,10 @@ function padLeftVisible(text: string, width: number): string {
|
|
|
314
314
|
export interface ModelBrowserOptions {
|
|
315
315
|
/** Render the dim `provider/` prefix before model ids. Default true. */
|
|
316
316
|
showProvider?: boolean;
|
|
317
|
-
/** Session token count used to
|
|
317
|
+
/** Session token count used to flag models whose context window is exceeded. */
|
|
318
318
|
currentContextTokens?: number;
|
|
319
|
-
/** When true, rows
|
|
320
|
-
|
|
319
|
+
/** When true, over-context rows render grayed; picking one compacts first (session-switch mode). */
|
|
320
|
+
markOverContext?: boolean;
|
|
321
321
|
/** Host-provided empty-state text (e.g. provider discovery status). */
|
|
322
322
|
emptyText?: () => string | undefined;
|
|
323
323
|
}
|
|
@@ -351,7 +351,7 @@ export class ModelBrowser implements Component {
|
|
|
351
351
|
#maxVisible = 10;
|
|
352
352
|
#showProvider: boolean;
|
|
353
353
|
#currentContextTokens: number;
|
|
354
|
-
#
|
|
354
|
+
#markOverContext: boolean;
|
|
355
355
|
#emptyText?: () => string | undefined;
|
|
356
356
|
/** Keep role-like virtual rows in their host-defined order during search. */
|
|
357
357
|
#preserveQueryOrder = false;
|
|
@@ -375,7 +375,7 @@ export class ModelBrowser implements Component {
|
|
|
375
375
|
this.#showProvider = options.showProvider ?? true;
|
|
376
376
|
const tokens = options.currentContextTokens ?? 0;
|
|
377
377
|
this.#currentContextTokens = Number.isFinite(tokens) && tokens > 0 ? Math.floor(tokens) : 0;
|
|
378
|
-
this.#
|
|
378
|
+
this.#markOverContext = options.markOverContext ?? false;
|
|
379
379
|
this.#emptyText = options.emptyText;
|
|
380
380
|
}
|
|
381
381
|
|
|
@@ -420,9 +420,9 @@ export class ModelBrowser implements Component {
|
|
|
420
420
|
setPreserveQueryOrder(preserve: boolean): void {
|
|
421
421
|
this.#preserveQueryOrder = preserve;
|
|
422
422
|
}
|
|
423
|
-
/** Allow hosts to toggle context-window
|
|
424
|
-
|
|
425
|
-
this.#
|
|
423
|
+
/** Allow hosts to toggle context-window flagging between browser modes. */
|
|
424
|
+
setMarkOverContext(mark: boolean): void {
|
|
425
|
+
this.#markOverContext = mark;
|
|
426
426
|
}
|
|
427
427
|
/** Focused: accent cursor + selected-row background band. Unfocused: dim cursor, no band. */
|
|
428
428
|
setFocused(focused: boolean): void {
|
|
@@ -461,8 +461,13 @@ export class ModelBrowser implements Component {
|
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
#isDisabled(item: ModelBrowserItem): boolean {
|
|
464
|
-
|
|
465
|
-
|
|
464
|
+
return item.id === "separator";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** True when `item`'s context window is smaller than the live session token count (grayed row; hosts compact before switching). */
|
|
468
|
+
isOverContext(item: ModelBrowserItem): boolean {
|
|
469
|
+
if (item.id === "separator") return false;
|
|
470
|
+
if (!this.#markOverContext || this.#currentContextTokens <= 0) return false;
|
|
466
471
|
const contextWindow = item.model.contextWindow ?? 0;
|
|
467
472
|
return contextWindow > 0 && this.#currentContextTokens > contextWindow;
|
|
468
473
|
}
|
|
@@ -718,7 +723,7 @@ export class ModelBrowser implements Component {
|
|
|
718
723
|
const line = theme.fg("muted", "─".repeat(dashCount));
|
|
719
724
|
return ` ${line} `;
|
|
720
725
|
}
|
|
721
|
-
const
|
|
726
|
+
const overContext = this.isOverContext(item);
|
|
722
727
|
const prefix = selected && this.#focused ? `${theme.fg("accent", theme.nav.cursor)} ` : " ";
|
|
723
728
|
const providerPrefix = this.#showProvider ? theme.fg("dim", `${item.provider}/`) : "";
|
|
724
729
|
const name = item.labelColor
|
|
@@ -728,7 +733,7 @@ export class ModelBrowser implements Component {
|
|
|
728
733
|
: item.id;
|
|
729
734
|
const currentMark =
|
|
730
735
|
item.selector === this.#currentSelector ? ` ${theme.fg("success", theme.status.enabled)}` : "";
|
|
731
|
-
const overLimit =
|
|
736
|
+
const overLimit = overContext
|
|
732
737
|
? ` ${theme.status.disabled} context>${formatNumber(item.model.contextWindow ?? 0).toLowerCase()}`
|
|
733
738
|
: "";
|
|
734
739
|
let left = `${prefix}${providerPrefix}${name}${currentMark}${overLimit}`;
|
|
@@ -743,12 +748,15 @@ export class ModelBrowser implements Component {
|
|
|
743
748
|
const gap = Math.max(0, available - visibleWidth(left));
|
|
744
749
|
|
|
745
750
|
let line = `${left}${" ".repeat(gap)} ${meta}`;
|
|
746
|
-
if (
|
|
747
|
-
|
|
751
|
+
if (overContext) {
|
|
752
|
+
// Gray the whole row but keep the selection cursor visible: over-context
|
|
753
|
+
// models stay selectable (the host compacts before switching).
|
|
754
|
+
const plainPrefix = Bun.stripANSI(prefix);
|
|
755
|
+
line = `${prefix}${theme.fg("dim", Bun.stripANSI(line).slice(plainPrefix.length))}`;
|
|
748
756
|
}
|
|
749
757
|
// The bg band is reserved for the mouse: it marks hover, nothing else.
|
|
750
758
|
// Keyboard selection is the cursor glyph + accent name.
|
|
751
|
-
if (hovered
|
|
759
|
+
if (hovered) {
|
|
752
760
|
line = theme.bg("selectedBg", line);
|
|
753
761
|
}
|
|
754
762
|
return line;
|
|
@@ -772,8 +780,8 @@ export class ModelBrowser implements Component {
|
|
|
772
780
|
}
|
|
773
781
|
const line1 = truncateToWidth(theme.fg("muted", ` ${facts.join(" · ")}`), width);
|
|
774
782
|
|
|
775
|
-
if (this
|
|
776
|
-
const warning = ` ${theme.status.disabled}
|
|
783
|
+
if (this.isOverContext(selected)) {
|
|
784
|
+
const warning = ` ${theme.status.disabled} context ${formatNumber(this.#currentContextTokens).toLowerCase()} exceeds ${formatNumber(model.contextWindow ?? 0).toLowerCase()} limit · compacts with current model, then switches`;
|
|
777
785
|
return [line1, truncateToWidth(theme.fg("warning", warning), width)];
|
|
778
786
|
}
|
|
779
787
|
|
|
@@ -22,8 +22,12 @@ import { bottomBorder, row, topBorder } from "./overlay-box";
|
|
|
22
22
|
import { resolveSegmentPalette } from "./segment-track";
|
|
23
23
|
|
|
24
24
|
export interface ModelPickerCallbacks {
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A model was chosen for a session-only switch. `selector` is `provider/id`.
|
|
27
|
+
* `overContext` is true when the session transcript exceeds the model's
|
|
28
|
+
* context window — the host must compact before switching.
|
|
29
|
+
*/
|
|
30
|
+
onPick: (model: Model, selector: string, meta: { overContext: boolean }) => void;
|
|
27
31
|
/** A configured ctrl+p quick role was chosen. */
|
|
28
32
|
onPickRole?: (entry: ResolvedRoleModel) => void;
|
|
29
33
|
/** The picker was dismissed. */
|
|
@@ -31,7 +35,7 @@ export interface ModelPickerCallbacks {
|
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export interface ModelPickerOptions {
|
|
34
|
-
/** Session token count; models with smaller context windows are
|
|
38
|
+
/** Session token count; models with smaller context windows are grayed and compact-first on pick. */
|
|
35
39
|
currentContextTokens?: number;
|
|
36
40
|
/** `provider/id` of the session's active model; highlighted and preselected. */
|
|
37
41
|
currentSelector?: string;
|
|
@@ -97,7 +101,7 @@ export class ModelPickerComponent implements Component {
|
|
|
97
101
|
|
|
98
102
|
this.#browser = new ModelBrowser(settings, {
|
|
99
103
|
currentContextTokens: options.currentContextTokens,
|
|
100
|
-
|
|
104
|
+
markOverContext: true,
|
|
101
105
|
emptyText: () => (this.#roleMode ? " No quick roles in the Ctrl+P cycle" : undefined),
|
|
102
106
|
});
|
|
103
107
|
this.#browser.onActivate = item => {
|
|
@@ -106,7 +110,7 @@ export class ModelPickerComponent implements Component {
|
|
|
106
110
|
callbacks.onPickRole?.(quickRole);
|
|
107
111
|
return;
|
|
108
112
|
}
|
|
109
|
-
callbacks.onPick(item.model, item.selector);
|
|
113
|
+
callbacks.onPick(item.model, item.selector, { overContext: this.#browser.isOverContext(item) });
|
|
110
114
|
};
|
|
111
115
|
this.#browser.onCancel = () => callbacks.onCancel();
|
|
112
116
|
this.#browser.onQueryChange = query => this.#syncItemsForQuery(query);
|
|
@@ -193,7 +197,7 @@ export class ModelPickerComponent implements Component {
|
|
|
193
197
|
|
|
194
198
|
this.#roleMode = roleMode;
|
|
195
199
|
this.#browser.setShowProvider(!roleMode);
|
|
196
|
-
this.#browser.
|
|
200
|
+
this.#browser.setMarkOverContext(!roleMode);
|
|
197
201
|
this.#browser.setPreserveQueryOrder(roleMode);
|
|
198
202
|
const currentSelector = roleMode ? this.#currentQuickRoleSelector : this.#currentSelector;
|
|
199
203
|
this.#browser.setCurrentSelector(currentSelector);
|
|
@@ -397,6 +397,32 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
397
397
|
this.#updateDisplay();
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Re-key an entry whose streamed tool-call id changed mid-stream (a provider
|
|
402
|
+
* rewriting the id across deltas; see EventController's
|
|
403
|
+
* `#streamedToolCallIdByIndex`). Preserves row order so a sibling read run is
|
|
404
|
+
* not visibly reshuffled, and no-ops when the rename would collide.
|
|
405
|
+
*/
|
|
406
|
+
renameEntry(oldId: string, newId: string): void {
|
|
407
|
+
if (oldId === newId || !newId) return;
|
|
408
|
+
const entry = this.#entries.get(oldId);
|
|
409
|
+
if (!entry || this.#entries.has(newId)) return;
|
|
410
|
+
entry.toolCallId = newId;
|
|
411
|
+
const reordered = [...this.#entries].map(([key, value]): [string, ReadEntry] => [
|
|
412
|
+
key === oldId ? newId : key,
|
|
413
|
+
value,
|
|
414
|
+
]);
|
|
415
|
+
this.#entries.clear();
|
|
416
|
+
for (const [key, value] of reordered) this.#entries.set(key, value);
|
|
417
|
+
this.#updateDisplay();
|
|
418
|
+
}
|
|
419
|
+
/** Remove one call without discarding successful siblings in the shared group. */
|
|
420
|
+
removeEntry(toolCallId: string): boolean {
|
|
421
|
+
if (!this.#entries.delete(toolCallId)) return this.#entries.size === 0;
|
|
422
|
+
this.#updateDisplay();
|
|
423
|
+
return this.#entries.size === 0;
|
|
424
|
+
}
|
|
425
|
+
|
|
400
426
|
updateResult(
|
|
401
427
|
result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
|
|
402
428
|
isPartial = false,
|
|
@@ -276,6 +276,9 @@ let toolExecutionInstanceSeq = 0;
|
|
|
276
276
|
export class ToolExecutionComponent extends Container implements NativeScrollbackLiveRegion {
|
|
277
277
|
#contentBox: Box; // Used for custom tools and bash visual truncation
|
|
278
278
|
#contentText: WidthAwareText; // Generic fallback (no custom/built-in renderer)
|
|
279
|
+
// Which container the constructor mounted: bespoke/built-in renderers use
|
|
280
|
+
// #contentBox, everything else the generic #contentText fallback.
|
|
281
|
+
#usesContentBox = false;
|
|
279
282
|
#multiFileBoxes: (Box | Spacer)[] = []; // Extra boxes for multi-file edit results
|
|
280
283
|
#imageComponents: Image[] = [];
|
|
281
284
|
#imageSpacers: Spacer[] = [];
|
|
@@ -411,26 +414,13 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
411
414
|
// strips PLAIN-blank edges, so framed/minimal blocks (no bg set) drop these
|
|
412
415
|
// lines and keep their tight spacing — only tinted lines survive.
|
|
413
416
|
this.#contentBox = new Box(0, 1);
|
|
414
|
-
this.#contentText = new WidthAwareText(
|
|
415
|
-
contentWidth =>
|
|
416
|
-
formatDefaultToolExecution(
|
|
417
|
-
{
|
|
418
|
-
label: this.#toolLabel,
|
|
419
|
-
args: this.#args,
|
|
420
|
-
result: this.#result ? { output: this.#getTextOutput(), isError: this.#result.isError } : undefined,
|
|
421
|
-
options: this.#renderState,
|
|
422
|
-
},
|
|
423
|
-
contentWidth,
|
|
424
|
-
theme,
|
|
425
|
-
),
|
|
426
|
-
1,
|
|
427
|
-
1,
|
|
428
|
-
);
|
|
417
|
+
this.#contentText = new WidthAwareText(contentWidth => this.#renderDefaultCard(contentWidth), 1, 1);
|
|
429
418
|
|
|
430
419
|
// Use Box for custom tools or built-in tools that have renderers
|
|
431
420
|
const hasRenderer = toolName in toolRenderers;
|
|
432
421
|
const hasCustomRenderer = !!(tool?.renderCall || tool?.renderResult);
|
|
433
|
-
|
|
422
|
+
this.#usesContentBox = hasCustomRenderer || hasRenderer;
|
|
423
|
+
if (this.#usesContentBox) {
|
|
434
424
|
this.addChild(this.#contentBox);
|
|
435
425
|
} else {
|
|
436
426
|
this.addChild(this.#contentText);
|
|
@@ -998,12 +988,21 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
998
988
|
|
|
999
989
|
// Non-self-framing tools (custom/extension renderers and the generic
|
|
1000
990
|
// fallback) get a padded, state-tinted block — built-ins that draw their
|
|
1001
|
-
// own frame opt out below via the framed-component mark.
|
|
1002
|
-
|
|
991
|
+
// own frame opt out below via the framed-component mark. A benign skip
|
|
992
|
+
// (steering/peer interrupt aborted a still-pending call) never ran, so it
|
|
993
|
+
// gets the neutral pending tint rather than the error tint (#7199).
|
|
994
|
+
const benignSkip = this.#isBenignSkip();
|
|
995
|
+
const stateBgKey =
|
|
996
|
+
this.#isPartial || benignSkip ? "toolPendingBg" : this.#result?.isError ? "toolErrorBg" : "toolSuccessBg";
|
|
1003
997
|
const stateBgFn = (t: string) => theme.bg(stateBgKey, t);
|
|
1004
998
|
|
|
1005
|
-
//
|
|
1006
|
-
|
|
999
|
+
// A benign skip is a synthetic placeholder for a call that never executed,
|
|
1000
|
+
// so bypass any bespoke error frame and draw the neutral generic card —
|
|
1001
|
+
// the per-tool ✘/red-border would misread normal mid-turn steering as a
|
|
1002
|
+
// failure (#7199).
|
|
1003
|
+
if (benignSkip) {
|
|
1004
|
+
this.#renderBenignSkipCard(stateBgFn);
|
|
1005
|
+
} else if (this.#tool && (this.#tool.renderCall || this.#tool.renderResult)) {
|
|
1007
1006
|
const tool = this.#tool;
|
|
1008
1007
|
const mergeCallAndResult = Boolean((tool as { mergeCallAndResult?: boolean }).mergeCallAndResult);
|
|
1009
1008
|
// Custom tools use Box for flexible component rendering
|
|
@@ -1399,4 +1398,62 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
1399
1398
|
|
|
1400
1399
|
return output;
|
|
1401
1400
|
}
|
|
1401
|
+
|
|
1402
|
+
/**
|
|
1403
|
+
* Format the generic call/result card at `contentWidth`. Shared by the
|
|
1404
|
+
* #contentText fallback and the benign-skip path so both render identically.
|
|
1405
|
+
*/
|
|
1406
|
+
#renderDefaultCard(contentWidth: number): string {
|
|
1407
|
+
return formatDefaultToolExecution(
|
|
1408
|
+
{
|
|
1409
|
+
label: this.#toolLabel,
|
|
1410
|
+
args: this.#args,
|
|
1411
|
+
result: this.#result
|
|
1412
|
+
? { output: this.#getTextOutput(), isError: this.#result.isError, skipped: this.#isBenignSkip() }
|
|
1413
|
+
: undefined,
|
|
1414
|
+
options: this.#renderState,
|
|
1415
|
+
},
|
|
1416
|
+
contentWidth,
|
|
1417
|
+
theme,
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* True for a steering/peer-interrupt placeholder. A synthetic placeholder
|
|
1423
|
+
* identifies a call that never entered `tool.execute`; an interrupted
|
|
1424
|
+
* placeholder identifies one that started but threw before returning usable
|
|
1425
|
+
* output. Both are normal steering control flow and render neutrally (#7199).
|
|
1426
|
+
*/
|
|
1427
|
+
#isBenignSkip(): boolean {
|
|
1428
|
+
if (this.#isPartial || !this.#result) return false;
|
|
1429
|
+
const details = this.#result.details as
|
|
1430
|
+
| { __synthetic?: boolean; __interrupted?: boolean; source?: string; execution?: string }
|
|
1431
|
+
| undefined;
|
|
1432
|
+
if (details?.source !== "interrupt_skipped") return false;
|
|
1433
|
+
return details.__synthetic === true || (details.__interrupted === true && details.execution === "started");
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Render a benign skip as the neutral generic card, replacing any bespoke
|
|
1438
|
+
* renderer's error frame. Generic-fallback tools already route through
|
|
1439
|
+
* {@link #renderDefaultCard} (which emits the info card for a skip); they
|
|
1440
|
+
* only need the neutral tint. Bespoke-renderer tools get their content box
|
|
1441
|
+
* swapped for the same neutral card.
|
|
1442
|
+
*/
|
|
1443
|
+
#renderBenignSkipCard(stateBgFn: (text: string) => string): void {
|
|
1444
|
+
if (!this.#usesContentBox) {
|
|
1445
|
+
this.#contentText.setCustomBgFn(stateBgFn);
|
|
1446
|
+
this.#contentText.invalidate();
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
for (const box of this.#multiFileBoxes) {
|
|
1450
|
+
this.removeChild(box);
|
|
1451
|
+
}
|
|
1452
|
+
this.#multiFileBoxes = [];
|
|
1453
|
+
this.#contentBox.setBgFn(undefined);
|
|
1454
|
+
this.#contentBox.clear();
|
|
1455
|
+
this.#contentBox.setPaddingX(1);
|
|
1456
|
+
this.#contentBox.setBgFn(stateBgFn);
|
|
1457
|
+
this.#contentBox.addChild(new WidthAwareText(contentWidth => this.#renderDefaultCard(contentWidth), 0, 0));
|
|
1458
|
+
}
|
|
1402
1459
|
}
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
seedAlreadyExists,
|
|
29
29
|
summarizeMentalModel,
|
|
30
30
|
} from "../../hindsight";
|
|
31
|
-
import { resolveMemoryBackend } from "../../memory-backend";
|
|
31
|
+
import { memoryStatsUnavailableMessage, resolveMemoryBackend } from "../../memory-backend";
|
|
32
32
|
import { BashExecutionComponent } from "../../modes/components/bash-execution";
|
|
33
33
|
import { BorderedLoader } from "../../modes/components/bordered-loader";
|
|
34
34
|
import { DynamicBorder } from "../../modes/components/dynamic-border";
|
|
@@ -658,7 +658,7 @@ export class CommandController {
|
|
|
658
658
|
try {
|
|
659
659
|
const payload = await hook?.(agentDir, this.ctx.sessionManager.getCwd(), this.ctx.session);
|
|
660
660
|
if (!payload) {
|
|
661
|
-
this.ctx.showWarning(
|
|
661
|
+
this.ctx.showWarning(memoryStatsUnavailableMessage(backend.id, action));
|
|
662
662
|
return;
|
|
663
663
|
}
|
|
664
664
|
showMarkdownPanel(this.ctx, `Memory ${action === "stats" ? "Stats" : "Diagnostics"}`, payload);
|