@pi-archimedes/core 2.2.0 → 2.3.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/package.json +5 -2
- package/src/index.ts +1 -1
- package/src/startup/index.ts +1 -1
- package/src/text.ts +1 -1
- package/src/thinking/patch.test.ts +48 -0
- package/src/thinking/patch.ts +63 -20
- package/src/tool-render.test.ts +79 -0
- package/src/tool-render.ts +101 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -19,13 +19,16 @@
|
|
|
19
19
|
"./overlay": "./src/overlay.ts",
|
|
20
20
|
"./config": "./src/config.ts",
|
|
21
21
|
"./settings-io": "./src/settings-io.ts",
|
|
22
|
-
"./profiler": "./src/profiler.ts"
|
|
22
|
+
"./profiler": "./src/profiler.ts",
|
|
23
|
+
"./tool-render": "./src/tool-render.ts"
|
|
23
24
|
},
|
|
24
25
|
"peerDependencies": {
|
|
25
26
|
"@earendil-works/pi-coding-agent": ">=0.1.0",
|
|
26
27
|
"@earendil-works/pi-tui": ">=0.1.0"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
30
|
+
"@earendil-works/pi-coding-agent": "^0.84.2",
|
|
31
|
+
"@earendil-works/pi-tui": "^0.84.2",
|
|
29
32
|
"typescript": "^6.0.0"
|
|
30
33
|
},
|
|
31
34
|
"pi": {
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext, KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { TUI, type EditorTheme, type Component, type SettingItem } from "@earendil-works/pi-tui";
|
|
3
|
+
import { type TUI, type EditorTheme, type Component, type SettingItem } from "@earendil-works/pi-tui";
|
|
4
4
|
|
|
5
5
|
import { HephaestusEditor } from "./editor/index.js";
|
|
6
6
|
|
package/src/startup/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { loadCoreConfig } from "../config.js";
|
|
|
4
4
|
import { detectSection, parseSectionText, parseModelScope, formatColumns, buildItemWrapper, type ParsedSection, SECTION_KEYS } from "./sections.js";
|
|
5
5
|
import { fetchLatestVersion, compareVersions } from "./version.js";
|
|
6
6
|
import { stripAnsi } from "../text.js";
|
|
7
|
-
import { Text, Spacer, Container, TUI, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
7
|
+
import { Text, Spacer, Container, type TUI, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
8
8
|
|
|
9
9
|
// Symbol keys (survive hot-reload)
|
|
10
10
|
const LISTING_REF = Symbol.for("splashscreen:listingRef");
|
package/src/text.ts
CHANGED
|
@@ -29,7 +29,7 @@ export function clampLine(line: string, maxW: number): string {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** Clamp an array of lines to maxW visible characters each. */
|
|
32
|
-
|
|
32
|
+
function clampLines(lines: string[], maxW: number): string[] {
|
|
33
33
|
return lines.map((l) => clampLine(l, maxW));
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -196,6 +196,54 @@ describe("patchThinkingRenderer", () => {
|
|
|
196
196
|
expect(MockClassV2.prototype[PATCH_VERSION_KEY]).toBe("2.0.0");
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
+
it("accepts a minified thinking-check variant (no whitespace around ===)", async () => {
|
|
200
|
+
const MockClass = function AssistantMessageComponent() {};
|
|
201
|
+
MockClass.prototype.updateContent = function updateContent() {
|
|
202
|
+
// Minified dist-chunk shape: no whitespace around ===, single quotes
|
|
203
|
+
const content = { type: "thinking" };
|
|
204
|
+
if (content.type==="thinking") {
|
|
205
|
+
this.markdownTheme.codeBlockIndent="";
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
vi.doMock("@earendil-works/pi-coding-agent", () => ({
|
|
210
|
+
AssistantMessageComponent: MockClass,
|
|
211
|
+
VERSION: "1.0.0",
|
|
212
|
+
highlightCode: vi.fn(),
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
const patch = await importPatch();
|
|
216
|
+
patch(() => ({} as any));
|
|
217
|
+
|
|
218
|
+
// The minification-safe regex probe must accept the minified variant
|
|
219
|
+
expect(MockClass.prototype[PATCHED_KEY]).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("rejects a negated thinking check", async () => {
|
|
223
|
+
const MockClass = function AssistantMessageComponent() {};
|
|
224
|
+
MockClass.prototype.updateContent = function updateContent() {
|
|
225
|
+
// pi 0.84.3's own minified chunk contains
|
|
226
|
+
// `thinkingContent.type!=="thinking"` (inner batch-loop break). A source whose
|
|
227
|
+
// ONLY thinking-relations are negations must NOT pass the probe.
|
|
228
|
+
const content = { type: "thinking" };
|
|
229
|
+
if (content.type!=="thinking") {
|
|
230
|
+
this.markdownTheme.codeBlockIndent="";
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
vi.doMock("@earendil-works/pi-coding-agent", () => ({
|
|
235
|
+
AssistantMessageComponent: MockClass,
|
|
236
|
+
VERSION: "1.0.0",
|
|
237
|
+
highlightCode: vi.fn(),
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
const patch = await importPatch();
|
|
241
|
+
patch(() => ({} as any));
|
|
242
|
+
|
|
243
|
+
// PATCHED_KEY must NOT be set — the probe must not match `!==`
|
|
244
|
+
expect(MockClass.prototype[PATCHED_KEY]).toBeUndefined();
|
|
245
|
+
});
|
|
246
|
+
|
|
199
247
|
it("re-patches on same version to update getTheme closure", async () => {
|
|
200
248
|
const MockClass = function AssistantMessageComponent() {};
|
|
201
249
|
MockClass.prototype.updateContent = function updateContent() {
|
package/src/thinking/patch.ts
CHANGED
|
@@ -30,7 +30,13 @@ export function patchThinkingRenderer(getTheme: () => Theme): void {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const src = proto.updateContent.toString();
|
|
33
|
-
|
|
33
|
+
// NOTE: pi ships the interactive TUI in a minified bundle chunk at runtime, so
|
|
34
|
+
// the source we see via .toString() can be `content.type==="thinking"` (no
|
|
35
|
+
// spaces) even where dist is readable. The probe below must therefore be
|
|
36
|
+
// minification-safe: a whitespace-tolerant regex rather than an exact
|
|
37
|
+
// substring. A bare `space === "thinking"` (or any other field) still does
|
|
38
|
+
// not match, as required.
|
|
39
|
+
const hasThinkingCheck = /content\.type\s*===\s*["']thinking["']/.test(src);
|
|
34
40
|
const hasMarkdownTheme = src.includes("this.markdownTheme");
|
|
35
41
|
if (!hasThinkingCheck || !hasMarkdownTheme) {
|
|
36
42
|
console.warn(
|
|
@@ -55,9 +61,25 @@ export function patchThinkingRenderer(getTheme: () => Theme): void {
|
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
// Re-
|
|
59
|
-
|
|
64
|
+
// Re-patched every session_start — /resume needs a fresh getTheme closure.
|
|
65
|
+
//
|
|
66
|
+
// Shape: 0.84.3 pi native updateContent:
|
|
67
|
+
// updateContent(message, isStreaming = this.isStreaming) {
|
|
68
|
+
// this.lastMessage = message;
|
|
69
|
+
// this.isStreaming = isStreaming;
|
|
70
|
+
// this.contentContainer.clear();
|
|
71
|
+
// ...
|
|
72
|
+
// // batches consecutive "thinking" parts into thinkingBlocks
|
|
73
|
+
// // (skipping empties), i-- after the inner loop,
|
|
74
|
+
// // renders as ONE Markdown section of thinkingBlocks.join("\n\n")
|
|
75
|
+
// // or ONE static Text label when hidden.
|
|
76
|
+
// // stop-reason: const hasToolCalls = content.some(...);
|
|
77
|
+
// // this.hasToolCalls = hasToolCalls; (render() uses for OSC-133 zones)
|
|
78
|
+
// // stopReason === "length" → Spacer + "truncated" Text
|
|
79
|
+
// // else if (!hasToolCalls) { aborted / error branches }
|
|
80
|
+
(proto as any).updateContent = function (this: any, message: any, isStreaming?: boolean): void {
|
|
60
81
|
this.lastMessage = message;
|
|
82
|
+
if (isStreaming !== undefined) this.isStreaming = isStreaming;
|
|
61
83
|
|
|
62
84
|
this.markdownTheme.codeBlockIndent = "";
|
|
63
85
|
this.contentContainer.clear();
|
|
@@ -105,11 +127,11 @@ export function patchThinkingRenderer(getTheme: () => Theme): void {
|
|
|
105
127
|
// We must preserve it here, otherwise those transformers are silently
|
|
106
128
|
// dropped when this patch replaces updateContent.
|
|
107
129
|
//
|
|
108
|
-
// NOTE:
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// the
|
|
130
|
+
// NOTE: `createMarkdownTransform` is not exported from pi-coding-agent, so
|
|
131
|
+
// we inline an equivalent pipeline over `this.markdownTransformers`.
|
|
132
|
+
//
|
|
133
|
+
// The `transform` option was added to @earendil-works/pi-tui in 0.84.1.
|
|
134
|
+
// At runtime older pi-tui ignores the field, 0.84.1+ honors it.
|
|
113
135
|
type MarkdownOptionsWithTransform = MarkdownOptions & {
|
|
114
136
|
transform?: (markdown: string, availableWidth: number) => string;
|
|
115
137
|
};
|
|
@@ -139,14 +161,27 @@ export function patchThinkingRenderer(getTheme: () => Theme): void {
|
|
|
139
161
|
this.contentContainer.addChild(
|
|
140
162
|
new Markdown(
|
|
141
163
|
content.text.trim(),
|
|
142
|
-
1,
|
|
164
|
+
this.outputPad ?? 1,
|
|
143
165
|
0,
|
|
144
166
|
this.markdownTheme,
|
|
145
167
|
undefined,
|
|
146
168
|
{ transform: transformFor("assistant") } as MarkdownOptionsWithTransform,
|
|
147
169
|
),
|
|
148
170
|
);
|
|
149
|
-
} else if (content.type === "thinking"
|
|
171
|
+
} else if (content.type === "thinking") {
|
|
172
|
+
// Batch a consecutive run of thinking parts into one section
|
|
173
|
+
// (mirrors 0.84.3 pi native behaviour: thinkBlocks, i-- on the
|
|
174
|
+
// inner loop, early continue on zero-length runs).
|
|
175
|
+
const thinkBlocks: string[] = [];
|
|
176
|
+
for (; i < message.content.length; i++) {
|
|
177
|
+
const thinkingPart = message.content[i];
|
|
178
|
+
if (thinkingPart.type !== "thinking") break;
|
|
179
|
+
const trimmed = thinkingPart.thinking.trim();
|
|
180
|
+
if (trimmed) thinkBlocks.push(trimmed);
|
|
181
|
+
}
|
|
182
|
+
i--;
|
|
183
|
+
if (thinkBlocks.length === 0) continue;
|
|
184
|
+
|
|
150
185
|
const hasVisibleContentAfter = message.content
|
|
151
186
|
.slice(i + 1)
|
|
152
187
|
.some(
|
|
@@ -156,16 +191,17 @@ export function patchThinkingRenderer(getTheme: () => Theme): void {
|
|
|
156
191
|
);
|
|
157
192
|
|
|
158
193
|
if (this.hideThinkingBlock) {
|
|
194
|
+
// One static label for the whole run when hidden.
|
|
159
195
|
const t = ensureTheme();
|
|
160
196
|
if (!t) continue;
|
|
161
197
|
this.contentContainer.addChild(
|
|
162
|
-
new Text(t.italic(t.fg("thinkingText", this.hiddenThinkingLabel)), 1, 0),
|
|
198
|
+
new Text(t.italic(t.fg("thinkingText", this.hiddenThinkingLabel)), this.outputPad ?? 1, 0),
|
|
163
199
|
);
|
|
164
200
|
if (hasVisibleContentAfter) {
|
|
165
201
|
this.contentContainer.addChild(new Spacer(1));
|
|
166
202
|
}
|
|
167
203
|
} else {
|
|
168
|
-
let thinkingContent =
|
|
204
|
+
let thinkingContent = thinkBlocks.join("\n\n");
|
|
169
205
|
if (!thinkingContent.startsWith(THINKING_LABEL)) {
|
|
170
206
|
thinkingContent = `${THINKING_LABEL}\n\n${thinkingContent}`;
|
|
171
207
|
}
|
|
@@ -175,7 +211,7 @@ export function patchThinkingRenderer(getTheme: () => Theme): void {
|
|
|
175
211
|
this.contentContainer.addChild(
|
|
176
212
|
new Markdown(
|
|
177
213
|
thinkingContent,
|
|
178
|
-
1,
|
|
214
|
+
this.outputPad ?? 1,
|
|
179
215
|
0,
|
|
180
216
|
muted ?? this.markdownTheme,
|
|
181
217
|
{
|
|
@@ -192,9 +228,19 @@ export function patchThinkingRenderer(getTheme: () => Theme): void {
|
|
|
192
228
|
}
|
|
193
229
|
}
|
|
194
230
|
|
|
195
|
-
//
|
|
231
|
+
// Stop-reason handling — 0.84.3 pi shape. `hasToolCalls` is required by
|
|
232
|
+
// the component's render() for OSC-133 prompt zones, so it must be set.
|
|
196
233
|
const hasToolCalls = message.content.some((c: any) => c.type === "toolCall");
|
|
197
|
-
|
|
234
|
+
this.hasToolCalls = hasToolCalls;
|
|
235
|
+
|
|
236
|
+
if (message.stopReason === "length") {
|
|
237
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
238
|
+
const t = ensureTheme();
|
|
239
|
+
if (t)
|
|
240
|
+
this.contentContainer.addChild(
|
|
241
|
+
new Text(t.fg("error", "Response was truncated before completion."), this.outputPad ?? 1, 0),
|
|
242
|
+
);
|
|
243
|
+
} else if (!hasToolCalls) {
|
|
198
244
|
if (message.stopReason === "aborted") {
|
|
199
245
|
const abortMessage =
|
|
200
246
|
message.errorMessage && message.errorMessage !== "Request was aborted"
|
|
@@ -202,21 +248,18 @@ export function patchThinkingRenderer(getTheme: () => Theme): void {
|
|
|
202
248
|
: "Operation aborted";
|
|
203
249
|
this.contentContainer.addChild(new Spacer(1));
|
|
204
250
|
const t = ensureTheme();
|
|
205
|
-
if (t) this.contentContainer.addChild(new Text(t.fg("error", abortMessage), 1, 0));
|
|
251
|
+
if (t) this.contentContainer.addChild(new Text(t.fg("error", abortMessage), this.outputPad ?? 1, 0));
|
|
206
252
|
} else if (message.stopReason === "error") {
|
|
207
253
|
const errorMsg = message.errorMessage || "Unknown error";
|
|
208
254
|
this.contentContainer.addChild(new Spacer(1));
|
|
209
255
|
const t = ensureTheme();
|
|
210
256
|
if (t) {
|
|
211
257
|
this.contentContainer.addChild(
|
|
212
|
-
new Text(t.fg("error", `Error: ${errorMsg}`), 1, 0),
|
|
258
|
+
new Text(t.fg("error", `Error: ${errorMsg}`), this.outputPad ?? 1, 0),
|
|
213
259
|
);
|
|
214
260
|
}
|
|
215
261
|
}
|
|
216
262
|
}
|
|
217
|
-
|
|
218
|
-
// Bottom padding so next message has breathing room
|
|
219
|
-
this.contentContainer.addChild(new Spacer(1));
|
|
220
263
|
};
|
|
221
264
|
|
|
222
265
|
// Mark as patched with version for incompatibility detection
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
renderToolHeader,
|
|
4
|
+
renderStatusLabel,
|
|
5
|
+
renderToolCallLine,
|
|
6
|
+
STATUS_GLYPH,
|
|
7
|
+
type ToolRenderTheme,
|
|
8
|
+
} from "./tool-render.js";
|
|
9
|
+
|
|
10
|
+
// Fake theme: wraps text in visible markers so assertions can verify tokens.
|
|
11
|
+
const theme: ToolRenderTheme = {
|
|
12
|
+
fg: (token: string, text: string) => `[${token}:${text}]`,
|
|
13
|
+
bold: (text: string) => `**${text}**`,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe("renderToolHeader", () => {
|
|
17
|
+
it("renders blue bold tool name + orange action", () => {
|
|
18
|
+
expect(renderToolHeader("mcp", "atlassian", theme)).toBe(
|
|
19
|
+
"[toolTitle:**mcp**] [accent:atlassian]",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("renders the name only when action is empty", () => {
|
|
24
|
+
expect(renderToolHeader("todo", "", theme)).toBe("[toolTitle:**todo**]");
|
|
25
|
+
expect(renderToolHeader("todo", undefined, theme)).toBe(
|
|
26
|
+
"[toolTitle:**todo**]",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("renderStatusLabel", () => {
|
|
32
|
+
it("running: muted glyph + muted label", () => {
|
|
33
|
+
expect(renderStatusLabel("running", "2/4 completed", theme)).toBe(
|
|
34
|
+
"[muted:▸ ][muted:2/4 completed]",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("success: green glyph + muted label", () => {
|
|
39
|
+
expect(renderStatusLabel("success", "done", theme)).toBe(
|
|
40
|
+
"[success:✓ ][muted:done]",
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("error: red glyph + muted label", () => {
|
|
45
|
+
expect(renderStatusLabel("error", "boom", theme)).toBe(
|
|
46
|
+
"[error:✗ ][muted:boom]",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("exposes the glyph map", () => {
|
|
51
|
+
expect(STATUS_GLYPH).toEqual({ running: "▸", success: "✓", error: "✗" });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("renderToolCallLine", () => {
|
|
56
|
+
it("success: green glyph + green name + dim suffix", () => {
|
|
57
|
+
expect(renderToolCallLine("success", "read", ": /path", theme)).toBe(
|
|
58
|
+
"[success:✓ ][success:read][dim:: /path]",
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("error: red glyph + red name + dim suffix", () => {
|
|
63
|
+
expect(renderToolCallLine("error", "read", ": /missing", theme)).toBe(
|
|
64
|
+
"[error:✗ ][error:read][dim:: /missing]",
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("running: muted glyph + muted name", () => {
|
|
69
|
+
expect(renderToolCallLine("running", "grep", ": pattern | 2s", theme)).toBe(
|
|
70
|
+
"[muted:▸ ][muted:grep][dim:: pattern | 2s]",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("omits the dim fragment when suffix is empty", () => {
|
|
75
|
+
expect(renderToolCallLine("success", "bash", "", theme)).toBe(
|
|
76
|
+
"[success:✓ ][success:bash]",
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool-row rendering helpers.
|
|
3
|
+
*
|
|
4
|
+
* Archimedes tools (mcp, todo, …) render a consistent two-part row:
|
|
5
|
+
*
|
|
6
|
+
* line 1 (header): <toolName> (blue bold) + <action> (orange accent)
|
|
7
|
+
* result line: <glyph> <label> — glyph reflects run status:
|
|
8
|
+
* ▸ running (muted) · ✓ success (green) · ✗ error (red)
|
|
9
|
+
* the label is muted so the glyph carries the colour.
|
|
10
|
+
*
|
|
11
|
+
* These helpers are pure (no TUI component imports) so they stay directly
|
|
12
|
+
* unit-testable and can be reused across packages. Callers wrap the returned
|
|
13
|
+
* string in whatever component they use (typically a pi-tui Text).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
/** Run status of a settled/in-flight tool row. */
|
|
19
|
+
export type ToolStatus = "running" | "success" | "error";
|
|
20
|
+
|
|
21
|
+
/** Glyph shown before the result label, keyed by status. */
|
|
22
|
+
export const STATUS_GLYPH: Record<ToolStatus, string> = {
|
|
23
|
+
running: "▸",
|
|
24
|
+
success: "✓",
|
|
25
|
+
error: "✗",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Theme color token used to colour each status glyph. */
|
|
29
|
+
const STATUS_TOKEN: Record<ToolStatus, ThemeColor> = {
|
|
30
|
+
running: "muted",
|
|
31
|
+
success: "success",
|
|
32
|
+
error: "error",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The subset of a pi Theme these helpers need. Typed with pi's ThemeColor so
|
|
37
|
+
* pi's real Theme is assignable (parameter contravariance: a fn requiring the
|
|
38
|
+
* wider string token would NOT accept a Theme whose fg only takes ThemeColor).
|
|
39
|
+
*/
|
|
40
|
+
export type ToolRenderTheme = {
|
|
41
|
+
fg: (token: ThemeColor, text: string) => string;
|
|
42
|
+
bold: (text: string) => string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render the tool header line:
|
|
47
|
+
* <toolName> (toolTitle, bold) + " " + <action> (accent)
|
|
48
|
+
*
|
|
49
|
+
* When action is empty/undefined only the tool name is rendered.
|
|
50
|
+
*/
|
|
51
|
+
export function renderToolHeader(
|
|
52
|
+
toolName: string,
|
|
53
|
+
action: string | undefined,
|
|
54
|
+
theme: ToolRenderTheme,
|
|
55
|
+
): string {
|
|
56
|
+
const name = theme.fg("toolTitle", theme.bold(toolName));
|
|
57
|
+
if (!action) return name;
|
|
58
|
+
return name + " " + theme.fg("accent", action);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Render a status result line:
|
|
63
|
+
* <glyph> (status-coloured) + <label> (muted)
|
|
64
|
+
*
|
|
65
|
+
* e.g. "✓ atlassian_searchJiraIssuesUsingJql" or "▸ 2/4 completed".
|
|
66
|
+
*/
|
|
67
|
+
export function renderStatusLabel(
|
|
68
|
+
status: ToolStatus,
|
|
69
|
+
label: string,
|
|
70
|
+
theme: ToolRenderTheme,
|
|
71
|
+
): string {
|
|
72
|
+
return (
|
|
73
|
+
theme.fg(STATUS_TOKEN[status], STATUS_GLYPH[status] + " ") +
|
|
74
|
+
theme.fg("muted", label)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render a tool-call line with a status glyph, a status-coloured name, and an
|
|
80
|
+
* optional dim args/suffix fragment:
|
|
81
|
+
*
|
|
82
|
+
* <glyph> (status-coloured) + <name> (status-coloured) + <suffix> (dim)
|
|
83
|
+
*
|
|
84
|
+
* e.g. "✓ read: /path/to/file" (green glyph+name, dim ": /path...") or
|
|
85
|
+
* "▸ grep: pattern" (muted glyph+name while running).
|
|
86
|
+
*
|
|
87
|
+
* The name shares the glyph's colour (unlike renderStatusLabel, which mutes
|
|
88
|
+
* the label) so a completed call reads as a single green/red unit. The suffix
|
|
89
|
+
* is passed pre-formatted (e.g. ": args" or ": args | 2s") and rendered dim.
|
|
90
|
+
*/
|
|
91
|
+
export function renderToolCallLine(
|
|
92
|
+
status: ToolStatus,
|
|
93
|
+
name: string,
|
|
94
|
+
suffix: string,
|
|
95
|
+
theme: ToolRenderTheme,
|
|
96
|
+
): string {
|
|
97
|
+
const token = STATUS_TOKEN[status];
|
|
98
|
+
const head =
|
|
99
|
+
theme.fg(token, STATUS_GLYPH[status] + " ") + theme.fg(token, name);
|
|
100
|
+
return suffix ? head + theme.fg("dim", suffix) : head;
|
|
101
|
+
}
|