@hheei/pi-ext-ui 0.7.0 → 0.7.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/dist/background.d.ts +7 -0
- package/dist/background.js +13 -0
- package/dist/host-transcript-patch.js +12 -2
- package/dist/message-view.js +8 -3
- package/dist/renderer.js +12 -8
- package/dist/tool-group-view.d.ts +2 -0
- package/dist/tool-group-view.js +59 -16
- package/dist/tool-view.d.ts +1 -1
- package/dist/tool-view.js +16 -10
- package/package.json +1 -1
package/dist/background.d.ts
CHANGED
|
@@ -9,3 +9,10 @@ import type { Theme, ThemeBg } from "@earendil-works/pi-coding-agent";
|
|
|
9
9
|
* one paints the rows it draws, not because this function tracks ANSI state.
|
|
10
10
|
*/
|
|
11
11
|
export declare function backgroundFill(theme: Theme, token: ThemeBg): (text: string) => string;
|
|
12
|
+
/**
|
|
13
|
+
* One independent surface: a filled padding row above and below, every row reaching `width`.
|
|
14
|
+
*
|
|
15
|
+
* A block that owns a surface also owns its breathing room, so two adjacent blocks separate with
|
|
16
|
+
* their own padding rows instead of sharing one unstyled gap.
|
|
17
|
+
*/
|
|
18
|
+
export declare function surfaceBlock(theme: Theme, token: ThemeBg, rows: readonly string[], width: number): string[];
|
package/dist/background.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
1
2
|
const MARK = "\u0000";
|
|
2
3
|
/** Any SGR sequence: parameters are `;`-separated, an empty parameter list means reset. */
|
|
3
4
|
const SGR = /\x1b\[([0-9;]*)m/g;
|
|
@@ -29,3 +30,15 @@ export function backgroundFill(theme, token) {
|
|
|
29
30
|
return `${out}${text.slice(from)}${end}`;
|
|
30
31
|
};
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* One independent surface: a filled padding row above and below, every row reaching `width`.
|
|
35
|
+
*
|
|
36
|
+
* A block that owns a surface also owns its breathing room, so two adjacent blocks separate with
|
|
37
|
+
* their own padding rows instead of sharing one unstyled gap.
|
|
38
|
+
*/
|
|
39
|
+
export function surfaceBlock(theme, token, rows, width) {
|
|
40
|
+
const fill = backgroundFill(theme, token);
|
|
41
|
+
const pad = fill(" ".repeat(Math.max(0, width)));
|
|
42
|
+
const body = rows.map((row) => fill(row + " ".repeat(Math.max(0, width - visibleWidth(row)))));
|
|
43
|
+
return [pad, ...body, pad];
|
|
44
|
+
}
|
|
@@ -200,6 +200,8 @@ function attach(host, run) {
|
|
|
200
200
|
throw new Error("Pi transcript cannot accept instance methods");
|
|
201
201
|
const projection = new Container();
|
|
202
202
|
let groups = new Map();
|
|
203
|
+
/** One wrapper per tail component, so its reserved height survives a transcript rebuild. */
|
|
204
|
+
let tails = new Map();
|
|
203
205
|
let active = true;
|
|
204
206
|
const disposeGroups = () => {
|
|
205
207
|
for (const group of groups.values())
|
|
@@ -254,6 +256,7 @@ function attach(host, run) {
|
|
|
254
256
|
};
|
|
255
257
|
const rebuild = () => {
|
|
256
258
|
const next = new Map();
|
|
259
|
+
const nextTails = new Map();
|
|
257
260
|
const children = [];
|
|
258
261
|
const blocks = [];
|
|
259
262
|
let calls = new Set();
|
|
@@ -290,8 +293,14 @@ function attach(host, run) {
|
|
|
290
293
|
});
|
|
291
294
|
next.set(first, group);
|
|
292
295
|
children.push(group);
|
|
293
|
-
if (tail !== undefined)
|
|
294
|
-
|
|
296
|
+
if (tail !== undefined) {
|
|
297
|
+
const key = memberParts(tail)[0];
|
|
298
|
+
const unit = (key === undefined ? undefined : tails.get(key)) ?? new TailUnit(tail);
|
|
299
|
+
unit.update(tail);
|
|
300
|
+
if (key !== undefined)
|
|
301
|
+
nextTails.set(key, unit);
|
|
302
|
+
children.push(unit);
|
|
303
|
+
}
|
|
295
304
|
}
|
|
296
305
|
else
|
|
297
306
|
children.push(...members.flatMap(memberParts));
|
|
@@ -363,6 +372,7 @@ function attach(host, run) {
|
|
|
363
372
|
group.dispose();
|
|
364
373
|
}
|
|
365
374
|
groups = next;
|
|
375
|
+
tails = nextTails;
|
|
366
376
|
projection.children = children;
|
|
367
377
|
};
|
|
368
378
|
const wrappers = {
|
package/dist/message-view.js
CHANGED
|
@@ -69,8 +69,12 @@ export class MessageView {
|
|
|
69
69
|
* `customMessageBg` box when no renderer is installed, so this view supplies the same block
|
|
70
70
|
* itself. Inside an activity group the rows come from `previewRows`/`bodyRows` and keep the
|
|
71
71
|
* group's background instead.
|
|
72
|
+
*
|
|
73
|
+
* A surface that owns its rows also owns one padding row above and below, the same breathing room
|
|
74
|
+
* a Pi-native tool surface gives itself; without it the colored block is a tight strip of text.
|
|
72
75
|
*/
|
|
73
|
-
#
|
|
76
|
+
#padY = 1;
|
|
77
|
+
#shell = new Box(0, this.#padY);
|
|
74
78
|
constructor(spec) {
|
|
75
79
|
this.#spec = spec;
|
|
76
80
|
this.#localExpanded = remembered(spec.stateHolder, spec.defaultExpanded === true);
|
|
@@ -130,9 +134,10 @@ export class MessageView {
|
|
|
130
134
|
const innerWidth = Math.max(1, effectiveWidth - pad * 2);
|
|
131
135
|
if (innerX < 0 || innerX >= innerWidth)
|
|
132
136
|
return undefined;
|
|
133
|
-
if (this.#lastHeight > 0 && (event.y < 0 || event.y >= this.#lastHeight))
|
|
137
|
+
if (this.#lastHeight > 0 && (event.y < 0 || event.y >= this.#lastHeight + this.#padY))
|
|
134
138
|
return undefined;
|
|
135
|
-
|
|
139
|
+
// The shell's own padding rows belong to the surface, not to the header row.
|
|
140
|
+
if (event.y !== this.#padY)
|
|
136
141
|
return undefined;
|
|
137
142
|
this.toggle();
|
|
138
143
|
return { handled: true, render: true };
|
package/dist/renderer.js
CHANGED
|
@@ -262,9 +262,15 @@ function renderGenericResult(params) {
|
|
|
262
262
|
const { outputText: rawOutput, wallTimeSeconds } = nativeResultText(toolName, result);
|
|
263
263
|
const isError = (context.isError ?? false) || (result.isError ?? false);
|
|
264
264
|
const status = toolName === "codemode"
|
|
265
|
-
? childCalls.length
|
|
266
|
-
? "
|
|
267
|
-
:
|
|
265
|
+
? childCalls.length > 0
|
|
266
|
+
? "none"
|
|
267
|
+
: isError
|
|
268
|
+
? "error"
|
|
269
|
+
: renderOptions.isPartial === true
|
|
270
|
+
? context.argsComplete
|
|
271
|
+
? "running"
|
|
272
|
+
: "streaming"
|
|
273
|
+
: "none"
|
|
268
274
|
: isMetaTool
|
|
269
275
|
? resolveContainerStatus({
|
|
270
276
|
isError,
|
|
@@ -672,11 +678,9 @@ function createRendererResolver(runtime, options, resolvePresentation, resolveNa
|
|
|
672
678
|
const isStreamingArgs = context.isPartial && !context.argsComplete;
|
|
673
679
|
const status = context.isError
|
|
674
680
|
? "error"
|
|
675
|
-
:
|
|
676
|
-
? "
|
|
677
|
-
:
|
|
678
|
-
? "streaming"
|
|
679
|
-
: "running";
|
|
681
|
+
: isStreamingArgs
|
|
682
|
+
? "streaming"
|
|
683
|
+
: "running";
|
|
680
684
|
const streamedTokens = isStreamingArgs ? estimateStreamingTokens(args) : 0;
|
|
681
685
|
const header = isStreamingArgs
|
|
682
686
|
? { summary: streamedTokens > 0 ? `· ${String(streamedTokens)} tokens` : "" }
|
|
@@ -57,6 +57,8 @@ export declare function memberParts(member: Member): readonly Component[];
|
|
|
57
57
|
export declare class TailUnit implements Component {
|
|
58
58
|
#private;
|
|
59
59
|
constructor(member: Member);
|
|
60
|
+
/** The same tail with a newer fact: the component list can grow, the reserved height must not. */
|
|
61
|
+
update(member: Member): void;
|
|
60
62
|
render(width: number): string[];
|
|
61
63
|
handleMouse(event: Parameters<NonNullable<Component["handleMouse"]>>[0]): import("@earendil-works/pi-tui").TuiMouseEventResult | undefined;
|
|
62
64
|
invalidate(): void;
|
package/dist/tool-group-view.js
CHANGED
|
@@ -4,6 +4,10 @@ import { backgroundFill } from "./background.js";
|
|
|
4
4
|
import { isCollapsible, toContentRows, toSingleRow, } from "./tool-view.js";
|
|
5
5
|
/** A settled block keeps its surface this long, then drops it so the transcript stays quiet. */
|
|
6
6
|
const SURFACE_HOLD_MS = 5_000;
|
|
7
|
+
/** A tool that has not received its final result yet: its own rows are still changing height. */
|
|
8
|
+
function isLiveTail(member) {
|
|
9
|
+
return member.kind === "tool" && member.completed !== true;
|
|
10
|
+
}
|
|
7
11
|
/** Every original component of a member, in transcript order. */
|
|
8
12
|
export function memberParts(member) {
|
|
9
13
|
return member.parts ?? [member.component];
|
|
@@ -11,8 +15,17 @@ export function memberParts(member) {
|
|
|
11
15
|
export class TailUnit {
|
|
12
16
|
#parts;
|
|
13
17
|
#offsets = [];
|
|
18
|
+
/** Rows already shown for a live member: a tool that grows may not shrink back and shift the block. */
|
|
19
|
+
#reserved = 0;
|
|
20
|
+
#live;
|
|
14
21
|
constructor(member) {
|
|
15
22
|
this.#parts = memberParts(member);
|
|
23
|
+
this.#live = isLiveTail(member);
|
|
24
|
+
}
|
|
25
|
+
/** The same tail with a newer fact: the component list can grow, the reserved height must not. */
|
|
26
|
+
update(member) {
|
|
27
|
+
this.#parts = memberParts(member);
|
|
28
|
+
this.#live = isLiveTail(member);
|
|
16
29
|
}
|
|
17
30
|
render(width) {
|
|
18
31
|
const offsets = [];
|
|
@@ -29,7 +42,18 @@ export class TailUnit {
|
|
|
29
42
|
rows.push(...own);
|
|
30
43
|
}
|
|
31
44
|
this.#offsets = offsets;
|
|
32
|
-
|
|
45
|
+
if (rows.length === 0)
|
|
46
|
+
return rows;
|
|
47
|
+
const out = rows[0] === "" ? rows : ["", ...rows];
|
|
48
|
+
// A live tail grows downward from a fixed top: the host keeps the transcript's bottom in view, so
|
|
49
|
+
// a row count that falls back drags everything above it down. Reserve the tallest frame instead.
|
|
50
|
+
if (this.#live)
|
|
51
|
+
this.#reserved = Math.max(this.#reserved, out.length);
|
|
52
|
+
else
|
|
53
|
+
this.#reserved = 0;
|
|
54
|
+
while (out.length < this.#reserved)
|
|
55
|
+
out.push("");
|
|
56
|
+
return out;
|
|
33
57
|
}
|
|
34
58
|
handleMouse(event) {
|
|
35
59
|
for (const { part, top, height, skip } of this.#offsets) {
|
|
@@ -78,9 +102,11 @@ function formatTokens(count) {
|
|
|
78
102
|
*/
|
|
79
103
|
function cacheHitRate(latestPrompt) {
|
|
80
104
|
if (latestPrompt === undefined)
|
|
81
|
-
return
|
|
105
|
+
return undefined;
|
|
82
106
|
const prompt = latestPrompt.input + latestPrompt.cacheRead + latestPrompt.cacheWrite;
|
|
83
|
-
|
|
107
|
+
if (prompt <= 0)
|
|
108
|
+
return undefined;
|
|
109
|
+
return `${((latestPrompt.cacheRead / prompt) * 100).toFixed(1)}%`;
|
|
84
110
|
}
|
|
85
111
|
const PREVIEW_GRAPHEMES = 20;
|
|
86
112
|
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
@@ -250,35 +276,51 @@ export class ToolGroupView extends Container {
|
|
|
250
276
|
let requests = 0;
|
|
251
277
|
let input = 0;
|
|
252
278
|
let output = 0;
|
|
253
|
-
let
|
|
279
|
+
let cacheRead = 0;
|
|
280
|
+
let cacheWrite = 0;
|
|
254
281
|
let latestPrompt;
|
|
255
282
|
for (const fact of this.#requests) {
|
|
256
283
|
requests += 1;
|
|
257
284
|
const usage = fact.usage;
|
|
258
285
|
if (usage === undefined)
|
|
259
286
|
continue;
|
|
260
|
-
measured = true;
|
|
261
287
|
input += usage.input;
|
|
262
288
|
output += usage.output;
|
|
289
|
+
cacheRead += usage.cacheRead;
|
|
290
|
+
cacheWrite += usage.cacheWrite;
|
|
263
291
|
// A streaming snapshot can arrive with every field zeroed; that is a placeholder, not a
|
|
264
292
|
// measured prompt, so it must not overwrite the last known cache-hit fact.
|
|
265
293
|
if (usage.input + usage.cacheRead + usage.cacheWrite > 0)
|
|
266
294
|
latestPrompt = usage;
|
|
267
295
|
}
|
|
268
|
-
return { requests,
|
|
296
|
+
return { requests, input, output, cacheRead, cacheWrite, latestPrompt };
|
|
269
297
|
}
|
|
270
|
-
/**
|
|
298
|
+
/**
|
|
299
|
+
* The block footer: the block's request count, then the same token facts and the same symbols
|
|
300
|
+
* Pi's own footer uses (`↑input ↓output RcacheRead WcacheWrite CHrate`). Pi totals the whole
|
|
301
|
+
* session; this totals one block, so the two never have to agree on the number.
|
|
302
|
+
*/
|
|
271
303
|
#footerRow() {
|
|
272
304
|
const theme = this.#ui.theme;
|
|
273
305
|
const usage = this.#usage();
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
306
|
+
const parts = [`requests ×${usage.requests}`];
|
|
307
|
+
if (usage.input + usage.output + usage.cacheRead + usage.cacheWrite === 0) {
|
|
308
|
+
parts.push("↑—", "↓—");
|
|
309
|
+
return theme.fg("dim", parts.join(" · "));
|
|
310
|
+
}
|
|
311
|
+
if (usage.input > 0)
|
|
312
|
+
parts.push(`↑${formatTokens(usage.input)}`);
|
|
313
|
+
if (usage.output > 0)
|
|
314
|
+
parts.push(`↓${formatTokens(usage.output)}`);
|
|
315
|
+
if (usage.cacheRead > 0)
|
|
316
|
+
parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
317
|
+
if (usage.cacheWrite > 0)
|
|
318
|
+
parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
319
|
+
if (usage.cacheRead > 0 || usage.cacheWrite > 0) {
|
|
320
|
+
const rate = cacheHitRate(usage.latestPrompt);
|
|
321
|
+
if (rate !== undefined)
|
|
322
|
+
parts.push(`CH${rate}`);
|
|
323
|
+
}
|
|
282
324
|
return theme.fg("dim", parts.join(" · "));
|
|
283
325
|
}
|
|
284
326
|
#categoryRow(category, label, detail) {
|
|
@@ -306,7 +348,8 @@ export class ToolGroupView extends Container {
|
|
|
306
348
|
const own = wrapper.render(Math.max(1, width));
|
|
307
349
|
if (own.length === 0)
|
|
308
350
|
continue;
|
|
309
|
-
// One blank row between adjacent semantic blocks
|
|
351
|
+
// One blank row between adjacent semantic blocks. A thinking run has no surface of its own,
|
|
352
|
+
// so it keeps the plain separator even when tools paint their own padded surfaces.
|
|
310
353
|
if (lines.length > 0) {
|
|
311
354
|
lines.push("");
|
|
312
355
|
hits.push({ height: 1, action: { kind: "none" } });
|
package/dist/tool-view.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export declare const MAX_LEVEL2_SECTION_LINES = 500;
|
|
|
66
66
|
export declare const MAX_LEVEL2_BYTES: number;
|
|
67
67
|
export declare function transitionFoldLevel(current: FoldLevel, ctrlKey: boolean): FoldLevel;
|
|
68
68
|
export declare const TOOL_SPINNER_FRAMES: readonly ["·", "✢", "✳", "✶", "✻", "✽", "✻", "✶", "✳", "✢"];
|
|
69
|
-
export declare const TOOL_SPINNER_INTERVAL_MS =
|
|
69
|
+
export declare const TOOL_SPINNER_INTERVAL_MS = 200;
|
|
70
70
|
/**
|
|
71
71
|
* One clock for every live tool, so the phase is shared and never restarts: streaming args and a
|
|
72
72
|
* running execution draw the same frame and differ only in color.
|
package/dist/tool-view.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { copyToClipboard as copyToSystemClipboard, } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
|
|
4
|
-
import {
|
|
4
|
+
import { surfaceBlock } from "./background.js";
|
|
5
5
|
/** Pi native tool surfaces: a failure outranks work in progress, which outranks a settled tool. */
|
|
6
6
|
function nativeSurface(status) {
|
|
7
7
|
if (status === "error")
|
|
@@ -67,7 +67,7 @@ function isCopyFailed(stateHolder, localFailed) {
|
|
|
67
67
|
return isRecord(stateHolder) && stateHolder[COPY_FAILED_STATE_KEY] === true;
|
|
68
68
|
}
|
|
69
69
|
export const TOOL_SPINNER_FRAMES = ["·", "✢", "✳", "✶", "✻", "✽", "✻", "✶", "✳", "✢"];
|
|
70
|
-
export const TOOL_SPINNER_INTERVAL_MS =
|
|
70
|
+
export const TOOL_SPINNER_INTERVAL_MS = 200;
|
|
71
71
|
/**
|
|
72
72
|
* One clock for every live tool, so the phase is shared and never restarts: streaming args and a
|
|
73
73
|
* running execution draw the same frame and differ only in color.
|
|
@@ -389,9 +389,13 @@ export class ToolView {
|
|
|
389
389
|
#renderHeader(width, level) {
|
|
390
390
|
const { theme, header, copy } = this.#spec;
|
|
391
391
|
const safeWidth = Math.max(1, width);
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const
|
|
392
|
+
// A live tool says so in its leading slot: one glyph, no `▸` in front of a second status mark.
|
|
393
|
+
// A container that already lists child calls is not live any more, so it keeps the fold pin.
|
|
394
|
+
const live = level === 0 && (header.status === "streaming" || header.status === "running");
|
|
395
|
+
const pin = live
|
|
396
|
+
? formatStatusGlyph(header.status, theme)
|
|
397
|
+
: theme.fg("dim", level === 0 ? "▸" : "▾");
|
|
398
|
+
const leftContent = `${pin} ${formatToolHeaderText(live ? { ...header, status: "none" } : header, theme)}`;
|
|
395
399
|
if (copy === undefined || safeWidth < 6) {
|
|
396
400
|
return truncate(leftContent, safeWidth);
|
|
397
401
|
}
|
|
@@ -507,12 +511,16 @@ export class ToolView {
|
|
|
507
511
|
const native = this.#spec.background?.() === true;
|
|
508
512
|
const surface = native ? nativeSurface(this.#spec.header.status) : undefined;
|
|
509
513
|
const noRail = this.#spec.noRail === true || native;
|
|
514
|
+
// The glyph of a live tool changes on its own clock, so a cached frame would freeze it.
|
|
515
|
+
const status = this.#spec.header.status;
|
|
516
|
+
const glyph = status === "streaming" || status === "running" ? toolSpinnerFrame() : "";
|
|
510
517
|
if (this.#cache !== undefined &&
|
|
511
518
|
this.#cache.width === width &&
|
|
512
519
|
this.#cache.level === level &&
|
|
513
520
|
this.#cache.noRail === noRail &&
|
|
514
521
|
this.#cache.surface === surface &&
|
|
515
|
-
this.#cache.copied === copied
|
|
522
|
+
this.#cache.copied === copied &&
|
|
523
|
+
this.#cache.glyph === glyph) {
|
|
516
524
|
return this.#cache.lines;
|
|
517
525
|
}
|
|
518
526
|
const pad = Math.max(0, this.#spec.outputPad ?? 0);
|
|
@@ -537,11 +545,9 @@ export class ToolView {
|
|
|
537
545
|
}
|
|
538
546
|
}
|
|
539
547
|
this.#lastTopPad = surface === undefined ? 0 : 1;
|
|
540
|
-
const lines = surface === undefined
|
|
541
|
-
? safeLines
|
|
542
|
-
: ["", ...safeLines, ""].map((line) => backgroundFill(this.#spec.theme, surface)(line + " ".repeat(Math.max(0, width - visibleWidth(line)))));
|
|
548
|
+
const lines = surface === undefined ? safeLines : surfaceBlock(this.#spec.theme, surface, safeLines, width);
|
|
543
549
|
this.#lastHeight = lines.length;
|
|
544
|
-
this.#cache = { width, level, copied, noRail, surface, lines };
|
|
550
|
+
this.#cache = { width, level, copied, noRail, surface, glyph, lines };
|
|
545
551
|
return lines;
|
|
546
552
|
}
|
|
547
553
|
handleMouse(event) {
|