pi-crew 0.1.46 → 0.1.49

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 +97 -0
  2. package/agents/analyst.md +11 -11
  3. package/agents/critic.md +11 -11
  4. package/agents/executor.md +11 -11
  5. package/agents/explorer.md +11 -11
  6. package/agents/planner.md +11 -11
  7. package/agents/reviewer.md +11 -11
  8. package/agents/security-reviewer.md +11 -11
  9. package/agents/test-engineer.md +11 -11
  10. package/agents/verifier.md +11 -11
  11. package/agents/writer.md +11 -11
  12. package/docs/next-upgrade-roadmap.md +117 -42
  13. package/docs/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
  18. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
  19. package/docs/research/AUDIT_OH_MY_PI.md +261 -0
  20. package/docs/research/AUDIT_PI_CREW.md +457 -0
  21. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
  22. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
  23. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
  24. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
  25. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
  26. package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
  27. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
  28. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
  29. package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
  30. package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
  31. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
  32. package/docs/research-awesome-agent-skills-distillation.md +100 -100
  33. package/docs/research-extension-examples.md +297 -297
  34. package/docs/research-extension-system.md +324 -324
  35. package/docs/research-oh-my-pi-distillation.md +56 -9
  36. package/docs/research-optimization-plan.md +548 -548
  37. package/docs/research-phase10-distillation.md +198 -198
  38. package/docs/research-phase11-distillation.md +201 -201
  39. package/docs/research-pi-coding-agent.md +357 -357
  40. package/docs/research-source-pi-crew-reference.md +174 -174
  41. package/docs/runtime-flow.md +148 -148
  42. package/docs/source-runtime-refactor-map.md +107 -107
  43. package/index.ts +6 -6
  44. package/package.json +99 -98
  45. package/schema.json +8 -0
  46. package/skills/async-worker-recovery/SKILL.md +42 -42
  47. package/skills/context-artifact-hygiene/SKILL.md +52 -52
  48. package/skills/delegation-patterns/SKILL.md +54 -54
  49. package/skills/mailbox-interactive/SKILL.md +40 -40
  50. package/skills/model-routing-context/SKILL.md +39 -39
  51. package/skills/multi-perspective-review/SKILL.md +58 -58
  52. package/skills/observability-reliability/SKILL.md +41 -41
  53. package/skills/orchestration/SKILL.md +157 -0
  54. package/skills/ownership-session-security/SKILL.md +41 -41
  55. package/skills/pi-extension-lifecycle/SKILL.md +39 -39
  56. package/skills/requirements-to-task-packet/SKILL.md +63 -63
  57. package/skills/resource-discovery-config/SKILL.md +41 -41
  58. package/skills/runtime-state-reader/SKILL.md +44 -44
  59. package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
  60. package/skills/state-mutation-locking/SKILL.md +42 -42
  61. package/skills/systematic-debugging/SKILL.md +67 -67
  62. package/skills/ui-render-performance/SKILL.md +39 -39
  63. package/skills/verification-before-done/SKILL.md +57 -57
  64. package/skills/worktree-isolation/SKILL.md +39 -39
  65. package/src/agents/agent-config.ts +6 -0
  66. package/src/agents/agent-search.ts +98 -0
  67. package/src/agents/agent-serializer.ts +4 -0
  68. package/src/agents/discover-agents.ts +17 -4
  69. package/src/config/config.ts +24 -0
  70. package/src/config/defaults.ts +11 -0
  71. package/src/extension/autonomous-policy.ts +26 -33
  72. package/src/extension/cross-extension-rpc.ts +82 -82
  73. package/src/extension/help.ts +1 -0
  74. package/src/extension/management.ts +5 -0
  75. package/src/extension/register.ts +58 -13
  76. package/src/extension/registration/commands.ts +33 -1
  77. package/src/extension/registration/compaction-guard.ts +125 -125
  78. package/src/extension/registration/team-tool.ts +6 -4
  79. package/src/extension/run-bundle-schema.ts +89 -89
  80. package/src/extension/run-index.ts +24 -18
  81. package/src/extension/run-maintenance.ts +68 -62
  82. package/src/extension/team-tool/api.ts +23 -2
  83. package/src/extension/team-tool/cancel.ts +86 -11
  84. package/src/extension/team-tool/context.ts +3 -0
  85. package/src/extension/team-tool/handle-settings.ts +188 -188
  86. package/src/extension/team-tool/inspect.ts +41 -41
  87. package/src/extension/team-tool/intent-policy.ts +42 -0
  88. package/src/extension/team-tool/lifecycle-actions.ts +47 -18
  89. package/src/extension/team-tool/parallel-dispatch.ts +156 -0
  90. package/src/extension/team-tool/plan.ts +19 -19
  91. package/src/extension/team-tool/respond.ts +10 -2
  92. package/src/extension/team-tool/run.ts +3 -2
  93. package/src/extension/team-tool/status.ts +1 -1
  94. package/src/extension/team-tool-types.ts +1 -0
  95. package/src/extension/team-tool.ts +13 -3
  96. package/src/hooks/registry.ts +61 -0
  97. package/src/hooks/types.ts +41 -0
  98. package/src/i18n.ts +184 -184
  99. package/src/observability/exporters/otlp-exporter.ts +77 -77
  100. package/src/prompt/prompt-runtime.ts +72 -72
  101. package/src/runtime/agent-control.ts +108 -2
  102. package/src/runtime/agent-memory.ts +72 -72
  103. package/src/runtime/agent-observability.ts +114 -114
  104. package/src/runtime/async-marker.ts +26 -26
  105. package/src/runtime/async-runner.ts +3 -1
  106. package/src/runtime/attention-events.ts +28 -28
  107. package/src/runtime/background-runner.ts +19 -0
  108. package/src/runtime/cancellation-token.ts +89 -0
  109. package/src/runtime/cancellation.ts +61 -51
  110. package/src/runtime/capability-inventory.ts +116 -0
  111. package/src/runtime/child-pi.ts +2 -1
  112. package/src/runtime/code-summary.ts +247 -0
  113. package/src/runtime/completion-guard.ts +190 -190
  114. package/src/runtime/crash-recovery.ts +181 -0
  115. package/src/runtime/crew-agent-records.ts +35 -7
  116. package/src/runtime/crew-agent-runtime.ts +1 -0
  117. package/src/runtime/custom-tools/irc-tool.ts +201 -0
  118. package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
  119. package/src/runtime/delivery-coordinator.ts +3 -1
  120. package/src/runtime/direct-run.ts +35 -35
  121. package/src/runtime/effectiveness.ts +81 -76
  122. package/src/runtime/event-stream-bridge.ts +90 -0
  123. package/src/runtime/foreground-control.ts +82 -82
  124. package/src/runtime/green-contract.ts +46 -46
  125. package/src/runtime/group-join.ts +106 -106
  126. package/src/runtime/heartbeat-gradient.ts +28 -28
  127. package/src/runtime/heartbeat-watcher.ts +124 -124
  128. package/src/runtime/live-agent-control.ts +88 -88
  129. package/src/runtime/live-agent-manager.ts +78 -2
  130. package/src/runtime/live-control-realtime.ts +36 -36
  131. package/src/runtime/live-extension-bridge.ts +150 -0
  132. package/src/runtime/live-irc.ts +92 -0
  133. package/src/runtime/live-session-health.ts +100 -0
  134. package/src/runtime/live-session-runtime.ts +297 -7
  135. package/src/runtime/mcp-proxy.ts +113 -0
  136. package/src/runtime/notebook-helpers.ts +90 -0
  137. package/src/runtime/orphan-sentinel.ts +7 -0
  138. package/src/runtime/output-validator.ts +187 -0
  139. package/src/runtime/parallel-research.ts +44 -44
  140. package/src/runtime/parallel-utils.ts +57 -0
  141. package/src/runtime/parent-guard.ts +80 -0
  142. package/src/runtime/pi-json-output.ts +111 -111
  143. package/src/runtime/policy-engine.ts +79 -79
  144. package/src/runtime/progress-event-coalescer.ts +43 -43
  145. package/src/runtime/prose-compressor.ts +164 -0
  146. package/src/runtime/recovery-recipes.ts +74 -74
  147. package/src/runtime/result-extractor.ts +121 -0
  148. package/src/runtime/role-permission.ts +39 -39
  149. package/src/runtime/runtime-resolver.ts +1 -4
  150. package/src/runtime/semaphore.ts +131 -0
  151. package/src/runtime/sensitive-paths.ts +92 -0
  152. package/src/runtime/session-resources.ts +25 -25
  153. package/src/runtime/session-snapshot.ts +59 -59
  154. package/src/runtime/session-usage.ts +79 -79
  155. package/src/runtime/sidechain-output.ts +29 -29
  156. package/src/runtime/stream-preview.ts +177 -0
  157. package/src/runtime/subagent-manager.ts +3 -2
  158. package/src/runtime/subprocess-tool-registry.ts +67 -0
  159. package/src/runtime/supervisor-contact.ts +59 -59
  160. package/src/runtime/task-display.ts +38 -38
  161. package/src/runtime/task-output-context.ts +59 -9
  162. package/src/runtime/task-runner/capabilities.ts +78 -78
  163. package/src/runtime/task-runner/live-executor.ts +2 -0
  164. package/src/runtime/task-runner/progress.ts +119 -119
  165. package/src/runtime/task-runner/prompt-builder.ts +70 -8
  166. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  167. package/src/runtime/task-runner/result-utils.ts +14 -14
  168. package/src/runtime/task-runner/run-projection.ts +104 -0
  169. package/src/runtime/task-runner/state-helpers.ts +22 -22
  170. package/src/runtime/task-runner.ts +75 -4
  171. package/src/runtime/team-runner.ts +60 -8
  172. package/src/runtime/worker-heartbeat.ts +21 -21
  173. package/src/runtime/worker-startup.ts +57 -57
  174. package/src/runtime/workspace-tree.ts +298 -0
  175. package/src/runtime/yield-handler.ts +189 -0
  176. package/src/schema/config-schema.ts +6 -0
  177. package/src/schema/team-tool-schema.ts +11 -1
  178. package/src/skills/discover-skills.ts +67 -0
  179. package/src/state/active-run-registry.ts +4 -2
  180. package/src/state/artifact-store.ts +4 -1
  181. package/src/state/atomic-write.ts +50 -1
  182. package/src/state/blob-store.ts +117 -0
  183. package/src/state/contracts.ts +1 -0
  184. package/src/state/event-log-rotation.ts +158 -0
  185. package/src/state/event-log.ts +52 -2
  186. package/src/state/mailbox.ts +87 -7
  187. package/src/state/state-store.ts +24 -4
  188. package/src/state/task-claims.ts +44 -44
  189. package/src/state/types.ts +20 -0
  190. package/src/state/usage.ts +29 -29
  191. package/src/subagents/async-entry.ts +1 -1
  192. package/src/subagents/index.ts +3 -3
  193. package/src/subagents/live/control.ts +1 -1
  194. package/src/subagents/live/manager.ts +1 -1
  195. package/src/subagents/live/realtime.ts +1 -1
  196. package/src/subagents/live/session-runtime.ts +1 -1
  197. package/src/subagents/manager.ts +1 -1
  198. package/src/subagents/spawn.ts +1 -1
  199. package/src/teams/team-serializer.ts +38 -38
  200. package/src/types/diff.d.ts +18 -18
  201. package/src/ui/agent-management-overlay.ts +144 -0
  202. package/src/ui/crew-footer.ts +101 -101
  203. package/src/ui/crew-select-list.ts +111 -111
  204. package/src/ui/crew-widget.ts +11 -2
  205. package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
  206. package/src/ui/dashboard-panes/capability-pane.ts +60 -0
  207. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
  208. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  209. package/src/ui/dynamic-border.ts +25 -25
  210. package/src/ui/layout-primitives.ts +106 -106
  211. package/src/ui/live-run-sidebar.ts +4 -0
  212. package/src/ui/loaders.ts +158 -158
  213. package/src/ui/powerbar-publisher.ts +77 -15
  214. package/src/ui/render-coalescer.ts +51 -0
  215. package/src/ui/render-diff.ts +119 -119
  216. package/src/ui/render-scheduler.ts +143 -143
  217. package/src/ui/run-dashboard.ts +4 -0
  218. package/src/ui/run-event-bus.ts +209 -0
  219. package/src/ui/run-snapshot-cache.ts +68 -16
  220. package/src/ui/snapshot-types.ts +8 -0
  221. package/src/ui/spinner.ts +17 -17
  222. package/src/ui/status-colors.ts +58 -58
  223. package/src/ui/syntax-highlight.ts +116 -116
  224. package/src/ui/transcript-entries.ts +258 -0
  225. package/src/utils/atomic-write.ts +33 -33
  226. package/src/utils/completion-dedupe.ts +63 -63
  227. package/src/utils/frontmatter.ts +68 -68
  228. package/src/utils/git.ts +262 -262
  229. package/src/utils/ids.ts +17 -12
  230. package/src/utils/incremental-reader.ts +104 -0
  231. package/src/utils/names.ts +27 -27
  232. package/src/utils/redaction.ts +44 -44
  233. package/src/utils/safe-paths.ts +47 -47
  234. package/src/utils/scan-cache.ts +137 -0
  235. package/src/utils/sleep.ts +32 -32
  236. package/src/utils/sse-parser.ts +134 -0
  237. package/src/utils/task-name-generator.ts +337 -0
  238. package/src/utils/visual.ts +33 -2
  239. package/src/workflows/validate-workflow.ts +40 -40
  240. package/src/worktree/branch-freshness.ts +45 -45
  241. package/src/worktree/cleanup.ts +2 -1
  242. package/teams/default.team.md +12 -12
  243. package/teams/fast-fix.team.md +11 -11
  244. package/teams/implementation.team.md +18 -18
  245. package/teams/parallel-research.team.md +14 -14
  246. package/teams/research.team.md +11 -11
  247. package/teams/review.team.md +12 -12
  248. package/workflows/default.workflow.md +29 -29
  249. package/workflows/fast-fix.workflow.md +22 -22
  250. package/workflows/implementation.workflow.md +38 -38
  251. package/workflows/parallel-research.workflow.md +46 -46
  252. package/workflows/research.workflow.md +22 -22
  253. package/workflows/review.workflow.md +30 -30
