pi-crew 0.9.68 → 0.10.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.
Files changed (253) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/NOTICE.md +21 -0
  3. package/README.md +44 -2
  4. package/agents/analyst.md +1 -1
  5. package/agents/cold-verifier.md +3 -1
  6. package/agents/critic.md +1 -1
  7. package/agents/executor.md +1 -1
  8. package/agents/explorer.md +1 -1
  9. package/agents/planner.md +1 -1
  10. package/agents/reviewer.md +1 -1
  11. package/agents/security-reviewer.md +1 -1
  12. package/agents/test-engineer.md +1 -1
  13. package/agents/verifier.md +1 -1
  14. package/agents/writer.md +1 -1
  15. package/dist/index.mjs +68113 -60774
  16. package/docs/README.md +2 -0
  17. package/docs/actions-reference.md +31 -0
  18. package/docs/commands-reference.md +17 -6
  19. package/docs/resource-formats.md +13 -0
  20. package/package.json +4 -2
  21. package/schema.json +503 -91
  22. package/scripts/resource-sampler.mjs +36 -2
  23. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  24. package/skills/widget-rendering/SKILL.md +7 -7
  25. package/src/agents/agent-config.ts +2 -1
  26. package/src/agents/discover-agents.ts +23 -14
  27. package/src/config/config-merge.ts +183 -0
  28. package/src/config/config-validation.ts +687 -0
  29. package/src/config/config.ts +22 -864
  30. package/src/config/defaults.ts +43 -2
  31. package/src/config/drift-detector.ts +1 -1
  32. package/src/config/env-vars.ts +691 -0
  33. package/src/config/role-tools.ts +11 -9
  34. package/src/config/sanitize-project-config.ts +172 -0
  35. package/src/config/types.ts +49 -1
  36. package/src/extension/async-notifier.ts +25 -2
  37. package/src/extension/crew-cleanup.ts +13 -0
  38. package/src/extension/crew-vibes/config.ts +2 -1
  39. package/src/extension/crew-vibes/footer.ts +19 -0
  40. package/src/extension/crew-vibes/index.ts +11 -1
  41. package/src/extension/plan-orchestrate.ts +132 -0
  42. package/src/extension/register.ts +8 -0
  43. package/src/extension/registration/command-registration.ts +1 -0
  44. package/src/extension/registration/commands/dashboard.ts +158 -0
  45. package/src/extension/registration/commands/index.ts +35 -0
  46. package/src/extension/registration/commands/manage.ts +303 -0
  47. package/src/extension/registration/commands/run.ts +228 -0
  48. package/src/extension/registration/commands/shared.ts +639 -0
  49. package/src/extension/registration/commands/status.ts +60 -0
  50. package/src/extension/registration/commands.ts +13 -1224
  51. package/src/extension/registration/foreground-run-controller.ts +10 -2
  52. package/src/extension/registration/lifecycle-handlers.ts +178 -17
  53. package/src/extension/registration/runtime-cleanup.ts +23 -5
  54. package/src/extension/registration/subagent-tools.ts +218 -9
  55. package/src/extension/registration/team-tool.ts +5 -1
  56. package/src/extension/registration/ui.ts +5 -4
  57. package/src/extension/rpc-hmac.ts +5 -3
  58. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  59. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  60. package/src/extension/team-tool/api/task-claims.ts +109 -40
  61. package/src/extension/team-tool/cancel.ts +84 -50
  62. package/src/extension/team-tool/dispatch/index.ts +1 -0
  63. package/src/extension/team-tool/dispatch/run.ts +4 -1
  64. package/src/extension/team-tool/doctor.ts +103 -1
  65. package/src/extension/team-tool/orchestrate.ts +66 -1
  66. package/src/extension/team-tool/plans.ts +192 -0
  67. package/src/extension/team-tool/respond.ts +197 -65
  68. package/src/extension/team-tool/run-deadline.ts +35 -3
  69. package/src/extension/team-tool/run-intent.ts +63 -0
  70. package/src/extension/team-tool/run.ts +74 -20
  71. package/src/extension/team-tool/status.ts +84 -26
  72. package/src/extension/team-tool.ts +11 -2
  73. package/src/hooks/registry.ts +1 -6
  74. package/src/i18n.ts +9 -0
  75. package/src/prompt/prompt-runtime.ts +521 -2
  76. package/src/prompt/worker-events-channel.ts +173 -0
  77. package/src/runtime/README.md +8 -8
  78. package/src/runtime/async-runner.ts +7 -3
  79. package/src/runtime/background-runner.ts +42 -14
  80. package/src/runtime/broker/broker-issuer.ts +9 -2
  81. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  82. package/src/runtime/broker/crew-broker.ts +838 -10
  83. package/src/runtime/broker/wait-status-cache.ts +157 -0
  84. package/src/runtime/budget-enforcement.ts +281 -0
  85. package/src/runtime/child-pi/child-pi-constants.ts +8 -0
  86. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  87. package/src/runtime/child-pi/child-pi-streams.ts +21 -1
  88. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  89. package/src/runtime/child-pi/child-pi.ts +97 -201
  90. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  91. package/src/runtime/crew-agent-records.ts +259 -14
  92. package/src/runtime/delegate-spawn.ts +148 -0
  93. package/src/runtime/detached-run-results.ts +90 -0
  94. package/src/runtime/deterministic-ast.ts +2 -1
  95. package/src/runtime/dispatch-batch.ts +945 -0
  96. package/src/runtime/finalize-run.ts +557 -0
  97. package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
  98. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
  99. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  100. package/src/runtime/group-join.ts +11 -125
  101. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  102. package/src/runtime/merge-gate.ts +32 -10
  103. package/src/runtime/merge-loop.ts +130 -0
  104. package/src/runtime/model/model-budget-summary.ts +53 -0
  105. package/src/runtime/model/model-fallback.ts +36 -2
  106. package/src/runtime/model/pi-args.ts +10 -0
  107. package/src/runtime/model/provider-extensions.ts +10 -0
  108. package/src/runtime/orphan-worker-registry.ts +1 -1
  109. package/src/runtime/output/output-validator.ts +45 -0
  110. package/src/runtime/parent-guard.ts +3 -1
  111. package/src/runtime/peer-dep.ts +2 -1
  112. package/src/runtime/per-write-validator.ts +0 -5
  113. package/src/runtime/pi-spawn.ts +61 -15
  114. package/src/runtime/plan-approval.ts +125 -0
  115. package/src/runtime/plan-replan.ts +151 -0
  116. package/src/runtime/process-status.ts +16 -1
  117. package/src/runtime/recovery/checkpoint.ts +0 -18
  118. package/src/runtime/recovery/crash-recovery.ts +111 -46
  119. package/src/runtime/run-tracker.ts +77 -10
  120. package/src/runtime/scheduler-context.ts +98 -0
  121. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  122. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  123. package/src/runtime/scheduling/nested-slots.ts +70 -0
  124. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  125. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  126. package/src/runtime/settings-store.ts +219 -0
  127. package/src/runtime/spawn-policy.ts +217 -0
  128. package/src/runtime/stale-reconciler.ts +87 -6
  129. package/src/runtime/subagent-manager.ts +25 -1
  130. package/src/runtime/task-output-context.ts +230 -9
  131. package/src/runtime/task-packet.ts +23 -1
  132. package/src/runtime/task-runner/child-executor.ts +106 -7
  133. package/src/runtime/task-runner/post-execution.ts +125 -1
  134. package/src/runtime/task-runner/pre-execution.ts +39 -1
  135. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  136. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  137. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  138. package/src/runtime/task-runner/state-helpers.ts +26 -24
  139. package/src/runtime/task-runner.ts +11 -0
  140. package/src/runtime/team-runner.ts +132 -1673
  141. package/src/runtime/verification/spec-sandbox.ts +255 -0
  142. package/src/runtime/verification/verification-gates.ts +3 -2
  143. package/src/runtime/verification/verification-worktree.ts +2 -1
  144. package/src/runtime/workflow-phase-advance.ts +100 -0
  145. package/src/runtime/workspace-tree.ts +9 -0
  146. package/src/schema/config-schema.ts +66 -26
  147. package/src/schema/sensitive-config-paths.ts +64 -0
  148. package/src/schema/team-tool-schema.ts +13 -3
  149. package/src/state/README.md +4 -10
  150. package/src/state/atomic-write.ts +20 -3
  151. package/src/state/contracts.ts +38 -0
  152. package/src/state/coordination/mailbox.ts +12 -2
  153. package/src/state/event-log/cursor.ts +223 -0
  154. package/src/state/event-log/event-log-rotation.ts +12 -4
  155. package/src/state/event-log/event-log.ts +152 -369
  156. package/src/state/event-log/sequence-cache.ts +373 -0
  157. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  158. package/src/state/stores/active-run-registry.ts +3 -2
  159. package/src/state/stores/manifest-io.ts +237 -0
  160. package/src/state/stores/ownership-map.ts +162 -0
  161. package/src/state/stores/plan-store.ts +241 -0
  162. package/src/state/stores/run-cache.ts +0 -90
  163. package/src/state/stores/spec-store.ts +189 -0
  164. package/src/state/stores/state-store.ts +139 -232
  165. package/src/state/types.ts +199 -0
  166. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  167. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  168. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  169. package/src/ui/dock-footer.ts +49 -0
  170. package/src/ui/heartbeat-aggregator.ts +9 -1
  171. package/src/ui/inline-panel/agent-pane.ts +375 -0
  172. package/src/ui/inline-panel/agent-transcript.ts +338 -0
  173. package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
  174. package/src/ui/inline-panel/crew-editor.ts +192 -0
  175. package/src/ui/inline-panel/index.ts +290 -0
  176. package/src/ui/inline-panel/panel-rows.ts +37 -0
  177. package/src/ui/inline-panel/panel-selection.ts +157 -0
  178. package/src/ui/inline-panel/panel-store.ts +111 -0
  179. package/src/ui/inline-panel/view-session-store.ts +36 -0
  180. package/src/ui/keybinding-map.ts +54 -13
  181. package/src/ui/pi-ui-compat.ts +9 -0
  182. package/src/ui/powerbar-publisher.ts +52 -1
  183. package/src/ui/run-dashboard.ts +31 -5
  184. package/src/ui/run-snapshot-cache.ts +57 -30
  185. package/src/ui/snapshot-types.ts +6 -1
  186. package/src/ui/widget/index.ts +176 -22
  187. package/src/ui/widget/task-list.ts +198 -0
  188. package/src/ui/widget/widget-formatters.ts +240 -4
  189. package/src/ui/widget/widget-renderer.ts +243 -38
  190. package/src/ui/widget/widget-types.ts +11 -0
  191. package/src/utils/child-process-shield.ts +106 -0
  192. package/src/utils/file-coalescer.ts +0 -4
  193. package/src/utils/fs-errno.ts +66 -0
  194. package/src/utils/fs-watch.ts +1 -1
  195. package/src/utils/internal-error.ts +3 -1
  196. package/src/utils/paths.ts +11 -3
  197. package/src/utils/redaction.ts +7 -0
  198. package/src/utils/safe-abort.ts +45 -0
  199. package/src/utils/task-name-generator.ts +1 -8
  200. package/src/workflows/discover-workflows.ts +20 -2
  201. package/src/workflows/validate-workflow.ts +7 -1
  202. package/src/workflows/workflow-config.ts +17 -0
  203. package/src/workflows/workflow-serializer.ts +3 -0
  204. package/src/worktree/worktree-manager.ts +22 -0
  205. package/workflows/default.workflow.md +36 -26
  206. package/workflows/strict-fast-fix.workflow.md +26 -0
  207. package/src/agents/agent-search.ts +0 -98
  208. package/src/benchmark/benchmark-runner.ts +0 -313
  209. package/src/benchmark/feedback-loop.ts +0 -73
  210. package/src/config/resilient-parser.ts +0 -117
  211. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  212. package/src/extension/result-watcher.ts +0 -139
  213. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  214. package/src/observability/metric-retention.ts +0 -64
  215. package/src/runtime/compaction/compaction-summary.ts +0 -278
  216. package/src/runtime/errors/crew-errors.ts +0 -162
  217. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  218. package/src/runtime/loop-gates.ts +0 -128
  219. package/src/runtime/metric-parser.ts +0 -36
  220. package/src/runtime/output/stream-preview.ts +0 -184
  221. package/src/runtime/output/tool-progress.ts +0 -278
  222. package/src/runtime/phase-tracker.ts +0 -385
  223. package/src/runtime/pipeline-runner.ts +0 -523
  224. package/src/runtime/process/process-lifecycle.ts +0 -491
  225. package/src/runtime/recovery/retry-runner.ts +0 -330
  226. package/src/runtime/run-drift.ts +0 -219
  227. package/src/runtime/task-quality.ts +0 -199
  228. package/src/runtime/task-runner/run-projection.ts +0 -128
  229. package/src/runtime/verification/post-checks.ts +0 -142
  230. package/src/state/coordination/schedule.ts +0 -166
  231. package/src/state/event-log/jsonl-writer.ts +0 -115
  232. package/src/state/hook-instinct-bridge.ts +0 -94
  233. package/src/state/hook-integrations.ts +0 -51
  234. package/src/state/session-state-map.ts +0 -51
  235. package/src/state/stores/blob-store.ts +0 -308
  236. package/src/state/stores/instinct-store.ts +0 -275
  237. package/src/state/stores/observation-store.ts +0 -176
  238. package/src/state/tiered-eval.ts +0 -480
  239. package/src/state/types-eval.ts +0 -58
  240. package/src/tools/safe-bash-extension.ts +0 -54
  241. package/src/tools/safe-bash.ts +0 -505
  242. package/src/ui/agent-management-overlay.ts +0 -160
  243. package/src/ui/crew-footer.ts +0 -102
  244. package/src/ui/crew-select-list.ts +0 -114
  245. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  246. package/src/ui/transcript-entries.ts +0 -256
  247. package/src/utils/conflict-detect.ts +0 -721
  248. package/src/utils/fingerprint.ts +0 -180
  249. package/src/utils/gh-protocol.ts +0 -556
  250. package/src/utils/project-detector.ts +0 -160
  251. package/src/utils/sse-parser.ts +0 -131
  252. package/src/workflows/cost-estimator.ts +0 -34
  253. package/src/workflows/intermediate-store.ts +0 -166
