@hheei/pi-ext-ui 0.8.3 → 0.9.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.
|
@@ -287,7 +287,8 @@ function attach(host, run, background, outputPad) {
|
|
|
287
287
|
weight += 0.5;
|
|
288
288
|
}
|
|
289
289
|
if (first !== undefined && weight >= 2) {
|
|
290
|
-
const group = groups.get(first) ??
|
|
290
|
+
const group = groups.get(first) ??
|
|
291
|
+
new ToolGroupView(host.ui, host.requestRender, background, outputPad);
|
|
291
292
|
const boundaryTime = ended && boundary !== undefined
|
|
292
293
|
? (run.observed.get(boundary)?.at ?? performance.now())
|
|
293
294
|
: undefined;
|
|
@@ -70,7 +70,7 @@ export declare class TailUnit implements Component {
|
|
|
70
70
|
*/
|
|
71
71
|
export declare class ToolGroupView extends Container {
|
|
72
72
|
#private;
|
|
73
|
-
constructor(ui: ExtensionUIContext, redraw: () => void, background: () => boolean);
|
|
73
|
+
constructor(ui: ExtensionUIContext, redraw: () => void, background: () => boolean, outputPad: () => number);
|
|
74
74
|
/**
|
|
75
75
|
* Release the block's own lifetime. The projection drops a block when it is evicted, when
|
|
76
76
|
* grouping is disabled, or when the patch unloads; an idempotent dispose keeps that path honest,
|
package/dist/tool-group-view.js
CHANGED
|
@@ -144,6 +144,12 @@ function thinkingPreview(text) {
|
|
|
144
144
|
}
|
|
145
145
|
return { head, more: content !== head };
|
|
146
146
|
}
|
|
147
|
+
function isLiveMember(member) {
|
|
148
|
+
return member.status === "running" || member.status === "streaming";
|
|
149
|
+
}
|
|
150
|
+
function isFailedMember(member) {
|
|
151
|
+
return member.status === "error";
|
|
152
|
+
}
|
|
147
153
|
/**
|
|
148
154
|
* The collapsed frame of one activity block.
|
|
149
155
|
*
|
|
@@ -157,6 +163,8 @@ export class ToolGroupView extends Container {
|
|
|
157
163
|
#redraw;
|
|
158
164
|
/** Pi-native block backgrounds are opt-in; while they are on, the fill replaces the rails. */
|
|
159
165
|
#background;
|
|
166
|
+
/** Pi's own output padding, so the frame lines up with the tool rows it wraps. */
|
|
167
|
+
#outputPad;
|
|
160
168
|
/** The stable clock for this block; restored history stays unknown until live content joins. */
|
|
161
169
|
#clock;
|
|
162
170
|
/** The block's own members, drawn inside the group. */
|
|
@@ -169,21 +177,27 @@ export class ToolGroupView extends Container {
|
|
|
169
177
|
#category;
|
|
170
178
|
#item;
|
|
171
179
|
#renderedExpanded = false;
|
|
172
|
-
#shell
|
|
180
|
+
#shell;
|
|
173
181
|
/** When the block last became quiet; the surface hold counts from here. */
|
|
174
182
|
#settledAt;
|
|
175
183
|
#holdTimer;
|
|
176
184
|
#hits = [];
|
|
177
185
|
#heading;
|
|
178
186
|
#body;
|
|
179
|
-
// One extra colored row above and below: the block needs breathing
|
|
180
|
-
|
|
187
|
+
// One extra colored row above and below while the surface is filled: the block needs breathing
|
|
188
|
+
// room inside its background, and a transparent block has nothing to breathe in. `Box` has no
|
|
189
|
+
// vertical setter, so the box is rebuilt when the fill flips.
|
|
190
|
+
#box = new Box(0, 0);
|
|
191
|
+
#filled;
|
|
181
192
|
#spacer = new Spacer(1);
|
|
182
|
-
|
|
193
|
+
/** The blank row between the expanded title and the first member. */
|
|
194
|
+
#gap = new Spacer(1);
|
|
195
|
+
constructor(ui, redraw, background, outputPad) {
|
|
183
196
|
super();
|
|
184
197
|
this.#ui = ui;
|
|
185
198
|
this.#redraw = redraw;
|
|
186
199
|
this.#background = background;
|
|
200
|
+
this.#outputPad = outputPad;
|
|
187
201
|
this.#heading = new MouseRegion(this.#header, (event) => {
|
|
188
202
|
if (event.type !== "click" || event.button !== "left")
|
|
189
203
|
return undefined;
|
|
@@ -195,11 +209,7 @@ export class ToolGroupView extends Container {
|
|
|
195
209
|
handleMouse: (event) => this.#hit(event),
|
|
196
210
|
invalidate() { },
|
|
197
211
|
};
|
|
198
|
-
|
|
199
|
-
// at the width they were laid out for; a tool's own outputPad must not be counted twice.
|
|
200
|
-
this.#box.addChild(this.#heading);
|
|
201
|
-
this.#box.addChild(this.#body);
|
|
202
|
-
this.children = [this.#spacer, this.#box];
|
|
212
|
+
this.#syncShell();
|
|
203
213
|
}
|
|
204
214
|
#toggleRaw() {
|
|
205
215
|
this.#raw = this.#ui.getToolsExpanded() ? false : !this.#raw;
|
|
@@ -239,35 +249,23 @@ export class ToolGroupView extends Container {
|
|
|
239
249
|
return this.#tail === undefined ? [...this.#members] : [...this.#members, this.#tail];
|
|
240
250
|
}
|
|
241
251
|
/** The block's own last fact: the newest member that states a status, tail included. */
|
|
242
|
-
#lastStatus() {
|
|
243
|
-
const block = this.#block();
|
|
244
|
-
for (let index = block.length - 1; index >= 0; index -= 1) {
|
|
245
|
-
const status = block[index]?.status;
|
|
246
|
-
if (status !== undefined)
|
|
247
|
-
return status;
|
|
248
|
-
}
|
|
249
|
-
return undefined;
|
|
250
|
-
}
|
|
251
252
|
/**
|
|
252
|
-
* The surface states the block
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* of colored panels.
|
|
253
|
+
* The surface states the block, decided once: pending until a call settles, then the settled color
|
|
254
|
+
* for five seconds, then the neutral surface. Reading the last call's status instead re-tinted the
|
|
255
|
+
* block on every call, which read as flashing while one request ran several tools.
|
|
256
256
|
*/
|
|
257
257
|
#surfaceToken() {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
258
|
+
if (this.#settledAt === undefined) {
|
|
259
|
+
const stated = this.#block().filter((member) => member.status !== undefined);
|
|
260
|
+
if (stated.length === 0 || stated.some(isLiveMember))
|
|
261
|
+
return "toolPendingBg";
|
|
262
|
+
this.#settledAt = performance.now();
|
|
263
263
|
}
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
const held = now - this.#settledAt;
|
|
267
|
-
if (last === undefined || held >= SURFACE_HOLD_MS)
|
|
264
|
+
const held = performance.now() - this.#settledAt;
|
|
265
|
+
if (held >= SURFACE_HOLD_MS)
|
|
268
266
|
return "toolPendingBg";
|
|
269
267
|
this.#hold(SURFACE_HOLD_MS - held);
|
|
270
|
-
return
|
|
268
|
+
return this.#block().some(isFailedMember) ? "toolErrorBg" : "toolSuccessBg";
|
|
271
269
|
}
|
|
272
270
|
/** One repaint when the hold expires; nothing keeps ticking after that. */
|
|
273
271
|
#hold(delay) {
|
|
@@ -387,8 +385,13 @@ export class ToolGroupView extends Container {
|
|
|
387
385
|
const theme = this.#ui.theme;
|
|
388
386
|
const lines = [];
|
|
389
387
|
const hits = [];
|
|
388
|
+
// The frame is the block's own row; a tool row inside it pads itself, so only these rows take
|
|
389
|
+
// Pi's output padding.
|
|
390
|
+
const pad = Math.max(0, this.#outputPad());
|
|
391
|
+
const lead = " ".repeat(pad);
|
|
392
|
+
const inner = Math.max(0, width - pad * 2);
|
|
390
393
|
const push = (row, action = { kind: "none" }) => {
|
|
391
|
-
lines.push(fitRow(row
|
|
394
|
+
lines.push(fitRow(`${lead}${row}`, Math.max(0, width), "…"));
|
|
392
395
|
hits.push({ height: 1, action });
|
|
393
396
|
};
|
|
394
397
|
/** Rows that belong to one component keep one hit so its own geometry stays correct. */
|
|
@@ -404,7 +407,7 @@ export class ToolGroupView extends Container {
|
|
|
404
407
|
if (member.kind === "tool")
|
|
405
408
|
tools.push(index);
|
|
406
409
|
}
|
|
407
|
-
push(theme.fg("borderMuted", "─".repeat(
|
|
410
|
+
push(theme.fg("borderMuted", "─".repeat(inner)));
|
|
408
411
|
const notices = this.#noticeItems();
|
|
409
412
|
if (notices.length > 0) {
|
|
410
413
|
const kinds = new Map();
|
|
@@ -418,7 +421,7 @@ export class ToolGroupView extends Container {
|
|
|
418
421
|
category: "notices",
|
|
419
422
|
});
|
|
420
423
|
if (this.#category === "notices")
|
|
421
|
-
this.#renderNotices(notices,
|
|
424
|
+
this.#renderNotices(notices, inner, push);
|
|
422
425
|
}
|
|
423
426
|
const thinking = this.#thinkingItems();
|
|
424
427
|
if (thinking.length > 0) {
|
|
@@ -428,7 +431,7 @@ export class ToolGroupView extends Container {
|
|
|
428
431
|
category: "thinking",
|
|
429
432
|
});
|
|
430
433
|
if (this.#category === "thinking")
|
|
431
|
-
this.#renderThinking(thinking,
|
|
434
|
+
this.#renderThinking(thinking, inner, push);
|
|
432
435
|
}
|
|
433
436
|
// Names and header calls share one eligibility: only completed calls are counted. A tool that
|
|
434
437
|
// is still streaming or running stays listed and expandable, but adds no number.
|
|
@@ -461,7 +464,7 @@ export class ToolGroupView extends Container {
|
|
|
461
464
|
this.#renderTools(tools, width, push, pushBlock);
|
|
462
465
|
}
|
|
463
466
|
// The rails bracket the header, categories, and any opened detail; the footer sits below them.
|
|
464
|
-
push(theme.fg("borderMuted", "─".repeat(
|
|
467
|
+
push(theme.fg("borderMuted", "─".repeat(inner)));
|
|
465
468
|
// The footer is always the block's request and token totals: one row, never a tool status.
|
|
466
469
|
push(this.#footerRow());
|
|
467
470
|
this.#hits = hits;
|
|
@@ -687,8 +690,16 @@ export class ToolGroupView extends Container {
|
|
|
687
690
|
}
|
|
688
691
|
this.#renderedExpanded = this.#raw || globalExpanded;
|
|
689
692
|
const theme = this.#ui.theme;
|
|
693
|
+
// The fill and its breathing rows belong to one surface: a transparent block keeps neither, so
|
|
694
|
+
// turning tool backgrounds off really removes them instead of leaving blank rows behind.
|
|
695
|
+
const filled = this.#background();
|
|
696
|
+
if (this.#filled !== filled) {
|
|
697
|
+
this.#filled = filled;
|
|
698
|
+
this.#box = new Box(0, filled ? 1 : 0);
|
|
699
|
+
this.#shell = undefined;
|
|
700
|
+
}
|
|
690
701
|
this.#syncShell();
|
|
691
|
-
this.#box.setBgFn(backgroundFill(theme, this.#surfaceToken()));
|
|
702
|
+
this.#box.setBgFn(filled ? backgroundFill(theme, this.#surfaceToken()) : undefined);
|
|
692
703
|
const calls = this.#calls();
|
|
693
704
|
const elapsed = this.#clock === undefined
|
|
694
705
|
? "—"
|
|
@@ -697,8 +708,12 @@ export class ToolGroupView extends Container {
|
|
|
697
708
|
theme.fg("text", "collapse views"),
|
|
698
709
|
theme.fg("dim", elapsed),
|
|
699
710
|
theme.fg("dim", `${calls} ${calls === 1 ? "call" : "calls"}`),
|
|
711
|
+
// The expanded view has no frame to hold a footer, so its totals join the title instead of
|
|
712
|
+
// adding a row after the last member.
|
|
713
|
+
...(this.#renderedExpanded ? [this.#footerRow()] : []),
|
|
700
714
|
].join(theme.fg("dim", " · "));
|
|
701
|
-
|
|
715
|
+
const pad = Math.max(0, this.#outputPad());
|
|
716
|
+
this.#header.setText(fitRow(`${" ".repeat(pad)}${theme.fg("dim", this.#renderedExpanded ? "▾" : "▸")} ${details}`, Math.max(0, width - pad * 2), "…"));
|
|
702
717
|
return super.render(width);
|
|
703
718
|
}
|
|
704
719
|
/**
|
|
@@ -710,7 +725,16 @@ export class ToolGroupView extends Container {
|
|
|
710
725
|
if (this.#shell === shell)
|
|
711
726
|
return;
|
|
712
727
|
this.#shell = shell;
|
|
728
|
+
// The title owns the surface in both shells. A collapsed frame keeps its body inside it; the
|
|
729
|
+
// expanded one leaves every member its own styling and separates the title from them by one
|
|
730
|
+
// blank row, so the block never tints the whole conversation.
|
|
731
|
+
this.#box.clear();
|
|
732
|
+
this.#box.addChild(this.#heading);
|
|
733
|
+
if (shell === "collapsed")
|
|
734
|
+
this.#box.addChild(this.#body);
|
|
713
735
|
this.children =
|
|
714
|
-
shell === "raw"
|
|
736
|
+
shell === "raw"
|
|
737
|
+
? [this.#spacer, this.#box, this.#gap, this.#body]
|
|
738
|
+
: [this.#spacer, this.#box];
|
|
715
739
|
}
|
|
716
740
|
}
|