@oh-my-pi/pi-coding-agent 15.12.4 → 15.13.0
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 +304 -6
- package/dist/cli.js +1015 -881
- package/dist/types/async/job-manager.d.ts +15 -0
- package/dist/types/autolearn/controller.d.ts +25 -0
- package/dist/types/autolearn/managed-skills.d.ts +45 -0
- package/dist/types/autoresearch/state.d.ts +1 -1
- package/dist/types/autoresearch/types.d.ts +1 -1
- package/dist/types/cli/args.d.ts +19 -1
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/cli/setup-model-picker.d.ts +14 -0
- package/dist/types/collab/protocol.d.ts +1 -1
- package/dist/types/commands/say.d.ts +24 -0
- package/dist/types/config/keybindings.d.ts +3 -3
- package/dist/types/config/model-registry.d.ts +10 -0
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/models-config.d.ts +8 -2
- package/dist/types/config/settings-schema.d.ts +261 -58
- package/dist/types/export/html/index.d.ts +2 -1
- package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +47 -1
- package/dist/types/extensibility/hooks/index.d.ts +2 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
- package/dist/types/extensibility/plugins/loader.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/extensibility/skills.d.ts +10 -0
- package/dist/types/goals/guided-setup.d.ts +18 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/hindsight/transcript.d.ts +1 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/internal-urls/local-protocol.d.ts +4 -2
- package/dist/types/main.d.ts +4 -3
- package/dist/types/mcp/startup-events.d.ts +11 -0
- package/dist/types/memories/index.d.ts +7 -0
- package/dist/types/memory-backend/local-backend.d.ts +4 -3
- package/dist/types/mnemopi/config.d.ts +4 -4
- package/dist/types/modes/components/agent-hub.d.ts +6 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
- package/dist/types/modes/components/custom-editor.d.ts +39 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +26 -16
- package/dist/types/modes/components/transcript-container.d.ts +23 -2
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/usage-row.d.ts +3 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/input-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/gradient-highlight.d.ts +9 -4
- package/dist/types/modes/image-references.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +27 -3
- package/dist/types/modes/magic-keywords.d.ts +13 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
- package/dist/types/modes/runtime-init.d.ts +4 -0
- package/dist/types/modes/theme/theme.d.ts +13 -2
- package/dist/types/modes/types.d.ts +8 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-registry.d.ts +17 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +14 -2
- package/dist/types/session/indexed-session-storage.d.ts +3 -4
- package/dist/types/session/session-context.d.ts +39 -0
- package/dist/types/session/session-entries.d.ts +159 -0
- package/dist/types/session/session-listing.d.ts +69 -0
- package/dist/types/session/session-loader.d.ts +16 -0
- package/dist/types/session/session-manager.d.ts +82 -474
- package/dist/types/session/session-migrations.d.ts +12 -0
- package/dist/types/session/session-paths.d.ts +25 -0
- package/dist/types/session/session-persistence.d.ts +8 -0
- package/dist/types/session/session-storage.d.ts +11 -12
- package/dist/types/session/snapcompact-inline.d.ts +12 -1
- package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
- package/dist/types/session/tool-choice-queue.d.ts +6 -6
- package/dist/types/stt/asr-client.d.ts +90 -0
- package/dist/types/stt/asr-protocol.d.ts +97 -0
- package/dist/types/stt/asr-worker.d.ts +2 -0
- package/dist/types/stt/downloader.d.ts +38 -0
- package/dist/types/stt/endpointer.d.ts +59 -0
- package/dist/types/stt/index.d.ts +5 -1
- package/dist/types/stt/models.d.ts +120 -0
- package/dist/types/stt/recorder.d.ts +17 -0
- package/dist/types/stt/stt-controller.d.ts +6 -0
- package/dist/types/stt/transcriber.d.ts +5 -7
- package/dist/types/stt/wav.d.ts +29 -0
- package/dist/types/system-prompt.d.ts +4 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/index.d.ts +9 -1
- package/dist/types/task/types.d.ts +36 -0
- package/dist/types/tools/bash.d.ts +2 -2
- package/dist/types/tools/eval-render.d.ts +1 -1
- package/dist/types/tools/index.d.ts +11 -1
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/learn.d.ts +51 -0
- package/dist/types/tools/manage-skill.d.ts +40 -0
- package/dist/types/tools/plan-mode-guard.d.ts +10 -0
- package/dist/types/tools/renderers.d.ts +7 -11
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +1 -1
- package/dist/types/tools/tts.d.ts +25 -0
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/tts/downloader.d.ts +20 -0
- package/dist/types/tts/index.d.ts +8 -0
- package/dist/types/tts/models.d.ts +82 -0
- package/dist/types/tts/player.d.ts +32 -0
- package/dist/types/tts/runtime.d.ts +6 -0
- package/dist/types/tts/streaming-player.d.ts +41 -0
- package/dist/types/tts/tts-client.d.ts +93 -0
- package/dist/types/tts/tts-protocol.d.ts +95 -0
- package/dist/types/tts/tts-worker.d.ts +2 -0
- package/dist/types/tts/vocalizer.d.ts +41 -0
- package/dist/types/tts/wav.d.ts +8 -0
- package/dist/types/utils/tool-choice.d.ts +8 -0
- package/dist/types/utils/tools-manager.d.ts +2 -1
- package/dist/types/utils/tools-manager.test.d.ts +1 -0
- package/dist/types/web/scrapers/github.d.ts +1 -1
- package/package.json +15 -14
- package/src/async/job-manager.ts +49 -0
- package/src/autolearn/controller.ts +139 -0
- package/src/autolearn/managed-skills.ts +257 -0
- package/src/autoresearch/state.ts +1 -1
- package/src/autoresearch/types.ts +1 -1
- package/src/cli/args.ts +56 -2
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +148 -47
- package/src/cli/setup-model-picker.ts +43 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +45 -13
- package/src/collab/host.ts +1 -1
- package/src/collab/protocol.ts +1 -1
- package/src/commands/say.ts +102 -0
- package/src/commands/setup.ts +1 -1
- package/src/commit/agentic/tools/analyze-file.ts +3 -0
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-discovery.ts +11 -5
- package/src/config/model-registry.ts +64 -9
- package/src/config/models-config-schema.ts +4 -1
- package/src/config/models-config.ts +2 -1
- package/src/config/settings-schema.ts +248 -32
- package/src/config/settings.ts +10 -0
- package/src/discovery/builtin.ts +23 -1
- package/src/discovery/claude-plugins.ts +44 -5
- package/src/discovery/helpers.ts +41 -1
- package/src/eval/__tests__/budget-bridge.test.ts +1 -1
- package/src/eval/js/shared/prelude.txt +69 -17
- package/src/export/html/index.ts +3 -6
- package/src/extensibility/extensions/model-api.ts +41 -0
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +52 -1
- package/src/extensibility/extensions/wrapper.ts +41 -5
- package/src/extensibility/hooks/index.ts +2 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
- package/src/extensibility/plugins/loader.ts +30 -19
- package/src/extensibility/plugins/manager.ts +221 -90
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +96 -15
- package/src/goals/guided-setup.ts +133 -0
- package/src/goals/state.ts +1 -1
- package/src/hindsight/transcript.ts +1 -1
- package/src/index.ts +5 -0
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/internal-urls/history-protocol.ts +1 -1
- package/src/internal-urls/local-protocol.ts +29 -7
- package/src/main.ts +27 -7
- package/src/mcp/startup-events.ts +21 -0
- package/src/mcp/transports/stdio.ts +2 -1
- package/src/memories/index.ts +146 -11
- package/src/memory-backend/local-backend.ts +11 -5
- package/src/mnemopi/backend.ts +1 -0
- package/src/mnemopi/config.ts +26 -10
- package/src/modes/acp/acp-agent.ts +3 -5
- package/src/modes/components/agent-hub.ts +49 -4
- package/src/modes/components/assistant-message.ts +4 -37
- package/src/modes/components/compaction-summary-message.ts +125 -26
- package/src/modes/components/custom-editor.test.ts +96 -0
- package/src/modes/components/custom-editor.ts +164 -8
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/tool-execution.ts +82 -43
- package/src/modes/components/transcript-container.ts +70 -1
- package/src/modes/components/tree-selector.ts +1 -1
- package/src/modes/components/usage-row.ts +18 -0
- package/src/modes/components/user-message.ts +4 -2
- package/src/modes/controllers/command-controller.ts +14 -4
- package/src/modes/controllers/event-controller.ts +78 -11
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +258 -27
- package/src/modes/controllers/selector-controller.ts +12 -2
- package/src/modes/gradient-highlight.ts +21 -9
- package/src/modes/image-references.ts +20 -0
- package/src/modes/interactive-mode.ts +286 -40
- package/src/modes/magic-keywords.ts +27 -5
- package/src/modes/rpc/rpc-mode.ts +146 -14
- package/src/modes/rpc/rpc-subagents.ts +2 -2
- package/src/modes/rpc/rpc-types.ts +8 -2
- package/src/modes/runtime-init.ts +28 -3
- package/src/modes/theme/theme.ts +98 -50
- package/src/modes/types.ts +6 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +34 -6
- package/src/priority.json +5 -1
- package/src/prompts/agents/task.md +1 -0
- package/src/prompts/goals/guided-goal-interview.md +8 -0
- package/src/prompts/goals/guided-goal-system.md +12 -0
- package/src/prompts/memories/read-path.md +6 -0
- package/src/prompts/system/autolearn-guidance-learn.md +1 -0
- package/src/prompts/system/autolearn-guidance.md +7 -0
- package/src/prompts/system/autolearn-nudge.md +3 -0
- package/src/prompts/system/eager-task.md +7 -0
- package/src/prompts/system/eager-todo.md +11 -6
- package/src/prompts/system/subagent-system-prompt.md +4 -0
- package/src/prompts/system/system-prompt.md +10 -5
- package/src/prompts/system/title-marker-instruction.md +1 -0
- package/src/prompts/system/title-system-marker.md +16 -0
- package/src/prompts/tools/job.md +1 -0
- package/src/prompts/tools/learn.md +7 -0
- package/src/prompts/tools/manage-skill.md +9 -0
- package/src/prompts/tools/task.md +3 -0
- package/src/registry/agent-registry.ts +30 -0
- package/src/sdk.ts +88 -24
- package/src/secrets/obfuscator.ts +1 -1
- package/src/session/agent-session.ts +209 -87
- package/src/session/history-storage.ts +2 -2
- package/src/session/indexed-session-storage.ts +7 -17
- package/src/session/session-context.ts +352 -0
- package/src/session/session-entries.ts +194 -0
- package/src/session/session-listing.ts +588 -0
- package/src/session/session-loader.ts +106 -0
- package/src/session/session-manager.ts +933 -3145
- package/src/session/session-migrations.ts +78 -0
- package/src/session/session-paths.ts +193 -0
- package/src/session/session-persistence.ts +131 -0
- package/src/session/session-storage.ts +91 -50
- package/src/session/snapcompact-inline.ts +21 -1
- package/src/session/snapcompact-savings-journal.ts +113 -0
- package/src/session/tool-choice-queue.ts +23 -11
- package/src/slash-commands/builtin-registry.ts +25 -3
- package/src/stt/asr-client.ts +520 -0
- package/src/stt/asr-protocol.ts +65 -0
- package/src/stt/asr-worker.ts +790 -0
- package/src/stt/downloader.ts +107 -47
- package/src/stt/endpointer.ts +259 -0
- package/src/stt/index.ts +5 -1
- package/src/stt/models.ts +150 -0
- package/src/stt/recorder.ts +247 -60
- package/src/stt/stt-controller.ts +201 -22
- package/src/stt/transcriber.ts +37 -68
- package/src/stt/wav.ts +173 -0
- package/src/system-prompt.ts +8 -0
- package/src/task/agents.ts +1 -2
- package/src/task/executor.ts +49 -15
- package/src/task/index.ts +60 -6
- package/src/task/render.ts +83 -8
- package/src/task/types.ts +53 -0
- package/src/tools/ask.ts +8 -0
- package/src/tools/bash.ts +4 -3
- package/src/tools/eval-render.ts +4 -3
- package/src/tools/index.ts +40 -4
- package/src/tools/irc.ts +10 -2
- package/src/tools/job.ts +14 -2
- package/src/tools/learn.ts +144 -0
- package/src/tools/manage-skill.ts +104 -0
- package/src/tools/plan-mode-guard.ts +53 -19
- package/src/tools/renderers.ts +7 -11
- package/src/tools/ssh.ts +4 -3
- package/src/tools/todo.ts +1 -1
- package/src/tools/tts.ts +203 -92
- package/src/tools/write.ts +18 -2
- package/src/tts/downloader.ts +64 -0
- package/src/tts/index.ts +8 -0
- package/src/tts/models.ts +137 -0
- package/src/tts/player.ts +137 -0
- package/src/tts/runtime.ts +21 -0
- package/src/tts/streaming-player.ts +266 -0
- package/src/tts/tts-client.ts +647 -0
- package/src/tts/tts-protocol.ts +60 -0
- package/src/tts/tts-worker.ts +497 -0
- package/src/tts/vocalizer.ts +162 -0
- package/src/tts/wav.ts +58 -0
- package/src/utils/title-generator.ts +48 -5
- package/src/utils/tool-choice.ts +16 -0
- package/src/utils/tools-manager.test.ts +25 -0
- package/src/utils/tools-manager.ts +19 -1
- package/src/web/scrapers/github.ts +96 -0
- package/src/web/search/index.ts +13 -0
- package/src/web/search/providers/searxng.ts +13 -1
- package/dist/types/stt/setup.d.ts +0 -18
- package/src/stt/setup.ts +0 -52
- package/src/stt/transcribe.py +0 -70
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Image,
|
|
9
9
|
ImageProtocol,
|
|
10
10
|
imageFallback,
|
|
11
|
+
type NativeScrollbackLiveRegion,
|
|
11
12
|
Spacer,
|
|
12
13
|
TERMINAL,
|
|
13
14
|
Text,
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
import { getProjectDir, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
17
18
|
import { EDIT_MODE_STRATEGIES, type EditMode, type PerFileDiffPreview } from "../../edit";
|
|
18
19
|
import type { Theme } from "../../modes/theme/theme";
|
|
19
|
-
import { theme } from "../../modes/theme/theme";
|
|
20
|
+
import { getThemeEpoch, theme } from "../../modes/theme/theme";
|
|
20
21
|
import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
|
|
21
22
|
import { EVAL_DEFAULT_PREVIEW_LINES } from "../../tools/eval";
|
|
22
23
|
import { isWaitingPollDetails } from "../../tools/job";
|
|
@@ -148,10 +149,16 @@ export interface ToolExecutionHandle {
|
|
|
148
149
|
/** Drive pending-tool redraws at 30fps for live tool headers and displaceable
|
|
149
150
|
* poll blocks. The TUI throttles at the same cadence, and static frames diff to
|
|
150
151
|
* a no-op redraw at ~zero cost. */
|
|
151
|
-
const SPINNER_RENDER_INTERVAL_MS = 1000 / 30;
|
|
152
|
+
export const SPINNER_RENDER_INTERVAL_MS = 1000 / 30;
|
|
152
153
|
/** Advance the spinner glyph at its classic ~12.5fps step, decoupled from the
|
|
153
154
|
* render cadence (mirrors `Loader`). */
|
|
154
|
-
const SPINNER_GLYPH_ADVANCE_MS = 80;
|
|
155
|
+
export const SPINNER_GLYPH_ADVANCE_MS = 80;
|
|
156
|
+
|
|
157
|
+
/** Phase-locked spinner glyph index shared by every live tool block so parallel
|
|
158
|
+
* spinners advance in lockstep instead of each tracking its own start time. */
|
|
159
|
+
export function sharedSpinnerFrame(frameCount: number, now: number = performance.now()): number {
|
|
160
|
+
return frameCount > 0 ? Math.floor(now / SPINNER_GLYPH_ADVANCE_MS) % frameCount : 0;
|
|
161
|
+
}
|
|
155
162
|
|
|
156
163
|
// Stable per-instance counter so each tool execution's inline images get a
|
|
157
164
|
// graphics id that survives child re-creation (the image budget keys off it).
|
|
@@ -160,7 +167,7 @@ let toolExecutionInstanceSeq = 0;
|
|
|
160
167
|
/**
|
|
161
168
|
* Component that renders a tool call with its result (updateable)
|
|
162
169
|
*/
|
|
163
|
-
export class ToolExecutionComponent extends Container {
|
|
170
|
+
export class ToolExecutionComponent extends Container implements NativeScrollbackLiveRegion {
|
|
164
171
|
#contentBox: Box; // Used for custom tools and bash visual truncation
|
|
165
172
|
#contentText: Text; // For built-in tools (with its own padding/bg)
|
|
166
173
|
#multiFileBoxes: (Box | Spacer)[] = []; // Extra boxes for multi-file edit results
|
|
@@ -176,6 +183,22 @@ export class ToolExecutionComponent extends Container {
|
|
|
176
183
|
#editAllowFuzzy: boolean | undefined;
|
|
177
184
|
#snapshots?: SnapshotStore;
|
|
178
185
|
#isPartial = true;
|
|
186
|
+
#resultVersion = 0;
|
|
187
|
+
#lastDisplayKey: string | undefined;
|
|
188
|
+
// Bumped whenever a render input that #rebuildDisplay consumes but the memo
|
|
189
|
+
// key cannot cheaply hash changes: streamed call args, the async edit-diff
|
|
190
|
+
// preview, and Kitty PNG conversions. Folded into the dirty key so those
|
|
191
|
+
// updates are not swallowed by the memo (see #updateDisplay).
|
|
192
|
+
#displayInputVersion = 0;
|
|
193
|
+
// Set once #rebuildDisplay has populated the display. Replaces a
|
|
194
|
+
// #contentBox.children.length probe so the memo fast-path also covers the
|
|
195
|
+
// #contentText fallback path (which leaves #contentBox empty).
|
|
196
|
+
#displayBuilt = false;
|
|
197
|
+
// Number of Image children the last rebuild emitted. Only when this is > 0 does
|
|
198
|
+
// the memo key fold in viewport-dependent image sizing (resolveImageOptions),
|
|
199
|
+
// so a terminal resize re-shapes image-bearing results to rescale them without
|
|
200
|
+
// forcing the common image-free result to re-shape on every resize tick.
|
|
201
|
+
#renderedImageCount = 0;
|
|
179
202
|
#tool?: AgentTool;
|
|
180
203
|
#ui: TUI;
|
|
181
204
|
#cwd: string;
|
|
@@ -196,7 +219,6 @@ export class ToolExecutionComponent extends Container {
|
|
|
196
219
|
// Spinner animation for partial task results
|
|
197
220
|
#spinnerFrame?: number;
|
|
198
221
|
#spinnerInterval?: NodeJS.Timeout;
|
|
199
|
-
#lastSpinnerAdvanceAt = 0;
|
|
200
222
|
// Todo write completion strikethrough reveal animation
|
|
201
223
|
#todoStrikeInterval?: NodeJS.Timeout;
|
|
202
224
|
// Track if args are still being streamed (for edit/write spinner)
|
|
@@ -281,6 +303,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
281
303
|
// signals "nothing meaningful changed" and the renderer can skip.
|
|
282
304
|
if (args === this.#args) return;
|
|
283
305
|
this.#args = args;
|
|
306
|
+
this.#displayInputVersion++;
|
|
284
307
|
this.#updateSpinnerAnimation();
|
|
285
308
|
this.#editDiffInFlight = this.#runPreviewDiff();
|
|
286
309
|
this.#updateDisplay();
|
|
@@ -365,6 +388,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
365
388
|
if (controller.signal.aborted) return;
|
|
366
389
|
if (previews) {
|
|
367
390
|
this.#editDiffPreview = isStreaming ? stabilizeStreamingPreviews(previews) : previews;
|
|
391
|
+
this.#displayInputVersion++;
|
|
368
392
|
this.#updateDisplay();
|
|
369
393
|
this.#ui.requestRender();
|
|
370
394
|
}
|
|
@@ -393,6 +417,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
393
417
|
return;
|
|
394
418
|
}
|
|
395
419
|
this.#result = result;
|
|
420
|
+
this.#resultVersion++;
|
|
396
421
|
this.#isPartial = isPartial;
|
|
397
422
|
// A `job` poll that found every watched job still running is transient
|
|
398
423
|
// "still waiting" chrome; keep the block displaceable so the next `job`
|
|
@@ -446,6 +471,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
446
471
|
.toBase64()
|
|
447
472
|
.then(data => {
|
|
448
473
|
this.#convertedImages.set(index, { data, mimeType: "image/png" });
|
|
474
|
+
this.#displayInputVersion++;
|
|
449
475
|
this.#updateDisplay();
|
|
450
476
|
this.#ui.requestRender();
|
|
451
477
|
})
|
|
@@ -470,32 +496,18 @@ export class ToolExecutionComponent extends Container {
|
|
|
470
496
|
// once the block leaves the live region.
|
|
471
497
|
const needsSpinner = isStreamingArgs || isPartialTask || this.isDisplaceableBlock();
|
|
472
498
|
if (needsSpinner && !this.#spinnerInterval) {
|
|
473
|
-
const now = performance.now();
|
|
474
499
|
const frameCount = theme.spinnerFrames.length;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
this.#renderState.spinnerFrame = 0;
|
|
479
|
-
}
|
|
500
|
+
const frame = sharedSpinnerFrame(frameCount);
|
|
501
|
+
this.#spinnerFrame = frame;
|
|
502
|
+
this.#renderState.spinnerFrame = frame;
|
|
480
503
|
this.#spinnerInterval = setInterval(() => {
|
|
481
504
|
// If a detached task interval from an older render path is still live,
|
|
482
505
|
// stop it the instant the block leaves the repaintable region.
|
|
483
506
|
if (this.#maybeFreezeBackgroundTask()) return;
|
|
484
507
|
const now = performance.now();
|
|
485
508
|
const frameCount = theme.spinnerFrames.length;
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
// instead of resetting to `now` avoids the 30fps timer quantizing the
|
|
489
|
-
// glyph down to one step every three ticks.
|
|
490
|
-
if (frameCount > 0) {
|
|
491
|
-
const elapsed = now - this.#lastSpinnerAdvanceAt;
|
|
492
|
-
if (elapsed >= SPINNER_GLYPH_ADVANCE_MS) {
|
|
493
|
-
const steps = Math.floor(elapsed / SPINNER_GLYPH_ADVANCE_MS);
|
|
494
|
-
this.#spinnerFrame = ((this.#spinnerFrame ?? 0) + steps) % frameCount;
|
|
495
|
-
this.#renderState.spinnerFrame = this.#spinnerFrame;
|
|
496
|
-
this.#lastSpinnerAdvanceAt += steps * SPINNER_GLYPH_ADVANCE_MS;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
509
|
+
this.#spinnerFrame = sharedSpinnerFrame(frameCount, now);
|
|
510
|
+
this.#renderState.spinnerFrame = this.#spinnerFrame;
|
|
499
511
|
this.#ui.requestRender();
|
|
500
512
|
}, SPINNER_RENDER_INTERVAL_MS);
|
|
501
513
|
} else if (!needsSpinner && this.#spinnerInterval) {
|
|
@@ -568,6 +580,17 @@ export class ToolExecutionComponent extends Container {
|
|
|
568
580
|
}
|
|
569
581
|
}
|
|
570
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Standalone harnesses may mount a tool component directly under `TUI`
|
|
585
|
+
* instead of inside `TranscriptContainer`. In that shape the component must
|
|
586
|
+
* report its own live-region seam for provisional previews, or the core
|
|
587
|
+
* renderer treats it like shell output and commits tail-window edit/eval/bash
|
|
588
|
+
* previews to immutable native scrollback before the result replaces them.
|
|
589
|
+
*/
|
|
590
|
+
getNativeScrollbackLiveRegionStart(): number | undefined {
|
|
591
|
+
return !this.isTranscriptBlockFinalized() && !this.isTranscriptBlockCommitStable() ? 0 : undefined;
|
|
592
|
+
}
|
|
593
|
+
|
|
571
594
|
/**
|
|
572
595
|
* Whether this block has reached a terminal state for transcript freezing.
|
|
573
596
|
* Reports `false` while it can still visually change so the
|
|
@@ -591,28 +614,20 @@ export class ToolExecutionComponent extends Container {
|
|
|
591
614
|
|
|
592
615
|
/**
|
|
593
616
|
* Whether this still-live block's settled rows may enter native scrollback
|
|
594
|
-
* (see `FinalizableBlock.isTranscriptBlockCommitStable`).
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
* the call box above the final block the moment the result lands. Every
|
|
601
|
-
* other pending preview streams top-anchored append-shaped rows the
|
|
602
|
-
* result render preserves (a task call's context/assignment markdown, a
|
|
603
|
-
* write's content), so it stays commit-eligible — a call taller than the
|
|
604
|
-
* viewport scrolls into native history mid-stream instead of reading as
|
|
605
|
-
* cut off until the result. Expanded blocks always stream top-anchored
|
|
606
|
-
* (the over-tall write/eval scrollback contract). Displaceable waiting
|
|
607
|
-
* polls are removed wholesale by the next poll and must never commit.
|
|
617
|
+
* (see `FinalizableBlock.isTranscriptBlockCommitStable`). Renderers classify
|
|
618
|
+
* pending views by durability instead of by tool name: a provisional view is
|
|
619
|
+
* allowed to be useful on screen, but finalization may replace or re-anchor
|
|
620
|
+
* it wholesale, so committing any of its rows would strand stale preview
|
|
621
|
+
* bytes in immutable scrollback. Non-provisional views stream rows whose
|
|
622
|
+
* committed prefix survives the remaining transitions.
|
|
608
623
|
*/
|
|
609
624
|
isTranscriptBlockCommitStable(): boolean {
|
|
610
625
|
if (this.#displaceable) return false;
|
|
611
|
-
if (this
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
return
|
|
626
|
+
if (this.isTranscriptBlockFinalized()) return true;
|
|
627
|
+
const tool = this.#tool as { provisionalPendingPreview?: boolean | "collapsed" } | undefined;
|
|
628
|
+
const provisionalPendingPreview =
|
|
629
|
+
tool?.provisionalPendingPreview ?? toolRenderers[this.#toolName]?.provisionalPendingPreview;
|
|
630
|
+
return provisionalPendingPreview !== true && (provisionalPendingPreview !== "collapsed" || this.#expanded);
|
|
616
631
|
}
|
|
617
632
|
|
|
618
633
|
/**
|
|
@@ -674,6 +689,29 @@ export class ToolExecutionComponent extends Container {
|
|
|
674
689
|
}
|
|
675
690
|
|
|
676
691
|
#updateDisplay(): void {
|
|
692
|
+
// `TERMINAL.imageProtocol` is resolved by an async capability probe during
|
|
693
|
+
// TUI startup, so a result rendered before it lands must re-shape once it
|
|
694
|
+
// does (it gates Image children vs text fallback in #rebuildDisplay); keyed
|
|
695
|
+
// here for the same reason markdown.ts keys its render cache on it.
|
|
696
|
+
const key = `${this.#resultVersion}|${this.#expanded}|${this.#isPartial}|${this.#spinnerFrame ?? "-"}|${this.#showImages}|${getThemeEpoch()}|${this.#displayInputVersion}|${this.#backgroundTaskFrozen}|${TERMINAL.imageProtocol ?? "-"}|${this.#imageSizeKey()}`;
|
|
697
|
+
if (key === this.#lastDisplayKey && this.#displayBuilt) return;
|
|
698
|
+
this.#lastDisplayKey = key;
|
|
699
|
+
|
|
700
|
+
this.#rebuildDisplay();
|
|
701
|
+
this.#displayBuilt = true;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Viewport-/settings-dependent image sizing folded into the memo key only when
|
|
705
|
+
// the last rebuild actually emitted images, so a terminal resize re-shapes an
|
|
706
|
+
// image-bearing result (to rescale it) without re-shaping every image-free
|
|
707
|
+
// result on each resize tick.
|
|
708
|
+
#imageSizeKey(): string {
|
|
709
|
+
if (this.#renderedImageCount === 0) return "-";
|
|
710
|
+
const o = resolveImageOptions();
|
|
711
|
+
return `${o.maxWidthCells}:${o.maxHeightCells ?? "-"}`;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
#rebuildDisplay(): void {
|
|
677
715
|
// Sync shared mutable render state for component closures
|
|
678
716
|
this.#renderState.expanded = this.#expanded;
|
|
679
717
|
this.#renderState.isPartial = this.#isPartial;
|
|
@@ -917,6 +955,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
917
955
|
}
|
|
918
956
|
}
|
|
919
957
|
}
|
|
958
|
+
this.#renderedImageCount = this.#imageComponents.length;
|
|
920
959
|
}
|
|
921
960
|
|
|
922
961
|
#getCallArgsForRender(): any {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type NativeScrollbackCommittedRows,
|
|
5
5
|
type NativeScrollbackLiveRegion,
|
|
6
6
|
type RenderStablePrefix,
|
|
7
|
+
type ViewportTailProvider,
|
|
7
8
|
} from "@oh-my-pi/pi-tui";
|
|
8
9
|
|
|
9
10
|
const kSnapshot = Symbol("transcript.liveDiffSnapshot");
|
|
@@ -139,6 +140,8 @@ interface BlockSegment {
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
const EMPTY_SEGMENTS: BlockSegment[] = [];
|
|
143
|
+
/** Shared empty result for an empty viewport-tail render (no allocation). */
|
|
144
|
+
const EMPTY_TAIL: readonly string[] = [];
|
|
142
145
|
|
|
143
146
|
interface LiveCommitState {
|
|
144
147
|
appendOnly: boolean;
|
|
@@ -415,7 +418,7 @@ function deriveLiveCommitState(
|
|
|
415
418
|
*/
|
|
416
419
|
export class TranscriptContainer
|
|
417
420
|
extends Container
|
|
418
|
-
implements NativeScrollbackLiveRegion, NativeScrollbackCommittedRows, RenderStablePrefix
|
|
421
|
+
implements NativeScrollbackLiveRegion, NativeScrollbackCommittedRows, RenderStablePrefix, ViewportTailProvider
|
|
419
422
|
{
|
|
420
423
|
// Bumped to retire every block's diff snapshot at once (theme change /
|
|
421
424
|
// clear); a snapshot is only honored when its stored generation matches.
|
|
@@ -432,6 +435,14 @@ export class TranscriptContainer
|
|
|
432
435
|
// until it re-earns append-only via VOLATILE_REARM_FRAMES clean frames;
|
|
433
436
|
// the engine then backfills the stalled gap.
|
|
434
437
|
#nativeScrollbackCommitSafeEnd: number | undefined;
|
|
438
|
+
// Local line index up to which the leading run of live blocks is DURABLE: a
|
|
439
|
+
// commit-stable block's full body is permanent content even while its interior
|
|
440
|
+
// rows re-lay-out (a streaming markdown table re-aligning columns), so the
|
|
441
|
+
// engine must append their scroll-off snapshot rather than drop it. Reported
|
|
442
|
+
// separately from the byte-stable commit-safe end because these rows may still
|
|
443
|
+
// drift after commit; the engine commits them audit-exempt. Provisional
|
|
444
|
+
// (commit-unstable) blocks never extend it.
|
|
445
|
+
#nativeScrollbackSnapshotSafeEnd: number | undefined;
|
|
435
446
|
// Persistent assembled transcript rows. Rows before the stable floor are
|
|
436
447
|
// byte-identical to the previous render; rows at/after it were re-pushed.
|
|
437
448
|
#lines: string[] = [];
|
|
@@ -476,6 +487,10 @@ export class TranscriptContainer
|
|
|
476
487
|
return this.#nativeScrollbackCommitSafeEnd;
|
|
477
488
|
}
|
|
478
489
|
|
|
490
|
+
getNativeScrollbackSnapshotSafeEnd(): number | undefined {
|
|
491
|
+
return this.#nativeScrollbackSnapshotSafeEnd;
|
|
492
|
+
}
|
|
493
|
+
|
|
479
494
|
/**
|
|
480
495
|
* Whether `component` sits below a still-mutating block — i.e. inside the
|
|
481
496
|
* live region, where its rows cannot have been committed to native
|
|
@@ -520,10 +535,54 @@ export class TranscriptContainer
|
|
|
520
535
|
return index === children.length - 1;
|
|
521
536
|
}
|
|
522
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Render only the bottom `maxRows` rows of the transcript at `width`, walking
|
|
540
|
+
* blocks from the last toward the first and stopping the instant enough rows
|
|
541
|
+
* are collected — blocks above the fold are never rendered. The engine's
|
|
542
|
+
* resize viewport fast path uses this so a drag (a SIGWINCH burst, each event
|
|
543
|
+
* a fresh width that misses every per-width cache) re-lays-out only the
|
|
544
|
+
* handful of visible blocks instead of the whole history every event.
|
|
545
|
+
*
|
|
546
|
+
* State-isolated by contract: touches none of the persistent full-compose
|
|
547
|
+
* fields (#lines, #segments, the per-block diff snapshots, the commit/stable
|
|
548
|
+
* bookkeeping), so the authoritative full render on settle reconciles exactly
|
|
549
|
+
* as if this never ran. Calling each block's render() still warms its own
|
|
550
|
+
* per-width cache, which that settle render then reuses for free.
|
|
551
|
+
*
|
|
552
|
+
* Consecutive visible blocks are joined by exactly one blank separator, the
|
|
553
|
+
* same rule render() applies, so the result equals the bottom of a full
|
|
554
|
+
* render except for an at-most-one-row separator on the topmost included
|
|
555
|
+
* block — a transient discrepancy the settle paint overwrites.
|
|
556
|
+
*/
|
|
557
|
+
renderViewportTail(width: number, maxRows: number): readonly string[] {
|
|
558
|
+
width = Math.max(1, width);
|
|
559
|
+
if (maxRows <= 0) return EMPTY_TAIL;
|
|
560
|
+
const collected: (readonly string[])[] = [];
|
|
561
|
+
let total = 0;
|
|
562
|
+
for (let i = this.children.length - 1; i >= 0 && total < maxRows; i--) {
|
|
563
|
+
const contribution = stripPlainBlankEdges(this.children[i]!.render(width));
|
|
564
|
+
if (contribution.length === 0) continue;
|
|
565
|
+
// One blank separator sits between this block and the (already
|
|
566
|
+
// collected) visible block below it.
|
|
567
|
+
if (collected.length > 0) total += 1;
|
|
568
|
+
collected.push(contribution);
|
|
569
|
+
total += contribution.length;
|
|
570
|
+
}
|
|
571
|
+
if (collected.length === 0) return EMPTY_TAIL;
|
|
572
|
+
const rows: string[] = [];
|
|
573
|
+
for (let k = collected.length - 1; k >= 0; k--) {
|
|
574
|
+
if (rows.length > 0) rows.push("");
|
|
575
|
+
const body = collected[k]!;
|
|
576
|
+
for (let j = 0; j < body.length; j++) rows.push(body[j]!);
|
|
577
|
+
}
|
|
578
|
+
return rows.length > maxRows ? rows.slice(rows.length - maxRows) : rows;
|
|
579
|
+
}
|
|
580
|
+
|
|
523
581
|
override render(width: number): readonly string[] {
|
|
524
582
|
width = Math.max(1, width);
|
|
525
583
|
this.#nativeScrollbackLiveRegionStart = undefined;
|
|
526
584
|
this.#nativeScrollbackCommitSafeEnd = undefined;
|
|
585
|
+
this.#nativeScrollbackSnapshotSafeEnd = undefined;
|
|
527
586
|
|
|
528
587
|
const count = this.children.length;
|
|
529
588
|
|
|
@@ -696,6 +755,16 @@ export class TranscriptContainer
|
|
|
696
755
|
if (safeLength > 0) {
|
|
697
756
|
this.#nativeScrollbackCommitSafeEnd = blockStart + safeLength;
|
|
698
757
|
}
|
|
758
|
+
// Durable snapshot end: a commit-stable block's whole body is durable
|
|
759
|
+
// content — its scrolled-off rows are permanent even while interior
|
|
760
|
+
// rows re-lay-out (a streaming table re-aligning columns), so the
|
|
761
|
+
// engine must commit their snapshot on scroll-off rather than drop it.
|
|
762
|
+
// Finalized blocks are wholly durable; provisional (commit-unstable)
|
|
763
|
+
// blocks offer nothing beyond their byte-stable safe length.
|
|
764
|
+
const snapshotLength = finalized || isBlockCommitStable(child) ? contribution.length : safeLength;
|
|
765
|
+
if (snapshotLength > 0) {
|
|
766
|
+
this.#nativeScrollbackSnapshotSafeEnd = blockStart + snapshotLength;
|
|
767
|
+
}
|
|
699
768
|
// A finalized, fully safe block may let the contiguous safe run extend
|
|
700
769
|
// into blocks rendered below it. A still-live block keeps pushing lower
|
|
701
770
|
// rows around as it grows, so the run closes there.
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import type { TreeFilterMode } from "../../config/settings-schema";
|
|
16
16
|
import { theme } from "../../modes/theme/theme";
|
|
17
17
|
import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
18
|
-
import type { SessionTreeNode } from "../../session/session-
|
|
18
|
+
import type { SessionTreeNode } from "../../session/session-entries";
|
|
19
19
|
import { shortenPath } from "../../tools/render-utils";
|
|
20
20
|
import { toPathList } from "../../tools/search";
|
|
21
21
|
import { DynamicBorder } from "./dynamic-border";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { theme } from "../../modes/theme/theme";
|
|
5
|
+
|
|
6
|
+
export function createUsageRowBlock(usage: Usage): Container {
|
|
7
|
+
const totalInput = usage.input + usage.cacheWrite;
|
|
8
|
+
const parts: string[] = [];
|
|
9
|
+
parts.push(`${theme.icon.input} ${formatNumber(totalInput)}`);
|
|
10
|
+
parts.push(`${theme.icon.output} ${formatNumber(usage.output)}`);
|
|
11
|
+
if (usage.cacheRead > 0) {
|
|
12
|
+
parts.push(`cache: ${formatNumber(usage.cacheRead)}`);
|
|
13
|
+
}
|
|
14
|
+
const block = new Container();
|
|
15
|
+
block.addChild(new Spacer(1));
|
|
16
|
+
block.addChild(new Text(theme.fg("dim", parts.join(" ")), 1, 0));
|
|
17
|
+
return block;
|
|
18
|
+
}
|
|
@@ -4,9 +4,11 @@ import { imageReferenceHyperlink, renderPlaceholders } from "../image-references
|
|
|
4
4
|
import { highlightMagicKeywords } from "../magic-keywords";
|
|
5
5
|
|
|
6
6
|
// OSC 133 shell integration: marks prompt zones for terminal multiplexers
|
|
7
|
+
// Do not emit OSC 133 C ("command start") here: the transcript has no matching
|
|
8
|
+
// command-finished marker, so terminals can group later assistant/tool output
|
|
9
|
+
// under the first submitted prompt.
|
|
7
10
|
const OSC133_ZONE_START = "\x1b]133;A\x07";
|
|
8
11
|
const OSC133_ZONE_END = "\x1b]133;B\x07";
|
|
9
|
-
const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Component that renders a user message
|
|
@@ -58,7 +60,7 @@ export class UserMessageComponent extends Container {
|
|
|
58
60
|
}
|
|
59
61
|
const wrapped = lines.slice();
|
|
60
62
|
wrapped[0] = OSC133_ZONE_START + wrapped[0];
|
|
61
|
-
wrapped[wrapped.length - 1] = wrapped[wrapped.length - 1] + OSC133_ZONE_END
|
|
63
|
+
wrapped[wrapped.length - 1] = wrapped[wrapped.length - 1] + OSC133_ZONE_END;
|
|
62
64
|
this.#zoneSource = lines;
|
|
63
65
|
this.#zoneLines = wrapped;
|
|
64
66
|
return wrapped;
|
|
@@ -38,7 +38,7 @@ import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
|
|
|
38
38
|
import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
|
|
39
39
|
import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
|
40
40
|
import type { AuthStorage, OAuthAccountIdentity } from "../../session/auth-storage";
|
|
41
|
-
import type { NewSessionOptions } from "../../session/session-
|
|
41
|
+
import type { NewSessionOptions } from "../../session/session-entries";
|
|
42
42
|
import { formatShakeSummary, type ShakeMode, type ShakeResult } from "../../session/shake-types";
|
|
43
43
|
import { limitMatchesActiveAccount } from "../../slash-commands/helpers/active-oauth-account";
|
|
44
44
|
import { outputMeta } from "../../tools/output-meta";
|
|
@@ -965,7 +965,10 @@ export class CommandController {
|
|
|
965
965
|
this.ctx.ui.requestRender();
|
|
966
966
|
}
|
|
967
967
|
|
|
968
|
-
async handleCompactCommand(
|
|
968
|
+
async handleCompactCommand(
|
|
969
|
+
customInstructions?: string,
|
|
970
|
+
beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>,
|
|
971
|
+
): Promise<CompactionOutcome> {
|
|
969
972
|
const entries = this.ctx.sessionManager.getEntries();
|
|
970
973
|
const messageCount = entries.filter(e => e.type === "message").length;
|
|
971
974
|
|
|
@@ -974,7 +977,7 @@ export class CommandController {
|
|
|
974
977
|
return "ok";
|
|
975
978
|
}
|
|
976
979
|
|
|
977
|
-
return this.executeCompaction(customInstructions, false);
|
|
980
|
+
return this.executeCompaction(customInstructions, false, beforeFlush);
|
|
978
981
|
}
|
|
979
982
|
|
|
980
983
|
/**
|
|
@@ -1019,6 +1022,7 @@ export class CommandController {
|
|
|
1019
1022
|
async executeCompaction(
|
|
1020
1023
|
customInstructionsOrOptions?: string | CompactOptions,
|
|
1021
1024
|
isAuto = false,
|
|
1025
|
+
beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>,
|
|
1022
1026
|
): Promise<CompactionOutcome> {
|
|
1023
1027
|
if (this.ctx.loadingAnimation) {
|
|
1024
1028
|
this.ctx.loadingAnimation.stop();
|
|
@@ -1026,7 +1030,6 @@ export class CommandController {
|
|
|
1026
1030
|
}
|
|
1027
1031
|
this.ctx.statusContainer.clear();
|
|
1028
1032
|
|
|
1029
|
-
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
1030
1033
|
const label = isAuto ? "Auto-compacting context... (esc to cancel)" : "Compacting context... (esc to cancel)";
|
|
1031
1034
|
const compactingLoader = new Loader(
|
|
1032
1035
|
this.ctx.ui,
|
|
@@ -1047,6 +1050,8 @@ export class CommandController {
|
|
|
1047
1050
|
: undefined;
|
|
1048
1051
|
await this.ctx.session.compact(instructions, options);
|
|
1049
1052
|
|
|
1053
|
+
compactingLoader.stop();
|
|
1054
|
+
this.ctx.statusContainer.clear();
|
|
1050
1055
|
this.ctx.rebuildChatFromMessages();
|
|
1051
1056
|
|
|
1052
1057
|
this.ctx.statusLine.invalidate();
|
|
@@ -1064,6 +1069,11 @@ export class CommandController {
|
|
|
1064
1069
|
compactingLoader.stop();
|
|
1065
1070
|
this.ctx.statusContainer.clear();
|
|
1066
1071
|
}
|
|
1072
|
+
// Run the caller's pre-flush hook (e.g. the plan-approval model transition)
|
|
1073
|
+
// before queued user input is dispatched, so any turn queued during
|
|
1074
|
+
// compaction executes on the post-compaction model rather than the model
|
|
1075
|
+
// compaction itself ran on.
|
|
1076
|
+
if (beforeFlush) await beforeFlush(outcome);
|
|
1067
1077
|
await this.ctx.flushCompactionQueue({ willRetry: false });
|
|
1068
1078
|
return outcome;
|
|
1069
1079
|
}
|
|
@@ -2,6 +2,7 @@ import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import { calculatePromptTokens } from "@oh-my-pi/pi-agent-core/compaction/compaction";
|
|
3
3
|
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { extractTextContent } from "../../commit/utils";
|
|
5
6
|
import { settings } from "../../config/settings";
|
|
6
7
|
import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
|
|
7
8
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
@@ -13,12 +14,14 @@ import {
|
|
|
13
14
|
import { TodoReminderComponent } from "../../modes/components/todo-reminder";
|
|
14
15
|
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
15
16
|
import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
|
|
17
|
+
import { createUsageRowBlock } from "../../modes/components/usage-row";
|
|
16
18
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
17
19
|
import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
|
|
18
20
|
import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
|
|
19
21
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
20
22
|
import { isSilentAbort, readQueueChipText, resolveAbortLabel } from "../../session/messages";
|
|
21
23
|
import type { ResolveToolDetails } from "../../tools/resolve";
|
|
24
|
+
import { vocalizer } from "../../tts/vocalizer";
|
|
22
25
|
import { hasVisibleThinking } from "../../utils/thinking-display";
|
|
23
26
|
import { interruptHint } from "../shared";
|
|
24
27
|
import { StreamingRevealController } from "./streaming-reveal";
|
|
@@ -92,8 +95,8 @@ export class EventController {
|
|
|
92
95
|
this.#handlers = {
|
|
93
96
|
agent_start: e => this.#handleAgentStart(e),
|
|
94
97
|
agent_end: e => this.#handleAgentEnd(e),
|
|
95
|
-
turn_start: async () =>
|
|
96
|
-
turn_end: async
|
|
98
|
+
turn_start: async () => this.#handleTurnStart(),
|
|
99
|
+
turn_end: async e => this.#handleTurnEnd(e),
|
|
97
100
|
message_start: e => this.#handleMessageStart(e),
|
|
98
101
|
message_update: e => this.#handleMessageUpdate(e),
|
|
99
102
|
message_end: e => this.#handleMessageEnd(e),
|
|
@@ -430,7 +433,47 @@ export class EventController {
|
|
|
430
433
|
}
|
|
431
434
|
}
|
|
432
435
|
|
|
436
|
+
/** A new turn interrupts any speech still queued/playing from the previous one. */
|
|
437
|
+
#handleTurnStart(): void {
|
|
438
|
+
vocalizer.clear();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Speak streamed assistant output as a side effect of the turn. The mode
|
|
443
|
+
* decides which deltas feed the vocalizer (the vocalizer re-checks enabled):
|
|
444
|
+
* assistant|all speak text; all also speaks thinking; yield speaks nothing
|
|
445
|
+
* live (the final message is spoken at turn end).
|
|
446
|
+
*/
|
|
447
|
+
#vocalizeDelta(event: Extract<AgentSessionEvent, { type: "message_update" }>): void {
|
|
448
|
+
if (!settings.get("speech.enabled")) return;
|
|
449
|
+
const mode = settings.get("speech.mode");
|
|
450
|
+
const delta = event.assistantMessageEvent;
|
|
451
|
+
if (delta.type === "text_delta" && (mode === "assistant" || mode === "all")) {
|
|
452
|
+
vocalizer.pushDelta(delta.delta);
|
|
453
|
+
} else if (delta.type === "thinking_delta" && mode === "all") {
|
|
454
|
+
vocalizer.pushDelta(delta.delta);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* End-of-turn vocalization: yield mode speaks the final assistant message in
|
|
460
|
+
* one shot here (the only mode that is post-hoc); every other mode just makes
|
|
461
|
+
* sure the live buffer's trailing partial gets flushed.
|
|
462
|
+
*/
|
|
463
|
+
#handleTurnEnd(event: Extract<AgentSessionEvent, { type: "turn_end" }>): void {
|
|
464
|
+
if (!settings.get("speech.enabled")) return;
|
|
465
|
+
if (settings.get("speech.mode") !== "yield") {
|
|
466
|
+
vocalizer.flush();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (event.message.role !== "assistant") return;
|
|
470
|
+
if (event.message.stopReason === "aborted") return; // interrupted: never speak the aborted partial
|
|
471
|
+
const text = extractTextContent(event.message);
|
|
472
|
+
if (text) vocalizer.speak(text);
|
|
473
|
+
}
|
|
474
|
+
|
|
433
475
|
async #handleMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): Promise<void> {
|
|
476
|
+
this.#vocalizeDelta(event);
|
|
434
477
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
435
478
|
this.ctx.streamingMessage = event.message;
|
|
436
479
|
this.#streamingReveal.setTarget(this.ctx.streamingMessage);
|
|
@@ -454,14 +497,7 @@ export class EventController {
|
|
|
454
497
|
// stream (a big write/edit/eval) sits below a still-live block and
|
|
455
498
|
// can never reach native scrollback: the head of the preview is
|
|
456
499
|
// neither committed nor on screen and the transcript reads as cut.
|
|
457
|
-
|
|
458
|
-
// known at message_end and appends to this block, which would shift
|
|
459
|
-
// committed tool rows below it every turn (audit recommit →
|
|
460
|
-
// duplicated preview copies in scrollback).
|
|
461
|
-
if (
|
|
462
|
-
this.ctx.streamingMessage.content.some(content => content.type === "toolCall") &&
|
|
463
|
-
!settings.get("display.showTokenUsage")
|
|
464
|
-
) {
|
|
500
|
+
if (this.ctx.streamingMessage.content.some(content => content.type === "toolCall")) {
|
|
465
501
|
this.ctx.streamingComponent.markTranscriptBlockFinalized();
|
|
466
502
|
}
|
|
467
503
|
for (const content of this.ctx.streamingMessage.content) {
|
|
@@ -566,6 +602,17 @@ export class EventController {
|
|
|
566
602
|
|
|
567
603
|
async #handleMessageEnd(event: Extract<AgentSessionEvent, { type: "message_end" }>): Promise<void> {
|
|
568
604
|
if (event.message.role === "user") return;
|
|
605
|
+
if (event.message.role === "assistant" && settings.get("speech.enabled")) {
|
|
606
|
+
if (event.message.stopReason === "aborted") {
|
|
607
|
+
// Esc / Ctrl+C / interrupt: stop speaking now and drop the trailing partial.
|
|
608
|
+
vocalizer.clear();
|
|
609
|
+
} else {
|
|
610
|
+
const mode = settings.get("speech.mode");
|
|
611
|
+
// Speak the last partial sentence of a completed message; yield mode
|
|
612
|
+
// instead speaks the whole final message at turn end.
|
|
613
|
+
if (mode === "assistant" || mode === "all") vocalizer.flush();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
569
616
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
570
617
|
this.ctx.streamingMessage = event.message;
|
|
571
618
|
this.#streamingReveal.stop();
|
|
@@ -614,8 +661,10 @@ export class EventController {
|
|
|
614
661
|
this.#resolveDisplaceablePoll();
|
|
615
662
|
}
|
|
616
663
|
this.#lastAssistantComponent = this.ctx.streamingComponent;
|
|
617
|
-
this.#lastAssistantComponent.setUsageInfo(event.message.usage);
|
|
618
664
|
this.#lastAssistantComponent.markTranscriptBlockFinalized();
|
|
665
|
+
if (settings.get("display.showTokenUsage")) {
|
|
666
|
+
this.ctx.chatContainer.addChild(createUsageRowBlock(event.message.usage));
|
|
667
|
+
}
|
|
619
668
|
this.ctx.streamingComponent = undefined;
|
|
620
669
|
this.ctx.streamingMessage = undefined;
|
|
621
670
|
// Pin a turn-ending provider error (e.g. Anthropic content-filter block)
|
|
@@ -844,10 +893,27 @@ export class EventController {
|
|
|
844
893
|
this.sendCompletionNotification();
|
|
845
894
|
}
|
|
846
895
|
|
|
896
|
+
/**
|
|
897
|
+
* Tear down the live "Working…" loader: stop its animation timer AND clear the
|
|
898
|
+
* reference. A transient overlay (auto-compaction / auto-retry) that only ran
|
|
899
|
+
* `statusContainer.clear()` detached the loader from the container but left
|
|
900
|
+
* `ctx.loadingAnimation` set, so the resumed turn's `agent_start` →
|
|
901
|
+
* `ensureLoadingAnimation()` (guarded by `if (!this.loadingAnimation)`) skipped
|
|
902
|
+
* re-adding it and the spinner vanished while the agent kept streaming. Nulling
|
|
903
|
+
* the reference here lets the next `agent_start` recreate and re-attach it.
|
|
904
|
+
*/
|
|
905
|
+
#stopWorkingLoader(): void {
|
|
906
|
+
if (this.ctx.loadingAnimation) {
|
|
907
|
+
this.ctx.loadingAnimation.stop();
|
|
908
|
+
this.ctx.loadingAnimation = undefined;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
847
912
|
async #handleAutoCompactionStart(
|
|
848
913
|
event: Extract<AgentSessionEvent, { type: "auto_compaction_start" }>,
|
|
849
914
|
): Promise<void> {
|
|
850
915
|
this.#cancelIdleCompaction();
|
|
916
|
+
this.#stopWorkingLoader();
|
|
851
917
|
this.ctx.statusContainer.clear();
|
|
852
918
|
const reasonText =
|
|
853
919
|
event.reason === "overflow"
|
|
@@ -933,6 +999,7 @@ export class EventController {
|
|
|
933
999
|
}
|
|
934
1000
|
|
|
935
1001
|
async #handleAutoRetryStart(event: Extract<AgentSessionEvent, { type: "auto_retry_start" }>): Promise<void> {
|
|
1002
|
+
this.#stopWorkingLoader();
|
|
936
1003
|
this.ctx.statusContainer.clear();
|
|
937
1004
|
const delaySeconds = Math.round(event.delayMs / 1000);
|
|
938
1005
|
this.ctx.retryLoader = new Loader(
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
TerminalInputHandler,
|
|
18
18
|
} from "../../extensibility/extensions";
|
|
19
19
|
import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
|
|
20
|
+
import { createExtensionModelQuery } from "../../extensibility/extensions/model-api";
|
|
20
21
|
import { HookEditorComponent } from "../../modes/components/hook-editor";
|
|
21
22
|
import { HookInputComponent } from "../../modes/components/hook-input";
|
|
22
23
|
import { HookSelectorComponent, type HookSelectorSlider } from "../../modes/components/hook-selector";
|
|
@@ -491,6 +492,11 @@ export class ExtensionUiController {
|
|
|
491
492
|
sessionManager: this.ctx.session.sessionManager,
|
|
492
493
|
modelRegistry: this.ctx.session.modelRegistry,
|
|
493
494
|
model: this.ctx.session.model,
|
|
495
|
+
models: createExtensionModelQuery(
|
|
496
|
+
this.ctx.session.modelRegistry,
|
|
497
|
+
this.ctx.session.settings,
|
|
498
|
+
() => this.ctx.session.model,
|
|
499
|
+
),
|
|
494
500
|
isIdle: () => !this.ctx.session.isStreaming,
|
|
495
501
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
496
502
|
abort: () => {
|