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,375 @@
1
+ /**
2
+ * agent-pane.ts — full-width transcript pane for a single agent.
3
+ *
4
+ * The rendering core behind the agent view: it tails the agent's on-disk
5
+ * event log and paints a session-shaped transcript. The FULL-SCREEN view
6
+ * (agent-view-overlay.ts) embeds it with `maxBodyLines` computed from the
7
+ * terminal size; a widget-mode instantiation keeps the historical
8
+ * `aboveEditor` layout formula (terminal minus a fixed reserve).
9
+ *
10
+ * Items render through pi's own transcript components (UserMessageComponent,
11
+ * ToolExecutionComponent, Markdown) so the pane has visual parity with the
12
+ * main conversation: real tool cards, markdown rendering, diffs. Components
13
+ * are cached in a WeakMap keyed on the transcript item and dropped on theme
14
+ * change or when a tool item gains its result.
15
+ */
16
+
17
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
18
+ import type { Theme } from "@earendil-works/pi-coding-agent";
19
+ import {
20
+ AssistantMessageComponent,
21
+ DynamicBorder,
22
+ getMarkdownTheme,
23
+ ToolExecutionComponent,
24
+ UserMessageComponent,
25
+ } from "@earendil-works/pi-coding-agent";
26
+ import { Markdown, type TUI, truncateToWidth } from "@earendil-works/pi-tui";
27
+
28
+ import { loadRunManifestById } from "../../state/stores/state-store.ts";
29
+ import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
30
+ import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
31
+ import { type CrewTranscriptItem, readAgentTranscript, resetAgentTranscriptCursor } from "./agent-transcript.ts";
32
+ import { getViewedAgent, subscribePanelChange } from "./panel-store.ts";
33
+
34
+ const MAX_BODY_FRACTION = 14;
35
+ /** Minimum gap between disk re-reads while the pane is open (transcript-viewer parity). */
36
+ const TRANSCRIPT_READ_THROTTLE_MS = 500;
37
+ /** Re-check the manifest/tasks every this often so the header's status/model
38
+ * stay current without a disk read on every host tick. */
39
+ const MANIFEST_REFRESH_MS = 1500;
40
+ /** Terminal task statuses — after these the header shows the full-session hint. */
41
+ const TERMINAL_TASK_STATUSES = new Set(["completed", "failed", "cancelled"]);
42
+
43
+ function formatTokenCount(count: number): string {
44
+ if (count < 1000) return String(count);
45
+ if (count < 1_000_000) return `${Math.round(count / 1000)}k`;
46
+ return `${(count / 1_000_000).toFixed(1)}M`;
47
+ }
48
+
49
+ /** pi-style per-turn usage footer: ↑in ↓out Rcache $cost (pi-subtask parity). */
50
+ function usageFooterLine(usage: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: { total: number } }): string {
51
+ const parts: string[] = [];
52
+ if (usage.input) parts.push(`↑${formatTokenCount(usage.input)}`);
53
+ if (usage.output) parts.push(`↓${formatTokenCount(usage.output)}`);
54
+ if (usage.cacheRead) parts.push(`R${formatTokenCount(usage.cacheRead)}`);
55
+ const promptTokens = usage.input + usage.cacheRead;
56
+ if (usage.cacheRead && promptTokens > 0) {
57
+ parts.push(`CH${((usage.cacheRead / promptTokens) * 100).toFixed(1)}%`);
58
+ }
59
+ if (usage.cost.total) parts.push(`$${usage.cost.total.toFixed(4)}`);
60
+ return parts.join(" ");
61
+ }
62
+
63
+ interface Renderable {
64
+ render(width: number): string[];
65
+ }
66
+
67
+ export interface CrewAgentPaneOptions {
68
+ /** Overlay mode: compute the body's max lines from the terminal size,
69
+ * instead of the widget formula (which reserves room for the transcript
70
+ * above the editor). Evaluated on every render so live chrome (the
71
+ * overlay's input row) is accounted for. */
72
+ maxBodyLines?: (headerLines: number) => number;
73
+ }
74
+
75
+ /**
76
+ * The pane is a window onto the run, never its owner: closing it must not
77
+ * touch worker state. `dispose()` detaches only.
78
+ */
79
+ export class CrewAgentPane {
80
+ private disposed = false;
81
+ /** Wrapped-line offset from the END of the transcript; 0 = tailing. */
82
+ scrollBack = 0;
83
+ private tui: TUI;
84
+ private theme: CrewTheme;
85
+ private cwd: string;
86
+ private readonly paneOptions: CrewAgentPaneOptions | undefined;
87
+ /** Current agent target, read fresh from the panel store each render. */
88
+ private currentTaskId: string | undefined;
89
+ /** Cached manifest for the current run, to avoid re-reading on every tick. */
90
+ private cachedRunId: string | undefined;
91
+ private cachedManifest: TeamRunManifest | undefined;
92
+ /** Tasks for the cached run (header shows the agent's real name/status). */
93
+ private cachedTasks: TeamTaskState[] = [];
94
+ /** Last time the manifest was re-read for header freshness. */
95
+ private lastManifestRefreshAt = 0;
96
+
97
+ private componentCache = new WeakMap<CrewTranscriptItem, Renderable>();
98
+ /** Last disk read, so the open pane does not re-parse the JSONL every tick. */
99
+ private lastTranscriptReadAt = 0;
100
+ /** Most recent parsed items; kept alive because componentCache holds weak refs to them. */
101
+ private lastItems: CrewTranscriptItem[] = [];
102
+ /**
103
+ * Rendered-body cache. Rebuilding the pane re-parses every Markdown item,
104
+ * which on a 500-item transcript is the bulk of each ~160ms host tick.
105
+ * The fingerprint covers identity-relevant bits (seq, type, text length,
106
+ * result presence) so tool-result folds still refresh the pane.
107
+ */
108
+ private bodyKey = 0;
109
+ private cachedBody: string[] = [];
110
+ private unsubscribePanel: () => void;
111
+
112
+ constructor(tui: TUI, theme: Theme, cwd: string, options?: CrewAgentPaneOptions) {
113
+ this.tui = tui;
114
+ this.theme = asCrewTheme(theme);
115
+ this.cwd = cwd;
116
+ this.paneOptions = options;
117
+ // Pane open/close/switch is a panel-store change; repaint immediately
118
+ // instead of waiting for the next host tick, so the overlay swap is
119
+ // instant even when the underlying run state is idle.
120
+ this.unsubscribePanel = subscribePanelChange(() => {
121
+ if (!this.disposed) this.tui.requestRender();
122
+ });
123
+ }
124
+
125
+ requestRender(): void {
126
+ if (!this.disposed) this.tui.requestRender();
127
+ }
128
+
129
+ scrollBy(delta: number): void {
130
+ this.scrollBack = Math.max(0, this.scrollBack + delta);
131
+ this.tui.requestRender();
132
+ }
133
+
134
+ /** Jump to the top of the buffered transcript (clamped on next render). */
135
+ scrollHome(): void {
136
+ this.scrollBack = Number.MAX_SAFE_INTEGER;
137
+ this.tui.requestRender();
138
+ }
139
+
140
+ /** Jump back to tailing the live transcript. */
141
+ scrollEnd(): void {
142
+ this.scrollBack = 0;
143
+ this.tui.requestRender();
144
+ }
145
+
146
+ private resolveManifest(runId: string): TeamRunManifest | undefined {
147
+ const stale = Date.now() - this.lastManifestRefreshAt >= MANIFEST_REFRESH_MS;
148
+ if (this.cachedRunId === runId && this.cachedManifest) {
149
+ if (stale) {
150
+ this.lastManifestRefreshAt = Date.now();
151
+ const refreshed = loadRunManifestById(this.cwd, runId);
152
+ if (refreshed) {
153
+ this.cachedManifest = refreshed.manifest;
154
+ this.cachedTasks = refreshed.tasks ?? [];
155
+ }
156
+ }
157
+ return this.cachedManifest;
158
+ }
159
+ const loaded = loadRunManifestById(this.cwd, runId);
160
+ if (!loaded) return undefined;
161
+ this.cachedRunId = runId;
162
+ this.cachedManifest = loaded.manifest;
163
+ this.cachedTasks = loaded.tasks ?? [];
164
+ this.lastManifestRefreshAt = Date.now();
165
+ return loaded.manifest;
166
+ }
167
+
168
+ private itemComponent(item: CrewTranscriptItem): Renderable {
169
+ const cached = this.componentCache.get(item);
170
+ if (cached) {
171
+ // Tool results land by MUTATING the pending item in place (the
172
+ // parser folds role:"toolResult" records into it), so a card
173
+ // cached while "started" must pick the result up here — otherwise
174
+ // it stays a running card forever. Re-updating an already-resulted
175
+ // card is a no-op render refresh.
176
+ if (item.type === "tool" && item.result !== undefined) {
177
+ try {
178
+ (cached as ToolExecutionComponent).updateResult(item.result);
179
+ } catch {
180
+ /* shape drift across pi versions — keep the started card */
181
+ }
182
+ }
183
+ return cached;
184
+ }
185
+
186
+ let comp: Renderable;
187
+ if (item.type === "user") {
188
+ comp = new UserMessageComponent(item.text);
189
+ } else if (item.type === "assistant") {
190
+ // Full-message parity with a real pi session: pi's own assistant
191
+ // component renders text, thinking blocks, and tool calls from the
192
+ // compacted message. Falls back to markdown for old event logs
193
+ // that predate message retention.
194
+ comp = item.message
195
+ ? new AssistantMessageComponent(item.message as unknown as AssistantMessage)
196
+ : new Markdown(item.text.trim(), 0, 0, getMarkdownTheme());
197
+ } else if (item.type === "system") {
198
+ // System lines are plain text — no component needed.
199
+ comp = {
200
+ render: () => [this.theme.fg("dim", truncateToWidth(item.text, 200, "…"))],
201
+ };
202
+ } else {
203
+ // Tool item: use pi's own ToolExecutionComponent for visual parity.
204
+ const tool = new ToolExecutionComponent(item.name, item.toolCallId, item.args, {}, undefined, this.tui, this.cwd);
205
+ tool.markExecutionStarted();
206
+ if (item.result !== undefined) {
207
+ tool.updateResult(item.result as never);
208
+ }
209
+ comp = tool;
210
+ }
211
+
212
+ this.componentCache.set(item, comp);
213
+ return comp;
214
+ }
215
+
216
+ /**
217
+ * Render one transcript item to lines, degrading to a dim text line when
218
+ * the item's shape confuses a pi component (the JSONL is worker-written
219
+ * and unvalidated — a once-bad record must never kill the pane render).
220
+ */
221
+ private renderItem(item: CrewTranscriptItem, width: number): string[] {
222
+ try {
223
+ return this.itemComponent(item).render(width);
224
+ } catch {
225
+ const label = item.type === "tool" ? item.name : "text";
226
+ return [this.theme.fg("dim", truncateToWidth(`(unrenderable ${label} item)`, width, "…"))];
227
+ }
228
+ }
229
+
230
+ /** FNV-1a over the parts that change pane output; 32-bit is plenty for a
231
+ * 500-item cache key (a collision only delays a repaint by one tick). */
232
+ private bodyFingerprint(items: readonly CrewTranscriptItem[], width: number): number {
233
+ let h = (2166136261 ^ width) >>> 0;
234
+ for (const item of items) {
235
+ h ^= item.seq;
236
+ h = Math.imul(h, 16777619) >>> 0;
237
+ h ^= item.type.length;
238
+ h = Math.imul(h, 16777619) >>> 0;
239
+ if (item.type === "tool") {
240
+ h ^= item.result !== undefined ? 7 : 3;
241
+ } else {
242
+ h ^= item.text.length;
243
+ }
244
+ h = Math.imul(h, 16777619) >>> 0;
245
+ }
246
+ return h;
247
+ }
248
+
249
+ private buildBody(items: readonly CrewTranscriptItem[], width: number): string[] {
250
+ const body: string[] = [];
251
+ for (const item of items) {
252
+ for (const line of this.renderItem(item, width)) body.push(line);
253
+ if (item.type === "assistant" && item.usage) {
254
+ // pi's session transcript prints a usage footer under each
255
+ // assistant message; mirror it so the pane reads like one.
256
+ const footer = usageFooterLine(item.usage);
257
+ if (footer) body.push(this.theme.fg("dim", footer));
258
+ }
259
+ }
260
+ return body;
261
+ }
262
+
263
+ /** Session-style header: agent name · task · run · model · state. */
264
+ private headerLines(manifest: TeamRunManifest, width: number): string[] {
265
+ const t = this.theme;
266
+ const task = this.cachedTasks.find((candidate) => candidate.id === this.currentTaskId);
267
+ const name = task?.displayName ?? task?.title ?? this.currentTaskId ?? "agent";
268
+ const model = manifest.modelContext?.parentModel ?? manifest.modelContext?.override;
269
+ const parts: string[] = [
270
+ t.fg("accent", t.bold(name)),
271
+ t.fg("dim", `· ${this.currentTaskId ?? ""}`),
272
+ t.fg("dim", `· …${manifest.runId.slice(-12)}`),
273
+ ];
274
+ if (model) parts.push(t.fg("dim", `· ${model}`));
275
+
276
+ const status = task?.status ?? manifest.status;
277
+ let stateText = "";
278
+ if (status === "completed") stateText = t.fg("success", "✓ completed");
279
+ else if (status === "failed") stateText = t.fg("error", "✗ failed");
280
+ else if (status === "cancelled") stateText = t.fg("warning", "■ cancelled");
281
+ else if (status === "running") stateText = t.fg("success", "● running");
282
+ else if (status) stateText = t.fg("dim", status);
283
+ const line = truncateToWidth(`${parts.join(" ")}${stateText ? ` ${stateText}` : ""}`, width, "…");
284
+ const lines = [line];
285
+ if (task && TERMINAL_TASK_STATUSES.has(task.status)) {
286
+ lines.push(t.fg("dim", truncateToWidth(`· finished — full log: /team-dashboard or artifacts/${manifest.runId}`, width, "…")));
287
+ }
288
+ return lines;
289
+ }
290
+
291
+ render(width: number): string[] {
292
+ if (this.disposed) return [];
293
+
294
+ const viewed = getViewedAgent();
295
+ if (!viewed) return [];
296
+
297
+ // Detect agent switch: reset the transcript cursor so the pane shows
298
+ // the new agent's full history instead of continuing from the old cursor.
299
+ if (this.currentTaskId !== viewed.taskId) {
300
+ resetAgentTranscriptCursor(viewed.taskId);
301
+ this.currentTaskId = viewed.taskId;
302
+ this.scrollBack = 0;
303
+ this.lastItems = [];
304
+ this.lastTranscriptReadAt = 0;
305
+ this.bodyKey = 0;
306
+ this.cachedBody = [];
307
+ }
308
+
309
+ const manifest = this.resolveManifest(viewed.runId);
310
+ if (!manifest) return [this.theme.fg("dim", "(run manifest unavailable)")];
311
+
312
+ // Throttled disk read: the host repaints on its own ~160ms cadence, but
313
+ // the JSONL only advances when the worker does something.
314
+ if (Date.now() - this.lastTranscriptReadAt >= TRANSCRIPT_READ_THROTTLE_MS) {
315
+ this.lastItems = readAgentTranscript(manifest, viewed.taskId);
316
+ this.lastTranscriptReadAt = Date.now();
317
+ }
318
+ const items = this.lastItems;
319
+
320
+ // Render every item at full width — pi's message components paint their
321
+ // own edge-to-edge background and carry their own padding, so indenting
322
+ // them leaves column 0 unpainted and notches the corners. The body is
323
+ // cached under a content fingerprint; unchanged state skips the
324
+ // Markdown re-parse on every host tick.
325
+ const fingerprint = this.bodyFingerprint(items, width);
326
+ if (fingerprint !== this.bodyKey) {
327
+ this.cachedBody = this.buildBody(items, width);
328
+ this.bodyKey = fingerprint;
329
+ }
330
+ const body = this.cachedBody;
331
+
332
+ // Height: in widget mode, size to content capped so the transcript
333
+ // above and the editor/footer below stay reachable on small terminals.
334
+ // In overlay mode the pane is the whole screen: the caller computes
335
+ // the body budget from the terminal size minus its own chrome.
336
+ const header = this.headerLines(manifest, width);
337
+ const rows = this.tui.terminal.rows;
338
+ const maxBody = this.paneOptions?.maxBodyLines
339
+ ? Math.max(4, this.paneOptions.maxBodyLines(header.length))
340
+ : Math.max(6, rows - MAX_BODY_FRACTION - header.length);
341
+ const visibleCount = Math.min(maxBody, Math.max(1, body.length));
342
+ this.scrollBack = Math.max(0, Math.min(this.scrollBack, Math.max(0, body.length - visibleCount)));
343
+ const end = body.length - this.scrollBack;
344
+ const visible = body.slice(Math.max(0, end - visibleCount), end);
345
+
346
+ const lines: string[] = [];
347
+ // Session-style header first, then the border, then the transcript.
348
+ for (const line of header) lines.push(line);
349
+ lines.push(...new DynamicBorder((str) => this.theme.fg("border", str)).render(width));
350
+ if (end - visibleCount > 0) {
351
+ lines.push(this.theme.fg("dim", ` ↑ ${Math.max(0, end - visibleCount)} more line(s) (pageUp)`));
352
+ } else {
353
+ lines.push("");
354
+ }
355
+ for (const line of visible) lines.push(line);
356
+ if (this.scrollBack > 0) {
357
+ lines.push(this.theme.fg("dim", ` ↓ ${this.scrollBack} more line(s) (pageDown)`));
358
+ }
359
+ return lines;
360
+ }
361
+
362
+ invalidate(): void {
363
+ // Components cache theme colors internally; rebuild on theme change.
364
+ this.componentCache = new WeakMap();
365
+ this.bodyKey = 0;
366
+ this.cachedBody = [];
367
+ }
368
+
369
+ dispose(): void {
370
+ this.disposed = true;
371
+ this.unsubscribePanel();
372
+ this.cachedManifest = undefined;
373
+ this.cachedRunId = undefined;
374
+ }
375
+ }