@@ -1,119 +1,119 @@
1
- import * as Diff from "diff";
2
- import type { CrewTheme } from "./theme-adapter.ts";
3
- import { asCrewTheme } from "./theme-adapter.ts";
4
-
5
- interface ParsedDiffLine {
6
- prefix: string;
7
- lineNum: string; content: string;
8
- }
9
-
10
- interface DiffLineContent {
11
- lineNum: string;
12
- content: string;
13
- }
14
-
15
- function parseDiffLine(line: string): ParsedDiffLine | null {
16
- const match = line.match(/^([+-\s])(\s*\d*)\s(.*)$/);
17
- if (!match) return null;
18
- return { prefix: match[1], lineNum: match[2], content: match[3] };
19
- }
20
-
21
- function replaceTabs(text: string): string {
22
- return text.replace(/\t/g, " ");
23
- }
24
-
25
- function renderIntraLineDiff(theme: CrewTheme, oldContent: string, newContent: string): { removedLine: string; addedLine: string } {
26
- const wordDiff = Diff.diffWords(oldContent, newContent);
27
- let removedLine = "";
28
- let addedLine = "";
29
- let isFirstRemoved = true;
30
- let isFirstAdded = true;
31
-
32
- for (const part of wordDiff) {
33
- if (part.removed) {
34
- let value = part.value;
35
- if (isFirstRemoved) {
36
- const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
37
- value = value.slice(leadingWs.length);
38
- removedLine += leadingWs;
39
- isFirstRemoved = false;
40
- }
41
- if (value) removedLine += theme.inverse?.(value) ?? value;
42
- } else if (part.added) {
43
- let value = part.value;
44
- if (isFirstAdded) {
45
- const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
46
- value = value.slice(leadingWs.length);
47
- addedLine += leadingWs;
48
- isFirstAdded = false;
49
- }
50
- if (value) addedLine += theme.inverse?.(value) ?? value;
51
- } else {
52
- removedLine += part.value;
53
- addedLine += part.value;
54
- }
55
- }
56
-
57
- return { removedLine, addedLine };
58
- }
59
-
60
- export interface RenderDiffOptions {
61
- filePath?: string;
62
- theme?: unknown;
63
- }
64
-
65
- export function renderDiff(diffText: string, options: RenderDiffOptions = {}): string {
66
- const theme = asCrewTheme(options.theme);
67
- const lines = diffText.split("\n");
68
- const result: string[] = [];
69
- let i = 0;
70
-
71
- while (i < lines.length) {
72
- const line = lines[i] ?? "";
73
- const parsed = parseDiffLine(line);
74
- if (!parsed) {
75
- result.push(theme.fg("toolDiffContext", line));
76
- i++;
77
- continue;
78
- }
79
-
80
- if (parsed.prefix === "-") {
81
- const removedLines: DiffLineContent[] = [];
82
- while (i < lines.length) {
83
- const nextParsed = parseDiffLine(lines[i] ?? "");
84
- if (!nextParsed || nextParsed.prefix !== "-") break;
85
- removedLines.push({ lineNum: nextParsed.lineNum, content: nextParsed.content });
86
- i++;
87
- }
88
-
89
- const addedLines: DiffLineContent[] = [];
90
- while (i < lines.length) {
91
- const nextParsed = parseDiffLine(lines[i] ?? "");
92
- if (!nextParsed || nextParsed.prefix !== "+") break;
93
- addedLines.push({ lineNum: nextParsed.lineNum, content: nextParsed.content });
94
- i++;
95
- }
96
-
97
- if (removedLines.length === 1 && addedLines.length === 1) {
98
- const { removedLine, addedLine } = renderIntraLineDiff(theme, replaceTabs(removedLines[0]!.content), replaceTabs(addedLines[0]!.content));
99
- result.push(theme.fg("toolDiffRemoved", `-${removedLines[0]!.lineNum} ${removedLine}`));
100
- result.push(theme.fg("toolDiffAdded", `+${addedLines[0]!.lineNum} ${addedLine}`));
101
- } else {
102
- for (const removed of removedLines) {
103
- result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`));
104
- }
105
- for (const added of addedLines) {
106
- result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`));
107
- }
108
- }
109
- } else if (parsed.prefix === "+") {
110
- result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`));
111
- i++;
112
- } else {
113
- result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`));
114
- i++;
115
- }
116
- }
117
-
118
- return result.join("\n");
119
- }
1
+ import * as Diff from "diff";
2
+ import type { CrewTheme } from "./theme-adapter.ts";
3
+ import { asCrewTheme } from "./theme-adapter.ts";
4
+
5
+ interface ParsedDiffLine {
6
+ prefix: string;
7
+ lineNum: string; content: string;
8
+ }
9
+
10
+ interface DiffLineContent {
11
+ lineNum: string;
12
+ content: string;
13
+ }
14
+
15
+ function parseDiffLine(line: string): ParsedDiffLine | null {
16
+ const match = line.match(/^([+-\s])(\s*\d*)\s(.*)$/);
17
+ if (!match) return null;
18
+ return { prefix: match[1], lineNum: match[2], content: match[3] };
19
+ }
20
+
21
+ function replaceTabs(text: string): string {
22
+ return text.replace(/\t/g, " ");
23
+ }
24
+
25
+ function renderIntraLineDiff(theme: CrewTheme, oldContent: string, newContent: string): { removedLine: string; addedLine: string } {
26
+ const wordDiff = Diff.diffWords(oldContent, newContent);
27
+ let removedLine = "";
28
+ let addedLine = "";
29
+ let isFirstRemoved = true;
30
+ let isFirstAdded = true;
31
+
32
+ for (const part of wordDiff) {
33
+ if (part.removed) {
34
+ let value = part.value;
35
+ if (isFirstRemoved) {
36
+ const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
37
+ value = value.slice(leadingWs.length);
38
+ removedLine += leadingWs;
39
+ isFirstRemoved = false;
40
+ }
41
+ if (value) removedLine += theme.inverse?.(value) ?? value;
42
+ } else if (part.added) {
43
+ let value = part.value;
44
+ if (isFirstAdded) {
45
+ const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
46
+ value = value.slice(leadingWs.length);
47
+ addedLine += leadingWs;
48
+ isFirstAdded = false;
49
+ }
50
+ if (value) addedLine += theme.inverse?.(value) ?? value;
51
+ } else {
52
+ removedLine += part.value;
53
+ addedLine += part.value;
54
+ }
55
+ }
56
+
57
+ return { removedLine, addedLine };
58
+ }
59
+
60
+ export interface RenderDiffOptions {
61
+ filePath?: string;
62
+ theme?: unknown;
63
+ }
64
+
65
+ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): string {
66
+ const theme = asCrewTheme(options.theme);
67
+ const lines = diffText.split("\n");
68
+ const result: string[] = [];
69
+ let i = 0;
70
+
71
+ while (i < lines.length) {
72
+ const line = lines[i] ?? "";
73
+ const parsed = parseDiffLine(line);
74
+ if (!parsed) {
75
+ result.push(theme.fg("toolDiffContext", line));
76
+ i++;
77
+ continue;
78
+ }
79
+
80
+ if (parsed.prefix === "-") {
81
+ const removedLines: DiffLineContent[] = [];
82
+ while (i < lines.length) {
83
+ const nextParsed = parseDiffLine(lines[i] ?? "");
84
+ if (!nextParsed || nextParsed.prefix !== "-") break;
85
+ removedLines.push({ lineNum: nextParsed.lineNum, content: nextParsed.content });
86
+ i++;
87
+ }
88
+
89
+ const addedLines: DiffLineContent[] = [];
90
+ while (i < lines.length) {
91
+ const nextParsed = parseDiffLine(lines[i] ?? "");
92
+ if (!nextParsed || nextParsed.prefix !== "+") break;
93
+ addedLines.push({ lineNum: nextParsed.lineNum, content: nextParsed.content });
94
+ i++;
95
+ }
96
+
97
+ if (removedLines.length === 1 && addedLines.length === 1) {
98
+ const { removedLine, addedLine } = renderIntraLineDiff(theme, replaceTabs(removedLines[0]!.content), replaceTabs(addedLines[0]!.content));
99
+ result.push(theme.fg("toolDiffRemoved", `-${removedLines[0]!.lineNum} ${removedLine}`));
100
+ result.push(theme.fg("toolDiffAdded", `+${addedLines[0]!.lineNum} ${addedLine}`));
101
+ } else {
102
+ for (const removed of removedLines) {
103
+ result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`));
104
+ }
105
+ for (const added of addedLines) {
106
+ result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`));
107
+ }
108
+ }
109
+ } else if (parsed.prefix === "+") {
110
+ result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`));
111
+ i++;
112
+ } else {
113
+ result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`));
114
+ i++;
115
+ }
116
+ }
117
+
118
+ return result.join("\n");
119
+ }
@@ -1,143 +1,143 @@
1
- import { logInternalError } from "../utils/internal-error.ts";
2
-
3
- export interface RenderSchedulerEventBus {
4
- on?: (event: string, handler: (payload: unknown) => void) => (() => void) | void;
5
- }
6
-
7
- export interface RenderSchedulerOptions {
8
- debounceMs?: number;
9
- fallbackMs?: number;
10
- events?: string[];
11
- onInvalidate?: (payload: unknown) => void;
12
- }
13
-
14
- const DEFAULT_EVENTS = [
15
- "crew.run.created",
16
- "crew.run.completed",
17
- "crew.run.failed",
18
- "crew.run.cancelled",
19
- "crew.subagent.completed",
20
- "crew.subagent.failed",
21
- "crew.mailbox.updated",
22
- "crew.mailbox.message",
23
- ];
24
-
25
- /**
26
- * Coordinates UI renders with debounce + fallback polling.
27
- *
28
- * Critical: uses recursive setTimeout instead of setInterval + a rendering
29
- * guard (`rendering` / `pendingRender`) so that when render() takes longer
30
- * than the fallback interval, callbacks do NOT pile up and storm the event
31
- * loop. Instead, overlapping schedules are collapsed into a single deferred
32
- * re-render.
33
- */
34
- export class RenderScheduler {
35
- private readonly render: () => void;
36
- private readonly onInvalidate?: (payload: unknown) => void;
37
- private readonly debounceMs: number;
38
- private readonly fallbackMs: number;
39
- private debounceTimer: ReturnType<typeof setTimeout> | undefined;
40
- private fallbackTimer: ReturnType<typeof setTimeout> | undefined;
41
- private disposed = false;
42
- private lastEventAt = 0;
43
- private rendering = false;
44
- private pendingRender = false;
45
- private readonly unsubs: Array<() => void> = [];
46
-
47
- constructor(events: RenderSchedulerEventBus | undefined, render: () => void, options: RenderSchedulerOptions = {}) {
48
- this.render = render;
49
- this.onInvalidate = options.onInvalidate;
50
- this.debounceMs = options.debounceMs ?? 75;
51
- this.fallbackMs = options.fallbackMs ?? 750;
52
- for (const event of options.events ?? DEFAULT_EVENTS) this.subscribe(events, event);
53
- this.fallbackTimer = setTimeout(() => this.fallbackLoop(), this.fallbackMs);
54
- this.fallbackTimer.unref();
55
- }
56
-
57
- private subscribe(events: RenderSchedulerEventBus | undefined, event: string): void {
58
- if (!events?.on) return;
59
- const handler = (payload: unknown): void => this.schedule(payload);
60
- try {
61
- const unsub = events.on(event, handler);
62
- if (typeof unsub === "function") this.unsubs.push(unsub);
63
- } catch (error) {
64
- logInternalError("render-scheduler.subscribe", error, event);
65
- }
66
- }
67
-
68
- /** Recursive setTimeout — avoids setInterval timer storms. */
69
- private fallbackLoop(): void {
70
- if (this.disposed) return;
71
- if (Date.now() - this.lastEventAt < this.fallbackMs) {
72
- if (this.disposed) return;
73
- this.fallbackTimer = setTimeout(() => this.fallbackLoop(), this.fallbackMs);
74
- this.fallbackTimer.unref();
75
- return;
76
- }
77
- this.schedule();
78
- if (this.disposed) return;
79
- this.fallbackTimer = setTimeout(() => this.fallbackLoop(), this.fallbackMs);
80
- this.fallbackTimer.unref();
81
- }
82
-
83
- schedule(payload?: unknown): void {
84
- if (this.disposed) return;
85
- this.lastEventAt = Date.now();
86
- try {
87
- this.onInvalidate?.(payload);
88
- } catch (error) {
89
- logInternalError("render-scheduler.invalidate", error);
90
- }
91
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
92
- this.debounceTimer = setTimeout(() => {
93
- this.debounceTimer = undefined;
94
- this.flush();
95
- }, this.debounceMs);
96
- this.debounceTimer.unref();
97
- }
98
-
99
- /**
100
- * Flush a render. If a render is already in progress the request is
101
- * collapsed: `pendingRender` is set and the caller that holds
102
- * `rendering==true` will loop one more time after finishing.
103
- */
104
- flush(): void {
105
- if (this.disposed) return;
106
- if (this.rendering) {
107
- this.pendingRender = true;
108
- return;
109
- }
110
- this.rendering = true;
111
- this.pendingRender = false;
112
- let iterations = 0;
113
- try {
114
- do {
115
- this.pendingRender = false;
116
- this.render();
117
- iterations += 1;
118
- // Safety valve: 5 re-renders max per flush to prevent infinite loops
119
- // if render() itself calls flush() synchronously.
120
- } while (this.pendingRender && !this.disposed && iterations < 5);
121
- } catch (error) {
122
- logInternalError("render-scheduler.render", error);
123
- } finally {
124
- this.rendering = false;
125
- // If we hit the iteration cap, schedule one more render to drain.
126
- if (iterations >= 5 && this.pendingRender && !this.disposed) {
127
- this.schedule();
128
- }
129
- }
130
- }
131
-
132
- dispose(): void {
133
- if (this.disposed) return;
134
- this.disposed = true;
135
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
136
- if (this.fallbackTimer) clearTimeout(this.fallbackTimer);
137
- this.debounceTimer = undefined;
138
- this.fallbackTimer = undefined;
139
- for (const unsub of this.unsubs.splice(0)) {
140
- try { unsub(); } catch (error) { logInternalError("render-scheduler.unsubscribe", error); }
141
- }
142
- }
143
- }
1
+ import { logInternalError } from "../utils/internal-error.ts";
2
+
3
+ export interface RenderSchedulerEventBus {
4
+ on?: (event: string, handler: (payload: unknown) => void) => (() => void) | void;
5
+ }
6
+
7
+ export interface RenderSchedulerOptions {
8
+ debounceMs?: number;
9
+ fallbackMs?: number;
10
+ events?: string[];
11
+ onInvalidate?: (payload: unknown) => void;
12
+ }
13
+
14
+ const DEFAULT_EVENTS = [
15
+ "crew.run.created",
16
+ "crew.run.completed",
17
+ "crew.run.failed",
18
+ "crew.run.cancelled",
19
+ "crew.subagent.completed",
20
+ "crew.subagent.failed",
21
+ "crew.mailbox.updated",
22
+ "crew.mailbox.message",
23
+ ];
24
+
25
+ /**
26
+ * Coordinates UI renders with debounce + fallback polling.
27
+ *
28
+ * Critical: uses recursive setTimeout instead of setInterval + a rendering
29
+ * guard (`rendering` / `pendingRender`) so that when render() takes longer
30
+ * than the fallback interval, callbacks do NOT pile up and storm the event
31
+ * loop. Instead, overlapping schedules are collapsed into a single deferred
32
+ * re-render.
33
+ */
34
+ export class RenderScheduler {
35
+ private readonly render: () => void;
36
+ private readonly onInvalidate?: (payload: unknown) => void;
37
+ private readonly debounceMs: number;
38
+ private readonly fallbackMs: number;
39
+ private debounceTimer: ReturnType<typeof setTimeout> | undefined;
40
+ private fallbackTimer: ReturnType<typeof setTimeout> | undefined;
41
+ private disposed = false;
42
+ private lastEventAt = 0;
43
+ private rendering = false;
44
+ private pendingRender = false;
45
+ private readonly unsubs: Array<() => void> = [];
46
+
47
+ constructor(events: RenderSchedulerEventBus | undefined, render: () => void, options: RenderSchedulerOptions = {}) {
48
+ this.render = render;
49
+ this.onInvalidate = options.onInvalidate;
50
+ this.debounceMs = options.debounceMs ?? 75;
51
+ this.fallbackMs = options.fallbackMs ?? 750;
52
+ for (const event of options.events ?? DEFAULT_EVENTS) this.subscribe(events, event);
53
+ this.fallbackTimer = setTimeout(() => this.fallbackLoop(), this.fallbackMs);
54
+ this.fallbackTimer.unref();
55
+ }
56
+
57
+ private subscribe(events: RenderSchedulerEventBus | undefined, event: string): void {
58
+ if (!events?.on) return;
59
+ const handler = (payload: unknown): void => this.schedule(payload);
60
+ try {
61
+ const unsub = events.on(event, handler);
62
+ if (typeof unsub === "function") this.unsubs.push(unsub);
63
+ } catch (error) {
64
+ logInternalError("render-scheduler.subscribe", error, event);
65
+ }
66
+ }
67
+
68
+ /** Recursive setTimeout — avoids setInterval timer storms. */
69
+ private fallbackLoop(): void {
70
+ if (this.disposed) return;
71
+ if (Date.now() - this.lastEventAt < this.fallbackMs) {
72
+ if (this.disposed) return;
73
+ this.fallbackTimer = setTimeout(() => this.fallbackLoop(), this.fallbackMs);
74
+ this.fallbackTimer.unref();
75
+ return;
76
+ }
77
+ this.schedule();
78
+ if (this.disposed) return;
79
+ this.fallbackTimer = setTimeout(() => this.fallbackLoop(), this.fallbackMs);
80
+ this.fallbackTimer.unref();
81
+ }
82
+
83
+ schedule(payload?: unknown): void {
84
+ if (this.disposed) return;
85
+ this.lastEventAt = Date.now();
86
+ try {
87
+ this.onInvalidate?.(payload);
88
+ } catch (error) {
89
+ logInternalError("render-scheduler.invalidate", error);
90
+ }
91
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
92
+ this.debounceTimer = setTimeout(() => {
93
+ this.debounceTimer = undefined;
94
+ this.flush();
95
+ }, this.debounceMs);
96
+ this.debounceTimer.unref();
97
+ }
98
+
99
+ /**
100
+ * Flush a render. If a render is already in progress the request is
101
+ * collapsed: `pendingRender` is set and the caller that holds
102
+ * `rendering==true` will loop one more time after finishing.
103
+ */
104
+ flush(): void {
105
+ if (this.disposed) return;
106
+ if (this.rendering) {
107
+ this.pendingRender = true;
108
+ return;
109
+ }
110
+ this.rendering = true;
111
+ this.pendingRender = false;
112
+ let iterations = 0;
113
+ try {
114
+ do {
115
+ this.pendingRender = false;
116
+ this.render();
117
+ iterations += 1;
118
+ // Safety valve: 5 re-renders max per flush to prevent infinite loops
119
+ // if render() itself calls flush() synchronously.
120
+ } while (this.pendingRender && !this.disposed && iterations < 5);
121
+ } catch (error) {
122
+ logInternalError("render-scheduler.render", error);
123
+ } finally {
124
+ this.rendering = false;
125
+ // If we hit the iteration cap, schedule one more render to drain.
126
+ if (iterations >= 5 && this.pendingRender && !this.disposed) {
127
+ this.schedule();
128
+ }
129
+ }
130
+ }
131
+
132
+ dispose(): void {
133
+ if (this.disposed) return;
134
+ this.disposed = true;
135
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
136
+ if (this.fallbackTimer) clearTimeout(this.fallbackTimer);
137
+ this.debounceTimer = undefined;
138
+ this.fallbackTimer = undefined;
139
+ for (const unsub of this.unsubs.splice(0)) {
140
+ try { unsub(); } catch (error) { logInternalError("render-scheduler.unsubscribe", error); }
141
+ }
142
+ }
143
+ }
@@ -24,6 +24,7 @@ import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
24
24
  import { spinnerBucket, spinnerFrame } from "./spinner.ts";
