@hheei/pi-ext-ui 0.8.4 → 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.
@@ -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
  *
@@ -171,16 +177,21 @@ export class ToolGroupView extends Container {
171
177
  #category;
172
178
  #item;
173
179
  #renderedExpanded = false;
174
- #shell = "collapsed";
180
+ #shell;
175
181
  /** When the block last became quiet; the surface hold counts from here. */
176
182
  #settledAt;
177
183
  #holdTimer;
178
184
  #hits = [];
179
185
  #heading;
180
186
  #body;
181
- // One extra colored row above and below: the block needs breathing room inside its background.
182
- #box = new Box(0, 1);
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;
183
192
  #spacer = new Spacer(1);
193
+ /** The blank row between the expanded title and the first member. */
194
+ #gap = new Spacer(1);
184
195
  constructor(ui, redraw, background, outputPad) {
185
196
  super();
186
197
  this.#ui = ui;
@@ -198,11 +209,7 @@ export class ToolGroupView extends Container {
198
209
  handleMouse: (event) => this.#hit(event),
199
210
  invalidate() { },
200
211
  };
201
- // The group background covers the header, categories, and footer. Zero padding keeps the rows
202
- // at the width they were laid out for; a tool's own outputPad must not be counted twice.
203
- this.#box.addChild(this.#heading);
204
- this.#box.addChild(this.#body);
205
- this.children = [this.#spacer, this.#box];
212
+ this.#syncShell();
206
213
  }
207
214
  #toggleRaw() {
208
215
  this.#raw = this.#ui.getToolsExpanded() ? false : !this.#raw;
@@ -242,35 +249,23 @@ export class ToolGroupView extends Container {
242
249
  return this.#tail === undefined ? [...this.#members] : [...this.#members, this.#tail];
243
250
  }
244
251
  /** The block's own last fact: the newest member that states a status, tail included. */
245
- #lastStatus() {
246
- const block = this.#block();
247
- for (let index = block.length - 1; index >= 0; index -= 1) {
248
- const status = block[index]?.status;
249
- if (status !== undefined)
250
- return status;
251
- }
252
- return undefined;
253
- }
254
252
  /**
255
- * The surface states the block's last fact, not its worst one, so one old failure cannot tint a
256
- * block forever. A settled block keeps that color for five seconds and then falls back to the
257
- * neutral surface, because a transcript full of settled blocks is a transcript to read, not a wall
258
- * 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.
259
256
  */
260
257
  #surfaceToken() {
261
- const last = this.#lastStatus();
262
- if (last === "running" || last === "streaming") {
263
- this.#settledAt = undefined;
264
- this.#stopHold();
265
- return "toolPendingBg";
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();
266
263
  }
267
- const now = performance.now();
268
- this.#settledAt ??= now;
269
- const held = now - this.#settledAt;
270
- if (last === undefined || held >= SURFACE_HOLD_MS)
264
+ const held = performance.now() - this.#settledAt;
265
+ if (held >= SURFACE_HOLD_MS)
271
266
  return "toolPendingBg";
272
267
  this.#hold(SURFACE_HOLD_MS - held);
273
- return last === "error" ? "toolErrorBg" : "toolSuccessBg";
268
+ return this.#block().some(isFailedMember) ? "toolErrorBg" : "toolSuccessBg";
274
269
  }
275
270
  /** One repaint when the hold expires; nothing keeps ticking after that. */
276
271
  #hold(delay) {
@@ -695,8 +690,16 @@ export class ToolGroupView extends Container {
695
690
  }
696
691
  this.#renderedExpanded = this.#raw || globalExpanded;
697
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
+ }
698
701
  this.#syncShell();
699
- this.#box.setBgFn(backgroundFill(theme, this.#surfaceToken()));
702
+ this.#box.setBgFn(filled ? backgroundFill(theme, this.#surfaceToken()) : undefined);
700
703
  const calls = this.#calls();
701
704
  const elapsed = this.#clock === undefined
702
705
  ? "—"
@@ -705,6 +708,9 @@ export class ToolGroupView extends Container {
705
708
  theme.fg("text", "collapse views"),
706
709
  theme.fg("dim", elapsed),
707
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()] : []),
708
714
  ].join(theme.fg("dim", " · "));
709
715
  const pad = Math.max(0, this.#outputPad());
710
716
  this.#header.setText(fitRow(`${" ".repeat(pad)}${theme.fg("dim", this.#renderedExpanded ? "▾" : "▸")} ${details}`, Math.max(0, width - pad * 2), "…"));
@@ -719,7 +725,16 @@ export class ToolGroupView extends Container {
719
725
  if (this.#shell === shell)
720
726
  return;
721
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);
722
735
  this.children =
723
- shell === "raw" ? [this.#spacer, this.#heading, this.#body] : [this.#spacer, this.#box];
736
+ shell === "raw"
737
+ ? [this.#spacer, this.#box, this.#gap, this.#body]
738
+ : [this.#spacer, this.#box];
724
739
  }
725
740
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hheei/pi-ext-ui",
3
- "version": "0.8.4",
3
+ "version": "0.9.0",
4
4
  "description": "Collapsible tool rendering frame and tool renderer pilot for Pi.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",