pi-crew 0.11.0 → 0.11.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 (174) hide show
  1. package/CHANGELOG.md +155 -9
  2. package/README.md +161 -1037
  3. package/agents/verifier.md +18 -7
  4. package/dist/index.mjs +744 -90644
  5. package/docs/README.md +57 -46
  6. package/docs/architecture.md +87 -33
  7. package/docs/commands-reference.md +9 -5
  8. package/docs/troubleshooting.md +3 -2
  9. package/package.json +1 -3
  10. package/schema.json +39 -0
  11. package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
  12. package/skills/real-test-pi-crew/SKILL.md +371 -34
  13. package/src/agents/agent-config.ts +1 -1
  14. package/src/agents/discover-agents.ts +1 -1
  15. package/src/config/config-validation.ts +15 -2
  16. package/src/config/config.ts +47 -13
  17. package/src/config/defaults.ts +0 -1
  18. package/src/config/env-vars.ts +35 -0
  19. package/src/config/types.ts +19 -5
  20. package/src/errors.ts +2 -2
  21. package/src/extension/async-notifier.ts +23 -0
  22. package/src/extension/crew-vibes/config.ts +0 -21
  23. package/src/extension/crew-vibes/index.ts +0 -2
  24. package/src/extension/crew-vibes/render.ts +1 -50
  25. package/src/extension/help.ts +21 -12
  26. package/src/extension/knowledge-injection.ts +2 -1
  27. package/src/extension/management.ts +8 -3
  28. package/src/extension/notification-sink.ts +17 -0
  29. package/src/extension/register.ts +7 -2
  30. package/src/extension/registration/command-utils.ts +28 -2
  31. package/src/extension/registration/commands/dashboard.ts +11 -1
  32. package/src/extension/registration/commands/manage.ts +36 -19
  33. package/src/extension/registration/commands/run.ts +24 -2
  34. package/src/extension/registration/commands/shared.ts +23 -1
  35. package/src/extension/registration/commands/status.ts +25 -2
  36. package/src/extension/registration/context-builder.ts +8 -2
  37. package/src/extension/registration/health-notify-policy.ts +100 -0
  38. package/src/extension/registration/lazy-configurers.ts +35 -0
  39. package/src/extension/registration/lifecycle-handlers.ts +91 -30
  40. package/src/extension/registration/lifecycle.ts +75 -10
  41. package/src/extension/registration/observability.ts +98 -35
  42. package/src/extension/registration/registration-types.ts +7 -5
  43. package/src/extension/registration/runtime-cleanup.ts +9 -3
  44. package/src/extension/registration/subagent-helpers.ts +38 -0
  45. package/src/extension/registration/subagent-tools.ts +16 -6
  46. package/src/extension/registration/team-tool.ts +10 -3
  47. package/src/extension/registration/terminal-status-wiring.ts +172 -0
  48. package/src/extension/registration/viewers.ts +6 -0
  49. package/src/extension/registration/wire-cross-extension.ts +28 -0
  50. package/src/extension/run-compare.ts +220 -0
  51. package/src/extension/run-export.ts +37 -5
  52. package/src/extension/run-maintenance.ts +155 -5
  53. package/src/extension/team-tool/dispatch/index.ts +3 -2
  54. package/src/extension/team-tool/dispatch/manage.ts +5 -2
  55. package/src/extension/team-tool/goal.ts +4 -1
  56. package/src/extension/team-tool/handle-settings.ts +33 -4
  57. package/src/extension/team-tool/health-monitor.ts +21 -7
  58. package/src/extension/team-tool/lifecycle-actions.ts +49 -1
  59. package/src/extension/team-tool/plan.ts +10 -0
  60. package/src/extension/team-tool/routing-hint.ts +63 -0
  61. package/src/extension/team-tool/status.ts +4 -0
  62. package/src/extension/team-tool.ts +52 -6
  63. package/src/extension/webhook-notify.ts +382 -0
  64. package/src/observability/metric-sink.ts +12 -2
  65. package/src/prompt/prompt-runtime.ts +82 -31
  66. package/src/prompt/worker-events-channel.ts +12 -0
  67. package/src/runtime/README.md +1 -1
  68. package/src/runtime/async-runner.ts +87 -1
  69. package/src/runtime/background-runner.ts +313 -234
  70. package/src/runtime/broker/crew-broker.ts +17 -11
  71. package/src/runtime/broker/delegate/shadow-lifecycle.ts +92 -0
  72. package/src/runtime/broker/wait-status-cache.ts +1 -1
  73. package/src/runtime/child-pi/child-pi-timers.ts +1 -1
  74. package/src/runtime/child-pi/mock-fixtures.ts +48 -0
  75. package/src/runtime/crew-agent-records.ts +337 -45
  76. package/src/runtime/deadletter.ts +43 -1
  77. package/src/runtime/delegate-spawn.ts +5 -1
  78. package/src/runtime/dispatch-batch.ts +72 -5
  79. package/src/runtime/goal-workflow/goal-loop-runner.ts +73 -4
  80. package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
  81. package/src/runtime/model/model-fallback.ts +21 -1
  82. package/src/runtime/model/pi-args.ts +8 -10
  83. package/src/runtime/recovery/crash-recovery.ts +25 -1
  84. package/src/runtime/run-worker.ts +12 -1
  85. package/src/runtime/scheduling/global-worker-cap.ts +13 -6
  86. package/src/runtime/scheduling/run-coalesced-task-group.ts +27 -1
  87. package/src/runtime/scheduling/scheduler.ts +49 -13
  88. package/src/runtime/scheduling/semaphore.ts +148 -20
  89. package/src/runtime/scratchpad/README.md +1 -1
  90. package/src/runtime/scratchpad/protocol.ts +1 -1
  91. package/src/runtime/settings-store.ts +1 -1
  92. package/src/runtime/skill-instructions.ts +22 -0
  93. package/src/runtime/stale-reconciler.ts +85 -13
  94. package/src/runtime/task-display.ts +1 -1
  95. package/src/runtime/task-runner/pre-execution.ts +26 -2
  96. package/src/runtime/task-runner/prompt-builder.ts +142 -45
  97. package/src/runtime/task-runner.ts +21 -1
  98. package/src/runtime/team-runner.ts +38 -1
  99. package/src/runtime/workspace-lock.ts +4 -1
  100. package/src/schema/config-schema.ts +18 -0
  101. package/src/schema/team-tool-schema.ts +17 -0
  102. package/src/state/atomic-write.ts +53 -0
  103. package/src/state/contracts.ts +109 -0
  104. package/src/state/coordination/locks.ts +191 -33
  105. package/src/state/coordination/mailbox.ts +140 -15
  106. package/src/state/crew-init.ts +87 -12
  107. package/src/state/event-log/cursor.ts +37 -1
  108. package/src/state/event-log/event-log-rotation.ts +72 -7
  109. package/src/state/stores/active-run-registry.ts +13 -1
  110. package/src/state/stores/state-store.ts +112 -22
  111. package/src/state/types.ts +4 -0
  112. package/src/ui/adaptive-card.ts +65 -0
  113. package/src/ui/agents-jobs-browser.ts +70 -64
  114. package/src/ui/card-colors.ts +36 -7
  115. package/src/ui/dashboard-panes/agents-pane.ts +55 -14
  116. package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
  117. package/src/ui/dashboard-panes/health-pane.ts +7 -5
  118. package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
  119. package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
  120. package/src/ui/dashboard-panes/pane-theme.ts +21 -0
  121. package/src/ui/dashboard-panes/plan-pane.ts +63 -30
  122. package/src/ui/dashboard-panes/progress-pane.ts +3 -2
  123. package/src/ui/dashboard-panes/schedules-pane.ts +44 -21
  124. package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
  125. package/src/ui/dwf-phase-display.ts +3 -20
  126. package/src/ui/format-helpers.ts +22 -0
  127. package/src/ui/heartbeat-aggregator.ts +34 -0
  128. package/src/ui/inline-panel/crew-editor.ts +13 -3
  129. package/src/ui/inline-panel/index.ts +60 -4
  130. package/src/ui/keybinding-map.ts +251 -35
  131. package/src/ui/live-conversation-overlay.ts +180 -47
  132. package/src/ui/live-run-sidebar.ts +134 -55
  133. package/src/ui/mascot.ts +32 -16
  134. package/src/ui/overlays/agent-picker-overlay.ts +81 -26
  135. package/src/ui/overlays/confirm-overlay.ts +55 -29
  136. package/src/ui/overlays/help-overlay.ts +108 -53
  137. package/src/ui/overlays/mailbox-compose-overlay.ts +89 -50
  138. package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
  139. package/src/ui/powerbar-publisher.ts +0 -1
  140. package/src/ui/rail.ts +333 -0
  141. package/src/ui/run-dashboard.ts +193 -79
  142. package/src/ui/run-snapshot-cache.ts +18 -1
  143. package/src/ui/settings-overlay.ts +81 -39
  144. package/src/ui/spinner.ts +26 -2
  145. package/src/ui/terminal-status.ts +7 -1
  146. package/src/ui/theme-adapter.ts +0 -45
  147. package/src/ui/theme-discovery.ts +12 -6
  148. package/src/ui/tool-progress-formatter.ts +128 -9
  149. package/src/ui/tool-renderers/brief-mode.ts +10 -67
  150. package/src/ui/tool-renderers/index.ts +374 -523
  151. package/src/ui/transcript-viewer.ts +30 -12
  152. package/src/ui/widget/index.ts +32 -52
  153. package/src/ui/widget/task-list.ts +64 -32
  154. package/src/ui/widget/widget-formatters.ts +3 -402
  155. package/src/ui/widget/widget-model.ts +28 -7
  156. package/src/ui/widget/widget-renderer.ts +201 -128
  157. package/src/ui/widget/widget-types.ts +0 -2
  158. package/src/utils/incremental-reader.ts +11 -3
  159. package/src/utils/paths.ts +94 -12
  160. package/src/utils/project-markers.ts +40 -0
  161. package/src/utils/visual.ts +0 -4
  162. package/src/worktree/worktree-manager.ts +206 -26
  163. package/workflows/distill.workflow.md +3 -3
  164. package/workflows/fast-fix.workflow.md +1 -1
  165. package/workflows/plan-execute.workflow.md +1 -1
  166. package/workflows/review.workflow.md +1 -1
  167. package/workflows/strict-fast-fix.workflow.md +1 -1
  168. package/docs/migration-v0.4-v0.5.md +0 -208
  169. package/docs/runtime-flow.md +0 -148
  170. package/src/extension/crew-vibes/figures.ts +0 -22
  171. package/src/extension/crew-vibes/font-detect.ts +0 -71
  172. package/src/ui/dynamic-border.ts +0 -35
  173. package/src/ui/loaders.ts +0 -6
  174. package/src/ui/overlay-stack.ts +0 -148
