@oh-my-pi/pi-tui 18.4.0 → 18.4.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/types/chat/image-loading.d.ts +24 -0
  3. package/dist/types/chat/transcript-entry.d.ts +9 -0
  4. package/dist/types/chrome/transcript-container.d.ts +9 -2
  5. package/dist/types/overlays/agents-hub.d.ts +7 -4
  6. package/dist/types/overlays/rewind-selector.d.ts +3 -3
  7. package/dist/types/overlays/usage-dashboard.d.ts +9 -2
  8. package/dist/types/prompt/composer-cache.d.ts +34 -16
  9. package/dist/types/prompt/composer.d.ts +15 -18
  10. package/dist/types/prompt/welcome.d.ts +5 -2
  11. package/dist/types/status-line/component.d.ts +13 -4
  12. package/dist/types/status-line/metrics.d.ts +0 -1
  13. package/dist/types/status-line/startup.d.ts +38 -0
  14. package/dist/types/status-line/types.d.ts +0 -2
  15. package/dist/types/terminal-capabilities.d.ts +10 -6
  16. package/dist/types/tools/output-meta.d.ts +2 -1
  17. package/dist/types/tools/vibe.d.ts +4 -0
  18. package/package.json +9 -9
  19. package/src/chat/assistant-message.ts +27 -21
  20. package/src/chat/image-loading.ts +81 -0
  21. package/src/chat/tool-execution.ts +29 -13
  22. package/src/chat/transcript-entry.ts +16 -0
  23. package/src/chrome/transcript-container.ts +208 -74
  24. package/src/components/image.ts +10 -2
  25. package/src/overlays/agents-hub.ts +7 -18
  26. package/src/overlays/copy-selector.ts +3 -31
  27. package/src/overlays/model-hub.ts +49 -16
  28. package/src/overlays/rewind-selector.ts +51 -12
  29. package/src/overlays/usage-dashboard.ts +47 -6
  30. package/src/prompt/composer-cache.ts +217 -196
  31. package/src/prompt/composer.ts +40 -70
  32. package/src/prompt/welcome.ts +31 -28
  33. package/src/status-line/component.ts +73 -50
  34. package/src/status-line/metrics.ts +3 -21
  35. package/src/status-line/segments.ts +34 -50
  36. package/src/status-line/startup.ts +181 -0
  37. package/src/status-line/types.ts +0 -2
  38. package/src/terminal-capabilities.ts +12 -6
  39. package/src/terminal.ts +38 -9
  40. package/src/tools/output-meta.ts +11 -3
  41. package/src/tools/vibe.ts +15 -2
  42. package/src/tui.ts +66 -42
