@zhuxixi/pi-agent-board 0.3.0 → 0.3.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.
@@ -7,14 +7,8 @@ import type { Component, KeybindingsManager, TUI } from "@earendil-works/pi-tui"
7
7
  import { CURSOR_MARKER, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
8
8
  import { isProbablyEmptyPiInputLine } from "../core/pty-input.mjs";
9
9
  import { findHttpUrlAtCells, findWordRangeAtCells } from "../core/pty-links.mjs";
10
- import { createAttachOutputRenderScheduler, nextAttachRender, shouldScheduleAttachRenderForMessage } from "../core/pty-attach-render.mjs";
11
- import {
12
- createJiggleRetryState,
13
- feedOutput as feedJiggleRetry,
14
- nextRetryDelay,
15
- advanceRetry,
16
- stopRetry,
17
- } from "../core/pty-attach-jiggle-retry.mjs";
10
+ import { createAttachOutputRenderScheduler, nextAttachRender, projectPtyCursor, shouldScheduleAttachRenderForMessage } from "../core/pty-attach-render.mjs";
11
+ import { createJiggleRetryController } from "../core/pty-attach-jiggle-controller.mjs";
18
12
  import { clampInt, parseMouseInputChunk, resolveWheelLines, scrollViewportTop, selectionDragScrollLines } from "../core/pty-scroll.mjs";
19
13
 
20
14
  export type PtyAttachResult = { action: "detached" } | { action: "closed"; exitCode?: number | null };
@@ -53,6 +47,11 @@ const OSC52_MAX_BYTES = 1_000_000;
53
47
  const OSC52_CARRY_MAX_BYTES = OSC52_MAX_BYTES + 4096;
54
48
  const TERMINAL_PASSTHROUGH_MAX_BYTES = 5_000_000;
55
49
  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;
56
55
  const KITTY_IMAGE_PREFIX = "\x1b_G";
57
56
  const ITERM2_FILE_PREFIX = "\x1b]1337;File=";
58
57
 
@@ -122,10 +121,19 @@ export class PtyAttachComponent implements Component {
122
121
  private mouseRefreshTimers: Array<ReturnType<typeof setTimeout>> = [];
123
122
  // Jiggle retry chain: re-send resize jiggle until we see a full-clear sequence
124
123
  // in the PTY output, proving the child pi-tui did a fullRender and the replay
125
- // garbage has been flushed. Replaces the one-shot forceChildRedrawAfterLiveOutput.
126
- private jiggleRetryState = createJiggleRetryState();
127
- private jiggleRetryTimer: ReturnType<typeof setTimeout> | null = null;
128
- private clearCarry = "";
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).
127
+ private readonly jiggleRetry = createJiggleRetryController({
128
+ sendJiggle: () => this.forceChildRedraw(),
129
+ shouldFire: () => !this.closed && this.connected,
130
+ setTimeoutFn: (fn, ms) => {
131
+ const t = setTimeout(fn, ms);
132
+ t.unref?.();
133
+ return t;
134
+ },
135
+ clearTimeoutFn: (t) => clearTimeout(t as ReturnType<typeof setTimeout>),
136
+ });
129
137
  private osc52Carry = "";
130
138
  private passthroughCarry = "";
131
139
  private readonly connectStartedAt = Date.now();
@@ -318,7 +326,7 @@ export class PtyAttachComponent implements Component {
318
326
  this.send({ type: "hello", clientId: `ui-${Date.now()}`, wantOutput: true });
319
327
  this.sendResize();
320
328
  this.forceChildRedraw();
321
- this.startJiggleRetry();
329
+ this.jiggleRetry.start();
322
330
  this.enableMouseScroll();
323
331
  this.scheduleRender();
324
332
  this.startAttachSettle();
@@ -804,56 +812,13 @@ export class PtyAttachComponent implements Component {
804
812
  this.sendResize(cols, rows);
805
813
  this.deferAttachSettle();
806
814
  }
807
- }, 40);
815
+ }, JIGGLE_RESTORE_MS);
808
816
  this.redrawTimer.unref?.();
809
817
  }
810
818
 
