@zhuxixi/pi-agent-board 0.4.0 → 0.4.2

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.
@@ -8,6 +8,7 @@ import { CURSOR_MARKER, Key, matchesKey, truncateToWidth, visibleWidth } from "@
8
8
  import { isProbablyEmptyPiInputLine } from "../core/pty-input.mjs";
9
9
  import { findHttpUrlAtCells, findWordRangeAtCells } from "../core/pty-links.mjs";
10
10
  import { createAttachOutputRenderScheduler, nextAttachRender, projectPtyCursor, shouldScheduleAttachRenderForMessage } from "../core/pty-attach-render.mjs";
11
+ import { installImeCursorCoalesce } from "../core/ime-cursor-coalesce.mjs";
11
12
  import { createJiggleRetryController } from "../core/pty-attach-jiggle-controller.mjs";
12
13
  import { clampInt, parseMouseInputChunk, resolveWheelLines, scrollViewportTop, selectionDragScrollLines } from "../core/pty-scroll.mjs";
13
14
 
@@ -47,11 +48,6 @@ const OSC52_MAX_BYTES = 1_000_000;
47
48
  const OSC52_CARRY_MAX_BYTES = OSC52_MAX_BYTES + 4096;
48
49
  const TERMINAL_PASSTHROUGH_MAX_BYTES = 5_000_000;
49
50
  const TERMINAL_PASSTHROUGH_CARRY_MAX_BYTES = TERMINAL_PASSTHROUGH_MAX_BYTES + 4096;
50
- /** Delay between the shrink and restore halves of a resize jiggle. 200ms keeps
51
- * the two SIGWINCHs apart well beyond pi-tui's 16ms render throttle, so a busy
52
- * (booting) child processes them as two separate renders instead of coalescing
53
- * the pair into a net-zero size change. */
54
- const JIGGLE_RESTORE_MS = 200;
55
51
  const KITTY_IMAGE_PREFIX = "\x1b_G";
56
52
  const ITERM2_FILE_PREFIX = "\x1b]1337;File=";
57
53
 
