@hheei/pi-ext-ui 0.7.1 → 0.8.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/dist/extension.js +2 -36
- package/dist/host-transcript-patch.d.ts +1 -1
- package/dist/host-transcript-patch.js +17 -26
- package/dist/nested-calls-view.js +30 -4
- package/dist/tool-group-view.d.ts +2 -4
- package/dist/tool-group-view.js +45 -30
- package/dist/tool-view.js +27 -2
- package/package.json +5 -6
package/dist/extension.js
CHANGED
|
@@ -4,7 +4,6 @@ import { createMessageView } from "./message-view.js";
|
|
|
4
4
|
import { createOptionalToolRendererFactory, registerCollapsibleToolRenderer } from "./renderer.js";
|
|
5
5
|
import { ToolUiRuntime } from "./runtime.js";
|
|
6
6
|
import { activityGroupsEnabled, createToolGroupSettings, toolBackgroundsEnabled, } from "./settings.js";
|
|
7
|
-
import { TOOL_SPINNER_INTERVAL_MS } from "./tool-view.js";
|
|
8
7
|
export default function (pi) {
|
|
9
8
|
// One runtime per extension instance: two Pi runtimes in one process never share these facts.
|
|
10
9
|
const runtime = new ToolUiRuntime();
|
|
@@ -27,7 +26,7 @@ export default function (pi) {
|
|
|
27
26
|
if (patch !== undefined)
|
|
28
27
|
return;
|
|
29
28
|
try {
|
|
30
|
-
patch = installHostToolGroups(context.extension);
|
|
29
|
+
patch = installHostToolGroups(context.extension, () => runtime.backgrounds);
|
|
31
30
|
redrawGroups = patch.requestRender;
|
|
32
31
|
}
|
|
33
32
|
catch (error) {
|
|
@@ -46,47 +45,14 @@ export default function (pi) {
|
|
|
46
45
|
patch?.beginRun();
|
|
47
46
|
redrawGroups?.();
|
|
48
47
|
});
|
|
49
|
-
// A live tool animates its status glyph. The ticker exists only while one is running, and the
|
|
50
|
-
// frame itself comes from one shared clock, so a phase never restarts on a status change.
|
|
51
|
-
let liveTools = 0;
|
|
52
|
-
let ticker;
|
|
53
|
-
const stopTicker = () => {
|
|
54
|
-
if (ticker === undefined)
|
|
55
|
-
return;
|
|
56
|
-
clearInterval(ticker);
|
|
57
|
-
ticker = undefined;
|
|
58
|
-
};
|
|
59
48
|
const onAgentEnd = pi.on("agent_end", () => {
|
|
60
49
|
patch?.endRun();
|
|
61
|
-
// No tool outlives the agent loop, so the animation cannot keep a ticker alive either.
|
|
62
|
-
liveTools = 0;
|
|
63
|
-
stopTicker();
|
|
64
50
|
redrawGroups?.();
|
|
65
51
|
});
|
|
66
|
-
|
|
67
|
-
liveTools += 1;
|
|
68
|
-
if (ticker !== undefined)
|
|
69
|
-
return;
|
|
70
|
-
ticker = setInterval(() => {
|
|
71
|
-
if (liveTools === 0 || redrawGroups === undefined) {
|
|
72
|
-
stopTicker();
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
redrawGroups();
|
|
76
|
-
}, TOOL_SPINNER_INTERVAL_MS);
|
|
77
|
-
});
|
|
78
|
-
const onToolEnd = pi.on("tool_execution_end", () => {
|
|
79
|
-
liveTools = Math.max(0, liveTools - 1);
|
|
80
|
-
if (liveTools === 0)
|
|
81
|
-
stopTicker();
|
|
82
|
-
});
|
|
52
|
+
// The live glyph clock lives with the rows that draw it, so nothing here ticks.
|
|
83
53
|
context.resources.add("run-clock", () => {
|
|
84
54
|
onAgentStart();
|
|
85
55
|
onAgentEnd();
|
|
86
|
-
onToolStart();
|
|
87
|
-
onToolEnd();
|
|
88
|
-
stopTicker();
|
|
89
|
-
liveTools = 0;
|
|
90
56
|
});
|
|
91
57
|
context.resources.add("tool-group-settings", registerSettings(provider, getRuntimeSettingsRegistry(pi)));
|
|
92
58
|
context.resources.add("tool-groups", () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
/** Private to pi-ext-ui: two process hooks, scoped by the actual host UI facade. */
|
|
3
|
-
export declare function installHostToolGroups(context: ExtensionContext): {
|
|
3
|
+
export declare function installHostToolGroups(context: ExtensionContext, background: () => boolean): {
|
|
4
4
|
dispose(): void;
|
|
5
5
|
requestRender(): void;
|
|
6
6
|
beginRun(): void;
|
|
@@ -182,7 +182,7 @@ class NotifyView {
|
|
|
182
182
|
this.#text.invalidate();
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
-
function attach(host, run) {
|
|
185
|
+
function attach(host, run, background) {
|
|
186
186
|
const chat = host.chat;
|
|
187
187
|
const original = {
|
|
188
188
|
render: chat.render,
|
|
@@ -200,8 +200,6 @@ 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();
|
|
205
203
|
let active = true;
|
|
206
204
|
const disposeGroups = () => {
|
|
207
205
|
for (const group of groups.values())
|
|
@@ -256,7 +254,6 @@ function attach(host, run) {
|
|
|
256
254
|
};
|
|
257
255
|
const rebuild = () => {
|
|
258
256
|
const next = new Map();
|
|
259
|
-
const nextTails = new Map();
|
|
260
257
|
const children = [];
|
|
261
258
|
const blocks = [];
|
|
262
259
|
let calls = new Set();
|
|
@@ -269,6 +266,14 @@ function attach(host, run) {
|
|
|
269
266
|
*/
|
|
270
267
|
const flush = (ended, boundary) => {
|
|
271
268
|
const first = members[0]?.component;
|
|
269
|
+
// Empty assistant/tool-call placeholders do not replace the last visible tail.
|
|
270
|
+
const tail = ended
|
|
271
|
+
? undefined
|
|
272
|
+
: [...members]
|
|
273
|
+
.reverse()
|
|
274
|
+
.find((member) => member.kind !== "thinking" ||
|
|
275
|
+
(member.thinking !== undefined && member.thinking !== ""));
|
|
276
|
+
const inside = members.filter((member) => member !== tail);
|
|
272
277
|
let weight = 0;
|
|
273
278
|
for (const member of members) {
|
|
274
279
|
if (member.kind === "tool")
|
|
@@ -278,32 +283,19 @@ function attach(host, run) {
|
|
|
278
283
|
weight += 0.5;
|
|
279
284
|
}
|
|
280
285
|
if (first !== undefined && weight >= 2) {
|
|
281
|
-
const group = groups.get(first) ?? new ToolGroupView(host.ui, host.requestRender);
|
|
282
|
-
const tail = ended ? undefined : members.at(-1);
|
|
283
|
-
const inside = tail === undefined ? members : members.slice(0, -1);
|
|
286
|
+
const group = groups.get(first) ?? new ToolGroupView(host.ui, host.requestRender, background);
|
|
284
287
|
const boundaryTime = ended && boundary !== undefined
|
|
285
288
|
? (run.observed.get(boundary)?.at ?? performance.now())
|
|
286
289
|
: undefined;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
members: inside,
|
|
290
|
-
tail,
|
|
291
|
-
requests,
|
|
292
|
-
clock: clockOf(members, boundaryTime),
|
|
293
|
-
});
|
|
290
|
+
const clock = clockOf(members, boundaryTime);
|
|
291
|
+
blocks.push({ group, members: inside, tail, requests, clock });
|
|
294
292
|
next.set(first, group);
|
|
295
293
|
children.push(group);
|
|
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
|
-
}
|
|
304
294
|
}
|
|
305
295
|
else
|
|
306
|
-
children.push(...
|
|
296
|
+
children.push(...inside.flatMap(memberParts));
|
|
297
|
+
if (tail !== undefined)
|
|
298
|
+
children.push(new TailUnit(tail, background));
|
|
307
299
|
members = [];
|
|
308
300
|
requests = [];
|
|
309
301
|
};
|
|
@@ -372,7 +364,6 @@ function attach(host, run) {
|
|
|
372
364
|
group.dispose();
|
|
373
365
|
}
|
|
374
366
|
groups = next;
|
|
375
|
-
tails = nextTails;
|
|
376
367
|
projection.children = children;
|
|
377
368
|
};
|
|
378
369
|
const wrappers = {
|
|
@@ -522,7 +513,7 @@ function patchState() {
|
|
|
522
513
|
return state;
|
|
523
514
|
}
|
|
524
515
|
/** Private to pi-ext-ui: two process hooks, scoped by the actual host UI facade. */
|
|
525
|
-
export function installHostToolGroups(context) {
|
|
516
|
+
export function installHostToolGroups(context, background) {
|
|
526
517
|
const parts = VERSION.split(".").map(Number);
|
|
527
518
|
const [major = 0, minor = 0, patch = 0] = parts;
|
|
528
519
|
if (parts.length !== 3 ||
|
|
@@ -554,7 +545,7 @@ export function installHostToolGroups(context) {
|
|
|
554
545
|
ui,
|
|
555
546
|
hosts: new Map(),
|
|
556
547
|
failed: new WeakSet(),
|
|
557
|
-
attach: (host) => attach(host, run),
|
|
548
|
+
attach: (host) => attach(host, run, background),
|
|
558
549
|
};
|
|
559
550
|
state.owners.set(ui, owner);
|
|
560
551
|
// Reload rebuilds history before session_start; reconnect already discovered instances.
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { copyToClipboard as copyToSystemClipboard, getLanguageFromPath, highlightCode, } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
|
|
3
3
|
import { extractChildToolCopyText, inlinePresentation, nativeRequestCopy } from "./native-tools.js";
|
|
4
|
-
import { formatToolHeaderText, ToolView, toContentRows, transitionFoldLevel, } from "./tool-view.js";
|
|
4
|
+
import { formatToolHeaderText, TOOL_SPINNER_INTERVAL_MS, ToolView, toContentRows, toolSpinnerFrame, transitionFoldLevel, } from "./tool-view.js";
|
|
5
5
|
function truncate(text, width) {
|
|
6
6
|
return truncateToWidth(text, Math.max(1, width));
|
|
7
7
|
}
|
|
8
8
|
function isRecord(value) {
|
|
9
9
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* A call without a final result is still running: its row carries the live spinner frame. A persisted
|
|
13
|
+
* record states its own status, so a recovered board never animates a call that already ended.
|
|
14
|
+
*/
|
|
15
|
+
function isLiveCall(call) {
|
|
16
|
+
return "status" in call
|
|
17
|
+
? call.status === "running"
|
|
18
|
+
: call.result === undefined;
|
|
19
|
+
}
|
|
11
20
|
/**
|
|
12
21
|
* A copy keeps the request half byte-exact and strips only what follows it: the result is display
|
|
13
22
|
* text, while the request is what the model sent and may legitimately contain escapes.
|
|
@@ -31,6 +40,7 @@ export class NestedCallsView {
|
|
|
31
40
|
#copiedIndex = undefined;
|
|
32
41
|
#failedIndex = undefined;
|
|
33
42
|
#copiedTimer = undefined;
|
|
43
|
+
#liveTimer = undefined;
|
|
34
44
|
#lineMapping = [];
|
|
35
45
|
#renderCache;
|
|
36
46
|
constructor(props) {
|
|
@@ -191,14 +201,30 @@ export class NestedCallsView {
|
|
|
191
201
|
if (state !== undefined)
|
|
192
202
|
delete state[NESTED_CALLS_COPIED_KEY];
|
|
193
203
|
}
|
|
204
|
+
/** A live child row owns the next frame; a settled board asks for none. */
|
|
205
|
+
#requestNextFrame() {
|
|
206
|
+
if (this.#liveTimer !== undefined)
|
|
207
|
+
return;
|
|
208
|
+
this.#liveTimer = setTimeout(() => {
|
|
209
|
+
this.#liveTimer = undefined;
|
|
210
|
+
this.#props.invalidate?.();
|
|
211
|
+
}, TOOL_SPINNER_INTERVAL_MS);
|
|
212
|
+
this.#liveTimer.unref?.();
|
|
213
|
+
}
|
|
194
214
|
render(width) {
|
|
195
215
|
const safeWidth = Math.max(1, width);
|
|
196
216
|
const levelsKey = this.#getLevelsKey();
|
|
217
|
+
// A live child row draws the shared spinner, so a cached board would freeze that glyph.
|
|
218
|
+
const frame = this.#props.calls.some(isLiveCall) ? toolSpinnerFrame() : "";
|
|
219
|
+
// A live child row draws the glyph, so this board owns the next frame for as long as it does.
|
|
220
|
+
if (frame !== "")
|
|
221
|
+
this.#requestNextFrame();
|
|
197
222
|
if (this.#renderCache !== undefined &&
|
|
198
223
|
this.#renderCache.width === safeWidth &&
|
|
199
224
|
this.#renderCache.copiedIndex === this.#copiedIndex &&
|
|
200
225
|
this.#renderCache.failedIndex === this.#failedIndex &&
|
|
201
|
-
this.#renderCache.levelsKey === levelsKey
|
|
226
|
+
this.#renderCache.levelsKey === levelsKey &&
|
|
227
|
+
this.#renderCache.frame === frame) {
|
|
202
228
|
this.#lineMapping = this.#renderCache.lineMapping;
|
|
203
229
|
return this.#renderCache.lines;
|
|
204
230
|
}
|
|
@@ -212,8 +238,7 @@ export class NestedCallsView {
|
|
|
212
238
|
continue;
|
|
213
239
|
const toolName = "toolName" in call ? call.toolName : call.name;
|
|
214
240
|
const isError = Boolean(call.isError || ("status" in call && call.status === "error"));
|
|
215
|
-
const isRunning = (
|
|
216
|
-
(!("status" in call) && call.result === undefined);
|
|
241
|
+
const isRunning = isLiveCall(call);
|
|
217
242
|
const expansionLevel = this.#getExpansionLevel(i);
|
|
218
243
|
const isExpanded = expansionLevel !== 0;
|
|
219
244
|
const rawSummary = "summary" in call ? call.summary : undefined;
|
|
@@ -281,6 +306,7 @@ export class NestedCallsView {
|
|
|
281
306
|
copiedIndex: this.#copiedIndex,
|
|
282
307
|
failedIndex: this.#failedIndex,
|
|
283
308
|
levelsKey,
|
|
309
|
+
frame,
|
|
284
310
|
lines,
|
|
285
311
|
lineMapping: this.#lineMapping,
|
|
286
312
|
};
|
|
@@ -56,9 +56,7 @@ export interface ToolGroupClock {
|
|
|
56
56
|
export declare function memberParts(member: Member): readonly Component[];
|
|
57
57
|
export declare class TailUnit implements Component {
|
|
58
58
|
#private;
|
|
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;
|
|
59
|
+
constructor(member: Member, background: () => boolean);
|
|
62
60
|
render(width: number): string[];
|
|
63
61
|
handleMouse(event: Parameters<NonNullable<Component["handleMouse"]>>[0]): import("@earendil-works/pi-tui").TuiMouseEventResult | undefined;
|
|
64
62
|
invalidate(): void;
|
|
@@ -72,7 +70,7 @@ export declare class TailUnit implements Component {
|
|
|
72
70
|
*/
|
|
73
71
|
export declare class ToolGroupView extends Container {
|
|
74
72
|
#private;
|
|
75
|
-
constructor(ui: ExtensionUIContext, redraw: () => void);
|
|
73
|
+
constructor(ui: ExtensionUIContext, redraw: () => void, background: () => boolean);
|
|
76
74
|
/**
|
|
77
75
|
* Release the block's own lifetime. The projection drops a block when it is evicted, when
|
|
78
76
|
* grouping is disabled, or when the patch unloads; an idempotent dispose keeps that path honest,
|
package/dist/tool-group-view.js
CHANGED
|
@@ -1,58 +1,62 @@
|
|
|
1
|
-
import { Box, Container, MouseRegion, Spacer, Text, truncateToWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
|
|
1
|
+
import { Box, Container, MouseRegion, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
|
|
2
2
|
import { fitRow } from "@hheei/pi-ext-core";
|
|
3
3
|
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
|
-
}
|
|
11
7
|
/** Every original component of a member, in transcript order. */
|
|
12
8
|
export function memberParts(member) {
|
|
13
9
|
return member.parts ?? [member.component];
|
|
14
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* A row that draws nothing visible. A separator row is not always an empty string: Pi marks the start
|
|
13
|
+
* of a message zone with an OSC 133 row that paints no cells, and counting it as content would add a
|
|
14
|
+
* second blank row after the block.
|
|
15
|
+
*/
|
|
16
|
+
function isBlankRow(row) {
|
|
17
|
+
return row === undefined || visibleWidth(row) === 0;
|
|
18
|
+
}
|
|
15
19
|
export class TailUnit {
|
|
16
20
|
#parts;
|
|
21
|
+
#background;
|
|
17
22
|
#offsets = [];
|
|
18
|
-
|
|
19
|
-
#reserved = 0;
|
|
20
|
-
#live;
|
|
21
|
-
constructor(member) {
|
|
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) {
|
|
23
|
+
constructor(member, background) {
|
|
27
24
|
this.#parts = memberParts(member);
|
|
28
|
-
this.#
|
|
25
|
+
this.#background = background;
|
|
29
26
|
}
|
|
30
27
|
render(width) {
|
|
31
28
|
const offsets = [];
|
|
32
29
|
const rows = [];
|
|
30
|
+
let contentRows = 0;
|
|
33
31
|
for (const part of this.#parts) {
|
|
34
32
|
const all = part.render(width);
|
|
35
33
|
const skip = all[0] === "" ? 1 : 0;
|
|
36
34
|
const own = all.slice(skip);
|
|
37
35
|
if (own.length === 0)
|
|
38
36
|
continue;
|
|
39
|
-
if (rows.length > 0)
|
|
37
|
+
if (rows.length > 0) {
|
|
40
38
|
rows.push("");
|
|
39
|
+
contentRows++;
|
|
40
|
+
}
|
|
41
|
+
// A leading OSC-only separator stays in the output, but is not a content row.
|
|
42
|
+
contentRows += own.length - (isBlankRow(own[0]) ? 1 : 0);
|
|
41
43
|
offsets.push({ part, top: rows.length, height: own.length, skip });
|
|
42
44
|
rows.push(...own);
|
|
43
45
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
if (rows.length === 0) {
|
|
47
|
+
this.#offsets = offsets;
|
|
46
48
|
return rows;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// a row
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
}
|
|
50
|
+
const out = isBlankRow(rows[0]) ? rows : ["", ...rows];
|
|
51
|
+
// The separator above is a row of this unit too, so every part offset moves down with it.
|
|
52
|
+
const shift = out.length - rows.length;
|
|
53
|
+
this.#offsets = offsets.map((offset) => ({ ...offset, top: offset.top + shift }));
|
|
54
|
+
// Surface padding counts toward the minimum; added rows stay outside surfaces and hit areas.
|
|
55
|
+
const minimum = this.#background() ? 4 : 2;
|
|
56
|
+
if (contentRows > 0) {
|
|
57
|
+
for (let row = contentRows; row < minimum; row++)
|
|
58
|
+
out.push("");
|
|
59
|
+
}
|
|
56
60
|
return out;
|
|
57
61
|
}
|
|
58
62
|
handleMouse(event) {
|
|
@@ -138,6 +142,8 @@ export class ToolGroupView extends Container {
|
|
|
138
142
|
#header = new Text("", 0, 0);
|
|
139
143
|
#ui;
|
|
140
144
|
#redraw;
|
|
145
|
+
/** Pi-native block backgrounds are opt-in; while they are on, the fill replaces the rails. */
|
|
146
|
+
#background;
|
|
141
147
|
/** The stable clock for this block; restored history stays unknown until live content joins. */
|
|
142
148
|
#clock;
|
|
143
149
|
/** The block's own members, drawn inside the group. */
|
|
@@ -160,10 +166,11 @@ export class ToolGroupView extends Container {
|
|
|
160
166
|
// One extra colored row above and below: the block needs breathing room inside its background.
|
|
161
167
|
#box = new Box(0, 1);
|
|
162
168
|
#spacer = new Spacer(1);
|
|
163
|
-
constructor(ui, redraw) {
|
|
169
|
+
constructor(ui, redraw, background) {
|
|
164
170
|
super();
|
|
165
171
|
this.#ui = ui;
|
|
166
172
|
this.#redraw = redraw;
|
|
173
|
+
this.#background = background;
|
|
167
174
|
this.#heading = new MouseRegion(this.#header, (event) => {
|
|
168
175
|
if (event.type !== "click" || event.button !== "left")
|
|
169
176
|
return undefined;
|
|
@@ -492,6 +499,13 @@ export class ToolGroupView extends Container {
|
|
|
492
499
|
push(row, index === 0 ? { kind: "item", member: member.component } : { kind: "none" });
|
|
493
500
|
}
|
|
494
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* The cells a thinking row starts with. Thinking has no surface of its own, so it keeps the same
|
|
504
|
+
* rail as a tool at every level; once a block is filled, the fill delimits it and the rail goes.
|
|
505
|
+
*/
|
|
506
|
+
#thinkingPrefix() {
|
|
507
|
+
return this.#background() ? " " : `${this.#ui.theme.fg("dim", "▎")} `;
|
|
508
|
+
}
|
|
495
509
|
/** One preview row: the grapheme head plus an ellipsis, then cut to the cells the rail leaves. */
|
|
496
510
|
#thinkingPreviewRow(text, width) {
|
|
497
511
|
const theme = this.#ui.theme;
|
|
@@ -499,15 +513,16 @@ export class ToolGroupView extends Container {
|
|
|
499
513
|
const room = Math.max(1, width - 2);
|
|
500
514
|
const fitted = truncateToWidth(head, more ? Math.max(1, room - 1) : room, "…");
|
|
501
515
|
const preview = more && fitted === head ? `${head}…` : fitted;
|
|
502
|
-
return `${
|
|
516
|
+
return `${this.#thinkingPrefix()}${theme.fg("thinkingText", theme.italic(preview))}`;
|
|
503
517
|
}
|
|
504
518
|
/** Full thinking text: paragraphs keep their separation, every row wraps to the indent's width. */
|
|
505
519
|
#thinkingRows(text, width) {
|
|
506
520
|
const theme = this.#ui.theme;
|
|
521
|
+
const prefix = this.#thinkingPrefix();
|
|
507
522
|
const inner = Math.max(1, width - 2);
|
|
508
523
|
return toContentRows(text).flatMap((line) => {
|
|
509
524
|
const wrapped = wrapTextWithAnsi(line, inner);
|
|
510
|
-
return (wrapped.length > 0 ? wrapped : [""]).map((row) => row === "" ? "" :
|
|
525
|
+
return (wrapped.length > 0 ? wrapped : [""]).map((row) => row === "" ? "" : `${prefix}${theme.fg("thinkingText", theme.italic(row))}`);
|
|
511
526
|
});
|
|
512
527
|
}
|
|
513
528
|
/**
|
package/dist/tool-view.js
CHANGED
|
@@ -245,6 +245,7 @@ export class ToolView {
|
|
|
245
245
|
#localCopiedUntil = 0;
|
|
246
246
|
#localCopyFailed = false;
|
|
247
247
|
#localTimer;
|
|
248
|
+
#liveTimer;
|
|
248
249
|
#lastWidth = 80;
|
|
249
250
|
#lastHeight = 0;
|
|
250
251
|
#lastNoRail = false;
|
|
@@ -386,6 +387,20 @@ export class ToolView {
|
|
|
386
387
|
this.#spec.stateHolder[COPY_FEEDBACK_TIMER_KEY] = timer;
|
|
387
388
|
}
|
|
388
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* A live glyph needs someone to ask for the next frame, and no host event says "still running".
|
|
392
|
+
* The frame that drew one owns that timer: it fires once, the repaint renders again, and a settled
|
|
393
|
+
* row simply stops asking.
|
|
394
|
+
*/
|
|
395
|
+
#requestNextFrame() {
|
|
396
|
+
if (this.#liveTimer !== undefined)
|
|
397
|
+
return;
|
|
398
|
+
this.#liveTimer = setTimeout(() => {
|
|
399
|
+
this.#liveTimer = undefined;
|
|
400
|
+
this.#spec.invalidate?.();
|
|
401
|
+
}, TOOL_SPINNER_INTERVAL_MS);
|
|
402
|
+
this.#liveTimer.unref?.();
|
|
403
|
+
}
|
|
389
404
|
#renderHeader(width, level) {
|
|
390
405
|
const { theme, header, copy } = this.#spec;
|
|
391
406
|
const safeWidth = Math.max(1, width);
|
|
@@ -514,7 +529,15 @@ export class ToolView {
|
|
|
514
529
|
// The glyph of a live tool changes on its own clock, so a cached frame would freeze it.
|
|
515
530
|
const status = this.#spec.header.status;
|
|
516
531
|
const glyph = status === "streaming" || status === "running" ? toolSpinnerFrame() : "";
|
|
517
|
-
|
|
532
|
+
// A live row at level 0 draws the glyph, so this frame owns the next one.
|
|
533
|
+
if (glyph !== "" && level === 0)
|
|
534
|
+
this.#requestNextFrame();
|
|
535
|
+
// A component body draws on a clock of its own: a container's child board animates a live child
|
|
536
|
+
// while the container itself already shows its fold pin, so a cached frame would freeze the
|
|
537
|
+
// child's glyph. Only a body of literal rows is fully known here.
|
|
538
|
+
const bodyOwnsItsClock = this.#spec.collapsedRows !== undefined && !Array.isArray(this.#spec.collapsedRows);
|
|
539
|
+
if (!bodyOwnsItsClock &&
|
|
540
|
+
this.#cache !== undefined &&
|
|
518
541
|
this.#cache.width === width &&
|
|
519
542
|
this.#cache.level === level &&
|
|
520
543
|
this.#cache.noRail === noRail &&
|
|
@@ -547,7 +570,9 @@ export class ToolView {
|
|
|
547
570
|
this.#lastTopPad = surface === undefined ? 0 : 1;
|
|
548
571
|
const lines = surface === undefined ? safeLines : surfaceBlock(this.#spec.theme, surface, safeLines, width);
|
|
549
572
|
this.#lastHeight = lines.length;
|
|
550
|
-
this.#cache =
|
|
573
|
+
this.#cache = bodyOwnsItsClock
|
|
574
|
+
? undefined
|
|
575
|
+
: { width, level, copied, noRail, surface, glyph, lines };
|
|
551
576
|
return lines;
|
|
552
577
|
}
|
|
553
578
|
handleMouse(event) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hheei/pi-ext-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Collapsible tool rendering frame and tool renderer pilot for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,29 +22,28 @@
|
|
|
22
22
|
"pi",
|
|
23
23
|
"pi-package",
|
|
24
24
|
"pi-extension",
|
|
25
|
-
"hepi",
|
|
26
25
|
"ui",
|
|
27
26
|
"collapsible"
|
|
28
27
|
],
|
|
29
28
|
"license": "MIT",
|
|
30
29
|
"repository": {
|
|
31
30
|
"type": "git",
|
|
32
|
-
"url": "git+https://github.com/hheei/
|
|
31
|
+
"url": "git+https://github.com/hheei/pi-mono.git",
|
|
33
32
|
"directory": "packages/pi-ext-ui"
|
|
34
33
|
},
|
|
35
34
|
"publishConfig": {
|
|
36
35
|
"access": "public"
|
|
37
36
|
},
|
|
38
37
|
"dependencies": {
|
|
39
|
-
"@hheei/pi-ext-core": "^0.
|
|
38
|
+
"@hheei/pi-ext-core": "^0.8.0"
|
|
40
39
|
},
|
|
41
40
|
"peerDependencies": {
|
|
42
41
|
"@earendil-works/pi-coding-agent": ">=1.0.1",
|
|
43
42
|
"@earendil-works/pi-tui": ">=1.0.1"
|
|
44
43
|
},
|
|
45
44
|
"devDependencies": {
|
|
46
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
47
|
-
"@earendil-works/pi-tui": "1.
|
|
45
|
+
"@earendil-works/pi-coding-agent": "^1.4.0",
|
|
46
|
+
"@earendil-works/pi-tui": "^1.4.0"
|
|
48
47
|
},
|
|
49
48
|
"scripts": {
|
|
50
49
|
"build": "node -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json"
|