@@ -1,6 +1,28 @@
1
+ /**
2
+ * Mailbox compose overlay — RAIL design language (M4/E1, 2026-09-16).
3
+ *
4
+ * ┏ COMPOSE ▸ mailbox
5
+ * ┃ › from: operator │ Preview
6
+ * ┃ › to: leader │ # Title
7
+ * ┃ › body: hello │ • item
8
+ * ┃ › taskId: │
9
+ * ┃ › [ ] Send to outbox │
10
+ * ┗ P close preview · Tab cycle · Enter submit · Esc discard
11
+ *
12
+ * Frame: canopy + `┃` body + `┗` close cap. When the preview is on, the body is
13
+ * a genuine two-column layout and `│` is the inner COLUMN separator (§2.D); the
14
+ * canopy/hint rows are NOT part of the split any more, so the hint keeps its
15
+ * full width instead of being cropped to 60% of the overlay.
16
+ *
17
+ * Keys/behaviour are unchanged (same `overlay:*` dispatch, same free-text
18
+ * passthrough, same validation errors, same discard confirmation). Hints come
19
+ * from `formatHint` (discard LAST, keys through `keyToken`).
20
+ */
21
+
1
22
  import type { MailboxDirection } from "../../state/coordination/mailbox.ts";
2
- import { pad, truncate } from "../../utils/visual.ts";
3
- import { matchesKey } from "../key-utils.ts";
23
+ import { pad, sanitizeLine, truncate } from "../../utils/visual.ts";
24
+ import { overlayActionForKey } from "../keybinding-map.ts";
25
+ import { CURSOR, canopyLine, formatHint, RAIL, railLine, railRaw } from "../rail.ts";
4
26
  import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
