@oh-my-pi/pi-coding-agent 16.2.2 → 16.2.4
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/cli.js +3682 -3615
- package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +8 -4
- package/dist/types/advisor/config.d.ts +88 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/collab/replication-shrink.d.ts +39 -0
- package/dist/types/config/provider-globals.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +81 -0
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/memories/index.d.ts +20 -1
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +35 -1
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/command-controller.d.ts +3 -3
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +11 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +9 -3
- package/dist/types/session/agent-session.d.ts +58 -10
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +26 -0
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +8 -7
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/ssh/connection-manager.d.ts +47 -0
- package/dist/types/ssh/utils.d.ts +16 -0
- package/dist/types/task/executor.d.ts +3 -16
- package/dist/types/task/render.d.ts +0 -5
- package/dist/types/task/renderer.d.ts +13 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/yield-assembly.d.ts +28 -0
- package/dist/types/tiny/text.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/review.d.ts +6 -4
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +6 -0
- package/dist/types/tools/yield.d.ts +8 -3
- package/dist/types/utils/thinking-display.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +242 -10
- package/src/advisor/__tests__/config.test.ts +173 -0
- package/src/advisor/advise-tool.ts +11 -6
- package/src/advisor/config.ts +256 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +12 -2
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/autolearn/controller.ts +13 -22
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/cli/update-cli.ts +254 -0
- package/src/collab/guest.ts +43 -7
- package/src/collab/host.ts +13 -8
- package/src/collab/replication-shrink.ts +111 -0
- package/src/config/model-registry.ts +80 -18
- package/src/config/provider-globals.ts +25 -0
- package/src/config/settings-schema.ts +77 -0
- package/src/debug/index.ts +32 -7
- package/src/debug/log-viewer.ts +111 -53
- package/src/debug/raw-sse.ts +68 -48
- package/src/discovery/codex.ts +13 -5
- package/src/edit/hashline/diff.ts +57 -4
- package/src/eval/__tests__/julia-prelude.test.ts +2 -2
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/extensions/loader.ts +5 -3
- package/src/extensibility/extensions/wrapper.ts +9 -3
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/tool-wrapper.ts +13 -4
- package/src/extensibility/plugins/manager.ts +2 -1
- package/src/extensibility/tool-event-input.ts +23 -0
- package/src/extensibility/utils.ts +74 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/client.ts +3 -1
- package/src/mcp/manager.ts +12 -5
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +130 -15
- package/src/memory-backend/local-backend.ts +5 -3
- package/src/modes/components/advisor-config.ts +555 -0
- package/src/modes/components/advisor-message.ts +9 -2
- package/src/modes/components/agent-hub.ts +9 -4
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/model-selector.ts +79 -48
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/component.ts +150 -5
- package/src/modes/components/status-line/segments.ts +46 -21
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +27 -36
- package/src/modes/controllers/event-controller.ts +113 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +74 -25
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +9 -3
- package/src/modes/utils/ui-helpers.ts +41 -23
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/goals/goal-todo-context.md +12 -0
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +28 -24
- package/src/session/agent-session.ts +899 -429
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +172 -9
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +115 -22
- package/src/session/session-persistence.ts +95 -1
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/slash-commands/builtin-registry.ts +23 -24
- package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
- package/src/ssh/connection-manager.ts +139 -12
- package/src/ssh/file-transfer.ts +23 -18
- package/src/ssh/ssh-executor.ts +2 -13
- package/src/ssh/utils.ts +19 -0
- package/src/task/executor.ts +21 -23
- package/src/task/render.ts +162 -20
- package/src/task/renderer.ts +14 -0
- package/src/task/types.ts +17 -0
- package/src/task/yield-assembly.ts +207 -0
- package/src/tiny/text.ts +23 -0
- package/src/tools/ask.ts +55 -4
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/renderers.ts +8 -2
- package/src/tools/review.ts +17 -7
- package/src/tools/ssh.ts +8 -4
- package/src/tools/todo.ts +17 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
- package/src/prompts/system/autolearn-nudge.md +0 -5
|
@@ -132,6 +132,14 @@ function rawTextInputFromPartialJson(partialJson: unknown): string | undefined {
|
|
|
132
132
|
return partialJson;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/** Read the streamed raw-JSON buffer a tool block stashes on its args, narrowed
|
|
136
|
+
* rather than cast: a missing or non-string `__partialJson` yields `undefined`. */
|
|
137
|
+
function partialJsonOf(args: unknown): string | undefined {
|
|
138
|
+
if (args == null || typeof args !== "object" || !("__partialJson" in args)) return undefined;
|
|
139
|
+
const value = args.__partialJson;
|
|
140
|
+
return typeof value === "string" ? value : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
135
143
|
function getArgsWithStreamedTextInput(args: unknown): unknown {
|
|
136
144
|
if (args == null || typeof args !== "object") return args;
|
|
137
145
|
const record = args as Record<string, unknown>;
|
|
@@ -242,6 +250,10 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
242
250
|
#editDiffLastArgsKey?: string;
|
|
243
251
|
// Latest in-flight streaming diff recompute, captured so it can be awaited.
|
|
244
252
|
#editDiffInFlight?: Promise<void>;
|
|
253
|
+
/** Set when newer args arrived while a preview compute was in flight; the
|
|
254
|
+
* drain loop re-runs once the current compute settles, so a slow diff
|
|
255
|
+
* coalesces streamed ticks instead of being aborted by each one. */
|
|
256
|
+
#editDiffDirty = false;
|
|
245
257
|
// Cached converted images for Kitty protocol (which requires PNG), keyed by index
|
|
246
258
|
#convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
|
|
247
259
|
// Spinner animation for partial task results
|
|
@@ -323,7 +335,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
323
335
|
this.setIgnoreTight(true);
|
|
324
336
|
|
|
325
337
|
this.#updateDisplay();
|
|
326
|
-
this.#
|
|
338
|
+
this.#schedulePreviewDiff();
|
|
327
339
|
}
|
|
328
340
|
|
|
329
341
|
updateArgs(args: any, _toolCallId?: string): void {
|
|
@@ -335,7 +347,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
335
347
|
this.#args = args;
|
|
336
348
|
this.#displayInputVersion++;
|
|
337
349
|
this.#updateSpinnerAnimation();
|
|
338
|
-
this.#
|
|
350
|
+
this.#schedulePreviewDiff();
|
|
339
351
|
this.#updateDisplay();
|
|
340
352
|
}
|
|
341
353
|
|
|
@@ -346,7 +358,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
346
358
|
setArgsComplete(_toolCallId?: string): void {
|
|
347
359
|
this.#argsComplete = true;
|
|
348
360
|
this.#updateSpinnerAnimation();
|
|
349
|
-
this.#
|
|
361
|
+
this.#schedulePreviewDiff();
|
|
350
362
|
}
|
|
351
363
|
|
|
352
364
|
/**
|
|
@@ -360,7 +372,32 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
360
372
|
await this.#editDiffInFlight;
|
|
361
373
|
}
|
|
362
374
|
|
|
363
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Schedule a streaming diff preview recompute, coalescing bursts of
|
|
377
|
+
* `updateArgs` into one compute at a time: run the current compute to
|
|
378
|
+
* completion and re-run only after it settles when newer args arrived, never
|
|
379
|
+
* cancelling an in-flight compute on a fresh tick. The reveal controller pushes
|
|
380
|
+
* args ~30fps and a whole-file hashline/large-file diff can outlast a frame, so
|
|
381
|
+
* cancel-per-tick would starve every compute and no preview would land until
|
|
382
|
+
* args complete. Coalescing lets each diff land, so the preview tracks the
|
|
383
|
+
* stream at the rate the diffs can sustain.
|
|
384
|
+
*/
|
|
385
|
+
#schedulePreviewDiff(): void {
|
|
386
|
+
this.#editDiffDirty = true;
|
|
387
|
+
if (this.#editDiffInFlight) return;
|
|
388
|
+
this.#editDiffInFlight = this.#drainPreviewDiff().finally(() => {
|
|
389
|
+
this.#editDiffInFlight = undefined;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async #drainPreviewDiff(): Promise<void> {
|
|
394
|
+
while (this.#editDiffDirty) {
|
|
395
|
+
this.#editDiffDirty = false;
|
|
396
|
+
await this.#computePreviewDiff();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async #computePreviewDiff(): Promise<void> {
|
|
364
401
|
const editMode = this.#editMode;
|
|
365
402
|
if (!editMode) return;
|
|
366
403
|
const strategy = EDIT_MODE_STRATEGIES[editMode];
|
|
@@ -370,7 +407,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
370
407
|
if (args == null || typeof args !== "object") return;
|
|
371
408
|
|
|
372
409
|
const previewArgs = getArgsWithStreamedTextInput(args);
|
|
373
|
-
const partialJson = (previewArgs
|
|
410
|
+
const partialJson = partialJsonOf(previewArgs);
|
|
374
411
|
let effectiveArgs: unknown;
|
|
375
412
|
try {
|
|
376
413
|
effectiveArgs = strategy.extractCompleteEdits(previewArgs, partialJson);
|
|
@@ -400,7 +437,8 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
400
437
|
if (argsKey === this.#editDiffLastArgsKey) return;
|
|
401
438
|
this.#editDiffLastArgsKey = argsKey;
|
|
402
439
|
|
|
403
|
-
|
|
440
|
+
// Single-flight (the drain loop never overlaps computes), so this controller
|
|
441
|
+
// only ever cancels the live compute on teardown via `stopAnimation`.
|
|
404
442
|
const controller = new AbortController();
|
|
405
443
|
this.#editDiffAbort = controller;
|
|
406
444
|
|
|
@@ -732,6 +770,9 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
732
770
|
this.#stopTodoStrikeAnimation();
|
|
733
771
|
this.#editDiffAbort?.abort();
|
|
734
772
|
this.#editDiffAbort = undefined;
|
|
773
|
+
// Drop any queued rerun so the drain loop exits instead of recomputing a
|
|
774
|
+
// preview for a torn-down block after its in-flight compute is aborted.
|
|
775
|
+
this.#editDiffDirty = false;
|
|
735
776
|
}
|
|
736
777
|
|
|
737
778
|
setExpanded(expanded: boolean): void {
|
|
@@ -42,7 +42,6 @@ import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
|
|
42
42
|
import type { AuthStorage, OAuthAccountIdentity } from "../../session/auth-storage";
|
|
43
43
|
import type { CompactMode } from "../../session/compact-modes";
|
|
44
44
|
import type { NewSessionOptions } from "../../session/session-entries";
|
|
45
|
-
import { SessionManager } from "../../session/session-manager";
|
|
46
45
|
import { formatShakeSummary, type ShakeMode, type ShakeResult } from "../../session/shake-types";
|
|
47
46
|
import { limitMatchesActiveAccount } from "../../slash-commands/helpers/active-oauth-account";
|
|
48
47
|
import { outputMeta } from "../../tools/output-meta";
|
|
@@ -360,6 +359,27 @@ export class CommandController {
|
|
|
360
359
|
]);
|
|
361
360
|
return;
|
|
362
361
|
}
|
|
362
|
+
if (stats.advisors.length > 1) {
|
|
363
|
+
let info = `${theme.bold("Advisor Status")} (${stats.advisors.length} advisors)\n`;
|
|
364
|
+
for (const a of stats.advisors) {
|
|
365
|
+
const ctx =
|
|
366
|
+
a.contextWindow > 0
|
|
367
|
+
? `${a.contextTokens.toLocaleString()} / ${a.contextWindow.toLocaleString()} (${Math.round((a.contextTokens / a.contextWindow) * 100)}%)`
|
|
368
|
+
: `${a.contextTokens.toLocaleString()}`;
|
|
369
|
+
info += `\n${theme.bold(a.name)}\n`;
|
|
370
|
+
info += `${theme.fg("dim", "Model:")} ${a.model.provider}/${a.model.id}\n`;
|
|
371
|
+
info += `${theme.fg("dim", "Context:")} ${ctx}\n`;
|
|
372
|
+
info += `${theme.fg("dim", "Messages:")} ${a.messages.total.toLocaleString()}\n`;
|
|
373
|
+
info += `${theme.fg("dim", "Spend:")} ${a.tokens.input.toLocaleString()} in / ${a.tokens.output.toLocaleString()} out`;
|
|
374
|
+
if (a.cost > 0) info += `, $${a.cost.toFixed(4)}`;
|
|
375
|
+
info += "\n";
|
|
376
|
+
}
|
|
377
|
+
info += `\n${theme.bold("Totals")}\n`;
|
|
378
|
+
info += `${theme.fg("dim", "Tokens:")} ${stats.tokens.total.toLocaleString()}\n`;
|
|
379
|
+
if (stats.cost > 0) info += `${theme.fg("dim", "Cost:")} $${stats.cost.toFixed(4)}\n`;
|
|
380
|
+
this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
363
383
|
const model = stats.model!;
|
|
364
384
|
let info = `${theme.bold("Advisor Status")}\n\n`;
|
|
365
385
|
info += `${theme.bold("Provider")}\n`;
|
|
@@ -846,7 +866,7 @@ export class CommandController {
|
|
|
846
866
|
setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
|
|
847
867
|
|
|
848
868
|
this.ctx.statusLine.invalidate();
|
|
849
|
-
this.ctx.statusLine.
|
|
869
|
+
this.ctx.statusLine.resetActiveTime();
|
|
850
870
|
this.ctx.updateEditorTopBorder();
|
|
851
871
|
this.ctx.updateEditorBorderColor();
|
|
852
872
|
this.ctx.chatContainer.clear();
|
|
@@ -914,13 +934,13 @@ export class CommandController {
|
|
|
914
934
|
}
|
|
915
935
|
|
|
916
936
|
/**
|
|
917
|
-
* `/move` —
|
|
937
|
+
* `/move` — relocate the current session to a different directory.
|
|
918
938
|
*
|
|
919
939
|
* With no `targetPath` (TUI only), opens an autocomplete overlay so the user
|
|
920
940
|
* can pick or type a directory. With a `targetPath`, resolves it directly.
|
|
921
941
|
* If the target directory does not exist, the user is asked whether to create
|
|
922
|
-
* it.
|
|
923
|
-
*
|
|
942
|
+
* it. The active session file and artifacts are moved into the target
|
|
943
|
+
* directory's session bucket so `/resume` from that directory can find it.
|
|
924
944
|
*/
|
|
925
945
|
async handleMoveCommand(targetPath?: string): Promise<void> {
|
|
926
946
|
if (this.ctx.session.isStreaming) {
|
|
@@ -982,47 +1002,18 @@ export class CommandController {
|
|
|
982
1002
|
}
|
|
983
1003
|
}
|
|
984
1004
|
|
|
985
|
-
let newSessionFile: string | undefined;
|
|
986
1005
|
try {
|
|
987
|
-
|
|
988
|
-
// folder, then switch to it. The current session is left behind and
|
|
989
|
-
// remains resumable via /resume.
|
|
990
|
-
newSessionFile = SessionManager.createEmptySessionFile(resolvedPath);
|
|
991
|
-
const switched = await this.ctx.session.switchSession(newSessionFile);
|
|
992
|
-
if (!switched) {
|
|
993
|
-
await this.ctx.sessionManager.dropSession(newSessionFile);
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
1006
|
+
await this.ctx.sessionManager.moveTo(resolvedPath);
|
|
996
1007
|
} catch (err) {
|
|
997
|
-
if (newSessionFile) {
|
|
998
|
-
try {
|
|
999
|
-
await this.ctx.sessionManager.dropSession(newSessionFile);
|
|
1000
|
-
} catch (dropErr) {
|
|
1001
|
-
this.ctx.showError(
|
|
1002
|
-
`Move failed: ${err instanceof Error ? err.message : String(err)}; failed to remove empty session: ${dropErr instanceof Error ? dropErr.message : String(dropErr)}`,
|
|
1003
|
-
);
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
1008
|
this.ctx.showError(`Move failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1008
1009
|
return;
|
|
1009
1010
|
}
|
|
1010
1011
|
|
|
1011
|
-
this.ctx.session.markMovedFromEmptySessionFile(newSessionFile!);
|
|
1012
1012
|
await this.ctx.applyCwdChange(resolvedPath);
|
|
1013
1013
|
|
|
1014
|
-
this.ctx.chatContainer.clear();
|
|
1015
|
-
this.ctx.pendingMessagesContainer.clear();
|
|
1016
|
-
this.ctx.compactionQueuedMessages = [];
|
|
1017
|
-
this.ctx.streamingComponent = undefined;
|
|
1018
|
-
this.ctx.streamingMessage = undefined;
|
|
1019
|
-
this.ctx.pendingTools.clear();
|
|
1020
|
-
this.ctx.statusLine.invalidate();
|
|
1021
|
-
this.ctx.statusLine.setSessionStartTime(Date.now());
|
|
1022
|
-
this.ctx.updateEditorTopBorder();
|
|
1023
1014
|
this.ctx.updateEditorBorderColor();
|
|
1024
1015
|
await this.ctx.reloadTodos();
|
|
1025
|
-
this.ctx.ui.requestRender(
|
|
1016
|
+
this.ctx.ui.requestRender();
|
|
1026
1017
|
|
|
1027
1018
|
this.ctx.present([
|
|
1028
1019
|
new Spacer(1),
|
|
@@ -2,6 +2,7 @@ import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
|
2
2
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
3
3
|
import { getStreamingPartialJson } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
4
4
|
import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
5
6
|
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
6
7
|
import { extractTextContent } from "../../commit/utils";
|
|
7
8
|
import { settings } from "../../config/settings";
|
|
@@ -20,9 +21,12 @@ import { createUsageRowBlock } from "../../modes/components/usage-row";
|
|
|
20
21
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
21
22
|
import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
|
|
22
23
|
import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
|
|
24
|
+
import idleRecapPrompt from "../../prompts/system/recap-user.md" with { type: "text" };
|
|
23
25
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
24
26
|
import { isSilentAbort, readQueueChipText, resolveAbortLabel } from "../../session/messages";
|
|
27
|
+
import { previewLine, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
|
25
28
|
import type { ResolveToolDetails } from "../../tools/resolve";
|
|
29
|
+
import { nextActionableTask } from "../../tools/todo";
|
|
26
30
|
import { vocalizer } from "../../tts/vocalizer";
|
|
27
31
|
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
28
32
|
import { interruptHint } from "../shared";
|
|
@@ -43,6 +47,8 @@ const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
|
|
|
43
47
|
* oldest live-region card retires as soon as a new one would exceed the cap.
|
|
44
48
|
*/
|
|
45
49
|
const MAX_LIVE_IRC_CARDS = 4;
|
|
50
|
+
const IDLE_RECAP_MIN_SECONDS = 1;
|
|
51
|
+
const IDLE_RECAP_MAX_SECONDS = 3600;
|
|
46
52
|
|
|
47
53
|
type AgentSessionEventHandlers = {
|
|
48
54
|
[E in AgentSessionEventKind]: (event: Extract<AgentSessionEvent, { type: E }>) => Promise<void>;
|
|
@@ -69,6 +75,10 @@ export class EventController {
|
|
|
69
75
|
// #handleMessageEnd / #handleAgentStart).
|
|
70
76
|
#pinnedErrorComponent: AssistantMessageComponent | undefined = undefined;
|
|
71
77
|
#idleCompactionTimer?: NodeJS.Timeout;
|
|
78
|
+
#idleRecapTimer?: NodeJS.Timeout;
|
|
79
|
+
// In-flight ephemeral recap turn; aborted by #cancelIdleRecap when any
|
|
80
|
+
// activity (new turn, compaction, editor draft) supersedes the idle recap.
|
|
81
|
+
#idleRecapAbort?: AbortController;
|
|
72
82
|
#ircExpiryTimers = new Map<string, NodeJS.Timeout>();
|
|
73
83
|
// Insertion-ordered IRC cards not yet retired; values are the transcript
|
|
74
84
|
// components each card contributed (see #retireIrcCard for the guard).
|
|
@@ -157,6 +167,7 @@ export class EventController {
|
|
|
157
167
|
this.#streamingReveal.stop();
|
|
158
168
|
this.#toolArgsReveal.stop();
|
|
159
169
|
this.#cancelIdleCompaction();
|
|
170
|
+
this.#cancelIdleRecap();
|
|
160
171
|
this.#setTerminalProgress(false);
|
|
161
172
|
for (const timer of this.#ircExpiryTimers.values()) {
|
|
162
173
|
clearTimeout(timer);
|
|
@@ -248,6 +259,7 @@ export class EventController {
|
|
|
248
259
|
this.#lastAssistantComponent = undefined;
|
|
249
260
|
this.#pinnedErrorComponent = undefined;
|
|
250
261
|
this.#cancelIdleCompaction();
|
|
262
|
+
this.#cancelIdleRecap();
|
|
251
263
|
for (const timer of this.#ircExpiryTimers.values()) {
|
|
252
264
|
clearTimeout(timer);
|
|
253
265
|
}
|
|
@@ -302,6 +314,8 @@ export class EventController {
|
|
|
302
314
|
this.ctx.statusContainer.clear();
|
|
303
315
|
}
|
|
304
316
|
this.#cancelIdleCompaction();
|
|
317
|
+
this.#cancelIdleRecap();
|
|
318
|
+
this.ctx.statusLine.markActivityStart();
|
|
305
319
|
this.#setTerminalProgress(true);
|
|
306
320
|
this.ctx.ensureLoadingAnimation();
|
|
307
321
|
this.ctx.ui.requestRender();
|
|
@@ -554,6 +568,11 @@ export class EventController {
|
|
|
554
568
|
this.#ensureWorkingLoaderWhileStreaming();
|
|
555
569
|
this.#vocalizeDelta(event);
|
|
556
570
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
571
|
+
const unlockedThinkingVisibility = this.ctx.noteDisplayableThinkingContent(event.message);
|
|
572
|
+
if (unlockedThinkingVisibility) {
|
|
573
|
+
this.ctx.streamingComponent.setHideThinkingBlock(this.ctx.effectiveHideThinkingBlock);
|
|
574
|
+
this.#streamingReveal.resyncVisibility();
|
|
575
|
+
}
|
|
557
576
|
this.ctx.streamingMessage = event.message;
|
|
558
577
|
this.#streamingReveal.setTarget(this.ctx.streamingMessage);
|
|
559
578
|
|
|
@@ -681,6 +700,12 @@ export class EventController {
|
|
|
681
700
|
|
|
682
701
|
async #handleMessageEnd(event: Extract<AgentSessionEvent, { type: "message_end" }>): Promise<void> {
|
|
683
702
|
if (event.message.role === "user") return;
|
|
703
|
+
const unlockedThinkingVisibility =
|
|
704
|
+
event.message.role === "assistant" && this.ctx.noteDisplayableThinkingContent(event.message);
|
|
705
|
+
if (unlockedThinkingVisibility && this.ctx.streamingComponent) {
|
|
706
|
+
this.ctx.streamingComponent.setHideThinkingBlock(this.ctx.effectiveHideThinkingBlock);
|
|
707
|
+
this.#streamingReveal.resyncVisibility();
|
|
708
|
+
}
|
|
684
709
|
if (event.message.role === "assistant" && settings.get("speech.enabled")) {
|
|
685
710
|
if (event.message.stopReason === "aborted") {
|
|
686
711
|
// Esc / Ctrl+C / interrupt: stop speaking now and drop the trailing partial.
|
|
@@ -928,9 +953,13 @@ export class EventController {
|
|
|
928
953
|
}
|
|
929
954
|
// Update todo display when todo tool completes
|
|
930
955
|
if (event.toolName === "todo" && !event.isError) {
|
|
956
|
+
const hadTodoReminder = (this.ctx.todoReminderContainer?.children.length ?? 0) > 0;
|
|
957
|
+
this.ctx.todoReminderContainer?.clear();
|
|
931
958
|
const details = event.result.details as { phases?: TodoPhase[] } | undefined;
|
|
932
959
|
if (details?.phases) {
|
|
933
960
|
this.ctx.setTodos(details.phases);
|
|
961
|
+
} else if (hadTodoReminder) {
|
|
962
|
+
this.ctx.ui.requestRender();
|
|
934
963
|
}
|
|
935
964
|
} else if (event.toolName === "todo" && event.isError) {
|
|
936
965
|
const textContent = event.result.content.find(
|
|
@@ -967,6 +996,7 @@ export class EventController {
|
|
|
967
996
|
|
|
968
997
|
async #finishAgentEnd(): Promise<void> {
|
|
969
998
|
this.#setTerminalProgress(false);
|
|
999
|
+
this.ctx.statusLine.markActivityEnd();
|
|
970
1000
|
this.#streamingReveal.stop();
|
|
971
1001
|
this.#toolArgsReveal.flushAll();
|
|
972
1002
|
if (this.ctx.loadingAnimation) {
|
|
@@ -1008,6 +1038,7 @@ export class EventController {
|
|
|
1008
1038
|
this.#lastAssistantComponent = undefined;
|
|
1009
1039
|
this.ctx.ui.requestRender();
|
|
1010
1040
|
this.#scheduleIdleCompaction();
|
|
1041
|
+
this.#scheduleIdleRecap();
|
|
1011
1042
|
this.sendCompletionNotification();
|
|
1012
1043
|
}
|
|
1013
1044
|
|
|
@@ -1053,6 +1084,7 @@ export class EventController {
|
|
|
1053
1084
|
event: Extract<AgentSessionEvent, { type: "auto_compaction_start" }>,
|
|
1054
1085
|
): Promise<void> {
|
|
1055
1086
|
this.#cancelIdleCompaction();
|
|
1087
|
+
this.#cancelIdleRecap();
|
|
1056
1088
|
this.#setTerminalProgress(true);
|
|
1057
1089
|
this.#stopWorkingLoader();
|
|
1058
1090
|
this.ctx.statusContainer.clear();
|
|
@@ -1085,6 +1117,7 @@ export class EventController {
|
|
|
1085
1117
|
|
|
1086
1118
|
async #handleAutoCompactionEnd(event: Extract<AgentSessionEvent, { type: "auto_compaction_end" }>): Promise<void> {
|
|
1087
1119
|
this.#cancelIdleCompaction();
|
|
1120
|
+
this.#cancelIdleRecap();
|
|
1088
1121
|
this.#setTerminalProgress(false);
|
|
1089
1122
|
if (this.ctx.autoCompactionLoader) {
|
|
1090
1123
|
this.ctx.autoCompactionLoader.stop();
|
|
@@ -1223,7 +1256,9 @@ export class EventController {
|
|
|
1223
1256
|
|
|
1224
1257
|
async #handleTodoReminder(event: Extract<AgentSessionEvent, { type: "todo_reminder" }>): Promise<void> {
|
|
1225
1258
|
const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts);
|
|
1226
|
-
this.ctx.
|
|
1259
|
+
this.ctx.todoReminderContainer.clear();
|
|
1260
|
+
this.ctx.todoReminderContainer.addChild(component);
|
|
1261
|
+
this.ctx.ui.requestRender();
|
|
1227
1262
|
}
|
|
1228
1263
|
|
|
1229
1264
|
async #handleTodoAutoClear(_event: Extract<AgentSessionEvent, { type: "todo_auto_clear" }>): Promise<void> {
|
|
@@ -1237,6 +1272,17 @@ export class EventController {
|
|
|
1237
1272
|
}
|
|
1238
1273
|
}
|
|
1239
1274
|
|
|
1275
|
+
#cancelIdleRecap(): void {
|
|
1276
|
+
if (this.#idleRecapTimer) {
|
|
1277
|
+
clearTimeout(this.#idleRecapTimer);
|
|
1278
|
+
this.#idleRecapTimer = undefined;
|
|
1279
|
+
}
|
|
1280
|
+
if (this.#idleRecapAbort) {
|
|
1281
|
+
this.#idleRecapAbort.abort();
|
|
1282
|
+
this.#idleRecapAbort = undefined;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1240
1286
|
#scheduleIdleCompaction(): void {
|
|
1241
1287
|
this.#cancelIdleCompaction();
|
|
1242
1288
|
// Don't schedule idle work while context maintenance is already running; the
|
|
@@ -1267,6 +1313,72 @@ export class EventController {
|
|
|
1267
1313
|
this.#idleCompactionTimer.unref?.();
|
|
1268
1314
|
}
|
|
1269
1315
|
|
|
1316
|
+
#scheduleIdleRecap(): void {
|
|
1317
|
+
this.#cancelIdleRecap();
|
|
1318
|
+
if (this.ctx.viewSession.isCompacting) return;
|
|
1319
|
+
|
|
1320
|
+
const recapSettings = settings.getGroup("recap");
|
|
1321
|
+
if (!recapSettings.enabled) return;
|
|
1322
|
+
if (this.ctx.editor.getText().trim()) return;
|
|
1323
|
+
|
|
1324
|
+
const timeoutMs =
|
|
1325
|
+
Math.max(IDLE_RECAP_MIN_SECONDS, Math.min(IDLE_RECAP_MAX_SECONDS, recapSettings.idleSeconds)) * 1000;
|
|
1326
|
+
this.#idleRecapTimer = setTimeout(() => {
|
|
1327
|
+
this.#idleRecapTimer = undefined;
|
|
1328
|
+
void this.#runIdleRecap();
|
|
1329
|
+
}, timeoutMs);
|
|
1330
|
+
this.#idleRecapTimer.unref?.();
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Generate the idle recap with an ephemeral side-channel turn over the
|
|
1335
|
+
* current conversation (same pipeline as `/btw`) and surface it as a status
|
|
1336
|
+
* line. Live goal/title and the active todo task are passed as anchoring
|
|
1337
|
+
* hints because the snapshot only carries conversation history, not the
|
|
1338
|
+
* controller's todo/goal state. The request is abortable: any activity
|
|
1339
|
+
* cancels it via #cancelIdleRecap, and idle conditions are re-checked after
|
|
1340
|
+
* the reply lands so a stale recap never paints over fresh work.
|
|
1341
|
+
*/
|
|
1342
|
+
async #runIdleRecap(): Promise<void> {
|
|
1343
|
+
if (!this.#idleConditionsHold()) return;
|
|
1344
|
+
if (!this.ctx.viewSession.model) return;
|
|
1345
|
+
if (this.ctx.viewSession.messages.length === 0) return;
|
|
1346
|
+
|
|
1347
|
+
const promptText = prompt.render(idleRecapPrompt, {
|
|
1348
|
+
goal: this.#idleRecapGoalText() ?? "",
|
|
1349
|
+
task: nextActionableTask(this.ctx.todoPhases)?.content ?? "",
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
const abort = new AbortController();
|
|
1353
|
+
this.#idleRecapAbort = abort;
|
|
1354
|
+
try {
|
|
1355
|
+
const { replyText } = await this.ctx.viewSession.runEphemeralTurn({ promptText, signal: abort.signal });
|
|
1356
|
+
if (this.#idleRecapAbort !== abort || abort.signal.aborted || !this.#idleConditionsHold()) return;
|
|
1357
|
+
const recap = previewLine(replyText, TRUNCATE_LENGTHS.RECAP);
|
|
1358
|
+
if (!recap) return;
|
|
1359
|
+
this.ctx.showStatus(theme.fg("dim", theme.italic(`※ recap: ${recap}`)), { dim: false });
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
if (!abort.signal.aborted) logger.debug("Idle recap turn failed", { error: String(error) });
|
|
1362
|
+
} finally {
|
|
1363
|
+
if (this.#idleRecapAbort === abort) this.#idleRecapAbort = undefined;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/** Idle gate shared by the recap timer fire and its post-reply re-check. */
|
|
1368
|
+
#idleConditionsHold(): boolean {
|
|
1369
|
+
if (this.ctx.viewSession.isStreaming) return false;
|
|
1370
|
+
if (this.ctx.viewSession.isCompacting) return false;
|
|
1371
|
+
if (this.ctx.editor.getText().trim()) return false;
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
#idleRecapGoalText(): string | undefined {
|
|
1376
|
+
const goal = this.ctx.viewSession.getGoalModeState?.()?.goal.objective.trim();
|
|
1377
|
+
if (goal) return goal;
|
|
1378
|
+
const title = this.ctx.sessionManager.getSessionName()?.trim();
|
|
1379
|
+
return title || undefined;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1270
1382
|
#currentContextTokens(): number {
|
|
1271
1383
|
return this.ctx.viewSession.getContextUsage()?.tokens ?? 0;
|
|
1272
1384
|
}
|
|
@@ -170,7 +170,7 @@ export class ExtensionUiController {
|
|
|
170
170
|
|
|
171
171
|
// Reset and update status line
|
|
172
172
|
this.ctx.statusLine.invalidate();
|
|
173
|
-
this.ctx.statusLine.
|
|
173
|
+
this.ctx.statusLine.resetActiveTime();
|
|
174
174
|
this.ctx.updateEditorTopBorder();
|
|
175
175
|
this.ctx.ui.requestRender();
|
|
176
176
|
|
|
@@ -13,9 +13,10 @@ 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 { invokeSkillCommandFromText, isKnownSkillCommand } from "../../modes/skill-command";
|
|
16
17
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
17
18
|
import manualContinuePrompt from "../../prompts/system/manual-continue.md" with { type: "text" };
|
|
18
|
-
import {
|
|
19
|
+
import { USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
19
20
|
import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
|
|
20
21
|
import { isTinyTitleLocalModelKey } from "../../tiny/models";
|
|
21
22
|
import { isLowSignalTitleInput } from "../../tiny/text";
|
|
@@ -715,11 +716,18 @@ export class InputController {
|
|
|
715
716
|
}
|
|
716
717
|
|
|
717
718
|
// Handle skill commands (/skill:name [args]). Enter ⇒ steer (matches the
|
|
718
|
-
// free-text Enter semantics
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
if (text && (
|
|
722
|
-
|
|
719
|
+
// free-text Enter semantics below); Ctrl+Enter routes through `handleFollowUp`.
|
|
720
|
+
// During compaction, queue immediately so bash/python/loop-mode branches do
|
|
721
|
+
// not consume the skill before the compaction-resume path re-parses it.
|
|
722
|
+
if (text && isKnownSkillCommand(this.ctx, text)) {
|
|
723
|
+
if (this.ctx.session.isCompacting) {
|
|
724
|
+
const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
|
|
725
|
+
this.ctx.queueCompactionMessage(text, "steer", images);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (await this.#invokeSkillCommand(text, "steer", inputImages, inputImageLinks)) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
723
731
|
}
|
|
724
732
|
|
|
725
733
|
// Handle bash command (! for normal, !! for excluded from context)
|
|
@@ -1074,59 +1082,52 @@ export class InputController {
|
|
|
1074
1082
|
|
|
1075
1083
|
/**
|
|
1076
1084
|
* Dispatch a `/skill:<name> [args]` invocation through `promptCustomMessage`
|
|
1077
|
-
* using the supplied `streamingBehavior`. Returns
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
* handling at that point would double-submit. Returns false when the text
|
|
1082
|
-
* isn't a `/skill:` prefix or the command name isn't a registered skill,
|
|
1083
|
-
* so the caller can fall through to plain-text handling (this branch
|
|
1084
|
-
* leaves the editor state untouched). `streamingBehavior` is only consulted
|
|
1085
|
-
* while the agent is streaming; the idle path of `promptCustomMessage`
|
|
1086
|
-
* ignores it.
|
|
1085
|
+
* using the supplied `streamingBehavior`. Returns false when the text is not
|
|
1086
|
+
* a registered skill command and leaves the editor state untouched. Registered
|
|
1087
|
+
* skills consume the full composer draft (text plus pending images) before
|
|
1088
|
+
* dispatch; if dispatch rejects, the draft is restored so the user can retry.
|
|
1087
1089
|
*/
|
|
1088
|
-
async #invokeSkillCommand(
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
if (!
|
|
1095
|
-
|
|
1096
|
-
|
|
1090
|
+
async #invokeSkillCommand(
|
|
1091
|
+
text: string,
|
|
1092
|
+
streamingBehavior: "steer" | "followUp",
|
|
1093
|
+
images?: ImageContent[],
|
|
1094
|
+
imageLinks?: (string | undefined)[],
|
|
1095
|
+
): Promise<boolean> {
|
|
1096
|
+
if (!isKnownSkillCommand(this.ctx, text)) return false;
|
|
1097
|
+
const draftImages = images && images.length > 0 ? [...images] : undefined;
|
|
1098
|
+
const draftImageLinks = draftImages && imageLinks && imageLinks.length > 0 ? [...imageLinks] : undefined;
|
|
1099
|
+
const restoreDraft = () => {
|
|
1100
|
+
this.ctx.editor.setText(text);
|
|
1101
|
+
if (draftImages && draftImages.length > 0) {
|
|
1102
|
+
this.ctx.editor.pendingImages = [...draftImages];
|
|
1103
|
+
this.ctx.editor.pendingImageLinks = draftImageLinks
|
|
1104
|
+
? [...draftImageLinks]
|
|
1105
|
+
: draftImages.map(() => undefined);
|
|
1106
|
+
this.ctx.editor.imageLinks = this.ctx.editor.pendingImageLinks;
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
this.ctx.editor.clearDraft(text);
|
|
1097
1111
|
try {
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1112
|
+
const handled = await invokeSkillCommandFromText(this.ctx, text, streamingBehavior, {
|
|
1113
|
+
images: draftImages,
|
|
1114
|
+
propagateErrors: true,
|
|
1115
|
+
});
|
|
1116
|
+
if (!handled) {
|
|
1117
|
+
restoreDraft();
|
|
1118
|
+
return false;
|
|
1103
1119
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
lineCount: body ? body.split("\n").length : 0,
|
|
1111
|
-
};
|
|
1112
|
-
await this.ctx.session.promptCustomMessage(
|
|
1113
|
-
{
|
|
1114
|
-
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
1115
|
-
content: message,
|
|
1116
|
-
display: true,
|
|
1117
|
-
details,
|
|
1118
|
-
attribution: "user",
|
|
1119
|
-
},
|
|
1120
|
-
{ streamingBehavior, queueChipText: text },
|
|
1121
|
-
);
|
|
1120
|
+
return true;
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
restoreDraft();
|
|
1123
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
1124
|
+
return true;
|
|
1125
|
+
} finally {
|
|
1122
1126
|
if (this.ctx.session.isStreaming) {
|
|
1123
1127
|
this.ctx.updatePendingMessagesDisplay();
|
|
1124
1128
|
this.ctx.ui.requestRender();
|
|
1125
1129
|
}
|
|
1126
|
-
} catch (err) {
|
|
1127
|
-
this.ctx.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
1128
1130
|
}
|
|
1129
|
-
return true;
|
|
1130
1131
|
}
|
|
1131
1132
|
|
|
1132
1133
|
async handleRetry(): Promise<void> {
|
|
@@ -1159,9 +1160,8 @@ export class InputController {
|
|
|
1159
1160
|
// Compaction first: while compacting, free text gets queued via
|
|
1160
1161
|
// `queueCompactionMessage`, and `/skill:*` rides the same queue so a
|
|
1161
1162
|
// skill typed during compaction is not lost or short-circuited through
|
|
1162
|
-
// `promptCustomMessage`. The
|
|
1163
|
-
//
|
|
1164
|
-
// separate concern owned by the compaction-resume path.
|
|
1163
|
+
// `promptCustomMessage`. The compaction-resume path re-parses the
|
|
1164
|
+
// queued text into a user-attributed skill invocation before delivery.
|
|
1165
1165
|
if (this.ctx.session.isCompacting) {
|
|
1166
1166
|
const images = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
|
|
1167
1167
|
this.ctx.queueCompactionMessage(text, "followUp", images);
|
|
@@ -1187,7 +1187,7 @@ export class InputController {
|
|
|
1187
1187
|
// Skill commands invoke through the custom-message path regardless of
|
|
1188
1188
|
// which keybinding submitted them. Enter routes them as `steer`;
|
|
1189
1189
|
// Ctrl+Enter (this handler) routes them as `followUp`.
|
|
1190
|
-
if (text && (await this.#invokeSkillCommand(text, "followUp"))) {
|
|
1190
|
+
if (text && (await this.#invokeSkillCommand(text, "followUp", images, imageLinks))) {
|
|
1191
1191
|
return;
|
|
1192
1192
|
}
|
|
1193
1193
|
|
|
@@ -1739,14 +1739,14 @@ export class InputController {
|
|
|
1739
1739
|
}
|
|
1740
1740
|
|
|
1741
1741
|
toggleThinkingBlockVisibility(): void {
|
|
1742
|
-
// When thinking is "off"
|
|
1743
|
-
//
|
|
1744
|
-
//
|
|
1745
|
-
//
|
|
1746
|
-
//
|
|
1742
|
+
// When thinking is "off" and the session has not produced reasoning
|
|
1743
|
+
// content, thinking blocks stay auto-hidden; the toggle would only corrupt
|
|
1744
|
+
// the persisted preference. OpenAI-compatible servers can stream reasoning
|
|
1745
|
+
// without advertising model support, so observed thinking content unlocks
|
|
1746
|
+
// the display toggle.
|
|
1747
1747
|
const thinkingOff =
|
|
1748
1748
|
((this.ctx.viewSession ?? this.ctx.session)?.thinkingLevel ?? ThinkingLevel.Off) === ThinkingLevel.Off;
|
|
1749
|
-
if (thinkingOff) {
|
|
1749
|
+
if (thinkingOff && !this.ctx.hasDisplayableThinkingContent) {
|
|
1750
1750
|
this.ctx.showStatus("Thinking is off — enable thinking to show blocks");
|
|
1751
1751
|
return;
|
|
1752
1752
|
}
|