@@ -0,0 +1,338 @@
1
+ /**
2
+ * agent-transcript.ts — per-agent event JSONL → typed transcript items.
3
+ *
4
+ * The source of truth is the on-disk per-agent event log
5
+ * (`<stateRoot>/agents/<taskId>/events.jsonl`), written by
6
+ * `appendCrewAgentEventBuffered` from the single `onJsonEvent` funnel in
7
+ * `child-executor.ts`. Events are already compacted upstream
8
+ * (`child-pi-streams.ts:52-101`), so each record is small and shaped for
9
+ * display.
10
+ *
11
+ * Disk is chosen over a pure in-memory feed because child-executor only lives
12
+ * in the extension process for **foreground** runs; async runs execute in a
13
+ * detached spawner, so a memory feed would show an empty pane for exactly the
14
+ * runs users background. Disk works for both with one parser.
15
+ *
16
+ * Per task, a module-level ring buffer accumulates parsed items across reads
17
+ * (cursor + persisted parse state), mirroring pi-subtask's `fork.transcript`.
18
+ * This matters for two reasons the naive delta-read gets wrong:
19
+ * - tool `start`/`end` events can straddle a read boundary, so the
20
+ * unmatched-start map must survive between reads;
21
+ * - the pane renders the FULL recent history on every tick, not just the
22
+ * new tail.
23
+ */
24
+
25
+ import * as fs from "node:fs";
26
+ import * as path from "node:path";
27
+
28
+ import { readCrewAgentEventsCursor } from "../../runtime/crew-agent-records.ts";
29
+ import type { TeamRunManifest } from "../../state/types.ts";
30
+
31
+ const MAX_TRANSCRIPT_ITEMS = 500;
32
+
33
+ /** Normalize a raw usage record to pi's shape (footer/dashboard consumers
34
+ * read usage.input / usage.cost.total unconditionally). */
35
+ export function normalizeUsage(raw: unknown): {
36
+ input: number;
37
+ output: number;
38
+ cacheRead: number;
39
+ cacheWrite: number;
40
+ cost: { total: number };
41
+ } {
42
+ const usage = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : undefined;
43
+ const toNum = (value: unknown): number => (typeof value === "number" && Number.isFinite(value) ? value : 0);
44
+ const costRaw = usage ? usage.cost : undefined;
45
+ const costRecord = costRaw && typeof costRaw === "object" && !Array.isArray(costRaw) ? (costRaw as Record<string, unknown>) : undefined;
46
+ return {
47
+ input: toNum(usage?.input),
48
+ output: toNum(usage?.output),
49
+ cacheRead: toNum(usage?.cacheRead),
50
+ cacheWrite: toNum(usage?.cacheWrite),
51
+ cost: { total: toNum(costRecord?.total ?? costRaw) },
52
+ };
53
+ }
54
+
55
+ export type CrewTranscriptItem =
56
+ | { type: "user"; text: string; seq: number }
57
+ | {
58
+ type: "assistant";
59
+ text: string;
60
+ seq: number;
61
+ /** The compacted assistant message (content/usage/model/stopReason),
62
+ * retained so the pane can render with pi's own
63
+ * AssistantMessageComponent instead of plain markdown. Usage is
64
+ * normalized to pi's shape (footer/dashboard consumers read
65
+ * usage.input / usage.cost.total unconditionally). */
66
+ message?: Record<string, unknown>;
67
+ /** Usage normalized for the pane's own footer line (parallels the
68
+ * view-session builder; absent when the event carried none). */
69
+ usage?: ReturnType<typeof normalizeUsage>;
70
+ }
71
+ | {
72
+ type: "tool";
73
+ name: string;
74
+ toolCallId: string;
75
+ args: Record<string, unknown>;
76
+ /** Normalized to ToolExecutionComponent.updateResult's shape:
77
+ * `{ content: parts[], isError }` (pi's own tool-result envelope). */
78
+ result?: { content: Array<{ type: string; text?: string }>; isError: boolean };
79
+ isError?: boolean;
80
+ seq: number;
81
+ }
82
+ | { type: "system"; text: string; seq: number };
83
+
84
+ type PendingTool = CrewTranscriptItem & { type: "tool" };
85
+
86
+ /** Per-task parse state, persisted across reads. */
87
+ const buffers = new Map<string, CrewTranscriptItem[]>();
88
+ const pendingByTask = new Map<string, Map<string, PendingTool>>();
89
+ const cursors = new Map<string, number>();
90
+ /** Tasks whose worker prompt has been prepended to the buffer. */
91
+ const promptSeeded = new Set<string>();
92
+
93
+ /**
94
+ * The child pi never logs its INITIAL user message (input is not an event),
95
+ * so the transcript would open on the first assistant message — unlike a
96
+ * real session. The full worker prompt is persisted at
97
+ * `artifacts/{runId}/prompts/{taskId}.md`; seed it as the opening user item
98
+ * for session parity. Returns undefined while the artifact has not been
99
+ * written yet (retried on the next read).
100
+ */
101
+ function readWorkerPrompt(manifest: TeamRunManifest, taskId: string): string | undefined {
102
+ try {
103
+ const file = path.join(manifest.artifactsRoot, "prompts", `${taskId}.md`);
104
+ const text = fs.readFileSync(file, "utf-8").trim();
105
+ return text || undefined;
106
+ } catch {
107
+ return undefined;
108
+ }
109
+ }
110
+
111
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
112
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
113
+ return value as Record<string, unknown>;
114
+ }
115
+
116
+ /**
117
+ * Normalize a tool result's content into ToolExecutionComponent.updateResult's
118
+ * envelope. The log holds either shape depending on age/source: a plain
119
+ * string (compacted toolResult part) or an array of content parts
120
+ * (role:"toolResult" message_end).
121
+ */
122
+ function normalizeResultContent(raw: unknown): Array<{ type: string; text?: string }> {
123
+ if (typeof raw === "string") return [{ type: "text", text: raw }];
124
+ if (!Array.isArray(raw)) return [];
125
+ return raw.flatMap((part) => {
126
+ if (typeof part === "string") return [{ type: "text", text: part }];
127
+ const record = asRecord(part);
128
+ if (!record) return [];
129
+ return [
130
+ {
131
+ type: typeof record.type === "string" ? record.type : "text",
132
+ text: typeof record.text === "string" ? record.text : undefined,
133
+ },
134
+ ];
135
+ });
136
+ }
137
+
138
+ function textFromContent(content: unknown): string {
139
+ if (!Array.isArray(content)) return "";
140
+ return content
141
+ .flatMap((part) => {
142
+ const item = asRecord(part);
143
+ if (!item) return [];
144
+ if (item.type === "text" && typeof item.text === "string") return [item.text];
145
+ return [];
146
+ })
147
+ .join("\n")
148
+ .trim();
149
+ }
150
+
151
+ /**
152
+ * Parse compacted event records into display items.
153
+ *
154
+ * The compaction in `child-pi-streams.ts` drops `toolCallId` from tool events,
155
+ * so `tool_execution_start` and its result are paired by tool name in arrival
156
+ * order — the nearest unmatched start with the same name. This is correct for
157
+ * sequential tool use; concurrent same-name tools are rare and the worst case
158
+ * is a result landing on the wrong start, not a crash.
159
+ *
160
+ * `message_end` events carry the full compacted `Message`. Assistant messages
161
+ * produce text items; tool cards come from `tool_execution_start` events.
162
+ * Results arrive either as role:"toolResult" message_end records (pi ≥0.84,
163
+ * no tool name — folded FIFO) or as toolResult parts inside older assistant
164
+ * messages (named — folded by name).
165
+ */
166
+ function parseEventRecord(record: Record<string, unknown>, pending: Map<string, PendingTool>): CrewTranscriptItem[] {
167
+ const seq = typeof record.seq === "number" ? record.seq : 0;
168
+ const event = asRecord(record.event) ?? record;
169
+ const type = typeof event.type === "string" ? event.type : "";
170
+
171
+ const items: CrewTranscriptItem[] = [];
172
+
173
+ if (type === "tool_execution_start") {
174
+ const name = typeof event.toolName === "string" ? event.toolName : "tool";
175
+ const args = (event.args as Record<string, unknown>) ?? {};
176
+ const id = `${name}#${seq}`;
177
+ const item: PendingTool = { type: "tool", name, toolCallId: id, args, seq };
178
+ pending.set(id, item);
179
+ items.push(item);
180
+ return items;
181
+ }
182
+
183
+ if (type === "tool_execution_end") {
184
+ // The compacted end event carries NO result (child-pi-streams.ts keeps
185
+ // only type/toolName/args). The actual result arrives as the next
186
+ // message_end's toolResult content part, so the pending start must stay
187
+ // in the map until that fold — removing it here would leave the card
188
+ // permanently stuck in "started, no result". Nothing to emit.
189
+ return items;
190
+ }
191
+
192
+ if (type === "message_end" || type === "message" || type === "tool_result_end") {
193
+ const message = asRecord(event.message);
194
+ if (!message) return items;
195
+
196
+ if (message.role === "toolResult") {
197
+ // pi ≥0.84 emits each tool result as its OWN message_end (role
198
+ // "toolResult", content parts, NO tool name) instead of toolResult
199
+ // parts inside the assistant message. Results arrive in the
200
+ // originating message's toolCall order, and starts are pushed in
201
+ // that same order, so fold FIFO into the oldest pending start that
202
+ // has no result yet — correct for concurrent tools too.
203
+ const result = {
204
+ content: normalizeResultContent(message.content),
205
+ isError: message.isError === true,
206
+ };
207
+ for (const [id, item] of pending) {
208
+ if (item.result !== undefined) continue;
209
+ item.result = result;
210
+ item.isError = result.isError;
211
+ pending.delete(id);
212
+ break;
213
+ }
214
+ return items;
215
+ }
216
+
217
+ if (message.role === "assistant") {
218
+ const content = Array.isArray(message.content) ? message.content : [];
219
+ const text = textFromContent(content);
220
+ if (text) {
221
+ // Compaction can carry usage at the RECORD level (usage-only
222
+ // tail) instead of inside the message — merge it in so the
223
+ // pane's full-message render and usage footer see it.
224
+ const recordUsage = asRecord(event.usage);
225
+ const messageUsage = asRecord(message.usage);
226
+ let merged = message;
227
+ if (recordUsage) {
228
+ merged = { ...message, usage: messageUsage ? { ...messageUsage, ...recordUsage } : recordUsage };
229
+ }
230
+ items.push({ type: "assistant", text, seq, message: merged, usage: normalizeUsage(merged.usage) });
231
+ }
232
+ // toolResult parts carry name + content; fold them into pending starts
233
+ // (normalized to the same updateResult envelope as the pi ≥0.84
234
+ // role:"toolResult" messages above).
235
+ for (const part of content) {
236
+ const item = asRecord(part);
237
+ if (item?.type !== "toolResult") continue;
238
+ const name = typeof item.name === "string" ? item.name : "tool";
239
+ const isError = item.isError === true;
240
+ matchPending(
241
+ pending,
242
+ name,
243
+ {
244
+ content: normalizeResultContent(item.content),
245
+ isError,
246
+ },
247
+ isError,
248
+ );
249
+ }
250
+ return items;
251
+ }
252
+
253
+ if (message.role === "user") {
254
+ const text = textFromContent(message.content);
255
+ if (text) items.push({ type: "user", text, seq });
256
+ return items;
257
+ }
258
+ }
259
+
260
+ // System / other events become a dim system line.
261
+ if (type && type !== "message_update") {
262
+ const text = typeof event.text === "string" ? event.text : "";
263
+ if (text) items.push({ type: "system", text, seq });
264
+ }
265
+
266
+ return items;
267
+ }
268
+
269
+ function matchPending(
270
+ pending: Map<string, PendingTool>,
271
+ name: string,
272
+ result: { content: Array<{ type: string; text?: string }>; isError: boolean },
273
+ isError: boolean | undefined,
274
+ ): void {
275
+ for (const [id, item] of [...pending.entries()].reverse()) {
276
+ if (item.name !== name) continue;
277
+ pending.delete(id);
278
+ if (result !== undefined) item.result = result;
279
+ if (isError !== undefined) item.isError = isError;
280
+ return;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Read new events and return the accumulated transcript (most recent
286
+ * MAX_TRANSCRIPT_ITEMS, chronological). Cheap when nothing new arrived: the
287
+ * cursor skips re-parse and the existing buffer is returned as-is.
288
+ */
289
+ export function readAgentTranscript(manifest: TeamRunManifest, taskId: string): CrewTranscriptItem[] {
290
+ const sinceSeq = cursors.get(taskId) ?? 0;
291
+ const { events, nextSeq } = readCrewAgentEventsCursor(manifest, taskId, { sinceSeq });
292
+ if (nextSeq > sinceSeq) cursors.set(taskId, nextSeq);
293
+
294
+ let buffer = buffers.get(taskId) ?? [];
295
+ if (events.length > 0) {
296
+ const pending = pendingByTask.get(taskId) ?? new Map<string, PendingTool>();
297
+ pendingByTask.set(taskId, pending);
298
+
299
+ for (const record of events) {
300
+ const parsed = asRecord(record);
301
+ if (!parsed) continue;
302
+ buffer.push(...parseEventRecord(parsed, pending));
303
+ }
304
+ }
305
+ if (buffer.length > 0 && !promptSeeded.has(taskId)) {
306
+ const prompt = readWorkerPrompt(manifest, taskId);
307
+ if (prompt !== undefined) {
308
+ promptSeeded.add(taskId);
309
+ buffer.unshift({ type: "user", text: prompt, seq: 0 });
310
+ }
311
+ }
312
+ if (buffer.length > MAX_TRANSCRIPT_ITEMS) {
313
+ buffer = buffer.slice(buffer.length - MAX_TRANSCRIPT_ITEMS);
314
+ }
315
+ buffers.set(taskId, buffer);
316
+ return buffer;
317
+ }
318
+
319
+ /** Drop everything learned about a task (used when the pane switches agents). */
320
+ export function resetAgentTranscriptCursor(taskId: string): void {
321
+ cursors.delete(taskId);
322
+ buffers.delete(taskId);
323
+ pendingByTask.delete(taskId);
324
+ promptSeeded.delete(taskId);
325
+ }
326
+
327
+ /** Clear all per-task state (session teardown / test isolation). */
328
+ export function resetAllAgentTranscriptCursors(): void {
329
+ cursors.clear();
330
+ buffers.clear();
331
+ pendingByTask.clear();
332
+ promptSeeded.clear();
333
+ }
334
+
335
+ /** Test-only: whether the module currently holds state for a task. */
336
+ export function __hasAgentTranscriptState(taskId: string): boolean {
337
+ return buffers.has(taskId) || cursors.has(taskId) || pendingByTask.has(taskId);
338
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * agent-view-overlay.ts — the agent view as a FULL-SCREEN, separate surface.
3
+ *
4
+ * Opened through `ctx.ui.custom(..., { overlay: true })` with width "100%"
5
+ * and zero margins, so viewing an agent TAKES OVER the terminal instead of
6
+ * flowing under the main transcript: what the user sees is that agent's live
7
+ * session and nothing else. The main session keeps running underneath (its
8
+ * editor, widgets and run state are untouched) and comes back the moment the
9
+ * overlay closes — no session is switched, resumed, or torn down, so a view
10
+ * can never kill a run.
11
+ *
12
+ * Keyboard (the overlay captures focus while open):
13
+ * esc / q / ctrl+c close, back to the main conversation
14
+ * pgup / pgdn / ↑ ↓ / j k / g G scroll the transcript
15
+ * tab / shift+tab switch to the next / previous agent in place
16
+ * i steer input — enter sends to the viewed agent, esc cancels
17
+ *
18
+ * The transcript itself is `CrewAgentPane` (shared with the widget-mode
19
+ * view): pi's own message/tool components, disk-tailed live, per-assistant
20
+ * usage footers — session parity, full height.
21
+ */
22
+
23
+ import type { Theme } from "@earendil-works/pi-coding-agent";
24
+ import { matchesKey, type TUI, truncateToWidth } from "@earendil-works/pi-tui";
25
+ import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
26
+ import { CrewAgentPane } from "./agent-pane.ts";
27
+ import type { PanelTarget } from "./panel-selection.ts";
28
+ import { getViewedAgent, panelRows, setViewedAgent } from "./panel-store.ts";
29
+
30
+ export interface CrewAgentOverlayOptions {
31
+ /** Close the view and restore the main conversation. */
32
+ close(): void;
33
+ /** Deliver a steer message to the viewed agent. */
34
+ steer(target: PanelTarget, message: string): void;
35
+ }
36
+
37
+ const INPUT_PROMPT = " ❯ ";
38
+
39
+ /** Printable single keypress or paste chunk: no control bytes, no ESC
40
+ * sequences (arrow keys etc. must not land in the steer text). */
41
+ function isPrintableInput(data: string): boolean {
42
+ return data.length > 0 && !data.startsWith("\x1b") && !/[\x00-\x1f\x7f]/.test(data);
43
+ }
44
+
45
+ export class CrewAgentOverlay {
46
+ /** The transcript core; exposed so the host can wire scroll/steer keys. */
47
+ readonly pane: CrewAgentPane;
48
+
49
+ private disposed = false;
50
+ private inputMode = false;
51
+ private inputText = "";
52
+ private closed = false;
53
+ private tui: TUI;
54
+ private theme: CrewTheme;
55
+ private readonly options: CrewAgentOverlayOptions;
56
+
57
+ constructor(tui: TUI, theme: Theme, cwd: string, options: CrewAgentOverlayOptions) {
58
+ this.tui = tui;
59
+ this.theme = asCrewTheme(theme);
60
+ this.options = options;
61
+ this.pane = new CrewAgentPane(tui, theme, cwd, {
62
+ // Fill the terminal: overlay chrome (hint + optional input row)
63
+ // plus the pane's own header/border/indicator lines.
64
+ maxBodyLines: (headerLines) => this.availableBodyLines(headerLines),
65
+ });
66
+ }
67
+
68
+ /** Idempotent: the escape key and the host's close path share this. */
69
+ requestClose(): void {
70
+ if (this.closed) return;
71
+ this.closed = true;
72
+ this.options.close();
73
+ }
74
+
75
+ requestRender(): void {
76
+ if (!this.disposed) this.tui.requestRender();
77
+ }
78
+
79
+ private chromeLines(): number {
80
+ return 1 /* hint */ + (this.inputMode ? 1 /* input row */ : 0);
81
+ }
82
+
83
+ private availableBodyLines(headerLines: number): number {
84
+ const rows = this.tui.terminal.rows;
85
+ // pane self-chrome: border + spacer/"more" indicator (+1 slack for the
86
+ // bottom "↓ more" indicator that appears while scrolled).
87
+ return Math.max(4, rows - headerLines - 4 - this.chromeLines());
88
+ }
89
+
90
+ private hintLine(width: number): string {
91
+ if (this.inputMode) {
92
+ return this.theme.fg("dim", truncateToWidth(" enter send · esc cancel steer", width, "…"));
93
+ }
94
+ const viewed = getViewedAgent();
95
+ const rows = panelRows();
96
+ const row = viewed ? rows.find((r) => r.runId === viewed.runId && r.taskId === viewed.taskId) : undefined;
97
+ const label = row?.name ? `@${row.name.replace(/\s+/g, " ")}` : "agent";
98
+ const multi = rows.length > 1 ? " · tab agent" : "";
99
+ return this.theme.fg("dim", truncateToWidth(` viewing ${label}${multi} · pgup/pgdn scroll · i steer · esc back`, width, "…"));
100
+ }
101
+
102
+ private inputLine(width: number): string {
103
+ const text = `${INPUT_PROMPT}${this.inputText}▏`;
104
+ return this.theme.fg("accent", truncateToWidth(text, width, "…"));
105
+ }
106
+
107
+ /** Switch the view to the next (delta=+1) or previous (delta=-1) agent. */
108
+ private cycleAgent(delta: 1 | -1): void {
109
+ const rows = panelRows();
110
+ if (rows.length === 0) return;
111
+ const viewed = getViewedAgent();
112
+ const index = viewed ? rows.findIndex((r) => r.runId === viewed.runId && r.taskId === viewed.taskId) : -1;
113
+ const next = rows[(((index + delta) % rows.length) + rows.length) % rows.length];
114
+ if (!next) return;
115
+ if (viewed && next.runId === viewed.runId && next.taskId === viewed.taskId) return;
116
+ setViewedAgent({ runId: next.runId, taskId: next.taskId });
117
+ this.pane.requestRender();
118
+ }
119
+
120
+ private sendSteer(): void {
121
+ const text = this.inputText.trim();
122
+ const viewed = getViewedAgent();
123
+ this.inputMode = false;
124
+ this.inputText = "";
125
+ if (!text || !viewed) return;
126
+ this.options.steer(viewed, text);
127
+ }
128
+
129
+ handleInput(data: string): void {
130
+ if (this.disposed || this.closed) return;
131
+
132
+ if (this.inputMode) {
133
+ if (matchesKey(data, "escape")) {
134
+ this.inputMode = false;
135
+ this.inputText = "";
136
+ } else if (matchesKey(data, "return")) {
137
+ this.sendSteer();
138
+ } else if (matchesKey(data, "backspace")) {
139
+ this.inputText = this.inputText.slice(0, -1);
140
+ } else if (data === "\x03") {
141
+ // ctrl+c inside steer input cancels the input, not the view.
142
+ this.inputMode = false;
143
+ this.inputText = "";
144
+ } else if (isPrintableInput(data)) {
145
+ // Printable (including multi-byte UTF-8 and bracketed paste).
146
+ this.inputText += data;
147
+ }
148
+ this.tui.requestRender();
149
+ return;
150
+ }
151
+
152
+ if (matchesKey(data, "escape") || data === "\x03") {
153
+ this.requestClose();
154
+ return;
155
+ }
156
+ if (data === "q") {
157
+ this.requestClose();
158
+ return;
159
+ }
160
+ if (matchesKey(data, "pageUp")) {
161
+ this.pane.scrollBy(10);
162
+ return;
163
+ }
164
+ if (matchesKey(data, "pageDown")) {
165
+ this.pane.scrollBy(-10);
166
+ return;
167
+ }
168
+ if (matchesKey(data, "up") || data === "k") {
169
+ this.pane.scrollBy(1);
170
+ return;
171
+ }
172
+ if (matchesKey(data, "down") || data === "j") {
173
+ this.pane.scrollBy(-1);
174
+ return;
175
+ }
176
+ if (data === "g" || matchesKey(data, "home")) {
177
+ this.pane.scrollHome();
178
+ return;
179
+ }
180
+ if (data === "G" || matchesKey(data, "end")) {
181
+ this.pane.scrollEnd();
182
+ return;
183
+ }
184
+ if (matchesKey(data, "tab")) {
185
+ this.cycleAgent(1);
186
+ return;
187
+ }
188
+ if (matchesKey(data, "shift+tab")) {
189
+ this.cycleAgent(-1);
190
+ return;
191
+ }
192
+ if (data === "i") {
193
+ this.inputMode = true;
194
+ this.inputText = "";
195
+ this.tui.requestRender();
196
+ return;
197
+ }
198
+ // Everything else (incl. kitty release events) is ignored — the view
199
+ // never leaks keys into the main editor underneath.
200
+ }
201
+
202
+ render(width: number): string[] {
203
+ if (this.disposed || this.closed) return [];
204
+
205
+ const rows = this.tui.terminal.rows;
206
+ const lines = this.pane.render(width);
207
+ // Pad so the overlay always covers the full terminal — the session
208
+ // underneath must not bleed through on short transcripts.
209
+ const bodyBudget = rows - this.chromeLines();
210
+ while (lines.length < bodyBudget) lines.push("");
211
+ if (lines.length > bodyBudget) lines.length = bodyBudget;
212
+ if (this.inputMode) lines.push(this.inputLine(width));
213
+ lines.push(this.hintLine(width));
214
+ return lines.slice(0, Math.max(1, rows));
215
+ }
216
+
217
+ invalidate(): void {
218
+ this.pane.invalidate();
219
+ }
220
+
221
+ dispose(): void {
222
+ this.disposed = true;
223
+ this.pane.dispose();
224
+ }
225
+ }