@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,366 @@
|
|
|
1
|
+
# Dashboard Keypress Lag Fix 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:** Eliminate perceptible ↑/↓ selection lag on the dashboard by moving prewarm off the keypress path, skipping artifact loads for archived views, and stopping forced PTY probes (issue #9).
|
|
6
|
+
|
|
7
|
+
**Architecture:** Three independent fixes: (A) a debounced prewarm scheduler so arrow keys only mutate selection and trigger a render; (B) an archived short-circuit in `listRows` so the 700ms poll (`reconcile()` + `refresh()` + `render()` each call it) stops reading ~10 artifact files per archived view; (C) `ensureHost` no longer forces `ptySupport({refresh:true})`, which spawned a probe process on every keypress when PTY support is broken.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node 20+, TypeScript (`.ts` UI) + ESM JavaScript (`.mjs` core), `node --test` test runner, no new dependencies.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- No new npm dependencies.
|
|
14
|
+
- All commits in this worktree branch `issue-9-dashboard-keypress-lag`; never touch `main` checkout.
|
|
15
|
+
- `git add` per-file; never `git add -A`.
|
|
16
|
+
- Commit messages in English, conventional-commits format, reference `issue #9`.
|
|
17
|
+
- `npm run verify` (typecheck + tests + pack dry-run) must pass before the PR.
|
|
18
|
+
- Core modules are `.mjs` with JSDoc types; UI is `.ts`. Follow existing file conventions.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### Task 1: `listRows` archived short-circuit (Fix B)
|
|
23
|
+
|
|
24
|
+
**Files:**
|
|
25
|
+
- Modify: `src/core/store.mjs` (function `listRows`, around line 220)
|
|
26
|
+
- Test: `test/store.test.mjs` (append new test)
|
|
27
|
+
|
|
28
|
+
**Interfaces:**
|
|
29
|
+
- Consumes: existing `readMeta(root, viewId)`, `loadRow(root, viewId)` from `src/core/store.mjs`.
|
|
30
|
+
- Produces: unchanged `listRows(root, opts)` signature and return type. Callers (`service.mjs` `reconcile()`/`rows()`/`pruneWarmHosts()`, `dashboard.ts` render) need no changes.
|
|
31
|
+
|
|
32
|
+
- [ ] **Step 1: Write the safety-net test**
|
|
33
|
+
|
|
34
|
+
Note: `readJson` swallows missing/corrupt files with a null fallback (`src/core/atomic.mjs`), so the archived short-circuit has no black-box failure mode — pre-fix and post-fix output is identical; the difference is pure IO volume, proven by the Task 4 real-store measurement. This test instead pins the behavioral contract that makes the short-circuit safe: archived views whose artifact files are absent/stale must not break `listRows`, and `includeArchived` semantics stay unchanged.
|
|
35
|
+
|
|
36
|
+
Append to `test/store.test.mjs` (imports `createView`, `listRows`, `writeMeta`, `P` are already at top; add `rmSync`/`writeFileSync`/`mkdirSync` to the existing `node:fs` import if missing):
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
test("listRows tolerates archived views with only meta.json", () => {
|
|
40
|
+
const root = freshRoot();
|
|
41
|
+
try {
|
|
42
|
+
const live = createView(root, { id: "live1", name: "live", cwd: "/r" });
|
|
43
|
+
for (const id of ["arch1", "arch2"]) {
|
|
44
|
+
const meta = createView(root, { id, name: id, cwd: "/r" });
|
|
45
|
+
meta.archived = true;
|
|
46
|
+
writeMeta(root, meta);
|
|
47
|
+
// Simulate the worst case the short-circuit must handle: archived view
|
|
48
|
+
// dirs containing nothing but meta.json (no state/evidence/host files).
|
|
49
|
+
rmSync(P.viewDir(root, id), { recursive: true, force: true });
|
|
50
|
+
mkdirSync(P.viewDir(root, id), { recursive: true });
|
|
51
|
+
writeFileSync(P.metaPath(root, id), JSON.stringify(meta));
|
|
52
|
+
}
|
|
53
|
+
const rows = listRows(root);
|
|
54
|
+
assert.equal(rows.length, 1);
|
|
55
|
+
assert.equal(rows[0].meta.id, live.id);
|
|
56
|
+
const all = listRows(root, { includeArchived: true });
|
|
57
|
+
assert.equal(all.length, 3);
|
|
58
|
+
assert.equal(all.filter((r) => r.meta.archived).length, 2);
|
|
59
|
+
} finally {
|
|
60
|
+
rmSync(root, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- [ ] **Step 2: Run test to confirm green baseline**
|
|
66
|
+
|
|
67
|
+
Run: `cd <worktree> && npm install --no-audit --no-fund 2>/dev/null; node --test test/store.test.mjs`
|
|
68
|
+
Expected: PASS pre-fix (safety net — see Step 1 note). This test must stay green after the implementation change.
|
|
69
|
+
|
|
70
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
71
|
+
|
|
72
|
+
In `src/core/store.mjs`, change `listRows`:
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
export function listRows(root, opts = {}) {
|
|
76
|
+
const roster = readRoster(root);
|
|
77
|
+
/** @type {Row[]} */
|
|
78
|
+
const rows = [];
|
|
79
|
+
for (const viewId of roster.views) {
|
|
80
|
+
// Archived short-circuit: archived rows are invisible on the dashboard, so
|
|
81
|
+
// never pay for their artifact files (state/evidence/host/diagnostics...).
|
|
82
|
+
// meta.json is the single authoritative source of the archived flag.
|
|
83
|
+
const meta = readMeta(root, viewId);
|
|
84
|
+
if (!meta) continue;
|
|
85
|
+
if (meta.archived && !opts.includeArchived) continue;
|
|
86
|
+
const row = loadRow(root, viewId);
|
|
87
|
+
if (!row) continue;
|
|
88
|
+
if (row.meta.archived && !opts.includeArchived) continue;
|
|
89
|
+
rows.push(row);
|
|
90
|
+
}
|
|
91
|
+
return rows;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
(The second archived check is belt-and-suspenders in case `loadRow` re-reads a meta that changed between reads.)
|
|
96
|
+
|
|
97
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
98
|
+
|
|
99
|
+
Run: `node --test test/store.test.mjs`
|
|
100
|
+
Expected: PASS, all tests in file green.
|
|
101
|
+
|
|
102
|
+
- [ ] **Step 5: Commit**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
git add src/core/store.mjs test/store.test.mjs
|
|
106
|
+
git commit -m "perf: skip artifact loading for archived views in listRows (issue #9)"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### Task 2: Debounced prewarm off the keypress path (Fix A)
|
|
112
|
+
|
|
113
|
+
**Files:**
|
|
114
|
+
- Create: `src/core/prewarm-schedule.mjs`
|
|
115
|
+
- Create: `test/prewarm-schedule.test.mjs`
|
|
116
|
+
- Modify: `src/ui/dashboard.ts` (fields near line 127, `moveSelection` ~L237, `refresh` ~L176, `dispose` ~L1086)
|
|
117
|
+
|
|
118
|
+
**Interfaces:**
|
|
119
|
+
- Produces: `createPrewarmScheduler(prewarm: () => void, delayMs?: number): { schedule(): void; cancel(): void }` — single-flight debounce; repeated `schedule()` within `delayMs` fire `prewarm()` exactly once after the last call; `cancel()` clears the pending timer.
|
|
120
|
+
- Consumes (dashboard.ts): existing private `prewarmSelected()`.
|
|
121
|
+
|
|
122
|
+
- [ ] **Step 1: Write the failing test**
|
|
123
|
+
|
|
124
|
+
`test/prewarm-schedule.test.mjs`:
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import assert from "node:assert/strict";
|
|
128
|
+
import { test } from "node:test";
|
|
129
|
+
import { createPrewarmScheduler } from "../src/core/prewarm-schedule.mjs";
|
|
130
|
+
|
|
131
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
132
|
+
|
|
133
|
+
test("rapid schedules fire prewarm once after quiet period", async () => {
|
|
134
|
+
let fired = 0;
|
|
135
|
+
const s = createPrewarmScheduler(() => { fired += 1; }, 15);
|
|
136
|
+
s.schedule();
|
|
137
|
+
s.schedule();
|
|
138
|
+
s.schedule();
|
|
139
|
+
await sleep(5);
|
|
140
|
+
assert.equal(fired, 0); // still debouncing
|
|
141
|
+
await sleep(25);
|
|
142
|
+
assert.equal(fired, 1); // exactly once
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("cancel prevents prewarm entirely", async () => {
|
|
146
|
+
let fired = 0;
|
|
147
|
+
const s = createPrewarmScheduler(() => { fired += 1; }, 10);
|
|
148
|
+
s.schedule();
|
|
149
|
+
s.cancel();
|
|
150
|
+
await sleep(30);
|
|
151
|
+
assert.equal(fired, 0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("schedule after cancel works again", async () => {
|
|
155
|
+
let fired = 0;
|
|
156
|
+
const s = createPrewarmScheduler(() => { fired += 1; }, 10);
|
|
157
|
+
s.schedule();
|
|
158
|
+
s.cancel();
|
|
159
|
+
s.schedule();
|
|
160
|
+
await sleep(30);
|
|
161
|
+
assert.equal(fired, 1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("prewarm errors do not crash the scheduler", async () => {
|
|
165
|
+
let fired = 0;
|
|
166
|
+
const s = createPrewarmScheduler(() => { fired += 1; throw new Error("boom"); }, 5);
|
|
167
|
+
s.schedule();
|
|
168
|
+
await sleep(20);
|
|
169
|
+
assert.equal(fired, 1);
|
|
170
|
+
s.schedule();
|
|
171
|
+
await sleep(20);
|
|
172
|
+
assert.equal(fired, 2);
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
177
|
+
|
|
178
|
+
Run: `node --test test/prewarm-schedule.test.mjs`
|
|
179
|
+
Expected: FAIL — cannot find module `../src/core/prewarm-schedule.mjs`.
|
|
180
|
+
|
|
181
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
182
|
+
|
|
183
|
+
`src/core/prewarm-schedule.mjs`:
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
/**
|
|
187
|
+
* Single-flight debounce for dashboard prewarm.
|
|
188
|
+
*
|
|
189
|
+
* Arrow-key navigation must move the selection and repaint immediately; host
|
|
190
|
+
* prewarm (which may spawn a PTY host and re-scan rows) is deferred so bursts
|
|
191
|
+
* of keypresses trigger exactly one prewarm for the final resting selection.
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {() => void} prewarm Invoked (with errors swallowed) once scheduling
|
|
196
|
+
* goes quiet for `delayMs`. Re-reads current state at fire time.
|
|
197
|
+
* @param {number} [delayMs=200]
|
|
198
|
+
* @returns {{ schedule: () => void, cancel: () => void }}
|
|
199
|
+
*/
|
|
200
|
+
export function createPrewarmScheduler(prewarm, delayMs = 200) {
|
|
201
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
202
|
+
let timer = null;
|
|
203
|
+
const fire = () => {
|
|
204
|
+
timer = null;
|
|
205
|
+
try {
|
|
206
|
+
prewarm();
|
|
207
|
+
} catch {
|
|
208
|
+
/* prewarm is best-effort; never break navigation */
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
schedule() {
|
|
213
|
+
if (timer !== null) clearTimeout(timer);
|
|
214
|
+
timer = setTimeout(fire, delayMs);
|
|
215
|
+
},
|
|
216
|
+
cancel() {
|
|
217
|
+
if (timer !== null) {
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
timer = null;
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
227
|
+
|
|
228
|
+
Run: `node --test test/prewarm-schedule.test.mjs`
|
|
229
|
+
Expected: PASS (4 tests).
|
|
230
|
+
|
|
231
|
+
- [ ] **Step 5: Wire into DashboardComponent**
|
|
232
|
+
|
|
233
|
+
In `src/ui/dashboard.ts`:
|
|
234
|
+
|
|
235
|
+
1. Import: add near the other `../core/` imports:
|
|
236
|
+
`import { createPrewarmScheduler } from "../core/prewarm-schedule.mjs";`
|
|
237
|
+
2. Add field (near `private prewarmedId: string | null = null;`):
|
|
238
|
+
`private readonly prewarmScheduler = createPrewarmScheduler(() => this.prewarmSelected(), 200);`
|
|
239
|
+
3. `moveSelection()`: replace `this.prewarmSelected();` with `this.prewarmScheduler.schedule();`
|
|
240
|
+
4. `refresh()` (line ~176): replace `if (this.selectedId && this.selectedId !== previousSelected) this.prewarmSelected();` with `if (this.selectedId && this.selectedId !== previousSelected) this.prewarmScheduler.schedule();`
|
|
241
|
+
5. `prewarmSelected()`: add a mode guard as the first line so a timer firing after the user entered peek/session/launch modes does not prewarm the wrong target:
|
|
242
|
+
`if (this.mode !== "list" && this.mode !== "select") return;`
|
|
243
|
+
6. `dispose()`: replace the comment-only body with `this.prewarmScheduler.cancel();` (keep the existing comment about poll interval ownership).
|
|
244
|
+
|
|
245
|
+
- [ ] **Step 6: Typecheck + full tests**
|
|
246
|
+
|
|
247
|
+
Run: `npx tsc --noEmit && node --test test/*.test.mjs`
|
|
248
|
+
Expected: no type errors; all tests green.
|
|
249
|
+
|
|
250
|
+
- [ ] **Step 7: Commit**
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
git add src/core/prewarm-schedule.mjs test/prewarm-schedule.test.mjs src/ui/dashboard.ts
|
|
254
|
+
git commit -m "perf: debounce dashboard prewarm off the keypress path (issue #9)"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### Task 3: `ensureHost` PTY probe TTL instead of forced refresh (Fix C)
|
|
260
|
+
|
|
261
|
+
**Files:**
|
|
262
|
+
- Modify: `src/runtime/service.mjs` (`ensureHost`, ~L674: `const pty = ptySupport({ refresh: true });`)
|
|
263
|
+
- Test: `test/service.test.mjs` (append new test)
|
|
264
|
+
|
|
265
|
+
**Interfaces:**
|
|
266
|
+
- Consumes: existing injected `ptySupport(opts)` option of `createService`; existing test helper `service(root, overrides)` in `test/service.test.mjs`.
|
|
267
|
+
- Produces: unchanged `ensureHost` return contract; only probe option semantics change (default = cached/TTL instead of forced refresh).
|
|
268
|
+
|
|
269
|
+
- [ ] **Step 1: Write the failing test**
|
|
270
|
+
|
|
271
|
+
Append to `test/service.test.mjs` (uses `createView`, `writeState`, `readState` imports already present; add `writeFileSync` if not imported):
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
test("ensureHost probes PTY support with TTL cache, not forced refresh", () => {
|
|
275
|
+
const root = freshRoot();
|
|
276
|
+
try {
|
|
277
|
+
process.env.AGENT_BOARD_FORCE_PTY = "1";
|
|
278
|
+
const meta = createView(root, { id: "v1", name: "a", cwd: "/r" });
|
|
279
|
+
writeFileSync(meta.sessionFile, JSON.stringify({ type: "session", id: "s1", cwd: "/r" }) + "\n");
|
|
280
|
+
const probeCalls = [];
|
|
281
|
+
const svc = service(root, {
|
|
282
|
+
ptySupport: (opts = {}) => { probeCalls.push(opts); return { ok: true }; },
|
|
283
|
+
launchHost: () => ({ pid: process.pid, configPath: "/no/host-config.json" }),
|
|
284
|
+
});
|
|
285
|
+
const res = svc.ensureHost("v1");
|
|
286
|
+
assert.equal(res.ok, true);
|
|
287
|
+
assert.ok(probeCalls.length >= 1);
|
|
288
|
+
for (const opts of probeCalls) {
|
|
289
|
+
assert.notEqual(opts?.refresh, true, "ensureHost must not force ptySupport refresh");
|
|
290
|
+
}
|
|
291
|
+
} finally {
|
|
292
|
+
delete process.env.AGENT_BOARD_FORCE_PTY;
|
|
293
|
+
rmSync(root, { recursive: true, force: true });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
299
|
+
|
|
300
|
+
Run: `node --test test/service.test.mjs`
|
|
301
|
+
Expected: new test FAILS — `ensureHost` currently calls `ptySupport({ refresh: true })` so `opts.refresh === true`.
|
|
302
|
+
|
|
303
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
304
|
+
|
|
305
|
+
In `src/runtime/service.mjs` `ensureHost`, change:
|
|
306
|
+
|
|
307
|
+
```js
|
|
308
|
+
const pty = ptySupport({ refresh: true });
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
to:
|
|
312
|
+
|
|
313
|
+
```js
|
|
314
|
+
// Default probe semantics: success is cached for the process lifetime and a
|
|
315
|
+
// failed probe retries on a short TTL. Forcing refresh here would spawn a
|
|
316
|
+
// probe process on every keypress-driven prewarm when PTY support is broken.
|
|
317
|
+
const pty = ptySupport();
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
321
|
+
|
|
322
|
+
Run: `node --test test/service.test.mjs`
|
|
323
|
+
Expected: PASS, all tests in file green.
|
|
324
|
+
|
|
325
|
+
- [ ] **Step 5: Commit**
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
git add src/runtime/service.mjs test/service.test.mjs
|
|
329
|
+
git commit -m "fix: stop forcing PTY probe refresh on every ensureHost (issue #9)"
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
### Task 4: Full verification + performance evidence
|
|
335
|
+
|
|
336
|
+
**Files:**
|
|
337
|
+
- No source changes expected (fix-only task; measurement script is throwaway, not committed).
|
|
338
|
+
|
|
339
|
+
**Interfaces:**
|
|
340
|
+
- Consumes: all previous tasks merged on the branch.
|
|
341
|
+
|
|
342
|
+
- [ ] **Step 1: Install deps and run full verify**
|
|
343
|
+
|
|
344
|
+
Run: `cd <worktree> && npm install --no-audit --no-fund && npm run verify`
|
|
345
|
+
Expected: `tsc --noEmit` clean, all 27+ test files pass, `npm pack --dry-run` succeeds.
|
|
346
|
+
|
|
347
|
+
- [ ] **Step 2: Measure real-store improvement (read-only)**
|
|
348
|
+
|
|
349
|
+
Run against the user's real store (read-only, no mutation):
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
node --input-type=module -e "
|
|
353
|
+
import { listRows } from './src/core/store.mjs';
|
|
354
|
+
import os from 'node:os'; import path from 'node:path';
|
|
355
|
+
const root = path.join(os.homedir(), '.pi/agent/agent-board');
|
|
356
|
+
listRows(root);
|
|
357
|
+
const t0 = performance.now(); listRows(root); const t1 = performance.now();
|
|
358
|
+
console.log('listRows post-fix:', (t1 - t0).toFixed(1), 'ms');
|
|
359
|
+
"
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Expected: ≤ 40ms on the 168-view store (was 180–204ms). Record the number.
|
|
363
|
+
|
|
364
|
+
- [ ] **Step 3: Report numbers**
|
|
365
|
+
|
|
366
|
+
Paste before/after into the PR body and as an issue #9 comment. No commit needed for this task.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Spec: screen.log Startup GC(startup 回收 ended view 的 screen.log)
|
|
2
|
+
|
|
3
|
+
- Issue: zhuxixi/pi-agent-board#1
|
|
4
|
+
- Date: 2026-08-15
|
|
5
|
+
- Status: draft(待 review)
|
|
6
|
+
|
|
7
|
+
## Background(来自调研,证据已评论到 issue)
|
|
8
|
+
|
|
9
|
+
上游 PR #41(commit `ee3780a`,2026-08-03 合入)已给**写路径**加上界:
|
|
10
|
+
`appendBoundedScreenLog()` 超过 `SCREEN_LOG_MAX_BYTES`(5 MB)时原子 compact 成
|
|
11
|
+
100 KB tail;runner 启动时 `reconcileScreenLog()` 压存量。本机验证(2026-08-15):
|
|
12
|
+
活跃 view 的 screen.log 最大 3.9 MB,cap 工作正常。
|
|
13
|
+
|
|
14
|
+
**残留缺口(本 spec 的 scope)**:
|
|
15
|
+
|
|
16
|
+
1. ended view 的 screen.log 无人回收——`reconcileScreenLog` 只在该 view 的
|
|
17
|
+
pty-runner 重启时触发;session 结束后 runner 不再运行,文件永久留存。
|
|
18
|
+
本机 views 目录仍有 1.6 GB 历史存量。
|
|
19
|
+
2. 全仓无 retention/prune/GC 机制(ended view 目录永久保留)。
|
|
20
|
+
3. `SCREEN_LOG_MAX_BYTES` / `SCREEN_LOG_REPLAY_BYTES` 硬编码,不可配。
|
|
21
|
+
|
|
22
|
+
## Goals
|
|
23
|
+
|
|
24
|
+
- Dashboard 启动时异步回收:ended 超龄 view 的 `screen.log` 被删除(unlink)。
|
|
25
|
+
- 两个配置项写入 `launch-prefs.json` 即生效:
|
|
26
|
+
- `screenLogRetentionDays`(默认 7;**0 = 关闭 GC**)
|
|
27
|
+
- `screenLogMaxSize`(默认 5 MB;透传给 pty-runner 写路径的 cap)
|
|
28
|
+
- 全程容错:GC 任何失败不得影响 dashboard 或 runner。
|
|
29
|
+
|
|
30
|
+
## Non-goals
|
|
31
|
+
|
|
32
|
+
- 不做 log 轮转(screen.log → screen.log.1)。写路径已有 cap + tail compact,轮转与其重叠(YAGNI)。
|
|
33
|
+
- 不做压缩(.gz)。
|
|
34
|
+
- 不删整个 view 目录;保留 `meta.json` / `state.json` / `evidence.json` 等 KB 级文件,dashboard 历史行不丢。
|
|
35
|
+
- 不动 job-runner 路径(它不写 screen.log)。
|
|
36
|
+
- 不引入防抖/状态持久化(扫描成本可忽略,每次启动都跑)。
|
|
37
|
+
|
|
38
|
+
## Design
|
|
39
|
+
|
|
40
|
+
### 新模块:`src/core/screen-log-gc.mjs`
|
|
41
|
+
|
|
42
|
+
单一职责,导出 `pruneScreenLogs(root, opts)`:
|
|
43
|
+
|
|
44
|
+
- 扫描 `<root>/views/view_*/`。
|
|
45
|
+
- 对每个 view,判定清理条件(全部满足才删):
|
|
46
|
+
1. `screen.log` 存在且非空;
|
|
47
|
+
2. view 已结束——`host.json` 的 `endedAt` 非 null(首选依据),
|
|
48
|
+
或 `host.state !== "alive"`;host.json 缺失/损坏时 fallback 用
|
|
49
|
+
screen.log 的 mtime 判定年龄;
|
|
50
|
+
3. 结束时间(或 mtime)早于 `now - retentionDays`。
|
|
51
|
+
- 满足条件 → `unlinkSync(screenLog)`。失败静默(单文件失败不中断整体扫描)。
|
|
52
|
+
- **活跃 view 一律跳过**:runner 持有 `screenLogBytes` 内存计数,外部动活跃
|
|
53
|
+
文件引入 race;活跃 view 由 `appendBoundedScreenLog` 的 cap 自管。
|
|
54
|
+
- fs 操作沿用 `screen-log.mjs` 的可注入 fs 模式(defaultScreenLogFs),便于单测。
|
|
55
|
+
- 返回统计 `{ scanned, removed, skippedActive, skippedFresh, bytesReclaimed, errors }`,
|
|
56
|
+
供可选的 diagnostics 记录。
|
|
57
|
+
|
|
58
|
+
### 触发点:`src/runtime/service.mjs`
|
|
59
|
+
|
|
60
|
+
- service 初始化时 fire-and-forget 调 `pruneScreenLogs()`(不 await、不阻塞首帧;
|
|
61
|
+
几百个 view 的 stat 扫描 <10ms,实际删除走 unlink,足够快)。
|
|
62
|
+
- 整个调用包 try/catch,异常静默;可选把统计 append 到 diagnostics.jsonl。
|
|
63
|
+
|
|
64
|
+
### 配置读取:`launch-prefs.json`
|
|
65
|
+
|
|
66
|
+
新增两个字段(缺省用默认值,向后兼容旧 prefs 文件):
|
|
67
|
+
|
|
68
|
+
| 字段 | 类型 | 默认 | 语义 |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| `screenLogRetentionDays` | number | 7 | ended 超过该天数的 view 清理 screen.log;0 = 关闭 GC |
|
|
71
|
+
| `screenLogMaxSize` | number | 5_000_000 | pty-runner 写路径 cap(字节),透传进 HostConfig → `appendBoundedScreenLog(opts.maxBytes)` |
|
|
72
|
+
|
|
73
|
+
- 非法值(负数、NaN、非数)回退默认值。
|
|
74
|
+
- `screenLogMaxSize` 传递链路:service 读 prefs → 写入 pty-runner 的 HostConfig
|
|
75
|
+
JSON → runner `appendBoundedScreenLog(screenLog, data, bytes, { maxBytes })`。
|
|
76
|
+
`appendBoundedScreenLog` 已支持 `opts.maxBytes`,runner 侧只需读取并透传。
|
|
77
|
+
|
|
78
|
+
### Error handling
|
|
79
|
+
|
|
80
|
+
- GC 整体 fire-and-forget + try/catch;单 view 失败跳过继续。
|
|
81
|
+
- host.json 读取失败 → mtime fallback;两者都失败 → 跳过该 view。
|
|
82
|
+
- prefs 读取失败 → 全默认值(不影响现有 launch-prefs 逻辑)。
|
|
83
|
+
|
|
84
|
+
### Testing
|
|
85
|
+
|
|
86
|
+
新增 `test/screen-log-gc.test.mjs`(沿用现有 test 风格与可注入 fs):
|
|
87
|
+
|
|
88
|
+
1. ended 超龄 view → screen.log 被删,meta/state 等文件保留。
|
|
89
|
+
2. 活跃 view(host.json `state: "alive"` 且 endedAt null)→ 不删。
|
|
90
|
+
3. ended 但未超龄 → 不删。
|
|
91
|
+
4. `screenLogRetentionDays: 0` → 全部不删。
|
|
92
|
+
5. host.json 缺失 → mtime fallback 正确判定。
|
|
93
|
+
6. `screenLogMaxSize` 经 HostConfig 透传到 runner cap(runner 侧单测或链路断言)。
|
|
94
|
+
7. 单文件 unlink 失败不中断其他 view 的清理。
|
|
95
|
+
|
|
96
|
+
## Data flow
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
/dashboard 打开
|
|
100
|
+
└─ service init
|
|
101
|
+
└─ (async, fire-and-forget) pruneScreenLogs(root, prefs)
|
|
102
|
+
├─ scan views/view_*/
|
|
103
|
+
├─ per view: ended? aged? → unlink screen.log
|
|
104
|
+
└─ return stats → (optional) diagnostics.jsonl
|
|
105
|
+
```
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Issue #2 Spec:attach 双光标 — jiggle 重试直到见到清屏
|
|
2
|
+
|
|
3
|
+
## 日期
|
|
4
|
+
2026-08-16
|
|
5
|
+
|
|
6
|
+
## 问题
|
|
7
|
+
attach 后出现双光标(PTY 光标块与编辑器假光标失同步),因为 resize jiggle 在冷启动/streaming 场景下失效,重放脏帧未被清屏自愈。
|
|
8
|
+
|
|
9
|
+
## 根因(已在 issue 中确认)
|
|
10
|
+
1. `forceChildRedraw()` 发 jiggle 后无确认/重试机制
|
|
11
|
+
2. `forceChildRedrawAfterLiveOutput()` 是 one-shot 补偿,触发后不再触发
|
|
12
|
+
3. 冷启动时 SIGWINCH 被丢弃 / streaming 时 jiggle 被渲染节流合并
|
|
13
|
+
|
|
14
|
+
## 修复方案:jiggle 重试直到见到清屏
|
|
15
|
+
|
|
16
|
+
### 设计原则:可测性优先
|
|
17
|
+
把重试逻辑抽成**纯状态机**(不依赖 socket/xterm/timer),`pty-attach.ts` 只做胶水。这样核心逻辑 100% 可单测,验收标准 = 单测全通过。
|
|
18
|
+
|
|
19
|
+
### 模块拆分
|
|
20
|
+
|
|
21
|
+
#### 1. 纯逻辑层:`src/core/pty-attach-jiggle-retry.mjs`
|
|
22
|
+
|
|
23
|
+
**`createJiggleRetryState()`** — 创建重试状态机,返回一个不可变状态对象 + 操作函数集。
|
|
24
|
+
|
|
25
|
+
状态机接口(纯函数,无副作用):
|
|
26
|
+
```typescript
|
|
27
|
+
interface JiggleRetryState {
|
|
28
|
+
/** 当前重试到第几轮(0 = 未开始) */
|
|
29
|
+
retryIndex: number;
|
|
30
|
+
/** 是否已检测到清屏 */
|
|
31
|
+
clearDetected: boolean;
|
|
32
|
+
/** 是否已停止(见过清屏/超限/手动停止) */
|
|
33
|
+
stopped: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 退避间隔表 */
|
|
37
|
+
const BACKOFF_MS: readonly number[]; // [120, 500, 1500, 3000]
|
|
38
|
+
|
|
39
|
+
/** 创建初始状态 */
|
|
40
|
+
function createJiggleRetryState(): JiggleRetryState;
|
|
41
|
+
|
|
42
|
+
/** 处理一段 output 数据,返回新状态 + 是否检测到清屏 */
|
|
43
|
+
function feedOutput(state: JiggleRetryState, data: string, carry: string): {
|
|
44
|
+
state: JiggleRetryState;
|
|
45
|
+
carry: string; // 更新后的跨 chunk carry
|
|
46
|
+
clearFound: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** 获取下一次重试的延迟(ms),返回 null 表示不再重试 */
|
|
50
|
+
function nextRetryDelay(state: JiggleRetryState): number | null;
|
|
51
|
+
|
|
52
|
+
/** 推进到下一轮重试 */
|
|
53
|
+
function advanceRetry(state: JiggleRetryState): JiggleRetryState;
|
|
54
|
+
|
|
55
|
+
/** 手动停止(attach 安定/close) */
|
|
56
|
+
function stopRetry(state: JiggleRetryState): JiggleRetryState;
|
|
57
|
+
|
|
58
|
+
/** 检测数据中是否包含全清序列 \x1b[2J(处理跨 chunk) */
|
|
59
|
+
function hasFullClearSequence(data: string): boolean;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### 2. 胶水层:`src/ui/pty-attach.ts` 变更
|
|
63
|
+
|
|
64
|
+
新增私有字段:
|
|
65
|
+
```typescript
|
|
66
|
+
private jiggleRetryState: JiggleRetryState | null = null;
|
|
67
|
+
private jiggleRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
68
|
+
private clearCarry = "";
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
新增私有方法:
|
|
72
|
+
```typescript
|
|
73
|
+
/** 启动 jiggle 重试链(connect 成功后调用) */
|
|
74
|
+
private startJiggleRetry(): void;
|
|
75
|
+
|
|
76
|
+
/** 处理 socket output 时检测清屏,命中则取消重试 */
|
|
77
|
+
private checkClearSequence(data: string): void;
|
|
78
|
+
|
|
79
|
+
/** 安排下一次重试 */
|
|
80
|
+
private scheduleNextJiggle(): void;
|
|
81
|
+
|
|
82
|
+
/** 取消重试(close 时调用;settle 时故意不取消,见生命周期) */
|
|
83
|
+
private cancelJiggleRetry(): void;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
修改点:
|
|
87
|
+
- `connect()` 成功后 → 调 `startJiggleRetry()`
|
|
88
|
+
- `onSocketData()` 处理 output 时 → 调 `checkClearSequence(data)`
|
|
89
|
+
- `close()` → 调 `cancelJiggleRetry()`
|
|
90
|
+
- **移除** `forceChildRedrawAfterLiveOutput()` 和 `forcedRedrawAfterLiveOutput` 字段(被重试链取代)
|
|
91
|
+
|
|
92
|
+
**设计变更(final review 后,commit 806bf3e)**:`finishAttachTransition()` **不再取消**重试链——settle 取消会让静默冷启动场景下只剩 retry 1 可达(链 ~410ms 就死),正是本 feature 要修的场景。链的自然终止条件:检测到清屏 / 达到 4 次上限 / close()。settle 后的残余 jiggle 只在子端吞掉所有此前 jiggle 时发生(此时画面本就脏),用一次全屏重绘闪烁换自愈是值得的;健康 session 下首个 jiggle 的清屏就会被检测到,链在 settle 前已停。
|
|
93
|
+
|
|
94
|
+
### 数据流
|
|
95
|
+
```
|
|
96
|
+
connect() → forceChildRedraw() → 发 jiggle → startJiggleRetry()
|
|
97
|
+
↓
|
|
98
|
+
onSocketData(output) → checkClearSequence(data) ↓
|
|
99
|
+
↓ ↓ ↓
|
|
100
|
+
↓ clearFound? → YES → cancelJiggleRetry()
|
|
101
|
+
↓ ↓ NO ↓
|
|
102
|
+
↓ scheduleNextJiggle() ←────────┘
|
|
103
|
+
↓ ↓
|
|
104
|
+
↓ setTimeout(BACKOFF_MS[i]) → forceChildRedraw() → advanceRetry()
|
|
105
|
+
↓ ↓
|
|
106
|
+
↓ (循环直到 clearFound / 超限 / 手动停止)
|
|
107
|
+
↓
|
|
108
|
+
finishAttachTransition() / close() → cancelJiggleRetry()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 测试策略(验收标准)
|
|
112
|
+
|
|
113
|
+
**文件:`test/pty-attach-jiggle-retry.test.mjs`**
|
|
114
|
+
|
|
115
|
+
| 测试场景 | 验证点 |
|
|
116
|
+
|----------|--------|
|
|
117
|
+
| 初始状态 | retryIndex=0, clearDetected=false, stopped=false |
|
|
118
|
+
| 输入含完整 `\x1b[2J` | clearFound=true, clearDetected=true |
|
|
119
|
+
| 输入含 `\x1b[2J` + 其他数据 | clearFound=true |
|
|
120
|
+
| 输入无 `\x1b[2J` | clearFound=false, clearDetected=false |
|
|
121
|
+
| 跨 chunk:`\x1b[` + `2J` | 第二次 feed 时 clearFound=true |
|
|
122
|
+
| 跨 chunk:`\x1b` + `[2J` | 第二次 feed 时 clearFound=true |
|
|
123
|
+
| 部分匹配 `\x1b[3J` | clearFound=false |
|
|
124
|
+
| 退避间隔 | nextRetryDelay 依次返回 120/500/1500/3000 |
|
|
125
|
+
| 达到上限 | 第 5 次 nextRetryDelay 返回 null |
|
|
126
|
+
| 见到清屏后 nextRetryDelay | 返回 null(不再重试) |
|
|
127
|
+
| 手动停止后 nextRetryDelay | 返回 null |
|
|
128
|
+
| advanceRetry 推进 | retryIndex 递增 |
|
|
129
|
+
| stopRetry 后置位 | stopped=true |
|
|
130
|
+
|
|
131
|
+
### 非目标
|
|
132
|
+
- 不改 pi-tui 渲染逻辑
|
|
133
|
+
- 不改 screen.log 重放策略
|
|
134
|
+
- 不做光标失同步检测加固(issue 优先级 2,后续 follow-up)
|
|
135
|
+
- 不改 attach 安定/loading banner 逻辑
|
|
136
|
+
|
|
137
|
+
### 文件变更
|
|
138
|
+
| 文件 | 变更类型 | 说明 |
|
|
139
|
+
|------|----------|------|
|
|
140
|
+
| `src/core/pty-attach-jiggle-retry.mjs` | 新增 | 纯逻辑状态机 |
|
|
141
|
+
| `src/ui/pty-attach.ts` | 修改 | 集成重试链,移除 one-shot 补偿 |
|
|
142
|
+
| `test/pty-attach-jiggle-retry.test.mjs` | 新增 | 状态机全场景测试 |
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Issue #9 Spec — dashboard 方向键卡顿修复
|
|
2
|
+
|
|
3
|
+
状态:待用户确认
|
|
4
|
+
类型:bug/perf(根因已完成,见 issue #9 正文与调研评论)
|
|
5
|
+
|
|
6
|
+
## 目标
|
|
7
|
+
|
|
8
|
+
1. 按方向键的选中移动延迟降到感知阈值内(<16ms):按键处理本身不做磁盘 IO、不 spawn 进程
|
|
9
|
+
2. 轮询周期内主线程同步 IO 阻塞显著下降(目标:单周期 <60ms,当前 ~400ms)
|
|
10
|
+
3. 不改变 attach 预热的行为收益(选中后仍会预热,只是延后)
|
|
11
|
+
|
|
12
|
+
## 非目标
|
|
13
|
+
|
|
14
|
+
- store 全面异步化(fs/promises 重写)——不做,风险大收益边际
|
|
15
|
+
- mtime 缓存层——归档短路后若实测仍慢再立项
|
|
16
|
+
- screen.log 体积治理——issue #1 已完成
|
|
17
|
+
- 渲染器/GPU 相关改动——已证实无关
|
|
18
|
+
|
|
19
|
+
## 设计
|
|
20
|
+
|
|
21
|
+
### Fix A:按键先响应,prewarm 延迟 + 防抖(src/ui/dashboard.ts)
|
|
22
|
+
|
|
23
|
+
- `moveSelection()`:只改 `selectedId` 并立刻触发渲染(现有 invalidate 链),移除同步 `prewarmSelected()` 调用
|
|
24
|
+
- `prewarmSelected()` 改为防抖调度:单例 timer,~200ms 无新移动才执行;新按键重置 timer(连续移动只预热最终落点,顺带消除连环 spawn/terminate host)
|
|
25
|
+
- 清理点:`dispose()`、模式切换、组件卸载时 clearTimeout
|
|
26
|
+
|
|
27
|
+
### Fix B:listRows 归档短路(src/core/store.mjs)
|
|
28
|
+
|
|
29
|
+
- `listRows(root, opts)`:循环内先 `readMeta()`,若 `meta.archived && !opts.includeArchived` 则 `continue`,不再进入 `loadRow()` 的完整 artifact 加载(state/evidence/host/diagnostics/queue/steering)
|
|
30
|
+
- `loadRow()` 本身不动(单行加载语义保持,service 内部按 id 操作仍走它)
|
|
31
|
+
- 效果预估:artifact 读取从 168 行降到 11 行(本机),单次 listRows ~204ms → ~20-30ms
|
|
32
|
+
- `pruneWarmHosts`、`reconcile()` 内的 listRows 自动受益,无需改动
|
|
33
|
+
|
|
34
|
+
### Fix C:ensureHost 探测降级为 TTL(src/runtime/service.mjs)
|
|
35
|
+
|
|
36
|
+
- `ensureHost` 的 `ptySupport({ refresh: true })` 改为默认参数(失败态 2s TTL,成功态进程生命周期缓存)
|
|
37
|
+
- 依据:#37 已确立"成功探测必须缓存、失败探测短 TTL"纪律;refresh:true 在 PTY 故障环境下会造成按键路径每键 spawn
|
|
38
|
+
|
|
39
|
+
## 决策表
|
|
40
|
+
|
|
41
|
+
| 决策 | 选择 | 理由 |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| prewarm 延迟方式 | setTimeout 防抖 200ms | setImmediate 会在同帧渲染前抢跑;200ms 让连续导航只预热落点 |
|
|
44
|
+
| 归档过滤位置 | listRows 内、readMeta 后 | 最小改动点;roster 不存 archived 标志,meta 是唯一权威来源 |
|
|
45
|
+
| 防抖期间用户已 attach | timer 触发时再查模式,非 list/已切换则跳过 | 防止陈旧预热 |
|
|
46
|
+
| evidence 按需加载 | 本期不做 | 归档短路后 11 行 evidence 读取 ~9ms,不构成瓶颈 |
|
|
47
|
+
|
|
48
|
+
## 测试计划
|
|
49
|
+
|
|
50
|
+
- 单测(store):临时目录构造 N 个 view,归档行**缺失** state/evidence/host 文件 → `listRows()` 不抛错、只返回非归档行(证明短路生效);`includeArchived: true` 行为不变
|
|
51
|
+
- 单测(dashboard):模拟连续 moveSelection,断言 prewarm 仅在静默 200ms 后触发一次(注入 fake service 计数)
|
|
52
|
+
- 存量测试全绿(`npm test`)
|
|
53
|
+
- 性能验证(文档化,非 CI 断言):本机真实 store 上 listRows 前后耗时对比 + 按键-渲染延迟对比,数据回贴 issue
|
|
54
|
+
|
|
55
|
+
## 降级/风险
|
|
56
|
+
|
|
57
|
+
- 防抖延迟 prewarm 200ms:attach 预热收益不受影响(用户从选中到按 enter 远大于 200ms)
|
|
58
|
+
- 归档短路只影响 `listRows`;service 单行路径(`loadRow`)不变,无行为回归面
|
|
59
|
+
- Fix C 唯一语义变化:手动修复 PTY 权限后最多 2s 才重试(原为立即)——可接受
|
package/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-discovery entry point. Pi loads `<extensions-dir>/<name>/index.ts`, so this
|
|
3
|
+
* re-exports the real extension from `src/index.ts`. Symlink this repo into
|
|
4
|
+
* `~/.pi/agent/extensions/agent-board` (or add its path to settings.json `extensions`).
|
|
5
|
+
*/
|
|
6
|
+
export { default } from "./src/index.ts";
|