@oh-my-pi/pi-coding-agent 15.10.4 → 15.10.5
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 +52 -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/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/image-references.d.ts +8 -3
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -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/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 -0
- 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/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 +1 -1
- package/src/cli/list-models.ts +5 -0
- package/src/cli/update-cli.ts +138 -16
- 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/render.ts +1 -1
- 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 +6 -5
- 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/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/extension-ui-controller.ts +143 -127
- package/src/modes/controllers/input-controller.ts +36 -10
- package/src/modes/controllers/mcp-command-controller.ts +28 -12
- 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 +2 -2
- 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 +2 -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 +213 -155
- package/src/session/session-manager.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/system-prompt.ts +15 -9
- package/src/task/render.ts +20 -8
- 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 -0
- 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/image-loading.ts +31 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/render.ts +14 -6
|
@@ -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
|
}
|
|
@@ -397,11 +397,8 @@ export class InputController {
|
|
|
397
397
|
|
|
398
398
|
// Queue input during compaction
|
|
399
399
|
if (this.ctx.session.isCompacting) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
this.ctx.queueCompactionMessage(text, "steer");
|
|
400
|
+
const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
|
|
401
|
+
this.ctx.queueCompactionMessage(text, "steer", images);
|
|
405
402
|
return;
|
|
406
403
|
}
|
|
407
404
|
|
|
@@ -636,7 +633,8 @@ export class InputController {
|
|
|
636
633
|
// the queued entry is later re-parsed into a skill invocation is a
|
|
637
634
|
// separate concern owned by the compaction-resume path.
|
|
638
635
|
if (this.ctx.session.isCompacting) {
|
|
639
|
-
this.ctx.
|
|
636
|
+
const images = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
|
|
637
|
+
this.ctx.queueCompactionMessage(text, "followUp", images);
|
|
640
638
|
return;
|
|
641
639
|
}
|
|
642
640
|
|
|
@@ -657,11 +655,20 @@ export class InputController {
|
|
|
657
655
|
return;
|
|
658
656
|
}
|
|
659
657
|
|
|
658
|
+
// Forward any pending clipboard-pasted images alongside the queued text;
|
|
659
|
+
// otherwise the follow-up would drop the image (mirrors the Enter/steer path).
|
|
660
|
+
const images = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
|
|
661
|
+
|
|
660
662
|
if (this.ctx.session.isStreaming) {
|
|
661
663
|
this.ctx.editor.addToHistory(text);
|
|
662
664
|
this.ctx.editor.setText("");
|
|
663
|
-
|
|
664
|
-
|
|
665
|
+
this.ctx.editor.imageLinks = undefined;
|
|
666
|
+
this.ctx.pendingImages = [];
|
|
667
|
+
this.ctx.pendingImageLinks = [];
|
|
668
|
+
await this.ctx.withLocalSubmission(
|
|
669
|
+
text,
|
|
670
|
+
() => this.ctx.session.prompt(text, { streamingBehavior: "followUp", images }),
|
|
671
|
+
{ imageCount: images?.length ?? 0 },
|
|
665
672
|
);
|
|
666
673
|
this.ctx.updatePendingMessagesDisplay();
|
|
667
674
|
this.ctx.ui.requestRender();
|
|
@@ -671,7 +678,12 @@ export class InputController {
|
|
|
671
678
|
// Not streaming — just submit normally
|
|
672
679
|
this.ctx.editor.addToHistory(text);
|
|
673
680
|
this.ctx.editor.setText("");
|
|
674
|
-
|
|
681
|
+
this.ctx.editor.imageLinks = undefined;
|
|
682
|
+
this.ctx.pendingImages = [];
|
|
683
|
+
this.ctx.pendingImageLinks = [];
|
|
684
|
+
await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text, { images }), {
|
|
685
|
+
imageCount: images?.length ?? 0,
|
|
686
|
+
});
|
|
675
687
|
}
|
|
676
688
|
|
|
677
689
|
restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
|
|
@@ -717,10 +729,24 @@ export class InputController {
|
|
|
717
729
|
this.ctx.pendingImageLinks.push(imageLink);
|
|
718
730
|
this.ctx.editor.imageLinks = this.ctx.pendingImageLinks;
|
|
719
731
|
const imageNum = this.ctx.pendingImages.length;
|
|
720
|
-
this
|
|
732
|
+
const dims = await this.#imageDimensions(imageData);
|
|
733
|
+
const label = dims ? `[Image #${imageNum}, ${dims.width}x${dims.height}]` : `[Image #${imageNum}]`;
|
|
734
|
+
this.ctx.editor.insertText(`${label} `);
|
|
721
735
|
this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
|
|
722
736
|
}
|
|
723
737
|
|
|
738
|
+
/** Probe pixel dimensions for the marker label (`[Image #N, WxH]`). Returns undefined when the
|
|
739
|
+
* header can't be decoded, so the caller falls back to a bare `[Image #N]`. */
|
|
740
|
+
async #imageDimensions(image: ImageContent): Promise<{ width: number; height: number } | undefined> {
|
|
741
|
+
try {
|
|
742
|
+
const { width, height } = await new Bun.Image(Buffer.from(image.data, "base64")).metadata();
|
|
743
|
+
if (width && height) return { width, height };
|
|
744
|
+
} catch {
|
|
745
|
+
// Unknown/corrupt header — fall back to a bare label.
|
|
746
|
+
}
|
|
747
|
+
return undefined;
|
|
748
|
+
}
|
|
749
|
+
|
|
724
750
|
async #normalizeAndInsertPastedImage(image: ImageContent, unsupportedMessage: string): Promise<boolean> {
|
|
725
751
|
let imageData = await ensureSupportedImageInput(image);
|
|
726
752
|
if (!imageData) {
|
|
@@ -802,7 +802,7 @@ export class MCPCommandController {
|
|
|
802
802
|
await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
|
|
803
803
|
}
|
|
804
804
|
if (state === "connected") {
|
|
805
|
-
block.setStatus(theme.fg("success",
|
|
805
|
+
block.setStatus(theme.fg("success", `${theme.status.enabled} Connected to "${name}"`));
|
|
806
806
|
} else if (state === "connecting") {
|
|
807
807
|
block.setStatus(theme.fg("muted", `◌ "${name}" is still connecting...`));
|
|
808
808
|
} else {
|
|
@@ -873,10 +873,10 @@ export class MCPCommandController {
|
|
|
873
873
|
|
|
874
874
|
// Show success message
|
|
875
875
|
const scopeLabel = scope === "user" ? "user" : "project";
|
|
876
|
-
const lines = ["", theme.fg("success",
|
|
876
|
+
const lines = ["", theme.fg("success", `+ Added server "${name}" to ${scopeLabel} config`), ""];
|
|
877
877
|
|
|
878
878
|
if (isConnected) {
|
|
879
|
-
lines.push(theme.fg("success",
|
|
879
|
+
lines.push(theme.fg("success", `${theme.status.enabled} Successfully connected to server`));
|
|
880
880
|
lines.push("");
|
|
881
881
|
} else if (isConnecting) {
|
|
882
882
|
lines.push(theme.fg("muted", `◌ Server is connecting in background...`));
|
|
@@ -1096,7 +1096,7 @@ export class MCPCommandController {
|
|
|
1096
1096
|
// Reload MCP manager
|
|
1097
1097
|
await this.#reloadMCP();
|
|
1098
1098
|
|
|
1099
|
-
this.#showMessage(["", theme.fg("success",
|
|
1099
|
+
this.#showMessage(["", theme.fg("success", `- Removed server "${name}" from ${scope} config`), ""].join("\n"));
|
|
1100
1100
|
} catch (error) {
|
|
1101
1101
|
this.ctx.showError(`Failed to remove server: ${error instanceof Error ? error.message : String(error)}`);
|
|
1102
1102
|
}
|
|
@@ -1156,7 +1156,7 @@ export class MCPCommandController {
|
|
|
1156
1156
|
|
|
1157
1157
|
const lines = [
|
|
1158
1158
|
"",
|
|
1159
|
-
theme.fg("success",
|
|
1159
|
+
theme.fg("success", `${theme.status.enabled} Successfully connected to "${name}"`),
|
|
1160
1160
|
"",
|
|
1161
1161
|
` Server: ${connection.serverInfo.name} v${connection.serverInfo.version}`,
|
|
1162
1162
|
` Tools: ${tools.length}`,
|
|
@@ -1243,12 +1243,18 @@ export class MCPCommandController {
|
|
|
1243
1243
|
? theme.fg("muted", "Connecting")
|
|
1244
1244
|
: theme.fg("warning", "Not connected yet");
|
|
1245
1245
|
this.#showMessage(
|
|
1246
|
-
[
|
|
1246
|
+
[
|
|
1247
|
+
"",
|
|
1248
|
+
theme.fg("success", `${theme.status.enabled} Enabled "${name}"`),
|
|
1249
|
+
"",
|
|
1250
|
+
` Status: ${status}`,
|
|
1251
|
+
"",
|
|
1252
|
+
].join("\n"),
|
|
1247
1253
|
);
|
|
1248
1254
|
} else {
|
|
1249
1255
|
await this.ctx.mcpManager?.disconnectServer(name);
|
|
1250
1256
|
await this.ctx.session.refreshMCPTools(this.ctx.mcpManager?.getTools() ?? []);
|
|
1251
|
-
this.#showMessage(["", theme.fg("
|
|
1257
|
+
this.#showMessage(["", theme.fg("muted", `${theme.status.disabled} Disabled "${name}"`), ""].join("\n"));
|
|
1252
1258
|
}
|
|
1253
1259
|
return;
|
|
1254
1260
|
}
|
|
@@ -1279,7 +1285,9 @@ export class MCPCommandController {
|
|
|
1279
1285
|
|
|
1280
1286
|
const lines = [
|
|
1281
1287
|
"",
|
|
1282
|
-
|
|
1288
|
+
enabled
|
|
1289
|
+
? theme.fg("success", `${theme.status.enabled} Enabled "${name}" (${found.scope} config)`)
|
|
1290
|
+
: theme.fg("muted", `${theme.status.disabled} Disabled "${name}" (${found.scope} config)`),
|
|
1283
1291
|
];
|
|
1284
1292
|
if (status) {
|
|
1285
1293
|
lines.push("");
|
|
@@ -1317,7 +1325,7 @@ export class MCPCommandController {
|
|
|
1317
1325
|
await this.#reloadMCP();
|
|
1318
1326
|
|
|
1319
1327
|
this.#showMessage(
|
|
1320
|
-
["", theme.fg("success",
|
|
1328
|
+
["", theme.fg("success", `- Cleared auth for "${name}" (${found.scope} config)`), ""].join("\n"),
|
|
1321
1329
|
);
|
|
1322
1330
|
} catch (error) {
|
|
1323
1331
|
this.ctx.showError(`Failed to clear auth: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1411,7 +1419,12 @@ export class MCPCommandController {
|
|
|
1411
1419
|
await this.#reloadMCP();
|
|
1412
1420
|
const connectedCount = this.ctx.mcpManager?.getConnectedServers().length ?? 0;
|
|
1413
1421
|
this.#showMessage(
|
|
1414
|
-
[
|
|
1422
|
+
[
|
|
1423
|
+
"",
|
|
1424
|
+
theme.fg("success", `${theme.icon.loop} MCP reload complete`),
|
|
1425
|
+
` Connected servers: ${connectedCount}`,
|
|
1426
|
+
"",
|
|
1427
|
+
].join("\n"),
|
|
1415
1428
|
);
|
|
1416
1429
|
} catch (error) {
|
|
1417
1430
|
this.ctx.showError(`Failed to reload MCP: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1442,9 +1455,12 @@ export class MCPCommandController {
|
|
|
1442
1455
|
await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
|
|
1443
1456
|
const serverTools = this.ctx.mcpManager.getTools().filter(t => t.mcpServerName === name);
|
|
1444
1457
|
this.#showMessage(
|
|
1445
|
-
[
|
|
1458
|
+
[
|
|
1446
1459
|
"\n",
|
|
1447
|
-
|
|
1460
|
+
theme.fg("success", `${theme.status.enabled} Reconnected to "${name}"`),
|
|
1461
|
+
` Tools: ${serverTools.length}`,
|
|
1462
|
+
"\n",
|
|
1463
|
+
].join("\n"),
|
|
1448
1464
|
);
|
|
1449
1465
|
} else {
|
|
1450
1466
|
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)}`);
|
|
@@ -3,30 +3,36 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
3
3
|
import { type BlobPutResult, blobExtensionForImageMimeType } from "../session/blob-store";
|
|
4
4
|
import { fileHyperlink } from "../tui/hyperlink";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** Matches `[Image #N]`/`[Image #N, WxH]` and `[Paste #N, +X lines]`/`[Paste #N, Y chars]` tokens.
|
|
7
|
+
* Group 1 is the kind (`Image`/`Paste`), group 2 the 1-based index. The optional metadata
|
|
8
|
+
* tail (`, …`) is captured loosely (no `]`/newline) so future label tweaks keep matching. */
|
|
9
|
+
export const PLACEHOLDER_REGEX = /\[(Image|Paste) #([1-9]\d*)(?:,[^\]\n]*)?\]/g;
|
|
7
10
|
|
|
8
11
|
type ImageBlobWriter = (data: Buffer, options?: { extension?: string }) => Promise<BlobPutResult>;
|
|
9
12
|
type ImageBlobWriterSync = (data: Buffer, options?: { extension?: string }) => BlobPutResult;
|
|
10
13
|
|
|
11
|
-
export
|
|
14
|
+
export type PlaceholderKind = "image" | "paste";
|
|
15
|
+
|
|
16
|
+
export interface PlaceholderRenderers {
|
|
12
17
|
renderText: (text: string) => string;
|
|
13
|
-
renderReference: (label: string, index: number) => string;
|
|
18
|
+
renderReference: (label: string, kind: PlaceholderKind, index: number) => string;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
export function
|
|
17
|
-
|
|
21
|
+
export function renderPlaceholders(text: string, renderers: PlaceholderRenderers): string {
|
|
22
|
+
PLACEHOLDER_REGEX.lastIndex = 0;
|
|
18
23
|
let result = "";
|
|
19
24
|
let last = 0;
|
|
20
25
|
let matched = false;
|
|
21
26
|
|
|
22
27
|
for (;;) {
|
|
23
|
-
const match =
|
|
28
|
+
const match = PLACEHOLDER_REGEX.exec(text);
|
|
24
29
|
if (match === null) break;
|
|
25
30
|
matched = true;
|
|
26
31
|
if (match.index > last) {
|
|
27
32
|
result += renderers.renderText(text.slice(last, match.index));
|
|
28
33
|
}
|
|
29
|
-
|
|
34
|
+
const kind: PlaceholderKind = match[1] === "Paste" ? "paste" : "image";
|
|
35
|
+
result += renderers.renderReference(match[0], kind, Number(match[2]));
|
|
30
36
|
last = match.index + match[0].length;
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -2806,8 +2806,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2806
2806
|
this.#uiHelpers.updatePendingMessagesDisplay();
|
|
2807
2807
|
}
|
|
2808
2808
|
|
|
2809
|
-
queueCompactionMessage(text: string, mode: "steer" | "followUp"): void {
|
|
2810
|
-
this.#uiHelpers.queueCompactionMessage(text, mode);
|
|
2809
|
+
queueCompactionMessage(text: string, mode: "steer" | "followUp", images?: ImageContent[]): void {
|
|
2810
|
+
this.#uiHelpers.queueCompactionMessage(text, mode, images);
|
|
2811
2811
|
}
|
|
2812
2812
|
|
|
2813
2813
|
flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void> {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - Events: AgentSessionEvent objects streamed as they occur
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
|
-
import { getOAuthProviders } from "@oh-my-pi/pi-ai/
|
|
13
|
+
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
14
14
|
import { $env, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
|
|
15
15
|
import {
|
|
16
16
|
type ExtensionUIContext,
|
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import
|
|
2
|
+
import { PASTE_CODE_LOGIN_PROVIDERS } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
|
|
3
4
|
import { Input, matchesKey, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
|
4
5
|
import { getAgentDbPath } from "@oh-my-pi/pi-utils";
|
|
5
6
|
import { OAuthSelectorComponent } from "../../components/oauth-selector";
|
|
6
7
|
import { theme } from "../../theme/theme";
|
|
7
8
|
import type { SetupSceneHost, SetupTab } from "./types";
|
|
8
9
|
|
|
9
|
-
/** Providers whose OAuth flow needs a pasted code/redirect URL rather than a callback server. */
|
|
10
|
-
const CALLBACK_SERVER_PROVIDERS: Partial<Record<OAuthProvider, true>> = {
|
|
11
|
-
anthropic: true,
|
|
12
|
-
"openai-codex": true,
|
|
13
|
-
"gitlab-duo": true,
|
|
14
|
-
"google-gemini-cli": true,
|
|
15
|
-
"google-antigravity": true,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
10
|
function loginUrlLink(url: string): string {
|
|
19
11
|
return `\x1b]8;;${url}\x07Open login URL\x1b]8;;\x07`;
|
|
20
12
|
}
|
|
@@ -119,7 +111,7 @@ export class SignInTab implements SetupTab {
|
|
|
119
111
|
|
|
120
112
|
async #login(providerId: string): Promise<void> {
|
|
121
113
|
if (this.#loggingInProvider || this.#disposed) return;
|
|
122
|
-
const useManualInput =
|
|
114
|
+
const useManualInput = PASTE_CODE_LOGIN_PROVIDERS.has(providerId);
|
|
123
115
|
this.#selector.stopValidation();
|
|
124
116
|
this.#loggingInProvider = providerId;
|
|
125
117
|
this.#statusLines = [theme.fg("dim", "Starting OAuth flow…")];
|