@oh-my-pi/pi-coding-agent 16.1.19 → 16.1.21
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 +25 -0
- package/dist/cli.js +3795 -3760
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/cli/gallery-cli.d.ts +6 -0
- package/dist/types/commands/gallery.d.ts +1 -1
- package/dist/types/config/service-tier.d.ts +34 -0
- package/dist/types/config/settings-schema.d.ts +36 -33
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +42 -5
- package/dist/types/modes/components/custom-editor.d.ts +32 -0
- package/dist/types/modes/components/tool-execution.d.ts +5 -5
- package/dist/types/modes/controllers/event-controller.d.ts +10 -0
- package/dist/types/modes/controllers/input-controller.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/task/executor.d.ts +9 -1
- package/dist/types/task/parallel.d.ts +17 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/utils/clipboard.d.ts +10 -0
- package/package.json +13 -13
- package/scripts/generate-legacy-pi-bundled-registry.ts +10 -0
- package/src/advisor/__tests__/advisor.test.ts +44 -0
- package/src/advisor/advise-tool.ts +33 -0
- package/src/autolearn/controller.ts +17 -2
- package/src/cli/gallery-cli.ts +31 -2
- package/src/cli/gallery-fixtures/agentic.ts +13 -4
- package/src/commands/gallery.ts +11 -3
- package/src/config/service-tier.ts +87 -0
- package/src/config/settings-schema.ts +48 -23
- package/src/eval/agent-bridge.ts +2 -0
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +1 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +4 -4
- package/src/extensibility/plugins/legacy-pi-compat.ts +193 -5
- package/src/main.ts +1 -0
- package/src/mcp/manager.ts +12 -3
- package/src/mcp/oauth-flow.ts +121 -7
- package/src/mcp/transports/stdio.ts +5 -0
- package/src/modes/components/chat-transcript-builder.ts +31 -0
- package/src/modes/components/custom-editor.test.ts +80 -0
- package/src/modes/components/custom-editor.ts +86 -6
- package/src/modes/components/tool-execution.ts +50 -25
- package/src/modes/controllers/event-controller.ts +57 -8
- package/src/modes/controllers/input-controller.ts +213 -93
- package/src/modes/controllers/mcp-command-controller.ts +18 -2
- package/src/modes/utils/ui-helpers.ts +40 -0
- package/src/prompts/system/autolearn-nudge-autocontinue.md +5 -0
- package/src/prompts/system/autolearn-nudge.md +4 -2
- package/src/prompts/tools/todo.md +1 -1
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +76 -15
- package/src/session/session-history-format.ts +21 -4
- package/src/task/executor.ts +79 -2
- package/src/task/index.ts +11 -6
- package/src/task/parallel.ts +59 -7
- package/src/tools/index.ts +3 -1
- package/src/tools/irc.ts +16 -2
- package/src/tools/todo.ts +20 -10
- package/src/utils/clipboard.ts +57 -0
- package/src/utils/shell-snapshot-fn-env.sh +60 -0
- package/src/utils/shell-snapshot.ts +77 -20
|
@@ -6,6 +6,7 @@ import { $env, isEnoent, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
|
6
6
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
7
7
|
import { resolveLocalRoot } from "../../internal-urls";
|
|
8
8
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
9
|
+
import { extractImagePathFromText } from "../../modes/components/custom-editor";
|
|
9
10
|
import { renderSegmentTrack } from "../../modes/components/segment-track";
|
|
10
11
|
import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
|
|
11
12
|
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
@@ -20,7 +21,12 @@ import { isLowSignalTitleInput } from "../../tiny/text";
|
|
|
20
21
|
import { tinyTitleClient } from "../../tiny/title-client";
|
|
21
22
|
import type { TinyTitleProgressEvent } from "../../tiny/title-protocol";
|
|
22
23
|
import { shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
|
|
23
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
copyToClipboard,
|
|
26
|
+
readImageFromClipboard,
|
|
27
|
+
readMacFileUrlsFromClipboard,
|
|
28
|
+
readTextFromClipboard,
|
|
29
|
+
} from "../../utils/clipboard";
|
|
24
30
|
import { EnhancedPasteController } from "../../utils/enhanced-paste";
|
|
25
31
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
26
32
|
import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
|
|
@@ -126,7 +132,12 @@ export class InputController {
|
|
|
126
132
|
private clipboard: {
|
|
127
133
|
readImage: typeof readImageFromClipboard;
|
|
128
134
|
readText: typeof readTextFromClipboard;
|
|
129
|
-
|
|
135
|
+
readMacFileUrls?: typeof readMacFileUrlsFromClipboard;
|
|
136
|
+
} = {
|
|
137
|
+
readImage: readImageFromClipboard,
|
|
138
|
+
readText: readTextFromClipboard,
|
|
139
|
+
readMacFileUrls: readMacFileUrlsFromClipboard,
|
|
140
|
+
},
|
|
130
141
|
) {}
|
|
131
142
|
|
|
132
143
|
#enhancedPaste?: EnhancedPasteController;
|
|
@@ -534,6 +545,7 @@ export class InputController {
|
|
|
534
545
|
setupEditorSubmitHandler(): void {
|
|
535
546
|
this.ctx.editor.onSubmit = async (text: string) => {
|
|
536
547
|
text = text.trim();
|
|
548
|
+
const hasPendingImages = this.ctx.editor.pendingImages.length > 0;
|
|
537
549
|
if ((!isSettingsInitialized() || settings.get("emojiAutocomplete")) && text) text = expandEmoticons(text);
|
|
538
550
|
|
|
539
551
|
// Focused subagent session: the editor is a plain chat box for it.
|
|
@@ -546,7 +558,7 @@ export class InputController {
|
|
|
546
558
|
|
|
547
559
|
// Empty submit while streaming with queued messages: abort the active
|
|
548
560
|
// turn and let the post-unwind drain deliver the agent-core queue.
|
|
549
|
-
if (!text && this.ctx.session.isStreaming) {
|
|
561
|
+
if (!text && !hasPendingImages && this.ctx.session.isStreaming) {
|
|
550
562
|
if (this.ctx.session.queuedMessageCount > 0) {
|
|
551
563
|
const aborting = this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
552
564
|
await aborting;
|
|
@@ -556,7 +568,7 @@ export class InputController {
|
|
|
556
568
|
return;
|
|
557
569
|
}
|
|
558
570
|
|
|
559
|
-
if (!text) return;
|
|
571
|
+
if (!text && !hasPendingImages) return;
|
|
560
572
|
|
|
561
573
|
// Continue shortcuts: "." or "c" resume the agent with a hidden agent-authored
|
|
562
574
|
// developer directive (no visible user message) instead of an empty turn, so the
|
|
@@ -579,6 +591,7 @@ export class InputController {
|
|
|
579
591
|
let inputImages = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
|
|
580
592
|
let inputImageLinks =
|
|
581
593
|
this.ctx.editor.pendingImageLinks.length > 0 ? [...this.ctx.editor.pendingImageLinks] : undefined;
|
|
594
|
+
let hasInputImages = (inputImages?.length ?? 0) > 0;
|
|
582
595
|
|
|
583
596
|
if (runner?.hasHandlers("input")) {
|
|
584
597
|
const result = await runner.emitInput(text, inputImages, "interactive");
|
|
@@ -596,24 +609,27 @@ export class InputController {
|
|
|
596
609
|
this.ctx.sessionManager.putBlob.bind(this.ctx.sessionManager),
|
|
597
610
|
);
|
|
598
611
|
}
|
|
612
|
+
hasInputImages = (inputImages?.length ?? 0) > 0;
|
|
599
613
|
}
|
|
600
614
|
|
|
601
|
-
if (!text) return;
|
|
615
|
+
if (!text && !hasInputImages) return;
|
|
602
616
|
|
|
603
617
|
// Handle built-in slash commands
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
618
|
+
if (text) {
|
|
619
|
+
const slashResult = await executeBuiltinSlashCommand(text, {
|
|
620
|
+
ctx: this.ctx,
|
|
621
|
+
});
|
|
622
|
+
if (slashResult === true) {
|
|
623
|
+
if (!shouldSkipHistory(text)) this.ctx.editor.addToHistory(text);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (typeof slashResult === "string") {
|
|
627
|
+
// Command handled but returned remaining text to use as prompt.
|
|
628
|
+
// Record the original slash command text so Up Arrow recalls
|
|
629
|
+
// "/loop 10 fix bug" rather than just "fix bug".
|
|
630
|
+
if (!shouldSkipHistory(text)) this.ctx.editor.addToHistory(text);
|
|
631
|
+
text = slashResult;
|
|
632
|
+
}
|
|
617
633
|
}
|
|
618
634
|
|
|
619
635
|
// Collab guest: prompts execute on the host; local slash/skill/bash/
|
|
@@ -647,7 +663,7 @@ export class InputController {
|
|
|
647
663
|
// free-text Enter semantics applied a few lines below at the streaming
|
|
648
664
|
// branch). Ctrl+Enter routes through `handleFollowUp` and dispatches the
|
|
649
665
|
// same helper with `"followUp"`.
|
|
650
|
-
if (await this.#invokeSkillCommand(text, "steer")) {
|
|
666
|
+
if (text && (await this.#invokeSkillCommand(text, "steer"))) {
|
|
651
667
|
return;
|
|
652
668
|
}
|
|
653
669
|
|
|
@@ -714,11 +730,25 @@ export class InputController {
|
|
|
714
730
|
// (a user-role `message_start` event) leaves any draft the user has
|
|
715
731
|
// typed since queuing intact. Same protection as #783, applied to
|
|
716
732
|
// the streaming/queue path.
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
733
|
+
try {
|
|
734
|
+
await this.ctx.withLocalSubmission(
|
|
735
|
+
text,
|
|
736
|
+
() => this.ctx.session.prompt(text, { streamingBehavior: "steer", images }),
|
|
737
|
+
{ imageCount: images?.length ?? 0 },
|
|
738
|
+
);
|
|
739
|
+
} catch (error) {
|
|
740
|
+
// Don't lose the queued steer draft: restore text and images so
|
|
741
|
+
// the user can retry after dispatch validation/queue failures.
|
|
742
|
+
this.ctx.editor.setText(text);
|
|
743
|
+
if (images && images.length > 0) {
|
|
744
|
+
this.ctx.editor.pendingImages = [...images];
|
|
745
|
+
this.ctx.editor.pendingImageLinks = inputImageLinks
|
|
746
|
+
? [...inputImageLinks]
|
|
747
|
+
: images.map(() => undefined);
|
|
748
|
+
this.ctx.editor.imageLinks = this.ctx.editor.pendingImageLinks;
|
|
749
|
+
}
|
|
750
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
751
|
+
}
|
|
722
752
|
this.ctx.updatePendingMessagesDisplay();
|
|
723
753
|
this.ctx.ui.requestRender();
|
|
724
754
|
return;
|
|
@@ -833,7 +863,10 @@ export class InputController {
|
|
|
833
863
|
/** Submit editor text to the focused subagent session (chat-only focus policy). */
|
|
834
864
|
async #submitToFocusedSession(text: string, streamingBehavior: "steer" | "followUp"): Promise<void> {
|
|
835
865
|
const target = this.ctx.viewSession;
|
|
836
|
-
|
|
866
|
+
const images = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
|
|
867
|
+
const imageLinks =
|
|
868
|
+
images && this.ctx.editor.pendingImageLinks.length > 0 ? [...this.ctx.editor.pendingImageLinks] : undefined;
|
|
869
|
+
if (!text && !images) {
|
|
837
870
|
if (target.isStreaming && target.queuedMessageCount > 0) {
|
|
838
871
|
const aborting = target.abort({ reason: USER_INTERRUPT_LABEL });
|
|
839
872
|
await aborting;
|
|
@@ -842,11 +875,10 @@ export class InputController {
|
|
|
842
875
|
}
|
|
843
876
|
return;
|
|
844
877
|
}
|
|
845
|
-
if (text.startsWith("/") || text.startsWith("!") || parsePythonCommandInput(text)) {
|
|
878
|
+
if (text && (text.startsWith("/") || text.startsWith("!") || parsePythonCommandInput(text))) {
|
|
846
879
|
this.ctx.showStatus("Commands run in the main session — press ←← to return first");
|
|
847
880
|
return; // editor text not cleared: Editor does not auto-clear on submit
|
|
848
881
|
}
|
|
849
|
-
const images = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
|
|
850
882
|
this.ctx.editor.clearDraft(text);
|
|
851
883
|
try {
|
|
852
884
|
// prompt() handles idle (new turn) and streaming (queues per streamingBehavior).
|
|
@@ -854,7 +886,14 @@ export class InputController {
|
|
|
854
886
|
imageCount: images?.length ?? 0,
|
|
855
887
|
});
|
|
856
888
|
} catch (error) {
|
|
857
|
-
|
|
889
|
+
// Hand the message back, mirroring the main submit error path: restore
|
|
890
|
+
// pasted images so the user can retry an image-only or text+image draft.
|
|
891
|
+
this.ctx.editor.setText(text);
|
|
892
|
+
if (images && images.length > 0) {
|
|
893
|
+
this.ctx.editor.pendingImages = [...images];
|
|
894
|
+
this.ctx.editor.pendingImageLinks = imageLinks ? [...imageLinks] : images.map(() => undefined);
|
|
895
|
+
this.ctx.editor.imageLinks = this.ctx.editor.pendingImageLinks;
|
|
896
|
+
}
|
|
858
897
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
859
898
|
}
|
|
860
899
|
this.ctx.updatePendingMessagesDisplay();
|
|
@@ -902,19 +941,19 @@ export class InputController {
|
|
|
902
941
|
}
|
|
903
942
|
|
|
904
943
|
handleCtrlZ(): void {
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
//
|
|
944
|
+
// Job-control suspend is POSIX-only: on Windows `process.kill(_, "SIGSTOP")`
|
|
945
|
+
// throws `TypeError: Unknown signal: SIGSTOP` and takes the whole agent down
|
|
946
|
+
// via an uncaught exception (issue #2036, originally for SIGTSTP — same
|
|
947
|
+
// shape for SIGSTOP). No-op on platforms that cannot suspend.
|
|
909
948
|
if (process.platform === "win32") {
|
|
910
949
|
this.ctx.showStatus("Suspend (Ctrl+Z) is not supported on this platform");
|
|
911
950
|
return;
|
|
912
951
|
}
|
|
913
952
|
|
|
914
|
-
// Capture the listener so we can detach it if the signal never
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
953
|
+
// Capture the listener so we can detach it if the signal never fires;
|
|
954
|
+
// otherwise a failed suspend would leave a stale SIGCONT handler that
|
|
955
|
+
// fires on the next unrelated continue and tries to re-`start()` an
|
|
956
|
+
// already-running TUI.
|
|
918
957
|
const onResume = (): void => {
|
|
919
958
|
this.ctx.ui.start();
|
|
920
959
|
this.ctx.ui.requestRender(true);
|
|
@@ -926,14 +965,41 @@ export class InputController {
|
|
|
926
965
|
this.ctx.ui.stop();
|
|
927
966
|
|
|
928
967
|
try {
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
|
|
968
|
+
// SIGSTOP — not SIGTSTP — to the foreground process group (pid=0).
|
|
969
|
+
//
|
|
970
|
+
// SIGTSTP: brush-core (the embedded shell behind every bash tool call)
|
|
971
|
+
// installs a tokio SIGTSTP listener on `Process::wait` to detect when
|
|
972
|
+
// its children have been stopped (`crates/brush-core-vendored/src/sys/
|
|
973
|
+
// unix/signal.rs::tstp_signal_listener` → `tokio::signal::unix::
|
|
974
|
+
// signal(SIGTSTP)`). Per tokio's documented contract, the first call
|
|
975
|
+
// for a given SignalKind permanently replaces the kernel-default
|
|
976
|
+
// handler for the lifetime of the process. So once the user has
|
|
977
|
+
// issued even one bash command — e.g. `/usr/bin/true` — SIGTSTP no
|
|
978
|
+
// longer stops omp: tokio swallows it and the TUI ends up torn down
|
|
979
|
+
// while the process keeps running with no live terminal (issue
|
|
980
|
+
// [#3461]). SIGSTOP cannot be caught, blocked, or ignored, so the
|
|
981
|
+
// kernel stops the process regardless of installed handlers.
|
|
982
|
+
//
|
|
983
|
+
// pid=0 (foreground process group, not just our PID): omp is not
|
|
984
|
+
// always the shell's direct child. Package-manager launchers (`npx`,
|
|
985
|
+
// `pnpm exec`, `bunx`, …) wait on the real CLI from a parent shim
|
|
986
|
+
// that shares omp's process group, and a `omp … | tee log` style
|
|
987
|
+
// pipeline puts a sibling foreground job member in the same group
|
|
988
|
+
// too. The shell sees the job as stopped only when its direct
|
|
989
|
+
// child / pipeline leader is stopped, so suspending only our PID
|
|
990
|
+
// leaves wrappers and pipeline peers running and the terminal
|
|
991
|
+
// hung — exactly the failure shape we're fixing. Stopping the whole
|
|
992
|
+
// group keeps the shell's job-control view consistent. Long-lived
|
|
993
|
+
// children that must survive the suspend (MCP stdio servers via
|
|
994
|
+
// the `detached: true` spawn in `mcp/transports/stdio.ts`, every
|
|
995
|
+
// brush external command via brush's per-child `setsid` in
|
|
996
|
+
// `crates/brush-core-vendored/src/commands.rs`) are already in
|
|
997
|
+
// their own sessions, so pgid=0 does not reach them.
|
|
998
|
+
process.kill(0, "SIGSTOP");
|
|
932
999
|
} catch (err) {
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
// on a frozen prompt.
|
|
1000
|
+
// The runtime refused the signal (e.g. seccomp filter blocks SIGSTOP
|
|
1001
|
+
// delivery to the process group). Tear the resume hook down and
|
|
1002
|
+
// bring the TUI back so the user is not stranded on a frozen prompt.
|
|
937
1003
|
process.removeListener("SIGCONT", onResume);
|
|
938
1004
|
this.ctx.ui.start();
|
|
939
1005
|
this.ctx.ui.requestRender(true);
|
|
@@ -1024,7 +1090,10 @@ export class InputController {
|
|
|
1024
1090
|
/** Send editor text as a follow-up message (queued behind current stream). */
|
|
1025
1091
|
async handleFollowUp(): Promise<void> {
|
|
1026
1092
|
let text = this.ctx.editor.getText().trim();
|
|
1027
|
-
|
|
1093
|
+
const images = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
|
|
1094
|
+
const imageLinks =
|
|
1095
|
+
images && this.ctx.editor.pendingImageLinks.length > 0 ? [...this.ctx.editor.pendingImageLinks] : undefined;
|
|
1096
|
+
if (!text && !images) return;
|
|
1028
1097
|
|
|
1029
1098
|
// Focused subagent session: follow-ups go to it; non-chat input is gated.
|
|
1030
1099
|
if (this.ctx.focusedAgentId) {
|
|
@@ -1044,38 +1113,53 @@ export class InputController {
|
|
|
1044
1113
|
return;
|
|
1045
1114
|
}
|
|
1046
1115
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1116
|
+
if (text) {
|
|
1117
|
+
const slashResult = await executeBuiltinSlashCommand(text, {
|
|
1118
|
+
ctx: this.ctx,
|
|
1119
|
+
});
|
|
1120
|
+
if (slashResult === true) {
|
|
1121
|
+
if (!shouldSkipHistory(text)) this.ctx.editor.addToHistory(text);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (typeof slashResult === "string") {
|
|
1125
|
+
// Command handled but returned remaining text to use as prompt.
|
|
1126
|
+
// Record the original slash command text so Up Arrow recalls it.
|
|
1127
|
+
if (!shouldSkipHistory(text)) this.ctx.editor.addToHistory(text);
|
|
1128
|
+
text = slashResult;
|
|
1129
|
+
}
|
|
1059
1130
|
}
|
|
1060
1131
|
|
|
1061
1132
|
// Skill commands invoke through the custom-message path regardless of
|
|
1062
1133
|
// which keybinding submitted them. Enter routes them as `steer`;
|
|
1063
1134
|
// Ctrl+Enter (this handler) routes them as `followUp`.
|
|
1064
|
-
if (await this.#invokeSkillCommand(text, "followUp")) {
|
|
1135
|
+
if (text && (await this.#invokeSkillCommand(text, "followUp"))) {
|
|
1065
1136
|
return;
|
|
1066
1137
|
}
|
|
1067
1138
|
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1070
|
-
|
|
1139
|
+
// Hand the message back on dispatch failure (model/API-key validation,
|
|
1140
|
+
// queue rejection): restore both text AND pending images so an image-only
|
|
1141
|
+
// or text+image draft can be retried, mirroring the main submit error path.
|
|
1142
|
+
const restoreOnError = (error: unknown) => {
|
|
1143
|
+
this.ctx.editor.setText(text);
|
|
1144
|
+
if (images && images.length > 0) {
|
|
1145
|
+
this.ctx.editor.pendingImages = [...images];
|
|
1146
|
+
this.ctx.editor.pendingImageLinks = imageLinks ? [...imageLinks] : images.map(() => undefined);
|
|
1147
|
+
this.ctx.editor.imageLinks = this.ctx.editor.pendingImageLinks;
|
|
1148
|
+
}
|
|
1149
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
1150
|
+
};
|
|
1071
1151
|
|
|
1072
1152
|
if (this.ctx.session.isStreaming) {
|
|
1073
1153
|
this.ctx.editor.clearDraft(text);
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1154
|
+
try {
|
|
1155
|
+
await this.ctx.withLocalSubmission(
|
|
1156
|
+
text,
|
|
1157
|
+
() => this.ctx.session.prompt(text, { streamingBehavior: "followUp", images }),
|
|
1158
|
+
{ imageCount: images?.length ?? 0 },
|
|
1159
|
+
);
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
restoreOnError(error);
|
|
1162
|
+
}
|
|
1079
1163
|
this.ctx.updatePendingMessagesDisplay();
|
|
1080
1164
|
this.ctx.ui.requestRender();
|
|
1081
1165
|
return;
|
|
@@ -1083,9 +1167,13 @@ export class InputController {
|
|
|
1083
1167
|
|
|
1084
1168
|
// Not streaming — just submit normally
|
|
1085
1169
|
this.ctx.editor.clearDraft(text);
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1170
|
+
try {
|
|
1171
|
+
await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text, { images }), {
|
|
1172
|
+
imageCount: images?.length ?? 0,
|
|
1173
|
+
});
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
restoreOnError(error);
|
|
1176
|
+
}
|
|
1089
1177
|
}
|
|
1090
1178
|
|
|
1091
1179
|
restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
|
|
@@ -1309,33 +1397,65 @@ export class InputController {
|
|
|
1309
1397
|
async handleImagePaste(): Promise<boolean> {
|
|
1310
1398
|
try {
|
|
1311
1399
|
const image = await this.clipboard.readImage();
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1400
|
+
if (image) {
|
|
1401
|
+
return await this.#normalizeAndInsertPastedImage(
|
|
1402
|
+
{
|
|
1403
|
+
type: "image",
|
|
1404
|
+
data: image.data.toBase64(),
|
|
1405
|
+
mimeType: image.mimeType,
|
|
1406
|
+
},
|
|
1407
|
+
`Unsupported clipboard image format: ${image.mimeType}`,
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
// #3506: macOS Finder `Cmd+C` puts only a `public.file-url`
|
|
1411
|
+
// representation on the pasteboard. `pbpaste` (the backing call
|
|
1412
|
+
// for `readText` on Darwin) only surfaces plain text / RTF / EPS,
|
|
1413
|
+
// so it returns empty for file-url-only pasteboards — the smart
|
|
1414
|
+
// text fallback below would dead-end with "Clipboard is empty".
|
|
1415
|
+
// Reach the file URL directly via AppleScript and route every
|
|
1416
|
+
// image-shaped path through {@link handleImagePathPaste}, matching
|
|
1417
|
+
// the bracketed-paste handler in `CustomEditor.handleInput` which
|
|
1418
|
+
// iterates every extracted image path. Multi-image Finder
|
|
1419
|
+
// selections must not silently drop after the first attach.
|
|
1420
|
+
// `readMacFileUrls` returns an empty list off Darwin, so the
|
|
1421
|
+
// check is free on every other platform.
|
|
1422
|
+
const fileUrls = (await this.clipboard.readMacFileUrls?.()) ?? [];
|
|
1423
|
+
let attachedFromFileUrls = false;
|
|
1424
|
+
for (const url of fileUrls) {
|
|
1425
|
+
const candidate = extractImagePathFromText(url);
|
|
1426
|
+
if (!candidate) continue;
|
|
1427
|
+
await this.handleImagePathPaste(candidate);
|
|
1428
|
+
attachedFromFileUrls = true;
|
|
1429
|
+
}
|
|
1430
|
+
if (attachedFromFileUrls) return true;
|
|
1431
|
+
// Smart paste (#1628): no image on the clipboard — fall back to
|
|
1432
|
+
// pasting its text so the same chord covers both payload kinds.
|
|
1433
|
+
// Hosts that pre-empt the terminal's own paste (VS Code's
|
|
1434
|
+
// integrated terminal, Win+V clipboard history) deliver only
|
|
1435
|
+
// this keypress, so a miss here must not dead-end.
|
|
1436
|
+
const text = await this.clipboard.readText();
|
|
1437
|
+
if (!text) {
|
|
1438
|
+
this.ctx.showStatus("Clipboard is empty");
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
// #3506: when the clipboard text is an explicit image file path,
|
|
1442
|
+
// route through {@link handleImagePathPaste} so the image is
|
|
1443
|
+
// loaded and attached instead of pasting the path as literal
|
|
1444
|
+
// text. Covers terminals that paste the Finder file path as
|
|
1445
|
+
// plain text rather than as a `public.file-url` (most macOS
|
|
1446
|
+
// terminals do this for image clipboards).
|
|
1447
|
+
const imagePath = extractImagePathFromText(text);
|
|
1448
|
+
if (imagePath) {
|
|
1449
|
+
await this.handleImagePathPaste(imagePath);
|
|
1329
1450
|
return true;
|
|
1330
1451
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
);
|
|
1452
|
+
// Route to the focused component when it accepts pastes (modal
|
|
1453
|
+
// Input prompts), matching the enhanced-paste text path (#2127).
|
|
1454
|
+
const focused = this.ctx.ui.getFocused();
|
|
1455
|
+
const target = focused && focused !== this.ctx.editor && hasPasteText(focused) ? focused : this.ctx.editor;
|
|
1456
|
+
target.pasteText(text);
|
|
1457
|
+
this.ctx.ui.requestRender();
|
|
1458
|
+
return true;
|
|
1339
1459
|
} catch {
|
|
1340
1460
|
this.ctx.showStatus("Failed to read clipboard");
|
|
1341
1461
|
return false;
|
|
@@ -496,6 +496,7 @@ export class MCPCommandController {
|
|
|
496
496
|
|
|
497
497
|
try {
|
|
498
498
|
const oauthResource = oauth.resource ?? finalConfig.url;
|
|
499
|
+
const oauthResourceIsFallback = !oauth.resource;
|
|
499
500
|
const oauthResult = await this.#handleOAuthFlow(
|
|
500
501
|
oauth.authorizationUrl,
|
|
501
502
|
oauth.tokenUrl,
|
|
@@ -509,11 +510,13 @@ export class MCPCommandController {
|
|
|
509
510
|
prompt: finalConfig.oauth?.prompt,
|
|
510
511
|
serverUrl: finalConfig.url,
|
|
511
512
|
resource: oauthResource,
|
|
513
|
+
stripSameOriginResource: oauthResourceIsFallback,
|
|
512
514
|
},
|
|
513
515
|
);
|
|
514
516
|
finalConfig = this.#persistOAuthResult(finalConfig, oauthResult, {
|
|
515
517
|
tokenUrl: oauth.tokenUrl,
|
|
516
518
|
resource: oauthResource,
|
|
519
|
+
stripSameOriginResource: oauthResourceIsFallback,
|
|
517
520
|
clientId: oauth.clientId,
|
|
518
521
|
userClientSecret: finalConfig.oauth?.clientSecret,
|
|
519
522
|
});
|
|
@@ -583,6 +586,7 @@ export class MCPCommandController {
|
|
|
583
586
|
prompt?: string;
|
|
584
587
|
serverUrl?: string;
|
|
585
588
|
resource?: string;
|
|
589
|
+
stripSameOriginResource?: boolean;
|
|
586
590
|
},
|
|
587
591
|
): Promise<OAuthFlowResult> {
|
|
588
592
|
const authStorage = this.ctx.session.modelRegistry.authStorage;
|
|
@@ -624,6 +628,7 @@ export class MCPCommandController {
|
|
|
624
628
|
callbackPort: opts?.callbackPort,
|
|
625
629
|
callbackPath: opts?.callbackPath,
|
|
626
630
|
resource: opts?.resource,
|
|
631
|
+
stripSameOriginResource: opts?.stripSameOriginResource,
|
|
627
632
|
},
|
|
628
633
|
{
|
|
629
634
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
@@ -711,6 +716,7 @@ export class MCPCommandController {
|
|
|
711
716
|
clientId: flow.resolvedClientId ?? resolvedClientId,
|
|
712
717
|
clientSecret: flow.registeredClientSecret ?? resolvedClientSecret,
|
|
713
718
|
resource: flow.resource,
|
|
719
|
+
authorizationUrl: flow.authorizationUrl,
|
|
714
720
|
};
|
|
715
721
|
|
|
716
722
|
await authStorage.set(credentialId, oauthCredential);
|
|
@@ -751,10 +757,17 @@ export class MCPCommandController {
|
|
|
751
757
|
#persistOAuthResult(
|
|
752
758
|
config: MCPServerConfig,
|
|
753
759
|
result: OAuthFlowResult,
|
|
754
|
-
opts: {
|
|
760
|
+
opts: {
|
|
761
|
+
tokenUrl: string;
|
|
762
|
+
resource?: string;
|
|
763
|
+
stripSameOriginResource?: boolean;
|
|
764
|
+
clientId?: string;
|
|
765
|
+
userClientSecret?: string;
|
|
766
|
+
},
|
|
755
767
|
): MCPServerConfig {
|
|
756
768
|
const clientId = result.clientId ?? opts.clientId ?? config.oauth?.clientId;
|
|
757
|
-
const resource =
|
|
769
|
+
const resource =
|
|
770
|
+
result.resource ?? (opts.stripSameOriginResource ? undefined : opts.resource) ?? config.auth?.resource;
|
|
758
771
|
return {
|
|
759
772
|
...config,
|
|
760
773
|
auth: {
|
|
@@ -1558,6 +1571,7 @@ export class MCPCommandController {
|
|
|
1558
1571
|
const currentAuthResource = currentAuth?.resource ? expandEnvVarsDeep(currentAuth.resource) : undefined;
|
|
1559
1572
|
const oauthResource =
|
|
1560
1573
|
oauth.resource ?? currentAuthResource ?? ("url" in runtimeBaseConfig ? runtimeBaseConfig.url : undefined);
|
|
1574
|
+
const oauthResourceIsFallback = !oauth.resource && !currentAuthResource;
|
|
1561
1575
|
|
|
1562
1576
|
const oauthResult = await this.#handleOAuthFlow(
|
|
1563
1577
|
oauth.authorizationUrl,
|
|
@@ -1572,6 +1586,7 @@ export class MCPCommandController {
|
|
|
1572
1586
|
prompt: found.config.oauth?.prompt,
|
|
1573
1587
|
serverUrl,
|
|
1574
1588
|
resource: oauthResource,
|
|
1589
|
+
stripSameOriginResource: oauthResourceIsFallback,
|
|
1575
1590
|
},
|
|
1576
1591
|
);
|
|
1577
1592
|
|
|
@@ -1592,6 +1607,7 @@ export class MCPCommandController {
|
|
|
1592
1607
|
clientId: oauth.clientId,
|
|
1593
1608
|
userClientSecret,
|
|
1594
1609
|
resource: oauthResource,
|
|
1610
|
+
stripSameOriginResource: oauthResourceIsFallback,
|
|
1595
1611
|
});
|
|
1596
1612
|
await updateMCPServer(found.filePath, name, updated);
|
|
1597
1613
|
}
|
|
@@ -317,6 +317,24 @@ export class UiHelpers {
|
|
|
317
317
|
// updateResult armed.
|
|
318
318
|
previous.seal();
|
|
319
319
|
};
|
|
320
|
+
let todoSnapshot: ToolExecutionComponent | null = null;
|
|
321
|
+
const resolveTodoSnapshot = (nextToolName?: string) => {
|
|
322
|
+
const previous = todoSnapshot;
|
|
323
|
+
if (!previous) return;
|
|
324
|
+
if (!previous.isDisplaceableBlock()) {
|
|
325
|
+
todoSnapshot = null;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (previous.canBeDisplacedBy(nextToolName)) {
|
|
329
|
+
todoSnapshot = null;
|
|
330
|
+
this.ctx.chatContainer.removeChild(previous);
|
|
331
|
+
previous.seal();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (nextToolName !== undefined) return;
|
|
335
|
+
todoSnapshot = null;
|
|
336
|
+
previous.seal();
|
|
337
|
+
};
|
|
320
338
|
const messages = sessionContext.messages;
|
|
321
339
|
const count = messages.length;
|
|
322
340
|
for (let i = 0; i < count; i++) {
|
|
@@ -477,11 +495,22 @@ export class UiHelpers {
|
|
|
477
495
|
component.isDisplaceableBlock()
|
|
478
496
|
) {
|
|
479
497
|
waitingPoll = component;
|
|
498
|
+
} else if (
|
|
499
|
+
message.toolName === "todo" &&
|
|
500
|
+
component instanceof ToolExecutionComponent &&
|
|
501
|
+
component.canBeDisplacedBy("todo")
|
|
502
|
+
) {
|
|
503
|
+
// A successful todo result supersedes the prior live snapshot. Failed
|
|
504
|
+
// follow-ups return false from canBeDisplacedBy("todo"), so the
|
|
505
|
+
// last-good panel stays on screen.
|
|
506
|
+
resolveTodoSnapshot("todo");
|
|
507
|
+
todoSnapshot = component;
|
|
480
508
|
}
|
|
481
509
|
}
|
|
482
510
|
} else {
|
|
483
511
|
// A user prompt closes the displacement window, same as the live path.
|
|
484
512
|
if (message.role === "user") resolveWaitingPoll();
|
|
513
|
+
if (message.role === "user") resolveTodoSnapshot();
|
|
485
514
|
// All other messages use standard rendering
|
|
486
515
|
this.ctx.addMessageToChat(message, options);
|
|
487
516
|
}
|
|
@@ -495,6 +524,17 @@ export class UiHelpers {
|
|
|
495
524
|
// A trailing waiting poll is final history on rebuild; seal it so it
|
|
496
525
|
// freezes (and its spinner timer stops) like every other block.
|
|
497
526
|
resolveWaitingPoll();
|
|
527
|
+
// A trailing todo snapshot is live state, not history: when the rebuild
|
|
528
|
+
// runs mid-turn (settings overlay close, focus attach during streaming),
|
|
529
|
+
// hand it back to the controller so a follow-up `todo` update keeps
|
|
530
|
+
// displacing instead of stacking. Idle rebuilds (resume / compaction)
|
|
531
|
+
// fall through to the seal path so the snapshot freezes as history.
|
|
532
|
+
if (todoSnapshot && this.ctx.session?.isStreaming) {
|
|
533
|
+
this.ctx.eventController?.inheritDisplaceableTodo(todoSnapshot);
|
|
534
|
+
todoSnapshot = null;
|
|
535
|
+
} else {
|
|
536
|
+
resolveTodoSnapshot();
|
|
537
|
+
}
|
|
498
538
|
|
|
499
539
|
this.ctx.pendingTools.clear();
|
|
500
540
|
this.ctx.ui.requestRender();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Automated capture turn — not a user reply. The user has not yet responded to your previous turn. Do not treat this prompt as their answer, as approval to continue, or as acceptance of any pending action; only the user can do that.
|
|
2
|
+
|
|
3
|
+
If your previous turn produced anything reusable, capture it now: a repeatable procedure becomes a managed skill (`manage_skill`); a durable fact, convention, or user preference is worth remembering (`learn`, when memory is enabled). Only capture what will genuinely help next time. If nothing is worth keeping, do nothing.
|
|
4
|
+
|
|
5
|
+
Then stop. Do not run any other tools, do not resume prior work, do not answer your own pending questions, and do not produce a continuation reply. Yield and wait for the user's next prompt.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
Hidden auto-learn reminder (not a user request). If your previous turn produced anything reusable, capture it now with your learning tools — a repeatable procedure becomes a managed skill (`manage_skill`), and a durable fact, convention, or user preference is worth remembering (`learn`, when memory is enabled).
|
|
2
2
|
|
|
3
|
-
Only capture what will genuinely help next time. If nothing
|
|
3
|
+
Only capture what will genuinely help next time. If nothing is worth keeping, do nothing.
|
|
4
|
+
|
|
5
|
+
This reminder is appended to the user's real message; answer that message normally — the capture is in addition to, not a replacement for, the work the user just asked for.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
**Tasks referenced by verbatim content string, NEVER an auto-generated ID — no "task-1"/"task-N" exists. Pass the content text in the `task` field.**
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
On each completion the earliest still-open task (in phase order) auto-promotes to `in_progress`. Completing tasks out of phase order can move this pointer **back** to an earlier phase — that is expected; completed tasks are never reverted.
|
|
4
4
|
|
|
5
5
|
## Operations
|
|
6
6
|
|
package/src/sdk.ts
CHANGED
|
@@ -1520,6 +1520,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1520
1520
|
getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
|
|
1521
1521
|
getActiveModelString,
|
|
1522
1522
|
getActiveModel: () => agent?.state.model ?? model,
|
|
1523
|
+
getServiceTier: () => session?.serviceTier,
|
|
1523
1524
|
getImageAttachments: () => session?.getImageAttachments() ?? [],
|
|
1524
1525
|
getPlanModeState: () => session?.getPlanModeState(),
|
|
1525
1526
|
getPlanReferencePath: () => session?.getPlanReferencePath() ?? "local://PLAN.md",
|