@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.
Files changed (65) hide show
  1. package/IMPLEMENTATION_PLAN.md +920 -0
  2. package/LICENSE +21 -0
  3. package/PRD.md +484 -0
  4. package/PROGRESS.md +127 -0
  5. package/README.md +131 -0
  6. package/VERIFY.md +113 -0
  7. package/docs/BATCH_SELECTION_READ_FLOW.md +277 -0
  8. package/docs/EXPLORATION.md +187 -0
  9. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +579 -0
  10. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +704 -0
  11. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +499 -0
  12. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +366 -0
  13. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +105 -0
  14. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +142 -0
  15. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +59 -0
  16. package/index.ts +6 -0
  17. package/package.json +81 -0
  18. package/runner/job-runner.mjs +420 -0
  19. package/runner/pty-runner.mjs +310 -0
  20. package/runner/state-runner.mjs +120 -0
  21. package/runner/title-runner.mjs +80 -0
  22. package/scripts/patch-vulns.mjs +59 -0
  23. package/src/commands/agent-board.ts +318 -0
  24. package/src/commands/attach-flow.ts +231 -0
  25. package/src/commands/bg.ts +70 -0
  26. package/src/core/atomic.mjs +145 -0
  27. package/src/core/auto-state.mjs +320 -0
  28. package/src/core/dashboard-render.mjs +10 -0
  29. package/src/core/derive.mjs +114 -0
  30. package/src/core/diagnostics.mjs +109 -0
  31. package/src/core/events.mjs +268 -0
  32. package/src/core/evidence.mjs +242 -0
  33. package/src/core/follow-up-queue.mjs +193 -0
  34. package/src/core/heuristics.mjs +240 -0
  35. package/src/core/ids.mjs +35 -0
  36. package/src/core/invocation.mjs +43 -0
  37. package/src/core/launch-options.mjs +317 -0
  38. package/src/core/launch.mjs +116 -0
  39. package/src/core/locks.mjs +80 -0
  40. package/src/core/paths.mjs +86 -0
  41. package/src/core/pid.mjs +42 -0
  42. package/src/core/prewarm-schedule.mjs +41 -0
  43. package/src/core/prompt-transport.mjs +13 -0
  44. package/src/core/pty-attach-jiggle-retry.mjs +90 -0
  45. package/src/core/pty-attach-render.mjs +51 -0
  46. package/src/core/pty-input.mjs +15 -0
  47. package/src/core/pty-links.mjs +71 -0
  48. package/src/core/pty-scroll.mjs +155 -0
  49. package/src/core/pty-support.mjs +327 -0
  50. package/src/core/repo.mjs +47 -0
  51. package/src/core/rows.mjs +290 -0
  52. package/src/core/screen-log-gc.mjs +198 -0
  53. package/src/core/screen-log.mjs +160 -0
  54. package/src/core/session-view.mjs +174 -0
  55. package/src/core/steering-prompts.mjs +34 -0
  56. package/src/core/steering.mjs +133 -0
  57. package/src/core/store.mjs +308 -0
  58. package/src/core/title.mjs +43 -0
  59. package/src/core/types.mjs +380 -0
  60. package/src/core/worktree.mjs +64 -0
  61. package/src/index.ts +109 -0
  62. package/src/runtime/service.mjs +1194 -0
  63. package/src/ui/dashboard-evidence.mjs +85 -0
  64. package/src/ui/dashboard.ts +1952 -0
  65. package/src/ui/pty-attach.ts +1378 -0
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Shared data shapes for the agent-board store. JSDoc typedefs (consumed by the TS
3
+ * extension via `allowJs`) plus the canonical state vocabularies as runtime constants.
4
+ */
5
+
6
+ /** Semantic (task) state of a row. @typedef {"queued"|"working"|"needs_input"|"idle"|"completed"|"failed"|"stopped"} SemanticState */
7
+ /** Process/liveness state. @typedef {"alive"|"exited"} ProcessState */
8
+ /** PTY host mode. @typedef {"json-runner"|"pty"} HostMode */
9
+ /** PTY host liveness. @typedef {"starting"|"alive"|"exited"|"failed"} HostState */
10
+ /** How a run was kicked off. @typedef {"dispatch"|"reply"|"plan"|"plan_change"|"plan_approval"} RunKind */
11
+ /** Worktree isolation mode for a row. @typedef {"off"|"worktree"} WorktreeMode */
12
+ /** Diagnostic severity. @typedef {"info"|"warn"|"error"} DiagnosticLevel */
13
+ /** Diagnostic event source. @typedef {"runner"|"host"|"service"|"queue"|"steering"|"evidence"|"store"} DiagnosticSource */
14
+ /** Evidence outcome. @typedef {"unknown"|"in_progress"|"ready"|"needs_input"|"failed"|"stopped"|"queued"|"working"|"idle"|"completed"} EvidenceOutcome */
15
+ /** Command kind. @typedef {"test"|"build"|"lint"|"git"|"install"|"other"} EvidenceCommandKind */
16
+ /** Command status. @typedef {"started"|"passed"|"failed"|"unknown"} EvidenceCommandStatus */
17
+ /** File change action. @typedef {"edited"|"written"|"deleted"|"unknown"} EvidenceFileAction */
18
+ /** Follow-up kind. @typedef {"reply"|"plan_request"|"plan_approval"|"plan_change"} FollowUpKind */
19
+ /** Follow-up status. @typedef {"queued"|"claimed"|"completed"|"failed"|"cancelled"} FollowUpStatus */
20
+ /** Steering state. @typedef {"none"|"plan_requested"|"awaiting_approval"|"approved"|"changes_requested"|"executing_approved_plan"} SteeringModeState */
21
+ /** Dashboard auto-state bucket. @typedef {"needs_input"|"in_progress"|"done"} AutoStateKind */
22
+ /** Auto-state classifier source. @typedef {"model"|"heuristic"} AutoStateSource */
23
+ /** Auto-state classifier confidence. @typedef {"high"|"medium"|"low"} AutoStateConfidence */
24
+
25
+ /**
26
+ * LLM/heuristic classification of the latest assistant turn.
27
+ * @typedef {Object} AutoStateClassification
28
+ * @property {number} version
29
+ * @property {AutoStateKind} kind
30
+ * @property {SemanticState} semanticState
31
+ * @property {AutoStateConfidence} confidence
32
+ * @property {AutoStateSource} source
33
+ * @property {string} reason
34
+ * @property {string|null} question
35
+ * @property {number} classifiedAt
36
+ * @property {number|null} lastAgentActivityAt
37
+ * @property {string} textHash
38
+ */
39
+
40
+ export const SEMANTIC_STATES = /** @type {const} */ ([
41
+ "queued",
42
+ "working",
43
+ "needs_input",
44
+ "idle",
45
+ "completed",
46
+ "failed",
47
+ "stopped",
48
+ ]);
49
+
50
+ export const PROCESS_STATES = /** @type {const} */ (["alive", "exited"]);
51
+
52
+ /** Order rows are grouped/shown in the dashboard (most-actionable first). */
53
+ export const GROUP_ORDER = /** @type {const} */ ([
54
+ "queued",
55
+ "working",
56
+ "needs_input",
57
+ "idle",
58
+ "completed",
59
+ "failed",
60
+ "stopped",
61
+ ]);
62
+
63
+ /** Human labels for group headers. @type {Record<SemanticState,string>} */
64
+ export const GROUP_LABELS = {
65
+ needs_input: "Needs input",
66
+ working: "Running",
67
+ queued: "Queued",
68
+ failed: "Failed",
69
+ completed: "Done",
70
+ idle: "In Progress",
71
+ stopped: "Stopped",
72
+ };
73
+
74
+ /**
75
+ * Stable row metadata (`meta.json`).
76
+ * @typedef {Object} ViewMeta
77
+ * @property {number} version
78
+ * @property {string} id
79
+ * @property {string} name
80
+ * @property {string} cwd Working dir the worker runs in (repo or worktree path).
81
+ * @property {string} repoCwd Original repo dir requested at dispatch (== cwd unless worktree).
82
+ * @property {string|null} repoRoot Git repo root, if any.
83
+ * @property {string} sessionFile Managed Pi session JSONL path.
84
+ * @property {number} createdAt
85
+ * @property {number} updatedAt
86
+ * @property {boolean} pinned
87
+ * @property {"pi-session"} kind
88
+ * @property {string|null} defaultModel
89
+ * @property {"off"|"minimal"|"low"|"medium"|"high"|"xhigh"|null} defaultThinking
90
+ * @property {WorktreeMode} worktreeMode
91
+ * @property {string|null} worktreePath
92
+ * @property {boolean} writeCapable Whether this session may mutate files (default true).
93
+ * @property {boolean} archived Soft-deleted from the dashboard (data preserved).
94
+ * @property {"agent-board"} source
95
+ */
96
+
97
+ /**
98
+ * Derived dashboard state for a row (`state.json`).
99
+ * @typedef {Object} ViewState
100
+ * @property {number} version
101
+ * @property {string} viewId
102
+ * @property {string|null} currentRunId
103
+ * @property {SemanticState} semanticState
104
+ * @property {ProcessState} processState
105
+ * @property {string} summary
106
+ * @property {number} lastActivityAt
107
+ * @property {number} updatedAt
108
+ * @property {boolean} needsInput
109
+ * @property {boolean} hasError
110
+ * @property {string} latestAssistantPreview
111
+ * @property {{name:string, path:string|null}|null} latestTool
112
+ * @property {string|null} question
113
+ * @property {Array<{toolCallId:string, question:string}>} pendingQuestions
114
+ * @property {string|null} error
115
+ * @property {number|null} lastVisitedAt
116
+ * @property {number|null} lastAgentActivityAt Timestamp of the latest assistant reply shown in the row.
117
+ * @property {ReviewSummary} [review] Compact collected-evidence summary.
118
+ * @property {DiagnosticSummary} [diagnostics] Compact diagnostics summary.
119
+ * @property {FollowUpSummary} [followUps] Compact queued follow-up summary.
120
+ * @property {SteeringSummary} [steering] Compact plan/approval steering summary.
121
+ * @property {AutoStateClassification|null} [autoState] Latest automatic terminal-state classification.
122
+ */
123
+
124
+ /**
125
+ * Durable PTY host snapshot (`host.json`).
126
+ * @typedef {Object} HostStatus
127
+ * @property {number} version
128
+ * @property {string} viewId
129
+ * @property {HostMode} mode
130
+ * @property {number|null} runnerPid
131
+ * @property {number|null} childPid
132
+ * @property {string|null} socketPath
133
+ * @property {HostState} state
134
+ * @property {number} startedAt
135
+ * @property {number} lastSeenAt
136
+ * @property {number|null} endedAt
137
+ * @property {number|null} exitCode
138
+ * @property {string|null} error
139
+ * @property {number} cols
140
+ * @property {number} rows
141
+ * @property {number} attachedClients
142
+ * @property {boolean} [attachedEver] Whether any client attached to this host.
143
+ */
144
+
145
+ /**
146
+ * Durable per-run snapshot (`runs/<runId>/status.json`).
147
+ * @typedef {Object} RunStatus
148
+ * @property {number} version
149
+ * @property {string} runId
150
+ * @property {string} viewId
151
+ * @property {number|null} pid
152
+ * @property {number} startedAt
153
+ * @property {number|null} endedAt
154
+ * @property {number|null} exitCode
155
+ * @property {RunKind} kind
156
+ * @property {string} prompt
157
+ * @property {string|null} model
158
+ * @property {SemanticState} semanticState
159
+ * @property {ProcessState} processState
160
+ * @property {string} summary
161
+ * @property {number} lastActivityAt
162
+ * @property {{name:string, path:string|null}|null} currentTool
163
+ * @property {string} latestAssistantPreview
164
+ * @property {string|null} question
165
+ * @property {Array<{toolCallId:string, question:string}>} pendingQuestions
166
+ * @property {string|null} error
167
+ * @property {number|null} lastAgentActivityAt Timestamp of the latest assistant reply.
168
+ * @property {string|null} stopReason
169
+ * @property {boolean} stoppedByUser
170
+ * @property {number} turns
171
+ * @property {number} toolCount
172
+ * @property {number} [eventCount]
173
+ * @property {number|null} [lastEventAt]
174
+ * @property {EvidenceUsage|null} [usage]
175
+ * @property {string|null} [stallReason]
176
+ * @property {ReviewSummary|null} [evidenceSummary]
177
+ * @property {AutoStateClassification|null} [autoState]
178
+ */
179
+
180
+ /**
181
+ * Configuration handed to the detached runner (written to the run dir as `config.json`
182
+ * and passed by path on argv).
183
+ * @typedef {Object} RunConfig
184
+ * @property {string} root
185
+ * @property {string} viewId
186
+ * @property {string} runId
187
+ * @property {RunKind} kind
188
+ * @property {string} sessionFile
189
+ * @property {string} cwd
190
+ * @property {string} prompt
191
+ * @property {string} piCommand Executable to launch the worker (e.g. "pi" or a node path).
192
+ * @property {string[]} piArgsPrefix Args before our flags (e.g. [cliJsPath] when piCommand is node).
193
+ * @property {string|null} model
194
+ * @property {"off"|"minimal"|"low"|"medium"|"high"|"xhigh"|null} thinkingLevel
195
+ * @property {string|null} tools
196
+ */
197
+
198
+ /**
199
+ * Configuration handed to the detached PTY host runner (written as `host-config.json`).
200
+ * @typedef {Object} HostConfig
201
+ * @property {string} root
202
+ * @property {string} viewId
203
+ * @property {string} sessionFile
204
+ * @property {string} cwd
205
+ * @property {string|null} initialPrompt
206
+ * @property {string} piCommand
207
+ * @property {string[]} piArgsPrefix
208
+ * @property {string|null} model
209
+ * @property {"off"|"minimal"|"low"|"medium"|"high"|"xhigh"|null} thinkingLevel
210
+ * @property {string|null} tools
211
+ * @property {Record<string,string>} env
212
+ * @property {number} cols
213
+ * @property {number} rows
214
+ * @property {number|null} screenLogMaxBytes per-view screen.log write cap; null = runner default
215
+ */
216
+
217
+ /**
218
+ * Configuration handed to the detached title runner (written as `title-config.json`).
219
+ * @typedef {Object} TitleConfig
220
+ * @property {string} root
221
+ * @property {string} viewId
222
+ * @property {string} cwd
223
+ * @property {string} prompt
224
+ * @property {string} fallbackName
225
+ * @property {string} piCommand
226
+ * @property {string[]} piArgsPrefix
227
+ * @property {string|null} model
228
+ */
229
+
230
+ /**
231
+ * Configuration handed to the detached auto-state runner.
232
+ * @typedef {Object} AutoStateConfig
233
+ * @property {string} root
234
+ * @property {string} viewId
235
+ * @property {string|null} runId
236
+ * @property {string} cwd
237
+ * @property {string} piCommand
238
+ * @property {string[]} piArgsPrefix
239
+ */
240
+
241
+ /**
242
+ * @typedef {Object} DiagnosticEvent
243
+ * @property {number} version
244
+ * @property {number} at
245
+ * @property {string} viewId
246
+ * @property {string|null} runId
247
+ * @property {DiagnosticSource|string} source
248
+ * @property {DiagnosticLevel} level
249
+ * @property {string} code
250
+ * @property {string} message
251
+ * @property {Record<string,any>} details
252
+ */
253
+
254
+ /**
255
+ * @typedef {Object} DiagnosticSummary
256
+ * @property {number} count
257
+ * @property {number} warningCount
258
+ * @property {number} errorCount
259
+ * @property {number|null} lastAt
260
+ * @property {DiagnosticLevel|null} lastLevel
261
+ * @property {string|null} lastCode
262
+ * @property {string|null} lastMessage
263
+ * @property {boolean} stalled
264
+ * @property {string|null} stallReason
265
+ */
266
+
267
+ /**
268
+ * @typedef {Object} EvidenceUsage
269
+ * @property {number} inputTokens
270
+ * @property {number} outputTokens
271
+ * @property {number} totalTokens
272
+ */
273
+
274
+ /**
275
+ * @typedef {Object} EvidenceCommand
276
+ * @property {string} id
277
+ * @property {number} at
278
+ * @property {string} command
279
+ * @property {EvidenceCommandKind} kind
280
+ * @property {EvidenceCommandStatus} status
281
+ * @property {number|null} exitCode
282
+ * @property {number|null} durationMs
283
+ * @property {string} outputPreview
284
+ */
285
+
286
+ /**
287
+ * @typedef {Object} EvidenceFileChange
288
+ * @property {string} path
289
+ * @property {EvidenceFileAction} action
290
+ * @property {string|null} toolName
291
+ * @property {number} firstSeenAt
292
+ * @property {number} lastSeenAt
293
+ * @property {number} count
294
+ */
295
+
296
+ /** @typedef {{ at:number, source:string, message:string, toolName:string|null }} EvidenceError */
297
+ /** @typedef {{ at:number, text:string }} EvidenceAssistantClaim */
298
+
299
+ /**
300
+ * @typedef {Object} EvidenceSnapshot
301
+ * @property {number} version
302
+ * @property {string} viewId
303
+ * @property {string|null} runId
304
+ * @property {number} updatedAt
305
+ * @property {EvidenceOutcome|string} outcome
306
+ * @property {boolean} ready
307
+ * @property {string} summary
308
+ * @property {EvidenceCommand[]} commands
309
+ * @property {EvidenceFileChange[]} fileChanges
310
+ * @property {EvidenceError[]} errors
311
+ * @property {EvidenceAssistantClaim[]} assistantEvidence
312
+ * @property {EvidenceUsage|null} usage
313
+ * @property {number} [eventCount]
314
+ * @property {string} [source]
315
+ */
316
+
317
+ /**
318
+ * @typedef {Object} ReviewSummary
319
+ * @property {boolean} ready
320
+ * @property {EvidenceOutcome|string} outcome
321
+ * @property {number} fileChangeCount
322
+ * @property {number} commandCount
323
+ * @property {number} failedCommandCount
324
+ * @property {number} errorCount
325
+ * @property {string} latestAssistantEvidence
326
+ * @property {number|null} updatedAt
327
+ */
328
+
329
+ /**
330
+ * @typedef {Object} FollowUpItem
331
+ * @property {string} id
332
+ * @property {number} seq
333
+ * @property {string} viewId
334
+ * @property {FollowUpKind} kind
335
+ * @property {string} text
336
+ * @property {number} createdAt
337
+ * @property {number} updatedAt
338
+ * @property {FollowUpStatus} status
339
+ * @property {string} source
340
+ * @property {"auto"|"now"|"queue"} delivery
341
+ * @property {string|null} runId
342
+ * @property {number|null} claimedAt
343
+ * @property {number|null} completedAt
344
+ * @property {number} attempts
345
+ * @property {string|null} error
346
+ */
347
+
348
+ /** @typedef {{ version:number, viewId:string, nextSeq:number, updatedAt:number, items:FollowUpItem[] }} FollowUpQueue */
349
+ /** @typedef {{ queuedCount:number, claimedCount:number, lastQueuedAt:number|null, lastQueuedPreview:string|null }} FollowUpSummary */
350
+
351
+ /**
352
+ * @typedef {Object} SteeringState
353
+ * @property {number} version
354
+ * @property {string} viewId
355
+ * @property {SteeringModeState} status
356
+ * @property {number} updatedAt
357
+ * @property {string} planText
358
+ * @property {string|null} planRunId
359
+ * @property {number|null} approvedAt
360
+ * @property {string|null} changeRequest
361
+ * @property {string|null} executionRunId
362
+ * @property {Array<{at:number, from:string, to:string, action:string, runId:string|null, note:string|null}>} history
363
+ */
364
+
365
+ /** @typedef {{ status:SteeringModeState, awaitingApproval:boolean, planPreview:string|null, updatedAt:number|null, question:string|null }} SteeringSummary */
366
+
367
+ /** Roster index (`roster.json`). @typedef {Object} Roster @property {number} version @property {string[]} views */
368
+
369
+ /**
370
+ * Persisted launch dialog defaults (`launch-prefs.json`).
371
+ * @typedef {Object} LaunchPrefs
372
+ * @property {number} version
373
+ * @property {string|null} cwd
374
+ * @property {string|null} model
375
+ * @property {"off"|"minimal"|"low"|"medium"|"high"|"xhigh"|null} thinkingLevel
376
+ * @property {number|null} screenLogRetentionDays days before an ended view's screen.log is GC'd; 0 disables GC
377
+ * @property {number|null} screenLogMaxSize per-view screen.log write cap in bytes; null = built-in default
378
+ */
379
+
380
+ export {};
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Git worktree isolation for same-repo parallel writer sessions (plan §11.2).
3
+ * Each managed view gets its own worktree + branch so concurrent writers never clobber.
4
+ */
5
+ import { execFileSync } from "node:child_process";
6
+ import { existsSync } from "node:fs";
7
+
8
+ /**
9
+ * @param {string} repoRoot
10
+ * @param {string[]} args
11
+ * @returns {{ ok: boolean, stdout: string, error: string|null }}
12
+ */
13
+ function git(repoRoot, args) {
14
+ try {
15
+ const stdout = execFileSync("git", ["-C", repoRoot, ...args], {
16
+ encoding: "utf8",
17
+ stdio: ["ignore", "pipe", "pipe"],
18
+ });
19
+ return { ok: true, stdout: stdout.trim(), error: null };
20
+ } catch (err) {
21
+ const e = /** @type {any} */ (err);
22
+ const error = (e?.stderr?.toString?.() || e?.message || "git error").trim();
23
+ return { ok: false, stdout: "", error };
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Create a worktree at `worktreePath` on a fresh branch `branch`, based on HEAD.
29
+ * Idempotent-ish: if the path already exists we assume it's already a worktree.
30
+ * @param {string} repoRoot
31
+ * @param {string} worktreePath
32
+ * @param {string} branch
33
+ * @returns {{ ok: boolean, error: string|null }}
34
+ */
35
+ export function addWorktree(repoRoot, worktreePath, branch) {
36
+ if (existsSync(worktreePath)) return { ok: true, error: null };
37
+ // Try with a new branch first; fall back to attaching if the branch already exists.
38
+ let res = git(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
39
+ if (!res.ok && /already exists/i.test(res.error ?? "")) {
40
+ res = git(repoRoot, ["worktree", "add", worktreePath, branch]);
41
+ }
42
+ return { ok: res.ok, error: res.error };
43
+ }
44
+
45
+ /**
46
+ * Remove a worktree (forced) and prune. Best-effort.
47
+ * @param {string} repoRoot
48
+ * @param {string} worktreePath
49
+ * @returns {{ ok: boolean, error: string|null }}
50
+ */
51
+ export function removeWorktree(repoRoot, worktreePath) {
52
+ const res = git(repoRoot, ["worktree", "remove", "--force", worktreePath]);
53
+ git(repoRoot, ["worktree", "prune"]);
54
+ return { ok: res.ok, error: res.error };
55
+ }
56
+
57
+ /**
58
+ * A short, filesystem/branch-safe identifier for a view's worktree branch.
59
+ * @param {string} viewId
60
+ * @returns {string}
61
+ */
62
+ export function worktreeBranch(viewId) {
63
+ return `agent-board/${viewId}`;
64
+ }
package/src/index.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Pi Agent Board — extension entry point.
3
+ *
4
+ * Registers the `/agent-board` dashboard command, resolves how to
5
+ * launch background workers and the detached runner, and keeps a small footer status with
6
+ * the count of sessions needing attention. See docs/EXPLORATION.md for the design.
7
+ */
8
+ import { fileURLToPath } from "node:url";
9
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import { resolvePiInvocation } from "./core/invocation.mjs";
11
+ import { defaultRoot } from "./core/paths.mjs";
12
+ import { listRows } from "./core/store.mjs";
13
+ import { createService } from "./runtime/service.mjs";
14
+ import { openDashboard, registerAgentBoardCommand } from "./commands/agent-board.js";
15
+ import { registerBgCommand } from "./commands/bg.js";
16
+
17
+ const RUNNER_SCRIPT = fileURLToPath(new URL("../runner/job-runner.mjs", import.meta.url));
18
+ const PTY_RUNNER_SCRIPT = fileURLToPath(new URL("../runner/pty-runner.mjs", import.meta.url));
19
+ const TITLE_RUNNER_SCRIPT = fileURLToPath(new URL("../runner/title-runner.mjs", import.meta.url));
20
+ const AUTO_STATE_RUNNER_SCRIPT = fileURLToPath(new URL("../runner/state-runner.mjs", import.meta.url));
21
+
22
+ export default function piAgentBoard(pi: ExtensionAPI): void {
23
+ const root = defaultRoot();
24
+ const { piCommand, piArgsPrefix } = resolvePiInvocation();
25
+
26
+ const isHostedChild = process.env.AGENT_BOARD_CHILD === "1" || process.env.AGENT_VIEW_CHILD === "1";
27
+ const hostedViewId = process.env.AGENT_BOARD_VIEW_ID ?? process.env.AGENT_VIEW_VIEW_ID;
28
+
29
+ const commandOpts = {
30
+ root,
31
+ runnerScript: RUNNER_SCRIPT,
32
+ titleRunnerScript: TITLE_RUNNER_SCRIPT,
33
+ autoStateRunnerScript: AUTO_STATE_RUNNER_SCRIPT,
34
+ ptyRunnerScript: PTY_RUNNER_SCRIPT,
35
+ piCommand,
36
+ piArgsPrefix,
37
+ getThinkingLevel: () => pi.getThinkingLevel(),
38
+ };
39
+ registerAgentBoardCommand(pi, commandOpts);
40
+ registerBgCommand(pi, commandOpts);
41
+ pi.registerFlag("agent-board", {
42
+ description: "Open the agent-board dashboard on startup",
43
+ type: "boolean",
44
+ default: false,
45
+ });
46
+
47
+ // Footer status: reconcile stale rows and surface how many need attention.
48
+ const serviceFor = (ctx: ExtensionContext) =>
49
+ createService({ root, runnerScript: RUNNER_SCRIPT, ptyRunnerScript: PTY_RUNNER_SCRIPT, titleRunnerScript: TITLE_RUNNER_SCRIPT, autoStateRunnerScript: AUTO_STATE_RUNNER_SCRIPT, piCommand, piArgsPrefix, defaultCwd: ctx.cwd });
50
+
51
+ const updateStatus = (ctx: ExtensionContext) => {
52
+ try {
53
+ serviceFor(ctx).reconcile();
54
+ const rows = listRows(root);
55
+ const needs = rows.filter((r) => r.state?.semanticState === "needs_input").length;
56
+ const working = rows.filter((r) => r.alive).length;
57
+ const queued = rows.reduce((sum, r) => sum + (r.state?.followUps?.queuedCount ?? 0), 0);
58
+ if (isHostedChild) return;
59
+ if (rows.length === 0) {
60
+ ctx.ui.setStatus("agent-board", undefined);
61
+ return;
62
+ }
63
+ const parts: string[] = [];
64
+ if (working > 0) parts.push(ctx.ui.theme.fg("accent", `●${working}`));
65
+ if (needs > 0) parts.push(ctx.ui.theme.fg("warning", `◆${needs}`));
66
+ if (queued > 0) parts.push(ctx.ui.theme.fg("muted", `q${queued}`));
67
+ ctx.ui.setStatus("agent-board", parts.length ? `${ctx.ui.theme.fg("muted", "agents")} ${parts.join(" ")}` : undefined);
68
+ } catch {
69
+ /* never break the session over a status update */
70
+ }
71
+ };
72
+
73
+ pi.on("session_start", async (event, ctx) => {
74
+ updateStatus(ctx);
75
+ if (event.reason === "startup" && !isHostedChild && pi.getFlag("agent-board") === true && ctx.hasUI) {
76
+ const service = createService({ root, runnerScript: RUNNER_SCRIPT, ptyRunnerScript: PTY_RUNNER_SCRIPT, titleRunnerScript: TITLE_RUNNER_SCRIPT, autoStateRunnerScript: AUTO_STATE_RUNNER_SCRIPT, piCommand, piArgsPrefix, defaultCwd: ctx.cwd });
77
+ service.reconcile();
78
+ ctx.ui.setWorkingVisible(false);
79
+ ctx.ui.setHeader(() => ({ render: () => [], invalidate() {} }));
80
+ ctx.ui.setFooter(() => ({ render: () => [], invalidate() {} }));
81
+ ctx.ui.setTitle("agent board");
82
+ const result = await openDashboard(ctx, service, { currentThinkingLevel: pi.getThinkingLevel() });
83
+ if (result.action === "attach") {
84
+ ctx.ui.notify("Attach requires the /agent-board command path; launch from a normal Pi session for now.", "warning");
85
+ } else {
86
+ ctx.shutdown();
87
+ }
88
+ }
89
+ });
90
+ const syncForeground = (event: unknown, ctx: ExtensionContext) => {
91
+ try {
92
+ const service = serviceFor(ctx);
93
+ if (hostedViewId) service.syncHostedEvent(hostedViewId, event);
94
+ else service.syncForegroundEvent(ctx.sessionManager.getSessionFile(), event);
95
+ updateStatus(ctx);
96
+ } catch {
97
+ /* never break the session over dashboard mirroring */
98
+ }
99
+ };
100
+
101
+ pi.on("input", async (event, ctx) => syncForeground(event, ctx));
102
+ pi.on("before_agent_start", async (event, ctx) => syncForeground(event, ctx));
103
+ pi.on("agent_start", async (event, ctx) => syncForeground(event, ctx));
104
+ pi.on("tool_execution_start", async (event, ctx) => syncForeground(event, ctx));
105
+ pi.on("tool_execution_end", async (event, ctx) => syncForeground(event, ctx));
106
+ pi.on("message_start", async (event, ctx) => syncForeground(event, ctx));
107
+ pi.on("message_end", async (event, ctx) => syncForeground(event, ctx));
108
+ pi.on("agent_end", async (event, ctx) => syncForeground(event, ctx));
109
+ }