@oh-my-pi/pi-coding-agent 15.10.4 → 15.10.6
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 +74 -0
- package/dist/types/capability/rule-buckets.d.ts +1 -1
- package/dist/types/capability/rule.d.ts +6 -1
- package/dist/types/cli/update-cli.d.ts +11 -1
- package/dist/types/config/model-registry.d.ts +18 -1
- package/dist/types/discovery/at-imports.d.ts +15 -0
- package/dist/types/edit/diff.d.ts +3 -2
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +7 -0
- package/dist/types/eval/js/context-manager.d.ts +1 -0
- package/dist/types/eval/js/executor.d.ts +2 -0
- package/dist/types/eval/js/index.d.ts +1 -1
- package/dist/types/eval/js/shared/helpers.d.ts +6 -0
- package/dist/types/eval/js/shared/runtime.d.ts +5 -0
- package/dist/types/eval/js/worker-protocol.d.ts +6 -0
- package/dist/types/eval/py/executor.d.ts +7 -0
- package/dist/types/eval/py/index.d.ts +1 -1
- package/dist/types/exa/index.d.ts +1 -19
- package/dist/types/exa/mcp-client.d.ts +10 -3
- package/dist/types/exa/types.d.ts +0 -83
- package/dist/types/export/ttsr.d.ts +14 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -1
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
- package/dist/types/internal-urls/local-protocol.d.ts +10 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -2
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/components/{status-line.d.ts → status-line/component.d.ts} +2 -32
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +31 -2
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +8 -0
- package/dist/types/modes/image-references.d.ts +8 -3
- package/dist/types/modes/interactive-mode.d.ts +9 -1
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +0 -2
- package/dist/types/task/render.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +15 -0
- package/dist/types/tools/index.d.ts +17 -2
- package/dist/types/tools/render-utils.d.ts +1 -1
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/block-context.d.ts +35 -0
- package/dist/types/utils/git.d.ts +6 -0
- package/dist/types/utils/image-loading.d.ts +12 -0
- package/package.json +29 -9
- package/src/capability/rule-buckets.ts +4 -2
- package/src/capability/rule.ts +10 -1
- package/src/cli/auth-broker-cli.ts +6 -7
- package/src/cli/auth-gateway-cli.ts +4 -3
- package/src/cli/list-models.ts +5 -0
- package/src/cli/update-cli.ts +138 -16
- package/src/commit/agentic/tools/split-commit.ts +8 -1
- package/src/config/model-provider-priority.ts +1 -0
- package/src/config/model-registry.ts +81 -2
- package/src/debug/index.ts +4 -8
- package/src/discovery/at-imports.ts +273 -0
- package/src/discovery/builtin-rules/index.ts +4 -0
- package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
- package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
- package/src/discovery/helpers.ts +2 -1
- package/src/edit/diff.ts +114 -4
- package/src/edit/hashline/diff.ts +1 -1
- package/src/edit/hashline/execute.ts +1 -1
- package/src/edit/modes/patch.ts +6 -2
- package/src/edit/modes/replace.ts +1 -1
- package/src/edit/renderer.ts +12 -2
- package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
- package/src/eval/backend.ts +15 -0
- package/src/eval/js/context-manager.ts +4 -2
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/index.ts +7 -1
- package/src/eval/js/shared/helpers.ts +53 -6
- package/src/eval/js/shared/runtime.ts +8 -0
- package/src/eval/js/worker-core.ts +1 -0
- package/src/eval/js/worker-protocol.ts +6 -0
- package/src/eval/py/executor.ts +12 -0
- package/src/eval/py/index.ts +7 -1
- package/src/eval/py/prelude.py +43 -4
- package/src/eval/py/runner.py +1 -0
- package/src/exa/index.ts +1 -26
- package/src/exa/mcp-client.ts +10 -10
- package/src/exa/types.ts +0 -97
- package/src/export/ttsr.ts +122 -1
- package/src/extensibility/extensions/types.ts +8 -1
- package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
- package/src/extensibility/plugins/doctor.ts +1 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +6 -5
- package/src/goals/tools/goal-tool.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/internal-urls/local-protocol.ts +13 -0
- package/src/lsp/render.ts +8 -6
- package/src/mcp/oauth-flow.ts +3 -3
- package/src/mcp/render.ts +7 -1
- package/src/modes/components/agent-dashboard.ts +6 -4
- package/src/modes/components/custom-editor.ts +12 -6
- package/src/modes/components/login-dialog.ts +1 -1
- package/src/modes/components/oauth-selector.ts +4 -4
- package/src/modes/components/read-tool-group.ts +10 -3
- package/src/modes/components/{status-line.ts → status-line/component.ts} +18 -40
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/types.ts +23 -8
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/transcript-container.ts +17 -10
- package/src/modes/components/user-message.ts +6 -3
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/event-controller.ts +8 -0
- package/src/modes/controllers/extension-ui-controller.ts +143 -127
- package/src/modes/controllers/input-controller.ts +60 -11
- package/src/modes/controllers/mcp-command-controller.ts +52 -17
- package/src/modes/controllers/selector-controller.ts +4 -11
- package/src/modes/controllers/ssh-command-controller.ts +2 -2
- package/src/modes/image-references.ts +13 -7
- package/src/modes/interactive-mode.ts +35 -3
- package/src/modes/rpc/rpc-mode.ts +1 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -11
- package/src/modes/theme/theme.ts +95 -1
- package/src/modes/types.ts +3 -1
- package/src/modes/utils/ui-helpers.ts +14 -5
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/eval.md +4 -4
- package/src/sdk.ts +31 -14
- package/src/session/agent-session.ts +290 -196
- package/src/session/session-manager.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +9 -1
- package/src/system-prompt.ts +15 -9
- package/src/task/index.ts +9 -1
- package/src/task/render.ts +36 -14
- package/src/tools/ask.ts +14 -5
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash.ts +14 -2
- package/src/tools/browser/render.ts +5 -2
- package/src/tools/browser/tab-worker.ts +211 -91
- package/src/tools/debug.ts +5 -2
- package/src/tools/eval-render.ts +6 -3
- package/src/tools/eval.ts +1 -1
- package/src/tools/gh-renderer.ts +29 -15
- package/src/tools/index.ts +32 -4
- package/src/tools/inspect-image-renderer.ts +12 -5
- package/src/tools/job.ts +9 -6
- package/src/tools/memory-render.ts +19 -5
- package/src/tools/read.ts +165 -18
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +1 -1
- package/src/tools/ssh.ts +4 -1
- package/src/tools/todo.ts +8 -1
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/tools/write.ts +1 -1
- package/src/tui/code-cell.ts +1 -1
- package/src/utils/block-context.ts +312 -0
- package/src/utils/git.ts +41 -0
- package/src/utils/image-loading.ts +31 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/render.ts +14 -6
- package/dist/types/exa/factory.d.ts +0 -13
- package/dist/types/exa/render.d.ts +0 -19
- package/dist/types/exa/researcher.d.ts +0 -9
- package/dist/types/exa/search.d.ts +0 -9
- package/dist/types/exa/websets.d.ts +0 -9
- package/src/exa/factory.ts +0 -60
- package/src/exa/render.ts +0 -244
- package/src/exa/researcher.ts +0 -36
- package/src/exa/search.ts +0 -47
- package/src/exa/websets.ts +0 -248
|
@@ -30,6 +30,11 @@ export class ExtensionUiController {
|
|
|
30
30
|
#extensionTerminalInputUnsubscribers = new Set<() => void>();
|
|
31
31
|
#hookWidgetsAbove = new Map<string, ExtensionUiComponent>();
|
|
32
32
|
#hookWidgetsBelow = new Map<string, ExtensionUiComponent>();
|
|
33
|
+
// Single-file dialog surface (`editorContainer` + focus) is shared by the
|
|
34
|
+
// selector / input / editor modals, so only one may be presented at a time;
|
|
35
|
+
// the rest queue. See `#presentDialog`.
|
|
36
|
+
#dialogActive = false;
|
|
37
|
+
#dialogQueue: Array<() => void> = [];
|
|
33
38
|
constructor(private ctx: InteractiveModeContext) {}
|
|
34
39
|
|
|
35
40
|
/**
|
|
@@ -528,58 +533,47 @@ export class ExtensionUiController {
|
|
|
528
533
|
dialogOptions?: InteractiveSelectorDialogOptions,
|
|
529
534
|
extra?: { slider?: HookSelectorSlider },
|
|
530
535
|
): Promise<string | undefined> {
|
|
531
|
-
|
|
532
|
-
(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
:
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
:
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
markableCount: dialogOptions?.markableCount,
|
|
573
|
-
maxVisible,
|
|
574
|
-
slider: extra?.slider,
|
|
575
|
-
},
|
|
576
|
-
);
|
|
577
|
-
this.ctx.editorContainer.clear();
|
|
578
|
-
this.ctx.editorContainer.addChild(this.ctx.hookSelector);
|
|
579
|
-
this.ctx.ui.setFocus(this.ctx.hookSelector);
|
|
580
|
-
this.ctx.ui.requestRender();
|
|
581
|
-
attachAbort();
|
|
582
|
-
return promise;
|
|
536
|
+
return this.#presentDialog(dialogOptions?.signal, settle => {
|
|
537
|
+
const maxVisible = Math.max(4, Math.min(15, this.ctx.ui.terminal.rows - 12));
|
|
538
|
+
this.ctx.hookSelector = new HookSelectorComponent(
|
|
539
|
+
title,
|
|
540
|
+
options,
|
|
541
|
+
option => settle(option),
|
|
542
|
+
() => settle(undefined),
|
|
543
|
+
{
|
|
544
|
+
onLeft: dialogOptions?.onLeft
|
|
545
|
+
? () => {
|
|
546
|
+
dialogOptions.onLeft?.();
|
|
547
|
+
settle(undefined);
|
|
548
|
+
}
|
|
549
|
+
: undefined,
|
|
550
|
+
onRight: dialogOptions?.onRight
|
|
551
|
+
? () => {
|
|
552
|
+
dialogOptions.onRight?.();
|
|
553
|
+
settle(undefined);
|
|
554
|
+
}
|
|
555
|
+
: undefined,
|
|
556
|
+
onExternalEditor: dialogOptions?.onExternalEditor,
|
|
557
|
+
helpText: dialogOptions?.helpText,
|
|
558
|
+
initialIndex: dialogOptions?.initialIndex,
|
|
559
|
+
timeout: dialogOptions?.timeout,
|
|
560
|
+
onTimeout: dialogOptions?.onTimeout,
|
|
561
|
+
tui: this.ctx.ui,
|
|
562
|
+
outline: dialogOptions?.outline,
|
|
563
|
+
disabledIndices: dialogOptions?.disabledIndices,
|
|
564
|
+
selectionMarker: dialogOptions?.selectionMarker,
|
|
565
|
+
checkedIndices: dialogOptions?.checkedIndices,
|
|
566
|
+
markableCount: dialogOptions?.markableCount,
|
|
567
|
+
maxVisible,
|
|
568
|
+
slider: extra?.slider,
|
|
569
|
+
},
|
|
570
|
+
);
|
|
571
|
+
this.ctx.editorContainer.clear();
|
|
572
|
+
this.ctx.editorContainer.addChild(this.ctx.hookSelector);
|
|
573
|
+
this.ctx.ui.setFocus(this.ctx.hookSelector);
|
|
574
|
+
this.ctx.ui.requestRender();
|
|
575
|
+
return () => this.hideHookSelector();
|
|
576
|
+
});
|
|
583
577
|
}
|
|
584
578
|
/**
|
|
585
579
|
* Hide the hook selector.
|
|
@@ -609,33 +603,24 @@ export class ExtensionUiController {
|
|
|
609
603
|
placeholder?: string,
|
|
610
604
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
611
605
|
): Promise<string | undefined> {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
tui: this.ctx.ui,
|
|
631
|
-
},
|
|
632
|
-
);
|
|
633
|
-
this.ctx.editorContainer.clear();
|
|
634
|
-
this.ctx.editorContainer.addChild(this.ctx.hookInput);
|
|
635
|
-
this.ctx.ui.setFocus(this.ctx.hookInput);
|
|
636
|
-
this.ctx.ui.requestRender();
|
|
637
|
-
attachAbort();
|
|
638
|
-
return promise;
|
|
606
|
+
return this.#presentDialog(dialogOptions?.signal, settle => {
|
|
607
|
+
this.ctx.hookInput = new HookInputComponent(
|
|
608
|
+
title,
|
|
609
|
+
placeholder,
|
|
610
|
+
value => settle(value),
|
|
611
|
+
() => settle(undefined),
|
|
612
|
+
{
|
|
613
|
+
timeout: dialogOptions?.timeout,
|
|
614
|
+
onTimeout: dialogOptions?.onTimeout,
|
|
615
|
+
tui: this.ctx.ui,
|
|
616
|
+
},
|
|
617
|
+
);
|
|
618
|
+
this.ctx.editorContainer.clear();
|
|
619
|
+
this.ctx.editorContainer.addChild(this.ctx.hookInput);
|
|
620
|
+
this.ctx.ui.setFocus(this.ctx.hookInput);
|
|
621
|
+
this.ctx.ui.requestRender();
|
|
622
|
+
return () => this.hideHookInput();
|
|
623
|
+
});
|
|
639
624
|
}
|
|
640
625
|
|
|
641
626
|
/**
|
|
@@ -659,31 +644,21 @@ export class ExtensionUiController {
|
|
|
659
644
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
660
645
|
editorOptions?: { promptStyle?: boolean },
|
|
661
646
|
): Promise<string | undefined> {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
()
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
},
|
|
678
|
-
editorOptions,
|
|
679
|
-
);
|
|
680
|
-
|
|
681
|
-
this.ctx.editorContainer.clear();
|
|
682
|
-
this.ctx.editorContainer.addChild(this.ctx.hookEditor);
|
|
683
|
-
this.ctx.ui.setFocus(this.ctx.hookEditor);
|
|
684
|
-
this.ctx.ui.requestRender();
|
|
685
|
-
attachAbort();
|
|
686
|
-
return promise;
|
|
647
|
+
return this.#presentDialog(dialogOptions?.signal, settle => {
|
|
648
|
+
this.ctx.hookEditor = new HookEditorComponent(
|
|
649
|
+
this.ctx.ui,
|
|
650
|
+
title,
|
|
651
|
+
prefill,
|
|
652
|
+
value => settle(value),
|
|
653
|
+
() => settle(undefined),
|
|
654
|
+
editorOptions,
|
|
655
|
+
);
|
|
656
|
+
this.ctx.editorContainer.clear();
|
|
657
|
+
this.ctx.editorContainer.addChild(this.ctx.hookEditor);
|
|
658
|
+
this.ctx.ui.setFocus(this.ctx.hookEditor);
|
|
659
|
+
this.ctx.ui.requestRender();
|
|
660
|
+
return () => this.hideHookEditor();
|
|
661
|
+
});
|
|
687
662
|
}
|
|
688
663
|
|
|
689
664
|
/**
|
|
@@ -834,37 +809,78 @@ export class ExtensionUiController {
|
|
|
834
809
|
}
|
|
835
810
|
}
|
|
836
811
|
|
|
837
|
-
|
|
838
|
-
|
|
812
|
+
/**
|
|
813
|
+
* Present a modal dialog on the shared editor surface, serializing against any
|
|
814
|
+
* dialog already open. `present` builds the component, swaps it into
|
|
815
|
+
* `editorContainer`, steals focus, and returns a `hide` closure; it is invoked
|
|
816
|
+
* with a single `settle` callback that the component fires on submit/cancel.
|
|
817
|
+
*
|
|
818
|
+
* Because selector / input / editor all clear `editorContainer` and re-focus,
|
|
819
|
+
* showing a second one while the first is open would orphan the first — its
|
|
820
|
+
* promise would hang until the caller's signal aborts. So at most one dialog is
|
|
821
|
+
* presented at a time and the rest queue (FIFO). `settle` (or an abort) hides
|
|
822
|
+
* the current dialog and hands the surface to the next queued request. A request
|
|
823
|
+
* whose signal aborts before its turn resolves `undefined` and is never shown.
|
|
824
|
+
*/
|
|
825
|
+
#presentDialog(
|
|
839
826
|
signal: AbortSignal | undefined,
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
attachAbort: () => void;
|
|
844
|
-
} {
|
|
845
|
-
const { promise, resolve } = Promise.withResolvers<string | undefined>();
|
|
827
|
+
present: (settle: (value: string | undefined) => void) => () => void,
|
|
828
|
+
): Promise<string | undefined> {
|
|
829
|
+
const { promise, resolve, reject } = Promise.withResolvers<string | undefined>();
|
|
846
830
|
let settled = false;
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const
|
|
831
|
+
let started = false;
|
|
832
|
+
let hide: (() => void) | undefined;
|
|
833
|
+
|
|
834
|
+
function onAbort(): void {
|
|
835
|
+
settle(undefined);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const settle = (value: string | undefined): void => {
|
|
855
839
|
if (settled) return;
|
|
856
840
|
settled = true;
|
|
857
841
|
signal?.removeEventListener("abort", onAbort);
|
|
842
|
+
if (started) {
|
|
843
|
+
hide?.();
|
|
844
|
+
this.#dialogActive = false;
|
|
845
|
+
this.#advanceDialogQueue();
|
|
846
|
+
}
|
|
858
847
|
resolve(value);
|
|
859
848
|
};
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
if (
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
849
|
+
|
|
850
|
+
const startPresentation = (): void => {
|
|
851
|
+
if (settled) {
|
|
852
|
+
// Aborted before its turn arrived — never present, hand off the surface.
|
|
853
|
+
this.#advanceDialogQueue();
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
started = true;
|
|
857
|
+
this.#dialogActive = true;
|
|
858
|
+
try {
|
|
859
|
+
hide = present(settle);
|
|
860
|
+
} catch (error) {
|
|
861
|
+
settled = true;
|
|
862
|
+
signal?.removeEventListener("abort", onAbort);
|
|
863
|
+
this.#dialogActive = false;
|
|
864
|
+
reject(error);
|
|
865
|
+
this.#advanceDialogQueue();
|
|
866
866
|
}
|
|
867
867
|
};
|
|
868
|
-
|
|
868
|
+
|
|
869
|
+
if (signal?.aborted) {
|
|
870
|
+
resolve(undefined);
|
|
871
|
+
return promise;
|
|
872
|
+
}
|
|
873
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
874
|
+
|
|
875
|
+
if (this.#dialogActive) {
|
|
876
|
+
this.#dialogQueue.push(startPresentation);
|
|
877
|
+
} else {
|
|
878
|
+
startPresentation();
|
|
879
|
+
}
|
|
880
|
+
return promise;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
#advanceDialogQueue(): void {
|
|
884
|
+
this.#dialogQueue.shift()?.();
|
|
869
885
|
}
|
|
870
886
|
}
|
|
@@ -32,6 +32,15 @@ function isExpandable(obj: unknown): obj is Expandable {
|
|
|
32
32
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** Minimal contract for any component that can receive a paste payload directly. */
|
|
36
|
+
interface PasteTarget {
|
|
37
|
+
pasteText(text: string): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hasPasteText(value: unknown): value is PasteTarget {
|
|
41
|
+
return typeof value === "object" && value !== null && typeof (value as PasteTarget).pasteText === "function";
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
const TINY_TITLE_PROGRESS_DONE_TTL_MS = 3_000;
|
|
36
45
|
// A cached model fires its file-load events in a short burst and then goes silent
|
|
37
46
|
// while onnxruntime builds the session; a genuine download keeps streaming progress
|
|
@@ -250,10 +259,24 @@ export class InputController {
|
|
|
250
259
|
this.#enhancedPaste = new EnhancedPasteController({
|
|
251
260
|
write: data => this.ctx.ui.terminal.write(data),
|
|
252
261
|
pasteText: text => {
|
|
253
|
-
|
|
262
|
+
// Route enhanced-paste text to the currently focused component when it
|
|
263
|
+
// exposes a `pasteText` hook (modal Input prompts: OAuth API-key entry,
|
|
264
|
+
// Perplexity OTP, GitHub Enterprise URL, manual redirect URL). Falling
|
|
265
|
+
// back to the main editor would have buried the text in the detached
|
|
266
|
+
// editor while the modal Input had focus (#2127).
|
|
267
|
+
const focused = this.ctx.ui.getFocused();
|
|
268
|
+
const target = focused && focused !== this.ctx.editor && hasPasteText(focused) ? focused : this.ctx.editor;
|
|
269
|
+
target.pasteText(text);
|
|
254
270
|
this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
|
|
255
271
|
},
|
|
256
272
|
pasteImage: async image => {
|
|
273
|
+
// Images can only land in the main editor — when a modal Input is
|
|
274
|
+
// focused, refuse rather than dump the binary blob in a hidden buffer.
|
|
275
|
+
const focused = this.ctx.ui.getFocused();
|
|
276
|
+
if (focused && focused !== this.ctx.editor && hasPasteText(focused)) {
|
|
277
|
+
this.ctx.showStatus("Image paste is not supported in this prompt");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
257
280
|
await this.#normalizeAndInsertPastedImage(image, `Unsupported pasted image format: ${image.mimeType}`);
|
|
258
281
|
},
|
|
259
282
|
showStatus: message => this.ctx.showStatus(message),
|
|
@@ -397,11 +420,8 @@ export class InputController {
|
|
|
397
420
|
|
|
398
421
|
// Queue input during compaction
|
|
399
422
|
if (this.ctx.session.isCompacting) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
this.ctx.queueCompactionMessage(text, "steer");
|
|
423
|
+
const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
|
|
424
|
+
this.ctx.queueCompactionMessage(text, "steer", images);
|
|
405
425
|
return;
|
|
406
426
|
}
|
|
407
427
|
|
|
@@ -636,7 +656,8 @@ export class InputController {
|
|
|
636
656
|
// the queued entry is later re-parsed into a skill invocation is a
|
|
637
657
|
// separate concern owned by the compaction-resume path.
|
|
638
658
|
if (this.ctx.session.isCompacting) {
|
|
639
|
-
this.ctx.
|
|
659
|
+
const images = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
|
|
660
|
+
this.ctx.queueCompactionMessage(text, "followUp", images);
|
|
640
661
|
return;
|
|
641
662
|
}
|
|
642
663
|
|
|
@@ -657,11 +678,20 @@ export class InputController {
|
|
|
657
678
|
return;
|
|
658
679
|
}
|
|
659
680
|
|
|
681
|
+
// Forward any pending clipboard-pasted images alongside the queued text;
|
|
682
|
+
// otherwise the follow-up would drop the image (mirrors the Enter/steer path).
|
|
683
|
+
const images = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
|
|
684
|
+
|
|
660
685
|
if (this.ctx.session.isStreaming) {
|
|
661
686
|
this.ctx.editor.addToHistory(text);
|
|
662
687
|
this.ctx.editor.setText("");
|
|
663
|
-
|
|
664
|
-
|
|
688
|
+
this.ctx.editor.imageLinks = undefined;
|
|
689
|
+
this.ctx.pendingImages = [];
|
|
690
|
+
this.ctx.pendingImageLinks = [];
|
|
691
|
+
await this.ctx.withLocalSubmission(
|
|
692
|
+
text,
|
|
693
|
+
() => this.ctx.session.prompt(text, { streamingBehavior: "followUp", images }),
|
|
694
|
+
{ imageCount: images?.length ?? 0 },
|
|
665
695
|
);
|
|
666
696
|
this.ctx.updatePendingMessagesDisplay();
|
|
667
697
|
this.ctx.ui.requestRender();
|
|
@@ -671,7 +701,12 @@ export class InputController {
|
|
|
671
701
|
// Not streaming — just submit normally
|
|
672
702
|
this.ctx.editor.addToHistory(text);
|
|
673
703
|
this.ctx.editor.setText("");
|
|
674
|
-
|
|
704
|
+
this.ctx.editor.imageLinks = undefined;
|
|
705
|
+
this.ctx.pendingImages = [];
|
|
706
|
+
this.ctx.pendingImageLinks = [];
|
|
707
|
+
await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text, { images }), {
|
|
708
|
+
imageCount: images?.length ?? 0,
|
|
709
|
+
});
|
|
675
710
|
}
|
|
676
711
|
|
|
677
712
|
restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
|
|
@@ -717,10 +752,24 @@ export class InputController {
|
|
|
717
752
|
this.ctx.pendingImageLinks.push(imageLink);
|
|
718
753
|
this.ctx.editor.imageLinks = this.ctx.pendingImageLinks;
|
|
719
754
|
const imageNum = this.ctx.pendingImages.length;
|
|
720
|
-
this
|
|
755
|
+
const dims = await this.#imageDimensions(imageData);
|
|
756
|
+
const label = dims ? `[Image #${imageNum}, ${dims.width}x${dims.height}]` : `[Image #${imageNum}]`;
|
|
757
|
+
this.ctx.editor.insertText(`${label} `);
|
|
721
758
|
this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
|
|
722
759
|
}
|
|
723
760
|
|
|
761
|
+
/** Probe pixel dimensions for the marker label (`[Image #N, WxH]`). Returns undefined when the
|
|
762
|
+
* header can't be decoded, so the caller falls back to a bare `[Image #N]`. */
|
|
763
|
+
async #imageDimensions(image: ImageContent): Promise<{ width: number; height: number } | undefined> {
|
|
764
|
+
try {
|
|
765
|
+
const { width, height } = await new Bun.Image(Buffer.from(image.data, "base64")).metadata();
|
|
766
|
+
if (width && height) return { width, height };
|
|
767
|
+
} catch {
|
|
768
|
+
// Unknown/corrupt header — fall back to a bare label.
|
|
769
|
+
}
|
|
770
|
+
return undefined;
|
|
771
|
+
}
|
|
772
|
+
|
|
724
773
|
async #normalizeAndInsertPastedImage(image: ImageContent, unsupportedMessage: string): Promise<boolean> {
|
|
725
774
|
let imageData = await ensureSupportedImageInput(image);
|
|
726
775
|
if (!imageData) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Handles /mcp subcommands for managing MCP servers.
|
|
5
5
|
*/
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import { Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
7
|
+
import { type Component, replaceTabs, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
8
8
|
import { getMCPConfigPath, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import type { SourceMeta } from "../../capability/types";
|
|
10
10
|
import { analyzeAuthError, discoverOAuthEndpoints, MCPManager } from "../../mcp";
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import type { MCPAuthConfig, MCPServerConfig, MCPServerConnection } from "../../mcp/types";
|
|
37
37
|
import type { OAuthCredential } from "../../session/auth-storage";
|
|
38
38
|
import { shortenPath } from "../../tools/render-utils";
|
|
39
|
+
import { urlHyperlink } from "../../tui";
|
|
39
40
|
import { openPath } from "../../utils/open";
|
|
40
41
|
import { ChatBlock } from "../components/chat-block";
|
|
41
42
|
import { MCPAddWizard } from "../components/mcp-add-wizard";
|
|
@@ -51,6 +52,26 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string)
|
|
|
51
52
|
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timer));
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/** Renders the MCP OAuth fallback URL without hard-wrapping the copy target. */
|
|
56
|
+
export class MCPAuthorizationLinkPrompt implements Component {
|
|
57
|
+
readonly #url: string;
|
|
58
|
+
|
|
59
|
+
constructor(url: string) {
|
|
60
|
+
this.#url = url;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
invalidate(): void {}
|
|
64
|
+
|
|
65
|
+
render(_width: number): string[] {
|
|
66
|
+
const link = urlHyperlink(this.#url, "Click here to authorize");
|
|
67
|
+
return [
|
|
68
|
+
` ${theme.fg("success", "Open authorization URL:")}`,
|
|
69
|
+
` ${theme.fg("accent", link)}`,
|
|
70
|
+
` ${theme.fg("muted", `Copy URL: ${replaceTabs(this.#url)}`)}`,
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
54
75
|
/**
|
|
55
76
|
* Animated "Connecting to …" transcript block. Owns its spinner interval: it
|
|
56
77
|
* starts on mount and is cleared on {@link ChatBlock.finish}/dispose, so callers
|
|
@@ -610,15 +631,13 @@ export class MCPCommandController {
|
|
|
610
631
|
block.addChild(new Text(theme.fg("success", "→ Opening browser automatically..."), 1, 0));
|
|
611
632
|
block.addChild(new Spacer(1));
|
|
612
633
|
block.addChild(new Text(theme.fg("muted", "Alternative if browser did not open:"), 1, 0));
|
|
613
|
-
block.addChild(new
|
|
614
|
-
block.addChild(new Text(theme.fg("accent", info.url), 1, 0));
|
|
634
|
+
block.addChild(new MCPAuthorizationLinkPrompt(info.url));
|
|
615
635
|
this.ctx.ui.requestRender();
|
|
616
636
|
} catch (_error) {
|
|
617
637
|
// Show error if browser doesn't open
|
|
618
638
|
block.addChild(new Spacer(1));
|
|
619
639
|
block.addChild(new Text(theme.fg("warning", "→ Could not open browser automatically"), 1, 0));
|
|
620
|
-
block.addChild(new
|
|
621
|
-
block.addChild(new Text(theme.fg("accent", info.url), 1, 0));
|
|
640
|
+
block.addChild(new MCPAuthorizationLinkPrompt(info.url));
|
|
622
641
|
this.ctx.ui.requestRender();
|
|
623
642
|
}
|
|
624
643
|
},
|
|
@@ -802,7 +821,7 @@ export class MCPCommandController {
|
|
|
802
821
|
await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
|
|
803
822
|
}
|
|
804
823
|
if (state === "connected") {
|
|
805
|
-
block.setStatus(theme.fg("success",
|
|
824
|
+
block.setStatus(theme.fg("success", `${theme.status.enabled} Connected to "${name}"`));
|
|
806
825
|
} else if (state === "connecting") {
|
|
807
826
|
block.setStatus(theme.fg("muted", `◌ "${name}" is still connecting...`));
|
|
808
827
|
} else {
|
|
@@ -873,10 +892,10 @@ export class MCPCommandController {
|
|
|
873
892
|
|
|
874
893
|
// Show success message
|
|
875
894
|
const scopeLabel = scope === "user" ? "user" : "project";
|
|
876
|
-
const lines = ["", theme.fg("success",
|
|
895
|
+
const lines = ["", theme.fg("success", `+ Added server "${name}" to ${scopeLabel} config`), ""];
|
|
877
896
|
|
|
878
897
|
if (isConnected) {
|
|
879
|
-
lines.push(theme.fg("success",
|
|
898
|
+
lines.push(theme.fg("success", `${theme.status.enabled} Successfully connected to server`));
|
|
880
899
|
lines.push("");
|
|
881
900
|
} else if (isConnecting) {
|
|
882
901
|
lines.push(theme.fg("muted", `◌ Server is connecting in background...`));
|
|
@@ -1096,7 +1115,7 @@ export class MCPCommandController {
|
|
|
1096
1115
|
// Reload MCP manager
|
|
1097
1116
|
await this.#reloadMCP();
|
|
1098
1117
|
|
|
1099
|
-
this.#showMessage(["", theme.fg("success",
|
|
1118
|
+
this.#showMessage(["", theme.fg("success", `- Removed server "${name}" from ${scope} config`), ""].join("\n"));
|
|
1100
1119
|
} catch (error) {
|
|
1101
1120
|
this.ctx.showError(`Failed to remove server: ${error instanceof Error ? error.message : String(error)}`);
|
|
1102
1121
|
}
|
|
@@ -1156,7 +1175,7 @@ export class MCPCommandController {
|
|
|
1156
1175
|
|
|
1157
1176
|
const lines = [
|
|
1158
1177
|
"",
|
|
1159
|
-
theme.fg("success",
|
|
1178
|
+
theme.fg("success", `${theme.status.enabled} Successfully connected to "${name}"`),
|
|
1160
1179
|
"",
|
|
1161
1180
|
` Server: ${connection.serverInfo.name} v${connection.serverInfo.version}`,
|
|
1162
1181
|
` Tools: ${tools.length}`,
|
|
@@ -1243,12 +1262,18 @@ export class MCPCommandController {
|
|
|
1243
1262
|
? theme.fg("muted", "Connecting")
|
|
1244
1263
|
: theme.fg("warning", "Not connected yet");
|
|
1245
1264
|
this.#showMessage(
|
|
1246
|
-
[
|
|
1265
|
+
[
|
|
1266
|
+
"",
|
|
1267
|
+
theme.fg("success", `${theme.status.enabled} Enabled "${name}"`),
|
|
1268
|
+
"",
|
|
1269
|
+
` Status: ${status}`,
|
|
1270
|
+
"",
|
|
1271
|
+
].join("\n"),
|
|
1247
1272
|
);
|
|
1248
1273
|
} else {
|
|
1249
1274
|
await this.ctx.mcpManager?.disconnectServer(name);
|
|
1250
1275
|
await this.ctx.session.refreshMCPTools(this.ctx.mcpManager?.getTools() ?? []);
|
|
1251
|
-
this.#showMessage(["", theme.fg("
|
|
1276
|
+
this.#showMessage(["", theme.fg("muted", `${theme.status.disabled} Disabled "${name}"`), ""].join("\n"));
|
|
1252
1277
|
}
|
|
1253
1278
|
return;
|
|
1254
1279
|
}
|
|
@@ -1279,7 +1304,9 @@ export class MCPCommandController {
|
|
|
1279
1304
|
|
|
1280
1305
|
const lines = [
|
|
1281
1306
|
"",
|
|
1282
|
-
|
|
1307
|
+
enabled
|
|
1308
|
+
? theme.fg("success", `${theme.status.enabled} Enabled "${name}" (${found.scope} config)`)
|
|
1309
|
+
: theme.fg("muted", `${theme.status.disabled} Disabled "${name}" (${found.scope} config)`),
|
|
1283
1310
|
];
|
|
1284
1311
|
if (status) {
|
|
1285
1312
|
lines.push("");
|
|
@@ -1317,7 +1344,7 @@ export class MCPCommandController {
|
|
|
1317
1344
|
await this.#reloadMCP();
|
|
1318
1345
|
|
|
1319
1346
|
this.#showMessage(
|
|
1320
|
-
["", theme.fg("success",
|
|
1347
|
+
["", theme.fg("success", `- Cleared auth for "${name}" (${found.scope} config)`), ""].join("\n"),
|
|
1321
1348
|
);
|
|
1322
1349
|
} catch (error) {
|
|
1323
1350
|
this.ctx.showError(`Failed to clear auth: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1411,7 +1438,12 @@ export class MCPCommandController {
|
|
|
1411
1438
|
await this.#reloadMCP();
|
|
1412
1439
|
const connectedCount = this.ctx.mcpManager?.getConnectedServers().length ?? 0;
|
|
1413
1440
|
this.#showMessage(
|
|
1414
|
-
[
|
|
1441
|
+
[
|
|
1442
|
+
"",
|
|
1443
|
+
theme.fg("success", `${theme.icon.loop} MCP reload complete`),
|
|
1444
|
+
` Connected servers: ${connectedCount}`,
|
|
1445
|
+
"",
|
|
1446
|
+
].join("\n"),
|
|
1415
1447
|
);
|
|
1416
1448
|
} catch (error) {
|
|
1417
1449
|
this.ctx.showError(`Failed to reload MCP: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1442,9 +1474,12 @@ export class MCPCommandController {
|
|
|
1442
1474
|
await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
|
|
1443
1475
|
const serverTools = this.ctx.mcpManager.getTools().filter(t => t.mcpServerName === name);
|
|
1444
1476
|
this.#showMessage(
|
|
1445
|
-
[
|
|
1477
|
+
[
|
|
1446
1478
|
"\n",
|
|
1447
|
-
|
|
1479
|
+
theme.fg("success", `${theme.status.enabled} Reconnected to "${name}"`),
|
|
1480
|
+
` Tools: ${serverTools.length}`,
|
|
1481
|
+
"\n",
|
|
1482
|
+
].join("\n"),
|
|
1448
1483
|
);
|
|
1449
1484
|
} else {
|
|
1450
1485
|
this.ctx.showError(`Failed to reconnect to "${name}". Check server status and logs.`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { PASTE_CODE_LOGIN_PROVIDERS } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
4
|
+
import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
|
|
4
5
|
import type { Component, OverlayHandle } from "@oh-my-pi/pi-tui";
|
|
5
6
|
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
6
7
|
import { getAgentDbPath, getProjectDir, normalizePathForComparison } from "@oh-my-pi/pi-utils";
|
|
@@ -57,14 +58,6 @@ import type { SessionObserverRegistry } from "../session-observer-registry";
|
|
|
57
58
|
import { computeContextBreakdown } from "../utils/context-usage";
|
|
58
59
|
import { buildCopyTargets } from "../utils/copy-targets";
|
|
59
60
|
|
|
60
|
-
const CALLBACK_SERVER_PROVIDERS = new Set<OAuthProvider>([
|
|
61
|
-
"anthropic",
|
|
62
|
-
"openai-codex",
|
|
63
|
-
"gitlab-duo",
|
|
64
|
-
"google-gemini-cli",
|
|
65
|
-
"google-antigravity",
|
|
66
|
-
]);
|
|
67
|
-
|
|
68
61
|
const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
|
|
69
62
|
|
|
70
63
|
export class SelectorController {
|
|
@@ -928,7 +921,7 @@ export class SelectorController {
|
|
|
928
921
|
async #handleOAuthLogin(providerId: string): Promise<void> {
|
|
929
922
|
this.ctx.showStatus(`Logging in to ${providerId}…`);
|
|
930
923
|
const manualInput = this.ctx.oauthManualInput;
|
|
931
|
-
const useManualInput =
|
|
924
|
+
const useManualInput = PASTE_CODE_LOGIN_PROVIDERS.has(providerId);
|
|
932
925
|
try {
|
|
933
926
|
await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
|
|
934
927
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
@@ -209,7 +209,7 @@ export class SSHCommandController {
|
|
|
209
209
|
const scopeLabel = scope === "user" ? "user" : "project";
|
|
210
210
|
const lines = [
|
|
211
211
|
"",
|
|
212
|
-
theme.fg("success",
|
|
212
|
+
theme.fg("success", `+ Added SSH host "${name}" to ${scopeLabel} config`),
|
|
213
213
|
"",
|
|
214
214
|
` Host: ${host}`,
|
|
215
215
|
];
|
|
@@ -368,7 +368,7 @@ export class SSHCommandController {
|
|
|
368
368
|
await this.ctx.session.refreshSshTool();
|
|
369
369
|
|
|
370
370
|
this.#showMessage(
|
|
371
|
-
["", theme.fg("success",
|
|
371
|
+
["", theme.fg("success", `- Removed SSH host "${name}" from ${scope} config`), ""].join("\n"),
|
|
372
372
|
);
|
|
373
373
|
} catch (error) {
|
|
374
374
|
this.ctx.showError(`Failed to remove host: ${error instanceof Error ? error.message : String(error)}`);
|