811
- /** Start the jiggle retry chain after initial jiggle. */
812
- private startJiggleRetry(): void {
813
- if (this.jiggleRetryTimer) {
814
- clearTimeout(this.jiggleRetryTimer);
815
- this.jiggleRetryTimer = null;
816
- }
817
- this.jiggleRetryState = createJiggleRetryState();
818
- this.clearCarry = "";
819
- this.scheduleNextJiggleRetry();
820
- }
821
-
822
- /** Check socket output for full-clear sequence; cancel retry if found. */
819
+ /** Feed socket output into the jiggle retry controller (clear/frame detection). */
823
820
  private checkClearSequence(data: string): void {
824
- if (this.jiggleRetryState.stopped) return;
825
- const result = feedJiggleRetry(this.jiggleRetryState, data, this.clearCarry);
826
- this.jiggleRetryState = result.state;
827
- this.clearCarry = result.carry;
828
- if (result.clearFound) this.cancelJiggleRetry();
829
- }
830
-
831
- /** Schedule the next jiggle retry with backoff. */
832
- private scheduleNextJiggleRetry(): void {
833
- const delay = nextRetryDelay(this.jiggleRetryState);
834
- if (delay === null) {
835
- // Chain exhausted (max retries) — mark stopped so checkClearSequence
836
- // short-circuits on subsequent output chunks.
837
- this.jiggleRetryState = stopRetry(this.jiggleRetryState);
838
- return;
839
- }
840
- this.jiggleRetryTimer = setTimeout(() => {
841
- this.jiggleRetryTimer = null;
842
- if (this.closed || !this.connected) return;
843
- this.forceChildRedraw();
844
- this.jiggleRetryState = advanceRetry(this.jiggleRetryState);
845
- this.scheduleNextJiggleRetry();
846
- }, delay);
847
- this.jiggleRetryTimer.unref?.();
848
- }
849
-
850
- /** Cancel the jiggle retry chain. */
851
- private cancelJiggleRetry(): void {
852
- if (this.jiggleRetryTimer) {
853
- clearTimeout(this.jiggleRetryTimer);
854
- this.jiggleRetryTimer = null;
855
- }
856
- this.jiggleRetryState = stopRetry(this.jiggleRetryState);
821
+ this.jiggleRetry.feed(data);
857
822
  }
858
823
 
859
824
  private tryScrollBy(linesUp: number): boolean {
@@ -1009,7 +974,7 @@ export class PtyAttachComponent implements Component {
1009
974
  // The PTY cursor: xterm buffer cursorY is relative to baseY (the viewport top of
1010
975
  // the child terminal); cursorX may equal cols (one past the last cell). Only render
1011
976
  // it when it lands inside the projected viewport.
1012
- const cursor = cursorInViewport(buf, start, height);
977
+ const cursor = projectPtyCursor(buf, start, height);
1013
978
  for (let i = start; i < end; i++) {
1014
979
  out.push(lineToAnsi(buf.getLine(i), reusable, this.term, i, selection, cursor));
1015
980
  }
@@ -1019,7 +984,7 @@ export class PtyAttachComponent implements Component {
1019
984
 
1020
985
  private close(): void {
1021
986
  this.closed = true;
1022
- this.cancelJiggleRetry();
987
+ this.jiggleRetry.stop();
1023
988
  this.disableMouseScroll();
1024
989
  this.clearMouseRefreshTimers();
1025
990
  this.clearPendingClick();
@@ -1177,22 +1142,6 @@ function lineToAnsi(
1177
1142
  return out + "\x1b[0m";
1178
1143
  }
1179
1144
 
1180
- /**
1181
- * Resolve the PTY cursor into viewport coordinates. xterm's buffer cursorY is relative
1182
- * to baseY (the child terminal's viewport top); returns null when the cursor is outside
1183
- * the projected window (e.g. the user scrolled up into history).
1184
- */
1185
- function cursorInViewport(
1186
- buf: XtermLike["buffer"]["active"],
1187
- start: number,
1188
- height: number,
1189
- ): { row: number; col: number } | null {
1190
- if (typeof buf.cursorX !== "number" || typeof buf.cursorY !== "number" || buf.cursorX < 0 || buf.cursorY < 0) return null;
1191
- const row = buf.baseY + buf.cursorY - start;
1192
- if (row < 0 || row >= height) return null;
1193
- return { row, col: buf.cursorX };
1194
- }
1195
-
1196
1145
  function attrKey(cell: BufferCellLike, selected = false, isCursor = false): string {
1197
1146
  return [
1198
1147
  selected ? 1 : 0,