@pi-archimedes/ui 2.10.3 → 2.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/README.md +11 -7
- package/package.json +8 -8
- package/src/bash/index.ts +0 -2
- package/src/bash/renderer.test.ts +50 -5
- package/src/bash/renderer.ts +28 -30
- package/src/codemode/index.ts +0 -9
- package/src/codemode/renderer.test.ts +32 -31
- package/src/codemode/renderer.ts +29 -61
- package/src/config.test.ts +62 -1
- package/src/config.ts +45 -1
- package/src/editor/index.test.ts +118 -2
- package/src/editor/index.ts +34 -3
- package/src/editor/spin-quips.ts +1 -1
- package/src/index.screen-mode.test.ts +411 -0
- package/src/index.test.ts +506 -101
- package/src/index.ts +252 -104
- package/src/settings.test.ts +37 -2
- package/src/settings.ts +16 -3
- package/src/startup/index.test.ts +239 -129
- package/src/startup/index.ts +179 -272
- package/src/startup/pi-listing.test.ts +290 -0
- package/src/startup/sections.test.ts +38 -208
- package/src/startup/sections.ts +83 -236
- package/src/startup-splash-gate.test.ts +546 -0
- package/src/thinking/patch.test.ts +81 -1
- package/src/thinking/patch.ts +22 -6
- package/src/bash/tool.test.ts +0 -26
- package/src/bash/tool.ts +0 -17
- package/src/codemode/loader.test.ts +0 -139
- package/src/codemode/loader.ts +0 -115
- package/src/codemode/tool.test.ts +0 -149
- package/src/codemode/tool.ts +0 -96
- package/src/startup/capture.ts +0 -47
- package/src/startup/version.test.ts +0 -98
- package/src/startup/version.ts +0 -26
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ TUI enhancements for the Pi Archimedes monorepo.
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
### Bash Tool Styling
|
|
8
|
+
- **Pi's tool, our row**: `bash` stays pi's own built-in tool — this package never registers it. It installs one `pi.registerToolRenderer()` resolver that decides how calls to `bash` are drawn, so pi keeps the executor, description, reachability and activation of that tool.
|
|
8
9
|
- **Collapsed/Expanded Views**: Toggleable tool execution details.
|
|
9
10
|
- **Live Timer**: Track tool execution duration.
|
|
10
11
|
- **Status Indicators**:
|
|
@@ -14,12 +15,12 @@ TUI enhancements for the Pi Archimedes monorepo.
|
|
|
14
15
|
|
|
15
16
|
### Codemode Tool Styling
|
|
16
17
|
- **Styling follows the `toolStyle` master switch**:
|
|
17
|
-
- **Minimal** (default): the
|
|
18
|
-
- **Native**: the
|
|
18
|
+
- **Minimal** (default): the resolver answers with our row — the collapsed row is the `codemode` header plus the nested tool calls the script made (last five, with an expand hint when there are more) — no script dump, no summary line. A script that made no calls shows a single `<glyph> <duration>` line instead.
|
|
19
|
+
- **Native**: the resolver answers `next()` — pi's own codemode rendering stands.
|
|
19
20
|
- **Expanded View**: The syntax-highlighted script, every nested call (status glyph, args, duration, and cost for `models.*` calls, with a `Model calls: $total` line when several were priced), the script output without the executor's `Script completed/failed` header (image blocks show a `[Image: …]` indicator — the renderer is text-only), the full-output path when truncated, and the timing + final status.
|
|
20
21
|
- **Live Timer**: In-flight calls show the `…` running glyph and update every second.
|
|
21
|
-
- **Native Behaviour Preserved**:
|
|
22
|
-
- **Graceful Fallback**:
|
|
22
|
+
- **Native Behaviour Preserved**: `codemode` is pi's own built-in extension and stays that way — nothing in this package registers it, so pi keeps the QuickJS executor, the dynamic description, the per-tool prompt guidelines and `defaultActive: false`, and no "built-in extension was not loaded" startup notice appears because nothing was replaced. Only the presentation is ours (ADR 0029).
|
|
23
|
+
- **Graceful Fallback**: The styling is installed behind `typeof pi.registerToolRenderer === "function"` (pi 1.0.1+), so a host without that API gets pi's own rendering for `bash` and `codemode` alike; a session with no `codemode` tool is simply never asked about it.
|
|
23
24
|
|
|
24
25
|
### Editor
|
|
25
26
|
- **HephaestusEditor**: Advanced text editing capabilities.
|
|
@@ -32,7 +33,9 @@ TUI enhancements for the Pi Archimedes monorepo.
|
|
|
32
33
|
- **Theme**: Unified styling for thinking blocks.
|
|
33
34
|
|
|
34
35
|
### Startup
|
|
35
|
-
- **
|
|
36
|
+
- **The splash is opt-in, and off by default.** Out of the box you get **pi's own startup screen** — its header, its key hints, its loaded-resource listing. `startupSplash: true` installs the Archimedes splash in pi's header slot instead; with it off nothing is installed, so pi's `quietStartup` keeps working exactly as it does with no Archimedes loaded.
|
|
37
|
+
- **Splash Logo Reveal** (`startupSplash: true`): An animated logo reveal with the running Pi version and the session's model scope (`ctx.scopedModels` — the set `/scoped-models` shows), drawn in pi's own row style so it reads as one document with the listing below it.
|
|
38
|
+
- **Honours Pi's `quietStartup`** (when the splash is on): `false` = splash with its details, `"header"` = splash alone, `true` = Pi's own quiet startup (the splash hands the header back). With the splash off, that setting is pi's alone to act on. Nothing is scraped from Pi's output to decide any of this, and the splash makes no network request — Pi's own update check is the only one.
|
|
36
39
|
|
|
37
40
|
## Settings (`archimedes.ui`)
|
|
38
41
|
|
|
@@ -47,8 +50,9 @@ TUI enhancements for the Pi Archimedes monorepo.
|
|
|
47
50
|
| `codeUnindent` | boolean | `true` | Strip common indentation from code blocks |
|
|
48
51
|
| `labelText` | string | `"Thinking..."` | Text shown before thinking blocks |
|
|
49
52
|
| `labelColor` | string | `"255,215,0"` | RGB color for the thinking label (e.g. `255,215,0`) |
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
53
|
+
| `startupSplash` | boolean | `false` | Install the Archimedes startup splash (off = pi's own startup screen) |
|
|
54
|
+
| `animationStyle` | "diagonal" \| "top-right" \| "bottom-left" \| "bottom-right" \| "center-out" \| "wave" \| "horizontal" \| "vertical" \| "vertical-up" | `"vertical-up"` | Startup logo animation (applies when `startupSplash` is true) |
|
|
55
|
+
| `colorfulLogo` | boolean | `false` | Use pi's brand colors for the splash logo (applies when `startupSplash` is true) |
|
|
52
56
|
| `editorSpinBorder` | boolean | `true` | Animated spinner border on editor |
|
|
53
57
|
| `editorSpinSpeed` | "slow" \| "normal" \| "fast" | `"normal"` | Editor border spinner speed |
|
|
54
58
|
| `editorSpinStyle` | "typing" \| "pulse" \| "rain" \| "cascade" \| "columns" \| "wave-rows" \| "diagonal-swipe" \| "sparkle" \| "pendulum" \| "marquee" | `"pendulum"` | Editor spinner style |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "Terminal UI enhancements, styled bash tool rendering, and visual presentation components for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -28,17 +28,17 @@
|
|
|
28
28
|
"./config": "./src/config.ts"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@pi-archimedes/core": "2.
|
|
31
|
+
"@pi-archimedes/core": "2.11.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@earendil-works/pi-ai": "
|
|
35
|
-
"@earendil-works/pi-coding-agent": "
|
|
36
|
-
"@earendil-works/pi-tui": "
|
|
34
|
+
"@earendil-works/pi-ai": "1.2.0",
|
|
35
|
+
"@earendil-works/pi-coding-agent": "1.2.0",
|
|
36
|
+
"@earendil-works/pi-tui": "1.2.0",
|
|
37
37
|
"typescript": "^6.0.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@earendil-works/pi-ai": ">=
|
|
41
|
-
"@earendil-works/pi-coding-agent": ">=
|
|
42
|
-
"@earendil-works/pi-tui": ">=
|
|
40
|
+
"@earendil-works/pi-ai": ">=1.2.0",
|
|
41
|
+
"@earendil-works/pi-coding-agent": ">=1.2.0",
|
|
42
|
+
"@earendil-works/pi-tui": ">=1.2.0"
|
|
43
43
|
}
|
|
44
44
|
}
|
package/src/bash/index.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
formatBashCommand,
|
|
8
8
|
renderBashCall,
|
|
9
9
|
renderBashResult,
|
|
10
|
-
clearActiveBashIntervals,
|
|
11
10
|
TruncatedTextComponent,
|
|
12
11
|
} from "./renderer.js";
|
|
13
12
|
|
|
@@ -316,7 +315,7 @@ describe("renderBashResult - Live timer lifecycle", () => {
|
|
|
316
315
|
expect(context.state.endedAt).toBeDefined();
|
|
317
316
|
});
|
|
318
317
|
|
|
319
|
-
it("
|
|
318
|
+
it("stops the once-a-second repaint as soon as the result settles", () => {
|
|
320
319
|
const context = {
|
|
321
320
|
executionStarted: true,
|
|
322
321
|
invalidate: vi.fn(),
|
|
@@ -326,14 +325,60 @@ describe("renderBashResult - Live timer lifecycle", () => {
|
|
|
326
325
|
renderBashResult({}, { isPartial: true }, theme, context);
|
|
327
326
|
expect(context.state.interval).toBeDefined();
|
|
328
327
|
|
|
329
|
-
|
|
328
|
+
renderBashResult({}, { isPartial: false }, theme, context);
|
|
330
329
|
expect(context.state.interval).toBeUndefined();
|
|
331
|
-
|
|
332
|
-
vi.advanceTimersByTime(
|
|
330
|
+
|
|
331
|
+
vi.advanceTimersByTime(3000);
|
|
333
332
|
expect(context.invalidate).not.toHaveBeenCalled();
|
|
334
333
|
});
|
|
335
334
|
});
|
|
336
335
|
|
|
336
|
+
describe("renderBashResult timing comes from the result", () => {
|
|
337
|
+
it("says how long the command took after a session reload, with no row state", () => {
|
|
338
|
+
// A row rebuilt from a stored session has never run in this process: all it
|
|
339
|
+
// has is the duration the result recorded.
|
|
340
|
+
const text = renderBashResult(
|
|
341
|
+
{ content: [{ type: "text", text: "ok" }] },
|
|
342
|
+
{ expanded: true, isPartial: false },
|
|
343
|
+
theme,
|
|
344
|
+
{ durationMs: 4200, executionStarted: true, state: {} },
|
|
345
|
+
);
|
|
346
|
+
expect(content(text)).toContain("Took 4.2s");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("shows the recorded duration, not the wall clock the row happened to span", () => {
|
|
350
|
+
const text = renderBashResult(
|
|
351
|
+
{ content: [{ type: "text", text: "ok" }] },
|
|
352
|
+
{ expanded: true, isPartial: false },
|
|
353
|
+
theme,
|
|
354
|
+
{ durationMs: 1500, executionStarted: true, state: { startedAt: Date.now() - 90_000 } },
|
|
355
|
+
);
|
|
356
|
+
const out = content(text);
|
|
357
|
+
expect(out).toContain("Took 1.5s");
|
|
358
|
+
expect(out).not.toContain("1m 30s");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("collapses to the recorded duration too", () => {
|
|
362
|
+
const text = renderBashResult(
|
|
363
|
+
{ content: [{ type: "text", text: "ok" }] },
|
|
364
|
+
{ expanded: false, isPartial: false },
|
|
365
|
+
theme,
|
|
366
|
+
{ durationMs: 4200, state: {} },
|
|
367
|
+
);
|
|
368
|
+
expect(content(text)).toContain("[muted:4.2s]");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("still counts up while the command runs", () => {
|
|
372
|
+
const text = renderBashResult(
|
|
373
|
+
{ content: [{ type: "text", text: "" }] },
|
|
374
|
+
{ expanded: true, isPartial: true },
|
|
375
|
+
theme,
|
|
376
|
+
{ state: { startedAt: Date.now() - 2500 }, executionStarted: true },
|
|
377
|
+
);
|
|
378
|
+
expect(content(text)).toContain("Elapsed");
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
337
382
|
describe("renderBashResult - Expanded view", () => {
|
|
338
383
|
it("renders full command with $ prefix, output lines, duration, and ✓ Done on success", () => {
|
|
339
384
|
const context = {
|
package/src/bash/renderer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
3
|
-
import { renderToolHeader } from "@pi-archimedes/core/tool-render";
|
|
3
|
+
import { renderToolHeader, toolRowDuration } from "@pi-archimedes/core/tool-render";
|
|
4
4
|
|
|
5
5
|
export interface BashRendererState {
|
|
6
6
|
startedAt?: number | undefined;
|
|
@@ -10,18 +10,6 @@ export interface BashRendererState {
|
|
|
10
10
|
command?: string | undefined;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const activeStates = new Set<BashRendererState>();
|
|
14
|
-
|
|
15
|
-
export function clearActiveBashIntervals(): void {
|
|
16
|
-
for (const state of activeStates) {
|
|
17
|
-
if (state.interval) {
|
|
18
|
-
clearInterval(state.interval);
|
|
19
|
-
state.interval = undefined;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
activeStates.clear();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
13
|
/**
|
|
26
14
|
* Format duration in milliseconds as:
|
|
27
15
|
* - <X.X>s for under 10 seconds (e.g. "1.5s")
|
|
@@ -171,6 +159,7 @@ export function renderBashResult(
|
|
|
171
159
|
const ctx = context as {
|
|
172
160
|
executionStarted?: boolean;
|
|
173
161
|
isError?: boolean;
|
|
162
|
+
durationMs?: number | undefined;
|
|
174
163
|
state?: BashRendererState;
|
|
175
164
|
args?: { command?: string; timeout?: number };
|
|
176
165
|
invalidate?: () => void;
|
|
@@ -185,22 +174,22 @@ export function renderBashResult(
|
|
|
185
174
|
state.startedAt = Date.now();
|
|
186
175
|
}
|
|
187
176
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
177
|
+
// While the result is partial the row repaints once a second so `Elapsed`
|
|
178
|
+
// moves. The interval lives on the row's own state and is cleared when the
|
|
179
|
+
// result settles — the same arrangement pi's rows use, and the reason there
|
|
180
|
+
// is no module-level registry of live rows to sweep on shutdown.
|
|
181
|
+
const settled = !options.isPartial || Boolean(ctx?.isError);
|
|
182
|
+
if (options.isPartial && !state.interval && ctx?.invalidate) {
|
|
183
|
+
state.interval = setInterval(() => {
|
|
184
|
+
ctx.invalidate?.();
|
|
185
|
+
}, 1000);
|
|
195
186
|
}
|
|
196
|
-
|
|
197
|
-
if (!options.isPartial || ctx?.isError) {
|
|
187
|
+
if (settled) {
|
|
198
188
|
state.endedAt ??= Date.now();
|
|
199
189
|
if (state.interval) {
|
|
200
190
|
clearInterval(state.interval);
|
|
201
191
|
state.interval = undefined;
|
|
202
192
|
}
|
|
203
|
-
activeStates.delete(state);
|
|
204
193
|
}
|
|
205
194
|
|
|
206
195
|
const res = result as {
|
|
@@ -219,10 +208,17 @@ export function renderBashResult(
|
|
|
219
208
|
} | undefined;
|
|
220
209
|
|
|
221
210
|
const isError = Boolean(ctx?.isError || res?.isError || result instanceof Error);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
211
|
+
// The duration the tool recorded wins over the row's own clock, so a
|
|
212
|
+
// reloaded session still says how long the command took.
|
|
213
|
+
const timing = toolRowDuration(
|
|
214
|
+
{
|
|
215
|
+
durationMs: ctx?.durationMs,
|
|
216
|
+
startedAt: state.startedAt,
|
|
217
|
+
endedAt: state.endedAt,
|
|
218
|
+
},
|
|
219
|
+
{ isPartial: Boolean(options.isPartial), isError },
|
|
220
|
+
);
|
|
221
|
+
const elapsed = timing?.ms;
|
|
226
222
|
|
|
227
223
|
const textComponent = getTextComponent(context);
|
|
228
224
|
|
|
@@ -241,7 +237,8 @@ export function renderBashResult(
|
|
|
241
237
|
? theme.fg("dim", ` (timeout: ${timeout}s)`)
|
|
242
238
|
: "";
|
|
243
239
|
|
|
244
|
-
const styledDuration =
|
|
240
|
+
const styledDuration =
|
|
241
|
+
elapsed === undefined ? "" : theme.fg("muted", formatDuration(elapsed));
|
|
245
242
|
|
|
246
243
|
textComponent.setText(`${statusGlyph} ${styledDuration}${timeoutSuffix}`);
|
|
247
244
|
return textComponent;
|
|
@@ -277,8 +274,9 @@ export function renderBashResult(
|
|
|
277
274
|
);
|
|
278
275
|
}
|
|
279
276
|
|
|
280
|
-
|
|
281
|
-
|
|
277
|
+
if (elapsed !== undefined) {
|
|
278
|
+
parts.push(theme.fg("muted", `${timing!.label} ${formatDuration(elapsed)}`));
|
|
279
|
+
}
|
|
282
280
|
|
|
283
281
|
if (!options.isPartial) {
|
|
284
282
|
if (isError) {
|
package/src/codemode/index.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
export {
|
|
2
|
-
loadCodemodeModule,
|
|
3
|
-
resolveCodemodeModuleFiles,
|
|
4
|
-
findAgentPackageRoot,
|
|
5
|
-
type CodemodeToolModule,
|
|
6
|
-
} from "./loader.js";
|
|
7
1
|
export {
|
|
8
2
|
renderCodemodeCall,
|
|
9
3
|
renderCodemodeResult,
|
|
@@ -11,10 +5,7 @@ export {
|
|
|
11
5
|
formatCost,
|
|
12
6
|
formatNestedCall,
|
|
13
7
|
getCodemodeOutput,
|
|
14
|
-
parseWallTimeMs,
|
|
15
|
-
clearActiveCodemodeIntervals,
|
|
16
8
|
type CodemodeNestedCall,
|
|
17
9
|
type CodemodeToolDetails,
|
|
18
10
|
type CodemodeRendererState,
|
|
19
11
|
} from "./renderer.js";
|
|
20
|
-
export { registerCodemodeToolOverride } from "./tool.js";
|
|
@@ -5,10 +5,8 @@ import {
|
|
|
5
5
|
formatCost,
|
|
6
6
|
formatNestedCall,
|
|
7
7
|
getCodemodeOutput,
|
|
8
|
-
parseWallTimeMs,
|
|
9
8
|
renderCodemodeCall,
|
|
10
9
|
renderCodemodeResult,
|
|
11
|
-
clearActiveCodemodeIntervals,
|
|
12
10
|
} from "./renderer.js";
|
|
13
11
|
|
|
14
12
|
// Mock Text from @earendil-works/pi-tui while preserving real utility
|
|
@@ -170,29 +168,31 @@ describe("formatNestedCall", () => {
|
|
|
170
168
|
});
|
|
171
169
|
});
|
|
172
170
|
|
|
173
|
-
describe("
|
|
174
|
-
it("
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
{ type: "text", text: "Script failed\nWall time 12.5 seconds\nOutput:\n" },
|
|
186
|
-
]),
|
|
187
|
-
).toBe(12500);
|
|
171
|
+
describe("renderCodemodeResult timing comes from the result", () => {
|
|
172
|
+
it("says how long the script took after a session reload, with no row state", () => {
|
|
173
|
+
// A row rebuilt from a stored session has never run in this process, and the
|
|
174
|
+
// executor's wall-time header is stripped from the output anyway: the
|
|
175
|
+
// duration the result recorded is what the row has.
|
|
176
|
+
const out = renderCodemodeResult(
|
|
177
|
+
{ content: [{ type: "text", text: "Script completed\nWall time 3.2 seconds\nOutput:\n" }] },
|
|
178
|
+
{ expanded: true },
|
|
179
|
+
theme,
|
|
180
|
+
{ durationMs: 3200, executionStarted: true, state: {} },
|
|
181
|
+
);
|
|
182
|
+
expect(text(out)).toContain("Took 3.2s");
|
|
188
183
|
});
|
|
189
184
|
|
|
190
|
-
it("
|
|
191
|
-
|
|
192
|
-
|
|
185
|
+
it("shows the recorded duration, not the wall clock the row happened to span", () => {
|
|
186
|
+
const out = renderCodemodeResult(
|
|
187
|
+
{ content: [] },
|
|
188
|
+
{ expanded: true },
|
|
189
|
+
theme,
|
|
190
|
+
{ durationMs: 250, executionStarted: true, state: { startedAt: Date.now() - 90_000 } },
|
|
193
191
|
);
|
|
194
|
-
|
|
195
|
-
expect(
|
|
192
|
+
const outText = text(out);
|
|
193
|
+
expect(outText).toContain("Took 250ms");
|
|
194
|
+
expect(outText).not.toContain("1.5m");
|
|
195
|
+
expect(outText).not.toContain("90.0s");
|
|
196
196
|
});
|
|
197
197
|
});
|
|
198
198
|
|
|
@@ -286,7 +286,7 @@ describe("renderCodemodeCall", () => {
|
|
|
286
286
|
});
|
|
287
287
|
|
|
288
288
|
describe("renderCodemodeResult - Collapsed view (Minimal)", () => {
|
|
289
|
-
it("renders the success glyph and the
|
|
289
|
+
it("renders the success glyph and the duration the script recorded", () => {
|
|
290
290
|
const out = renderCodemodeResult(
|
|
291
291
|
{
|
|
292
292
|
content: [
|
|
@@ -299,7 +299,7 @@ describe("renderCodemodeResult - Collapsed view (Minimal)", () => {
|
|
|
299
299
|
},
|
|
300
300
|
{ expanded: false },
|
|
301
301
|
theme,
|
|
302
|
-
{},
|
|
302
|
+
{ durationMs: 400 },
|
|
303
303
|
);
|
|
304
304
|
expect(text(out)).toBe("[success:✓] [muted:400ms]");
|
|
305
305
|
});
|
|
@@ -375,7 +375,7 @@ describe("renderCodemodeResult - Collapsed view (Minimal)", () => {
|
|
|
375
375
|
},
|
|
376
376
|
{ expanded: false },
|
|
377
377
|
theme,
|
|
378
|
-
{},
|
|
378
|
+
{ durationMs: 1000 },
|
|
379
379
|
);
|
|
380
380
|
expect(text(out)).toBe("[success:✓] [muted:1.0s]");
|
|
381
381
|
});
|
|
@@ -406,7 +406,7 @@ describe("renderCodemodeResult - Collapsed view (Minimal)", () => {
|
|
|
406
406
|
},
|
|
407
407
|
{ expanded: false },
|
|
408
408
|
theme,
|
|
409
|
-
{ isError: true },
|
|
409
|
+
{ isError: true, durationMs: 200 },
|
|
410
410
|
);
|
|
411
411
|
expect(text(out)).toBe("[error:✗] [muted:200ms]");
|
|
412
412
|
});
|
|
@@ -521,7 +521,7 @@ describe("renderCodemodeResult - Live timer lifecycle", () => {
|
|
|
521
521
|
expect(context.state.endedAt).toBeDefined();
|
|
522
522
|
});
|
|
523
523
|
|
|
524
|
-
it("
|
|
524
|
+
it("stops the once-a-second repaint as soon as the script settles", () => {
|
|
525
525
|
const context = {
|
|
526
526
|
executionStarted: true,
|
|
527
527
|
invalidate: vi.fn(),
|
|
@@ -531,9 +531,10 @@ describe("renderCodemodeResult - Live timer lifecycle", () => {
|
|
|
531
531
|
renderCodemodeResult({}, { isPartial: true }, theme, context);
|
|
532
532
|
expect(context.state.interval).toBeDefined();
|
|
533
533
|
|
|
534
|
-
|
|
534
|
+
renderCodemodeResult({}, { isPartial: false }, theme, context);
|
|
535
535
|
expect(context.state.interval).toBeUndefined();
|
|
536
|
-
|
|
536
|
+
|
|
537
|
+
vi.advanceTimersByTime(3000);
|
|
537
538
|
expect(context.invalidate).not.toHaveBeenCalled();
|
|
538
539
|
});
|
|
539
540
|
|
|
@@ -616,7 +617,7 @@ describe("renderCodemodeResult - Expanded view", () => {
|
|
|
616
617
|
},
|
|
617
618
|
{ expanded: true },
|
|
618
619
|
theme,
|
|
619
|
-
{},
|
|
620
|
+
{ durationMs: 300 },
|
|
620
621
|
);
|
|
621
622
|
const outText = text(out);
|
|
622
623
|
|
|
@@ -645,7 +646,7 @@ describe("renderCodemodeResult - Expanded view", () => {
|
|
|
645
646
|
},
|
|
646
647
|
{ expanded: true },
|
|
647
648
|
theme,
|
|
648
|
-
{ isError: true },
|
|
649
|
+
{ isError: true, durationMs: 100 },
|
|
649
650
|
);
|
|
650
651
|
const outText = text(out);
|
|
651
652
|
|
package/src/codemode/renderer.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
Text,
|
|
8
8
|
type Component,
|
|
9
9
|
} from "@earendil-works/pi-tui";
|
|
10
|
-
import { renderToolHeader } from "@pi-archimedes/core/tool-render";
|
|
10
|
+
import { renderToolHeader, toolRowDuration } from "@pi-archimedes/core/tool-render";
|
|
11
11
|
|
|
12
12
|
/** Nested-call args preview budget, collapsed (the native renderer uses 80). */
|
|
13
13
|
const COLLAPSED_CALL_ARGS_CHARS = 80;
|
|
@@ -42,23 +42,6 @@ export interface CodemodeRendererState {
|
|
|
42
42
|
interval?: NodeJS.Timeout | undefined;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const activeStates = new Set<CodemodeRendererState>();
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Clear live-timer intervals of in-flight codemode rows. Called on
|
|
49
|
-
* session_shutdown so a cancelled script does not keep invalidating a
|
|
50
|
-
* component that no longer exists.
|
|
51
|
-
*/
|
|
52
|
-
export function clearActiveCodemodeIntervals(): void {
|
|
53
|
-
for (const state of activeStates) {
|
|
54
|
-
if (state.interval) {
|
|
55
|
-
clearInterval(state.interval);
|
|
56
|
-
state.interval = undefined;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
activeStates.clear();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
45
|
/**
|
|
63
46
|
* Format a duration in milliseconds: `<X>ms` under a second, `<X.X>s`
|
|
64
47
|
* otherwise (the native codemode renderer's format).
|
|
@@ -157,16 +140,17 @@ export function getCodemodeOutput(
|
|
|
157
140
|
}
|
|
158
141
|
|
|
159
142
|
/**
|
|
160
|
-
*
|
|
161
|
-
* render after execution began,
|
|
162
|
-
*
|
|
143
|
+
* Keep the row's own clock for the live `Elapsed` count: start on the first
|
|
144
|
+
* render after execution began, stop the repaint when the result settles (or
|
|
145
|
+
* errors). The interval lives on the row's own state — pi's rows do the same —
|
|
146
|
+
* and there is no module-level registry of live rows to sweep on shutdown.
|
|
163
147
|
*
|
|
164
148
|
* `isPartial` means "still in flight": the CALL renderer passes `true`
|
|
165
149
|
* (a call is in flight until its result settles) — passing `false` there
|
|
166
|
-
* would record `endedAt` at call-render time and freeze the
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
150
|
+
* would record `endedAt` at call-render time and freeze the count for the rest
|
|
151
|
+
* of the script. The interval is (re)created only while the result has not
|
|
152
|
+
* settled yet (`endedAt === undefined`), so a re-render of a settled call never
|
|
153
|
+
* spawns a permanent interval.
|
|
170
154
|
*/
|
|
171
155
|
function updateTiming(
|
|
172
156
|
ctx: {
|
|
@@ -183,41 +167,22 @@ function updateTiming(
|
|
|
183
167
|
if ((ctx?.executionStarted || isPartial) && state.startedAt === undefined) {
|
|
184
168
|
state.startedAt = Date.now();
|
|
185
169
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
170
|
+
|
|
171
|
+
const settled = !isPartial || ctx?.isError === true;
|
|
172
|
+
if (isPartial && !state.interval && state.endedAt === undefined && ctx?.invalidate) {
|
|
173
|
+
state.interval = setInterval(() => {
|
|
174
|
+
ctx.invalidate?.();
|
|
175
|
+
}, 1000);
|
|
193
176
|
}
|
|
194
|
-
if (
|
|
177
|
+
if (settled) {
|
|
195
178
|
state.endedAt ??= Date.now();
|
|
196
179
|
if (state.interval) {
|
|
197
180
|
clearInterval(state.interval);
|
|
198
181
|
state.interval = undefined;
|
|
199
182
|
}
|
|
200
|
-
activeStates.delete(state);
|
|
201
183
|
}
|
|
202
184
|
}
|
|
203
185
|
|
|
204
|
-
/**
|
|
205
|
-
* The script's wall time in ms, parsed from the result header when present
|
|
206
|
-
* (more accurate than the renderer's timer, which spans TUI updates).
|
|
207
|
-
*/
|
|
208
|
-
export function parseWallTimeMs(
|
|
209
|
-
content: Array<{ type: string; text?: string }>,
|
|
210
|
-
): number | undefined {
|
|
211
|
-
const first = content[0];
|
|
212
|
-
if (first?.type !== "text" || typeof first.text !== "string") return undefined;
|
|
213
|
-
const match = first.text.match(
|
|
214
|
-
/^Script (completed|failed)\nWall time ([\d.]+) seconds/,
|
|
215
|
-
);
|
|
216
|
-
if (!match) return undefined;
|
|
217
|
-
const seconds = Number.parseFloat(match[2]!);
|
|
218
|
-
return Number.isFinite(seconds) ? seconds * 1000 : undefined;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
186
|
/**
|
|
222
187
|
* Render the codemode tool call: the bold `codemode` header. Collapsed, the
|
|
223
188
|
* header stands alone — the script (syntax-highlighted) shows when expanded.
|
|
@@ -294,6 +259,7 @@ export function renderCodemodeResult(
|
|
|
294
259
|
const ctx = context as {
|
|
295
260
|
executionStarted?: boolean;
|
|
296
261
|
isError?: boolean;
|
|
262
|
+
durationMs?: number | undefined;
|
|
297
263
|
state?: CodemodeRendererState;
|
|
298
264
|
invalidate?: () => void;
|
|
299
265
|
} | undefined;
|
|
@@ -308,14 +274,17 @@ export function renderCodemodeResult(
|
|
|
308
274
|
const calls = res?.details?.calls ?? [];
|
|
309
275
|
const isError = Boolean(ctx?.isError);
|
|
310
276
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
:
|
|
318
|
-
|
|
277
|
+
// The duration the script recorded wins over the row's own clock, so a
|
|
278
|
+
// reloaded session still says how long the script took.
|
|
279
|
+
const timing = toolRowDuration(
|
|
280
|
+
{
|
|
281
|
+
durationMs: ctx?.durationMs,
|
|
282
|
+
startedAt: ctx?.state?.startedAt,
|
|
283
|
+
endedAt: ctx?.state?.endedAt,
|
|
284
|
+
},
|
|
285
|
+
{ isPartial: Boolean(options.isPartial), isError },
|
|
286
|
+
);
|
|
287
|
+
const durationMs = timing?.ms;
|
|
319
288
|
|
|
320
289
|
const container = new Container();
|
|
321
290
|
|
|
@@ -378,9 +347,8 @@ export function renderCodemodeResult(
|
|
|
378
347
|
}
|
|
379
348
|
|
|
380
349
|
if (durationMs !== undefined) {
|
|
381
|
-
const label = options.isPartial ? "Elapsed" : "Took";
|
|
382
350
|
parts.push(
|
|
383
|
-
theme.fg("muted", `${label} ${formatCodemodeDuration(durationMs)}`),
|
|
351
|
+
theme.fg("muted", `${timing!.label} ${formatCodemodeDuration(durationMs)}`),
|
|
384
352
|
);
|
|
385
353
|
}
|
|
386
354
|
|