@oh-my-pi/pi-coding-agent 16.4.4 → 16.4.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 +62 -0
- package/dist/cli.js +3799 -3729
- package/dist/types/async/job-manager.d.ts +8 -0
- package/dist/types/cli/bench-cli.d.ts +1 -7
- package/dist/types/cli/usage-cli.d.ts +1 -0
- package/dist/types/commands/usage.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +19 -9
- package/dist/types/config/settings.d.ts +3 -2
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +36 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +1 -0
- package/dist/types/modes/components/ask-dialog.d.ts +27 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -8
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/model-browser.d.ts +100 -0
- package/dist/types/modes/components/model-hub.d.ts +52 -0
- package/dist/types/modes/components/pause-screen.d.ts +43 -0
- package/dist/types/modes/components/session-selector.d.ts +13 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/queue-input.d.ts +8 -0
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/agent-storage.d.ts +57 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -13
- package/dist/types/task/index.d.ts +12 -11
- package/dist/types/task/label.d.ts +4 -0
- package/dist/types/task/repair-args.d.ts +8 -8
- package/dist/types/task/types.d.ts +31 -56
- package/dist/types/tools/ask.d.ts +12 -0
- package/dist/types/tools/conflict-detect.d.ts +17 -1
- package/dist/types/tools/job.d.ts +16 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +0 -1
- package/scripts/compile-binary.ts +4 -3
- package/src/async/job-manager.ts +9 -0
- package/src/cli/bench-cli.ts +7 -26
- package/src/cli/usage-cli.ts +11 -0
- package/src/commands/usage.ts +13 -2
- package/src/commit/agentic/tools/analyze-file.ts +2 -3
- package/src/config/settings-schema.ts +18 -7
- package/src/config/settings.ts +13 -4
- package/src/discovery/helpers.ts +3 -4
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/extensions/types.ts +46 -0
- package/src/irc/bus.ts +61 -20
- package/src/modes/components/__tests__/pause-screen.test.ts +143 -0
- package/src/modes/components/advisor-config.ts +32 -22
- package/src/modes/components/ask-dialog.ts +888 -0
- package/src/modes/components/custom-editor.test.ts +58 -1
- package/src/modes/components/custom-editor.ts +42 -11
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/model-browser.ts +769 -0
- package/src/modes/components/model-hub.ts +2002 -0
- package/src/modes/components/pause-screen.ts +208 -0
- package/src/modes/components/session-selector.ts +299 -42
- package/src/modes/components/tool-execution.ts +2 -0
- package/src/modes/components/usage-row.ts +5 -6
- package/src/modes/controllers/event-controller.ts +8 -2
- package/src/modes/controllers/extension-ui-controller.ts +252 -5
- package/src/modes/controllers/input-controller.ts +140 -6
- package/src/modes/controllers/selector-controller.ts +160 -97
- package/src/modes/controllers/tan-command-controller.ts +1 -1
- package/src/modes/controllers/todo-command-controller.ts +1 -2
- package/src/modes/interactive-mode.ts +8 -0
- package/src/modes/queue-input.ts +132 -0
- package/src/modes/shared.ts +1 -1
- package/src/modes/theme/theme.ts +3 -3
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +50 -24
- package/src/prompts/agents/scout.md +0 -1
- package/src/prompts/agents/task.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +1 -5
- package/src/prompts/system/subagent-yield-reminder.md +10 -0
- package/src/prompts/system/task-label.md +23 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/task-summary.md +3 -0
- package/src/prompts/tools/task.md +17 -18
- package/src/session/agent-session.ts +186 -49
- package/src/session/agent-storage.ts +330 -3
- package/src/session/history-storage.ts +1 -34
- package/src/session/session-context.test.ts +73 -0
- package/src/session/session-context.ts +43 -26
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +159 -46
- package/src/task/index.ts +377 -239
- package/src/task/label.ts +38 -0
- package/src/task/render.ts +74 -22
- package/src/task/repair-args.ts +20 -31
- package/src/task/spawn-policy.test.ts +4 -4
- package/src/task/types.ts +46 -66
- package/src/tools/ask.ts +233 -40
- package/src/tools/conflict-detect.ts +102 -5
- package/src/tools/index.ts +1 -0
- package/src/tools/irc.ts +20 -11
- package/src/tools/job.ts +158 -18
- package/src/tools/write.ts +70 -6
- package/src/vibe/runtime.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +30 -13
- package/dist/types/modes/components/model-selector.d.ts +0 -37
- package/dist/types/tools/bash-command-fixup.d.ts +0 -3
- package/src/modes/components/model-selector.ts +0 -1291
- package/src/tools/bash-command-fixup.ts +0 -4
|
@@ -943,12 +943,18 @@ export class EventController {
|
|
|
943
943
|
if (component) {
|
|
944
944
|
const asyncState = (event.partialResult.details as { async?: { state?: string } } | undefined)?.async?.state;
|
|
945
945
|
const isFinalAsyncState = asyncState === "completed" || asyncState === "failed";
|
|
946
|
+
// A final async snapshot is terminal only for a parked background
|
|
947
|
+
// block (the call already returned and was kept alive for its jobs).
|
|
948
|
+
// While the call is still executing — a mixed blocking+async task
|
|
949
|
+
// call whose jobs settle before its blocking subset — treat it as a
|
|
950
|
+
// partial frame: `tool_execution_end` still owns the terminal result.
|
|
951
|
+
const isTerminal = isFinalAsyncState && this.#backgroundToolCallIds.has(event.toolCallId);
|
|
946
952
|
component.updateResult(
|
|
947
953
|
{ ...event.partialResult, isError: asyncState === "failed" },
|
|
948
|
-
!
|
|
954
|
+
!isTerminal,
|
|
949
955
|
event.toolCallId,
|
|
950
956
|
);
|
|
951
|
-
if (
|
|
957
|
+
if (isTerminal) {
|
|
952
958
|
this.ctx.pendingTools.delete(event.toolCallId);
|
|
953
959
|
this.#backgroundToolCallIds.delete(event.toolCallId);
|
|
954
960
|
}
|
|
@@ -5,6 +5,9 @@ import { KeybindingsManager } from "../../config/keybindings";
|
|
|
5
5
|
import type {
|
|
6
6
|
CompactOptions,
|
|
7
7
|
ExtensionActions,
|
|
8
|
+
ExtensionAskDialogQuestion,
|
|
9
|
+
ExtensionAskDialogResult,
|
|
10
|
+
ExtensionAskDialogResultItem,
|
|
8
11
|
ExtensionCommandContextActions,
|
|
9
12
|
ExtensionContextActions,
|
|
10
13
|
ExtensionError,
|
|
@@ -19,6 +22,7 @@ import type {
|
|
|
19
22
|
} from "../../extensibility/extensions";
|
|
20
23
|
import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
|
|
21
24
|
import { createExtensionModelQuery } from "../../extensibility/extensions/model-api";
|
|
25
|
+
import { AskDialogComponent, boundPromptTitle } from "../../modes/components/ask-dialog";
|
|
22
26
|
import { HookEditorComponent } from "../../modes/components/hook-editor";
|
|
23
27
|
import { HookInputComponent } from "../../modes/components/hook-input";
|
|
24
28
|
import { HookSelectorComponent, type HookSelectorSlider } from "../../modes/components/hook-selector";
|
|
@@ -28,12 +32,26 @@ import { normalizeCustomMessagePayload, USER_INTERRUPT_LABEL } from "../../sessi
|
|
|
28
32
|
import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
29
33
|
|
|
30
34
|
const MAX_WIDGET_LINES = 10;
|
|
35
|
+
const ASK_OTHER_OPTION = "Other (type your own)";
|
|
36
|
+
const ASK_CHAT_OPTION = "Chat about this";
|
|
37
|
+
const ASK_NEXT_OPTION = "Next →";
|
|
31
38
|
|
|
32
39
|
interface CollabDialogWinner {
|
|
33
40
|
source: "local" | "remote";
|
|
34
41
|
value: string | undefined;
|
|
35
42
|
}
|
|
36
43
|
|
|
44
|
+
interface CollabAskDialogWinner {
|
|
45
|
+
source: "local" | "remote";
|
|
46
|
+
value: ExtensionAskDialogResult | undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Tagged result from a guest UI request, distinguishing a real answer (even
|
|
49
|
+
* one whose literal value is "unavailable"), an explicit guest cancel, and a
|
|
50
|
+
* transport-unavailable sentinel (collab teardown / abort). Replaces the old
|
|
51
|
+
* `string | "unavailable" | undefined` channel that let a guest answer of
|
|
52
|
+
* "unavailable" collide with the transport sentinel. */
|
|
53
|
+
type GuestUiResult = { kind: "answered"; value: string } | { kind: "cancelled" } | { kind: "unavailable" };
|
|
54
|
+
|
|
37
55
|
function toWireSelectOptions(options: ExtensionUISelectItem[]): CollabUiSelectItem[] {
|
|
38
56
|
return options.map(option =>
|
|
39
57
|
typeof option === "string"
|
|
@@ -65,6 +83,7 @@ export class ExtensionUiController {
|
|
|
65
83
|
select: (title, options, dialogOptions) => this.showCollabAwareSelector(title, options, dialogOptions),
|
|
66
84
|
confirm: (title, message, _dialogOptions) => this.showHookConfirm(title, message),
|
|
67
85
|
input: (title, placeholder, dialogOptions) => this.showHookInput(title, placeholder, dialogOptions),
|
|
86
|
+
askDialog: (questions, dialogOptions) => this.showAskDialog(questions, dialogOptions),
|
|
68
87
|
notify: (message, type) => this.showHookNotify(message, type),
|
|
69
88
|
onTerminalInput: handler => this.addExtensionTerminalInputListener(handler),
|
|
70
89
|
setStatus: (key, text) => this.setHookStatus(key, text),
|
|
@@ -558,6 +577,106 @@ export class ExtensionUiController {
|
|
|
558
577
|
);
|
|
559
578
|
}
|
|
560
579
|
|
|
580
|
+
async showAskDialog(
|
|
581
|
+
questions: ExtensionAskDialogQuestion[],
|
|
582
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
583
|
+
): Promise<ExtensionAskDialogResult | undefined> {
|
|
584
|
+
const host = this.ctx.collabHost;
|
|
585
|
+
if (!host) return this.#showLocalAskDialog(questions, dialogOptions);
|
|
586
|
+
const localAbort = new AbortController();
|
|
587
|
+
const remoteAbort = new AbortController();
|
|
588
|
+
const parentSignal = dialogOptions?.signal;
|
|
589
|
+
const localSignal = parentSignal ? AbortSignal.any([parentSignal, localAbort.signal]) : localAbort.signal;
|
|
590
|
+
const remoteSignal = parentSignal ? AbortSignal.any([parentSignal, remoteAbort.signal]) : remoteAbort.signal;
|
|
591
|
+
const localWinner = this.#showLocalAskDialog(questions, { ...dialogOptions, signal: localSignal }).then(
|
|
592
|
+
(value): CollabAskDialogWinner => ({ source: "local", value }),
|
|
593
|
+
);
|
|
594
|
+
const remoteWinner: Promise<CollabAskDialogWinner> = this.#runGuestAskDialog(questions, remoteSignal).then(
|
|
595
|
+
result => (result === "unavailable" ? localWinner : { source: "remote", value: result }),
|
|
596
|
+
);
|
|
597
|
+
const winner = await Promise.race([localWinner, remoteWinner]);
|
|
598
|
+
if (winner.source === "remote") localAbort.abort();
|
|
599
|
+
else remoteAbort.abort();
|
|
600
|
+
return winner.value;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
#showLocalAskDialog(
|
|
604
|
+
questions: ExtensionAskDialogQuestion[],
|
|
605
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
606
|
+
): Promise<ExtensionAskDialogResult | undefined> {
|
|
607
|
+
return this.#presentDialog<ExtensionAskDialogResult>(dialogOptions?.signal, settle => {
|
|
608
|
+
let askDialog: AskDialogComponent | undefined;
|
|
609
|
+
let promptEditor: HookEditorComponent | undefined;
|
|
610
|
+
let promptResolve: ((value: string | undefined) => void) | undefined;
|
|
611
|
+
let closed = false;
|
|
612
|
+
|
|
613
|
+
const restoreAskDialog = (): void => {
|
|
614
|
+
if (closed || !askDialog) return;
|
|
615
|
+
this.ctx.editorContainer.clear();
|
|
616
|
+
this.ctx.editorContainer.addChild(askDialog);
|
|
617
|
+
this.ctx.ui.setFocus(askDialog);
|
|
618
|
+
this.ctx.ui.requestRender();
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const finishPrompt = (value: string | undefined): void => {
|
|
622
|
+
const resolvePrompt = promptResolve;
|
|
623
|
+
promptResolve = undefined;
|
|
624
|
+
promptEditor = undefined;
|
|
625
|
+
restoreAskDialog();
|
|
626
|
+
resolvePrompt?.(value);
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const promptForText = (title: string, prefill?: string): Promise<string | undefined> => {
|
|
630
|
+
if (closed) return Promise.resolve(undefined);
|
|
631
|
+
const { promise, resolve } = Promise.withResolvers<string | undefined>();
|
|
632
|
+
promptResolve = resolve;
|
|
633
|
+
promptEditor = new HookEditorComponent(
|
|
634
|
+
this.ctx.ui,
|
|
635
|
+
title,
|
|
636
|
+
prefill,
|
|
637
|
+
value => finishPrompt(value),
|
|
638
|
+
() => finishPrompt(undefined),
|
|
639
|
+
{ promptStyle: true },
|
|
640
|
+
);
|
|
641
|
+
this.ctx.editorContainer.clear();
|
|
642
|
+
this.ctx.editorContainer.addChild(promptEditor);
|
|
643
|
+
this.ctx.ui.setFocus(promptEditor);
|
|
644
|
+
this.ctx.ui.requestRender();
|
|
645
|
+
return promise;
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
askDialog = new AskDialogComponent(
|
|
649
|
+
questions,
|
|
650
|
+
{
|
|
651
|
+
onSubmit: result => settle(result),
|
|
652
|
+
onCancel: () => settle(undefined),
|
|
653
|
+
onPrompt: promptForText,
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
timeout: dialogOptions?.timeout,
|
|
657
|
+
onTimeout: dialogOptions?.onTimeout,
|
|
658
|
+
tui: this.ctx.ui,
|
|
659
|
+
},
|
|
660
|
+
);
|
|
661
|
+
this.ctx.editorContainer.clear();
|
|
662
|
+
this.ctx.editorContainer.addChild(askDialog);
|
|
663
|
+
this.ctx.ui.setFocus(askDialog);
|
|
664
|
+
this.ctx.ui.requestRender();
|
|
665
|
+
|
|
666
|
+
return () => {
|
|
667
|
+
closed = true;
|
|
668
|
+
askDialog?.dispose();
|
|
669
|
+
promptResolve?.(undefined);
|
|
670
|
+
promptResolve = undefined;
|
|
671
|
+
promptEditor = undefined;
|
|
672
|
+
this.ctx.editorContainer.clear();
|
|
673
|
+
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
674
|
+
this.ctx.ui.setFocus(this.ctx.editor);
|
|
675
|
+
this.ctx.ui.requestRender();
|
|
676
|
+
};
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
561
680
|
/**
|
|
562
681
|
* Race the local hook dialog against a mirrored guest ask. First *answer*
|
|
563
682
|
* wins and cancels the other side. A remote `unavailable` settlement
|
|
@@ -591,6 +710,134 @@ export class ExtensionUiController {
|
|
|
591
710
|
return winner.value;
|
|
592
711
|
}
|
|
593
712
|
|
|
713
|
+
async #runGuestAskDialog(
|
|
714
|
+
questions: ExtensionAskDialogQuestion[],
|
|
715
|
+
signal: AbortSignal,
|
|
716
|
+
): Promise<ExtensionAskDialogResult | "unavailable" | undefined> {
|
|
717
|
+
const results: ExtensionAskDialogResultItem[] = [];
|
|
718
|
+
for (const question of questions) {
|
|
719
|
+
const result = await this.#runGuestAskQuestion(question, signal);
|
|
720
|
+
if (result === "unavailable" || result === undefined) return result;
|
|
721
|
+
if (result === "chat") return { kind: "chat" };
|
|
722
|
+
results.push(result);
|
|
723
|
+
}
|
|
724
|
+
return { kind: "submit", results };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async #runGuestAskQuestion(
|
|
728
|
+
question: ExtensionAskDialogQuestion,
|
|
729
|
+
signal: AbortSignal,
|
|
730
|
+
): Promise<ExtensionAskDialogResultItem | "chat" | "unavailable" | undefined> {
|
|
731
|
+
const selected = new Set<string>();
|
|
732
|
+
let customInput: string | undefined;
|
|
733
|
+
const baseOptions: CollabUiSelectItem[] = question.options.map(option =>
|
|
734
|
+
option.description?.trim() ? { label: option.label, description: option.description.trim() } : option.label,
|
|
735
|
+
);
|
|
736
|
+
if (question.multi) {
|
|
737
|
+
while (true) {
|
|
738
|
+
const checkedIndices = question.options
|
|
739
|
+
.map((option, index) => (selected.has(option.label) ? index : -1))
|
|
740
|
+
.filter(index => index >= 0);
|
|
741
|
+
// Mirror the local dialog's Next gating: omit the Next option until
|
|
742
|
+
// at least one option is checked or a custom answer exists, so a
|
|
743
|
+
// guest cannot submit an empty multi-select result
|
|
744
|
+
// (PRRT_kwDOQxs0bc6OFbDW). The remote select has no "disabled" row
|
|
745
|
+
// concept, so we omit rather than dim it.
|
|
746
|
+
const hasAnswer = selected.size > 0 || customInput !== undefined;
|
|
747
|
+
const options = [...baseOptions, ASK_OTHER_OPTION];
|
|
748
|
+
if (hasAnswer) options.push(ASK_NEXT_OPTION);
|
|
749
|
+
options.push(ASK_CHAT_OPTION);
|
|
750
|
+
const choice = await this.#requestGuestUiString(
|
|
751
|
+
{
|
|
752
|
+
kind: "select",
|
|
753
|
+
title: question.question,
|
|
754
|
+
options,
|
|
755
|
+
selectionMarker: "checkbox",
|
|
756
|
+
checkedIndices,
|
|
757
|
+
markableCount: question.options.length,
|
|
758
|
+
helpText: hasAnswer
|
|
759
|
+
? "up/down navigate enter toggle Next → continue esc cancel"
|
|
760
|
+
: "up/down navigate enter toggle esc cancel",
|
|
761
|
+
},
|
|
762
|
+
signal,
|
|
763
|
+
);
|
|
764
|
+
if (choice.kind === "unavailable") return "unavailable";
|
|
765
|
+
if (choice.kind === "cancelled") return undefined;
|
|
766
|
+
if (choice.value === ASK_CHAT_OPTION) return "chat";
|
|
767
|
+
if (choice.value === ASK_NEXT_OPTION) break;
|
|
768
|
+
if (choice.value === ASK_OTHER_OPTION) {
|
|
769
|
+
const input = await this.#requestGuestUiString(
|
|
770
|
+
{ kind: "editor", title: boundPromptTitle("Custom answer: ", question.question) },
|
|
771
|
+
signal,
|
|
772
|
+
);
|
|
773
|
+
if (input.kind === "unavailable") return "unavailable";
|
|
774
|
+
// Guest cancelled the Other editor: keep the ask open and
|
|
775
|
+
// return to the option list instead of cancelling the whole ask.
|
|
776
|
+
if (input.kind === "cancelled") continue;
|
|
777
|
+
customInput = input.value;
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
if (selected.has(choice.value)) selected.delete(choice.value);
|
|
781
|
+
else selected.add(choice.value);
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
const recommended =
|
|
785
|
+
typeof question.recommended === "number" && Number.isInteger(question.recommended)
|
|
786
|
+
? question.recommended
|
|
787
|
+
: 0;
|
|
788
|
+
const initialIndex = Math.max(0, Math.min(recommended, Math.max(0, question.options.length - 1)));
|
|
789
|
+
while (true) {
|
|
790
|
+
const choice = await this.#requestGuestUiString(
|
|
791
|
+
{
|
|
792
|
+
kind: "select",
|
|
793
|
+
title: question.question,
|
|
794
|
+
options: [...baseOptions, ASK_OTHER_OPTION, ASK_CHAT_OPTION],
|
|
795
|
+
initialIndex,
|
|
796
|
+
selectionMarker: "radio",
|
|
797
|
+
markableCount: question.options.length,
|
|
798
|
+
helpText: "up/down navigate enter select esc cancel",
|
|
799
|
+
},
|
|
800
|
+
signal,
|
|
801
|
+
);
|
|
802
|
+
if (choice.kind === "unavailable") return "unavailable";
|
|
803
|
+
if (choice.kind === "cancelled") return undefined;
|
|
804
|
+
if (choice.value === ASK_CHAT_OPTION) return "chat";
|
|
805
|
+
if (choice.value === ASK_OTHER_OPTION) {
|
|
806
|
+
const input = await this.#requestGuestUiString(
|
|
807
|
+
{ kind: "editor", title: boundPromptTitle("Custom answer: ", question.question) },
|
|
808
|
+
signal,
|
|
809
|
+
);
|
|
810
|
+
if (input.kind === "unavailable") return "unavailable";
|
|
811
|
+
// Guest cancelled the Other editor: re-show the select list
|
|
812
|
+
// instead of cancelling the whole ask.
|
|
813
|
+
if (input.kind === "cancelled") continue;
|
|
814
|
+
customInput = input.value;
|
|
815
|
+
} else {
|
|
816
|
+
selected.add(choice.value);
|
|
817
|
+
}
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
id: question.id,
|
|
823
|
+
question: question.question,
|
|
824
|
+
options: question.options.map(option => option.label),
|
|
825
|
+
multi: question.multi ?? false,
|
|
826
|
+
selectedOptions: question.options.map(option => option.label).filter(label => selected.has(label)),
|
|
827
|
+
customInput,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async #requestGuestUiString(request: CollabUiRequestDraft, signal: AbortSignal): Promise<GuestUiResult> {
|
|
832
|
+
const host = this.ctx.collabHost;
|
|
833
|
+
if (!host) return { kind: "unavailable" };
|
|
834
|
+
const remote = host.requestGuestUi(request, signal);
|
|
835
|
+
if (!remote) return { kind: "unavailable" };
|
|
836
|
+
const result = await remote;
|
|
837
|
+
if (result.kind === "unavailable") return { kind: "unavailable" };
|
|
838
|
+
return typeof result.value === "string" ? { kind: "answered", value: result.value } : { kind: "cancelled" };
|
|
839
|
+
}
|
|
840
|
+
|
|
594
841
|
/**
|
|
595
842
|
* Show a selector for hooks.
|
|
596
843
|
*/
|
|
@@ -894,11 +1141,11 @@ export class ExtensionUiController {
|
|
|
894
1141
|
* the current dialog and hands the surface to the next queued request. A request
|
|
895
1142
|
* whose signal aborts before its turn resolves `undefined` and is never shown.
|
|
896
1143
|
*/
|
|
897
|
-
#presentDialog(
|
|
1144
|
+
#presentDialog<T = string>(
|
|
898
1145
|
signal: AbortSignal | undefined,
|
|
899
|
-
present: (settle: (value:
|
|
900
|
-
): Promise<
|
|
901
|
-
const { promise, resolve, reject } = Promise.withResolvers<
|
|
1146
|
+
present: (settle: (value: T | undefined) => void) => () => void,
|
|
1147
|
+
): Promise<T | undefined> {
|
|
1148
|
+
const { promise, resolve, reject } = Promise.withResolvers<T | undefined>();
|
|
902
1149
|
let settled = false;
|
|
903
1150
|
let started = false;
|
|
904
1151
|
let hide: (() => void) | undefined;
|
|
@@ -907,7 +1154,7 @@ export class ExtensionUiController {
|
|
|
907
1154
|
settle(undefined);
|
|
908
1155
|
}
|
|
909
1156
|
|
|
910
|
-
const settle = (value:
|
|
1157
|
+
const settle = (value: T | undefined): void => {
|
|
911
1158
|
if (settled) return;
|
|
912
1159
|
settled = true;
|
|
913
1160
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -13,6 +13,7 @@ import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-
|
|
|
13
13
|
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
14
14
|
import { materializeImageReferenceLinks, shiftImageMarkers } from "../../modes/image-references";
|
|
15
15
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
16
|
+
import { parseQueueShorthand, splitQueuedMessages } from "../../modes/queue-input";
|
|
16
17
|
import { invokeSkillCommandFromText, isKnownSkillCommand } from "../../modes/skill-command";
|
|
17
18
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
18
19
|
import manualContinuePrompt from "../../prompts/system/manual-continue.md" with { type: "text" };
|
|
@@ -266,6 +267,8 @@ export class InputController {
|
|
|
266
267
|
});
|
|
267
268
|
}
|
|
268
269
|
this.ctx.editor.onEscape = () => {
|
|
270
|
+
// Side-channel panels are the topmost view. Esc dismisses them before
|
|
271
|
+
// touching loop mode, maintenance, or the underlying main turn.
|
|
269
272
|
// Active context maintenance owns Esc: auto/manual compaction,
|
|
270
273
|
// handoff generation, and auto-retry backoff all advertise
|
|
271
274
|
// "(esc to cancel)". Dispatch on live session state instead of
|
|
@@ -281,6 +284,13 @@ export class InputController {
|
|
|
281
284
|
// (see EventController). Main-session maintenance still owns Esc and
|
|
282
285
|
// stays cancellable from the main view (focused submit gates /compact
|
|
283
286
|
// and handoff, so manual maintenance is main-only anyway).
|
|
287
|
+
if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (this.ctx.hasActiveOmfg() && this.ctx.handleOmfgEscape()) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
284
294
|
if (!this.ctx.focusedAgentId) {
|
|
285
295
|
const viewSession = this.ctx.viewSession;
|
|
286
296
|
let aborted = false;
|
|
@@ -308,12 +318,6 @@ export class InputController {
|
|
|
308
318
|
}
|
|
309
319
|
return;
|
|
310
320
|
}
|
|
311
|
-
if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
if (this.ctx.hasActiveOmfg() && this.ctx.handleOmfgEscape()) {
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
321
|
if (this.ctx.focusedAgentId) {
|
|
318
322
|
// Esc never interrupts the focused agent's turn: clear typed text,
|
|
319
323
|
// else return the view to the main session. Interrupt via empty
|
|
@@ -647,6 +651,16 @@ export class InputController {
|
|
|
647
651
|
|
|
648
652
|
if (!text && !hasInputImages) return;
|
|
649
653
|
|
|
654
|
+
const queueBody = parseQueueShorthand(text);
|
|
655
|
+
if (queueBody !== undefined) {
|
|
656
|
+
await this.#queueForYield(queueBody, {
|
|
657
|
+
historyText: text,
|
|
658
|
+
images: inputImages,
|
|
659
|
+
imageLinks: inputImageLinks,
|
|
660
|
+
});
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
650
664
|
// Handle built-in slash commands
|
|
651
665
|
if (text) {
|
|
652
666
|
const slashResult = await executeBuiltinSlashCommand(text, {
|
|
@@ -1114,6 +1128,126 @@ export class InputController {
|
|
|
1114
1128
|
}
|
|
1115
1129
|
}
|
|
1116
1130
|
|
|
1131
|
+
/** Queue `/queue` input behind an active turn, or start it immediately when idle. */
|
|
1132
|
+
async handleQueueCommand(text: string): Promise<void> {
|
|
1133
|
+
const images = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
|
|
1134
|
+
const imageLinks =
|
|
1135
|
+
images && this.ctx.editor.pendingImageLinks.length > 0 ? [...this.ctx.editor.pendingImageLinks] : undefined;
|
|
1136
|
+
await this.#queueForYield(text, { images, imageLinks });
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
async #queueForYield(
|
|
1140
|
+
text: string,
|
|
1141
|
+
options: {
|
|
1142
|
+
historyText?: string;
|
|
1143
|
+
images?: ImageContent[];
|
|
1144
|
+
imageLinks?: (string | undefined)[];
|
|
1145
|
+
},
|
|
1146
|
+
): Promise<void> {
|
|
1147
|
+
const splitMessages = splitQueuedMessages(text);
|
|
1148
|
+
if (splitMessages.length === 0 && !options.images?.length) {
|
|
1149
|
+
this.ctx.editor.clearDraft();
|
|
1150
|
+
this.ctx.showWarning("Usage: /queue <message> (or start a prompt with -> / =>)");
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const messages = splitMessages.length > 0 ? splitMessages : [""];
|
|
1155
|
+
const originalDraft = this.ctx.editor.getText();
|
|
1156
|
+
const images = options.images?.length ? [...options.images] : undefined;
|
|
1157
|
+
const imageLinks = options.imageLinks
|
|
1158
|
+
? [...options.imageLinks]
|
|
1159
|
+
: images
|
|
1160
|
+
? images.map(() => undefined)
|
|
1161
|
+
: undefined;
|
|
1162
|
+
this.ctx.editor.clearDraft(options.historyText);
|
|
1163
|
+
|
|
1164
|
+
if (this.ctx.session.isCompacting) {
|
|
1165
|
+
for (let index = 0; index < messages.length; index++) {
|
|
1166
|
+
this.ctx.compactionQueuedMessages.push({
|
|
1167
|
+
text: messages[index] ?? "",
|
|
1168
|
+
mode: "followUp",
|
|
1169
|
+
images: index === 0 ? images : undefined,
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
1173
|
+
this.ctx.showStatus(
|
|
1174
|
+
messages.length === 1
|
|
1175
|
+
? "Queued message for after compaction"
|
|
1176
|
+
: `Queued ${messages.length} messages for after compaction`,
|
|
1177
|
+
);
|
|
1178
|
+
this.ctx.ui.requestRender();
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const startImmediately = !this.ctx.session.isStreaming && this.ctx.session.queuedMessageCount === 0;
|
|
1183
|
+
let queuedCount = 0;
|
|
1184
|
+
try {
|
|
1185
|
+
if (startImmediately && this.ctx.onInputCallback) {
|
|
1186
|
+
const first = messages[0] ?? "";
|
|
1187
|
+
const submission = this.ctx.startPendingSubmission({
|
|
1188
|
+
text: first,
|
|
1189
|
+
images,
|
|
1190
|
+
imageLinks,
|
|
1191
|
+
streamingBehavior: "followUp",
|
|
1192
|
+
});
|
|
1193
|
+
this.ctx.onInputCallback(submission);
|
|
1194
|
+
queuedCount = 1;
|
|
1195
|
+
}
|
|
1196
|
+
while (queuedCount < messages.length) {
|
|
1197
|
+
const message = messages[queuedCount] ?? "";
|
|
1198
|
+
const queuedImages = queuedCount === 0 ? images : undefined;
|
|
1199
|
+
await this.ctx.withLocalSubmission(
|
|
1200
|
+
message,
|
|
1201
|
+
async () => {
|
|
1202
|
+
if (startImmediately && queuedCount === 0) {
|
|
1203
|
+
await this.ctx.session.prompt(message, {
|
|
1204
|
+
images: queuedImages,
|
|
1205
|
+
streamingBehavior: "followUp",
|
|
1206
|
+
});
|
|
1207
|
+
} else {
|
|
1208
|
+
await this.ctx.session.followUp(message, queuedImages);
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1211
|
+
{ imageCount: queuedImages?.length ?? 0 },
|
|
1212
|
+
);
|
|
1213
|
+
queuedCount++;
|
|
1214
|
+
}
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
if (queuedCount === 0) {
|
|
1217
|
+
this.ctx.editor.setText(originalDraft);
|
|
1218
|
+
if (images) {
|
|
1219
|
+
this.ctx.editor.pendingImages = images;
|
|
1220
|
+
this.ctx.editor.pendingImageLinks = imageLinks ?? images.map(() => undefined);
|
|
1221
|
+
this.ctx.editor.imageLinks = this.ctx.editor.pendingImageLinks;
|
|
1222
|
+
}
|
|
1223
|
+
} else {
|
|
1224
|
+
const remaining = messages.slice(queuedCount);
|
|
1225
|
+
const restored =
|
|
1226
|
+
remaining.length === 1
|
|
1227
|
+
? `=> ${remaining[0]}`
|
|
1228
|
+
: `=>\n${remaining
|
|
1229
|
+
.map((message, index) => `${index + 1}. ${message.replaceAll("\n", "\n ")}`)
|
|
1230
|
+
.join("\n")}`;
|
|
1231
|
+
this.ctx.editor.setText(restored);
|
|
1232
|
+
}
|
|
1233
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
1237
|
+
if (queuedCount === messages.length) {
|
|
1238
|
+
this.ctx.showStatus(
|
|
1239
|
+
startImmediately
|
|
1240
|
+
? queuedCount === 1
|
|
1241
|
+
? "Sent queued message"
|
|
1242
|
+
: `Sent first message; queued ${queuedCount - 1} for later yields`
|
|
1243
|
+
: queuedCount === 1
|
|
1244
|
+
? "Queued message for when the agent yields"
|
|
1245
|
+
: `Queued ${queuedCount} messages for when the agent yields`,
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
this.ctx.ui.requestRender();
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1117
1251
|
/** Send editor text as a follow-up message (queued behind current stream). */
|
|
1118
1252
|
async handleFollowUp(): Promise<void> {
|
|
1119
1253
|
let text = this.ctx.editor.getExpandedText().trim();
|