@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.1
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 +75 -1
- package/dist/types/cli/dry-balance-cli.d.ts +15 -1
- package/dist/types/commit/analysis/conventional.d.ts +2 -2
- package/dist/types/commit/analysis/summary.d.ts +2 -2
- package/dist/types/commit/changelog/generate.d.ts +2 -2
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/index.d.ts +3 -3
- package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
- package/dist/types/commit/model-selection.d.ts +10 -4
- package/dist/types/config/api-key-resolver.d.ts +34 -0
- package/dist/types/config/model-registry.d.ts +17 -1
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/dap/config.d.ts +14 -1
- package/dist/types/dap/types.d.ts +10 -0
- package/dist/types/lsp/utils.d.ts +3 -2
- package/dist/types/modes/components/chat-block.d.ts +64 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/components/overlay-box.d.ts +17 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
- package/dist/types/modes/components/plan-toc.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +11 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
- package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +15 -5
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +18 -5
- package/dist/types/modes/utils/copy-targets.d.ts +21 -1
- package/dist/types/plan-mode/approved-plan.d.ts +27 -8
- package/dist/types/plan-mode/plan-protection.d.ts +4 -4
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/session/messages.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/slash-commands/types.d.ts +4 -6
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/render.d.ts +3 -2
- package/dist/types/tools/archive-reader.d.ts +5 -0
- package/dist/types/tools/ast-edit.d.ts +3 -0
- package/dist/types/tools/ast-grep.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/find.d.ts +8 -4
- package/dist/types/tools/grouped-file-output.d.ts +95 -12
- package/dist/types/tools/memory-render.d.ts +4 -1
- package/dist/types/tools/plan-mode-guard.d.ts +8 -9
- package/dist/types/tools/render-utils.d.ts +5 -9
- package/dist/types/tools/search.d.ts +4 -0
- package/dist/types/tools/sqlite-reader.d.ts +1 -0
- package/dist/types/tools/todo.d.ts +3 -2
- package/dist/types/tools/write.d.ts +3 -0
- package/dist/types/tui/output-block.d.ts +16 -4
- package/dist/types/tui/status-line.d.ts +3 -0
- package/dist/types/utils/enhanced-paste.d.ts +20 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -1
- package/package.json +9 -9
- package/src/auto-thinking/classifier.ts +5 -1
- package/src/cli/dry-balance-cli.ts +52 -17
- package/src/cli/gallery-cli.ts +4 -1
- package/src/cli/gallery-fixtures/misc.ts +29 -0
- package/src/commit/analysis/conventional.ts +2 -2
- package/src/commit/analysis/summary.ts +2 -2
- package/src/commit/changelog/generate.ts +2 -2
- package/src/commit/changelog/index.ts +2 -2
- package/src/commit/map-reduce/index.ts +3 -3
- package/src/commit/map-reduce/map-phase.ts +2 -2
- package/src/commit/map-reduce/reduce-phase.ts +2 -2
- package/src/commit/model-selection.ts +33 -9
- package/src/commit/pipeline.ts +4 -4
- package/src/config/api-key-resolver.ts +58 -0
- package/src/config/model-registry.ts +25 -2
- package/src/config/settings-schema.ts +10 -0
- package/src/config/settings.ts +20 -2
- package/src/dap/config.ts +41 -2
- package/src/dap/defaults.json +1 -0
- package/src/dap/session.ts +1 -0
- package/src/dap/types.ts +10 -0
- package/src/debug/index.ts +40 -54
- package/src/edit/renderer.ts +82 -78
- package/src/eval/__tests__/llm-bridge.test.ts +90 -31
- package/src/eval/llm-bridge.ts +8 -3
- package/src/goals/tools/goal-tool.ts +36 -26
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/utils.ts +3 -2
- package/src/main.ts +9 -7
- package/src/memories/index.ts +12 -5
- package/src/mnemopi/backend.ts +5 -1
- package/src/modes/acp/acp-agent.ts +33 -26
- package/src/modes/components/assistant-message.ts +2 -9
- package/src/modes/components/chat-block.ts +111 -0
- package/src/modes/components/copy-selector.ts +1 -44
- package/src/modes/components/custom-editor.ts +23 -0
- package/src/modes/components/custom-message.ts +1 -3
- package/src/modes/components/execution-shared.ts +1 -2
- package/src/modes/components/hook-message.ts +1 -3
- package/src/modes/components/overlay-box.ts +108 -0
- package/src/modes/components/plan-review-overlay.ts +799 -0
- package/src/modes/components/plan-toc.ts +138 -0
- package/src/modes/components/read-tool-group.ts +20 -4
- package/src/modes/components/skill-message.ts +0 -1
- package/src/modes/components/tips.txt +1 -0
- package/src/modes/components/todo-reminder.ts +0 -2
- package/src/modes/components/tool-execution.ts +68 -88
- package/src/modes/components/transcript-container.ts +84 -24
- package/src/modes/components/user-message.ts +1 -2
- package/src/modes/controllers/command-controller-shared.ts +7 -6
- package/src/modes/controllers/command-controller.ts +57 -55
- package/src/modes/controllers/event-controller.ts +41 -40
- package/src/modes/controllers/extension-ui-controller.ts +10 -73
- package/src/modes/controllers/input-controller.ts +124 -119
- package/src/modes/controllers/mcp-command-controller.ts +69 -60
- package/src/modes/controllers/selector-controller.ts +23 -25
- package/src/modes/controllers/streaming-reveal.ts +212 -0
- package/src/modes/controllers/tan-command-controller.ts +173 -0
- package/src/modes/interactive-mode.ts +169 -94
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/theme/theme-schema.json +1 -1
- package/src/modes/theme/theme.ts +8 -4
- package/src/modes/types.ts +18 -7
- package/src/modes/utils/copy-targets.ts +133 -27
- package/src/modes/utils/ui-helpers.ts +44 -46
- package/src/plan-mode/approved-plan.ts +66 -43
- package/src/plan-mode/plan-protection.ts +4 -4
- package/src/prompts/system/background-tan-dispatch.md +8 -0
- package/src/prompts/system/plan-mode-active.md +67 -58
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/sdk.ts +11 -37
- package/src/session/agent-session.ts +82 -6
- package/src/session/messages.ts +26 -0
- package/src/session/session-manager.ts +13 -5
- package/src/slash-commands/builtin-registry.ts +36 -9
- package/src/slash-commands/types.ts +4 -6
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +4 -0
- package/src/task/render.ts +212 -147
- package/src/tools/archive-reader.ts +64 -0
- package/src/tools/ask.ts +119 -164
- package/src/tools/ast-edit.ts +98 -71
- package/src/tools/ast-grep.ts +37 -43
- package/src/tools/bash.ts +50 -6
- package/src/tools/debug.ts +20 -8
- package/src/tools/fetch.ts +297 -7
- package/src/tools/find.ts +44 -30
- package/src/tools/gh-renderer.ts +81 -42
- package/src/tools/grouped-file-output.ts +272 -48
- package/src/tools/image-gen.ts +150 -103
- package/src/tools/inspect-image-renderer.ts +63 -41
- package/src/tools/inspect-image.ts +8 -1
- package/src/tools/job.ts +3 -4
- package/src/tools/memory-render.ts +4 -1
- package/src/tools/plan-mode-guard.ts +21 -39
- package/src/tools/read.ts +23 -16
- package/src/tools/render-utils.ts +21 -37
- package/src/tools/resolve.ts +14 -0
- package/src/tools/search-tool-bm25.ts +36 -23
- package/src/tools/search.ts +80 -78
- package/src/tools/sqlite-reader.ts +9 -12
- package/src/tools/todo.ts +118 -52
- package/src/tools/write.ts +81 -62
- package/src/tui/output-block.ts +60 -13
- package/src/tui/status-line.ts +5 -1
- package/src/utils/commit-message-generator.ts +9 -1
- package/src/utils/enhanced-paste.ts +202 -0
- package/src/utils/title-generator.ts +2 -1
- package/src/web/search/providers/anthropic.ts +25 -19
- package/src/web/search/providers/exa.ts +11 -3
- package/src/web/search/providers/kimi.ts +28 -17
- package/src/web/search/providers/parallel.ts +35 -24
- package/src/web/search/providers/synthetic.ts +8 -6
- package/src/web/search/providers/tavily.ts +9 -8
- package/src/web/search/providers/zai.ts +8 -6
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure heading/section parser for the plan-review overlay. It splits a plan's
|
|
3
|
+
* markdown into a flat list of sections — a leading preamble (text before the
|
|
4
|
+
* first heading) followed by one entry per ATX heading — preserving the exact
|
|
5
|
+
* source bytes of each section so the overlay can render, reorder-free delete,
|
|
6
|
+
* and round-trip the document without a full markdown re-render.
|
|
7
|
+
*
|
|
8
|
+
* No TUI dependencies: this module is unit-tested in isolation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** ATX heading: 1-6 `#`, required whitespace, a title, optional closing `#`s. */
|
|
12
|
+
const HEADING_RE = /^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
|
|
13
|
+
/** Opening/closing code fence run (``` or ~~~), allowing up to 3 lead spaces. */
|
|
14
|
+
const FENCE_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
15
|
+
|
|
16
|
+
export interface PlanSection {
|
|
17
|
+
/** `0` = preamble (no heading, no ToC entry); `1..6` = heading depth. */
|
|
18
|
+
level: number;
|
|
19
|
+
/** Plain-text heading label with inline markdown lightly stripped. */
|
|
20
|
+
title: string;
|
|
21
|
+
/** Exact source slice for this section, including its trailing newline(s). */
|
|
22
|
+
raw: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Collapse inline markdown emphasis/link/code syntax to readable text. This is
|
|
27
|
+
* a deliberately light strip (not a full markdown render) just so ToC entries
|
|
28
|
+
* read cleanly — `**Goal** & [docs](x)` becomes `Goal & docs`.
|
|
29
|
+
*/
|
|
30
|
+
export function stripInlineMarkdown(text: string): string {
|
|
31
|
+
let out = text;
|
|
32
|
+
// Images first (so the link pass below does not eat the `(url)`), then links.
|
|
33
|
+
out = out.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
34
|
+
out = out.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
35
|
+
out = out.replace(/\[([^\]]*)\]\[[^\]]*\]/g, "$1");
|
|
36
|
+
// Autolinks `<https://…>` keep their URL as the readable text.
|
|
37
|
+
out = out.replace(/<([^>\s]+)>/g, "$1");
|
|
38
|
+
// Inline code, then bold/italic/strikethrough emphasis runs.
|
|
39
|
+
out = out.replace(/`([^`]+)`/g, "$1");
|
|
40
|
+
out = out.replace(/(\*\*|__)(.+?)\1/g, "$2");
|
|
41
|
+
out = out.replace(/(\*|_)(.+?)\1/g, "$2");
|
|
42
|
+
out = out.replace(/~~(.+?)~~/g, "$1");
|
|
43
|
+
return out.replace(/\s+/g, " ").trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Split `text` into preamble + heading sections. `#` characters inside fenced
|
|
48
|
+
* code blocks are never treated as headings. Concatenating every section's
|
|
49
|
+
* `raw` reproduces the original text exactly.
|
|
50
|
+
*/
|
|
51
|
+
export function parsePlanSections(text: string): PlanSection[] {
|
|
52
|
+
const lines = text.split("\n");
|
|
53
|
+
// Character offset of each line start so section `raw` can slice the source.
|
|
54
|
+
const offsets: number[] = new Array(lines.length);
|
|
55
|
+
let cursor = 0;
|
|
56
|
+
for (let i = 0; i < lines.length; i++) {
|
|
57
|
+
offsets[i] = cursor;
|
|
58
|
+
cursor += lines[i]!.length + 1; // +1 for the "\n" join separator
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Heading line indices (start of each heading section), with metadata.
|
|
62
|
+
const heads: { line: number; level: number; title: string }[] = [];
|
|
63
|
+
let fenceChar: string | null = null;
|
|
64
|
+
let fenceLen = 0;
|
|
65
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66
|
+
const line = lines[i]!;
|
|
67
|
+
const fence = FENCE_RE.exec(line);
|
|
68
|
+
if (fenceChar === null) {
|
|
69
|
+
if (fence) {
|
|
70
|
+
fenceChar = fence[1]![0]!;
|
|
71
|
+
fenceLen = fence[1]!.length;
|
|
72
|
+
}
|
|
73
|
+
// Opening-fence lines are body, not headings.
|
|
74
|
+
if (fence) continue;
|
|
75
|
+
} else {
|
|
76
|
+
// Inside a fence: only a matching-or-longer run of the same char closes.
|
|
77
|
+
if (fence && fence[1]![0] === fenceChar && fence[1]!.length >= fenceLen && fence[2]!.trim() === "") {
|
|
78
|
+
fenceChar = null;
|
|
79
|
+
fenceLen = 0;
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const heading = HEADING_RE.exec(line);
|
|
84
|
+
if (heading) {
|
|
85
|
+
heads.push({ line: i, level: heading[1]!.length, title: stripInlineMarkdown(heading[2]!) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const sections: PlanSection[] = [];
|
|
90
|
+
const sliceRaw = (startLine: number, endLine: number): string => {
|
|
91
|
+
const startOffset = offsets[startLine]!;
|
|
92
|
+
const endOffset = endLine < lines.length ? offsets[endLine]! : text.length;
|
|
93
|
+
return text.slice(startOffset, endOffset);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Preamble: everything before the first heading (only when non-empty).
|
|
97
|
+
const firstHeadLine = heads.length > 0 ? heads[0]!.line : lines.length;
|
|
98
|
+
if (firstHeadLine > 0) {
|
|
99
|
+
const raw = sliceRaw(0, firstHeadLine);
|
|
100
|
+
if (raw.length > 0) sections.push({ level: 0, title: "", raw });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (let h = 0; h < heads.length; h++) {
|
|
104
|
+
const head = heads[h]!;
|
|
105
|
+
const endLine = h + 1 < heads.length ? heads[h + 1]!.line : lines.length;
|
|
106
|
+
sections.push({ level: head.level, title: head.title, raw: sliceRaw(head.line, endLine) });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return sections;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Concatenate every section's `raw` back into a single document and guarantee a
|
|
114
|
+
* single trailing newline. Inverse of {@link parsePlanSections} for any input
|
|
115
|
+
* that already ends with a newline.
|
|
116
|
+
*/
|
|
117
|
+
export function joinPlanSections(sections: readonly PlanSection[]): string {
|
|
118
|
+
let joined = "";
|
|
119
|
+
for (const section of sections) joined += section.raw;
|
|
120
|
+
if (joined.length === 0) return "";
|
|
121
|
+
return joined.endsWith("\n") ? joined : `${joined}\n`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Indices to remove when deleting `sections[index]`: the heading itself plus
|
|
126
|
+
* every following section nested deeper than it (its sub-headings). The
|
|
127
|
+
* preamble (level 0) is never a deletion target and yields an empty span.
|
|
128
|
+
*/
|
|
129
|
+
export function sectionDeletionSpan(sections: readonly PlanSection[], index: number): number[] {
|
|
130
|
+
const target = sections[index];
|
|
131
|
+
if (!target || target.level === 0) return [];
|
|
132
|
+
const span = [index];
|
|
133
|
+
for (let j = index + 1; j < sections.length; j++) {
|
|
134
|
+
if (sections[j]!.level > target.level) span.push(j);
|
|
135
|
+
else break;
|
|
136
|
+
}
|
|
137
|
+
return span;
|
|
138
|
+
}
|
|
@@ -81,6 +81,14 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
81
81
|
#text: Text;
|
|
82
82
|
#expanded = false;
|
|
83
83
|
#showContentPreview: boolean;
|
|
84
|
+
// A read group accretes entries across multiple assistant completions for as
|
|
85
|
+
// long as the run of reads is uninterrupted. While it is the active group it
|
|
86
|
+
// must stay in the transcript's repaintable live region — its header line
|
|
87
|
+
// re-layouts from `Read <path>` to `Read (N)` + tree as entries arrive, so a
|
|
88
|
+
// frozen snapshot taken on a risk terminal would strand the single-entry form
|
|
89
|
+
// (see TranscriptContainer / NativeScrollbackLiveRegion). The controller calls
|
|
90
|
+
// `finalize()` once the run breaks so the block can commit to native scrollback.
|
|
91
|
+
#finalized = false;
|
|
84
92
|
|
|
85
93
|
constructor(options: ReadToolGroupOptions = {}) {
|
|
86
94
|
super();
|
|
@@ -90,6 +98,14 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
90
98
|
this.#updateDisplay();
|
|
91
99
|
}
|
|
92
100
|
|
|
101
|
+
isTranscriptBlockFinalized(): boolean {
|
|
102
|
+
return this.#finalized;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
finalize(): void {
|
|
106
|
+
this.#finalized = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
93
109
|
updateArgs(args: ReadRenderArgs, toolCallId?: string): void {
|
|
94
110
|
if (!toolCallId) return;
|
|
95
111
|
const basePath = args.file_path || args.path || "";
|
|
@@ -181,9 +197,9 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
181
197
|
const total = entriesWithoutPreview.length;
|
|
182
198
|
for (const [index, entry] of entriesWithoutPreview.entries()) {
|
|
183
199
|
const connector = index === total - 1 ? theme.tree.last : theme.tree.branch;
|
|
184
|
-
const
|
|
200
|
+
const statusPrefix = entry.status === "success" ? "" : `${this.#formatStatus(entry.status)} `;
|
|
185
201
|
const pathDisplay = this.#formatPath(entry);
|
|
186
|
-
lines.push(` ${theme.fg("dim", connector)} ${
|
|
202
|
+
lines.push(` ${theme.fg("dim", connector)} ${statusPrefix}${pathDisplay}`.trimEnd());
|
|
187
203
|
}
|
|
188
204
|
|
|
189
205
|
this.#text.setText(lines.join("\n"));
|
|
@@ -198,7 +214,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
198
214
|
|
|
199
215
|
/**
|
|
200
216
|
* Add a code-cell content preview below the entry summary.
|
|
201
|
-
* When collapsed: shows first COLLAPSED_PREVIEW_LINES lines with "… N more lines
|
|
217
|
+
* When collapsed: shows first COLLAPSED_PREVIEW_LINES lines with a "… N more lines ⟨<key>: Expand⟩" hint.
|
|
202
218
|
* When expanded: shows full content.
|
|
203
219
|
*/
|
|
204
220
|
#addContentPreview(entry: ReadEntry): void {
|
|
@@ -254,7 +270,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
254
270
|
|
|
255
271
|
#formatStatus(status: ReadEntry["status"]): string {
|
|
256
272
|
if (status === "success") {
|
|
257
|
-
return theme.fg("
|
|
273
|
+
return theme.fg("text", theme.status.enabled);
|
|
258
274
|
}
|
|
259
275
|
if (status === "warning") {
|
|
260
276
|
return theme.fg("warning", theme.status.warning);
|
|
@@ -11,7 +11,6 @@ export class SkillMessageComponent extends Container {
|
|
|
11
11
|
|
|
12
12
|
constructor(private readonly message: CustomMessage<SkillPromptDetails>) {
|
|
13
13
|
super();
|
|
14
|
-
this.addChild(new Spacer(1));
|
|
15
14
|
|
|
16
15
|
this.#box = new Box(1, 1, t => theme.bg("customMessageBg", t));
|
|
17
16
|
this.#rebuild();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Tired of typing "keep going"? Just send a '.'
|
|
2
2
|
You can /btw to ask a side question
|
|
3
|
+
Use /tan to fork the current conversation into a background agent
|
|
3
4
|
Ctrl+D can be used to exit, but with your draft saved!
|
|
4
5
|
Find out which model you emotionally abuse the most with `omp stats`
|
|
5
6
|
Try task isolation to create CoW worktrees
|
|
@@ -37,26 +37,6 @@ import { isFramedBlockComponent, renderStatusLine } from "../../tui";
|
|
|
37
37
|
import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
38
38
|
import { renderDiff } from "./diff";
|
|
39
39
|
|
|
40
|
-
function ensureInvalidate(component: unknown): Component {
|
|
41
|
-
const c = component as { render: Component["render"]; invalidate?: () => void };
|
|
42
|
-
if (!c.invalidate) {
|
|
43
|
-
c.invalidate = () => {};
|
|
44
|
-
}
|
|
45
|
-
return c as Component;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function addBoxChild(box: Box, component: unknown): boolean {
|
|
49
|
-
const child = ensureInvalidate(component);
|
|
50
|
-
box.addChild(child);
|
|
51
|
-
return isFramedBlockComponent(child);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function setBoxPaddingForFramedBlock(box: Box, hasFramedBlock: boolean): void {
|
|
55
|
-
const padding = hasFramedBlock ? 0 : 1;
|
|
56
|
-
box.setPaddingX(padding);
|
|
57
|
-
box.setPaddingY(padding);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
40
|
/**
|
|
61
41
|
* Drop trailing removal/hunk-header lines that appear in a streaming diff
|
|
62
42
|
* before the matching `+added` lines have arrived. Without this, a partial
|
|
@@ -153,12 +133,12 @@ export interface ToolExecutionHandle {
|
|
|
153
133
|
setExpanded(expanded: boolean): void;
|
|
154
134
|
}
|
|
155
135
|
|
|
156
|
-
/** Drive pending-tool redraws at
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
const SPINNER_RENDER_INTERVAL_MS =
|
|
136
|
+
/** Drive pending-tool redraws at 30fps so the animated border sweep stays
|
|
137
|
+
* smooth without spending twice the frame budget. The TUI throttles at the same
|
|
138
|
+
* cadence, and static frames diff to a no-op redraw at ~zero cost. */
|
|
139
|
+
const SPINNER_RENDER_INTERVAL_MS = 1000 / 30;
|
|
160
140
|
/** Advance the spinner glyph at its classic ~12.5fps step, decoupled from the
|
|
161
|
-
*
|
|
141
|
+
* render cadence (mirrors `Loader`). */
|
|
162
142
|
const SPINNER_GLYPH_ADVANCE_MS = 80;
|
|
163
143
|
|
|
164
144
|
// Stable per-instance counter so each tool execution's inline images get a
|
|
@@ -245,11 +225,13 @@ export class ToolExecutionComponent extends Container {
|
|
|
245
225
|
this.#cwd = cwd;
|
|
246
226
|
this.#args = args;
|
|
247
227
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
228
|
+
// Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins.
|
|
229
|
+
// paddingY is 1 so background-tinted blocks (custom/extension tools and the
|
|
230
|
+
// generic fallback) get top/bottom breathing room. TranscriptContainer
|
|
231
|
+
// strips PLAIN-blank edges, so framed/minimal blocks (no bg set) drop these
|
|
232
|
+
// lines and keep their tight spacing — only tinted lines survive.
|
|
233
|
+
this.#contentBox = new Box(0, 1);
|
|
234
|
+
this.#contentText = new Text("", 1, 1);
|
|
253
235
|
|
|
254
236
|
// Use Box for custom tools or built-in tools that have renderers
|
|
255
237
|
const hasRenderer = toolName in toolRenderers;
|
|
@@ -439,26 +421,43 @@ export class ToolExecutionComponent extends Container {
|
|
|
439
421
|
#updateSpinnerAnimation(): void {
|
|
440
422
|
// Spinner for: task tool with partial result, or edit/write while args streaming
|
|
441
423
|
const isStreamingArgs = !this.#argsComplete && (isEditLikeToolName(this.#toolName) || this.#toolName === "write");
|
|
442
|
-
const
|
|
443
|
-
this.#toolName === "task" &&
|
|
424
|
+
const isBackgroundAsyncRunning =
|
|
444
425
|
(this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
|
|
426
|
+
const isBackgroundAsyncTask = this.#toolName === "task" && isBackgroundAsyncRunning;
|
|
445
427
|
const isPartialTask = this.#isPartial && this.#toolName === "task" && !isBackgroundAsyncTask;
|
|
446
|
-
// Sweep the border of bash/eval execution blocks while they're pending
|
|
428
|
+
// Sweep the border of bash/eval execution blocks while they're pending — but
|
|
429
|
+
// not once they've been backgrounded: a backgrounded job's block gets
|
|
430
|
+
// committed to scrollback and finalizes later via the async update path, so a
|
|
431
|
+
// mid-sweep frame would freeze a stray dark "bar" segment into the border.
|
|
447
432
|
const isPendingExecBlock =
|
|
448
|
-
this.#isPartial &&
|
|
433
|
+
this.#isPartial &&
|
|
434
|
+
shimmerEnabled() &&
|
|
435
|
+
(this.#toolName === "bash" || this.#toolName === "eval") &&
|
|
436
|
+
!isBackgroundAsyncRunning;
|
|
449
437
|
const needsSpinner = isStreamingArgs || isPartialTask || isPendingExecBlock;
|
|
450
438
|
if (needsSpinner && !this.#spinnerInterval) {
|
|
451
|
-
|
|
439
|
+
const now = performance.now();
|
|
440
|
+
const frameCount = theme.spinnerFrames.length;
|
|
441
|
+
this.#lastSpinnerAdvanceAt = now;
|
|
442
|
+
if (frameCount > 0 && this.#spinnerFrame === undefined) {
|
|
443
|
+
this.#spinnerFrame = 0;
|
|
444
|
+
this.#renderState.spinnerFrame = 0;
|
|
445
|
+
}
|
|
452
446
|
this.#spinnerInterval = setInterval(() => {
|
|
453
447
|
const now = performance.now();
|
|
454
448
|
const frameCount = theme.spinnerFrames.length;
|
|
455
|
-
// Redraw at
|
|
456
|
-
// glyph
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
449
|
+
// Redraw at 30fps for a smooth border sweep, but keep the spinner
|
|
450
|
+
// glyph phase-locked to its classic ~12.5fps cadence. Advancing the
|
|
451
|
+
// anchor by elapsed frames instead of resetting to `now` avoids the
|
|
452
|
+
// 30fps timer quantizing the glyph down to one step every three ticks.
|
|
453
|
+
if (frameCount > 0) {
|
|
454
|
+
const elapsed = now - this.#lastSpinnerAdvanceAt;
|
|
455
|
+
if (elapsed >= SPINNER_GLYPH_ADVANCE_MS) {
|
|
456
|
+
const steps = Math.floor(elapsed / SPINNER_GLYPH_ADVANCE_MS);
|
|
457
|
+
this.#spinnerFrame = ((this.#spinnerFrame ?? 0) + steps) % frameCount;
|
|
458
|
+
this.#renderState.spinnerFrame = this.#spinnerFrame;
|
|
459
|
+
this.#lastSpinnerAdvanceAt += steps * SPINNER_GLYPH_ADVANCE_MS;
|
|
460
|
+
}
|
|
462
461
|
}
|
|
463
462
|
this.#ui.requestRender();
|
|
464
463
|
}, SPINNER_RENDER_INTERVAL_MS);
|
|
@@ -607,27 +606,24 @@ export class ToolExecutionComponent extends Container {
|
|
|
607
606
|
}
|
|
608
607
|
|
|
609
608
|
#updateDisplay(): void {
|
|
610
|
-
// Set background based on state
|
|
611
|
-
const bgFn = this.#isPartial
|
|
612
|
-
? (text: string) => theme.bg("toolPendingBg", text)
|
|
613
|
-
: this.#result?.isError
|
|
614
|
-
? (text: string) => theme.bg("toolErrorBg", text)
|
|
615
|
-
: (text: string) => theme.bg("toolSuccessBg", text);
|
|
616
|
-
|
|
617
609
|
// Sync shared mutable render state for component closures
|
|
618
610
|
this.#renderState.expanded = this.#expanded;
|
|
619
611
|
this.#renderState.isPartial = this.#isPartial;
|
|
620
612
|
this.#renderState.spinnerFrame = this.#spinnerFrame;
|
|
621
613
|
|
|
614
|
+
// Non-self-framing tools (custom/extension renderers and the generic
|
|
615
|
+
// fallback) get a padded, state-tinted block — built-ins that draw their
|
|
616
|
+
// own frame opt out below via the framed-component mark.
|
|
617
|
+
const stateBgKey = this.#isPartial ? "toolPendingBg" : this.#result?.isError ? "toolErrorBg" : "toolSuccessBg";
|
|
618
|
+
const stateBgFn = (t: string) => theme.bg(stateBgKey, t);
|
|
619
|
+
|
|
622
620
|
// Check for custom tool rendering
|
|
623
621
|
if (this.#tool && (this.#tool.renderCall || this.#tool.renderResult)) {
|
|
624
622
|
const tool = this.#tool;
|
|
625
623
|
const mergeCallAndResult = Boolean((tool as { mergeCallAndResult?: boolean }).mergeCallAndResult);
|
|
626
624
|
// Custom tools use Box for flexible component rendering
|
|
627
|
-
|
|
628
|
-
this.#contentBox.setBgFn(inline ? undefined : bgFn);
|
|
625
|
+
this.#contentBox.setBgFn(undefined);
|
|
629
626
|
this.#contentBox.clear();
|
|
630
|
-
let contentBoxHasFramedBlock = false;
|
|
631
627
|
// Mirror the built-in renderer branch so custom renderers (notably the
|
|
632
628
|
// task tool, whose live instance routes through here) receive the same
|
|
633
629
|
// render context — e.g. the `hasResult` flag that suppresses the task
|
|
@@ -643,18 +639,15 @@ export class ToolExecutionComponent extends Container {
|
|
|
643
639
|
if (tool.renderCall) {
|
|
644
640
|
try {
|
|
645
641
|
const callComponent = tool.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
|
|
646
|
-
if (callComponent)
|
|
647
|
-
contentBoxHasFramedBlock =
|
|
648
|
-
addBoxChild(this.#contentBox, callComponent) || contentBoxHasFramedBlock;
|
|
649
|
-
}
|
|
642
|
+
if (callComponent) this.#contentBox.addChild(callComponent as Component);
|
|
650
643
|
} catch (err) {
|
|
651
644
|
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
652
645
|
// Fall back to default on error
|
|
653
|
-
|
|
646
|
+
this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
|
|
654
647
|
}
|
|
655
648
|
} else {
|
|
656
649
|
// No custom renderCall, show tool name
|
|
657
|
-
|
|
650
|
+
this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
|
|
658
651
|
}
|
|
659
652
|
}
|
|
660
653
|
|
|
@@ -677,25 +670,27 @@ export class ToolExecutionComponent extends Container {
|
|
|
677
670
|
theme,
|
|
678
671
|
this.#args,
|
|
679
672
|
);
|
|
680
|
-
if (resultComponent)
|
|
681
|
-
contentBoxHasFramedBlock = addBoxChild(this.#contentBox, resultComponent) || contentBoxHasFramedBlock;
|
|
682
|
-
}
|
|
673
|
+
if (resultComponent) this.#contentBox.addChild(resultComponent);
|
|
683
674
|
} catch (err) {
|
|
684
675
|
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
685
676
|
// Fall back to showing raw output on error
|
|
686
677
|
const output = this.#getTextOutput();
|
|
687
678
|
if (output) {
|
|
688
|
-
|
|
679
|
+
this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
|
|
689
680
|
}
|
|
690
681
|
}
|
|
691
682
|
} else if (this.#result) {
|
|
692
683
|
// Has result but no custom renderResult
|
|
693
684
|
const output = this.#getTextOutput();
|
|
694
685
|
if (output) {
|
|
695
|
-
|
|
686
|
+
this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
|
|
696
687
|
}
|
|
697
688
|
}
|
|
698
|
-
|
|
689
|
+
// Custom tools that draw their own frame (task) render flush; plain
|
|
690
|
+
// extension renderers get the padded, state-tinted block back.
|
|
691
|
+
const customFramed = this.#contentBox.children.some(isFramedBlockComponent);
|
|
692
|
+
this.#contentBox.setPaddingX(customFramed ? 0 : 1);
|
|
693
|
+
this.#contentBox.setBgFn(customFramed ? undefined : stateBgFn);
|
|
699
694
|
} else if (this.#toolName in toolRenderers) {
|
|
700
695
|
// Built-in tools with renderers
|
|
701
696
|
const renderer = toolRenderers[this.#toolName];
|
|
@@ -714,7 +709,6 @@ export class ToolExecutionComponent extends Container {
|
|
|
714
709
|
// Multi-file: render each file as its own Box (identical to separate tool calls)
|
|
715
710
|
this.#contentBox.setBgFn(undefined);
|
|
716
711
|
this.#contentBox.clear();
|
|
717
|
-
this.#contentBox.setPaddingX(1);
|
|
718
712
|
|
|
719
713
|
const renderContext = this.#buildRenderContext();
|
|
720
714
|
this.#renderState.renderContext = renderContext;
|
|
@@ -726,20 +720,14 @@ export class ToolExecutionComponent extends Container {
|
|
|
726
720
|
this.#multiFileBoxes.push(spacer);
|
|
727
721
|
this.addChild(spacer);
|
|
728
722
|
}
|
|
729
|
-
const
|
|
730
|
-
? (text: string) => theme.bg("toolErrorBg", text)
|
|
731
|
-
: (text: string) => theme.bg("toolSuccessBg", text);
|
|
732
|
-
const fileBox = new Box(1, 1, fileBgFn);
|
|
723
|
+
const fileBox = new Box(0, 0);
|
|
733
724
|
try {
|
|
734
725
|
const resultComponent = renderer.renderResult(
|
|
735
726
|
{ content: [], details: fileResult, isError: fileResult.isError },
|
|
736
727
|
this.#renderState,
|
|
737
728
|
theme,
|
|
738
729
|
);
|
|
739
|
-
if (resultComponent)
|
|
740
|
-
const fileBoxHasFramedBlock = addBoxChild(fileBox, resultComponent);
|
|
741
|
-
setBoxPaddingForFramedBlock(fileBox, fileBoxHasFramedBlock);
|
|
742
|
-
}
|
|
730
|
+
if (resultComponent) fileBox.addChild(resultComponent);
|
|
743
731
|
} catch (err) {
|
|
744
732
|
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
745
733
|
}
|
|
@@ -756,7 +744,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
756
744
|
const pendingSpacer = new Spacer(1);
|
|
757
745
|
this.#multiFileBoxes.push(pendingSpacer);
|
|
758
746
|
this.addChild(pendingSpacer);
|
|
759
|
-
const pendingBox = new Box(
|
|
747
|
+
const pendingBox = new Box(0, 0);
|
|
760
748
|
const pendingText = renderStatusLine(
|
|
761
749
|
{
|
|
762
750
|
icon: "pending",
|
|
@@ -772,9 +760,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
772
760
|
} else {
|
|
773
761
|
// Single-file or no result: standard rendering
|
|
774
762
|
// Inline renderers skip background styling
|
|
775
|
-
this.#contentBox.setBgFn(
|
|
763
|
+
this.#contentBox.setBgFn(undefined);
|
|
776
764
|
this.#contentBox.clear();
|
|
777
|
-
let contentBoxHasFramedBlock = false;
|
|
778
765
|
|
|
779
766
|
const renderContext = this.#buildRenderContext();
|
|
780
767
|
this.#renderState.renderContext = renderContext;
|
|
@@ -784,14 +771,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
784
771
|
// Render call component
|
|
785
772
|
try {
|
|
786
773
|
const callComponent = renderer.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
|
|
787
|
-
if (callComponent)
|
|
788
|
-
contentBoxHasFramedBlock =
|
|
789
|
-
addBoxChild(this.#contentBox, callComponent) || contentBoxHasFramedBlock;
|
|
790
|
-
}
|
|
774
|
+
if (callComponent) this.#contentBox.addChild(callComponent);
|
|
791
775
|
} catch (err) {
|
|
792
776
|
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
793
777
|
// Fall back to default on error
|
|
794
|
-
|
|
778
|
+
this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
|
|
795
779
|
}
|
|
796
780
|
}
|
|
797
781
|
|
|
@@ -808,24 +792,20 @@ export class ToolExecutionComponent extends Container {
|
|
|
808
792
|
theme,
|
|
809
793
|
this.#getCallArgsForRender(),
|
|
810
794
|
);
|
|
811
|
-
if (resultComponent)
|
|
812
|
-
contentBoxHasFramedBlock =
|
|
813
|
-
addBoxChild(this.#contentBox, resultComponent) || contentBoxHasFramedBlock;
|
|
814
|
-
}
|
|
795
|
+
if (resultComponent) this.#contentBox.addChild(resultComponent);
|
|
815
796
|
} catch (err) {
|
|
816
797
|
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
817
798
|
// Fall back to showing raw output on error
|
|
818
799
|
const output = this.#getTextOutput();
|
|
819
800
|
if (output) {
|
|
820
|
-
|
|
801
|
+
this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
|
|
821
802
|
}
|
|
822
803
|
}
|
|
823
804
|
}
|
|
824
|
-
setBoxPaddingForFramedBlock(this.#contentBox, contentBoxHasFramedBlock);
|
|
825
805
|
}
|
|
826
806
|
} else {
|
|
827
807
|
// Other built-in tools: use Text directly with caching
|
|
828
|
-
this.#contentText.setCustomBgFn(
|
|
808
|
+
this.#contentText.setCustomBgFn(stateBgFn);
|
|
829
809
|
this.#contentText.setText(this.#formatToolExecution());
|
|
830
810
|
}
|
|
831
811
|
|