@zhuxixi/pi-agent-board 0.3.0
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/IMPLEMENTATION_PLAN.md +920 -0
- package/LICENSE +21 -0
- package/PRD.md +484 -0
- package/PROGRESS.md +127 -0
- package/README.md +131 -0
- package/VERIFY.md +113 -0
- package/docs/BATCH_SELECTION_READ_FLOW.md +277 -0
- package/docs/EXPLORATION.md +187 -0
- package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +579 -0
- package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +704 -0
- package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +499 -0
- package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +366 -0
- package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +105 -0
- package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +142 -0
- package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +59 -0
- package/index.ts +6 -0
- package/package.json +81 -0
- package/runner/job-runner.mjs +420 -0
- package/runner/pty-runner.mjs +310 -0
- package/runner/state-runner.mjs +120 -0
- package/runner/title-runner.mjs +80 -0
- package/scripts/patch-vulns.mjs +59 -0
- package/src/commands/agent-board.ts +318 -0
- package/src/commands/attach-flow.ts +231 -0
- package/src/commands/bg.ts +70 -0
- package/src/core/atomic.mjs +145 -0
- package/src/core/auto-state.mjs +320 -0
- package/src/core/dashboard-render.mjs +10 -0
- package/src/core/derive.mjs +114 -0
- package/src/core/diagnostics.mjs +109 -0
- package/src/core/events.mjs +268 -0
- package/src/core/evidence.mjs +242 -0
- package/src/core/follow-up-queue.mjs +193 -0
- package/src/core/heuristics.mjs +240 -0
- package/src/core/ids.mjs +35 -0
- package/src/core/invocation.mjs +43 -0
- package/src/core/launch-options.mjs +317 -0
- package/src/core/launch.mjs +116 -0
- package/src/core/locks.mjs +80 -0
- package/src/core/paths.mjs +86 -0
- package/src/core/pid.mjs +42 -0
- package/src/core/prewarm-schedule.mjs +41 -0
- package/src/core/prompt-transport.mjs +13 -0
- package/src/core/pty-attach-jiggle-retry.mjs +90 -0
- package/src/core/pty-attach-render.mjs +51 -0
- package/src/core/pty-input.mjs +15 -0
- package/src/core/pty-links.mjs +71 -0
- package/src/core/pty-scroll.mjs +155 -0
- package/src/core/pty-support.mjs +327 -0
- package/src/core/repo.mjs +47 -0
- package/src/core/rows.mjs +290 -0
- package/src/core/screen-log-gc.mjs +198 -0
- package/src/core/screen-log.mjs +160 -0
- package/src/core/session-view.mjs +174 -0
- package/src/core/steering-prompts.mjs +34 -0
- package/src/core/steering.mjs +133 -0
- package/src/core/store.mjs +308 -0
- package/src/core/title.mjs +43 -0
- package/src/core/types.mjs +380 -0
- package/src/core/worktree.mjs +64 -0
- package/src/index.ts +109 -0
- package/src/runtime/service.mjs +1194 -0
- package/src/ui/dashboard-evidence.mjs +85 -0
- package/src/ui/dashboard.ts +1952 -0
- package/src/ui/pty-attach.ts +1378 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
# Attach Double-Cursor Jiggle Retry Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add jiggle retry logic that re-sends resize jiggle with backoff until a full-clear sequence (`\x1b[2J`) is detected in the PTY output, fixing the double-cursor desync on cold-start/streaming attach.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Pure state machine in `src/core/pty-attach-jiggle-retry.mjs` handles all retry logic (clear detection, backoff scheduling, stop conditions). The UI component `src/ui/pty-attach.ts` acts as glue — feeding socket data to the state machine and managing the retry timer.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript (ESM), Node.js `node:test`, `@xterm/headless`
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- Pure logic must be dependency-free (no socket, no xterm, no timers) — testable in isolation
|
|
14
|
+
- Use `node:test` + `node:assert/strict` for tests (existing project pattern)
|
|
15
|
+
- Commit messages in English, conventional format (`feat:`/`fix:`/`test:`)
|
|
16
|
+
- Worktree: `/home/elling/git-repo/github/pi-agent-board/.claude/worktrees/issue-2-attach-double-cursor-jiggle-retry`
|
|
17
|
+
- Don't touch `main` branch — all work on `issue-2-attach-double-cursor-jiggle-retry`
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
### Task 1: Pure Logic — Jiggle Retry State Machine
|
|
22
|
+
|
|
23
|
+
**Files:**
|
|
24
|
+
- Create: `src/core/pty-attach-jiggle-retry.mjs`
|
|
25
|
+
- Test: `test/pty-attach-jiggle-retry.test.mjs`
|
|
26
|
+
|
|
27
|
+
**Interfaces:**
|
|
28
|
+
- Consumes: nothing (standalone module)
|
|
29
|
+
- Produces:
|
|
30
|
+
- `BACKOFF_MS: readonly number[]` — `[120, 500, 1500, 3000]`
|
|
31
|
+
- `MAX_RETRIES: number` — `4`
|
|
32
|
+
- `createJiggleRetryState(): JiggleRetryState`
|
|
33
|
+
- `feedOutput(state, data, carry): { state, carry, clearFound }`
|
|
34
|
+
- `nextRetryDelay(state): number | null`
|
|
35
|
+
- `advanceRetry(state): JiggleRetryState`
|
|
36
|
+
- `stopRetry(state): JiggleRetryState`
|
|
37
|
+
- `hasFullClearSequence(data: string): boolean`
|
|
38
|
+
|
|
39
|
+
- [ ] **Step 1: Write the failing tests**
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
import assert from "node:assert/strict";
|
|
43
|
+
import test from "node:test";
|
|
44
|
+
import {
|
|
45
|
+
BACKOFF_MS,
|
|
46
|
+
MAX_RETRIES,
|
|
47
|
+
createJiggleRetryState,
|
|
48
|
+
feedOutput,
|
|
49
|
+
nextRetryDelay,
|
|
50
|
+
advanceRetry,
|
|
51
|
+
stopRetry,
|
|
52
|
+
hasFullClearSequence,
|
|
53
|
+
} from "../src/core/pty-attach-jiggle-retry.mjs";
|
|
54
|
+
|
|
55
|
+
// --- hasFullClearSequence ---
|
|
56
|
+
|
|
57
|
+
test("detects \\x1b[2J in plain data", () => {
|
|
58
|
+
assert.equal(hasFullClearSequence("hello\x1b[2Jworld"), true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("detects \\x1b[2J at start", () => {
|
|
62
|
+
assert.equal(hasFullClearSequence("\x1b[2J"), true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("detects \\x1b[2J followed by \\x1b[H", () => {
|
|
66
|
+
assert.equal(hasFullClearSequence("\x1b[2J\x1b[H"), true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("rejects data without \\x1b[2J", () => {
|
|
70
|
+
assert.equal(hasFullClearSequence("hello world"), false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("rejects \\x1b[3J (scrollback clear, not full clear)", () => {
|
|
74
|
+
assert.equal(hasFullClearSequence("\x1b[3J"), false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("rejects partial escape \\x1b[2 (no J)", () => {
|
|
78
|
+
assert.equal(hasFullClearSequence("\x1b[2"), false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// --- createJiggleRetryState ---
|
|
82
|
+
|
|
83
|
+
test("initial state is fresh", () => {
|
|
84
|
+
const s = createJiggleRetryState();
|
|
85
|
+
assert.equal(s.retryIndex, 0);
|
|
86
|
+
assert.equal(s.clearDetected, false);
|
|
87
|
+
assert.equal(s.stopped, false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// --- feedOutput ---
|
|
91
|
+
|
|
92
|
+
test("feedOutput detects clear sequence", () => {
|
|
93
|
+
const s = createJiggleRetryState();
|
|
94
|
+
const r = feedOutput(s, "some output \x1b[2J more", "");
|
|
95
|
+
assert.equal(r.clearFound, true);
|
|
96
|
+
assert.equal(r.state.clearDetected, true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("feedOutput returns clearFound=false when no clear", () => {
|
|
100
|
+
const s = createJiggleRetryState();
|
|
101
|
+
const r = feedOutput(s, "normal output", "");
|
|
102
|
+
assert.equal(r.clearFound, false);
|
|
103
|
+
assert.equal(r.state.clearDetected, false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("feedOutput handles cross-chunk split: \\x1b[ + 2J", () => {
|
|
107
|
+
const s = createJiggleRetryState();
|
|
108
|
+
const r1 = feedOutput(s, "data\x1b[", "");
|
|
109
|
+
assert.equal(r1.clearFound, false);
|
|
110
|
+
assert.equal(r1.carry, "\x1b[");
|
|
111
|
+
const r2 = feedOutput(r1.state, "2Jrest", r1.carry);
|
|
112
|
+
assert.equal(r2.clearFound, true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("feedOutput handles cross-chunk split: \\x1b + [2J", () => {
|
|
116
|
+
const s = createJiggleRetryState();
|
|
117
|
+
const r1 = feedOutput(s, "data\x1b", "");
|
|
118
|
+
assert.equal(r1.clearFound, false);
|
|
119
|
+
assert.equal(r1.carry, "\x1b");
|
|
120
|
+
const r2 = feedOutput(r1.state, "[2Jrest", r1.carry);
|
|
121
|
+
assert.equal(r2.clearFound, true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("feedOutput clears carry after successful detection", () => {
|
|
125
|
+
const s = createJiggleRetryState();
|
|
126
|
+
const r1 = feedOutput(s, "data\x1b[", "");
|
|
127
|
+
const r2 = feedOutput(r1.state, "2Jrest", r1.carry);
|
|
128
|
+
assert.equal(r2.carry, "");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("feedOutput keeps carry for non-matching partial", () => {
|
|
132
|
+
const s = createJiggleRetryState();
|
|
133
|
+
const r = feedOutput(s, "data\x1b[3", "");
|
|
134
|
+
assert.equal(r.clearFound, false);
|
|
135
|
+
assert.ok(r.carry.length > 0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// --- nextRetryDelay ---
|
|
139
|
+
|
|
140
|
+
test("nextRetryDelay returns BACKOFF_MS in order", () => {
|
|
141
|
+
let s = createJiggleRetryState();
|
|
142
|
+
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
143
|
+
assert.equal(nextRetryDelay(s), BACKOFF_MS[i]);
|
|
144
|
+
s = advanceRetry(s);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("nextRetryDelay returns null after MAX_RETRIES", () => {
|
|
149
|
+
let s = createJiggleRetryState();
|
|
150
|
+
for (let i = 0; i < MAX_RETRIES; i++) s = advanceRetry(s);
|
|
151
|
+
assert.equal(nextRetryDelay(s), null);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("nextRetryDelay returns null when clearDetected", () => {
|
|
155
|
+
const s = createJiggleRetryState();
|
|
156
|
+
const r = feedOutput(s, "\x1b[2J", "");
|
|
157
|
+
assert.equal(nextRetryDelay(r.state), null);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("nextRetryDelay returns null when stopped", () => {
|
|
161
|
+
const s = stopRetry(createJiggleRetryState());
|
|
162
|
+
assert.equal(nextRetryDelay(s), null);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// --- advanceRetry ---
|
|
166
|
+
|
|
167
|
+
test("advanceRetry increments retryIndex", () => {
|
|
168
|
+
const s0 = createJiggleRetryState();
|
|
169
|
+
const s1 = advanceRetry(s0);
|
|
170
|
+
assert.equal(s1.retryIndex, 1);
|
|
171
|
+
const s2 = advanceRetry(s1);
|
|
172
|
+
assert.equal(s2.retryIndex, 2);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// --- stopRetry ---
|
|
176
|
+
|
|
177
|
+
test("stopRetry sets stopped=true", () => {
|
|
178
|
+
const s = stopRetry(createJiggleRetryState());
|
|
179
|
+
assert.equal(s.stopped, true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// --- Immutability ---
|
|
183
|
+
|
|
184
|
+
test("feedOutput does not mutate original state", () => {
|
|
185
|
+
const s = createJiggleRetryState();
|
|
186
|
+
feedOutput(s, "\x1b[2J", "");
|
|
187
|
+
assert.equal(s.clearDetected, false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("advanceRetry does not mutate original state", () => {
|
|
191
|
+
const s = createJiggleRetryState();
|
|
192
|
+
advanceRetry(s);
|
|
193
|
+
assert.equal(s.retryIndex, 0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("stopRetry does not mutate original state", () => {
|
|
197
|
+
const s = createJiggleRetryState();
|
|
198
|
+
stopRetry(s);
|
|
199
|
+
assert.equal(s.stopped, false);
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
cd /home/elling/git-repo/github/pi-agent-board/.claude/worktrees/issue-2-attach-double-cursor-jiggle-retry
|
|
207
|
+
node --test test/pty-attach-jiggle-retry.test.mjs
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Expected: FAIL — module not found
|
|
211
|
+
|
|
212
|
+
- [ ] **Step 3: Implement the state machine**
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
/** Pure logic for attach jiggle-retry: detect full-clear, schedule backoff retries. */
|
|
216
|
+
|
|
217
|
+
export const BACKOFF_MS = Object.freeze([120, 500, 1500, 3000]);
|
|
218
|
+
export const MAX_RETRIES = BACKOFF_MS.length;
|
|
219
|
+
|
|
220
|
+
const FULL_CLEAR = "\x1b[2J";
|
|
221
|
+
/** Carry enough bytes to catch a split escape sequence at chunk boundary. */
|
|
222
|
+
const CARRY_LEN = FULL_CLEAR.length - 1; // 3 bytes: \x1b, \x1b[, \x1b[2
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @typedef {Object} JiggleRetryState
|
|
226
|
+
* @property {number} retryIndex - Current retry round (0 = initial, not yet retried).
|
|
227
|
+
* @property {boolean} clearDetected - Whether a full-clear sequence was seen.
|
|
228
|
+
* @property {boolean} stopped - Whether the retry chain has been stopped.
|
|
229
|
+
*/
|
|
230
|
+
|
|
231
|
+
/** Create initial retry state. */
|
|
232
|
+
export function createJiggleRetryState() {
|
|
233
|
+
return { retryIndex: 0, clearDetected: false, stopped: false };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Feed PTY output data into the state machine.
|
|
238
|
+
* Returns new state, updated carry buffer, and whether a clear was found.
|
|
239
|
+
* @param {JiggleRetryState} state
|
|
240
|
+
* @param {string} data - New output data chunk.
|
|
241
|
+
* @param {string} carry - Leftover partial escape sequence from previous chunk.
|
|
242
|
+
* @returns {{ state: JiggleRetryState, carry: string, clearFound: boolean }}
|
|
243
|
+
*/
|
|
244
|
+
export function feedOutput(state, data, carry) {
|
|
245
|
+
const combined = carry + data;
|
|
246
|
+
const clearFound = hasFullClearSequence(combined);
|
|
247
|
+
const newCarry = clearFound ? "" : tailCarry(combined);
|
|
248
|
+
const newState = clearFound
|
|
249
|
+
? { ...state, clearDetected: true }
|
|
250
|
+
: state;
|
|
251
|
+
return { state: newState, carry: newCarry, clearFound };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get the delay before the next retry, or null if no more retries.
|
|
256
|
+
* @param {JiggleRetryState} state
|
|
257
|
+
* @returns {number | null}
|
|
258
|
+
*/
|
|
259
|
+
export function nextRetryDelay(state) {
|
|
260
|
+
if (state.stopped || state.clearDetected) return null;
|
|
261
|
+
if (state.retryIndex >= MAX_RETRIES) return null;
|
|
262
|
+
return BACKOFF_MS[state.retryIndex];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Advance to the next retry round.
|
|
267
|
+
* @param {JiggleRetryState} state
|
|
268
|
+
* @returns {JiggleRetryState}
|
|
269
|
+
*/
|
|
270
|
+
export function advanceRetry(state) {
|
|
271
|
+
return { ...state, retryIndex: state.retryIndex + 1 };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Stop the retry chain (attach settled, component closed, etc.).
|
|
276
|
+
* @param {JiggleRetryState} state
|
|
277
|
+
* @returns {JiggleRetryState}
|
|
278
|
+
*/
|
|
279
|
+
export function stopRetry(state) {
|
|
280
|
+
return { ...state, stopped: true };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Check if data contains the full-clear escape sequence.
|
|
285
|
+
* @param {string} data
|
|
286
|
+
* @returns {boolean}
|
|
287
|
+
*/
|
|
288
|
+
export function hasFullClearSequence(data) {
|
|
289
|
+
return data.includes(FULL_CLEAR);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Extract the tail carry for cross-chunk boundary detection. */
|
|
293
|
+
function tailCarry(data) {
|
|
294
|
+
// We only need to carry up to CARRY_LEN bytes to catch a split \x1b[2J.
|
|
295
|
+
const tail = data.slice(-CARRY_LEN);
|
|
296
|
+
// Find the last \x1b in the tail — everything before it can't be part of
|
|
297
|
+
// a split escape sequence.
|
|
298
|
+
const escIdx = tail.lastIndexOf("\x1b");
|
|
299
|
+
return escIdx >= 0 ? tail.slice(escIdx) : "";
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
cd /home/elling/git-repo/github/pi-agent-board/.claude/worktrees/issue-2-attach-double-cursor-jiggle-retry
|
|
307
|
+
node --test test/pty-attach-jiggle-retry.test.mjs
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Expected: All 18 tests PASS
|
|
311
|
+
|
|
312
|
+
- [ ] **Step 5: Commit**
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
cd /home/elling/git-repo/github/pi-agent-board/.claude/worktrees/issue-2-attach-double-cursor-jiggle-retry
|
|
316
|
+
git add src/core/pty-attach-jiggle-retry.mjs test/pty-attach-jiggle-retry.test.mjs
|
|
317
|
+
git commit -m "feat: add jiggle retry state machine with clear-sequence detection"
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
### Task 2: Integrate Retry Chain into PtyAttachComponent
|
|
323
|
+
|
|
324
|
+
**Files:**
|
|
325
|
+
- Modify: `src/ui/pty-attach.ts`
|
|
326
|
+
|
|
327
|
+
**Interfaces:**
|
|
328
|
+
- Consumes: `createJiggleRetryState`, `feedOutput`, `nextRetryDelay`, `advanceRetry`, `stopRetry`, `BACKOFF_MS` from `src/core/pty-attach-jiggle-retry.mjs`
|
|
329
|
+
- Produces: no new exports — internal behavior change only
|
|
330
|
+
|
|
331
|
+
- [ ] **Step 1: Add import and new private fields**
|
|
332
|
+
|
|
333
|
+
In `src/ui/pty-attach.ts`, add to the imports at top:
|
|
334
|
+
```typescript
|
|
335
|
+
import {
|
|
336
|
+
createJiggleRetryState,
|
|
337
|
+
feedOutput as feedJiggleRetry,
|
|
338
|
+
nextRetryDelay,
|
|
339
|
+
advanceRetry,
|
|
340
|
+
stopRetry,
|
|
341
|
+
} from "../core/pty-attach-jiggle-retry.mjs";
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Add new private fields to the class (after existing fields, near `forcedRedrawAfterLiveOutput`):
|
|
345
|
+
```typescript
|
|
346
|
+
// Jiggle retry chain: re-send resize jiggle until we see a full-clear sequence
|
|
347
|
+
// in the PTY output, proving the child pi-tui did a fullRender and the replay
|
|
348
|
+
// garbage has been flushed. Replaces the one-shot forceChildRedrawAfterLiveOutput.
|
|
349
|
+
private jiggleRetryState = createJiggleRetryState();
|
|
350
|
+
private jiggleRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
351
|
+
private clearCarry = "";
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
- [ ] **Step 2: Add new private methods**
|
|
355
|
+
|
|
356
|
+
Add these methods to the class (near `forceChildRedraw`):
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
/** Start the jiggle retry chain after initial jiggle. */
|
|
360
|
+
private startJiggleRetry(): void {
|
|
361
|
+
this.jiggleRetryState = createJiggleRetryState();
|
|
362
|
+
this.clearCarry = "";
|
|
363
|
+
this.scheduleNextJiggleRetry();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Check socket output for full-clear sequence; cancel retry if found. */
|
|
367
|
+
private checkClearSequence(data: string): void {
|
|
368
|
+
const result = feedJiggleRetry(this.jiggleRetryState, data, this.clearCarry);
|
|
369
|
+
this.jiggleRetryState = result.state;
|
|
370
|
+
this.clearCarry = result.carry;
|
|
371
|
+
if (result.clearFound) this.cancelJiggleRetry();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Schedule the next jiggle retry with backoff. */
|
|
375
|
+
private scheduleNextJiggleRetry(): void {
|
|
376
|
+
const delay = nextRetryDelay(this.jiggleRetryState);
|
|
377
|
+
if (delay === null) return;
|
|
378
|
+
this.jiggleRetryTimer = setTimeout(() => {
|
|
379
|
+
this.jiggleRetryTimer = null;
|
|
380
|
+
if (this.closed || !this.connected) return;
|
|
381
|
+
this.forceChildRedraw();
|
|
382
|
+
this.jiggleRetryState = advanceRetry(this.jiggleRetryState);
|
|
383
|
+
this.scheduleNextJiggleRetry();
|
|
384
|
+
}, delay);
|
|
385
|
+
this.jiggleRetryTimer.unref?.();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Cancel the jiggle retry chain. */
|
|
389
|
+
private cancelJiggleRetry(): void {
|
|
390
|
+
if (this.jiggleRetryTimer) {
|
|
391
|
+
clearTimeout(this.jiggleRetryTimer);
|
|
392
|
+
this.jiggleRetryTimer = null;
|
|
393
|
+
}
|
|
394
|
+
this.jiggleRetryState = stopRetry(this.jiggleRetryState);
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
- [ ] **Step 3: Wire into connect() — start retry after initial jiggle**
|
|
399
|
+
|
|
400
|
+
In `connect()`, after `this.forceChildRedraw()` (around line 309), add:
|
|
401
|
+
```typescript
|
|
402
|
+
this.startJiggleRetry();
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
- [ ] **Step 4: Wire into onSocketData() — check for clear sequence**
|
|
406
|
+
|
|
407
|
+
In `onSocketData()`, inside the `msg.type === "output"` branch, after `this.pushOutput(msg.data, ...)`, replace the `this.forceChildRedrawAfterLiveOutput()` call with:
|
|
408
|
+
```typescript
|
|
409
|
+
this.checkClearSequence(msg.data);
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
- [ ] **Step 5: finishAttachTransition() — do NOT cancel retry (updated post-review)**
|
|
413
|
+
|
|
414
|
+
**Update (final review, commit 806bf3e):** the retry chain intentionally survives the settle transition. Originally this step wired `cancelJiggleRetry()` into `finishAttachTransition()`, but review found that kills the chain at ~410ms in the silent cold-start case — the exact failure mode this feature fixes — leaving only retry 1 reachable. The chain now self-terminates on: clear detected / max retries / close(). Post-settle jiggles only fire when the child consumed all earlier jiggles (screen already stale), trading a brief full-render flicker for self-healing.
|
|
415
|
+
|
|
416
|
+
- [ ] **Step 6: Wire into close() — cancel retry**
|
|
417
|
+
|
|
418
|
+
In `close()`, add (alongside other cleanup):
|
|
419
|
+
```typescript
|
|
420
|
+
this.cancelJiggleRetry();
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
- [ ] **Step 7: Remove old one-shot logic**
|
|
424
|
+
|
|
425
|
+
Delete the `forcedRedrawAfterLiveOutput` field declaration.
|
|
426
|
+
|
|
427
|
+
Delete the `forceChildRedrawAfterLiveOutput()` method entirely.
|
|
428
|
+
|
|
429
|
+
Delete the `liveRedrawTimer` field declaration.
|
|
430
|
+
|
|
431
|
+
In `close()`, remove `this.liveRedrawTimer` cleanup if present (it was cleaned by `clearRedrawTimer()` which also clears `liveRedrawTimer` — verify and clean up).
|
|
432
|
+
|
|
433
|
+
- [ ] **Step 8: Verify TypeScript compiles**
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
cd /home/elling/git-repo/github/pi-agent-board/.claude/worktrees/issue-2-attach-double-cursor-jiggle-retry
|
|
437
|
+
npx tsc --noEmit
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Expected: No errors
|
|
441
|
+
|
|
442
|
+
- [ ] **Step 9: Run all existing tests**
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
cd /home/elling/git-repo/github/pi-agent-board/.claude/worktrees/issue-2-attach-double-cursor-jiggle-retry
|
|
446
|
+
node --test test/*.test.mjs
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Expected: All tests PASS (no regressions)
|
|
450
|
+
|
|
451
|
+
- [ ] **Step 10: Commit**
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
cd /home/elling/git-repo/github/pi-agent-board/.claude/worktrees/issue-2-attach-double-cursor-jiggle-retry
|
|
455
|
+
git add src/ui/pty-attach.ts
|
|
456
|
+
git commit -m "feat: integrate jiggle retry chain into attach component"
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
### Task 3: Final Verification — Full Test Suite + Lint
|
|
462
|
+
|
|
463
|
+
**Files:**
|
|
464
|
+
- No new files; verification only
|
|
465
|
+
|
|
466
|
+
- [ ] **Step 1: Run full test suite**
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
cd /home/elling/git-repo/github/pi-agent-board/.claude/worktrees/issue-2-attach-double-cursor-jiggle-retry
|
|
470
|
+
node --test test/*.test.mjs
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Expected: All PASS
|
|
474
|
+
|
|
475
|
+
- [ ] **Step 2: TypeScript check**
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
cd /home/elling/git-repo/github/pi-agent-board/.claude/worktrees/issue-2-attach-double-cursor-jiggle-retry
|
|
479
|
+
npx tsc --noEmit
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Expected: No errors
|
|
483
|
+
|
|
484
|
+
- [ ] **Step 3: Check for lint issues**
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
cd /home/elling/git-repo/github/pi-agent-board/.claude/worktrees/issue-2-attach-double-cursor-jiggle-retry
|
|
488
|
+
npx biome check src/core/pty-attach-jiggle-retry.mjs src/ui/pty-attach.ts test/pty-attach-jiggle-retry.test.mjs 2>&1 || true
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Expected: No new issues (fix if any)
|
|
492
|
+
|
|
493
|
+
- [ ] **Step 4: Final commit (if any fixes needed)**
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
cd /home/elling/git-repo/github/pi-agent-board/.claude/worktrees/issue-2-attach-double-cursor-jiggle-retry
|
|
497
|
+
git add -p # stage only relevant fixes
|
|
498
|
+
git commit -m "fix: address lint/type issues from final verification"
|
|
499
|
+
```
|