package/src/tools/vibe.ts CHANGED
@@ -23,6 +23,8 @@ export interface VibeToolDetails {
23
23
  op: VibeOp;
24
24
  /** Live TV-wall snapshot of the owner's worker sessions at (or during) the call. */
25
25
  screens: VibeScreenSnapshot[];
26
+ /** Sessions the director killed, left off `screens`; shown as a count. */
27
+ hiddenKilled?: string[];
26
28
  spawned?: { id: string; cli: VibeCli; jobId: string };
27
29
  send?: VibeSendOutcome;
28
30
  wait?: {
@@ -46,6 +48,8 @@ export interface VibeScreenSnapshot {
46
48
  id: string;
47
49
  cli: VibeCli;
48
50
  state: VibeSessionState;
51
+ /** Terminated by the director (`vibe_kill`) or mode teardown; unset for workers that died on their own. */
52
+ killed?: boolean;
49
53
  model?: string;
50
54
  turns: number;
51
55
  queued: number;
@@ -373,13 +377,21 @@ export function createVibeToolRenderer(op: VibeOp) {
373
377
  return new Text(header, 0, 0);
374
378
  }
375
379
 
376
- // wait/list: the TV wall.
380
+ // wait/list: the TV wall. Director-killed sessions arrive pre-filtered
381
+ // (see `hiddenKilled`) so a long-running director's wall stays legible.
377
382
  const screens = details.screens;
383
+ const hiddenKilled = details.hiddenKilled?.length ?? 0;
384
+ const killedMeta = hiddenKilled > 0 ? [uiTheme.fg("dim", `${hiddenKilled} killed hidden`)] : [];
378
385
  if (screens.length === 0) {
379
386
  const fallback = result.content.find(part => part.type === "text")?.text ?? "no sessions";
387
+ const gist = hiddenKilled > 0 ? "no live sessions" : fallback;
380
388
  return new Text(
381
389
  renderStatusLine(
382
- { icon: "warning", title: `vibe ${op}`, meta: [uiTheme.fg("dim", frameText(fallback, 60))] },
390
+ {
391
+ icon: "warning",
392
+ title: `vibe ${op}`,
393
+ meta: [uiTheme.fg("dim", frameText(gist, 60)), ...killedMeta],
394
+ },
383
395
  uiTheme,
384
396
  ),
385
397
  0,
@@ -394,6 +406,7 @@ export function createVibeToolRenderer(op: VibeOp) {
394
406
  if (running > 0) meta.push(uiTheme.fg("accent", `${running} on air`));
395
407
  if (settledById.size > 0) meta.push(uiTheme.fg("success", `${settledById.size} settled`));
396
408
  if (details.wait?.timedOut) meta.push(uiTheme.fg("warning", "timed out"));
409
+ meta.push(...killedMeta);
397
410
  const title =
398
411
  op === "wait"
399
412
  ? waiting
package/src/tui.ts CHANGED
@@ -883,7 +883,8 @@ export class TUI extends Container {
883
883
  // engine paints only the modal on the alt buffer and leaves every
884
884
  // normal-screen accounting field (#previousFrameLength, #viewportTopRow, …)
885
885
  // untouched, so exiting reconciles cleanly against the terminal-restored
886
- // normal screen. #altPreviousLines is the last alt frame, for repaint-skip.
886
+ // normal screen. #altPreviousLines is the last alt frame, diffed row by row
887
+ // against the next one.
887
888
  #altActive = false;
888
889
  #mouseTracking: MouseTrackingState = "off";
889
890
  /** Product-owned probe for opt-in normal-buffer click capture (`tui.mouse`). Read every frame. */
@@ -2867,16 +2868,18 @@ export class TUI extends Container {
2867
2868
  this.#providerWindow.length > 0;
2868
2869
  if (diffable) {
2869
2870
  for (let index = 0; index < rows; index++) {
2870
- const previous = this.#providerPreparedRows[index];
2871
- const current = prepared.rows[index]!;
2872
2871
  if (
2873
- this.#providerWindow[index] === prepared.lines[index] &&
2874
- previous !== undefined &&
2875
- previous.widthEpoch === current.widthEpoch &&
2876
- previous.imageProtocol === current.imageProtocol
2872
+ !this.#rowNeedsRewrite(
2873
+ this.#providerWindow,
2874
+ this.#providerPreparedRows,
2875
+ prepared.lines,
2876
+ prepared.rows,
2877
+ index,
2878
+ )
2877
2879
  ) {
2878
2880
  continue;
2879
2881
  }
2882
+ const current = prepared.rows[index]!;
2880
2883
  buffer += `\x1b[${newTop + index + 1};1H${this.#lineRewriteSequence(
2881
2884
  current,
2882
2885
  width,
@@ -3440,6 +3443,29 @@ export class TUI extends Container {
3440
3443
  return visibleWidth(above);
3441
3444
  }
3442
3445
 
3446
+ /**
3447
+ * Whether screen row `index` must be rewritten to turn the previously painted
3448
+ * frame into this one: its line, or the preparation it was painted with,
3449
+ * changed.
3450
+ */
3451
+ #rowNeedsRewrite(
3452
+ previousLines: readonly string[],
3453
+ previousRows: readonly PreparedLine[],
3454
+ lines: readonly string[],
3455
+ rows: readonly PreparedLine[],
3456
+ index: number,
3457
+ ): boolean {
3458
+ const previous = previousRows[index];
3459
+ const current = rows[index]!;
3460
+ return (
3461
+ previousLines[index] !== lines[index] ||
3462
+ previous === undefined ||
3463
+ previous.width !== current.width ||
3464
+ previous.widthEpoch !== current.widthEpoch ||
3465
+ previous.imageProtocol !== current.imageProtocol
3466
+ );
3467
+ }
3468
+
3443
3469
  #lineRewriteSequence(
3444
3470
  line: PreparedLine,
3445
3471
  width: number,
@@ -3549,9 +3575,11 @@ export class TUI extends Container {
3549
3575
  }
3550
3576
 
3551
3577
  /**
3552
- * Full per-row viewport rewrite on the alt buffer. Emits only sync-output
3553
- * brackets, a cursor home, and per-row rewrites — never ED3 or any
3554
- * native-scrollback byte. The hardware cursor stays hidden here.
3578
+ * Paint a frame on the alt buffer: only the rows that changed since the
3579
+ * previous frame, or every row when the height changed, a repaint is forced,
3580
+ * or a changed frame holds OSC 66 text before or after. Emits only
3581
+ * sync-output brackets, cursor moves, and per-row rewrites — never ED3 or
3582
+ * any native-scrollback byte. The hardware cursor stays hidden here.
3555
3583
  */
3556
3584
  #emitAltFrame(prepared: PreparedLines, width: number, height: number, notifyPaint: boolean): void {
3557
3585
  // The pass that composed this frame ran with `altScreen`, so the normal
@@ -3573,38 +3601,34 @@ export class TUI extends Container {
3573
3601
  for (const seq of imageTransmits) transmitBuffer += seq;
3574
3602
  this.terminal.write(transmitBuffer);
3575
3603
  }
3576
- // Skip an identical repaint (the modal is mostly static between
3577
- // keystrokes) — unless a forced repaint (resetDisplay,
3578
- // requestRender(true)) is pending: the redraw gesture must repair a
3579
- // corrupted modal even when our cached frame is byte-identical.
3604
+ // A forced repaint (resetDisplay, requestRender(true)) rewrites every row
3605
+ // even when the cached frame is byte-identical: the redraw gesture must
3606
+ // repair a corrupted modal. So does a changed frame with OSC 66 text in
3607
+ // it, before or after: a scaled glyph spans the rows below its own and the
3608
+ // terminal drops it when any of them is written, so those rows are not
3609
+ // independent. Otherwise rewrite only the rows that changed (a keystroke
3610
+ // in a modal touches a row or two), and skip an identical frame entirely.
3580
3611
  const force = this.#forceViewportRepaintOnNextRender;
3581
3612
  this.#forceViewportRepaintOnNextRender = false;
3582
- if (!force && this.#altPreviousLines.length === height) {
3583
- let same = true;
3584
- for (let r = 0; r < height; r++) {
3585
- const previous = this.#altPreparedRows[r];
3586
- const current = prepared.rows[r]!;
3587
- if (
3588
- prepared.lines[r] !== this.#altPreviousLines[r] ||
3589
- previous === undefined ||
3590
- previous.width !== current.width ||
3591
- previous.widthEpoch !== current.widthEpoch ||
3592
- previous.imageProtocol !== current.imageProtocol
3593
- ) {
3594
- same = false;
3595
- break;
3596
- }
3597
- }
3598
- if (same) {
3599
- this.#altPreviousLines = prepared.lines;
3600
- this.#altPreparedRows = prepared.rows;
3601
- return;
3602
- }
3603
- }
3604
- let buffer = `${this.#paintBeginSequence}\x1b[H`;
3613
+ const full =
3614
+ force ||
3615
+ this.#altPreviousLines.length !== height ||
3616
+ ((this.#altPreviousLines.some(isOsc66Line) || prepared.lines.some(isOsc66Line)) &&
3617
+ prepared.rows.some((_row, r) =>
3618
+ this.#rowNeedsRewrite(this.#altPreviousLines, this.#altPreparedRows, prepared.lines, prepared.rows, r),
3619
+ ));
3620
+ let rowsBuffer = "";
3605
3621
  for (let r = 0; r < height; r++) {
3606
- if (r > 0) buffer += "\n";
3607
- buffer += this.#lineRewriteSequence(
3622
+ if (full) {
3623
+ if (r > 0) rowsBuffer += "\n";
3624
+ } else if (
3625
+ this.#rowNeedsRewrite(this.#altPreviousLines, this.#altPreparedRows, prepared.lines, prepared.rows, r)
3626
+ ) {
3627
+ rowsBuffer += `\x1b[${r + 1};1H`;
3628
+ } else {
3629
+ continue;
3630
+ }
3631
+ rowsBuffer += this.#lineRewriteSequence(
3608
3632
  prepared.rows[r]!,
3609
3633
  width,
3610
3634
  r,
@@ -3613,10 +3637,10 @@ export class TUI extends Container {
3613
3637
  this.#osc66SpacerGlyphWidth(prepared.lines, r),
3614
3638
  );
3615
3639
  }
3616
- buffer += this.#paintEndSequence;
3617
- this.terminal.write(buffer);
3618
3640
  this.#altPreviousLines = prepared.lines;
3619
3641
  this.#altPreparedRows = prepared.rows;
3642
+ if (rowsBuffer === "") return;
3643
+ this.terminal.write(`${this.#paintBeginSequence}${full ? "\x1b[H" : ""}${rowsBuffer}${this.#paintEndSequence}`);
3620
3644
  this.#debugPaint = { lines: prepared.lines, windowTop: 0, altScreen: true };
3621
3645
  if (notifyPaint) {
3622
3646
  this.#notifyPaint({
@@ -3628,6 +3652,6 @@ export class TUI extends Container {
3628
3652
  rows: height,
3629
3653
  });
3630
3654
  }
3631
- this.#fullRedrawCount += 1;
3655
+ if (full) this.#fullRedrawCount += 1;
3632
3656
  }
3633
3657
  }