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,192 @@
1
+ /**
2
+ * crew-editor.ts — the inline agent panel's keyboard half.
3
+ *
4
+ * A `CustomEditor` wrapper (pi-subtask's `SubtaskEditor` pattern): pressing
5
+ * `↓` on an empty prompt moves the cursor into the agent rows rendered by the
6
+ * crew widget; `↑`/`↓` navigate, `enter` opens the agent's live transcript
7
+ * pane, `x` cancels/dismisses, `escape` returns to typing.
8
+ *
9
+ * While an agent is viewed, typed text goes to the viewed agent (Claude
10
+ * Code's `@name` convention — the editor border is relabeled), pageUp/
11
+ * pageDown scroll, and `enter` steers. Normally the FULL-SCREEN overlay
12
+ * (agent-view-overlay.ts) owns the keyboard while open; this branch is the
13
+ * fallback for when the overlay could not spawn (e.g. a stale ctx between
14
+ * keypress and open), so the user is never stuck viewing an agent neither
15
+ * surface controls. Neither path is a session switch: the main conversation
16
+ * is never torn down to look at an agent.
17
+ *
18
+ * Every unhandled key falls through to `super.handleInput`, so the user is
19
+ * never trapped in a mode. All state lives in panel-store (shared with the
20
+ * widget renderer); this class only translates keys into state changes and
21
+ * host calls.
22
+ */
23
+
24
+ import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
25
+ import { CustomEditor } from "@earendil-works/pi-coding-agent";
26
+ import { type EditorTheme, matchesKey, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
27
+
28
+ import type { PanelKeys, PanelTarget } from "./panel-selection.ts";
29
+ import { dispatchPanelKey } from "./panel-selection.ts";
30
+ import { getPanelSelection, getViewedAgent, panelRows, setPanelSelection } from "./panel-store.ts";
31
+
32
+ export const AGENT_LABEL_MAX = 24;
33
+
34
+ export interface CrewEditorOptions {
35
+ /** Open the transcript pane on the given agent. */
36
+ onOpenPane: (target: PanelTarget) => void;
37
+ /** Close the pane and return to the main conversation. */
38
+ onClosePane: () => void;
39
+ /** Scroll the open pane by ±wrapped lines. */
40
+ onScrollPane: (delta: number) => void;
41
+ /** Steer the viewed agent with the typed message. */
42
+ onSteer: (target: PanelTarget, message: string) => void;
43
+ /** `x`: cancel a running agent's run, or dismiss a finished one. */
44
+ onAct: (target: PanelTarget, finished: boolean) => void;
45
+ }
46
+
47
+ export class CrewInlineEditor extends CustomEditor {
48
+ private readonly options: CrewEditorOptions;
49
+
50
+ constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options: CrewEditorOptions) {
51
+ super(tui, theme, keybindings);
52
+ this.options = options;
53
+ }
54
+
55
+ private panelKeys(data: string): PanelKeys {
56
+ return {
57
+ up: matchesKey(data, "up"),
58
+ down: matchesKey(data, "down"),
59
+ enter: matchesKey(data, "return"),
60
+ escape: matchesKey(data, "escape"),
61
+ act: matchesKey(data, "x"),
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Apply a dispatch result. `closePaneOnMain` is true while the pane is open:
67
+ * `enter` on the main row then closes the pane instead of doing nothing.
68
+ */
69
+ private applyDispatch(data: string, rows: ReturnType<typeof panelRows>, keys: PanelKeys, closePaneOnMain: boolean): void {
70
+ const selection = getPanelSelection();
71
+ const result = dispatchPanelKey(keys, rows, selection, { holdAtMain: closePaneOnMain });
72
+ switch (result.action.kind) {
73
+ case "none": {
74
+ // Not a navigation key: hand the editor the exact key and clear
75
+ // the cursor so typing proceeds normally (pi-subtask §2.5).
76
+ setPanelSelection(null);
77
+ super.handleInput(data);
78
+ return;
79
+ }
80
+ case "consumed": {
81
+ setPanelSelection(result.selection);
82
+ return;
83
+ }
84
+ case "open": {
85
+ setPanelSelection(null);
86
+ const target = result.action.target;
87
+ if (target) this.options.onOpenPane(target);
88
+ else if (closePaneOnMain) this.options.onClosePane();
89
+ return;
90
+ }
91
+ case "act": {
92
+ setPanelSelection(result.selection);
93
+ const target = result.action.target;
94
+ const row = rows.find((r) => r.runId === target.runId && r.taskId === target.taskId);
95
+ this.options.onAct(target, row?.finished ?? false);
96
+ return;
97
+ }
98
+ }
99
+ }
100
+
101
+ handleInput(data: string): void {
102
+ const rows = panelRows();
103
+ const viewed = getViewedAgent();
104
+
105
+ // ── Pane open: typing goes to the viewed agent ─────────────────────
106
+ // Navigation still works inside the pane: move the cursor over the
107
+ // rows and `enter` switches the pane to another agent (or closes it
108
+ // on the main row) — in place, no session involved.
109
+ if (viewed) {
110
+ if (getPanelSelection() !== null) {
111
+ this.applyDispatch(data, rows, this.panelKeys(data), true);
112
+ return;
113
+ }
114
+ if (matchesKey(data, "down") && this.getText() === "" && rows.length > 0) {
115
+ setPanelSelection("main");
116
+ return;
117
+ }
118
+ if (matchesKey(data, "escape")) {
119
+ this.options.onClosePane();
120
+ return;
121
+ }
122
+ if (matchesKey(data, "pageUp")) {
123
+ this.options.onScrollPane(10);
124
+ return;
125
+ }
126
+ if (matchesKey(data, "pageDown")) {
127
+ this.options.onScrollPane(-10);
128
+ return;
129
+ }
130
+ if (matchesKey(data, "return")) {
131
+ const text = (this.getExpandedText?.() ?? this.getText()).trim();
132
+ if (!text) {
133
+ // Empty enter: hand it back to the editor rather than
134
+ // swallowing the key (keeps the "never stick" contract).
135
+ super.handleInput(data);
136
+ return;
137
+ }
138
+ if (text.startsWith("/")) {
139
+ // Slash commands still act on the main session's command
140
+ // executor, like Claude Code's transcript view.
141
+ super.handleInput(data);
142
+ return;
143
+ }
144
+ // Shift+Enter (newline) never reaches here: matchesKey
145
+ // distinguishes the shifted sequence from plain return.
146
+ this.setText("");
147
+ this.options.onSteer(viewed, text);
148
+ return;
149
+ }
150
+ super.handleInput(data);
151
+ return;
152
+ }
153
+
154
+ // ── Idle: `↓` on an empty prompt enters the panel ──────────────────
155
+ if (getPanelSelection() === null) {
156
+ if (matchesKey(data, "down") && this.getText() === "" && rows.length > 0) {
157
+ // dispatch enters at the MAIN row; the widget renders that row
158
+ // with its own ❯ marker so the very first press is visible
159
+ // (pi-subtask's selectRow(rows, 0)).
160
+ const result = dispatchPanelKey(this.panelKeys(data), rows, null);
161
+ setPanelSelection(result.selection);
162
+ return;
163
+ }
164
+ super.handleInput(data);
165
+ return;
166
+ }
167
+
168
+ // ── Navigating: consume or fall through ────────────────────────────
169
+ this.applyDispatch(data, rows, this.panelKeys(data), false);
170
+ }
171
+
172
+ /**
173
+ * Relabel the editor's top border with the viewed agent, so it is
174
+ * unambiguous where typed text lands — pi-subtask's `@name` marker.
175
+ */
176
+ render(width: number): string[] {
177
+ const lines = super.render(width);
178
+ const viewed = getViewedAgent();
179
+ if (!viewed) return lines;
180
+ const rows = panelRows();
181
+ const row = rows.find((r) => r.runId === viewed.runId && r.taskId === viewed.taskId);
182
+ const name = row?.name ?? viewed.taskId.slice(-AGENT_LABEL_MAX);
183
+ if (name && lines.length > 0) {
184
+ const label = ` @${truncateToWidth(name.replace(/\s+/g, " "), AGENT_LABEL_MAX)} `;
185
+ const labelWidth = visibleWidth(label);
186
+ if (visibleWidth(lines[0]) >= labelWidth + 4) {
187
+ lines[0] = truncateToWidth(lines[0], width - labelWidth - 2, "") + label + "──";
188
+ }
189
+ }
190
+ return lines;
191
+ }
192
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * inline-panel/index.ts — install/uninstall of the inline agent panel.
3
+ *
4
+ * Owns the two half-mounted surfaces:
5
+ * - the `CrewInlineEditor` wrapper (installed via `setEditorComponent` when no
6
+ * other extension owns the editor), which translates ↓/↑/enter/x/escape
7
+ * into panel-store changes and host calls;
8
+ * - the FULL-SCREEN agent view overlay (`ctx.ui.custom` with `overlay:
9
+ * true`, width "100%"), the LIVE transcript of the viewed agent.
10
+ *
11
+ * Entering an agent row takes over the whole terminal with that agent's live
12
+ * transcript — a separate view, not content appended under the main session.
13
+ * The overlay tails the agent's on-disk event log (events.jsonl, appended in
14
+ * real time by the running child pi worker) and renders through pi's own
15
+ * transcript components. The main session is never switched, resumed, or torn
16
+ * down to look at an agent, so viewing can never kill a run or strand the
17
+ * editor. (The previous design — copy the worker's session file and
18
+ * `switchSession` to it, re-switching every few seconds — cancelled live runs
19
+ * on teardown, froze at the copy timestamp, and crashed on stale extension
20
+ * ctxs; see the "fix(view)" chain in git history.)
21
+ *
22
+ * All heavy team-tool interaction (steer, cancel) is lazy-imported so this
23
+ * module never pulls the runtime chain at startup (AGENTS.md lazy boundary).
24
+ */
25
+
26
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
27
+
28
+ import type { CrewUiConfig } from "../../config/types.ts";
29
+ import { isToolError, type PiTeamsToolResult, textFromToolResult } from "../../extension/tool-result.ts";
30
+ import { logInternalError } from "../../utils/internal-error.ts";
31
+ import { requestRender } from "../pi-ui-compat.ts";
32
+ import type { CrewAgentPane } from "./agent-pane.ts";
33
+ import { resetAllAgentTranscriptCursors } from "./agent-transcript.ts";
34
+ import { CrewAgentOverlay } from "./agent-view-overlay.ts";
35
+ import { CrewInlineEditor } from "./crew-editor.ts";
36
+ import type { PanelTarget } from "./panel-selection.ts";
37
+ import { resetPanelStore, setViewedAgent } from "./panel-store.ts";
38
+
39
+ /**
40
+ * Live repaint cadence while the view is open. The pane re-reads the agent's
41
+ * event log during render() (throttled internally), so a periodic
42
+ * requestRender keeps it following the worker even when pi's own repaints
43
+ * are idle (no spinner, no typing). pi-subtask drives this from child RPC
44
+ * events; our workers write to disk, so the pane is disk-tailed instead.
45
+ */
46
+ const PANE_LIVE_TICK_MS = 700;
47
+
48
+ let livePane: CrewAgentPane | undefined;
49
+ let liveOverlay: CrewAgentOverlay | undefined;
50
+ let paneTickTimer: ReturnType<typeof setInterval> | undefined;
51
+ let editorInstalled = false;
52
+ /** Guards the one-time per-process event hooks. */
53
+ let hooksRegistered = false;
54
+
55
+ function startPaneTicker(): void {
56
+ if (paneTickTimer) return;
57
+ paneTickTimer = setInterval(() => livePane?.requestRender(), PANE_LIVE_TICK_MS);
58
+ paneTickTimer.unref?.();
59
+ }
60
+
61
+ function stopPaneTicker(): void {
62
+ if (paneTickTimer) {
63
+ clearInterval(paneTickTimer);
64
+ paneTickTimer = undefined;
65
+ }
66
+ livePane = undefined;
67
+ }
68
+
69
+ async function runTeamTool(params: Record<string, unknown>, ctx: ExtensionContext): Promise<PiTeamsToolResult> {
70
+ // LAZY: team-tool.ts pulls in the entire runtime chain (same boundary as
71
+ // run-action-dispatcher.ts).
72
+ // LAZY: (marker on the import line's previous line, per check-lazy-imports)
73
+ const { handleTeamTool } = await import("../../extension/team-tool.ts");
74
+ return handleTeamTool(params as never, ctx);
75
+ }
76
+
77
+ function notifyResult(ctx: ExtensionContext, result: PiTeamsToolResult): void {
78
+ const text = textFromToolResult(result);
79
+ ctx.ui.notify(isToolError(result) ? `panel: ${text}` : text, isToolError(result) ? "error" : "info");
80
+ }
81
+
82
+ /**
83
+ * Open the viewed agent's LIVE transcript as a FULL-SCREEN overlay (or, when
84
+ * one is already open, just re-target it — the pane follows the panel store).
85
+ * Pure overlay wiring — no session is touched, so there is nothing to settle,
86
+ * detach, or guard: the view works in every session state, for foreground and
87
+ * async runs alike, and closing it never affects the run.
88
+ */
89
+ function openPane(ctx: ExtensionContext, target: PanelTarget): void {
90
+ setViewedAgent(target);
91
+ if (!liveOverlay) {
92
+ try {
93
+ void ctx.ui
94
+ .custom(
95
+ (tui, theme, _keybindings, done) => {
96
+ const overlay = new CrewAgentOverlay(tui as never, theme as never, ctx.cwd, {
97
+ close: () => {
98
+ liveOverlay = undefined;
99
+ livePane = undefined;
100
+ setViewedAgent(undefined);
101
+ stopPaneTicker();
102
+ try {
103
+ done(undefined);
104
+ } catch {
105
+ /* already closed by the host */
106
+ }
107
+ },
108
+ steer: (steerTarget, message) => void steerAgent(ctx, steerTarget, message),
109
+ });
110
+ liveOverlay = overlay;
111
+ livePane = overlay.pane;
112
+ return overlay as never;
113
+ },
114
+ {
115
+ overlay: true,
116
+ overlayOptions: { width: "100%", margin: 0, maxHeight: "100%", anchor: "top-left" },
117
+ },
118
+ )
119
+ .catch((error) => {
120
+ // Factory throw or the host tearing the overlay down: drop the
121
+ // view state so the dock does not point at a view that no
122
+ // longer exists.
123
+ liveOverlay = undefined;
124
+ livePane = undefined;
125
+ setViewedAgent(undefined);
126
+ stopPaneTicker();
127
+ logInternalError("view.openOverlay", error, target.taskId);
128
+ });
129
+ } catch (error) {
130
+ // A stale ctx (session replaced between keypress and here) drops the
131
+ // overlay spawn; the fallback editor path still closes cleanly.
132
+ logInternalError("view.openPane", error, target.taskId);
133
+ }
134
+ }
135
+ startPaneTicker();
136
+ try {
137
+ requestRender(ctx);
138
+ } catch {
139
+ /* stale ctx — nothing to repaint */
140
+ }
141
+ }
142
+
143
+ /** Close the view and return to the main conversation. Never touches the run. */
144
+ function closePane(_ctx: ExtensionContext): void {
145
+ if (liveOverlay) {
146
+ // The overlay owns its teardown (state reset + host `done()`); the
147
+ // requestClose path is idempotent.
148
+ liveOverlay.requestClose();
149
+ return;
150
+ }
151
+ setViewedAgent(undefined);
152
+ stopPaneTicker();
153
+ }
154
+
155
+ async function steerAgent(ctx: ExtensionContext, target: PanelTarget, message: string): Promise<void> {
156
+ try {
157
+ notifyResult(ctx, await runTeamTool({ action: "steer", runId: target.runId, taskId: target.taskId, message }, ctx));
158
+ // Delivery is at the child's next turn boundary, not mid-tool-call;
159
+ // say so explicitly so the user does not read silence as a dropped message.
160
+ ctx.ui.notify("Will be delivered at the worker's next turn boundary.", "info");
161
+ } catch (error) {
162
+ ctx.ui.notify(`panel: steer failed — ${error instanceof Error ? error.message : String(error)}`, "error");
163
+ }
164
+ }
165
+
166
+ async function actOnAgent(ctx: ExtensionContext, target: PanelTarget, finished: boolean): Promise<void> {
167
+ if (finished) {
168
+ // Finished rows age out of the linger window on their own. `x` here
169
+ // only returns focus to typing; resume/inspect stays in the dashboard
170
+ // so a single keystroke never mutates anything.
171
+ ctx.ui.notify("Agent is finished — use /team-dashboard to inspect or resume.", "info");
172
+ return;
173
+ }
174
+ // Cancel is run-level in the team tool, so a single-keystroke `x` must not
175
+ // silently destroy a whole run: confirm first.
176
+ const confirmed = await ctx.ui.confirm(
177
+ "Cancel entire run?",
178
+ `This cancels run ${target.runId} and all its workers. Ongoing work stops; resume re-queues what was left.`,
179
+ );
180
+ if (!confirmed) {
181
+ ctx.ui.notify("Cancel aborted.", "info");
182
+ return;
183
+ }
184
+ try {
185
+ notifyResult(ctx, await runTeamTool({ action: "cancel", runId: target.runId }, ctx));
186
+ } catch (error) {
187
+ ctx.ui.notify(`panel: cancel failed — ${error instanceof Error ? error.message : String(error)}`, "error");
188
+ }
189
+ }
190
+
191
+ // ── Agent view commands ────────────────────────────────────────────────
192
+ //
193
+ // `/crew-view <runId> <taskId>` and `/crew-back` are thin aliases for the
194
+ // pane wiring above (open / close). They never switch sessions — the dock's
195
+ // enter key takes the same path.
196
+
197
+ async function handleCrewViewCommand(args: string, ctx: ExtensionContext): Promise<void> {
198
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
199
+ if (tokens.length < 2) {
200
+ ctx.ui.notify("Usage: /crew-view <runId> <taskId>", "error");
201
+ return;
202
+ }
203
+ const [runId, taskId] = tokens;
204
+ // LAZY: state-store is runtime state, not a UI dependency.
205
+ // LAZY: (marker on the import line's previous line, per check-lazy-imports)
206
+ const { loadRunManifestById } = await import("../../state/stores/state-store.ts");
207
+ if (!loadRunManifestById(ctx.cwd, runId)) {
208
+ ctx.ui.notify(`No run ${runId} in this project.`, "error");
209
+ return;
210
+ }
211
+ openPane(ctx, { runId, taskId });
212
+ }
213
+
214
+ async function handleCrewBackCommand(_args: string, ctx: ExtensionContext): Promise<void> {
215
+ closePane(ctx);
216
+ }
217
+
218
+ /**
219
+ * Install the panel for a session. Call from session_start AFTER the widget
220
+ * has been registered, with the already-loaded UI config.
221
+ *
222
+ * Yields to any other extension that owns the editor component (pi-subtask
223
+ * rule §2.6): without the editor wrapper there is no keyboard access, so the
224
+ * panel stays display-only and /team-dashboard remains the full path.
225
+ */
226
+ export function installInlinePanel(pi: ExtensionAPI, ctx: ExtensionContext, uiConfig?: CrewUiConfig): void {
227
+ if (!ctx.hasUI) return;
228
+
229
+ const enabled = uiConfig?.inlinePanel !== false;
230
+ try {
231
+ if (enabled && !editorInstalled && !ctx.ui.getEditorComponent()) {
232
+ ctx.ui.setEditorComponent((tui, theme, kb) => {
233
+ // Fresh instance per session; options close over the current ctx.
234
+ return new CrewInlineEditor(tui, theme, kb, {
235
+ onOpenPane: (target) => openPane(ctx, target),
236
+ onClosePane: () => closePane(ctx),
237
+ onScrollPane: (delta) => livePane?.scrollBy(delta),
238
+ onSteer: (target, message) => void steerAgent(ctx, target, message),
239
+ onAct: (target, finished) => void actOnAgent(ctx, target, finished),
240
+ });
241
+ });
242
+ editorInstalled = true;
243
+ }
244
+ } catch {
245
+ /* editor context can be transient across session replacement */
246
+ }
247
+
248
+ // Commands must be re-registered for EVERY session: pi rebuilds the
249
+ // extension command table on session replacement. registerCommand is
250
+ // idempotent on the current session's runner.
251
+ pi.registerCommand("crew-view", {
252
+ description: "Open an agent's live full-screen transcript view (usage: crew-view <runId> <taskId>)",
253
+ handler: handleCrewViewCommand,
254
+ });
255
+ pi.registerCommand("crew-back", {
256
+ description: "Close the agent transcript view and return to the main conversation",
257
+ handler: handleCrewBackCommand,
258
+ });
259
+
260
+ if (!hooksRegistered) {
261
+ hooksRegistered = true;
262
+ pi.on("session_shutdown", () => {
263
+ stopPaneTicker();
264
+ livePane = undefined;
265
+ liveOverlay = undefined;
266
+ resetPanelStore();
267
+ resetAllAgentTranscriptCursors();
268
+ });
269
+ pi.on("session_start", () => {
270
+ // pi may have replaced or dropped its editor between sessions; the
271
+ // "installed" flag is reset so the next install re-registers the
272
+ // factory. The open overlay outlives the session swap (it belongs
273
+ // to the interactive-mode UI, not the session's ctx) — keep it, but
274
+ // stop ticking until it is re-targeted.
275
+ editorInstalled = false;
276
+ stopPaneTicker();
277
+ });
278
+ }
279
+ }
280
+
281
+ /** Test hook: force the next install to re-attempt the editor. */
282
+ export function __resetInlinePanelForTest(): void {
283
+ editorInstalled = false;
284
+ liveOverlay = undefined;
285
+ stopPaneTicker();
286
+ }
287
+
288
+ /** Test seam for openPane (the panel wires it as the dock's Enter action). */
289
+ /** Test seam for closePane. */
290
+ export { closePane as __test__closePane, openPane as __test__openPane };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * panel-rows.ts — project the widget's run list into navigable panel rows.
3
+ *
4
+ * The order here MUST match what the widget paints, otherwise the cursor index
5
+ * drifts from the visible rows and `enter`/`x` act on the wrong agent. Both
6
+ * sides therefore call the same `orderWidgetAgents` helper; this module only
7
+ * flattens the per-run sections into one list.
8
+ */
9
+
10
+ import { isFinishedRunStatus } from "../../runtime/process-status.ts";
11
+ import { orderWidgetAgents } from "../widget/widget-renderer.ts";
12
+ import type { WidgetRun } from "../widget/widget-types.ts";
13
+ import type { PanelRow } from "./panel-selection.ts";
14
+
15
+ /**
16
+ * Flatten runs → rows in paint order: per run, active agents (running > queued >
17
+ * waiting) followed by the finished agents still inside their linger window.
18
+ */
19
+ export function panelRowsFromRuns(runs: readonly WidgetRun[], now = Date.now()): PanelRow[] {
20
+ const rows: PanelRow[] = [];
21
+ for (const entry of runs) {
22
+ const { active, finished } = orderWidgetAgents(entry, now);
23
+ for (const agent of active) {
24
+ rows.push({ runId: entry.run.runId, taskId: agent.taskId, finished: false, name: agent.agent });
25
+ }
26
+ for (const agent of finished) {
27
+ rows.push({ runId: entry.run.runId, taskId: agent.taskId, finished: true, name: agent.agent });
28
+ }
29
+ }
30
+ return rows;
31
+ }
32
+
33
+ /** True when the row's run has reached a terminal status (nothing left to cancel). */
34
+ export function isRunFinished(runs: readonly WidgetRun[], runId: string): boolean {
35
+ const entry = runs.find((item) => item.run.runId === runId);
36
+ return entry ? isFinishedRunStatus(entry.run.status) : true;
37
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * panel-selection.ts — pure cursor state machine for the inline agent panel.
3
+ *
4
+ * No I/O, no TUI types: the editor wrapper feeds it a key and the current row
5
+ * list, and gets back an action to perform. That keeps the whole navigation
6
+ * contract unit-testable against a synthetic row list.
7
+ *
8
+ * Row 0 is always `main` (the conversation itself); agents occupy 1..n.
9
+ *
10
+ * The cursor is stored as an **identity**, not an index. Rows reorder while the
11
+ * user is navigating — a cancelled agent sinks from the active section into the
12
+ * finished one — and an index cursor would silently retarget whichever agent
13
+ * took the vacated row. Storing the taskId means a second keystroke acts on the
14
+ * same agent the first one did.
15
+ */
16
+
17
+ /** A concrete agent the panel can act on. */
18
+ export interface PanelTarget {
19
+ runId: string;
20
+ taskId: string;
21
+ }
22
+
23
+ /** One navigable agent row, as projected from the widget's run list. */
24
+ export interface PanelRow extends PanelTarget {
25
+ /** Terminal statuses are dismissed by `x`; live ones are cancelled. */
26
+ finished: boolean;
27
+ /** Agent name, used for the editor's `@<name>` label while its pane is open. */
28
+ name: string;
29
+ }
30
+
31
+ /**
32
+ * `null` means the editor owns the cursor (normal typing). `"main"` is the
33
+ * conversation row. Anything else is an agent.
34
+ */
35
+ export type PanelSelection = "main" | PanelTarget | null;
36
+
37
+ export type PanelAction =
38
+ /** Not a navigation key — the caller must forward it to the editor. */
39
+ | { kind: "none" }
40
+ /** Cursor moved or selection cleared; repaint and swallow the key. */
41
+ | { kind: "consumed" }
42
+ /** `enter`: open the target's pane, or return to `main` when undefined. */
43
+ | { kind: "open"; target: PanelTarget | undefined }
44
+ /** `x`: cancel a running agent, or dismiss a finished one. */
45
+ | { kind: "act"; target: PanelTarget };
46
+
47
+ export interface PanelKeys {
48
+ up: boolean;
49
+ down: boolean;
50
+ enter: boolean;
51
+ escape: boolean;
52
+ act: boolean;
53
+ }
54
+
55
+ export interface DispatchOptions {
56
+ /**
57
+ * When true, `up` at the main row keeps the cursor there instead of handing
58
+ * focus back to the editor. Used while a transcript pane is open, so one
59
+ * `down` + `enter` is always a reliable way back to the conversation.
60
+ */
61
+ holdAtMain?: boolean;
62
+ }
63
+
64
+ export interface DispatchResult {
65
+ action: PanelAction;
66
+ selection: PanelSelection;
67
+ }
68
+
69
+ function sameTarget(a: PanelTarget, b: PanelTarget): boolean {
70
+ return a.runId === b.runId && a.taskId === b.taskId;
71
+ }
72
+
73
+ /** True when `selection` points at a concrete agent (not `main`/editor). */
74
+ export function isAgentSelection(selection: PanelSelection): selection is PanelTarget {
75
+ return selection !== null && selection !== "main";
76
+ }
77
+
78
+ /**
79
+ * Numeric cursor position for the current rows, or `null` when the editor owns
80
+ * the cursor. A selection whose agent has disappeared (dismissed, aged out of
81
+ * the linger window) resolves to the main row rather than to a stranger.
82
+ */
83
+ export function resolveIndex(rows: readonly PanelRow[], selection: PanelSelection): number | null {
84
+ if (selection === null) return null;
85
+ if (selection === "main") return 0;
86
+ const found = rows.findIndex((row) => sameTarget(row, selection));
87
+ return found >= 0 ? found + 1 : 0;
88
+ }
89
+
90
+ /** Selection for a numeric position, clamped into range. Index 0 is `main`. */
91
+ export function selectionAtIndex(rows: readonly PanelRow[], index: number): PanelSelection {
92
+ const clamped = Math.max(0, Math.min(rows.length, index));
93
+ if (clamped === 0) return "main";
94
+ const row = rows[clamped - 1];
95
+ return row ? { runId: row.runId, taskId: row.taskId } : "main";
96
+ }
97
+
98
+ /** The row a selection currently points at, if it is still present. */
99
+ export function rowFor(rows: readonly PanelRow[], selection: PanelSelection): PanelRow | undefined {
100
+ if (!isAgentSelection(selection)) return undefined;
101
+ return rows.find((row) => sameTarget(row, selection));
102
+ }
103
+
104
+ /**
105
+ * Apply one keypress.
106
+ *
107
+ * Any key that is not a navigation key returns `{ kind: "none" }` with the
108
+ * selection cleared, so the caller can forward it to the editor and the user is
109
+ * never trapped in a mode.
110
+ */
111
+ export function dispatchPanelKey(
112
+ keys: PanelKeys,
113
+ rows: readonly PanelRow[],
114
+ selection: PanelSelection,
115
+ options: DispatchOptions = {},
116
+ ): DispatchResult {
117
+ // Editor owns the cursor: the only panel key that makes sense is `down`,
118
+ // entering at the main row (pi-subtask: selectRow(rows, 0)). The main row
119
+ // is a real rendered row with its own ❯ marker, so this first press is
120
+ // visible immediately — no dead-key feeling. Everything else falls
121
+ // through untouched.
122
+ if (selection === null) {
123
+ if (keys.down) return { action: { kind: "consumed" }, selection: "main" };
124
+ return { action: { kind: "none" }, selection: null };
125
+ }
126
+
127
+ const index = resolveIndex(rows, selection) ?? 0;
128
+
129
+ if (keys.up) {
130
+ if (index === 0 && options.holdAtMain !== true) return { action: { kind: "consumed" }, selection: null };
131
+ return { action: { kind: "consumed" }, selection: selectionAtIndex(rows, index - 1) };
132
+ }
133
+ if (keys.down) {
134
+ return { action: { kind: "consumed" }, selection: selectionAtIndex(rows, index + 1) };
135
+ }
136
+ if (keys.escape) {
137
+ return { action: { kind: "consumed" }, selection: null };
138
+ }
139
+ if (keys.enter) {
140
+ const target = index > 0 ? rows[index - 1] : undefined;
141
+ return {
142
+ action: { kind: "open", target: target ? { runId: target.runId, taskId: target.taskId } : undefined },
143
+ selection: null,
144
+ };
145
+ }
146
+ if (keys.act && index > 0) {
147
+ const target = rows[index - 1];
148
+ if (!target) return { action: { kind: "consumed" }, selection: selectionAtIndex(rows, 0) };
149
+ // Selection stays on the SAME agent so a follow-up keystroke acts on it
150
+ // again rather than on whichever row the reorder promoted.
151
+ return {
152
+ action: { kind: "act", target: { runId: target.runId, taskId: target.taskId } },
153
+ selection: { runId: target.runId, taskId: target.taskId },
154
+ };
155
+ }
156
+ return { action: { kind: "none" }, selection: null };
157
+ }