pi-crew 0.6.1 → 0.6.4

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 (119) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +81 -33
  3. package/docs/issue-29-analysis.md +189 -0
  4. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
  5. package/docs/ui-optimization-plan.md +447 -0
  6. package/package.json +2 -2
  7. package/src/config/config.ts +106 -15
  8. package/src/errors.ts +107 -0
  9. package/src/extension/async-notifier.ts +6 -2
  10. package/src/extension/crew-cleanup.ts +8 -5
  11. package/src/extension/management.ts +464 -109
  12. package/src/extension/register.ts +213 -35
  13. package/src/extension/registration/brief-tool-overrides.ts +400 -0
  14. package/src/extension/registration/commands.ts +27 -2
  15. package/src/extension/registration/subagent-helpers.ts +2 -2
  16. package/src/extension/registration/subagent-tools.ts +9 -4
  17. package/src/extension/registration/team-tool.ts +17 -10
  18. package/src/extension/registration/viewers.ts +2 -2
  19. package/src/extension/team-tool/api.ts +3 -3
  20. package/src/extension/team-tool/cancel.ts +3 -3
  21. package/src/extension/team-tool/explain.ts +1 -1
  22. package/src/extension/team-tool/handle-schedule.ts +40 -0
  23. package/src/extension/team-tool/inspect.ts +3 -3
  24. package/src/extension/team-tool/lifecycle-actions.ts +4 -4
  25. package/src/extension/team-tool/respond.ts +2 -2
  26. package/src/extension/team-tool/run.ts +64 -14
  27. package/src/extension/team-tool/status.ts +1 -1
  28. package/src/extension/team-tool-types.ts +2 -0
  29. package/src/extension/team-tool.ts +173 -46
  30. package/src/hooks/registry.ts +77 -13
  31. package/src/hooks/types.ts +9 -0
  32. package/src/plugins/plugin-define.ts +6 -0
  33. package/src/plugins/plugin-registry.ts +32 -0
  34. package/src/plugins/plugins/index.ts +3 -0
  35. package/src/plugins/plugins/nextjs.ts +19 -0
  36. package/src/plugins/plugins/vite.ts +14 -0
  37. package/src/plugins/plugins/vitest.ts +14 -0
  38. package/src/runtime/async-runner.ts +35 -7
  39. package/src/runtime/background-runner.ts +529 -144
  40. package/src/runtime/chain-parser.ts +5 -1
  41. package/src/runtime/chain-runner.ts +9 -3
  42. package/src/runtime/checkpoint.ts +262 -180
  43. package/src/runtime/child-pi.ts +164 -37
  44. package/src/runtime/crash-recovery.ts +25 -14
  45. package/src/runtime/crew-agent-records.ts +2 -0
  46. package/src/runtime/diagnostic-export.ts +1 -1
  47. package/src/runtime/dynamic-script-runner.ts +10 -7
  48. package/src/runtime/foreground-watchdog.ts +1 -1
  49. package/src/runtime/heartbeat-watcher.ts +19 -2
  50. package/src/runtime/intercom-bridge.ts +7 -0
  51. package/src/runtime/iteration-hooks.ts +1 -1
  52. package/src/runtime/manifest-cache.ts +4 -0
  53. package/src/runtime/orphan-worker-registry.ts +444 -0
  54. package/src/runtime/parent-guard.ts +70 -16
  55. package/src/runtime/pi-args.ts +437 -14
  56. package/src/runtime/pi-spawn.ts +1 -0
  57. package/src/runtime/post-checks.ts +1 -1
  58. package/src/runtime/retry-runner.ts +9 -3
  59. package/src/runtime/run-tracker.ts +38 -10
  60. package/src/runtime/sandbox.ts +17 -2
  61. package/src/runtime/scheduler.ts +25 -2
  62. package/src/runtime/skill-effectiveness.ts +105 -62
  63. package/src/runtime/skill-instructions.ts +110 -32
  64. package/src/runtime/stale-reconciler.ts +310 -69
  65. package/src/runtime/subagent-manager.ts +251 -57
  66. package/src/runtime/task-health.ts +76 -0
  67. package/src/runtime/task-id.ts +20 -0
  68. package/src/runtime/task-runner/live-executor.ts +1 -1
  69. package/src/runtime/task-runner/progress.ts +1 -1
  70. package/src/runtime/task-runner/state-helpers.ts +110 -6
  71. package/src/runtime/task-runner.ts +92 -70
  72. package/src/runtime/team-runner.ts +186 -20
  73. package/src/schema/team-tool-schema.ts +27 -9
  74. package/src/skills/discover-skills.ts +9 -3
  75. package/src/state/active-run-registry.ts +170 -38
  76. package/src/state/artifact-store.ts +25 -13
  77. package/src/state/atomic-write-v2.ts +86 -0
  78. package/src/state/atomic-write.ts +346 -55
  79. package/src/state/blob-store.ts +178 -10
  80. package/src/state/crew-init.ts +161 -28
  81. package/src/state/decision-ledger.ts +172 -111
  82. package/src/state/event-log-rotation.ts +82 -52
  83. package/src/state/event-log.ts +254 -70
  84. package/src/state/health-store.ts +71 -0
  85. package/src/state/locks.ts +102 -20
  86. package/src/state/mailbox.ts +45 -7
  87. package/src/state/observation-store.ts +4 -1
  88. package/src/state/run-graph.ts +141 -130
  89. package/src/state/run-metrics.ts +24 -8
  90. package/src/state/session-state-map.ts +51 -0
  91. package/src/state/state-store.ts +330 -43
  92. package/src/ui/live-run-sidebar.ts +1 -1
  93. package/src/ui/loaders.ts +4 -0
  94. package/src/ui/overlays/agent-picker-overlay.ts +1 -1
  95. package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
  96. package/src/ui/powerbar-publisher.ts +1 -1
  97. package/src/ui/run-action-dispatcher.ts +2 -2
  98. package/src/ui/run-snapshot-cache.ts +1 -1
  99. package/src/ui/status-colors.ts +5 -1
  100. package/src/ui/theme-adapter.ts +80 -1
  101. package/src/ui/tool-progress-formatter.ts +9 -5
  102. package/src/ui/tool-render.ts +4 -0
  103. package/src/ui/tool-renderers/brief-mode.ts +207 -0
  104. package/src/ui/tool-renderers/index.ts +627 -0
  105. package/src/ui/widget/index.ts +224 -0
  106. package/src/ui/widget/widget-formatters.ts +148 -0
  107. package/src/ui/widget/widget-model.ts +90 -0
  108. package/src/ui/widget/widget-renderer.ts +130 -0
  109. package/src/ui/widget/widget-types.ts +37 -0
  110. package/src/utils/env-filter.ts +66 -0
  111. package/src/utils/file-coalescer.ts +9 -1
  112. package/src/utils/guards.ts +110 -0
  113. package/src/utils/paths.ts +80 -5
  114. package/src/utils/redaction.ts +6 -1
  115. package/src/utils/safe-paths.ts +281 -10
  116. package/src/worktree/cleanup.ts +112 -24
  117. package/src/worktree/worktree-manager.ts +128 -15
  118. package/test-bugs-all.mjs +10 -6
  119. package/test-integration-check.ts +4 -0
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Crew widget — public API and component.
3
+ *
4
+ * Re-exports from widget submodules. The main component class and
5
+ * update/stop functions live here.
6
+ */
7
+
8
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
9
+ import type { CrewUiConfig } from "../../config/config.ts";
10
+ import type { ManifestCache } from "../../runtime/manifest-cache.ts";
11
+ import type { TeamRunManifest } from "../../state/types.ts";
12
+ import type { RunSnapshotCache, RunUiSnapshot } from "../snapshot-types.ts";
13
+ import type { CrewTheme } from "../theme-adapter.ts";
14
+ import { asCrewTheme, subscribeThemeChange } from "../theme-adapter.ts";
15
+ import { truncate } from "../../utils/visual.ts";
16
+ import { requestRender, setExtensionWidget } from "../pi-ui-compat.ts";
17
+ import { spinnerBucket, spinnerFrame } from "../spinner.ts";
18
+ import { runEventBus } from "../run-event-bus.ts";
19
+ import { DEFAULT_UI } from "../../config/defaults.ts";
20
+ import { activeWidgetRuns, statusSummary } from "./widget-model.ts";
21
+ import { buildWidgetLines, colorWidgetLine, renderLines } from "./widget-renderer.ts";
22
+ import type { CrewWidgetModel, CrewWidgetState, WidgetRun } from "./widget-types.ts";
23
+
24
+ // Re-export types and helpers for backward compatibility
25
+ export type { WidgetRun, CrewWidgetModel, CrewWidgetState } from "./widget-types.ts";
26
+ export { activeWidgetRuns, statusSummary } from "./widget-model.ts";
27
+ export { buildWidgetLines as buildCrewWidgetLines, widgetHeader } from "./widget-renderer.ts";
28
+ export { notificationBadge } from "./widget-formatters.ts";
29
+
30
+ // ── Constants ─────────────────────────────────────────────────────────
31
+
32
+ const MAX_LINES_DEFAULT = DEFAULT_UI.widgetMaxLines;
33
+ const LEGACY_WIDGET_KEY = "pi-crew";
34
+ const WIDGET_KEY = "pi-crew-active";
35
+ const STATUS_KEY = "pi-crew";
36
+
37
+ // ── Widget Component ──────────────────────────────────────────────────
38
+
39
+ interface WidgetComponent {
40
+ render(width: number): string[];
41
+ invalidate(): void;
42
+ }
43
+
44
+ class CrewWidgetComponent implements WidgetComponent {
45
+ private readonly model: CrewWidgetModel;
46
+ private theme: CrewTheme;
47
+ private cacheSignature = "";
48
+ private cachedWidth = 0;
49
+ private cachedLines: string[] = [];
50
+ private cachedBaseLines: string[] = [];
51
+ private cachedTheme: CrewTheme;
52
+ private readonly unsubscribeTheme: () => void;
53
+ private readonly unsubscribeEventBus: () => void;
54
+
55
+ constructor(model: CrewWidgetModel, themeLike: unknown) {
56
+ this.model = model;
57
+ this.theme = asCrewTheme(themeLike);
58
+ this.cachedTheme = this.theme;
59
+ this.unsubscribeTheme = subscribeThemeChange(themeLike, () => this.invalidate());
60
+ this.unsubscribeEventBus = runEventBus.onAny(() => this.invalidate());
61
+ }
62
+
63
+ private buildSignature(runs: WidgetRun[]): string {
64
+ const liveSig = [...listLiveAgents()].map((h) =>
65
+ `${h.agentId}:${h.status}:${h.activity.turnCount}:${h.activity.toolUses}:${[...h.activity.activeTools.values()].join(",")}:${h.activity.responseText.slice(-30)}`
66
+ ).join("|");
67
+
68
+ const hasRunning = runs.some((entry) => entry.agents.some((a) => a.status === "running"))
69
+ || [...listLiveAgents()].some((h) => h.status === "running");
70
+ const animation = hasRunning ? `:spin=${spinnerBucket()}` : "";
71
+
72
+ return runs
73
+ .map((entry) => entry.snapshot?.signature ?? `${entry.run.runId}:${entry.run.status}:${entry.run.updatedAt}:` +
74
+ entry.agents.map((a) => {
75
+ const recentOutput = a.progress?.recentOutput.at(-1) ?? "";
76
+ const progress = [a.progress?.currentTool ?? "", a.progress?.toolCount ?? 0, a.progress?.tokens ?? 0, a.progress?.turns ?? 0, a.progress?.lastActivityAt ?? "", recentOutput].join(":");
77
+ return `${a.status}:${a.startedAt}:${a.completedAt ?? ""}:${a.toolUses ?? 0}:${progress}`;
78
+ }).join(","))
79
+ .join("|") + `|live:${liveSig}${animation}`;
80
+ }
81
+
82
+ private colorize(lines: string[], width: number): string[] {
83
+ return renderLines(
84
+ lines.map((line, index) => colorWidgetLine(line, index, this.theme)),
85
+ width,
86
+ );
87
+ }
88
+
89
+ invalidate(): void {
90
+ this.cacheSignature = "";
91
+ this.cachedBaseLines = [];
92
+ this.cachedLines = [];
93
+ }
94
+
95
+ dispose(): void {
96
+ this.unsubscribeTheme();
97
+ this.unsubscribeEventBus();
98
+ }
99
+
100
+ render(width: number): string[] {
101
+ const runs = activeWidgetRuns(this.model.cwd, this.model.manifestCache, this.model.snapshotCache, this.model.preloadManifests);
102
+ const signature = `${this.buildSignature(runs)}:${this.model.notificationCount ?? 0}`;
103
+ const runningGlyph = spinnerFrame("widget-header");
104
+
105
+ if (this.cacheSignature !== signature || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
106
+ this.cachedBaseLines = buildWidgetLines(this.model.cwd, 0, this.model.maxLines, runs, this.model.notificationCount ?? 0).map((line, index) => {
107
+ if (index === 0 && line.length > 0) return `${runningGlyph}${line.slice(1)}`;
108
+ return line;
109
+ });
110
+ this.cachedLines = this.colorize(this.cachedBaseLines, width);
111
+ this.cachedWidth = width;
112
+ this.cachedTheme = this.theme;
113
+ this.cacheSignature = signature;
114
+ }
115
+
116
+ if (runs.length === 0) {
117
+ this.invalidate();
118
+ return [];
119
+ }
120
+
121
+ const updatedHeader = `${runningGlyph}${this.cachedBaseLines[0]?.slice(1) ?? ""}`;
122
+ this.cachedLines[0] = truncate(colorWidgetLine(updatedHeader, 0, this.theme), width);
123
+ return this.cachedLines.map((line) => truncate(line, width));
124
+ }
125
+ }
126
+
127
+ // ── Re-export listLiveAgents for buildSignature ───────────────────────
128
+
129
+ import { listLiveAgents } from "../../runtime/live-agent-manager.ts";
130
+
131
+ // ── Public API ────────────────────────────────────────────────────────
132
+
133
+ export function updateCrewWidget(
134
+ ctx: Pick<ExtensionContext, "cwd" | "hasUI" | "ui" | "sessionManager">,
135
+ state: CrewWidgetState,
136
+ config?: CrewUiConfig,
137
+ manifestCache?: ManifestCache,
138
+ snapshotCache?: RunSnapshotCache,
139
+ preloadedManifests?: TeamRunManifest[],
140
+ ): void {
141
+ if (!ctx.hasUI) return;
142
+ state.frame += 1;
143
+ const maxLines = config?.widgetMaxLines ?? MAX_LINES_DEFAULT;
144
+
145
+ let workspaceId = ctx.sessionManager?.getSessionId?.();
146
+ if (!workspaceId && manifestCache) {
147
+ const runs = manifestCache.list(20);
148
+ const active = runs.find((r) => r.status === "running" || r.status === "queued");
149
+ if (active?.ownerSessionId) workspaceId = active.ownerSessionId;
150
+ }
151
+
152
+ const runs = activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests, workspaceId);
153
+ const lines = buildWidgetLines(ctx.cwd, state.frame, maxLines, runs, state.notificationCount ?? 0);
154
+ const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
155
+
156
+ ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(runs) : undefined);
157
+
158
+ const shouldClearLegacy = state.legacyCleared !== true || state.lastPlacement !== placement;
159
+ if (shouldClearLegacy) {
160
+ setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
161
+ state.legacyCleared = true;
162
+ }
163
+
164
+ if (!lines.length) {
165
+ if (state.lastVisibility !== "hidden" || state.lastPlacement !== placement) {
166
+ setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
167
+ state.lastVisibility = "hidden";
168
+ state.lastPlacement = placement;
169
+ state.lastKey = WIDGET_KEY;
170
+ state.lastMaxLines = maxLines;
171
+ state.lastCwd = ctx.cwd;
172
+ state.model = undefined;
173
+ }
174
+ requestRender(ctx);
175
+ return;
176
+ }
177
+
178
+ const needsWidgetInstall = state.lastVisibility !== "visible" || state.lastPlacement !== placement || state.lastKey !== WIDGET_KEY || state.lastMaxLines !== maxLines || state.lastCwd !== ctx.cwd || !state.model;
179
+
180
+ if (!state.model) state.model = { cwd: ctx.cwd, frame: state.frame, maxLines, notificationCount: state.notificationCount ?? 0, manifestCache, snapshotCache, preloadManifests: preloadedManifests };
181
+ else {
182
+ state.model.cwd = ctx.cwd;
183
+ state.model.frame = state.frame;
184
+ state.model.maxLines = maxLines;
185
+ state.model.notificationCount = state.notificationCount ?? 0;
186
+ state.model.manifestCache = manifestCache;
187
+ state.model.snapshotCache = snapshotCache;
188
+ state.model.preloadManifests = preloadedManifests;
189
+ }
190
+
191
+ if (needsWidgetInstall) {
192
+ const model = state.model;
193
+ setExtensionWidget(
194
+ ctx,
195
+ WIDGET_KEY,
196
+ ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(model, theme)) as never,
197
+ { placement, persist: true },
198
+ );
199
+ state.lastVisibility = "visible";
200
+ state.lastPlacement = placement;
201
+ state.lastKey = WIDGET_KEY;
202
+ state.lastMaxLines = maxLines;
203
+ state.lastCwd = ctx.cwd;
204
+ }
205
+
206
+ requestRender(ctx);
207
+ }
208
+
209
+ export function stopCrewWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined, state: CrewWidgetState, config?: CrewUiConfig): void {
210
+ if (state.interval) clearInterval(state.interval);
211
+ state.interval = undefined;
212
+ if (ctx?.hasUI) {
213
+ const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
214
+ ctx.ui.setStatus(STATUS_KEY, undefined);
215
+ setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
216
+ setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
217
+ state.lastVisibility = "hidden";
218
+ state.lastPlacement = placement;
219
+ state.lastKey = WIDGET_KEY;
220
+ state.model = undefined;
221
+ state.legacyCleared = true;
222
+ requestRender(ctx);
223
+ }
224
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Widget formatting utilities.
3
+ *
4
+ * Extracted from crew-widget.ts for reuse and testability.
5
+ */
6
+
7
+ import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
8
+ import type { LiveAgentHandle } from "../../runtime/live-agent-manager.ts";
9
+ import { getTaskUsage } from "../../runtime/usage-tracker.ts";
10
+
11
+ // ── Token formatting ──────────────────────────────────────────────────
12
+
13
+ export function formatTokensCompact(count: number): string {
14
+ if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M tok`;
15
+ if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k tok`;
16
+ return `${count} tok`;
17
+ }
18
+
19
+ // ── Elapsed time ──────────────────────────────────────────────────────
20
+
21
+ export function elapsed(iso: string | undefined, now = Date.now()): string | undefined {
22
+ if (!iso) return undefined;
23
+ const ms = Math.max(0, now - new Date(iso).getTime());
24
+ if (!Number.isFinite(ms)) return undefined;
25
+ if (ms < 1000) return "now";
26
+ if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
27
+ if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;
28
+ return `${Math.floor(ms / 3_600_000)}h`;
29
+ }
30
+
31
+ // ── Agent activity description ────────────────────────────────────────
32
+
33
+ const TOOL_LABELS: Record<string, string> = {
34
+ read: "reading",
35
+ bash: "running command",
36
+ edit: "editing",
37
+ write: "writing",
38
+ grep: "searching",
39
+ find: "finding files",
40
+ ls: "listing",
41
+ };
42
+
43
+ const TOOL_ICONS: Record<string, string> = {
44
+ read: "📖",
45
+ bash: ">",
46
+ edit: "✏",
47
+ write: "📝",
48
+ grep: "🔍",
49
+ find: "📁",
50
+ ls: "📋",
51
+ agent: "🤖",
52
+ };
53
+
54
+ export function describeLiveActivity(handle: LiveAgentHandle): string {
55
+ const act = handle.activity;
56
+ if (act.activeTools.size > 0) {
57
+ const groups = new Map<string, number>();
58
+ for (const toolName of act.activeTools.values()) {
59
+ groups.set(toolName, (groups.get(toolName) ?? 0) + 1);
60
+ }
61
+ const parts: string[] = [];
62
+ for (const [toolName, count] of groups) {
63
+ const icon = TOOL_ICONS[toolName] ?? "?";
64
+ const label = TOOL_LABELS[toolName] ?? toolName;
65
+ if (count > 1) {
66
+ parts.push(`${icon}${count} ${label}s`);
67
+ } else {
68
+ parts.push(`${icon} ${label}`);
69
+ }
70
+ }
71
+ return parts.join(", ") + "…";
72
+ }
73
+ if (act.responseText?.trim()) {
74
+ const line = act.responseText.split("\n").find((l) => l.trim())?.trim() ?? "";
75
+ return line.length > 60 ? line.slice(0, 60) + "…" : line;
76
+ }
77
+ return "thinking…";
78
+ }
79
+
80
+ export function agentActivity(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle): string {
81
+ if (liveHandle && liveHandle.status === "running") {
82
+ const live = describeLiveActivity(liveHandle);
83
+ if (live === "thinking…" && agent.progress?.currentTool) return `${TOOL_LABELS[agent.progress.currentTool] ?? agent.progress.currentTool}…`;
84
+ return live;
85
+ }
86
+ if (agent.progress?.currentTool) return `${TOOL_LABELS[agent.progress.currentTool] ?? agent.progress.currentTool}…`;
87
+ const recent = agent.progress?.recentOutput?.at(-1);
88
+ if (recent) {
89
+ const cleaned = recent.replace(/\s+/g, " ").trim();
90
+ return cleaned.length > 60 ? cleaned.slice(0, 60) + "…" : cleaned;
91
+ }
92
+ if (agent.progress?.activityState === "needs_attention") return "needs attention";
93
+ if (agent.status === "queued") return "queued";
94
+ if (agent.status === "running") {
95
+ const age = agent.startedAt ? Date.now() - new Date(agent.startedAt).getTime() : Infinity;
96
+ if (age < 5000 && !agent.progress?.currentTool) return "spawning…";
97
+ return "thinking…";
98
+ }
99
+ if (agent.status === "failed") return agent.error ?? "failed";
100
+ return "done";
101
+ }
102
+
103
+ // ── Agent stats line ──────────────────────────────────────────────────
104
+
105
+ export function agentStats(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle): string {
106
+ const parts: string[] = [];
107
+ if (liveHandle) {
108
+ const act = liveHandle.activity;
109
+ if (act.toolUses > 0) parts.push(`${act.toolUses} tools`);
110
+ const usage = getTaskUsage(liveHandle.taskId);
111
+ const total = (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheWrite ?? 0);
112
+ if (total > 0) parts.push(formatTokensCompact(total));
113
+ try {
114
+ const stats = liveHandle.session.getSessionStats?.();
115
+ const ctxPct = stats?.contextUsage?.percent;
116
+ if (ctxPct != null) parts.push(`${Math.round(ctxPct)}% ctx`);
117
+ } catch { /* ignore */ }
118
+ const rawStarted = act.startedAtMs || 0;
119
+ const rawCompleted = act.completedAtMs || 0;
120
+ const nowMs = Date.now();
121
+ const toMs = (v: number): number => {
122
+ if (v <= 0) return 0;
123
+ if (v > 1000000000 && v < 10000000000) return v * 1000;
124
+ if (v > 100000000000 && v < 10000000000000) return v;
125
+ return v;
126
+ };
127
+ const startedMs = toMs(rawStarted);
128
+ const completedMs = rawCompleted > 0 ? toMs(rawCompleted) : 0;
129
+ const isValidStarted = startedMs > 0 && startedMs < nowMs + 60000 && startedMs > nowMs - 3155692600000;
130
+ const ms = (completedMs > 0 && completedMs < nowMs + 60000 ? completedMs : nowMs) - (isValidStarted ? startedMs : nowMs);
131
+ parts.push(`${(ms / 1000).toFixed(1)}s`);
132
+ } else {
133
+ if (agent.toolUses) parts.push(`${agent.toolUses} tools`);
134
+ if (agent.progress?.tokens) parts.push(formatTokensCompact(agent.progress.tokens));
135
+ const age = elapsed(agent.completedAt ?? agent.startedAt);
136
+ if (age) parts.push(age);
137
+ }
138
+ return parts.join(" · ");
139
+ }
140
+
141
+ // ── Notification badge ────────────────────────────────────────────────
142
+
143
+ export function notificationBadge(count: number | undefined, env: NodeJS.ProcessEnv = process.env): string {
144
+ if (!count || count <= 0) return "";
145
+ const term = `${env.TERM ?? ""} ${env.WT_SESSION ?? ""} ${env.TERM_PROGRAM ?? ""}`.toLowerCase();
146
+ const supportsEmoji = !term.includes("dumb") && env.NO_COLOR !== "1";
147
+ return supportsEmoji ? ` 🔔${count}` : ` [!${count}]`;
148
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Widget data model — fetching, filtering, and caching.
3
+ *
4
+ * Extracted from crew-widget.ts.
5
+ */
6
+
7
+ import type { TeamRunManifest } from "../../state/types.ts";
8
+ import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
9
+ import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
10
+ import { isDisplayActiveRun } from "../../runtime/process-status.ts";
11
+ import { listLiveAgents, evictStaleLiveAgentHandles } from "../../runtime/live-agent-manager.ts";
12
+ import type { ManifestCache } from "../../runtime/manifest-cache.ts";
13
+ import { reconcileAllStaleRuns } from "../../runtime/crash-recovery.ts";
14
+ import { listRecentRuns } from "../../extension/run-index.ts";
15
+ import type { RunSnapshotCache } from "../snapshot-types.ts";
16
+ import type { WidgetRun } from "./widget-types.ts";
17
+
18
+ let lastStaleReconcileAt = 0;
19
+ const STALE_RECONCILE_INTERVAL_MS = 60_000;
20
+
21
+ function agentsFor(run: TeamRunManifest): CrewAgentRecord[] {
22
+ try {
23
+ return readCrewAgents(run);
24
+ } catch {
25
+ return [];
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Get active widget runs for display.
31
+ */
32
+ export function activeWidgetRuns(
33
+ cwd: string,
34
+ manifestCache?: ManifestCache,
35
+ snapshotCache?: RunSnapshotCache,
36
+ preloadedManifests?: TeamRunManifest[],
37
+ workspaceId?: string,
38
+ ): WidgetRun[] {
39
+ evictStaleLiveAgentHandles();
40
+
41
+ const now = Date.now();
42
+ if (now - lastStaleReconcileAt > STALE_RECONCILE_INTERVAL_MS && manifestCache) {
43
+ lastStaleReconcileAt = now;
44
+ try { reconcileAllStaleRuns(cwd, manifestCache); } catch { /* non-critical */ }
45
+ }
46
+
47
+ let runs = preloadedManifests ?? (manifestCache ? manifestCache.list(20) : listRecentRuns(cwd, 20));
48
+ if (workspaceId) {
49
+ runs = runs.filter((run) => !run.ownerSessionId || run.ownerSessionId === workspaceId);
50
+ }
51
+
52
+ return runs
53
+ .map((run) => {
54
+ try {
55
+ const snapshot = snapshotCache?.get(run.runId);
56
+ return snapshot
57
+ ? { run: snapshot.manifest, agents: snapshot.agents, snapshot }
58
+ : { run, agents: agentsFor(run) };
59
+ } catch {
60
+ return { run, agents: agentsFor(run) };
61
+ }
62
+ })
63
+ .filter((item) => isDisplayActiveRun(item.run, item.agents));
64
+ }
65
+
66
+ /**
67
+ * Build a status summary string for the status bar.
68
+ */
69
+ export function statusSummary(runs: WidgetRun[]): string {
70
+ const agents = runs.flatMap((item) => item.agents);
71
+ const runningAgents = agents.filter((a) => a.status === "running").length;
72
+ const queuedAgents = agents.filter((a) => a.status === "queued" || a.status === "waiting").length;
73
+ const completedAgents = agents.filter((a) => a.status === "completed").length;
74
+ const totalAgents = agents.length;
75
+ const totalRuns = runs.length;
76
+ const model = agents.find((a) => a.model)?.model?.split("/").at(-1);
77
+ const parts = [`⚙ ${runningAgents}r`];
78
+ if (queuedAgents > 0) parts.push(`${queuedAgents}q`);
79
+ if (completedAgents > 0) parts.push(`${completedAgents}/${totalAgents}done`);
80
+ if (totalRuns > 1) parts.push(`${totalRuns}runs`);
81
+ if (model) parts.push(model);
82
+ return parts.join(" · ");
83
+ }
84
+
85
+ /**
86
+ * Build the short run label (team/workflow).
87
+ */
88
+ export function shortRunLabel(run: TeamRunManifest): string {
89
+ return `${run.team}/${run.workflow ?? "none"}`;
90
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Widget rendering — builds and colorizes widget lines.
3
+ *
4
+ * Extracted from crew-widget.ts.
5
+ */
6
+
7
+ import type { CrewTheme } from "../theme-adapter.ts";
8
+ import { iconForStatus } from "../status-colors.ts";
9
+ import { truncate } from "../../utils/visual.ts";
10
+ import { Box, Text } from "../layout-primitives.ts";
11
+ import { listLiveAgents } from "../../runtime/live-agent-manager.ts";
12
+ import { computePhaseProgress, formatPhaseProgressLine } from "../../runtime/phase-progress.ts";
13
+ import { spinnerFrame } from "../spinner.ts";
14
+ import { agentActivity, agentStats, notificationBadge } from "./widget-formatters.ts";
15
+ import { shortRunLabel } from "./widget-model.ts";
16
+ import type { WidgetRun } from "./widget-types.ts";
17
+
18
+ const MAX_AGENTS_DISPLAY = 3;
19
+ const FINISHED_LINGER_MAX_AGE = 1;
20
+ const ERROR_LINGER_MAX_AGE = 2;
21
+ const ERROR_STATUSES = new Set(["failed", "cancelled", "stopped", "needs_attention"]);
22
+
23
+ // ── Header ────────────────────────────────────────────────────────────
24
+
25
+ export function widgetHeader(runs: WidgetRun[], runningGlyph: string, maxLines = 20, notificationCount = 0): string {
26
+ const agents = runs.flatMap((item) => item.agents);
27
+ const runningAgents = agents.filter((a) => a.status === "running").length;
28
+ const queuedAgents = agents.filter((a) => a.status === "queued").length;
29
+ const waitingAgents = agents.filter((a) => a.status === "waiting").length;
30
+ const completedAgents = agents.filter((a) => a.status === "completed").length;
31
+ const parts = [`${runningAgents} running`];
32
+ if (queuedAgents) parts.push(`${queuedAgents} queued`);
33
+ if (waitingAgents) parts.push(`${waitingAgents} waiting`);
34
+ if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
35
+ return `${runningGlyph} Crew agents${notificationBadge(notificationCount)} · ${parts.join(" · ")} · /team-dashboard`;
36
+ }
37
+
38
+ // ── Line builder ──────────────────────────────────────────────────────
39
+
40
+ export function buildWidgetLines(cwd: string, frame = 0, maxLines = 8, providedRuns?: WidgetRun[], notificationCount = 0): string[] {
41
+ const runs = providedRuns ?? [];
42
+ if (!runs.length) return [];
43
+
44
+ const runningGlyph = spinnerFrame("widget-header");
45
+ const lines: string[] = [widgetHeader(runs, runningGlyph, maxLines, notificationCount)];
46
+
47
+ for (const { run, agents, snapshot } of runs) {
48
+ const activeAgents = agents.filter((a) => a.status === "running" || a.status === "queued" || a.status === "waiting");
49
+ const now = Date.now();
50
+ const finishedAgents = agents.filter((item) => {
51
+ if (item.status === "running" || item.status === "queued" || item.status === "waiting") return false;
52
+ if (!item.completedAt) return false;
53
+ const maxAgeMs = (ERROR_STATUSES.has(item.status) ? ERROR_LINGER_MAX_AGE : FINISHED_LINGER_MAX_AGE) * 60_000;
54
+ const age = now - new Date(item.completedAt).getTime();
55
+ return Number.isFinite(age) && age < maxAgeMs;
56
+ });
57
+ const completed = agents.filter((a) => a.status === "completed").length;
58
+ const runGlyph = iconForStatus(run.status, { runningGlyph });
59
+ const phaseLine = snapshot ? formatPhaseProgressLine(computePhaseProgress(snapshot.tasks)) : "";
60
+ const progressPart = phaseLine || `${completed}/${agents.length} done`;
61
+ lines.push(`├─ ${runGlyph} ${shortRunLabel(run)} · ${progressPart} · ${run.runId.slice(-8)}`);
62
+
63
+ const liveForRun = listLiveAgents().filter((a) => a.runId === run.runId);
64
+
65
+ for (const agent of finishedAgents.slice(0, 2)) {
66
+ const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
67
+ const name = liveHandle?.agent ?? agent.agent;
68
+ const icon = agent.status === "completed" ? "✓" : agent.status === "failed" ? "✗" : agent.status === "needs_attention" ? "⚠" : "▪";
69
+ const stats = agentStats(agent, liveHandle);
70
+ const desc = liveHandle?.description ?? agent.role;
71
+ lines.push(`│ ├─ ${icon} ${name} · ${desc}${stats ? ` · ${stats}` : ""}`);
72
+ }
73
+
74
+ const visibleAgents = activeAgents.slice(0, MAX_AGENTS_DISPLAY);
75
+ for (const [index, agent] of visibleAgents.entries()) {
76
+ const last = index === visibleAgents.length - 1 && activeAgents.length <= MAX_AGENTS_DISPLAY;
77
+ const branch = last ? "└─" : "├─";
78
+ const agentGlyph = iconForStatus(agent.status, { runningGlyph });
79
+ const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
80
+ const stats = agentStats(agent, liveHandle);
81
+ const name = liveHandle?.agent ?? agent.agent;
82
+ const desc = liveHandle?.description ?? agent.role;
83
+ lines.push(`│ ${branch} ${agentGlyph} ${name}${desc ? ` · ${desc}` : ` · ${agent.role}`}`);
84
+ lines.push(`│ ⊶ ${agentActivity(agent, liveHandle)}${stats ? ` · ${stats}` : ""}`);
85
+ }
86
+
87
+ if (activeAgents.length > MAX_AGENTS_DISPLAY) {
88
+ lines.push(`│ └─ … +${activeAgents.length - MAX_AGENTS_DISPLAY} more agents`);
89
+ }
90
+
91
+ if (lines.length >= maxLines) break;
92
+ }
93
+
94
+ return lines.slice(0, maxLines);
95
+ }
96
+
97
+ // ── Colorization ──────────────────────────────────────────────────────
98
+
99
+ function statusGlyphColor(icon: string): Parameters<CrewTheme["fg"]>[0] {
100
+ const mapping: Record<string, Parameters<CrewTheme["fg"]>[0]> = {
101
+ "✓": "success",
102
+ "✗": "error",
103
+ "■": "warning",
104
+ "⏸": "warning",
105
+ "◦": "dim",
106
+ "·": "dim",
107
+ "▶": "accent",
108
+ };
109
+ return mapping[icon] ?? "accent";
110
+ }
111
+
112
+ export function colorWidgetLine(line: string, index: number, theme: CrewTheme): string {
113
+ let result = line;
114
+ if (index === 0) {
115
+ result = result.replace("Crew agents", theme.bold(theme.fg("accent", "Crew agents")));
116
+ }
117
+ result = result.replace(/[✓✗■⏸◦·▶]/g, (icon) => theme.fg(statusGlyphColor(icon), icon));
118
+ if (index === 0) {
119
+ result = theme.fg("accent", result);
120
+ }
121
+ return result;
122
+ }
123
+
124
+ export function renderLines(lines: string[], width: number): string[] {
125
+ const box = new Box(0, 0);
126
+ for (const line of lines) {
127
+ box.addChild(new Text(line));
128
+ }
129
+ return box.render(width);
130
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Widget type definitions.
3
+ */
4
+
5
+ import type { TeamRunManifest } from "../../state/types.ts";
6
+ import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
7
+ import type { ManifestCache } from "../../runtime/manifest-cache.ts";
8
+ import type { RunSnapshotCache, RunUiSnapshot } from "../snapshot-types.ts";
9
+
10
+ export interface WidgetRun {
11
+ run: TeamRunManifest;
12
+ agents: CrewAgentRecord[];
13
+ snapshot?: RunUiSnapshot;
14
+ }
15
+
16
+ export interface CrewWidgetModel {
17
+ cwd: string;
18
+ frame: number;
19
+ maxLines: number;
20
+ notificationCount?: number;
21
+ manifestCache?: ManifestCache;
22
+ snapshotCache?: RunSnapshotCache;
23
+ preloadManifests?: TeamRunManifest[];
24
+ }
25
+
26
+ export interface CrewWidgetState {
27
+ frame: number;
28
+ interval?: ReturnType<typeof setInterval>;
29
+ lastPlacement?: string;
30
+ lastVisibility?: "hidden" | "visible";
31
+ lastKey?: string;
32
+ lastMaxLines?: number;
33
+ lastCwd?: string;
34
+ legacyCleared?: boolean;
35
+ model?: CrewWidgetModel;
36
+ notificationCount?: number;
37
+ }