pi-better-harness 0.40.4 → 0.40.5

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.
@@ -278,6 +278,19 @@ function sdkEntry(require: ReturnType<typeof createRequire>): string {
278
278
  }
279
279
 
280
280
  /** Internal TUI adapter: no tools are replaced and no result content is changed. */
281
+ const FRAME_MS = 80;
282
+ const IDLE_FRAME_MS = 1000;
283
+ const STALE_RENDER_MS = 400;
284
+
285
+ /**
286
+ * Shimmer cadence for the next frame. Fast while any animated row rendered recently, one frame
287
+ * a second once none has, and never off: an unfocused window can stop repainting without the row
288
+ * being gone, and stopping the timer would latch the animation until an unrelated render restarted it.
289
+ */
290
+ export function shimmerFrameDelay(lastRenderMs: number, nowMs: number): number {
291
+ return nowMs - lastRenderMs <= STALE_RENDER_MS ? FRAME_MS : IDLE_FRAME_MS;
292
+ }
293
+
281
294
  export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: () => Theme, compactionPrototype?: CompactionPrototype, customMessagePrototype?: CustomMessagePrototype): MinimalOutputHook {
282
295
  for (const name of ["updateDisplay", "getResultRenderer", "getTextOutput", "setExpanded", "invalidate", "render"] as const) {
283
296
  if (typeof prototype[name] !== "function") {
@@ -319,10 +332,16 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
319
332
  const animationReferences = new WeakMap<ToolComponent, WeakRef<ToolComponent>>();
320
333
  const animated = new Map<WeakRef<ToolComponent>, number>();
321
334
  const settled = new WeakSet<ToolComponent>();
322
- let animationTimer: ReturnType<typeof setInterval> | undefined;
335
+ // Frame while the transcript paints, then one frame a second once it stops. An unfocused
336
+ // window may stop repainting, and clearing the timer outright would latch the animation off
337
+ // until an unrelated render happened to restart it. Slowing keeps the saving without the latch.
338
+ let animationTimer: ReturnType<typeof setTimeout> | undefined;
339
+ let animationIdle = false;
340
+ let paintingFrame = false;
323
341
  const stopAnimation = () => {
324
- if (animationTimer !== undefined) clearInterval(animationTimer);
342
+ if (animationTimer !== undefined) clearTimeout(animationTimer);
325
343
  animationTimer = undefined;
344
+ animationIdle = false;
326
345
  animated.clear();
327
346
  };
328
347
  const canAnimate = (component: ToolComponent) => {
@@ -333,30 +352,57 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
333
352
  && !settled.has(component);
334
353
  };
335
354
  const pruneAnimation = () => {
336
- for (const [reference, lastRender] of animated) {
355
+ for (const reference of animated.keys()) {
337
356
  const component = reference.deref();
338
- if (!component || !canAnimate(component) || Date.now() - lastRender > 400
357
+ if (!component || !canAnimate(component)
339
358
  || toolVisibility(component.ui, component) !== true) animated.delete(reference);
340
359
  }
341
360
  if (!animated.size) stopAnimation();
342
361
  };
362
+ const scheduleFrame = (delayMs: number) => {
363
+ if (animationTimer !== undefined) clearTimeout(animationTimer);
364
+ animationTimer = setTimeout(paintFrame, Math.max(0, delayMs));
365
+ animationTimer.unref?.();
366
+ };
367
+ const paintFrame = () => {
368
+ animationTimer = undefined;
369
+ pruneAnimation();
370
+ if (!animated.size) return;
371
+ const uis = new Set<ToolUI>();
372
+ for (const reference of animated.keys()) {
373
+ const component = reference.deref();
374
+ if (!component) continue;
375
+ uis.add(component.ui);
376
+ }
377
+ paintingFrame = true;
378
+ try {
379
+ for (const ui of uis) ui.requestRender();
380
+ } finally {
381
+ paintingFrame = false;
382
+ }
383
+ pruneAnimation();
384
+ if (!animated.size) return;
385
+ // Choose the cadence from the timestamps this frame produced. Reading them before the
386
+ // renders instead would keep seeing the previous idle gap and re-arm the idle period
387
+ // forever, so painting that resumed on a timer-driven frame could never speed back up.
388
+ let newestRender = 0;
389
+ for (const lastRender of animated.values()) {
390
+ if (lastRender > newestRender) newestRender = lastRender;
391
+ }
392
+ const delay = shimmerFrameDelay(newestRender, Date.now());
393
+ animationIdle = delay === IDLE_FRAME_MS;
394
+ scheduleFrame(delay);
395
+ };
343
396
  const animationTime = (component: ToolComponent, width: number, indent: number) => {
344
397
  if (width <= indent + 3 || !canAnimate(component) || toolVisibility(component.ui, component) === false) return undefined;
345
398
  let reference = animationReferences.get(component);
346
399
  if (!reference) { reference = new WeakRef(component); animationReferences.set(component, reference); }
347
400
  animated.set(reference, Date.now());
348
- if (animationTimer === undefined) {
349
- animationTimer = setInterval(() => {
350
- pruneAnimation();
351
- const uis = new Set<ToolUI>();
352
- for (const reference of animated.keys()) {
353
- const component = reference.deref();
354
- if (!component) continue;
355
- uis.add(component.ui);
356
- }
357
- for (const ui of uis) ui.requestRender();
358
- }, 80);
359
- animationTimer.unref?.();
401
+ // A render outside the clock means painting resumed. Re-arm at the frame cadence rather
402
+ // than waiting out a pending idle period, which would otherwise cost up to a second.
403
+ if (!paintingFrame && (animationTimer === undefined || animationIdle)) {
404
+ animationIdle = false;
405
+ scheduleFrame(FRAME_MS);
360
406
  }
361
407
  return toolVisibility(component.ui, component) === true ? Date.now() : undefined;
362
408
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-harness",
3
- "version": "0.40.4",
3
+ "version": "0.40.5",
4
4
  "description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, model routing, and Outcome & Todo.",
5
5
  "license": "MIT",
6
6
  "type": "module",