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
@@ -3,8 +3,9 @@ import { matchesKey } from "@earendil-works/pi-tui";
3
3
  import { agentOutputPath, readCrewAgents } from "../runtime/crew-agent-records.ts";
4
4
  import type { TeamRunManifest } from "../state/types.ts";
5
5
  import { resolveRealContainedPath } from "../utils/safe-paths.ts";
6
- import { pad, truncate, truncateToVisualLines, truncateToVisualLinesTail } from "../utils/visual.ts";
6
+ import { truncate, truncateToVisualLines, truncateToVisualLinesTail } from "../utils/visual.ts";
7
7
  import type { InteractiveComponent } from "./component.ts";
8
+ import { canopyLine, formatHint, overflowHint, RAIL, railLine } from "./rail.ts";
8
9
  import { renderDiff } from "./render-diff.ts";
9
10
  import { colorForStatus, iconForStatus, type RunStatus } from "./status-colors.ts";
10
11
  import { highlightCode, highlightJson } from "./syntax-highlight.ts";
@@ -244,6 +245,21 @@ interface ViewerState {
244
245
  sourceLen: number;
245
246
  }
246
247
 
248
+ /**
249
+ * Overlay footer hints in the ONE rail hint format (`formatHint`): keys `label`
250
+ * pairs joined by ` · `, close action LAST, `Esc` spelled via `keyToken`.
251
+ * The bound keys/actions are unchanged — `handleInput` still reads j/k, PgUp/
252
+ * PgDn, g/G, a, f and q/Esc exactly as before.
253
+ */
254
+ const VIEWER_HINTS = formatHint([
255
+ [["j", "k"], "scroll"],
256
+ [["pageup", "pagedown"], "page"],
257
+ ["g/G", "top/bottom"],
258
+ ["a", "auto"],
259
+ ["f", "full/tail"],
260
+ [["q", "escape"], "close"],
261
+ ]);
262
+
247
263
  function renderViewerBase(state: ViewerState, width: number, lines: string[], title: string, subtitle: string): string[] {
248
264
  const inner = Math.max(20, width - 4);
249
265
  // PERF (2026-08-24): transcripts grow to thousands of lines; wrapping every
@@ -289,21 +305,23 @@ function renderViewerBase(state: ViewerState, width: number, lines: string[], ti
289
305
  const totalLines = tailWindow ? skippedCount + visible.length : visualLines.length;
290
306
  const totalLabel = tailWindow && skippedCount > 0 ? `≥ ${totalLines}` : `${totalLines}`;
291
307
  const statusLine = `${totalLabel} lines · ${totalLines ? Math.round(((state.scroll + visible.length) / totalLines) * 100) : 100}% · auto-scroll ${state.autoScroll ? "on" : "off"}`;
292
- const fg = (color: Parameters<TranscriptTheme["fg"]>[0], text: string) => state.theme.fg(color, text);
293
- const row = (text: string) => `${fg("border", "│")} ${pad(truncate(text, inner), inner)} ${fg("border", "│")}`;
308
+ // RAIL frame (design system §2.E): `┏ NAME ▸ SUBJECT` canopy, `┃` body rows,
309
+ // `┗ <outcome/hint>` close. The rounded `╭─╮│╰─╯` box is retired. This
310
+ // viewer is a standalone overlay that paints its own frame, so the rail
311
+ // glyphs are correct here (dashboard PANES must not draw them).
312
+ const budget = Math.max(18, width - 2);
313
+ const row = (text: string) => railLine(RAIL.body, "border", text, state.theme, budget);
294
314
  const linesOut: string[] = [
295
- fg("border", `╭${"─".repeat(inner + 2)}╮`),
296
- row(`${fg("accent", title)} ${fg("dim", subtitle)}`),
297
- row(fg("dim", "j/k scroll · PgUp/PgDn · g/G top/bottom · a auto · f full/tail · q close")),
298
- fg("border", `├${"─".repeat(inner + 2)}┤`),
299
- ...visible.map(row),
300
- fg("border", `├${"─".repeat(inner + 2)}┤`),
301
- row(fg("dim", statusLine)),
302
- fg("border", `╰${"─".repeat(inner + 2)}╯`),
315
+ canopyLine({ word: "TRANSCRIPT", subject: title, theme: state.theme, budget, right: subtitle }),
316
+ row(state.theme.fg("dim", VIEWER_HINTS)),
303
317
  ];
304
318
  if (skippedCount > 0) {
305
- linesOut.splice(linesOut.length - 1, 0, row(fg("muted", `… (${skippedCount} lines truncated above`)));
319
+ // ONE overflow dialect (`▲ n above` / `▼ m below`) instead of the legacy
320
+ // `… (n lines truncated above` (which also never closed its paren).
321
+ linesOut.push(row(state.theme.fg("muted", overflowHint(skippedCount, 0, state.theme))));
306
322
  }
323
+ for (const visibleLine of visible) linesOut.push(row(visibleLine));
324
+ linesOut.push(railLine(RAIL.close, "border", state.theme.fg("dim", statusLine), state.theme, budget));
307
325
  return linesOut.map((line) => truncate(line, width));
308
326
  }
309
327
 
@@ -14,17 +14,22 @@ import { truncate } from "../../utils/visual.ts";
14
14
  import { panelRowsFromRuns } from "../inline-panel/panel-rows.ts";
15
15
  import { panelDisplayState, setPanelRowsProvider, subscribePanelChange } from "../inline-panel/panel-store.ts";
16
16
  import { requestRender, requestRenderTarget, setExtensionWidget } from "../pi-ui-compat.ts";
17
+ import type { RailSlot } from "../rail.ts";
17
18
  import type { OverlaySchedulerHandle } from "../shared-overlay-scheduler.ts";
18
19
  import { registerOverlayScheduler } from "../shared-overlay-scheduler.ts";
19
20
  import type { RunSnapshotCache } from "../snapshot-types.ts";
20
- import { spinnerBucket, spinnerFrame } from "../spinner.ts";
21
+ import { spinnerBucket } from "../spinner.ts";
21
22
  import type { CrewTheme } from "../theme-adapter.ts";
22
23
  import { asCrewTheme, subscribeThemeChange } from "../theme-adapter.ts";
23
24
  import { buildTaskListLines } from "./task-list.ts";
24
25
  import { activeWidgetRuns, statusSummary } from "./widget-model.ts";
25
- import { buildWidgetLines, colorWidgetLine, DEFAULT_WIDGET_WIDTH, renderLines, schedulesWidgetLine } from "./widget-renderer.ts";
26
+ import { buildWidgetLines, colorWidgetLine, idleWidgetLine, renderLines, schedulesWidgetLine, widgetRailSlot } from "./widget-renderer.ts";
26
27
  import type { CrewWidgetModel, CrewWidgetState, WidgetRun } from "./widget-types.ts";
27
28
 
29
+ export {
30
+ NOTIFICATION_BADGE_CAP,
31
+ notificationBadge,
32
+ } from "./widget-formatters.ts";
28
33
  export { activeWidgetRuns, statusSummary } from "./widget-model.ts";
29
34
  export {
30
35
  buildWidgetLines as buildCrewWidgetLines,
@@ -39,27 +44,6 @@ export type {
39
44
  WidgetRun,
40
45
  } from "./widget-types.ts";
41
46
 
42
- /**
43
- * Resolve the real render width for widget lines, in priority order:
44
- * 1. explicit `width` argument (e.g. from caller that already knows terminal width)
45
- * 2. `process.stdout.columns` (works in Node when stdout is a TTY)
46
- * 3. `DEFAULT_WIDGET_WIDTH` (100) — last-resort fallback so we never paint
47
- * a line wider than the smallest expected TUI.
48
- *
49
- * Callers SHOULD pass the width they already hold (e.g. `WidgetRender.render(width)`
50
- * in this file already receives one). This helper exists for paths that don't.
51
- */
52
- export function getRenderWidth(width?: number): number {
53
- if (Number.isFinite(width) && width! > 0) return Math.floor(width!);
54
- const stdoutCols = (globalThis as { process?: { stdout?: { columns?: number } } }).process?.stdout?.columns;
55
- if (Number.isFinite(stdoutCols) && stdoutCols! > 0) return Math.floor(stdoutCols!);
56
- return DEFAULT_WIDGET_WIDTH;
57
- }
58
- export {
59
- NOTIFICATION_BADGE_CAP,
60
- notificationBadge,
61
- } from "./widget-formatters.ts";
62
-
63
47
  // ── Constants ─────────────────────────────────────────────────────────
64
48
 
65
49
  const MAX_LINES_DEFAULT = DEFAULT_UI.widgetMaxLines;
@@ -270,9 +254,9 @@ class CrewWidgetComponent implements WidgetComponent {
270
254
  return sig;
271
255
  }
272
256
 
273
- private colorize(lines: string[], width: number): string[] {
257
+ private colorize(lines: string[], width: number, slot: RailSlot = "border"): string[] {
274
258
  return renderLines(
275
- lines.map((line, index) => colorWidgetLine(line, index, this.theme)),
259
+ lines.map((line, index) => colorWidgetLine(line, index, this.theme, slot)),
276
260
  width,
277
261
  );
278
262
  }
@@ -334,14 +318,13 @@ class CrewWidgetComponent implements WidgetComponent {
334
318
  this.cachedBuildSignatureAt = now;
335
319
  }
336
320
  const signature = `${sigBase}:${this.model.notificationCount ?? 0}`;
337
- const runningGlyph = spinnerFrame("widget-header");
338
321
 
339
322
  // Task-list variant: the run's plan above the editor (task-list.ts).
340
323
  // No panel state, no spinner glyph — the list changes only on task
341
324
  // transitions, which the run signature already covers.
342
325
  if (this.variant === "tasks") {
343
326
  if (this.cacheSignature !== signature || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
344
- this.cachedBaseLines = buildTaskListLines(runs, width);
327
+ this.cachedBaseLines = buildTaskListLines(runs, width, this.theme);
345
328
  this.cachedLines = this.colorize(this.cachedBaseLines, width);
346
329
  this.cachedWidth = width;
347
330
  this.cachedTheme = this.theme;
@@ -372,11 +355,12 @@ class CrewWidgetComponent implements WidgetComponent {
372
355
  const schedLine = schedulesWidgetLine(this.model.cwd, schedNow);
373
356
  const signatureWithPanel = `${signature}|panel:${panel.selectedTaskId ?? ""}/${panel.viewedTaskId ?? ""}/${panel.focused ? 1 : 0}|sched:${schedLine ?? ""}`;
374
357
 
375
- // The spinner-frame swap only belongs on the LEGACY header, whose line 0
376
- // already starts with a glyph position (`<frame> Crew agents …`). The
377
- // compact dock's line 0 is the HINT text ("agents (N) — ↓ to select"):
378
- // swapping would visibly eat its first character on every frame.
379
- const compactDock = this.model.rowStyle === "compact";
358
+ // RAIL (§2.B): the dock is ONE row whose identity is `CREW ▸ <subject>`
359
+ // and whose leading `┃` carries the AGGREGATE run state as its colour.
360
+ // The spinner rides INSIDE the row (buildWidgetLines composes it), so the
361
+ // old "swap line[0]'s first character" hack is gone — it would eat the
362
+ // rail glyph. The cache key still carries the spinner bucket when a worker
363
+ // is running, so the row rebuilds once per frame and animates.
380
364
  if (this.cacheSignature !== signatureWithPanel || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
381
365
  this.cachedBaseLines = buildWidgetLines(
382
366
  this.model.cwd,
@@ -385,12 +369,9 @@ class CrewWidgetComponent implements WidgetComponent {
385
369
  runs,
386
370
  this.model.notificationCount ?? 0,
387
371
  width,
388
- { rowStyle: this.model.rowStyle, now: schedNow, ...panel },
389
- ).map((line, index) => {
390
- if (!compactDock && index === 0 && line.length > 0) return `${runningGlyph}${line.slice(1)}`;
391
- return line;
392
- });
393
- this.cachedLines = this.colorize(this.cachedBaseLines, width);
372
+ { now: schedNow, ...panel },
373
+ );
374
+ this.cachedLines = this.colorize(this.cachedBaseLines, width, widgetRailSlot(runs));
394
375
  this.cachedWidth = width;
395
376
  this.cachedTheme = this.theme;
396
377
  this.cacheSignature = signatureWithPanel;
@@ -408,20 +389,22 @@ class CrewWidgetComponent implements WidgetComponent {
408
389
  // meaningful on run-transition frames, which always have runs>0 and take
409
390
  // a different branch. At zero runs the only paintable content is the
410
391
  // schedules line (or nothing).
411
- // Single-line contract (round 3): the keep-alive row carries the
412
- // ↓·enter interaction hint + the focused ❯ marker — identical to
413
- // buildWidgetLines' zero-runs branch.
414
- const zeroBase = `${schedLine} — ↓·enter`;
415
- const focused = panel.focused === true;
416
- return [truncate(focused ? `❯ ${zeroBase}` : zeroBase, width)];
392
+ // Single-line contract (round 3): the keep-alive row is the SAME
393
+ // `┃ CREW ▸ idle · ⏰ … ···· ↓·enter` row buildWidgetLines paints at zero
394
+ // runs (idleWidgetLine) — one helper, one row shape.
395
+ // Zero runs + no schedules = nothing to paint. Guard BEFORE composing:
396
+ // without it the row painted the literal string `undefined — ↓·enter`
397
+ // (live bug 2026-09-16: right after a team run finished,
398
+ // activeWidgetRuns() empties and schedulesWidgetLine() returns
399
+ // undefined). widget-renderer.buildWidgetLines has always guarded this
400
+ // branch; the component path did not.
401
+ if (!schedLine) return [];
402
+ const zeroLine = idleWidgetLine(schedLine, panel.focused === true, width);
403
+ if (!zeroLine) return [];
404
+ return [truncate(colorWidgetLine(zeroLine, 0, this.theme), width)];
417
405
  }
418
406
 
419
407
  this.ensureTruncated(width);
420
- if (!compactDock) {
421
- const updatedHeader = `${runningGlyph}${this.cachedBaseLines[0]?.slice(1) ?? ""}`;
422
- this.cachedLines[0] = truncate(colorWidgetLine(updatedHeader, 0, this.theme), width);
423
- this.truncatedLines[0] = truncate(this.cachedLines[0], width);
424
- }
425
408
  return this.truncatedLines;
426
409
  }
427
410
  }
@@ -451,7 +434,6 @@ export function updateCrewWidget(
451
434
  }
452
435
 
453
436
  const runs = activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests, workspaceId);
454
- const rowStyle = config?.widgetRowStyle ?? DEFAULT_UI.widgetRowStyle;
455
437
  // The inline panel navigates the same run list the widget paints, and this is
456
438
  // the only place already holding the manifest/snapshot caches — so the row
457
439
  // projection is registered here instead of re-reading state on every keypress.
@@ -524,7 +506,6 @@ export function updateCrewWidget(
524
506
  snapshotCache,
525
507
  preloadManifests: preloadedManifests,
526
508
  workspaceId,
527
- rowStyle,
528
509
  };
529
510
  else {
530
511
  state.model.cwd = ctx.cwd;
@@ -535,7 +516,6 @@ export function updateCrewWidget(
535
516
  state.model.snapshotCache = snapshotCache;
536
517
  state.model.preloadManifests = preloadedManifests;
537
518
  state.model.workspaceId = workspaceId;
538
- state.model.rowStyle = rowStyle;
539
519
  }
540
520
 
541
521
  if (needsWidgetInstall) {
@@ -1,18 +1,20 @@
1
1
  /**
2
- * task-list.ts — the run's plan, painted above the editor (pi-tasks /
3
- * Claude Code style): a `● N tasks (…)` header, then one row per task in
4
- * PLAN ORDER with task numbers (#1, #2, …) instead of technical ids.
2
+ * task-list.ts — the run's plan, painted above the editor, in the RAIL card
3
+ * grammar (design system §2.A): a `┏ PLAN ▸ <team/workflow>` canopy carrying
4
+ * the progress gauge, one `┃` row per task in PLAN ORDER with task numbers
5
+ * (#1, #2, …), and a `┗ <counts>` cap.
5
6
  * Completed rows are dimmed and struck through; the task actively executing
6
7
  * carries a spinner frame with elapsed time and token counts; queued rows
7
8
  * name the dependencies they wait on.
8
9
  *
9
10
  * Example:
10
11
  * ```
11
- * ● 4 tasks (1 done, 1 in progress, 2 open)
12
- * ✔ #1 Design the flux capacitor
13
- * ⠙ #2 Wire the overlay scroll (2m 49s · ↑ 4.1k ↓ 1.2k)
14
- * ◻ #3 Run the suite › blocked by #2
15
- * ◻ #4 Report the result
12
+ * ┏ PLAN ▸ default/build ···················· ▕████▎░░░░░▏ 2/5
13
+ * ┃ ✔ #1 Design the flux capacitor
14
+ * ┃ ⠙ #2 Wire the overlay scroll (2m 49s · ↑ 4.1k ↓ 1.2k)
15
+ * ┃ ◻ #3 Run the suite › blocked by #2
16
+ * ┃ ◻ #4 Report the result
17
+ * ┗ 2 done · 2 in progress
16
18
  * ```
17
19
  *
18
20
  * Pure plan surface: which worker/agent/model runs a task is the dock's
@@ -24,11 +26,21 @@
24
26
  */
25
27
 
26
28
  import type { TeamTaskState } from "../../state/types.ts";
27
- import { truncate } from "../../utils/visual.ts";
29
+ import { visibleWidth } from "../../utils/visual.ts";
30
+ import { formatCount } from "../format-helpers.ts";
31
+ import { canopyLine, gaugeBar, overflowHint, RAIL, railLine, statusSlot } from "../rail.ts";
28
32
  import { spinnerFrame } from "../spinner.ts";
33
+ import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
29
34
  import { shortRunLabel } from "./widget-model.ts";
30
35
  import type { WidgetRun } from "./widget-types.ts";
31
36
 
37
+ /** Plain (uncoloured) default — the widget component passes the real theme. */
38
+ const PLAIN_THEME = asCrewTheme(undefined);
39
+
40
+ /** Gauge width on the canopy line (eighth-block precision, so it stays
41
+ * truthful even at 10 cells). */
42
+ const GAUGE_WIDTH = 12;
43
+
32
44
  /** Task rows painted before the "… and N more" overflow line (pi-tasks
33
45
  * defaults its `maxVisible` to 10). */
34
46
  const MAX_TASK_ROWS = 10;
@@ -125,21 +137,16 @@ function blockedSuffix(task: TeamTaskState, tasks: readonly TeamTaskState[], num
125
137
  }
126
138
 
127
139
  /** One line per task: `#n title` behind its status glyph — pi-tasks style.
128
- * Completed titles are dimmed and struck through. */
129
- function taskRow(
130
- task: TeamTaskState,
131
- taskNumber: number,
132
- tasks: readonly TeamTaskState[],
133
- numberById: Map<string, number>,
134
- width: number,
135
- ): string {
140
+ * Completed titles are dimmed and struck through. Returns the ROW CONTENT:
141
+ * the rail glyph (`┃ `) is owned by `railLine`. */
142
+ function taskRow(task: TeamTaskState, taskNumber: number, tasks: readonly TeamTaskState[], numberById: Map<string, number>): string {
136
143
  const title = taskTitle(task);
137
144
  const suffix = task.status === "running" ? runningSuffix(task) : blockedSuffix(task, tasks, numberById);
138
145
  if (task.status === "completed") {
139
146
  const struck = `${STRIKE_ON}${DIM_ON}#${taskNumber} ${title}${DIM_OFF}${STRIKE_OFF}`;
140
- return truncate(` ✔ ${struck}`.trimEnd(), width);
147
+ return `${taskStatusIcon(task)} ${struck}`;
141
148
  }
142
- return truncate(` ${taskStatusIcon(task)} #${taskNumber} ${title}${suffix}`.trimEnd(), width);
149
+ return `${taskStatusIcon(task)} #${taskNumber} ${title}${suffix}`.trimEnd();
143
150
  }
144
151
 
145
152
  /** The run whose plan is shown: the first with unfinished work, else the first. */
@@ -163,15 +170,17 @@ function visibleTasks(tasks: readonly TeamTaskState[]): TeamTaskState[] {
163
170
  }
164
171
 
165
172
  /**
166
- * Build the task-list lines for the current runs. Empty when no run carries a
167
- * tasks slice (nothing to say — the dock alone stays).
173
+ * Build the plan card for the current runs: canopy (identity + gauge), one
174
+ * `┃` row per task, the overflow hint, and the `┗` counts cap. Empty when no
175
+ * run carries a tasks slice (nothing to say — the dock alone stays).
168
176
  */
169
- export function buildTaskListLines(runs: readonly WidgetRun[], width: number): string[] {
177
+ export function buildTaskListLines(runs: readonly WidgetRun[], width: number, theme: CrewTheme = PLAIN_THEME): string[] {
170
178
  const entry = primaryRun(runs);
171
179
  if (!entry) return [];
172
180
  const tasks = entry.snapshot?.tasks ?? [];
173
181
  if (tasks.length === 0) return [];
174
182
 
183
+ const budget = Math.max(8, width - 2);
175
184
  const numberById = new Map(tasks.map((task, index) => [task.id, index + 1]));
176
185
  const done = tasks.filter((task) => task.status === "completed").length;
177
186
  const dead = tasks.filter((task) => isDoneStatus(task.status) && task.status !== "completed").length;
@@ -179,20 +188,43 @@ export function buildTaskListLines(runs: readonly WidgetRun[], width: number): s
179
188
  (task) => task.status === "running" || task.status === "waiting" || task.status === "needs_attention",
180
189
  ).length;
181
190
  const open = tasks.filter((task) => task.status === "queued").length;
182
-
183
- const counts = [`${done} done`];
184
- if (dead) counts.push(`${dead} failed`);
185
- counts.push(`${inProgress} in progress`, `${open} open`);
186
- const headerParts = [`● ${tasks.length} tasks (${counts.join(", ")})`];
187
- if (runs.length > 1) headerParts.push(shortRunLabel(entry.run));
188
- const lines = [truncate(headerParts.join(" · "), width)];
191
+ const slot = statusSlot(entry.run.status);
192
+
193
+ // Canopy: `┏ PLAN ▸ <title>` + the progress gauge on the right, where a bar
194
+ // fits (a 12-cell bar plus its tally needs ~6 columns of clearance).
195
+ const title = shortRunLabel(entry.run);
196
+ const ratio = done / tasks.length;
197
+ const tally = `${done}/${tasks.length}`;
198
+ const gauge = `${gaugeBar(ratio, GAUGE_WIDTH, theme, dead > 0 ? "error" : "success")} ${tally}`;
199
+ const labelWidth = visibleWidth(`PLAN ${title}`) + 5; // word + ` ▸ `
200
+ const right = budget - labelWidth - 6 >= GAUGE_WIDTH + tally.length ? gauge : undefined;
201
+ const lines = [
202
+ canopyLine({
203
+ word: "PLAN",
204
+ subject: title,
205
+ theme,
206
+ budget,
207
+ slot,
208
+ right,
209
+ }),
210
+ ];
189
211
 
190
212
  const visible = visibleTasks(tasks);
191
213
  for (const task of visible) {
192
- lines.push(taskRow(task, numberById.get(task.id) ?? 0, tasks, numberById, width));
214
+ lines.push(railLine(RAIL.body, "border", taskRow(task, numberById.get(task.id) ?? 0, tasks, numberById), theme, budget));
193
215
  }
194
- if (tasks.length > visible.length) {
195
- lines.push(truncate(` … and ${tasks.length - visible.length} more`, width));
216
+ const hidden = tasks.length - visible.length;
217
+ if (hidden > 0) {
218
+ // Canonical overflow dialect (RAIL §1) — the legacy `… and N more` is
219
+ // retired with the other three overflow spellings.
220
+ lines.push(railLine(RAIL.body, "border", overflowHint(0, hidden, theme), theme, budget));
196
221
  }
222
+
223
+ // Close cap: the counts (formatCount → singular/plural; the participle
224
+ // labels pass an explicit plural so nothing renders as `2 dones`).
225
+ const counts = [formatCount(done, "done", "done")];
226
+ if (dead) counts.push(formatCount(dead, "failed", "failed"));
227
+ counts.push(formatCount(inProgress, "in progress", "in progress"), formatCount(open, "open", "open"));
228
+ lines.push(railLine(RAIL.close, slot, counts.join(" · "), theme, budget));
197
229
  return lines;
198
230
  }