@oh-my-pi/pi-coding-agent 17.3.7 → 17.3.8
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 +60 -0
- package/dist/{CHANGELOG-1hmwwt45.md → CHANGELOG-vr9cckb4.md} +60 -0
- package/dist/cli.js +2993 -3001
- package/dist/docs-index.generated.txt +1 -1
- package/dist/{tool-views.generated-jdfmzwmn.js → tool-views.generated-dd2km5r2.js} +19 -19
- package/dist/types/advisor/advise-tool.d.ts +4 -2
- package/dist/types/cli/auth-broker-cli.d.ts +15 -0
- package/dist/types/cli/update-cli.d.ts +8 -0
- package/dist/types/cli-commands.d.ts +10 -2
- package/dist/types/config/settings-schema.d.ts +28 -0
- package/dist/types/config/settings.d.ts +9 -0
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/launch/presence.d.ts +4 -1
- package/dist/types/mcp/oauth-credentials.d.ts +23 -0
- package/dist/types/mcp/oauth-flow.d.ts +11 -0
- package/dist/types/mnemopi/backend.d.ts +12 -0
- package/dist/types/modes/components/tool-execution.d.ts +12 -0
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +25 -3
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/session/agent-session.d.ts +3 -0
- package/dist/types/session/prewalk.d.ts +4 -0
- package/dist/types/session/session-entries.d.ts +0 -1
- package/dist/types/session/session-manager.d.ts +12 -0
- package/dist/types/session/session-stats.d.ts +13 -1
- package/dist/types/session/skill-title-input.d.ts +13 -0
- package/dist/types/subprocess/worker-client.d.ts +7 -4
- package/dist/types/task/label.d.ts +2 -0
- package/dist/types/task/render.d.ts +2 -0
- package/dist/types/tiny/completion-prompt.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +6 -4
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tiny/worker.d.ts +27 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/read-format.d.ts +6 -0
- package/dist/types/tools/read-summary.d.ts +7 -1
- package/dist/types/utils/block-context.d.ts +14 -0
- package/dist/types/utils/fetch-timeout.d.ts +15 -0
- package/dist/types/utils/git.d.ts +25 -1
- package/dist/types/web/search/providers/tinyfish.d.ts +4 -0
- package/package.json +13 -13
- package/src/advisor/advise-tool.ts +5 -3
- package/src/cli/auth-broker-cli.ts +36 -1
- package/src/cli/profile-bootstrap.ts +2 -6
- package/src/cli/update-cli.ts +63 -11
- package/src/cli-commands.ts +61 -7
- package/src/commands/completions.ts +2 -1
- package/src/commit/agentic/index.ts +15 -2
- package/src/commit/git/diff.ts +6 -2
- package/src/config/model-resolver.ts +52 -6
- package/src/config/models-config.ts +2 -2
- package/src/config/settings-schema.ts +33 -0
- package/src/config/settings.ts +159 -30
- package/src/discovery/helpers.ts +45 -2
- package/src/discovery/omp-plugins.ts +2 -1
- package/src/discovery/opencode.ts +56 -3
- package/src/eval/js/process-entry.ts +4 -4
- package/src/export/html/tool-views.generated.js +19 -19
- package/src/extensibility/extensions/runner.ts +3 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +47 -0
- package/src/launch/client.ts +9 -4
- package/src/launch/presence.ts +19 -4
- package/src/lsp/defaults.json +1 -1
- package/src/mcp/manager.ts +41 -20
- package/src/mcp/oauth-credentials.ts +38 -0
- package/src/mcp/oauth-flow.ts +21 -0
- package/src/mcp/tool-bridge.ts +32 -16
- package/src/mnemopi/backend.ts +35 -3
- package/src/modes/components/model-hub.ts +37 -4
- package/src/modes/components/settings-selector.ts +17 -11
- package/src/modes/components/tool-execution.ts +97 -29
- package/src/modes/components/tree-selector.ts +7 -2
- package/src/modes/controllers/event-controller.ts +12 -2
- package/src/modes/controllers/input-controller.ts +64 -27
- package/src/modes/controllers/mcp-command-controller.ts +13 -4
- package/src/modes/interactive-mode.ts +79 -11
- package/src/modes/types.ts +11 -0
- package/src/prompts/system/memory-extraction-system.md +5 -22
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/session/agent-session.ts +52 -6
- package/src/session/messages.ts +6 -0
- package/src/session/prewalk.ts +25 -7
- package/src/session/session-entries.ts +0 -1
- package/src/session/session-maintenance.ts +10 -1
- package/src/session/session-manager.ts +15 -0
- package/src/session/session-stats.ts +24 -3
- package/src/session/settings-stream-fn.ts +7 -0
- package/src/session/skill-title-input.ts +32 -0
- package/src/session/turn-recovery.ts +23 -18
- package/src/subprocess/worker-client.ts +8 -5
- package/src/task/executor.ts +11 -0
- package/src/task/index.ts +2 -0
- package/src/task/label.ts +14 -1
- package/src/task/persisted-revive.ts +13 -0
- package/src/task/render.ts +1 -1
- package/src/task/structured-subagent.ts +5 -2
- package/src/tiny/completion-prompt.ts +16 -0
- package/src/tiny/title-client.ts +15 -6
- package/src/tiny/title-protocol.ts +8 -1
- package/src/tiny/worker.ts +21 -19
- package/src/tools/bash.ts +7 -1
- package/src/tools/read-format.ts +16 -2
- package/src/tools/read-summary.ts +9 -4
- package/src/tools/read.ts +306 -72
- package/src/utils/block-context.ts +15 -1
- package/src/utils/fetch-timeout.ts +33 -0
- package/src/utils/git.ts +54 -11
- package/src/web/search/providers/browser-page.ts +21 -3
- package/src/web/search/providers/tinyfish.ts +26 -0
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
Text,
|
|
15
15
|
type TUI,
|
|
16
16
|
} from "@oh-my-pi/pi-tui";
|
|
17
|
-
import { getProjectDir, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
17
|
+
import { getProjectDir, isRecord, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
18
18
|
import { EDIT_MODE_STRATEGIES, type EditMode, type PerFileDiffPreview } from "../../edit";
|
|
19
19
|
import type { Theme } from "../../modes/theme/theme";
|
|
20
20
|
import { getThemeEpoch, theme } from "../../modes/theme/theme";
|
|
@@ -73,6 +73,21 @@ function isTodoToolDetails(details: unknown): details is TodoToolDetails {
|
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
interface ToolImageBlock {
|
|
77
|
+
data?: string;
|
|
78
|
+
mimeType?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function imageBlocksFromDetails(details: unknown): ToolImageBlock[] {
|
|
82
|
+
if (!isRecord(details) || !Array.isArray(details.images)) return [];
|
|
83
|
+
return details.images.filter(
|
|
84
|
+
(image): image is ToolImageBlock =>
|
|
85
|
+
isRecord(image) &&
|
|
86
|
+
(image.data === undefined || typeof image.data === "string") &&
|
|
87
|
+
(image.mimeType === undefined || typeof image.mimeType === "string"),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
76
91
|
function displaceableToolName(
|
|
77
92
|
toolName: string,
|
|
78
93
|
result: { details?: unknown; isError?: boolean },
|
|
@@ -269,6 +284,52 @@ export function sharedSpinnerFrame(frameCount: number, now: number = performance
|
|
|
269
284
|
return frameCount > 0 ? Math.floor(now / SPINNER_GLYPH_ADVANCE_MS) % frameCount : 0;
|
|
270
285
|
}
|
|
271
286
|
|
|
287
|
+
/** Live tool blocks currently driving a spinner. A single shared ticker (below)
|
|
288
|
+
* advances and repaints every registered block per glyph step, so N concurrent
|
|
289
|
+
* live/streaming blocks — e.g. parallel `task` subagents — cost one 80ms timer
|
|
290
|
+
* and one coalesced render frame per tick instead of N unsynchronized timers
|
|
291
|
+
* each independently waking the render scheduler (issue #8731). */
|
|
292
|
+
const liveSpinnerBlocks = new Set<ToolExecutionComponent>();
|
|
293
|
+
let sharedSpinnerTimer: NodeJS.Timeout | undefined;
|
|
294
|
+
|
|
295
|
+
/** Arm the shared spinner ticker if it is not already running. */
|
|
296
|
+
function ensureSharedSpinnerTicker(): void {
|
|
297
|
+
if (sharedSpinnerTimer) return;
|
|
298
|
+
sharedSpinnerTimer = setInterval(() => {
|
|
299
|
+
const frame = sharedSpinnerFrame(theme.spinnerFrames.length);
|
|
300
|
+
// Deleting the current block mid-iteration (freeze path) is safe on a Set.
|
|
301
|
+
for (const block of liveSpinnerBlocks) block.tickSpinner(frame);
|
|
302
|
+
}, SPINNER_RENDER_INTERVAL_MS);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Register a live block with the shared ticker, starting it on first use. */
|
|
306
|
+
function registerSpinnerBlock(block: ToolExecutionComponent): void {
|
|
307
|
+
liveSpinnerBlocks.add(block);
|
|
308
|
+
ensureSharedSpinnerTicker();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Drop a block; stop the ticker once no live block remains. */
|
|
312
|
+
function unregisterSpinnerBlock(block: ToolExecutionComponent): void {
|
|
313
|
+
if (!liveSpinnerBlocks.delete(block)) return;
|
|
314
|
+
if (liveSpinnerBlocks.size === 0 && sharedSpinnerTimer) {
|
|
315
|
+
clearInterval(sharedSpinnerTimer);
|
|
316
|
+
sharedSpinnerTimer = undefined;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Stop the shared spinner ticker and drop every registered live block.
|
|
321
|
+
* Called on interactive-mode teardown so a stray live block cannot keep the
|
|
322
|
+
* process-wide 80ms interval alive past shutdown (lingering event-loop
|
|
323
|
+
* handles pin the process; cf. `postmortem.quit`). Test files that assert on
|
|
324
|
+
* ticker arming also use this to start from a clean slate. */
|
|
325
|
+
export function stopSharedSpinnerTicker(): void {
|
|
326
|
+
liveSpinnerBlocks.clear();
|
|
327
|
+
if (sharedSpinnerTimer) {
|
|
328
|
+
clearInterval(sharedSpinnerTimer);
|
|
329
|
+
sharedSpinnerTimer = undefined;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
272
333
|
// Stable per-instance counter so each tool execution's inline images get a
|
|
273
334
|
// graphics id that survives child re-creation (the image budget keys off it).
|
|
274
335
|
let toolExecutionInstanceSeq = 0;
|
|
@@ -337,7 +398,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
337
398
|
#convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
|
|
338
399
|
// Spinner animation for partial task results
|
|
339
400
|
#spinnerFrame?: number;
|
|
340
|
-
#
|
|
401
|
+
#spinnerActive = false;
|
|
341
402
|
// Todo write completion strikethrough reveal animation
|
|
342
403
|
#todoStrikeInterval?: NodeJS.Timeout;
|
|
343
404
|
// Track if args are still being streamed (for edit/write spinner)
|
|
@@ -617,14 +678,18 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
617
678
|
}
|
|
618
679
|
|
|
619
680
|
/**
|
|
620
|
-
* Get all image blocks from result content and details.
|
|
621
|
-
* Some tools (like generate_image) store images in details to avoid bloating
|
|
681
|
+
* Get all image blocks from result content and details.
|
|
682
|
+
* Some tools (like generate_image) store images in details to avoid bloating
|
|
683
|
+
* model context. Xdev-dispatched tools preserve those details under
|
|
684
|
+
* details.xdev.inner.
|
|
622
685
|
*/
|
|
623
|
-
#getAllImageBlocks():
|
|
686
|
+
#getAllImageBlocks(): ToolImageBlock[] {
|
|
624
687
|
if (!this.#result) return [];
|
|
625
|
-
const contentImages = this.#result.content
|
|
626
|
-
const
|
|
627
|
-
|
|
688
|
+
const contentImages = this.#result.content.filter(block => block.type === "image");
|
|
689
|
+
const details = this.#result.details;
|
|
690
|
+
const detailImages = imageBlocksFromDetails(details);
|
|
691
|
+
const xdevImages = isRecord(details) && isRecord(details.xdev) ? imageBlocksFromDetails(details.xdev.inner) : [];
|
|
692
|
+
return [...contentImages, ...detailImages, ...xdevImages];
|
|
628
693
|
}
|
|
629
694
|
|
|
630
695
|
/**
|
|
@@ -697,27 +762,16 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
697
762
|
!isBackgroundAsyncRunning &&
|
|
698
763
|
(pendingCallConsumesSpinner || partialResultConsumesSpinner);
|
|
699
764
|
const needsSpinner = isStreamingArgs || isLivePartialTool || this.#displaceableByToolName === "hub";
|
|
700
|
-
if (needsSpinner && !this.#
|
|
765
|
+
if (needsSpinner && !this.#spinnerActive) {
|
|
701
766
|
const frameCount = theme.spinnerFrames.length;
|
|
702
767
|
const frame = sharedSpinnerFrame(frameCount);
|
|
703
768
|
this.#spinnerFrame = frame;
|
|
704
769
|
this.#renderState.spinnerFrame = frame;
|
|
705
|
-
this.#
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const frameCount = theme.spinnerFrames.length;
|
|
711
|
-
this.#spinnerFrame = sharedSpinnerFrame(frameCount, now);
|
|
712
|
-
this.#renderState.spinnerFrame = this.#spinnerFrame;
|
|
713
|
-
// Component-scoped: a spinner tick only changes this tool block, so
|
|
714
|
-
// the TUI reuses every other root subtree instead of walking the
|
|
715
|
-
// whole tree (issue #4377).
|
|
716
|
-
this.#ui.requestComponentRender(this);
|
|
717
|
-
}, SPINNER_RENDER_INTERVAL_MS);
|
|
718
|
-
} else if (!needsSpinner && this.#spinnerInterval) {
|
|
719
|
-
clearInterval(this.#spinnerInterval);
|
|
720
|
-
this.#spinnerInterval = undefined;
|
|
770
|
+
this.#spinnerActive = true;
|
|
771
|
+
registerSpinnerBlock(this);
|
|
772
|
+
} else if (!needsSpinner && this.#spinnerActive) {
|
|
773
|
+
this.#spinnerActive = false;
|
|
774
|
+
unregisterSpinnerBlock(this);
|
|
721
775
|
// Clear the last drawn frame so a non-live renderCall (e.g. a write whose
|
|
722
776
|
// args just completed) stops showing a frozen spinner glyph. Skip when a
|
|
723
777
|
// todo strike owns the frame — it sets its own value right after this.
|
|
@@ -728,6 +782,20 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
728
782
|
}
|
|
729
783
|
}
|
|
730
784
|
|
|
785
|
+
/**
|
|
786
|
+
* Advance to the shared spinner glyph and repaint just this block. Driven by
|
|
787
|
+
* the single shared spinner ticker (see `registerSpinnerBlock`); the tick is
|
|
788
|
+
* component-scoped so the TUI reuses every other root subtree (issue #4377).
|
|
789
|
+
*/
|
|
790
|
+
tickSpinner(frame: number): void {
|
|
791
|
+
// A detached task block that scrolled into native scrollback stops the
|
|
792
|
+
// instant it leaves the repaintable region.
|
|
793
|
+
if (this.#maybeFreezeBackgroundTask()) return;
|
|
794
|
+
this.#spinnerFrame = frame;
|
|
795
|
+
this.#renderState.spinnerFrame = frame;
|
|
796
|
+
this.#ui.requestComponentRender(this);
|
|
797
|
+
}
|
|
798
|
+
|
|
731
799
|
/**
|
|
732
800
|
* Freeze a detached (`async.state === "running"`) task block once its rows
|
|
733
801
|
* become native-scrollback history: the block left the transcript's live
|
|
@@ -790,7 +858,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
790
858
|
clearInterval(this.#todoStrikeInterval);
|
|
791
859
|
this.#todoStrikeInterval = undefined;
|
|
792
860
|
}
|
|
793
|
-
if (!this.#
|
|
861
|
+
if (!this.#spinnerActive) {
|
|
794
862
|
this.#spinnerFrame = undefined;
|
|
795
863
|
this.#renderState.spinnerFrame = undefined;
|
|
796
864
|
}
|
|
@@ -886,9 +954,9 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
886
954
|
* Stop spinner animation and cleanup resources.
|
|
887
955
|
*/
|
|
888
956
|
stopAnimation(): void {
|
|
889
|
-
if (this.#
|
|
890
|
-
|
|
891
|
-
this
|
|
957
|
+
if (this.#spinnerActive) {
|
|
958
|
+
this.#spinnerActive = false;
|
|
959
|
+
unregisterSpinnerBlock(this);
|
|
892
960
|
this.#spinnerFrame = undefined;
|
|
893
961
|
this.#renderState.spinnerFrame = undefined;
|
|
894
962
|
}
|
|
@@ -849,13 +849,18 @@ class TreeList implements Component {
|
|
|
849
849
|
this.#selectedIndex = Math.max(0, this.#selectedIndex - this.maxVisibleLines);
|
|
850
850
|
} else if (matchesSelectPageDown(keyData) || matchesKey(keyData, "right")) {
|
|
851
851
|
this.#selectedIndex = Math.min(this.#filteredNodes.length - 1, this.#selectedIndex + this.maxVisibleLines);
|
|
852
|
-
} else if (
|
|
852
|
+
} else if (
|
|
853
|
+
matchesKey(keyData, "shift+enter") ||
|
|
854
|
+
matchesKey(keyData, "shift+return") ||
|
|
855
|
+
keyData === "\n" || // Shift+Enter delivered as bare LF (iTerm2 legacy mapping) — matches the composer (issue #8821)
|
|
856
|
+
keyData === "\x1b[13;2~" // Shift+Enter legacy CSI ~ form — also accepted by the composer (editor.ts:1466)
|
|
857
|
+
) {
|
|
853
858
|
// Summarize-and-switch: fork with a branch summary without the extra prompt.
|
|
854
859
|
const selected = this.#filteredNodes[this.#selectedIndex];
|
|
855
860
|
if (selected && this.onSelect) {
|
|
856
861
|
this.onSelect(selected.node.entry.id, { summarize: true });
|
|
857
862
|
}
|
|
858
|
-
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return")
|
|
863
|
+
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return")) {
|
|
859
864
|
const selected = this.#filteredNodes[this.#selectedIndex];
|
|
860
865
|
if (selected && this.onSelect) {
|
|
861
866
|
this.onSelect(selected.node.entry.id, { summarize: false });
|
|
@@ -24,7 +24,7 @@ import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
|
24
24
|
import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
|
|
25
25
|
import idleRecapPrompt from "../../prompts/system/recap-user.md" with { type: "text" };
|
|
26
26
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
27
|
-
import { isSilentAbort, readQueueChipText, resolveAbortLabel } from "../../session/messages";
|
|
27
|
+
import { isSilentAbort, isUserInvokedSkillPrompt, readQueueChipText, resolveAbortLabel } from "../../session/messages";
|
|
28
28
|
import { type ApprovalMode, resolveApproval } from "../../tools/approval";
|
|
29
29
|
import { previewLine, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
|
30
30
|
import { PROPOSE_DEVICE_NAME, writeDeviceDispatch } from "../../tools/resolve";
|
|
@@ -778,7 +778,17 @@ export class EventController {
|
|
|
778
778
|
}
|
|
779
779
|
this.#renderedCustomMessages.add(signature);
|
|
780
780
|
this.#resetReadGroup();
|
|
781
|
-
|
|
781
|
+
if (
|
|
782
|
+
event.message.role === "custom" &&
|
|
783
|
+
this.ctx.optimisticSkillMessagePending &&
|
|
784
|
+
isUserInvokedSkillPrompt(event.message)
|
|
785
|
+
) {
|
|
786
|
+
// The optimistic `/skill:` row painted at submit time (issue #8895):
|
|
787
|
+
// swap it for the canonical message instead of appending a duplicate.
|
|
788
|
+
this.ctx.reconcileOptimisticSkillMessage(event.message);
|
|
789
|
+
} else {
|
|
790
|
+
this.ctx.addMessageToChat(event.message);
|
|
791
|
+
}
|
|
782
792
|
// Queued custom-message chips are derived from the agent queue; refresh the
|
|
783
793
|
// pending bar when the queued custom is consumed so the chip disappears
|
|
784
794
|
// immediately.
|
|
@@ -17,7 +17,7 @@ import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
|
17
17
|
import { materializeImageReferenceLinks, shiftImageMarkers } from "../../modes/image-references";
|
|
18
18
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
19
19
|
import { parseQueueShorthand, splitQueuedMessages } from "../../modes/queue-input";
|
|
20
|
-
import {
|
|
20
|
+
import { buildSkillCommandPrompt, isKnownSkillCommand } from "../../modes/skill-command";
|
|
21
21
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
22
22
|
import manualContinuePrompt from "../../prompts/system/manual-continue.md" with { type: "text" };
|
|
23
23
|
import { USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
@@ -173,6 +173,11 @@ export class InputController {
|
|
|
173
173
|
},
|
|
174
174
|
) {}
|
|
175
175
|
|
|
176
|
+
/** Session-level title starts (user `/skill:` via promptCustomMessage) reuse this UI. */
|
|
177
|
+
notifyTitleGenerationStart(): void {
|
|
178
|
+
this.#showTinyTitleDownloadProgress(this.ctx.settings.get("providers.tinyModel"));
|
|
179
|
+
}
|
|
180
|
+
|
|
176
181
|
#enhancedPaste?: EnhancedPasteController;
|
|
177
182
|
#focusedLeftTapListenerInstalled = false;
|
|
178
183
|
#focusedPasteListenerInstalled = false;
|
|
@@ -1168,21 +1173,42 @@ export class InputController {
|
|
|
1168
1173
|
};
|
|
1169
1174
|
|
|
1170
1175
|
this.ctx.editor.clearDraft(text);
|
|
1176
|
+
let optimistic = false;
|
|
1171
1177
|
try {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
if (!handled) {
|
|
1178
|
+
// Build the user-attributed skill message once so the optimistic
|
|
1179
|
+
// transcript row and the dispatched message share content.
|
|
1180
|
+
const built = await buildSkillCommandPrompt(this.ctx, text, streamingBehavior, draftImages);
|
|
1181
|
+
if (!built) {
|
|
1177
1182
|
restoreDraft();
|
|
1178
1183
|
return false;
|
|
1179
1184
|
}
|
|
1185
|
+
// Paint the row before the awaited dispatch so a slow preflight (memory
|
|
1186
|
+
// recall, before_agent_start hooks, auto-thinking, pre-prompt compaction)
|
|
1187
|
+
// does not leave the submission invisible (issue #8895). A streaming
|
|
1188
|
+
// submission queues instead and surfaces its chip, so only paint when the
|
|
1189
|
+
// turn will run fresh.
|
|
1190
|
+
optimistic = !this.ctx.session.isStreaming;
|
|
1191
|
+
if (optimistic) {
|
|
1192
|
+
// Mirror the message promptCustomMessage will build for the turn so the
|
|
1193
|
+
// canonical message_start reconciles this row rather than duplicating it.
|
|
1194
|
+
this.ctx.renderOptimisticSkillMessage(
|
|
1195
|
+
{ role: "custom", ...built.message, timestamp: Date.now() },
|
|
1196
|
+
{ imageLinks: draftImageLinks },
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
await this.ctx.session.promptCustomMessage(built.message, built.options);
|
|
1180
1200
|
return true;
|
|
1181
1201
|
} catch (error) {
|
|
1202
|
+
if (optimistic) this.ctx.clearOptimisticSkillMessage();
|
|
1182
1203
|
restoreDraft();
|
|
1183
1204
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
1184
1205
|
return true;
|
|
1185
1206
|
} finally {
|
|
1207
|
+
if (optimistic && this.ctx.optimisticSkillMessagePending) {
|
|
1208
|
+
// Dispatch resolved without a canonical skill message_start (aborted
|
|
1209
|
+
// preflight, or a streaming-race requeue): drop the pending row.
|
|
1210
|
+
this.ctx.clearOptimisticSkillMessage();
|
|
1211
|
+
}
|
|
1186
1212
|
if (this.ctx.session.isStreaming) {
|
|
1187
1213
|
this.ctx.updatePendingMessagesDisplay();
|
|
1188
1214
|
this.ctx.ui.requestRender();
|
|
@@ -1637,6 +1663,38 @@ export class InputController {
|
|
|
1637
1663
|
const focusedNow = this.ctx.ui.getFocused();
|
|
1638
1664
|
const promptTarget =
|
|
1639
1665
|
focusedNow && focusedNow !== this.ctx.editor && hasPasteText(focusedNow) ? focusedNow : null;
|
|
1666
|
+
// #8769: On macOS, Finder `Cmd+C` on an image file puts BOTH a
|
|
1667
|
+
// `public.file-url` representation and a generated 1024x1024
|
|
1668
|
+
// file-icon bitmap on the pasteboard. `arboard::get_image()`
|
|
1669
|
+
// succeeds with the icon, so probing the image representation first
|
|
1670
|
+
// would attach the generic Finder icon instead of the copied
|
|
1671
|
+
// screenshot — a vision model then sees a white `PNG` document
|
|
1672
|
+
// icon. Probe the file URLs before the bitmap and let any that
|
|
1673
|
+
// resolve to a supported image file win over the icon: the
|
|
1674
|
+
// authoritative file bytes are what the user copied.
|
|
1675
|
+
//
|
|
1676
|
+
// #3506: this branch also recovers file-url-only pasteboards
|
|
1677
|
+
// (Finder selections, certain screenshot tools) where
|
|
1678
|
+
// `arboard::get_image()` returns `ContentNotAvailable` and
|
|
1679
|
+
// `pbpaste` is empty. Every image-shaped path routes through
|
|
1680
|
+
// {@link handleImagePathPaste}, matching the bracketed-paste
|
|
1681
|
+
// handler in `CustomEditor.handleInput`; multi-image Finder
|
|
1682
|
+
// selections must not silently drop after the first attach.
|
|
1683
|
+
// `readMacFileUrls` returns an empty list off Darwin, so on every
|
|
1684
|
+
// other platform this is a no-op and the bitmap read below still
|
|
1685
|
+
// runs first.
|
|
1686
|
+
const fileUrls = promptTarget ? [] : ((await this.clipboard.readMacFileUrls?.()) ?? []);
|
|
1687
|
+
let attachedFromFileUrls = false;
|
|
1688
|
+
for (const url of fileUrls) {
|
|
1689
|
+
const candidate = extractImagePathFromText(url);
|
|
1690
|
+
if (!candidate) continue;
|
|
1691
|
+
await this.handleImagePathPaste(candidate);
|
|
1692
|
+
attachedFromFileUrls = true;
|
|
1693
|
+
}
|
|
1694
|
+
if (attachedFromFileUrls) return true;
|
|
1695
|
+
// No usable image-file URL (pure bitmap pasteboard: screenshots,
|
|
1696
|
+
// browser copies, or a non-image Finder selection). Fall to the
|
|
1697
|
+
// image representation.
|
|
1640
1698
|
const image = await this.clipboard.readImage();
|
|
1641
1699
|
if (image) {
|
|
1642
1700
|
if (promptTarget) {
|
|
@@ -1652,27 +1710,6 @@ export class InputController {
|
|
|
1652
1710
|
`Unsupported clipboard image format: ${image.mimeType}`,
|
|
1653
1711
|
);
|
|
1654
1712
|
}
|
|
1655
|
-
// #3506: macOS Finder `Cmd+C` puts only a `public.file-url`
|
|
1656
|
-
// representation on the pasteboard. `pbpaste` (the backing call
|
|
1657
|
-
// for `readText` on Darwin) only surfaces plain text / RTF / EPS,
|
|
1658
|
-
// so it returns empty for file-url-only pasteboards — the smart
|
|
1659
|
-
// text fallback below would dead-end with "Clipboard is empty".
|
|
1660
|
-
// Reach the file URL directly via AppleScript and route every
|
|
1661
|
-
// image-shaped path through {@link handleImagePathPaste}, matching
|
|
1662
|
-
// the bracketed-paste handler in `CustomEditor.handleInput` which
|
|
1663
|
-
// iterates every extracted image path. Multi-image Finder
|
|
1664
|
-
// selections must not silently drop after the first attach.
|
|
1665
|
-
// `readMacFileUrls` returns an empty list off Darwin, so the
|
|
1666
|
-
// check is free on every other platform.
|
|
1667
|
-
const fileUrls = promptTarget ? [] : ((await this.clipboard.readMacFileUrls?.()) ?? []);
|
|
1668
|
-
let attachedFromFileUrls = false;
|
|
1669
|
-
for (const url of fileUrls) {
|
|
1670
|
-
const candidate = extractImagePathFromText(url);
|
|
1671
|
-
if (!candidate) continue;
|
|
1672
|
-
await this.handleImagePathPaste(candidate);
|
|
1673
|
-
attachedFromFileUrls = true;
|
|
1674
|
-
}
|
|
1675
|
-
if (attachedFromFileUrls) return true;
|
|
1676
1713
|
// Smart paste (#1628): no image on the clipboard — fall back to
|
|
1677
1714
|
// pasting its text so the same chord covers both payload kinds.
|
|
1678
1715
|
// Hosts that pre-empt the terminal's own paste (VS Code's
|
|
@@ -1198,11 +1198,20 @@ export class MCPCommandController {
|
|
|
1198
1198
|
connectionError = error as Error;
|
|
1199
1199
|
}
|
|
1200
1200
|
|
|
1201
|
-
// Server connected fine without auth
|
|
1202
|
-
//
|
|
1203
|
-
//
|
|
1201
|
+
// Server connected fine without auth. A tool-level challenge overrides
|
|
1202
|
+
// this: servers may allow the anonymous handshake yet protect individual
|
|
1203
|
+
// tool calls with `_meta["mcp/www_authenticate"]`. Even without such a
|
|
1204
|
+
// challenge, a clean `initialize` is only weak evidence — per the MCP
|
|
1205
|
+
// spec a server MAY permit unauthenticated `initialize` while requiring a
|
|
1206
|
+
// bearer token for `tools/call`. The user explicitly asked to reauth, so
|
|
1207
|
+
// honor it when the server advertises OAuth discovery metadata; only
|
|
1208
|
+
// refuse when there is genuinely no OAuth endpoint to acquire.
|
|
1204
1209
|
if (connectionSucceeded && !authChallenge) {
|
|
1205
|
-
|
|
1210
|
+
const discovered = "url" in config && config.url ? await discoverOAuthEndpoints(config.url) : null;
|
|
1211
|
+
if (!discovered) {
|
|
1212
|
+
throw new Error("Server connection succeeded without OAuth; reauthorization is not required.");
|
|
1213
|
+
}
|
|
1214
|
+
return discovered;
|
|
1206
1215
|
}
|
|
1207
1216
|
|
|
1208
1217
|
// Tool calls can carry richer RFC 6750/RFC 9728 hints than the original
|
|
@@ -117,7 +117,8 @@ import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES, buildTuiBuiltinSlashCommands } fr
|
|
|
117
117
|
import { formatDuration } from "../slash-commands/helpers/format";
|
|
118
118
|
import { STTController, type SttState } from "../stt";
|
|
119
119
|
import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
|
|
120
|
-
import {
|
|
120
|
+
import { labelEchoesHandle } from "../task/label";
|
|
121
|
+
import { agentTypeBadge, formatTaskId } from "../task/render";
|
|
121
122
|
import type { ConfiguredThinkingLevel } from "../thinking";
|
|
122
123
|
import { tinyTitleClient } from "../tiny/title-client";
|
|
123
124
|
import type { LspStartupServerInfo } from "../tools";
|
|
@@ -165,7 +166,7 @@ import type { HookInputComponent } from "./components/hook-input";
|
|
|
165
166
|
import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
|
|
166
167
|
import { type PlanReviewAnnotationState, PlanReviewOverlay } from "./components/plan-review-overlay";
|
|
167
168
|
import { StatusLineComponent } from "./components/status-line";
|
|
168
|
-
import type
|
|
169
|
+
import { stopSharedSpinnerTicker, type ToolExecutionHandle } from "./components/tool-execution";
|
|
169
170
|
import { TranscriptContainer } from "./components/transcript-container";
|
|
170
171
|
import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
|
|
171
172
|
import { BtwController } from "./controllers/btw-controller";
|
|
@@ -452,8 +453,8 @@ const SUBAGENT_OBSERVER_UI_COALESCE_MS = 100;
|
|
|
452
453
|
|
|
453
454
|
/**
|
|
454
455
|
* Build the anchored subagent HUD block: a bold accent "Subagents" header plus
|
|
455
|
-
* a bounded set of running-agent rows in the same `Id
|
|
456
|
-
* inline task rows use (muted task preview when no description was given).
|
|
456
|
+
* a bounded set of running-agent rows in the same `Id ⟨role⟩: description` shape
|
|
457
|
+
* the inline task rows use (muted task preview when no description was given).
|
|
457
458
|
* Layout mirrors the Todos HUD exactly: unindented header, then
|
|
458
459
|
* `renderTreeList` rows (dim connectors) shifted right by one space.
|
|
459
460
|
* Only detached background spawns are listed: a sync task call blocks the
|
|
@@ -476,16 +477,23 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n
|
|
|
476
477
|
expanded: true,
|
|
477
478
|
renderItem: session => {
|
|
478
479
|
const displayId = formatTaskId(session.id);
|
|
479
|
-
|
|
480
|
+
const role = session.agent ?? session.progress?.agent;
|
|
481
|
+
const badge = agentTypeBadge(role, theme);
|
|
482
|
+
let line = `${dot} ${theme.fg("accent", theme.bold(displayId))}${badge}`;
|
|
480
483
|
const description = session.description?.trim() || session.progress?.description?.trim();
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
+
const distinctDescription =
|
|
485
|
+
description && !labelEchoesHandle(session.id, description) ? description : undefined;
|
|
486
|
+
if (distinctDescription) {
|
|
487
|
+
const budget = Math.max(
|
|
488
|
+
TRUNCATE_LENGTHS.SHORT,
|
|
489
|
+
columns - visibleWidth(displayId) - visibleWidth(Bun.stripANSI(badge)) - 10,
|
|
490
|
+
);
|
|
491
|
+
line += `${theme.fg("accent", ":")} ${theme.fg("accent", truncateToWidth(replaceTabs(distinctDescription), budget))}`;
|
|
484
492
|
} else {
|
|
485
493
|
// No spawn description: fall back to a muted task preview, same as
|
|
486
494
|
// the inline task rows when a row has no label.
|
|
487
495
|
const taskPreview = session.progress?.task?.trim();
|
|
488
|
-
if (taskPreview) {
|
|
496
|
+
if (taskPreview && !labelEchoesHandle(session.id, taskPreview)) {
|
|
489
497
|
line += ` ${theme.fg("muted", truncateToWidth(replaceTabs(taskPreview), TRUNCATE_LENGTHS.SHORT))}`;
|
|
490
498
|
}
|
|
491
499
|
}
|
|
@@ -604,6 +612,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
604
612
|
#pendingSubmissionDispose: (() => void) | undefined;
|
|
605
613
|
#pendingSubmissionPreservesDraft = false;
|
|
606
614
|
#optimisticUserMessageComponents: Component[] = [];
|
|
615
|
+
#optimisticSkillMessageComponents: Component[] = [];
|
|
616
|
+
/** True while an optimistically-rendered `/skill:` row awaits its canonical
|
|
617
|
+
* `message_start`. Read by the event controller to reconcile the row. */
|
|
618
|
+
optimisticSkillMessagePending = false;
|
|
607
619
|
lastSigintTime = 0;
|
|
608
620
|
lastEscapeTime = 0;
|
|
609
621
|
lastLeftTapTime = 0;
|
|
@@ -895,6 +907,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
895
907
|
this.#selectorController = new SelectorController(this);
|
|
896
908
|
this.#focusController = new SessionFocusController(this);
|
|
897
909
|
this.#inputController = new InputController(this);
|
|
910
|
+
this.session.setTitleGenerationStart?.(() => {
|
|
911
|
+
this.#inputController.notifyTitleGenerationStart();
|
|
912
|
+
});
|
|
898
913
|
this.#observerRegistry = new SessionObserverRegistry();
|
|
899
914
|
}
|
|
900
915
|
|
|
@@ -1678,6 +1693,51 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1678
1693
|
this.addMessageToChat(message, options);
|
|
1679
1694
|
}
|
|
1680
1695
|
|
|
1696
|
+
/**
|
|
1697
|
+
* Optimistically render a user-invoked `/skill:` row before its awaited
|
|
1698
|
+
* dispatch so a slow preflight (memory recall, `before_agent_start` hooks,
|
|
1699
|
+
* auto-thinking classification, pre-prompt compaction) does not leave the
|
|
1700
|
+
* submission invisible — normal prompts paint their row via
|
|
1701
|
+
* {@link startPendingSubmission} the same way (issue #8895). The canonical
|
|
1702
|
+
* skill `message_start` swaps this row in place via
|
|
1703
|
+
* {@link reconcileOptimisticSkillMessage}; a failed or bailed dispatch drops
|
|
1704
|
+
* it via {@link clearOptimisticSkillMessage}.
|
|
1705
|
+
*/
|
|
1706
|
+
renderOptimisticSkillMessage(
|
|
1707
|
+
message: AgentMessage,
|
|
1708
|
+
options?: { imageLinks?: readonly (string | undefined)[] },
|
|
1709
|
+
): void {
|
|
1710
|
+
this.clearOptimisticSkillMessage();
|
|
1711
|
+
this.optimisticSkillMessagePending = true;
|
|
1712
|
+
this.#optimisticSkillMessageComponents = this.#captureAddedChatComponents(() => {
|
|
1713
|
+
this.addMessageToChat(message, options);
|
|
1714
|
+
});
|
|
1715
|
+
this.ensureLoadingAnimation();
|
|
1716
|
+
this.ui.requestRender();
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/** Replace the optimistic `/skill:` row with the canonical message emitted by
|
|
1720
|
+
* the session, mirroring {@link replaceOptimisticUserMessage} for skills. */
|
|
1721
|
+
reconcileOptimisticSkillMessage(message: AgentMessage): void {
|
|
1722
|
+
this.optimisticSkillMessagePending = false;
|
|
1723
|
+
for (const component of this.#optimisticSkillMessageComponents) {
|
|
1724
|
+
this.chatContainer.removeChild(component);
|
|
1725
|
+
}
|
|
1726
|
+
this.#optimisticSkillMessageComponents = [];
|
|
1727
|
+
this.addMessageToChat(message);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/** Drop the optimistic `/skill:` row when dispatch fails or bails before the
|
|
1731
|
+
* message reaches the agent (aborted preflight, streaming-race requeue). */
|
|
1732
|
+
clearOptimisticSkillMessage(): void {
|
|
1733
|
+
this.optimisticSkillMessagePending = false;
|
|
1734
|
+
if (this.#optimisticSkillMessageComponents.length === 0) return;
|
|
1735
|
+
for (const component of this.#optimisticSkillMessageComponents) {
|
|
1736
|
+
this.chatContainer.removeChild(component);
|
|
1737
|
+
}
|
|
1738
|
+
this.#optimisticSkillMessageComponents = [];
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1681
1741
|
startPendingSubmission(
|
|
1682
1742
|
input: {
|
|
1683
1743
|
text: string;
|
|
@@ -4149,6 +4209,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4149
4209
|
this.#stopLoadingAnimation(false);
|
|
4150
4210
|
}
|
|
4151
4211
|
this.#cleanupMicAnimation();
|
|
4212
|
+
// Stop the shared tool-spinner ticker: a live block missed by per-component
|
|
4213
|
+
// stopAnimation would otherwise keep an 80ms interval pinning the process.
|
|
4214
|
+
stopSharedSpinnerTicker();
|
|
4152
4215
|
this.#liveCommandController.dispose();
|
|
4153
4216
|
this.#cancelTodoAutoClearTimer();
|
|
4154
4217
|
this.#cancelObserverUiSyncTimer();
|
|
@@ -4239,10 +4302,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4239
4302
|
popTerminalTitle();
|
|
4240
4303
|
this.stop();
|
|
4241
4304
|
|
|
4242
|
-
// Print resumption hint if
|
|
4305
|
+
// Print resumption hint only if the session was actually materialized to
|
|
4306
|
+
// durable storage. Persistence is lazy — a session that exits before its
|
|
4307
|
+
// first assistant message (or dies early to an auth error, a mid-flight
|
|
4308
|
+
// Ctrl+C, or a launch-then-quit) never wrote its JSONL, so the path is
|
|
4309
|
+
// allocated but the file does not exist and `--resume <id>` would fail
|
|
4310
|
+
// (issue #8860).
|
|
4243
4311
|
const sessionId = this.sessionManager.getSessionId();
|
|
4244
4312
|
const sessionFile = this.sessionManager.getSessionFile();
|
|
4245
|
-
if (sessionId && sessionFile) {
|
|
4313
|
+
if (sessionId && sessionFile && this.sessionManager.isSessionOnDisk()) {
|
|
4246
4314
|
process.stderr.write(`\n${chalk.dim(`Resume this session with ${APP_NAME} --resume ${sessionId}`)}\n`);
|
|
4247
4315
|
}
|
|
4248
4316
|
|
package/src/modes/types.ts
CHANGED
|
@@ -313,6 +313,17 @@ export interface InteractiveModeContext {
|
|
|
313
313
|
message: AgentMessage,
|
|
314
314
|
options?: { imageLinks?: readonly (string | undefined)[] },
|
|
315
315
|
): void;
|
|
316
|
+
/** True while an optimistically-rendered `/skill:` row awaits its canonical `message_start`. */
|
|
317
|
+
optimisticSkillMessagePending: boolean;
|
|
318
|
+
/** Optimistically renders a user-invoked `/skill:` row before its awaited dispatch (issue #8895). */
|
|
319
|
+
renderOptimisticSkillMessage(
|
|
320
|
+
message: AgentMessage,
|
|
321
|
+
options?: { imageLinks?: readonly (string | undefined)[] },
|
|
322
|
+
): void;
|
|
323
|
+
/** Swaps the optimistic `/skill:` row for the canonical message emitted by the session. */
|
|
324
|
+
reconcileOptimisticSkillMessage(message: AgentMessage): void;
|
|
325
|
+
/** Drops the optimistic `/skill:` row when dispatch fails or bails before reaching the agent. */
|
|
326
|
+
clearOptimisticSkillMessage(): void;
|
|
316
327
|
isKnownSlashCommand(text: string): boolean;
|
|
317
328
|
addMessageToChat(
|
|
318
329
|
message: AgentMessage,
|
|
@@ -1,26 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
You are a precise long-term memory extractor.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Capture only persistent, reusable information:
|
|
5
|
-
- facts (name, role, employer, config, ports, versions, numbers)
|
|
6
|
-
- explicit instructions to the assistant
|
|
7
|
-
- stable preferences
|
|
8
|
-
- dated events or deadlines
|
|
3
|
+
Extract only persistent information explicitly stated in the user message: stable facts, explicit instructions to the assistant, stable preferences, dates, deadlines, paths, ports, and versions.
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
If nothing qualifies, output exactly: NO_FACTS
|
|
5
|
+
Never infer, explain, invent, or copy information from another message. Ignore greetings, acknowledgements, weather, and one-off plans. When a value is corrected, output only the latest value.
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
Message: My name is Sam, I work at Globex, and I always use 2-space indents.
|
|
15
|
-
Items:
|
|
16
|
-
name is Sam
|
|
17
|
-
works at Globex
|
|
18
|
-
prefers 2-space indents
|
|
7
|
+
Preserve names, numbers, paths, versions, dates, and the original language exactly. Output one short plain-text fact per line with no bullets, numbering, labels, JSON, or commentary.
|
|
19
8
|
|
|
20
|
-
|
|
21
|
-
Message: lol nice weather today, might grab a coffee later
|
|
22
|
-
Items:
|
|
23
|
-
NO_FACTS
|
|
24
|
-
|
|
25
|
-
Message: {text}
|
|
26
|
-
Items:
|
|
9
|
+
If nothing qualifies, output exactly NO_FACTS.
|
|
@@ -96,7 +96,7 @@ Write JSON args as `content` to `xd://<tool>` via `{{toolRefs.write}}`. Invalid
|
|
|
96
96
|
|
|
97
97
|
{{#has tools "think"}}
|
|
98
98
|
§ Scratchpad
|
|
99
|
-
`{{toolRefs.think}}`: private scratchpad; not shown to user.
|
|
99
|
+
`{{toolRefs.think}}`: private scratchpad; not shown to user. MUST use for planning; other tools become callable when it completes.
|
|
100
100
|
{{/has}}
|
|
101
101
|
|
|
102
102
|
§ Tool Policy
|