25
25
  import type { MetricRegistry } from "../observability/metric-registry.ts";
26
26
  import { resolveRealContainedPath } from "../utils/safe-paths.ts";
27
+ import { runEventBus } from "./run-event-bus.ts";
27
28
 
28
29
  interface DashboardComponent {
29
30
  invalidate(): void;
@@ -253,6 +254,7 @@ export class RunDashboard implements DashboardComponent {
253
254
  private cachedVersion = "";
254
255
  private cachedLines: string[] = [];
255
256
  private readonly unsubscribeTheme: () => void;
257
+ private readonly unsubscribeEventBus: () => void;
256
258
 
257
259
  constructor(
258
260
  runs: TeamRunManifest[],
@@ -265,6 +267,7 @@ export class RunDashboard implements DashboardComponent {
265
267
  this.theme = asCrewTheme(theme);
266
268
  this.options = options;
267
269
  this.unsubscribeTheme = subscribeThemeChange(theme, () => this.invalidate());
270
+ this.unsubscribeEventBus = runEventBus.onAny(() => this.invalidate());
268
271
  }
269
272
 
270
273
  private refreshRuns(): void {
@@ -301,6 +304,7 @@ export class RunDashboard implements DashboardComponent {
301
304
 
302
305
  dispose(): void {
303
306
  this.unsubscribeTheme();
307
+ this.unsubscribeEventBus();
304
308
  }
305
309
 
306
310
  private selectedRunId(): string | undefined {