@@ -117,16 +113,16 @@ export class PtyAttachComponent implements Component {
117
113
  private parserBuffer = "";
118
114
  private retryTimer: ReturnType<typeof setTimeout> | null = null;
119
115
  private loadingTimer: ReturnType<typeof setInterval> | null = null;
120
- private redrawTimer: ReturnType<typeof setTimeout> | null = null;
121
116
  private mouseRefreshTimers: Array<ReturnType<typeof setTimeout>> = [];
122
- // Jiggle retry chain: re-send resize jiggle until we see a full-clear sequence
123
- // in the PTY output, proving the child pi-tui did a fullRender and the replay
124
- // garbage has been flushed. The controller re-arms once on the child TUI's
125
- // first frame so cold-start attaches get a fresh budget exactly when the
126
- // child can finally observe a resize (issue #10).
117
+ // Shrink-and-hold jiggle protocol (issue #25): on attach we resize the child to
118
+ // (cols-1, rows-1) and hold it there until the child emits a full clear
119
+ // (\x1b[2J) any render the child makes while held sees a width delta and
120
+ // must fullRender, so boot-storm coalescing of ±1 pulses can no longer
121
+ // produce a net-zero change. The controller restores the original size on
122
+ // clear / first TUI frame (re-arm) / no-frame 6s guard / budget exhaustion /
123
+ // close / external resize (guards G1-G5, see controller module).
127
124
  private readonly jiggleRetry = createJiggleRetryController({
128
- sendJiggle: () => this.forceChildRedraw(),
129
- shouldFire: () => !this.closed && this.connected,
125
+ sendResize: (cols, rows) => this.sendResize(cols, rows),
130
126
  setTimeoutFn: (fn, ms) => {
131
127
  const t = setTimeout(fn, ms);
132
128
  t.unref?.();
@@ -170,6 +166,10 @@ export class PtyAttachComponent implements Component {
170
166
  // into many small chunks; painting after each chunk would drive the outer TUI to its
171
167
  // frame cap and expose intermediate frames (visible as flicker on a busy session).
172
168
  private readonly outputRenderScheduler = createAttachOutputRenderScheduler(() => this.scheduleRender());
169
+ // Fold pi-tui's post-frame cursor-park writes back into the frame's sync block so the
170
+ // terminal reports one stable IME cursor rect per frame instead of two (issue #28).
171
+ // Never active when AGENT_BOARD_IME_FIX=0; no-op passthrough if pi-tui changes shape.
172
+ private readonly imeCoalesceUninstall: (() => void) | null;
173
173
 
174
174
  constructor(
175
175
  private readonly tui: TUI,
@@ -181,6 +181,9 @@ export class PtyAttachComponent implements Component {
181
181
  const size = this.currentSize();
182
182
  this.cols = size.cols;
183
183
  this.rows = size.rows;
184
+ // Assigned in the body (not as a field initializer) so it runs after the `tui`
185
+ // parameter property is set regardless of the TS loader's field-init semantics.
186
+ this.imeCoalesceUninstall = installImeCursorCoalesce(this.tui);
184
187
  this.term = new Terminal({ cols: this.cols, rows: this.rows, scrollback: 2000, allowProposedApi: true });
185
188
  // Keep mouse reporting enabled by default so wheel scrolling and local drag-to-copy
186
189
  // selection can coexist inside the attach surface. Set AGENT_BOARD_ATTACH_MOUSE=0
@@ -324,9 +327,7 @@ export class PtyAttachComponent implements Component {
324
327
  this.connected = true;
325
328
  this.status = "attached";
326
329
  this.send({ type: "hello", clientId: `ui-${Date.now()}`, wantOutput: true });
327
- this.sendResize();
328
- this.forceChildRedraw();
329
- this.jiggleRetry.start();
330
+ this.jiggleRetry.start(this.cols, this.rows);
330
331
  this.enableMouseScroll();
331
332
  this.scheduleRender();
332
333
  this.startAttachSettle();
@@ -390,7 +391,7 @@ export class PtyAttachComponent implements Component {
390
391
  /**
391
392
  * Attach transition lifecycle. Keep the loading banner up while the screen-log replay
392
393
  * and the initial resize-jiggle redraws settle, so the buffer doesn't visibly scroll or
393
- * flash through the viewport on attach. Each `forceChildRedraw` defers the settle
394
+ * flash through the viewport on attach. Each output chunk defers the settle
394
395
  * window; a hard timeout guards against a session that never produces output.
395
396
  */
396
397
  private startAttachSettle(): void {
@@ -435,13 +436,6 @@ export class PtyAttachComponent implements Component {
435
436
  this.scheduleRender(true);
436
437
  }
437
438
 
438
- private clearRedrawTimer(): void {
439
- if (this.redrawTimer) {
440
- clearTimeout(this.redrawTimer);
441
- this.redrawTimer = null;
442
- }
443
- }
444
-
445
439
  private clearMouseRefreshTimers(): void {
446
440
  for (const timer of this.mouseRefreshTimers) clearTimeout(timer);
447
441
  this.mouseRefreshTimers = [];
@@ -784,6 +778,9 @@ export class PtyAttachComponent implements Component {
784
778
  if (size.cols === this.cols && size.rows === this.rows) return;
785
779
  this.cols = size.cols;
786
780
  this.rows = size.rows;
781
+ // A real terminal resize supersedes the hold protocol: cancel any armed
782
+ // hold and adopt the new size as the baseline (guard G4, issue #25).
783
+ this.jiggleRetry.notifyExternalResize(size.cols, size.rows);
787
784
  this.term.resize(this.cols, this.rows);
788
785
  this.sendResize();
789
786
  this.enableMouseScroll();
@@ -794,28 +791,6 @@ export class PtyAttachComponent implements Component {
794
791
  this.clampViewportTop(this.bodyHeight());
795
792
  }
796
793
 
797
- private forceChildRedraw(): void {
798
- this.clearRedrawTimer();
799
- if (!this.connected) return;
800
- const cols = this.cols;
801
- const rows = this.rows;
802
- const jiggle = localResizeJiggleSize(cols, rows);
803
- if (!jiggle) return;
804
- // A completed-session reattach often starts from an old screen.log recorded at
805
- // a different terminal size. Real terminal zoom fixes that by causing SIGWINCH;
806
- // do the same proactively so the child Pi redraws for the attach viewport.
807
- this.sendResize(jiggle.cols, jiggle.rows);
808
- this.deferAttachSettle();
809
- this.redrawTimer = setTimeout(() => {
810
- this.redrawTimer = null;
811
- if (!this.closed && this.connected) {
812
- this.sendResize(cols, rows);
813
- this.deferAttachSettle();
814
- }
815
- }, JIGGLE_RESTORE_MS);
816
- this.redrawTimer.unref?.();
817
- }
818
-
819
794
  /** Feed socket output into the jiggle retry controller (clear/frame detection). */
820
795
  private checkClearSequence(data: string): void {
821
796
  this.jiggleRetry.feed(data);
@@ -984,13 +959,13 @@ export class PtyAttachComponent implements Component {
984
959
 
985
960
  private close(): void {
986
961
  this.closed = true;
987
- this.jiggleRetry.stop();
962
+ this.imeCoalesceUninstall?.();
963
+ this.jiggleRetry.restoreAndStop();
988
964
  this.disableMouseScroll();
989
965
  this.clearMouseRefreshTimers();
990
966
  this.clearPendingClick();
991
967
  this.clearSelectionAutoScroll();
992
968
  this.clearRetry();
993
- this.clearRedrawTimer();
994
969
  this.stopLoadingTicker();
995
970
  this.outputRenderScheduler.dispose();
996
971
  if (this.attachSettleTimer) {
@@ -1009,13 +984,6 @@ export class PtyAttachComponent implements Component {
1009
984
  }
1010
985
  }
1011
986
 
1012
- function localResizeJiggleSize(cols: number, rows: number): { cols: number; rows: number } | null {
1013
- if (cols > 21 && rows > 6) return { cols: cols - 1, rows: rows - 1 };
1014
- if (rows > 6) return { cols, rows: rows - 1 };
1015
- if (cols > 21) return { cols: cols - 1, rows };
1016
- return null;
1017
- }
1018
-
1019
987
  function sameMousePoint(a: MousePoint, b: MousePoint): boolean {
1020
988
  return a.line === b.line && Math.abs(a.col - b.col) <= 1;
1021
989
  }