5
27
  import { ConfirmOverlay } from "./confirm-overlay.ts";
6
28
  import { renderComposePreview } from "./mailbox-compose-preview.ts";
@@ -49,36 +71,53 @@ export class MailboxComposeOverlay {
49
71
 
50
72
  render(width: number): string[] {
51
73
  if (this.confirm) return this.confirm.render(width);
52
- const inner = Math.max(24, width - 4);
53
- const formWidth = this.preview ? Math.max(24, Math.floor(inner * 0.6)) : inner;
54
- const lines = [
55
- this.theme.bold("Compose mailbox message"),
56
- this.preview
57
- ? "P close preview · Tab cycle · Enter submit · ESC discard"
58
- : "P preview · Tab cycle · Enter submit · ESC discard",
59
- ...(this.error ? [this.theme.fg("error", this.error)] : []),
60
- this.fieldLine("from", formWidth),
61
- this.fieldLine("to", formWidth),
62
- this.fieldLine("body", formWidth),
63
- this.fieldLine("taskId", formWidth),
64
- `${this.activeField === 4 ? "›" : " "} [${this.fields.direction === "outbox" ? "x" : " "}] Send to outbox`,
74
+ if (width < 10) return [];
75
+ const theme = this.theme;
76
+ const budget = width - 2;
77
+ const hint = formatHint([
78
+ ["P", this.preview ? "close preview" : "preview"],
79
+ ["tab", "cycle"],
80
+ ["enter", "submit"],
81
+ ["escape", "discard"],
82
+ ]);
83
+ const lines: string[] = [canopyLine({ word: "COMPOSE", subject: "mailbox", theme, budget })];
84
+ if (this.error) lines.push(railLine(RAIL.body, "error", truncate(this.error, budget), theme, budget));
85
+
86
+ const formBudget = this.preview ? Math.max(10, Math.floor(budget * 0.6)) : budget;
87
+ const previewBudget = Math.max(8, budget - formBudget - 3);
88
+ const formRows = [
89
+ this.fieldLine("from", formBudget),
90
+ this.fieldLine("to", formBudget),
91
+ this.fieldLine("body", formBudget),
92
+ this.fieldLine("taskId", formBudget),
93
+ `${this.activeField === 4 ? CURSOR : " "} [${this.fields.direction === "outbox" ? "x" : " "}] Send to outbox`,
65
94
  ];
66
- if (!this.preview) return lines.map((line) => pad(truncate(line, inner), inner));
67
- const previewLines = renderComposePreview(this.fields.body, Math.max(20, inner - formWidth - 3), this.theme);
68
- const max = Math.max(lines.length, previewLines.length);
69
- const split: string[] = [];
70
- for (let index = 0; index < max; index += 1) {
71
- split.push(
72
- `${pad(truncate(lines[index] ?? "", formWidth), formWidth)} │ ${truncate(previewLines[index] ?? "", inner - formWidth - 3)}`,
73
- );
95
+ if (!this.preview) {
96
+ for (const row of formRows) lines.push(railLine(RAIL.body, "border", truncate(row, budget), theme, budget));
97
+ } else {
98
+ const previewLines = renderComposePreview(this.fields.body, previewBudget, theme);
99
+ const max = Math.max(formRows.length, previewLines.length);
100
+ for (let index = 0; index < max; index += 1) {
101
+ const left = pad(truncate(formRows[index] ?? "", formBudget), formBudget);
102
+ const right = truncate(previewLines[index] ?? "", previewBudget);
103
+ // No separator when the preview column has no row at this height:
104
+ // a `│` with nothing to its right reads as a frame edge.
105
+ lines.push(railRaw(RAIL.body, "border", right ? `${left} ${theme.fg("dim", "│")} ${right}` : left, theme));
106
+ }
74
107
  }
75
- return split;
108
+ lines.push(railLine(RAIL.close, "border", theme.fg("dim", hint), theme, budget));
109
+ return lines;
76
110
  }
77
111
 
78
112
  private fieldLine(field: Exclude<FieldName, "direction">, width: number): string {
79
113
  const active = FIELD_ORDER[this.activeField] === field;
80
114
  const label = field === "taskId" ? "taskId" : field;
81
- return `${active ? "›" : " "} ${label}: ${truncate(this.fields[field] ?? "", Math.max(8, width - label.length - 5))}`;
115
+ // `sanitizeLine`: a pasted / pre-filled value can carry a raw newline or
116
+ // TAB — printing it verbatim would break the rail row into several lines
117
+ // (the field is a single-line cell; multi-line content belongs to the
118
+ // preview column).
119
+ const value = sanitizeLine(this.fields[field] ?? "");
120
+ return `${active ? CURSOR : " "} ${label}: ${truncate(value, Math.max(8, width - label.length - 5))}`;
82
121
  }
83
122
 
84
123
  private activeName(): FieldName {
@@ -151,32 +190,32 @@ export class MailboxComposeOverlay {
151
190
  this.confirm.handleInput(data);
152
191
  return;
153
192
  }
154
- if (matchesKey(data, "escape")) {
155
- this.cancel();
156
- return;
157
- }
158
- if (data === "P") {
159
- this.preview = !this.preview;
160
- return;
161
- }
162
- if (data === "\t") {
163
- this.activeField = (this.activeField + 1) % FIELD_ORDER.length;
164
- return;
165
- }
166
- if (data === " ") {
167
- if (this.activeName() === "direction") this.fields.direction = this.fields.direction === "inbox" ? "outbox" : "inbox";
168
- else this.appendText(data);
169
- return;
170
- }
171
- if (matchesKey(data, "backspace")) {
172
- this.backspace();
173
- return;
174
- }
175
- if (matchesKey(data, "return")) {
176
- if (this.activeName() === "body" || this.fields.body.trim()) this.submit();
177
- else this.activeField = (this.activeField + 1) % FIELD_ORDER.length;
178
- return;
193
+ // M2-1: keys come from the central `overlay:*` keyspace (remappable via
194
+ // `.crew/config.json` → keybindings["overlay:mailbox-compose:<action>"]).
195
+ switch (overlayActionForKey("mailbox-compose", data)) {
196
+ case "cancel":
197
+ this.cancel();
198
+ return;
199
+ case "preview":
200
+ this.preview = !this.preview;
201
+ return;
202
+ case "nextField":
203
+ this.activeField = (this.activeField + 1) % FIELD_ORDER.length;
204
+ return;
205
+ case "space":
206
+ if (this.activeName() === "direction") this.fields.direction = this.fields.direction === "inbox" ? "outbox" : "inbox";
207
+ else this.appendText(data);
208
+ return;
209
+ case "backspace":
210
+ this.backspace();
211
+ return;
212
+ case "submit":
213
+ if (this.activeName() === "body" || this.fields.body.trim()) this.submit();
214
+ else this.activeField = (this.activeField + 1) % FIELD_ORDER.length;
215
+ return;
179
216
  }
217
+ // Free-text passthrough (unchanged): any other printable char is typed
218
+ // into the active field.
180
219
  if (data.length === 1 && data >= " ") this.appendText(data);
181
220
  }
182
221
  }
@@ -1,7 +1,32 @@
1
+ /**
2
+ * Mailbox detail overlay — RAIL design language (M4/E1, 2026-09-16).
3
+ *
4
+ * ┏ MAILBOX ▸ 1a2b3c4d
5
+ * ┃ Inbox │ Outbox
6
+ * ┃ ›✓ lead ▸ one: ping │ !
7
+ * ┃ ▼ 3 below
8
+ * ┣ MESSAGE
9
+ * ┃ lead ▸ one (one) · pending
10
+ * ┗ Tab side · ↑/↓ select · Enter expand · … ···· Esc close
11
+ *
12
+ * The `│` in the body is a genuine inner COLUMN separator of the two-column
13
+ * mailbox table (§2.D) — it is not a frame edge. The frame is canopy + `┃`
14
+ * body + `┗` close cap; the legacy title/hint header rows, the `───` inline
15
+ * rule before the expanded message and the hand-typed hint string are gone.
16
+ *
17
+ * Hints: built by `formatHint` (close LAST) and laid out with `railLeaders` so
18
+ * the `Esc close` segment stays visible on a narrow overlay instead of being
19
+ * truncated away with the long action list.
20
+ *
21
+ * Every disk-sourced field is guarded (`?? "?"`): mailbox JSON is not
22
+ * schema-validated at read time.
23
+ */
24
+
1
25
  import { type MailboxMessage, readDeliveryState, readMailbox } from "../../state/coordination/mailbox.ts";
2
26
  import { loadRunManifestById } from "../../state/stores/state-store.ts";
3
27
  import { pad, truncate } from "../../utils/visual.ts";
4
- import { keyOf, matchesKey } from "../key-utils.ts";
28
+ import { overlayActionForKey } from "../keybinding-map.ts";
29
+ import { ACTIVE, CURSOR, canopyLine, formatHint, overflowHint, RAIL, railLeaders, railLine, sectionLine, shortId } from "../rail.ts";
5
30
  import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
6
31
 
7
32
  export type MailboxAction =
@@ -11,6 +36,9 @@ export type MailboxAction =
11
36
  | { type: "ackAll" }
12
37
  | { type: "close" };
13
38
 
39
+ /** Rows shown per column before the overflow hint takes over. */
40
+ const MAX_ROWS = 12;
41
+
14
42
  export class MailboxDetailOverlay {
15
43
  private readonly runId: string;
16
44
  private readonly cwd: string;
@@ -84,72 +112,124 @@ export class MailboxDetailOverlay {
84
112
  this.refresh();
85
113
  this.needsRefresh = false;
86
114
  }
87
- const inner = Math.max(40, width - 4);
88
- const col = Math.max(18, Math.floor((inner - 3) / 2));
89
- const lines = [
90
- this.theme.bold(`Mailbox detail · ${this.runId}`),
91
- "Tab side · ↑/↓ select · Enter expand · A ack · N nudge · C compose · X ack all · ESC close",
92
- `${pad(this.theme.bold("Inbox"), col)} │ ${pad(this.theme.bold("Outbox"), col)}`,
93
- ];
94
- const max = Math.max(this.inbox.length, this.outbox.length, 1);
95
- for (let index = 0; index < Math.min(max, 12); index += 1) {
96
- lines.push(`${this.row(this.inbox[index], "inbox", index, col)} │ ${this.row(this.outbox[index], "outbox", index, col)}`);
115
+ if (width < 10) return [];
116
+ const theme = this.theme;
117
+ const budget = width - 2;
118
+ const lines: string[] = [canopyLine({ word: "MAILBOX", subject: shortId(this.runId), theme, budget })];
119
+
120
+ // Two-column body (inbox ↔ outbox): `│` is the inner column separator.
121
+ const col = Math.max(10, Math.floor((budget - 3) / 2));
122
+ const rightCol = Math.max(1, budget - col - 3);
123
+ const twoCol = (left: string, right: string) =>
124
+ `${pad(truncate(left, col), col)} ${theme.fg("dim", "│")} ${truncate(right, rightCol)}`;
125
+ lines.push(railLine(RAIL.body, "border", twoCol(theme.bold("Inbox"), theme.bold("Outbox")), theme, budget));
126
+ const total = Math.max(this.inbox.length, this.outbox.length);
127
+ const shown = Math.min(total, MAX_ROWS);
128
+ for (let index = 0; index < shown; index += 1) {
129
+ lines.push(
130
+ railLine(
131
+ RAIL.body,
132
+ "border",
133
+ twoCol(this.row(this.inbox[index], "inbox", index, col), this.row(this.outbox[index], "outbox", index, col)),
134
+ theme,
135
+ budget,
136
+ ),
137
+ );
138
+ }
139
+ if (total > shown) {
140
+ lines.push(railLine(RAIL.body, "border", overflowHint(0, total - shown, theme), theme, budget));
97
141
  }
142
+
98
143
  const selected = this.selectedMessage();
99
144
  if (this.expanded && selected) {
100
- lines.push("─".repeat(Math.min(inner, 72)));
101
- lines.push(`${selected.from} → ${selected.to}${selected.taskId ? ` (${selected.taskId})` : ""} · ${selected.status}`);
102
- lines.push(...selected.body.split(/\r?\n/).map((line) => truncate(line, inner)));
145
+ const from = selected.from ?? "?";
146
+ const to = selected.to ?? "?";
147
+ lines.push(sectionLine({ name: "Message", theme, budget }));
148
+ lines.push(
149
+ railLine(
150
+ RAIL.body,
151
+ "border",
152
+ truncate(
153
+ `${from} ${ACTIVE} ${to}${selected.taskId ? ` (${selected.taskId})` : ""} · ${selected.status ?? "?"}`,
154
+ budget,
155
+ ),
156
+ theme,
157
+ budget,
158
+ ),
159
+ );
160
+ for (const line of (selected.body ?? "").split(/\r?\n/)) {
161
+ lines.push(railLine(RAIL.body, "border", truncate(line, budget), theme, budget));
162
+ }
163
+ }
164
+
165
+ if (!this.inbox.length && !this.outbox.length) {
166
+ lines.push(railLine(RAIL.body, "border", theme.fg("dim", "Mailbox is empty."), theme, budget));
103
167
  }
104
- if (!this.inbox.length && !this.outbox.length) lines.push("Mailbox is empty.");
105
- return lines.map((line) => truncate(line, inner));
168
+
169
+ const actions = formatHint([
170
+ ["tab", "side"],
171
+ [["up", "down"], "select"],
172
+ ["enter", "expand"],
173
+ ["A", "ack"],
174
+ ["N", "nudge"],
175
+ ["C", "compose"],
176
+ ["X", "ack all"],
177
+ ]);
178
+ const close = formatHint([["escape", "close"]]);
179
+ lines.push(
180
+ railLine(RAIL.close, "border", railLeaders(theme.fg("dim", actions), theme.fg("dim", close), budget, theme), theme, budget),
181
+ );
182
+ return lines;
106
183
  }
107
184
 
108
185
  private row(message: MailboxMessage | undefined, side: "inbox" | "outbox", index: number, width: number): string {
109
- if (!message) return pad("", width);
110
- const marker = this.side === side && this.selected === index ? "›" : " ";
111
- const status = message.status === "acknowledged" ? "✓" : "!";
112
- return pad(truncate(`${marker}${status} ${message.from}->${message.to}: ${message.body.replace(/\s+/g, " ")}`, width), width);
186
+ if (!message) return "";
187
+ const marker = this.side === side && this.selected === index ? CURSOR : " ";
188
+ const acknowledged = message.status === "acknowledged";
189
+ const status = this.theme.fg(acknowledged ? "success" : "warning", acknowledged ? "✓" : "!");
190
+ const from = message.from ?? "?";
191
+ const to = message.to ?? "?";
192
+ const body = (message.body ?? "").replace(/\s+/g, " ");
193
+ return truncate(`${marker}${status} ${from} ${ACTIVE} ${to}: ${body}`, width);
113
194
  }
114
195
 
115
196
  handleInput(data: string): void {
116
- if (matchesKey(data, "escape") || data === "q") {
117
- this.done({ type: "close" });
118
- return;
119
- }
120
- if (data === "\t") {
121
- this.side = this.side === "inbox" ? "outbox" : "inbox";
122
- this.selected = Math.min(this.selected, Math.max(0, this.current().length - 1));
123
- return;
124
- }
125
- if (data === "k" || keyOf(data) === "up") {
126
- this.selected = Math.max(0, this.selected - 1);
127
- return;
128
- }
129
- if (data === "j" || keyOf(data) === "down") {
130
- this.selected = Math.min(Math.max(0, this.current().length - 1), this.selected + 1);
131
- return;
132
- }
133
- if (matchesKey(data, "return")) {
134
- this.expanded = !this.expanded;
135
- return;
136
- }
137
- if (data === "A") {
138
- const message = this.selectedMessage();
139
- if (message) this.done({ type: "ack", messageId: message.id });
140
- return;
141
- }
142
- if (data === "N") {
143
- this.done({
144
- type: "nudge",
145
- agentId: this.selectedMessage()?.taskId,
146
- });
147
- return;
148
- }
149
- if (data === "C") {
150
- this.done({ type: "compose" });
151
- return;
197
+ // M2-1: keys come from the central `overlay:*` keyspace (remappable via
198
+ // `.crew/config.json` → keybindings["overlay:mailbox-detail:<action>"]).
199
+ switch (overlayActionForKey("mailbox-detail", data)) {
200
+ case "close":
201
+ this.done({ type: "close" });
202
+ return;
203
+ case "toggleSide":
204
+ this.side = this.side === "inbox" ? "outbox" : "inbox";
205
+ this.selected = Math.min(this.selected, Math.max(0, this.current().length - 1));
206
+ return;
207
+ case "up":
208
+ this.selected = Math.max(0, this.selected - 1);
209
+ return;
210
+ case "down":
211
+ this.selected = Math.min(Math.max(0, this.current().length - 1), this.selected + 1);
212
+ return;
213
+ case "toggleDetail":
214
+ this.expanded = !this.expanded;
215
+ return;
216
+ case "ack": {
217
+ const message = this.selectedMessage();
218
+ if (message) this.done({ type: "ack", messageId: message.id });
219
+ return;
220
+ }
221
+ case "nudge":
222
+ this.done({
223
+ type: "nudge",
224
+ agentId: this.selectedMessage()?.taskId,
225
+ });
226
+ return;
227
+ case "compose":
228
+ this.done({ type: "compose" });
229
+ return;
230
+ case "ackAll":
231
+ this.done({ type: "ackAll" });
232
+ return;
152
233
  }
153
- if (data === "X") this.done({ type: "ackAll" });
154
234
  }
155
235
  }
@@ -271,7 +271,6 @@ class PowerbarPublisher {
271
271
  workspaceId?: string,
272
272
  ): void {
273
273
  if (config?.powerbar === false) return;
274
- const useStatusFallback = !hasPowerbarConsumer(events);
275
274
  // P3 (#10): prefer an explicit workspaceId arg; otherwise self-derive
276
275
  // from the ctx every caller already passes (ExtensionContext exposes
277
276
  // sessionManager). Back-compat: when neither is present, no filtering.