@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.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 +103 -2
- package/dist/cli.js +5790 -5731
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +85 -34
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/eval/py/executor.d.ts +5 -0
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/history-protocol.d.ts +14 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/irc/bus.d.ts +66 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +66 -1
- package/dist/types/modes/components/agent-hub.d.ts +30 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
- package/dist/types/modes/components/welcome.d.ts +3 -9
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
- package/dist/types/modes/setup-wizard/index.d.ts +5 -1
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +5 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +51 -0
- package/dist/types/registry/agent-registry.d.ts +16 -5
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +8 -2
- package/dist/types/session/agent-session.d.ts +49 -32
- package/dist/types/session/messages.d.ts +2 -4
- package/dist/types/session/session-history-format.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +21 -3
- package/dist/types/session/streaming-output.d.ts +46 -0
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +1 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +12 -2
- package/dist/types/task/index.d.ts +13 -6
- package/dist/types/task/output-manager.d.ts +0 -7
- package/dist/types/task/repair-args.d.ts +8 -7
- package/dist/types/task/types.d.ts +63 -51
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +11 -0
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/find.d.ts +0 -11
- package/dist/types/tools/grouped-file-output.d.ts +0 -49
- package/dist/types/tools/index.d.ts +7 -3
- package/dist/types/tools/irc.d.ts +76 -38
- package/dist/types/tools/job.d.ts +7 -1
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/examples/extensions/with-deps/package.json +1 -0
- package/package.json +11 -10
- package/scripts/bundle-dist.ts +28 -19
- package/src/async/index.ts +0 -1
- package/src/auto-thinking/classifier.ts +1 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/agentic.ts +230 -115
- package/src/cli/gallery-fixtures/types.ts +5 -0
- package/src/cli-commands.ts +29 -0
- package/src/cli.ts +28 -15
- package/src/commands/launch.ts +4 -0
- package/src/commit/agentic/tools/analyze-file.ts +38 -19
- package/src/commit/model-selection.ts +3 -2
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/keybindings.ts +6 -1
- package/src/config/model-registry.ts +97 -30
- package/src/config/model-resolver.ts +60 -0
- package/src/config/settings-schema.ts +99 -55
- package/src/config/settings.ts +68 -3
- package/src/edit/hashline/execute.ts +39 -2
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/eval/__tests__/agent-bridge.test.ts +5 -3
- package/src/eval/agent-bridge.ts +3 -16
- package/src/eval/completion-bridge.ts +1 -0
- package/src/eval/js/shared/prelude.txt +1 -1
- package/src/eval/py/executor.ts +29 -7
- package/src/eval/py/index.ts +6 -1
- package/src/eval/py/kernel.ts +31 -11
- package/src/eval/py/prelude.py +5 -6
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +82 -3
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +38 -13
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/runner.ts +6 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/shared-events.ts +2 -2
- package/src/hindsight/bank.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +3 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/irc/bus.ts +292 -0
- package/src/main.ts +26 -66
- package/src/memories/index.ts +2 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/local-backend.ts +9 -0
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +81 -1
- package/src/mnemopi/backend.ts +151 -4
- package/src/modes/acp/acp-agent.ts +119 -11
- package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/assistant-message.ts +19 -21
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/footer.ts +3 -1
- package/src/modes/components/status-line/component.ts +118 -34
- package/src/modes/components/tool-execution.ts +31 -1
- package/src/modes/components/ttsr-notification.ts +72 -30
- package/src/modes/components/welcome.ts +9 -33
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +65 -0
- package/src/modes/controllers/extension-ui-controller.ts +8 -8
- package/src/modes/controllers/input-controller.ts +19 -2
- package/src/modes/controllers/mcp-command-controller.ts +38 -3
- package/src/modes/controllers/selector-controller.ts +21 -17
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +47 -22
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +154 -3
- package/src/modes/rpc/rpc-mode.ts +97 -12
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +81 -1
- package/src/modes/setup-wizard/index.ts +12 -2
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/theme/theme.ts +18 -5
- package/src/modes/types.ts +5 -5
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +51 -49
- package/src/prompts/system/irc-incoming.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +0 -5
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/irc.md +29 -19
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/task-summary.md +5 -16
- package/src/prompts/tools/task.md +38 -29
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +16 -5
- package/src/sdk.ts +37 -10
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +422 -291
- package/src/session/messages.ts +11 -78
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-manager.ts +59 -5
- package/src/session/streaming-output.ts +226 -10
- package/src/slash-commands/acp-builtins.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/types.ts +1 -1
- package/src/system-prompt.ts +14 -0
- package/src/task/executor.ts +851 -461
- package/src/task/index.ts +721 -796
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +148 -63
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +82 -66
- package/src/thinking.ts +7 -0
- package/src/tiny/title-client.ts +34 -5
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +6 -4
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +61 -10
- package/src/tools/browser/tab-worker.ts +26 -7
- package/src/tools/browser.ts +28 -1
- package/src/tools/find.ts +2 -27
- package/src/tools/grouped-file-output.ts +1 -118
- package/src/tools/image-gen.ts +11 -4
- package/src/tools/index.ts +17 -13
- package/src/tools/inspect-image.ts +1 -0
- package/src/tools/irc.ts +596 -171
- package/src/tools/job.ts +41 -7
- package/src/tools/read.ts +57 -1
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +4 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/src/async/support.ts +0 -5
- package/src/task/simple-mode.ts +0 -27
|
@@ -222,7 +222,9 @@ export class AssistantMessageComponent extends Container {
|
|
|
222
222
|
this.#contentContainer.clear();
|
|
223
223
|
|
|
224
224
|
const hasVisibleContent = message.content.some(
|
|
225
|
-
c =>
|
|
225
|
+
c =>
|
|
226
|
+
(c.type === "text" && c.text.trim()) ||
|
|
227
|
+
(!this.hideThinkingBlock && c.type === "thinking" && c.thinking.trim()),
|
|
226
228
|
);
|
|
227
229
|
|
|
228
230
|
// Render content in order
|
|
@@ -236,32 +238,28 @@ export class AssistantMessageComponent extends Container {
|
|
|
236
238
|
markdown.transientRenderCache = this.#lastUpdateTransient;
|
|
237
239
|
this.#contentContainer.addChild(markdown);
|
|
238
240
|
} else if (content.type === "thinking" && content.thinking.trim()) {
|
|
241
|
+
if (this.hideThinkingBlock) {
|
|
242
|
+
thinkingIndex += 1;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
239
245
|
// Add spacing only when another visible assistant content block follows.
|
|
240
246
|
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
|
|
241
247
|
const hasVisibleContentAfter = message.content
|
|
242
248
|
.slice(i + 1)
|
|
243
249
|
.some(c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
|
|
244
250
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
});
|
|
258
|
-
thinkingMarkdown.transientRenderCache = this.#lastUpdateTransient;
|
|
259
|
-
this.#contentContainer.addChild(thinkingMarkdown);
|
|
260
|
-
this.#appendThinkingExtensions(i, thinkingIndex, thinkingText);
|
|
261
|
-
thinkingIndex += 1;
|
|
262
|
-
if (hasVisibleContentAfter) {
|
|
263
|
-
this.#contentContainer.addChild(new Spacer(1));
|
|
264
|
-
}
|
|
251
|
+
const thinkingText = content.thinking.trim();
|
|
252
|
+
// Thinking traces in thinkingText color, italic
|
|
253
|
+
const thinkingMarkdown = new Markdown(thinkingText, 1, 0, getMarkdownTheme(), {
|
|
254
|
+
color: (text: string) => theme.fg("thinkingText", text),
|
|
255
|
+
italic: true,
|
|
256
|
+
});
|
|
257
|
+
thinkingMarkdown.transientRenderCache = this.#lastUpdateTransient;
|
|
258
|
+
this.#contentContainer.addChild(thinkingMarkdown);
|
|
259
|
+
this.#appendThinkingExtensions(i, thinkingIndex, thinkingText);
|
|
260
|
+
thinkingIndex += 1;
|
|
261
|
+
if (hasVisibleContentAfter) {
|
|
262
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
265
263
|
}
|
|
266
264
|
}
|
|
267
265
|
}
|
|
@@ -1,51 +1,87 @@
|
|
|
1
|
-
import { Box,
|
|
1
|
+
import { Box, type Component, Markdown } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
3
3
|
import type { CompactionSummaryMessage } from "../../session/messages";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Compaction point in the transcript, rendered as a slim horizontal divider:
|
|
7
|
+
*
|
|
8
|
+
* ──────── 📷 compacted · ctrl+o ────────
|
|
9
|
+
*
|
|
10
|
+
* The conversation above the divider stays visible (display transcript keeps
|
|
11
|
+
* full history); only the LLM context was reset. Expanding (ctrl+o) reveals
|
|
12
|
+
* the compaction summary below the divider.
|
|
8
13
|
*/
|
|
9
|
-
export class CompactionSummaryMessageComponent
|
|
14
|
+
export class CompactionSummaryMessageComponent implements Component {
|
|
10
15
|
#expanded = false;
|
|
16
|
+
#cache?: { width: number; lines: string[] };
|
|
17
|
+
#detail?: Box;
|
|
11
18
|
|
|
12
|
-
constructor(private readonly message: CompactionSummaryMessage) {
|
|
13
|
-
super(1, 1, t => theme.bg("customMessageBg", t));
|
|
14
|
-
this.#updateDisplay();
|
|
15
|
-
}
|
|
19
|
+
constructor(private readonly message: CompactionSummaryMessage) {}
|
|
16
20
|
|
|
17
21
|
setExpanded(expanded: boolean): void {
|
|
22
|
+
if (this.#expanded === expanded) return;
|
|
18
23
|
this.#expanded = expanded;
|
|
19
|
-
this.#
|
|
24
|
+
this.#cache = undefined;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
invalidate(): void {
|
|
28
|
+
this.#cache = undefined;
|
|
29
|
+
// Theme may have changed — rebuild the detail box lazily on next render.
|
|
30
|
+
this.#detail = undefined;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
render(width: number): readonly string[] {
|
|
34
|
+
width = Math.max(1, width);
|
|
35
|
+
if (this.#cache?.width === width) {
|
|
36
|
+
return this.#cache.lines;
|
|
37
|
+
}
|
|
38
|
+
const lines = this.#expanded
|
|
39
|
+
? ["", this.#divider(width), "", ...this.#detailBox().render(width)]
|
|
40
|
+
: ["", this.#divider(width), ""];
|
|
41
|
+
this.#cache = { width, lines };
|
|
42
|
+
return lines;
|
|
43
|
+
}
|
|
29
44
|
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
#divider(width: number): string {
|
|
46
|
+
const rule = theme.tree.horizontal;
|
|
47
|
+
const label = `${theme.icon.camera} compacted`;
|
|
48
|
+
// sep.dot ships pre-padded (" · "); trim so the hint joins with single spaces.
|
|
49
|
+
const hint = `${theme.sep.dot.trim()} ctrl+o`;
|
|
50
|
+
const plainWidth = Bun.stringWidth(`${label} ${hint}`, { countAnsiEscapeCodes: false });
|
|
51
|
+
// ` label hint ` framed by rules on both sides.
|
|
52
|
+
const remaining = width - plainWidth - 2;
|
|
53
|
+
if (remaining < 4) {
|
|
54
|
+
// Too narrow for a framed rule — emit the bare label.
|
|
55
|
+
return theme.fg("muted", label);
|
|
56
|
+
}
|
|
57
|
+
const left = Math.floor(remaining / 2);
|
|
58
|
+
const right = remaining - left;
|
|
59
|
+
return (
|
|
60
|
+
theme.fg("dim", rule.repeat(left)) +
|
|
61
|
+
` ${theme.fg("muted", label)} ${theme.fg("dim", hint)} ` +
|
|
62
|
+
theme.fg("dim", rule.repeat(right))
|
|
63
|
+
);
|
|
64
|
+
}
|
|
34
65
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
66
|
+
#detailBox(): Box {
|
|
67
|
+
if (this.#detail) return this.#detail;
|
|
68
|
+
const box = new Box(1, 1, t => theme.bg("customMessageBg", t));
|
|
69
|
+
const tokenStr = this.message.tokensBefore.toLocaleString();
|
|
70
|
+
const frameCount = this.message.images?.length ?? 0;
|
|
71
|
+
const frameNote =
|
|
72
|
+
frameCount > 0 ? `\n\n_${frameCount} snapcompact frame${frameCount === 1 ? "" : "s"} attached_` : "";
|
|
73
|
+
box.addChild(
|
|
74
|
+
new Markdown(
|
|
75
|
+
`**Compacted from ${tokenStr} tokens**\n\n${this.message.summary}${frameNote}`,
|
|
76
|
+
0,
|
|
77
|
+
0,
|
|
78
|
+
getMarkdownTheme(),
|
|
79
|
+
{
|
|
39
80
|
color: (text: string) => theme.fg("customMessageText", text),
|
|
40
|
-
}
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
);
|
|
46
|
-
if (this.message.shortSummary) {
|
|
47
|
-
this.addChild(new Text(theme.fg("customMessageText", this.message.shortSummary), 0, 1));
|
|
48
|
-
}
|
|
49
|
-
}
|
|
81
|
+
},
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
this.#detail = box;
|
|
85
|
+
return box;
|
|
50
86
|
}
|
|
51
87
|
}
|
|
@@ -175,6 +175,8 @@ export class CustomEditor extends Editor {
|
|
|
175
175
|
onDequeue?: () => void;
|
|
176
176
|
/** Called when Caps Lock is pressed. */
|
|
177
177
|
onCapsLock?: () => void;
|
|
178
|
+
/** Called when left-arrow is pressed while the editor is empty (cursor necessarily at start). */
|
|
179
|
+
onLeftAtStart?: () => void;
|
|
178
180
|
|
|
179
181
|
/** Custom key handlers from extensions and non-built-in app actions. */
|
|
180
182
|
#customKeyHandlers = new Map<KeyId, () => void>();
|
|
@@ -257,6 +259,14 @@ export class CustomEditor extends Editor {
|
|
|
257
259
|
const parsedKey = parseKey(data);
|
|
258
260
|
const canonical = parsedKey !== undefined ? canonicalKeyId(parsedKey) : undefined;
|
|
259
261
|
|
|
262
|
+
// Left-arrow on an empty editor: surface for the agent-hub double-tap
|
|
263
|
+
// gesture. Plain "left" only — modified arrows and any in-text cursor
|
|
264
|
+
// movement fall through to normal handling.
|
|
265
|
+
if (canonical === "left" && this.onLeftAtStart && this.getText().trim() === "") {
|
|
266
|
+
this.onLeftAtStart();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
260
270
|
if (canonical !== undefined) {
|
|
261
271
|
// Intercept configured image paste (async - fires and handles result)
|
|
262
272
|
if (this.#matchesAction(canonical, "app.clipboard.pasteImage") && this.onPasteImage) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import { stripVTControlCharacters } from "node:util";
|
|
3
4
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
4
5
|
import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
@@ -65,7 +66,8 @@ export class FooterComponent implements Component {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
try {
|
|
68
|
-
|
|
69
|
+
const watchPath = head.isReftable ? path.join(head.gitDir, "reftable") : head.headPath;
|
|
70
|
+
this.#gitWatcher = fs.watch(watchPath, () => {
|
|
69
71
|
this.#cachedBranch = undefined; // Invalidate cache
|
|
70
72
|
if (this.#onBranchChange) {
|
|
71
73
|
this.#onBranchChange();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
4
|
import { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
|
|
4
5
|
import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
@@ -120,6 +121,18 @@ function tokensForMessage(msg: AgentMessage): number {
|
|
|
120
121
|
return tokens;
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
interface MessageTokenTotalsCache {
|
|
125
|
+
messagesRef: readonly AgentMessage[];
|
|
126
|
+
stableCount: number;
|
|
127
|
+
stableTokens: number;
|
|
128
|
+
lastStableMessage: AgentMessage | undefined;
|
|
129
|
+
lastStableFingerprint: string | undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function hasContextSegment(segments: readonly StatusLineSegmentId[]): boolean {
|
|
133
|
+
return segments.includes("context_pct") || segments.includes("context_total");
|
|
134
|
+
}
|
|
135
|
+
|
|
123
136
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
124
137
|
// StatusLineComponent
|
|
125
138
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -129,6 +142,7 @@ export class StatusLineComponent implements Component {
|
|
|
129
142
|
#effectiveSettings: EffectiveStatusLineSettings | undefined;
|
|
130
143
|
#cachedBranch: string | null | undefined = undefined;
|
|
131
144
|
#cachedBranchRepoId: string | null | undefined = undefined;
|
|
145
|
+
#cachedBranchCwd: string | undefined = undefined;
|
|
132
146
|
#gitWatcher: fs.FSWatcher | null = null;
|
|
133
147
|
#onBranchChange: (() => void) | null = null;
|
|
134
148
|
#autoCompactEnabled: boolean = true;
|
|
@@ -159,20 +173,19 @@ export class StatusLineComponent implements Component {
|
|
|
159
173
|
} | null = null;
|
|
160
174
|
#usageFetchedAt = 0;
|
|
161
175
|
#usageInFlight = false;
|
|
162
|
-
// Context breakdown — incremental cache.
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
// branch rebuild, replaceMessages with the same length) are detected
|
|
170
|
-
// and recomputed.
|
|
176
|
+
// Context breakdown — incremental rolling cache. The status line refreshes
|
|
177
|
+
// on every agent event, so the hot path must not re-tokenize the full
|
|
178
|
+
// message list. Stable messages are accumulated once; normal streaming
|
|
179
|
+
// refreshes only recompute the current tail message and newly appended
|
|
180
|
+
// entries. History rewrites/compaction replace or shrink the message array
|
|
181
|
+
// and rebuild this cache. Stable messages are treated as immutable after
|
|
182
|
+
// promotion, matching the normal append-only session flow.
|
|
171
183
|
// Cached non-message total (system prompt + tools + skills). Invalidated
|
|
172
184
|
// when the inputs-identity fingerprint changes (model swap, skill toggle,
|
|
173
185
|
// tool registration).
|
|
174
186
|
#nonMessageTokensCache: number | undefined;
|
|
175
187
|
#nonMessageInputsKey: string | undefined;
|
|
188
|
+
#messageTokenTotalsCache: MessageTokenTotalsCache | undefined;
|
|
176
189
|
|
|
177
190
|
constructor(private readonly session: AgentSession) {
|
|
178
191
|
this.#settings = {
|
|
@@ -238,11 +251,15 @@ export class StatusLineComponent implements Component {
|
|
|
238
251
|
this.#gitWatcher = null;
|
|
239
252
|
}
|
|
240
253
|
|
|
241
|
-
const
|
|
242
|
-
if (!
|
|
254
|
+
const repository = git.repo.resolveSync(getProjectDir());
|
|
255
|
+
if (!repository) return;
|
|
256
|
+
|
|
257
|
+
const watchPath = git.repo.isReftableSync(repository)
|
|
258
|
+
? path.join(repository.gitDir, "reftable")
|
|
259
|
+
: repository.headPath;
|
|
243
260
|
|
|
244
261
|
try {
|
|
245
|
-
this.#gitWatcher = fs.watch(
|
|
262
|
+
this.#gitWatcher = fs.watch(watchPath, () => {
|
|
246
263
|
this.#invalidateGitCaches();
|
|
247
264
|
if (this.#onBranchChange) {
|
|
248
265
|
this.#onBranchChange();
|
|
@@ -267,15 +284,18 @@ export class StatusLineComponent implements Component {
|
|
|
267
284
|
#invalidateGitCaches(): void {
|
|
268
285
|
this.#cachedBranch = undefined;
|
|
269
286
|
this.#cachedBranchRepoId = undefined;
|
|
287
|
+
this.#cachedBranchCwd = undefined;
|
|
270
288
|
this.#cachedPrContext = undefined;
|
|
271
289
|
}
|
|
272
290
|
#getCurrentBranch(): string | null {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
if (this.#cachedBranch !== undefined && this.#cachedBranchRepoId === gitHeadPath) {
|
|
291
|
+
const cwd = getProjectDir();
|
|
292
|
+
if (this.#cachedBranch !== undefined && this.#cachedBranchCwd === cwd) {
|
|
276
293
|
return this.#cachedBranch;
|
|
277
294
|
}
|
|
278
295
|
|
|
296
|
+
const head = git.head.resolveSync(cwd);
|
|
297
|
+
const gitHeadPath = head?.headPath ?? null;
|
|
298
|
+
this.#cachedBranchCwd = cwd;
|
|
279
299
|
this.#cachedBranchRepoId = gitHeadPath;
|
|
280
300
|
if (!head) {
|
|
281
301
|
this.#cachedBranch = null;
|
|
@@ -503,24 +523,79 @@ export class StatusLineComponent implements Component {
|
|
|
503
523
|
this.#nonMessageInputsKey = inputsKey;
|
|
504
524
|
}
|
|
505
525
|
|
|
506
|
-
// 2) Message tokens — incremental. The sidecar cache lives
|
|
507
|
-
// message object
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
|
|
512
|
-
// mismatch. The LAST message is always recomputed because it
|
|
513
|
-
// may still be growing during streaming.
|
|
514
|
-
let messagesTokens = 0;
|
|
515
|
-
const lastIdx = messages.length - 1;
|
|
516
|
-
for (let i = 0; i < messages.length; i++) {
|
|
517
|
-
messagesTokens += i === lastIdx ? estimateTokens(messages[i]) : tokensForMessage(messages[i]);
|
|
518
|
-
}
|
|
526
|
+
// 2) Message tokens — incremental rolling total. The sidecar cache lives
|
|
527
|
+
// on each stable message object (all but the current tail). Normal
|
|
528
|
+
// streaming turns only recompute the last message and newly appended
|
|
529
|
+
// entries. Full rebuild only when the message array is replaced,
|
|
530
|
+
// shrinks, or the recently-promoted stable tail mutates in place.
|
|
531
|
+
const messagesTokens = this.#getCachedMessageTokens(messages);
|
|
519
532
|
|
|
520
533
|
const usedTokens = this.#nonMessageTokensCache + messagesTokens;
|
|
521
534
|
return { usedTokens, contextWindow };
|
|
522
535
|
}
|
|
523
536
|
|
|
537
|
+
#getCachedMessageTokens(messages: readonly AgentMessage[]): number {
|
|
538
|
+
const cache = this.#messageTokenTotalsCache;
|
|
539
|
+
if (!cache || cache.messagesRef !== messages || messages.length <= cache.stableCount) {
|
|
540
|
+
return this.#rebuildMessageTokenTotals(messages);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
let stableTokens = cache.stableTokens;
|
|
544
|
+
let stableCount = cache.stableCount;
|
|
545
|
+
const stableLimit = Math.max(0, messages.length - 1);
|
|
546
|
+
|
|
547
|
+
if (
|
|
548
|
+
cache.lastStableMessage &&
|
|
549
|
+
stableCount > 0 &&
|
|
550
|
+
messages[stableCount - 1] === cache.lastStableMessage &&
|
|
551
|
+
cache.lastStableFingerprint !== undefined &&
|
|
552
|
+
cache.lastStableFingerprint !== messageFingerprint(cache.lastStableMessage)
|
|
553
|
+
) {
|
|
554
|
+
return this.#rebuildMessageTokenTotals(messages);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
while (stableCount < stableLimit) {
|
|
558
|
+
const promoted = messages[stableCount]!;
|
|
559
|
+
stableTokens += tokensForMessage(promoted);
|
|
560
|
+
stableCount++;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const lastStableMessage = stableCount > 0 ? messages[stableCount - 1] : undefined;
|
|
564
|
+
const lastStableFingerprint = lastStableMessage ? messageFingerprint(lastStableMessage) : undefined;
|
|
565
|
+
const lastMessage = messages.at(-1);
|
|
566
|
+
const lastTokens = lastMessage ? estimateTokens(lastMessage) : 0;
|
|
567
|
+
this.#messageTokenTotalsCache = {
|
|
568
|
+
messagesRef: messages,
|
|
569
|
+
stableCount,
|
|
570
|
+
stableTokens,
|
|
571
|
+
lastStableMessage,
|
|
572
|
+
lastStableFingerprint,
|
|
573
|
+
};
|
|
574
|
+
return stableTokens + lastTokens;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
#rebuildMessageTokenTotals(messages: readonly AgentMessage[]): number {
|
|
578
|
+
let stableTokens = 0;
|
|
579
|
+
const stableLimit = Math.max(0, messages.length - 1);
|
|
580
|
+
for (let i = 0; i < stableLimit; i++) {
|
|
581
|
+
stableTokens += tokensForMessage(messages[i]!);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const lastStableMessage = stableLimit > 0 ? messages[stableLimit - 1] : undefined;
|
|
585
|
+
const lastStableFingerprint = lastStableMessage ? messageFingerprint(lastStableMessage) : undefined;
|
|
586
|
+
const lastMessage = messages.at(-1);
|
|
587
|
+
const lastTokens = lastMessage ? estimateTokens(lastMessage) : 0;
|
|
588
|
+
|
|
589
|
+
this.#messageTokenTotalsCache = {
|
|
590
|
+
messagesRef: messages,
|
|
591
|
+
stableCount: stableLimit,
|
|
592
|
+
stableTokens,
|
|
593
|
+
lastStableMessage,
|
|
594
|
+
lastStableFingerprint,
|
|
595
|
+
};
|
|
596
|
+
return stableTokens + lastTokens;
|
|
597
|
+
}
|
|
598
|
+
|
|
524
599
|
/**
|
|
525
600
|
* Build an identity fingerprint for the non-message inputs (system prompt,
|
|
526
601
|
* tools, skills). When this changes, the non-message token cache must be
|
|
@@ -535,7 +610,11 @@ export class StatusLineComponent implements Component {
|
|
|
535
610
|
return `${modelId}|${sp.length}:${sp[0]?.length ?? 0}|${tools.length}|${skills.length}`;
|
|
536
611
|
}
|
|
537
612
|
|
|
538
|
-
#buildSegmentContext(
|
|
613
|
+
#buildSegmentContext(
|
|
614
|
+
width: number,
|
|
615
|
+
segmentOptions: StatusLineSettings["segmentOptions"],
|
|
616
|
+
includeContext: boolean,
|
|
617
|
+
): SegmentContext {
|
|
539
618
|
const state = this.session.state;
|
|
540
619
|
|
|
541
620
|
// Trigger background fetch (5-min TTL); render uses cached value
|
|
@@ -555,10 +634,13 @@ export class StatusLineComponent implements Component {
|
|
|
555
634
|
tokensPerSecond: this.#getTokensPerSecond(),
|
|
556
635
|
};
|
|
557
636
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
637
|
+
let contextTokens = 0;
|
|
638
|
+
let contextWindow = state.model?.contextWindow ?? this.session.model?.contextWindow ?? 0;
|
|
639
|
+
if (includeContext) {
|
|
640
|
+
const breakdown = this.getCachedContextBreakdown();
|
|
641
|
+
contextTokens = breakdown.usedTokens;
|
|
642
|
+
contextWindow = breakdown.contextWindow || contextWindow;
|
|
643
|
+
}
|
|
562
644
|
const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
563
645
|
|
|
564
646
|
return {
|
|
@@ -626,7 +708,9 @@ export class StatusLineComponent implements Component {
|
|
|
626
708
|
|
|
627
709
|
#buildStatusLine(width: number): string {
|
|
628
710
|
const effectiveSettings = this.#resolveSettings();
|
|
629
|
-
const
|
|
711
|
+
const includeContext =
|
|
712
|
+
hasContextSegment(effectiveSettings.leftSegments) || hasContextSegment(effectiveSettings.rightSegments);
|
|
713
|
+
const ctx = this.#buildSegmentContext(width, effectiveSettings.segmentOptions, includeContext);
|
|
630
714
|
const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
|
|
631
715
|
|
|
632
716
|
const bgAnsi = theme.getBgAnsi("statusLineBg");
|
|
@@ -19,6 +19,7 @@ import type { Theme } from "../../modes/theme/theme";
|
|
|
19
19
|
import { theme } from "../../modes/theme/theme";
|
|
20
20
|
import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
|
|
21
21
|
import { EVAL_DEFAULT_PREVIEW_LINES } from "../../tools/eval";
|
|
22
|
+
import { isWaitingPollDetails } from "../../tools/job";
|
|
22
23
|
import {
|
|
23
24
|
formatArgsInline,
|
|
24
25
|
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
@@ -194,6 +195,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
194
195
|
// sealed the block stays in the transcript's repaintable live region so a
|
|
195
196
|
// late result still repaints instead of stranding the streaming preview.
|
|
196
197
|
#sealed = false;
|
|
198
|
+
// A `job` poll result whose watched jobs are all still running. Such a
|
|
199
|
+
// block never finalizes (stays in the transcript live region) so a
|
|
200
|
+
// follow-up `job` call can displace it instead of stacking another
|
|
201
|
+
// "waiting on N jobs" frame. Cleared by `seal()`.
|
|
202
|
+
#displaceable = false;
|
|
197
203
|
#renderState: {
|
|
198
204
|
spinnerFrame?: number;
|
|
199
205
|
expanded: boolean;
|
|
@@ -359,6 +365,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
359
365
|
): void {
|
|
360
366
|
this.#result = result;
|
|
361
367
|
this.#isPartial = isPartial;
|
|
368
|
+
// A `job` poll that found every watched job still running is transient
|
|
369
|
+
// "still waiting" chrome; keep the block displaceable so the next `job`
|
|
370
|
+
// call replaces it instead of stacking another waiting frame (see the
|
|
371
|
+
// event controller's displaceable-poll bookkeeping).
|
|
372
|
+
this.#displaceable = this.#toolName === "job" && result.isError !== true && isWaitingPollDetails(result.details);
|
|
362
373
|
// When tool is complete, ensure args are marked complete so spinner stops
|
|
363
374
|
if (!isPartial) {
|
|
364
375
|
this.#argsComplete = true;
|
|
@@ -425,7 +436,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
425
436
|
(this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
|
|
426
437
|
const isBackgroundAsyncTask = this.#toolName === "task" && isBackgroundAsyncRunning;
|
|
427
438
|
const isPartialTask = this.#isPartial && this.#toolName === "task" && !isBackgroundAsyncTask;
|
|
428
|
-
|
|
439
|
+
// A displaceable waiting poll keeps its spinner ticking: it reads as one
|
|
440
|
+
// persistent live poll, and the changing leading glyph keeps the
|
|
441
|
+
// transcript's stable-prefix ratchet from committing rows of a block
|
|
442
|
+
// that a follow-up `job` call may remove.
|
|
443
|
+
const needsSpinner = isStreamingArgs || isPartialTask || this.isDisplaceableBlock();
|
|
429
444
|
if (needsSpinner && !this.#spinnerInterval) {
|
|
430
445
|
const now = performance.now();
|
|
431
446
|
const frameCount = theme.spinnerFrames.length;
|
|
@@ -513,6 +528,9 @@ export class ToolExecutionComponent extends Container {
|
|
|
513
528
|
isTranscriptBlockFinalized(): boolean {
|
|
514
529
|
if (this.#sealed) return true;
|
|
515
530
|
if (this.#result === undefined) return false;
|
|
531
|
+
// A displaceable waiting poll stays live: its rows are kept out of
|
|
532
|
+
// native scrollback so a follow-up `job` call can remove the block.
|
|
533
|
+
if (this.#displaceable) return false;
|
|
516
534
|
if (!this.#isPartial) return true;
|
|
517
535
|
// Partial result: a background async tool is accepted to freeze (the agent
|
|
518
536
|
// continues while it runs and would otherwise pin an unbounded live region);
|
|
@@ -528,11 +546,23 @@ export class ToolExecutionComponent extends Container {
|
|
|
528
546
|
seal(): void {
|
|
529
547
|
if (this.#sealed) return;
|
|
530
548
|
this.#sealed = true;
|
|
549
|
+
this.#displaceable = false;
|
|
531
550
|
this.stopAnimation();
|
|
532
551
|
this.#updateDisplay();
|
|
533
552
|
this.#ui.requestRender();
|
|
534
553
|
}
|
|
535
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Whether this block is a waiting `job` poll (every watched job still
|
|
557
|
+
* running) that has not been sealed. Such a block never finalized, so none
|
|
558
|
+
* of its rows entered native scrollback (the ticking spinner keeps the
|
|
559
|
+
* stable-prefix ratchet at zero) and the whole block can be removed when a
|
|
560
|
+
* follow-up `job` call supersedes it.
|
|
561
|
+
*/
|
|
562
|
+
isDisplaceableBlock(): boolean {
|
|
563
|
+
return this.#displaceable && !this.#sealed;
|
|
564
|
+
}
|
|
565
|
+
|
|
536
566
|
/**
|
|
537
567
|
* Stop spinner animation and cleanup resources.
|
|
538
568
|
*/
|