@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.
- package/README.md +1 -0
- package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +337 -0
- package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +211 -0
- package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +107 -0
- package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +114 -0
- package/package.json +1 -1
- package/src/core/events.mjs +7 -3
- package/src/core/ime-cursor-coalesce.mjs +193 -0
- package/src/core/pty-attach-jiggle-controller.mjs +158 -45
- package/src/ui/pty-attach.ts +23 -55
package/src/ui/pty-attach.ts
CHANGED
|
@@ -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
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
}
|