@backburner/cli 0.1.0

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 (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +329 -0
  3. package/dist/src/agents/antigravity-launch.js +162 -0
  4. package/dist/src/agents/antigravity.js +251 -0
  5. package/dist/src/agents/claude-launch.js +117 -0
  6. package/dist/src/agents/claude.js +271 -0
  7. package/dist/src/agents/codex-launch.js +82 -0
  8. package/dist/src/agents/codex.js +261 -0
  9. package/dist/src/agents/composite-runner.js +13 -0
  10. package/dist/src/agents/extraction.js +442 -0
  11. package/dist/src/agents/failure-feedback.js +132 -0
  12. package/dist/src/agents/gemini-launch.js +96 -0
  13. package/dist/src/agents/gemini.js +392 -0
  14. package/dist/src/agents/job-agent-assignments.js +180 -0
  15. package/dist/src/agents/lifecycle-feedback.js +147 -0
  16. package/dist/src/agents/limit-detector.js +83 -0
  17. package/dist/src/agents/opencode-launch.js +138 -0
  18. package/dist/src/agents/opencode.js +156 -0
  19. package/dist/src/agents/plan-breakdown-feedback.js +97 -0
  20. package/dist/src/agents/pool.js +168 -0
  21. package/dist/src/agents/prompt.js +462 -0
  22. package/dist/src/agents/provider-sessions.js +105 -0
  23. package/dist/src/agents/review-feedback.js +255 -0
  24. package/dist/src/agents/selection.js +113 -0
  25. package/dist/src/agents/service.js +1087 -0
  26. package/dist/src/agents/types.js +1 -0
  27. package/dist/src/agents/wave-assessment-feedback.js +69 -0
  28. package/dist/src/attention/derive.js +337 -0
  29. package/dist/src/capabilities/index.js +2 -0
  30. package/dist/src/capabilities/projection.js +183 -0
  31. package/dist/src/capabilities/types.js +1 -0
  32. package/dist/src/capabilities/validator.js +87 -0
  33. package/dist/src/cli/commands/broker-smoke.js +96 -0
  34. package/dist/src/cli/commands/broker.js +56 -0
  35. package/dist/src/cli/commands/init.js +4 -0
  36. package/dist/src/cli/commands/journal.js +43 -0
  37. package/dist/src/cli/commands/run-loop.js +56 -0
  38. package/dist/src/cli/commands/run.js +1241 -0
  39. package/dist/src/cli/commands/tui.js +187 -0
  40. package/dist/src/cli/dispatcher.js +51 -0
  41. package/dist/src/cli/errors.js +2 -0
  42. package/dist/src/cli/init.js +92 -0
  43. package/dist/src/cli/options.js +86 -0
  44. package/dist/src/cli/output.js +231 -0
  45. package/dist/src/cli/run-journal.js +342 -0
  46. package/dist/src/cli/run-types.js +1 -0
  47. package/dist/src/cli/run.js +32 -0
  48. package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
  49. package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
  50. package/dist/src/cli/runtime/provider-runtime.js +152 -0
  51. package/dist/src/cli/runtime/provider-tools.js +73 -0
  52. package/dist/src/cli/runtime/providers/antigravity.js +26 -0
  53. package/dist/src/cli/runtime/providers/claude.js +20 -0
  54. package/dist/src/cli/runtime/providers/codex.js +20 -0
  55. package/dist/src/cli/runtime/providers/gemini.js +20 -0
  56. package/dist/src/cli/runtime/providers/opencode.js +20 -0
  57. package/dist/src/cli/runtime/run-context.js +131 -0
  58. package/dist/src/cli/tui/index.js +3 -0
  59. package/dist/src/cli/tui/screen.js +388 -0
  60. package/dist/src/cli/tui/tracker.js +558 -0
  61. package/dist/src/cli/tui/types.js +1 -0
  62. package/dist/src/cli/tui/views.js +1006 -0
  63. package/dist/src/config/loader.js +136 -0
  64. package/dist/src/config/schemas.js +403 -0
  65. package/dist/src/config/types.js +1 -0
  66. package/dist/src/context/types.js +331 -0
  67. package/dist/src/domain/entities.js +1 -0
  68. package/dist/src/git/backburner-git-tool-service.js +680 -0
  69. package/dist/src/git/gitops-lifecycle.js +56 -0
  70. package/dist/src/git/merge-prep-workspace.js +357 -0
  71. package/dist/src/git/types.js +484 -0
  72. package/dist/src/github/broker-model-content.js +215 -0
  73. package/dist/src/github/broker-server.js +237 -0
  74. package/dist/src/github/broker.js +638 -0
  75. package/dist/src/github/composed.js +53 -0
  76. package/dist/src/github/gateway.js +478 -0
  77. package/dist/src/github/normalize.js +222 -0
  78. package/dist/src/github/pr-classification.js +73 -0
  79. package/dist/src/github/scoped-broker.js +35 -0
  80. package/dist/src/github/security.js +126 -0
  81. package/dist/src/github/service.js +332 -0
  82. package/dist/src/github/types.js +1 -0
  83. package/dist/src/github/utils.js +16 -0
  84. package/dist/src/journal/index.js +4 -0
  85. package/dist/src/journal/reader.js +62 -0
  86. package/dist/src/journal/renderer.js +50 -0
  87. package/dist/src/journal/service.js +72 -0
  88. package/dist/src/journal/types.js +1 -0
  89. package/dist/src/journal/writer.js +19 -0
  90. package/dist/src/mcps/bridge.js +166 -0
  91. package/dist/src/mcps/index.js +5 -0
  92. package/dist/src/mcps/reconcile.js +113 -0
  93. package/dist/src/mcps/smithery-client.js +186 -0
  94. package/dist/src/mcps/smithery.js +164 -0
  95. package/dist/src/mcps/types.js +1 -0
  96. package/dist/src/memory/json-store.js +71 -0
  97. package/dist/src/memory/prompt.js +16 -0
  98. package/dist/src/memory/scope.js +31 -0
  99. package/dist/src/memory/store.js +1 -0
  100. package/dist/src/memory/tool-service.js +142 -0
  101. package/dist/src/memory/types.js +12 -0
  102. package/dist/src/onboarding/config-writer.js +96 -0
  103. package/dist/src/onboarding/engine.js +86 -0
  104. package/dist/src/onboarding/prompt.js +102 -0
  105. package/dist/src/onboarding/services/provider-discovery.js +58 -0
  106. package/dist/src/onboarding/services/repo-discovery.js +246 -0
  107. package/dist/src/onboarding/steps/check-github-auth.js +32 -0
  108. package/dist/src/onboarding/steps/check-requirements.js +34 -0
  109. package/dist/src/onboarding/steps/configure-models.js +105 -0
  110. package/dist/src/onboarding/steps/configure-paths.js +33 -0
  111. package/dist/src/onboarding/steps/discover-providers.js +38 -0
  112. package/dist/src/onboarding/steps/discover-repos.js +39 -0
  113. package/dist/src/onboarding/steps/generate-config.js +106 -0
  114. package/dist/src/onboarding/steps/readiness-validation.js +158 -0
  115. package/dist/src/onboarding/steps/select-repos.js +76 -0
  116. package/dist/src/onboarding/steps/show-summary.js +97 -0
  117. package/dist/src/onboarding/steps/welcome.js +27 -0
  118. package/dist/src/onboarding/types.js +1 -0
  119. package/dist/src/onboarding/validators/git-access.js +63 -0
  120. package/dist/src/onboarding/validators/github-auth.js +85 -0
  121. package/dist/src/onboarding/validators/tool-checker.js +153 -0
  122. package/dist/src/onboarding/validators/workspace.js +83 -0
  123. package/dist/src/prompts/resolver.js +106 -0
  124. package/dist/src/retrospectives/candidates.js +224 -0
  125. package/dist/src/retrospectives/derive.js +169 -0
  126. package/dist/src/retrospectives/ingest.js +321 -0
  127. package/dist/src/retrospectives/management-prs.js +380 -0
  128. package/dist/src/retrospectives/proposals.js +199 -0
  129. package/dist/src/retrospectives/scopes.js +272 -0
  130. package/dist/src/retrospectives/scoring.js +171 -0
  131. package/dist/src/retrospectives/types.js +1 -0
  132. package/dist/src/state/loader.js +473 -0
  133. package/dist/src/state/types.js +15 -0
  134. package/dist/src/tasks/derivation/builders.js +70 -0
  135. package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
  136. package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
  137. package/dist/src/tasks/derivation/handlers/index.js +54 -0
  138. package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
  139. package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
  140. package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
  141. package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
  142. package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
  143. package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
  144. package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
  145. package/dist/src/tasks/derivation/handlers/shared.js +24 -0
  146. package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
  147. package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
  148. package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
  149. package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
  150. package/dist/src/tasks/derivation/runner.js +18 -0
  151. package/dist/src/tasks/derivation/types.js +1 -0
  152. package/dist/src/tasks/derive.js +810 -0
  153. package/dist/src/tasks/mcp-skip.js +10 -0
  154. package/dist/src/tasks/retry.js +10 -0
  155. package/dist/src/tasks/task.js +184 -0
  156. package/dist/src/tasks/types.js +1 -0
  157. package/dist/src/utils/command-runner.js +192 -0
  158. package/dist/src/utils/json.js +38 -0
  159. package/dist/src/utils/logger.js +43 -0
  160. package/dist/src/utils/paths.js +38 -0
  161. package/dist/src/utils/slug.js +10 -0
  162. package/dist/src/workflows/registry.js +174 -0
  163. package/dist/src/workflows/triage.js +90 -0
  164. package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
  165. package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
  166. package/dist/src/workflows/workstream-machine/events.js +1 -0
  167. package/dist/src/workflows/workstream-machine/machine.js +355 -0
  168. package/dist/src/workflows/workstream-machine/selectors.js +19 -0
  169. package/dist/src/workflows/workstream-machine/types.js +1 -0
  170. package/dist/src/workstreams/branch-sync.js +87 -0
  171. package/dist/src/workstreams/derive.js +629 -0
  172. package/dist/src/workstreams/mcp-blockers.js +420 -0
  173. package/dist/src/workstreams/packet-candidates.js +42 -0
  174. package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
  175. package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
  176. package/dist/src/workstreams/packet-plan.js +597 -0
  177. package/dist/src/workstreams/packet-projections.js +190 -0
  178. package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
  179. package/dist/src/workstreams/parent-branch-sync.js +148 -0
  180. package/dist/src/workstreams/phases.js +4 -0
  181. package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
  182. package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
  183. package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
  184. package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
  185. package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
  186. package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
  187. package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
  188. package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
  189. package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
  190. package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
  191. package/dist/src/workstreams/utils.js +15 -0
  192. package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
  193. package/dist/src/workstreams/wave-assessment/index.js +3 -0
  194. package/dist/src/workstreams/wave-assessment/session.js +84 -0
  195. package/dist/src/workstreams/wave-assessment/store.js +51 -0
  196. package/dist/src/workstreams/wave-assessment/tools.js +419 -0
  197. package/dist/src/workstreams/wave-assessment/types.js +1 -0
  198. package/dist/src/workstreams/wave-assessment/validate.js +78 -0
  199. package/dist/src/worktrees/service.js +839 -0
  200. package/dist/src/worktrees/workspace-ownership.js +34 -0
  201. package/dist/src/worktrees/workspace-recovery.js +177 -0
  202. package/package.json +61 -0
@@ -0,0 +1,1006 @@
1
+ import { SENSOR_TOOL_MAP, CONTROLLER_TOOL_MAP } from "../../capabilities/projection.js";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ // ANSI escape code helper functions
5
+ export const reset = "\x1b[0m";
6
+ export const bold = (t) => `\x1b[1m${t}\x1b[22m`;
7
+ export const dim = (t) => `\x1b[2m${t}\x1b[22m`;
8
+ export const green = (t) => `\x1b[32m${t}${reset}`;
9
+ export const yellow = (t) => `\x1b[33m${t}${reset}`;
10
+ export const red = (t) => `\x1b[31m${t}${reset}`;
11
+ export const cyan = (t) => `\x1b[36m${t}${reset}`;
12
+ export const blue = (t) => `\x1b[34m${t}${reset}`;
13
+ export const magenta = (t) => `\x1b[35m${t}${reset}`;
14
+ export const inverse = (t) => `\x1b[7m${t}${reset}`;
15
+ // Helper to strip ANSI codes safely for width calculations
16
+ export function stripAnsi(str) {
17
+ // eslint-disable-next-line no-control-regex
18
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
19
+ }
20
+ export function formatAgent(agent) {
21
+ if (!agent) {
22
+ return dim("⚙️ ORCHESTRATOR");
23
+ }
24
+ const provider = agent.provider;
25
+ switch (provider) {
26
+ case "antigravity":
27
+ return magenta("✦ ANTIGRAVITY");
28
+ case "claude":
29
+ return yellow("🦺 CLAUDE");
30
+ case "codex":
31
+ return green("🌀 CODEX");
32
+ case "gemini":
33
+ return blue("◆ GEMINI");
34
+ case "opencode":
35
+ return cyan("🔓 OPENCODE");
36
+ default:
37
+ return dim(`🤖 ${String(provider).toUpperCase()}`);
38
+ }
39
+ }
40
+ // ANSI-safe truncation helper
41
+ export function truncateAnsi(str, limit) {
42
+ let plainLen = 0;
43
+ let result = "";
44
+ let inEscape = false;
45
+ const chars = Array.from(str);
46
+ for (let i = 0; i < chars.length; i++) {
47
+ const char = chars[i] || "";
48
+ if (char === "\x1b") {
49
+ inEscape = true;
50
+ result += char;
51
+ continue;
52
+ }
53
+ if (inEscape) {
54
+ result += char;
55
+ if (/[a-zA-Z]/.test(char)) {
56
+ inEscape = false;
57
+ }
58
+ continue;
59
+ }
60
+ if (plainLen < limit) {
61
+ result += char;
62
+ plainLen++;
63
+ }
64
+ else {
65
+ break;
66
+ }
67
+ }
68
+ result += reset;
69
+ return result;
70
+ }
71
+ // ANSI-safe pad and truncate helper
72
+ export function padOrTruncateAnsi(str, limit, align = "left") {
73
+ const plainLen = Array.from(stripAnsi(str)).length;
74
+ if (plainLen > limit) {
75
+ return truncateAnsi(str, limit);
76
+ }
77
+ const pad = " ".repeat(limit - plainLen);
78
+ if (align === "left") {
79
+ return str + pad;
80
+ }
81
+ else {
82
+ return pad + str;
83
+ }
84
+ }
85
+ // Formatting helpers
86
+ export function formatStatus(s) {
87
+ switch (s) {
88
+ case "executing": return green(s.toUpperCase());
89
+ case "idle": return dim(s.toUpperCase());
90
+ case "syncing": return cyan(s.toUpperCase());
91
+ case "waiting": return yellow("AWAITING APPROVAL");
92
+ case "blocked": return red(s.toUpperCase());
93
+ case "failed": return red(s.toUpperCase());
94
+ default: return s.toUpperCase();
95
+ }
96
+ }
97
+ export function getStatusColorSymbol(s) {
98
+ switch (s) {
99
+ case "executing":
100
+ return green("●");
101
+ case "idle":
102
+ return dim("●");
103
+ case "syncing":
104
+ return cyan("●");
105
+ case "waiting":
106
+ return yellow("●");
107
+ case "blocked":
108
+ case "failed":
109
+ return red("●");
110
+ default:
111
+ return "●";
112
+ }
113
+ }
114
+ export function formatReadiness(r) {
115
+ switch (r) {
116
+ case "ok": return green(r);
117
+ case "warnings": return yellow(r);
118
+ case "blocking problems": return red(r);
119
+ default: return r;
120
+ }
121
+ }
122
+ export function formatUptime(s) {
123
+ const hrs = Math.floor(s / 3600);
124
+ const mins = Math.floor((s % 3600) / 60);
125
+ const secs = s % 60;
126
+ return [hrs, mins, secs].map(n => String(n).padStart(2, "0")).join(":");
127
+ }
128
+ function sectionHeader(title, width) {
129
+ const prefix = `── ${title} `;
130
+ const line = prefix + "─".repeat(Math.max(0, width - stripAnsi(prefix).length));
131
+ return bold(cyan(line));
132
+ }
133
+ const LOGO = [
134
+ "╔════════════════════╗",
135
+ "║ BACKBURNER ║",
136
+ "║ local GitHub ops ║",
137
+ "║ agents on deck ║",
138
+ "╚════════════════════╝"
139
+ ];
140
+ const SPIN_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
141
+ function getAnimatedLogoLine(lineIndex, status, spinnerChar) {
142
+ const line = LOGO[lineIndex] || "";
143
+ if (status === "syncing" || status === "executing") {
144
+ const colors = [cyan, blue, magenta, green, yellow];
145
+ const spinIndex = spinnerChar ? SPIN_CHARS.indexOf(spinnerChar) : 0;
146
+ const colorFn = colors[(lineIndex + (spinIndex >= 0 ? spinIndex : 0)) % colors.length] || cyan;
147
+ return bold(colorFn(line));
148
+ }
149
+ else if (status === "blocked" || status === "failed") {
150
+ return bold(red(line));
151
+ }
152
+ else {
153
+ return bold(cyan(line));
154
+ }
155
+ }
156
+ export class StatusView {
157
+ navStack = [];
158
+ reposSelectedIndex = 0;
159
+ reposScrollOffset = 0;
160
+ tracker;
161
+ explorerMaxHeight = 5;
162
+ constructor(tracker) {
163
+ this.tracker = tracker;
164
+ }
165
+ handleKey(key, str, triggerRender) {
166
+ if (!this.tracker)
167
+ return false;
168
+ const k = (key?.name || str || "").toLowerCase();
169
+ // 1. If navStack is empty, we are at "repos" level
170
+ if (this.navStack.length === 0) {
171
+ if (k === "up" || k === "k") {
172
+ this.moveSelection(-1, triggerRender);
173
+ return true;
174
+ }
175
+ if (k === "down" || k === "j") {
176
+ this.moveSelection(1, triggerRender);
177
+ return true;
178
+ }
179
+ if (key?.name === "return") {
180
+ this.drillDown(triggerRender);
181
+ return true;
182
+ }
183
+ return false;
184
+ }
185
+ // 2. If stack is not empty, get the top entry
186
+ const top = this.navStack[this.navStack.length - 1];
187
+ if (k === "up" || k === "k") {
188
+ if (top.type === "state_view") {
189
+ top.scrollOffset = Math.max(0, top.scrollOffset - 1);
190
+ triggerRender();
191
+ }
192
+ else if (top.type === "file_view") {
193
+ if (!top.loading) {
194
+ top.scrollOffset = Math.max(0, top.scrollOffset - 1);
195
+ triggerRender();
196
+ }
197
+ }
198
+ else {
199
+ this.moveSelection(-1, triggerRender);
200
+ }
201
+ return true;
202
+ }
203
+ if (k === "down" || k === "j") {
204
+ if (top.type === "state_view") {
205
+ const linesCount = top.linesCount ?? 0;
206
+ top.scrollOffset = Math.min(Math.max(0, linesCount - this.explorerMaxHeight), top.scrollOffset + 1);
207
+ triggerRender();
208
+ }
209
+ else if (top.type === "file_view") {
210
+ if (!top.loading) {
211
+ const linesCount = top.linesCount ?? 0;
212
+ top.scrollOffset = Math.min(Math.max(0, linesCount - this.explorerMaxHeight), top.scrollOffset + 1);
213
+ triggerRender();
214
+ }
215
+ }
216
+ else {
217
+ this.moveSelection(1, triggerRender);
218
+ }
219
+ return true;
220
+ }
221
+ if (key?.name === "return") {
222
+ this.drillDown(triggerRender);
223
+ return true;
224
+ }
225
+ // Filter input for workstreams level
226
+ if (top.type === "workstreams") {
227
+ if (key?.name === "backspace" && top.filter.length > 0) {
228
+ top.filter = top.filter.slice(0, -1);
229
+ top.selectedIndex = 0;
230
+ top.scrollOffset = 0;
231
+ triggerRender();
232
+ return true;
233
+ }
234
+ if (key?.name === "escape" && top.filter.length > 0) {
235
+ top.filter = "";
236
+ top.selectedIndex = 0;
237
+ top.scrollOffset = 0;
238
+ triggerRender();
239
+ return true;
240
+ }
241
+ if (str && str.length === 1 && !key?.ctrl && !key?.meta && str >= " " && str !== "\x7f") {
242
+ top.filter += str;
243
+ top.selectedIndex = 0;
244
+ top.scrollOffset = 0;
245
+ triggerRender();
246
+ return true;
247
+ }
248
+ }
249
+ if (key?.name === "escape" || key?.name === "backspace") {
250
+ this.navStack.pop();
251
+ triggerRender();
252
+ return true;
253
+ }
254
+ return false;
255
+ }
256
+ getRepoKeys(tracker) {
257
+ return tracker.getUniqueRepoKeys();
258
+ }
259
+ getFilteredWorkstreams(top) {
260
+ const all = this.tracker.getWorkstreamsForRepo(top.repoKey);
261
+ if (!top.filter)
262
+ return all;
263
+ const q = top.filter.toLowerCase();
264
+ return all.filter(ws => {
265
+ if (String(ws.origin.number).includes(q))
266
+ return true;
267
+ return (this.tracker.getWorkstreamTitle(ws) || "").toLowerCase().includes(q);
268
+ });
269
+ }
270
+ getItemsCount(top) {
271
+ if (top.type === "workstreams") {
272
+ return this.getFilteredWorkstreams(top).length;
273
+ }
274
+ if (top.type === "workstream_menu") {
275
+ return 2; // "State", "Executions"
276
+ }
277
+ if (top.type === "executions_list") {
278
+ return this.tracker.getExecutionsForWorkstream(top.workstream.id).length;
279
+ }
280
+ if (top.type === "execution_files") {
281
+ return top.files.length;
282
+ }
283
+ return 0;
284
+ }
285
+ moveSelection(delta, triggerRender) {
286
+ if (this.navStack.length === 0) {
287
+ const repos = this.getRepoKeys(this.tracker);
288
+ if (repos.length === 0)
289
+ return;
290
+ this.reposSelectedIndex = Math.max(0, Math.min(repos.length - 1, this.reposSelectedIndex + delta));
291
+ triggerRender();
292
+ return;
293
+ }
294
+ const top = this.navStack[this.navStack.length - 1];
295
+ if (top.type === "workstreams" ||
296
+ top.type === "workstream_menu" ||
297
+ top.type === "executions_list" ||
298
+ top.type === "execution_files") {
299
+ const itemsCount = this.getItemsCount(top);
300
+ if (itemsCount === 0)
301
+ return;
302
+ top.selectedIndex = Math.max(0, Math.min(itemsCount - 1, top.selectedIndex + delta));
303
+ triggerRender();
304
+ }
305
+ }
306
+ drillDown(triggerRender) {
307
+ if (this.navStack.length === 0) {
308
+ const repos = this.getRepoKeys(this.tracker);
309
+ if (repos.length === 0)
310
+ return;
311
+ const repoKey = repos[this.reposSelectedIndex];
312
+ if (!repoKey)
313
+ return;
314
+ this.navStack.push({
315
+ type: "workstreams",
316
+ repoKey,
317
+ selectedIndex: 0,
318
+ scrollOffset: 0,
319
+ filter: ""
320
+ });
321
+ triggerRender();
322
+ return;
323
+ }
324
+ const top = this.navStack[this.navStack.length - 1];
325
+ if (top.type === "workstreams") {
326
+ const workstreams = this.getFilteredWorkstreams(top);
327
+ if (workstreams.length === 0)
328
+ return;
329
+ const workstream = workstreams[top.selectedIndex];
330
+ if (!workstream)
331
+ return;
332
+ this.navStack.push({
333
+ type: "workstream_menu",
334
+ repoKey: top.repoKey,
335
+ workstream,
336
+ selectedIndex: 0
337
+ });
338
+ triggerRender();
339
+ return;
340
+ }
341
+ if (top.type === "workstream_menu") {
342
+ if (top.selectedIndex === 0) {
343
+ const stateStr = JSON.stringify(top.workstream, null, 2);
344
+ this.navStack.push({
345
+ type: "state_view",
346
+ repoKey: top.repoKey,
347
+ workstream: top.workstream,
348
+ scrollOffset: 0,
349
+ linesCount: stateStr.split("\n").length
350
+ });
351
+ }
352
+ else {
353
+ this.navStack.push({
354
+ type: "executions_list",
355
+ repoKey: top.repoKey,
356
+ workstream: top.workstream,
357
+ selectedIndex: 0,
358
+ scrollOffset: 0
359
+ });
360
+ }
361
+ triggerRender();
362
+ return;
363
+ }
364
+ if (top.type === "executions_list") {
365
+ const executions = this.tracker.getExecutionsForWorkstream(top.workstream.id);
366
+ if (executions.length === 0)
367
+ return;
368
+ const execution = executions[top.selectedIndex];
369
+ if (!execution)
370
+ return;
371
+ const fileEntry = {
372
+ type: "execution_files",
373
+ repoKey: top.repoKey,
374
+ workstream: top.workstream,
375
+ execution,
376
+ selectedIndex: 0,
377
+ scrollOffset: 0,
378
+ files: [],
379
+ loading: true
380
+ };
381
+ this.navStack.push(fileEntry);
382
+ triggerRender();
383
+ const artifactDir = resolveExecutionArtifactDir(execution);
384
+ if (!artifactDir) {
385
+ fileEntry.files = ["No artifact directory recorded for this execution."];
386
+ fileEntry.loading = false;
387
+ triggerRender();
388
+ return;
389
+ }
390
+ fs.readdir(artifactDir)
391
+ .then(files => {
392
+ if (!this.navStack.includes(fileEntry))
393
+ return;
394
+ fileEntry.files = files.filter(f => !f.startsWith("."));
395
+ fileEntry.loading = false;
396
+ triggerRender();
397
+ })
398
+ .catch(err => {
399
+ if (!this.navStack.includes(fileEntry))
400
+ return;
401
+ fileEntry.files = [`Error listing directory: ${err.message}`];
402
+ fileEntry.loading = false;
403
+ triggerRender();
404
+ });
405
+ return;
406
+ }
407
+ if (top.type === "execution_files") {
408
+ if (top.loading || top.files.length === 0)
409
+ return;
410
+ const fileName = top.files[top.selectedIndex];
411
+ if (!fileName || fileName.startsWith("Error listing directory"))
412
+ return;
413
+ const fileViewEntry = {
414
+ type: "file_view",
415
+ repoKey: top.repoKey,
416
+ workstream: top.workstream,
417
+ execution: top.execution,
418
+ fileName,
419
+ content: "",
420
+ scrollOffset: 0,
421
+ loading: true
422
+ };
423
+ this.navStack.push(fileViewEntry);
424
+ triggerRender();
425
+ const artifactDir = resolveExecutionArtifactDir(top.execution);
426
+ if (!artifactDir) {
427
+ const errMsg = "No artifact directory recorded for this execution.";
428
+ fileViewEntry.content = errMsg;
429
+ fileViewEntry.linesCount = 1;
430
+ fileViewEntry.loading = false;
431
+ triggerRender();
432
+ return;
433
+ }
434
+ const filePath = path.join(artifactDir, fileName);
435
+ fs.readFile(filePath, "utf8")
436
+ .then(content => {
437
+ if (!this.navStack.includes(fileViewEntry))
438
+ return;
439
+ fileViewEntry.content = content;
440
+ fileViewEntry.linesCount = content.split("\n").length;
441
+ fileViewEntry.loading = false;
442
+ triggerRender();
443
+ })
444
+ .catch(err => {
445
+ if (!this.navStack.includes(fileViewEntry))
446
+ return;
447
+ const errMsg = `Error reading file: ${err.message}`;
448
+ fileViewEntry.content = errMsg;
449
+ fileViewEntry.linesCount = errMsg.split("\n").length;
450
+ fileViewEntry.loading = false;
451
+ triggerRender();
452
+ });
453
+ return;
454
+ }
455
+ }
456
+ getBreadcrumbs() {
457
+ const parts = ["Repos"];
458
+ for (const entry of this.navStack) {
459
+ if (entry.type === "workstreams") {
460
+ parts.push(entry.repoKey);
461
+ }
462
+ else if (entry.type === "workstream_menu") {
463
+ const origin = entry.workstream.origin;
464
+ parts.push(`${origin.type === "issue" ? "Issue" : "PR"} #${origin.number}`);
465
+ }
466
+ else if (entry.type === "state_view") {
467
+ parts.push("State");
468
+ }
469
+ else if (entry.type === "executions_list") {
470
+ parts.push("Executions");
471
+ }
472
+ else if (entry.type === "execution_files") {
473
+ parts.push(entry.execution.id.slice(-8));
474
+ }
475
+ else if (entry.type === "file_view") {
476
+ parts.push(entry.fileName);
477
+ }
478
+ }
479
+ return parts.join(" › ");
480
+ }
481
+ render(model, tracker, width, height, spinnerChar) {
482
+ this.tracker = tracker;
483
+ const lines = [];
484
+ // Header layout
485
+ const showLogo = width >= 85;
486
+ const leftWidth = showLogo ? width - 26 : width;
487
+ const formatRowStr = (k1, v1, k2, v2) => {
488
+ const col1W = Math.floor(leftWidth * 0.5);
489
+ const col2W = leftWidth - col1W;
490
+ const k1F = bold(dim(padOrTruncateAnsi(k1, 11)));
491
+ const v1F = padOrTruncateAnsi(v1, Math.max(0, col1W - 11));
492
+ const k2F = bold(dim(padOrTruncateAnsi(k2, 11)));
493
+ const v2F = padOrTruncateAnsi(v2, Math.max(0, col2W - 11));
494
+ return `${k1F}${v1F}${k2F}${v2F}`;
495
+ };
496
+ const statusStr = `${getStatusColorSymbol(model.status)} ${formatStatus(model.status)}`;
497
+ const readinessStr = formatReadiness(model.readiness);
498
+ const reposStr = model.repos === "—" ? "—" : `${model.repos} enabled`;
499
+ const cycleStr = model.cycleNo === "—" ? "—" : `#${model.cycleNo}`;
500
+ const narrow = width < 60;
501
+ if (narrow) {
502
+ const formatSingleRow = (k, v) => {
503
+ const kF = bold(dim(padOrTruncateAnsi(k, 11)));
504
+ const vF = padOrTruncateAnsi(v, Math.max(0, leftWidth - 11));
505
+ return `${kF}${vF}`;
506
+ };
507
+ const narrowRows = [
508
+ formatSingleRow("STATUS", statusStr),
509
+ formatSingleRow("UPTIME", formatUptime(model.uptime)),
510
+ formatSingleRow("READINESS", readinessStr),
511
+ formatSingleRow("CYCLE", cycleStr),
512
+ formatSingleRow("REPOS", reposStr),
513
+ formatSingleRow("LAST OK", model.lastOk),
514
+ formatSingleRow("NEXT", model.nextText)
515
+ ];
516
+ lines.push(...narrowRows);
517
+ }
518
+ else {
519
+ const rawRows = [
520
+ formatRowStr("STATUS", statusStr, "UPTIME", formatUptime(model.uptime)),
521
+ formatRowStr("READINESS", readinessStr, "CYCLE", cycleStr),
522
+ formatRowStr("REPOS", reposStr, "LAST OK", model.lastOk),
523
+ formatRowStr("NEXT", model.nextText, "", "")
524
+ ];
525
+ // Combine with Logo
526
+ for (let i = 0; i < 5; i++) {
527
+ let leftPart = rawRows[i] || " ".repeat(leftWidth);
528
+ if (showLogo) {
529
+ const logoPart = getAnimatedLogoLine(i, model.status, spinnerChar);
530
+ lines.push(`${leftPart} ${logoPart}`);
531
+ }
532
+ else {
533
+ lines.push(leftPart);
534
+ }
535
+ }
536
+ }
537
+ lines.push("");
538
+ // Section 1: CURRENT OPERATION
539
+ lines.push(sectionHeader("CURRENT OPERATION", width));
540
+ let opGlyph = model.status === "syncing" || model.status === "executing" ? (spinnerChar || "●") : "○";
541
+ let opGlyphAnsi = model.status === "syncing" ? cyan(opGlyph) : model.status === "executing" ? green(opGlyph) : dim(opGlyph);
542
+ lines.push(`${opGlyphAnsi} ${bold(model.opText)}`);
543
+ if (model.opTarget) {
544
+ lines.push(` Target: ${model.opTarget}`);
545
+ }
546
+ if (model.opSub) {
547
+ lines.push(` Detail: ${dim(model.opSub)}`);
548
+ }
549
+ // List running tasks (separated by Orchestrator vs Agent)
550
+ const running = tracker.getRunningTasks();
551
+ if (running.size > 0) {
552
+ const orchestratorTasks = [];
553
+ const agentTasks = [];
554
+ for (const { task, agent, startTime } of running.values()) {
555
+ if (!agent) {
556
+ orchestratorTasks.push({ task, agent, startTime });
557
+ }
558
+ else {
559
+ agentTasks.push({ task, agent, startTime });
560
+ }
561
+ }
562
+ if (orchestratorTasks.length > 0) {
563
+ lines.push("");
564
+ lines.push(bold(" Active Orchestrator Tasks:"));
565
+ for (const { task, agent, startTime } of orchestratorTasks) {
566
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
567
+ const elapsedStr = `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
568
+ const agentDisplay = formatAgent(agent);
569
+ const taskKind = tracker.getTaskKindText(task.kind);
570
+ lines.push(` - ${agentDisplay} runs ${dim(taskKind)} [elapsed ${cyan(elapsedStr)}]`);
571
+ }
572
+ }
573
+ if (agentTasks.length > 0) {
574
+ lines.push("");
575
+ lines.push(bold(" Active Agent Tasks:"));
576
+ for (const { task, agent, startTime } of agentTasks) {
577
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
578
+ const elapsedStr = `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
579
+ const agentDisplay = formatAgent(agent);
580
+ const taskKind = tracker.getTaskKindText(task.kind);
581
+ lines.push(` - ${agentDisplay} runs ${green(taskKind)} [elapsed ${cyan(elapsedStr)}]`);
582
+ }
583
+ }
584
+ }
585
+ lines.push("");
586
+ // Render Latest Cycle outcome and Hints if any
587
+ let cycleGlyphColor = model.cycleGlyph === "✓" ? green : model.cycleGlyph === "✗" ? red : model.cycleGlyph === "⚠" ? yellow : dim;
588
+ lines.push(` Latest Cycle: ${cycleGlyphColor(model.cycleGlyph)} ${bold(model.cycleText)} ${dim(`(${model.cycleMeta})`)}`);
589
+ if (model.hints && model.hints.length > 0) {
590
+ for (const hint of model.hints) {
591
+ let hintGlyphAnsi = dim(hint.glyph);
592
+ if (hint.color === "yellow")
593
+ hintGlyphAnsi = yellow(hint.glyph);
594
+ else if (hint.color === "cyan")
595
+ hintGlyphAnsi = cyan(hint.glyph);
596
+ else if (hint.color === "red")
597
+ hintGlyphAnsi = red(hint.glyph);
598
+ let hintTextAnsi = hint.text;
599
+ if (hint.color === "dim")
600
+ hintTextAnsi = dim(hint.text);
601
+ let prefix = " Status: ";
602
+ if (hint.color === "yellow" || hint.color === "red") {
603
+ prefix = " Investigate: ";
604
+ }
605
+ else if (hint.color === "cyan") {
606
+ prefix = " Info: ";
607
+ }
608
+ lines.push(`${prefix}${hintGlyphAnsi} ${hintTextAnsi}`);
609
+ }
610
+ }
611
+ lines.push("");
612
+ // Section 2: EXECUTIONS & STATE EXPLORER
613
+ lines.push(sectionHeader("EXECUTIONS & STATE EXPLORER", width));
614
+ const crumbs = this.getBreadcrumbs();
615
+ lines.push(bold(cyan(" " + crumbs)));
616
+ lines.push("");
617
+ const explorerStartLineCount = lines.length;
618
+ this.explorerMaxHeight = Math.max(5, height - explorerStartLineCount - 2);
619
+ const explorerMaxHeight = this.explorerMaxHeight;
620
+ const explorerLines = [];
621
+ if (this.navStack.length === 0) {
622
+ const repos = this.getRepoKeys(tracker);
623
+ this.reposSelectedIndex = Math.min(this.reposSelectedIndex, Math.max(0, repos.length - 1));
624
+ if (repos.length === 0) {
625
+ explorerLines.push(` ${dim("No active repositories or workstreams in state.")}`);
626
+ }
627
+ else {
628
+ // Keep cursor visible
629
+ if (this.reposSelectedIndex < this.reposScrollOffset)
630
+ this.reposScrollOffset = this.reposSelectedIndex;
631
+ if (this.reposSelectedIndex >= this.reposScrollOffset + explorerMaxHeight)
632
+ this.reposScrollOffset = this.reposSelectedIndex - explorerMaxHeight + 1;
633
+ this.reposScrollOffset = Math.max(0, Math.min(this.reposScrollOffset, Math.max(0, repos.length - explorerMaxHeight)));
634
+ const visibleEnd = Math.min(repos.length, this.reposScrollOffset + explorerMaxHeight);
635
+ for (let i = this.reposScrollOffset; i < visibleEnd; i++) {
636
+ const isSelected = i === this.reposSelectedIndex;
637
+ const pointer = isSelected ? green("› ") : " ";
638
+ const repoText = isSelected ? bold(repos[i] || "") : (repos[i] || "");
639
+ explorerLines.push(` ${pointer}${repoText}`);
640
+ }
641
+ if (repos.length > explorerMaxHeight) {
642
+ const pct = Math.round((visibleEnd / repos.length) * 100);
643
+ explorerLines.push(` ${dim(`-- ${visibleEnd}/${repos.length} (${pct}%) --`)}`);
644
+ }
645
+ }
646
+ }
647
+ else {
648
+ const top = this.navStack[this.navStack.length - 1];
649
+ if (top.type === "workstreams") {
650
+ const allWorkstreams = tracker.getWorkstreamsForRepo(top.repoKey);
651
+ const workstreams = this.getFilteredWorkstreams(top);
652
+ top.selectedIndex = Math.min(top.selectedIndex, Math.max(0, workstreams.length - 1));
653
+ if (top.filter) {
654
+ const matchStr = `${workstreams.length}/${allWorkstreams.length}`;
655
+ explorerLines.push(` ${cyan("/")} ${bold(top.filter)}${dim("_")} ${dim(matchStr + " matches")}`);
656
+ }
657
+ if (workstreams.length === 0) {
658
+ explorerLines.push(` ${dim(top.filter ? `No matches for "${top.filter}"` : "No active workstreams for this repository.")}`);
659
+ }
660
+ else {
661
+ // Keep cursor visible
662
+ if (top.selectedIndex < top.scrollOffset)
663
+ top.scrollOffset = top.selectedIndex;
664
+ if (top.selectedIndex >= top.scrollOffset + explorerMaxHeight)
665
+ top.scrollOffset = top.selectedIndex - explorerMaxHeight + 1;
666
+ top.scrollOffset = Math.max(0, Math.min(top.scrollOffset, Math.max(0, workstreams.length - explorerMaxHeight)));
667
+ const visibleEnd = Math.min(workstreams.length, top.scrollOffset + explorerMaxHeight);
668
+ for (let i = top.scrollOffset; i < visibleEnd; i++) {
669
+ const isSelected = i === top.selectedIndex;
670
+ const pointer = isSelected ? green("› ") : " ";
671
+ const ws = workstreams[i];
672
+ const typeStr = ws.origin.type.toUpperCase();
673
+ const title = tracker.getWorkstreamTitle(ws) || "No title";
674
+ const label = `${typeStr} #${ws.origin.number} · ${title}`;
675
+ const formatted = isSelected ? bold(label) : label;
676
+ explorerLines.push(` ${pointer}${formatted}`);
677
+ }
678
+ if (workstreams.length > explorerMaxHeight) {
679
+ const pct = Math.round((visibleEnd / workstreams.length) * 100);
680
+ explorerLines.push(` ${dim(`-- ${visibleEnd}/${workstreams.length} (${pct}%) --`)}`);
681
+ }
682
+ }
683
+ }
684
+ else if (top.type === "workstream_menu") {
685
+ const menuItems = ["State", "Executions"];
686
+ for (let i = 0; i < menuItems.length; i++) {
687
+ const isSelected = i === top.selectedIndex;
688
+ const pointer = isSelected ? green("› ") : " ";
689
+ const formatted = isSelected ? bold(menuItems[i] || "") : (menuItems[i] || "");
690
+ explorerLines.push(` ${pointer}${formatted}`);
691
+ }
692
+ }
693
+ else if (top.type === "state_view") {
694
+ const liveWorkstream = tracker.getWorkstreamById(top.workstream.id) || top.workstream;
695
+ const stateStr = JSON.stringify(liveWorkstream, null, 2);
696
+ const allStateLines = stateStr.split("\n");
697
+ top.linesCount = allStateLines.length;
698
+ top.scrollOffset = Math.min(top.scrollOffset, Math.max(0, top.linesCount - explorerMaxHeight));
699
+ const start = top.scrollOffset;
700
+ const end = start + explorerMaxHeight;
701
+ const visible = allStateLines.slice(start, end);
702
+ for (const line of visible) {
703
+ explorerLines.push(` ${line}`);
704
+ }
705
+ if (allStateLines.length > explorerMaxHeight) {
706
+ const pct = Math.round(((start + visible.length) / allStateLines.length) * 100);
707
+ explorerLines.push(` ${dim(`-- scroll: line ${start + 1}/${allStateLines.length} (${pct}%) --`)}`);
708
+ }
709
+ }
710
+ else if (top.type === "executions_list") {
711
+ const executions = tracker.getExecutionsForWorkstream(top.workstream.id);
712
+ top.selectedIndex = Math.min(top.selectedIndex, Math.max(0, executions.length - 1));
713
+ if (executions.length === 0) {
714
+ explorerLines.push(` ${dim("No executions recorded for this workstream.")}`);
715
+ }
716
+ else {
717
+ // Keep cursor visible
718
+ if (top.selectedIndex < top.scrollOffset)
719
+ top.scrollOffset = top.selectedIndex;
720
+ if (top.selectedIndex >= top.scrollOffset + explorerMaxHeight)
721
+ top.scrollOffset = top.selectedIndex - explorerMaxHeight + 1;
722
+ top.scrollOffset = Math.max(0, Math.min(top.scrollOffset, Math.max(0, executions.length - explorerMaxHeight)));
723
+ const visibleEnd = Math.min(executions.length, top.scrollOffset + explorerMaxHeight);
724
+ for (let i = top.scrollOffset; i < visibleEnd; i++) {
725
+ const isSelected = i === top.selectedIndex;
726
+ const pointer = isSelected ? green("› ") : " ";
727
+ const exec = executions[i];
728
+ const dateStr = new Date(exec.startedAt).toLocaleString();
729
+ const taskKind = tracker.getTaskKindText(exec.taskKind);
730
+ const statusText = exec.taskStatus === "completed" ? green("SUCCESS") : red("FAILED");
731
+ const details = `${dateStr} ${bold(taskKind)} [${statusText} (exit: ${exec.exitCode})]`;
732
+ const formatted = isSelected ? bold(details) : details;
733
+ explorerLines.push(` ${pointer}${formatted}`);
734
+ }
735
+ if (executions.length > explorerMaxHeight) {
736
+ const pct = Math.round((visibleEnd / executions.length) * 100);
737
+ explorerLines.push(` ${dim(`-- ${visibleEnd}/${executions.length} (${pct}%) --`)}`);
738
+ }
739
+ }
740
+ }
741
+ else if (top.type === "execution_files") {
742
+ top.selectedIndex = Math.min(top.selectedIndex, Math.max(0, top.files.length - 1));
743
+ if (top.loading) {
744
+ explorerLines.push(` ${cyan("Loading files...")}`);
745
+ }
746
+ else if (top.files.length === 0) {
747
+ explorerLines.push(` ${dim("No files found in execution directory.")}`);
748
+ }
749
+ else {
750
+ // Keep cursor visible
751
+ if (top.selectedIndex < top.scrollOffset)
752
+ top.scrollOffset = top.selectedIndex;
753
+ if (top.selectedIndex >= top.scrollOffset + explorerMaxHeight)
754
+ top.scrollOffset = top.selectedIndex - explorerMaxHeight + 1;
755
+ top.scrollOffset = Math.max(0, Math.min(top.scrollOffset, Math.max(0, top.files.length - explorerMaxHeight)));
756
+ const visibleEnd = Math.min(top.files.length, top.scrollOffset + explorerMaxHeight);
757
+ for (let i = top.scrollOffset; i < visibleEnd; i++) {
758
+ const isSelected = i === top.selectedIndex;
759
+ const pointer = isSelected ? green("› ") : " ";
760
+ const fileName = top.files[i] || "";
761
+ const formatted = isSelected ? bold(fileName) : fileName;
762
+ explorerLines.push(` ${pointer}${formatted}`);
763
+ }
764
+ if (top.files.length > explorerMaxHeight) {
765
+ const pct = Math.round((visibleEnd / top.files.length) * 100);
766
+ explorerLines.push(` ${dim(`-- ${visibleEnd}/${top.files.length} (${pct}%) --`)}`);
767
+ }
768
+ }
769
+ }
770
+ else if (top.type === "file_view") {
771
+ if (top.loading) {
772
+ explorerLines.push(` ${cyan("Loading file content...")}`);
773
+ }
774
+ else {
775
+ const fileContentLines = top.content.split("\n");
776
+ top.scrollOffset = Math.min(top.scrollOffset, Math.max(0, fileContentLines.length - explorerMaxHeight));
777
+ const start = top.scrollOffset;
778
+ const end = start + explorerMaxHeight;
779
+ const visible = fileContentLines.slice(start, end);
780
+ for (const line of visible) {
781
+ explorerLines.push(` ${line.replace(/\t/g, " ")}`);
782
+ }
783
+ if (fileContentLines.length > explorerMaxHeight) {
784
+ const pct = Math.round(((start + visible.length) / fileContentLines.length) * 100);
785
+ explorerLines.push(` ${dim(`-- scroll: line ${start + 1}/${fileContentLines.length} (${pct}%) --`)}`);
786
+ }
787
+ }
788
+ }
789
+ }
790
+ lines.push(...explorerLines);
791
+ return lines;
792
+ }
793
+ }
794
+ function resolveExecutionArtifactDir(execution) {
795
+ const artifactPath = firstNonEmptyString(execution.promptArtifactPath, execution.stdoutArtifactPath, execution.stderrArtifactPath, execution.streamArtifactPath, execution.providerTranscriptArtifactPath, execution.promptPacketPath, execution.settingsArtifactPath);
796
+ return artifactPath ? path.dirname(artifactPath) : undefined;
797
+ }
798
+ function firstNonEmptyString(...values) {
799
+ return values.find((value) => typeof value === "string" && value.trim().length > 0);
800
+ }
801
+ export class McpsView {
802
+ render(model, tracker, width, height, spinnerChar) {
803
+ const lines = [];
804
+ lines.push("");
805
+ lines.push(bold("SMITHERY MCP CONNECTIONS & CAPABILITIES"));
806
+ const reloadErr = tracker.getConfigReloadError();
807
+ if (reloadErr) {
808
+ lines.push(`${red("⚠️ CONFIG RELOAD FAILED:")} ${yellow(reloadErr.message || String(reloadErr))}`);
809
+ lines.push("");
810
+ }
811
+ const mcps = tracker.getMcps();
812
+ const statuses = tracker.getMcpStatuses();
813
+ const maxConcurrent = tracker.getMaxConcurrent();
814
+ lines.push(`${dim("Active MCPs:")} ${mcps.length} declared · ${dim("Concurrency limit:")} max ${maxConcurrent}`);
815
+ lines.push("");
816
+ if (mcps.length === 0) {
817
+ lines.push(dim("No Smithery MCPs declared in mcps.json."));
818
+ return lines;
819
+ }
820
+ for (const mcp of mcps) {
821
+ const statusRecord = statuses.find(s => s.handle === mcp.handle);
822
+ let statusSymbol = dim("●");
823
+ let statusText = "NOT CHECKED";
824
+ let lastChecked = "—";
825
+ if (statusRecord) {
826
+ lastChecked = statusRecord.lastCheckedAt ? new Date(statusRecord.lastCheckedAt).toLocaleTimeString() : "—";
827
+ switch (statusRecord.status) {
828
+ case "ready":
829
+ statusSymbol = green("●");
830
+ statusText = green("READY");
831
+ break;
832
+ case "resolved":
833
+ statusSymbol = cyan("●");
834
+ statusText = cyan("RESOLVED");
835
+ break;
836
+ case "action_required":
837
+ statusSymbol = yellow("●");
838
+ statusText = yellow("ACTION REQUIRED");
839
+ break;
840
+ case "resolution_failed":
841
+ statusSymbol = red("●");
842
+ statusText = red("RESOLUTION FAILED");
843
+ break;
844
+ case "error":
845
+ statusSymbol = red("●");
846
+ statusText = red("ERROR");
847
+ break;
848
+ }
849
+ }
850
+ lines.push(sectionHeader(mcp.handle.toUpperCase(), width));
851
+ lines.push(` ${dim("Smithery ID:")} ${mcp.smitheryId}`);
852
+ lines.push(` ${dim("Status:")} ${statusSymbol} ${statusText} ${dim(`(checked ${lastChecked})`)}`);
853
+ if (statusRecord?.message) {
854
+ lines.push(` ${dim("Message:")} ${yellow(statusRecord.message)}`);
855
+ }
856
+ if (statusRecord?.actionRequired?.message) {
857
+ lines.push(` ${dim("Message:")} ${yellow(statusRecord.actionRequired.message)}`);
858
+ }
859
+ if (statusRecord?.connection) {
860
+ const conn = statusRecord.connection;
861
+ lines.push(` ${dim("Connection:")} ${dim("Transport:")} ${conn.transport ?? "stdio"} · ${dim("Proof:")} ${conn.readinessProof ?? "connected"}`);
862
+ }
863
+ // Map capabilities using this MCP
864
+ const caps = tracker.getCapabilities();
865
+ const sensors = caps.sensors.filter(s => s.uses === mcp.handle);
866
+ const controllers = caps.controllers.filter(c => c.uses === mcp.handle);
867
+ if (sensors.length > 0 || controllers.length > 0) {
868
+ lines.push(" " + bold("Capabilities Using This MCP:"));
869
+ for (const sensor of sensors) {
870
+ const isProjected = statusRecord?.status === "ready";
871
+ const projStatusText = isProjected ? green("PROJECTED") : red("BLOCKED");
872
+ lines.push(` ${dim("-")} ${bold("sensor:")} ${sensor.name} ${dim(`(observes: ${sensor.observes})`)} [${projStatusText}]`);
873
+ if (isProjected) {
874
+ const mappedTools = SENSOR_TOOL_MAP[sensor.observes] ?? [];
875
+ lines.push(" " + dim("Projected Tools:"));
876
+ for (const tool of mappedTools) {
877
+ lines.push(` ${green("→")} ${cyan(`${mcp.handle}.${tool}`)}`);
878
+ }
879
+ }
880
+ }
881
+ for (const controller of controllers) {
882
+ const isProjected = statusRecord?.status === "ready";
883
+ const projStatusText = isProjected ? green("PROJECTED") : red("BLOCKED");
884
+ lines.push(` ${dim("-")} ${bold("controller:")} ${controller.name} ${dim(`(grants: ${controller.grants.join(", ")})`)} [${projStatusText}]`);
885
+ if (isProjected) {
886
+ const mappedTools = controller.grants.flatMap(g => CONTROLLER_TOOL_MAP[g] ?? []);
887
+ const uniqueTools = Array.from(new Set(mappedTools));
888
+ if (uniqueTools.length > 0) {
889
+ lines.push(" " + dim("Projected Tools:"));
890
+ for (const tool of uniqueTools) {
891
+ lines.push(` ${green("→")} ${cyan(`${mcp.handle}.${tool}`)}`);
892
+ }
893
+ }
894
+ }
895
+ }
896
+ }
897
+ else {
898
+ lines.push(dim(" No capabilities currently configured to use this MCP."));
899
+ }
900
+ lines.push("");
901
+ }
902
+ return lines;
903
+ }
904
+ }
905
+ export class TasksView {
906
+ render(model, tracker, width, height, spinnerChar) {
907
+ const lines = [];
908
+ lines.push("");
909
+ lines.push(bold("CURRENTLY RUNNING TASKS"));
910
+ const running = tracker.getRunningTasks();
911
+ const maxConcurrent = tracker.getMaxConcurrent();
912
+ lines.push(`${dim("Total:")} ${running.size} active · ${dim("Concurrency limit:")} max ${maxConcurrent}`);
913
+ lines.push("");
914
+ if (running.size === 0) {
915
+ lines.push(dim("No tasks are currently running."));
916
+ return lines;
917
+ }
918
+ // Dynamic Columns config
919
+ let colWidths;
920
+ let headers;
921
+ if (width >= 100) {
922
+ const agentW = 16;
923
+ const jobW = 20;
924
+ const timeW = 7;
925
+ const targetW = Math.max(20, Math.floor((width - 10) * 0.22));
926
+ const worktreeW = Math.max(20, Math.floor((width - 10) * 0.22));
927
+ const idW = Math.max(8, width - 10 - agentW - jobW - timeW - targetW - worktreeW);
928
+ colWidths = [agentW, jobW, timeW, targetW, worktreeW, idW];
929
+ headers = ["AGENT", "JOB/KIND", "TIME", "TARGET", "WORKTREE", "TASK ID"];
930
+ }
931
+ else if (width >= 70) {
932
+ const agentW = 14;
933
+ const jobW = 18;
934
+ const timeW = 7;
935
+ const targetW = Math.max(15, Math.floor((width - 8) * 0.3));
936
+ const worktreeW = width - 8 - agentW - jobW - timeW - targetW;
937
+ colWidths = [agentW, jobW, timeW, targetW, worktreeW];
938
+ headers = ["AGENT", "JOB/KIND", "TIME", "TARGET", "WORKTREE"];
939
+ }
940
+ else {
941
+ const agentW = 12;
942
+ const jobW = 16;
943
+ const timeW = 6;
944
+ const targetW = width - 6 - agentW - jobW - timeW;
945
+ colWidths = [agentW, jobW, timeW, targetW];
946
+ headers = ["AGENT", "JOB/KIND", "TIME", "TARGET"];
947
+ }
948
+ const headerRow = colWidths.map((w, idx) => padOrTruncateAnsi(bold(headers[idx] || ""), w)).join(" ");
949
+ const underline = colWidths.map(w => "─".repeat(w)).join(" ");
950
+ lines.push(bold(cyan(headerRow)));
951
+ lines.push(dim(underline));
952
+ for (const [taskId, { task, agent, startTime }] of running.entries()) {
953
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
954
+ const min = Math.floor(elapsed / 60);
955
+ const sec = elapsed % 60;
956
+ const elapsedStr = `${min}:${String(sec).padStart(2, "0")}`;
957
+ // 1. Agent column styling
958
+ const agentText = formatAgent(agent);
959
+ // 2. Job column styling
960
+ const taskKind = tracker.getTaskKindText(task.kind);
961
+ let jobText = taskKind;
962
+ if (!agent) {
963
+ jobText = dim(taskKind);
964
+ }
965
+ else if (task.kind === "implement_plan") {
966
+ jobText = green(taskKind);
967
+ }
968
+ else if (task.kind === "write_plan") {
969
+ jobText = blue(taskKind);
970
+ }
971
+ else if (task.kind === "review_pr") {
972
+ jobText = yellow(taskKind);
973
+ }
974
+ else if (task.kind === "prepare_pr_for_merge") {
975
+ jobText = magenta(taskKind);
976
+ }
977
+ else if (task.kind === "sync_parent_branch") {
978
+ jobText = dim(taskKind);
979
+ }
980
+ // 3. Time column styling
981
+ const timeText = agent ? cyan(elapsedStr) : dim(elapsedStr);
982
+ // 4. Target column
983
+ const rawTarget = tracker.getTaskTargetText(task);
984
+ const targetText = agent ? rawTarget : dim(rawTarget);
985
+ // 5. Worktree column
986
+ const rawWorktree = task.context.localRepoPath ? (task.context.localRepoPath.split("/").pop() ?? "—") : "—";
987
+ const worktreeText = agent ? rawWorktree : dim(rawWorktree);
988
+ // 6. ID column
989
+ const idText = agent ? task.id : dim(task.id);
990
+ // Build cols array based on configuration
991
+ let rowCols = [];
992
+ if (width >= 100) {
993
+ rowCols = [agentText, jobText, timeText, targetText, worktreeText, idText];
994
+ }
995
+ else if (width >= 70) {
996
+ rowCols = [agentText, jobText, timeText, targetText, worktreeText];
997
+ }
998
+ else {
999
+ rowCols = [agentText, jobText, timeText, targetText];
1000
+ }
1001
+ const rowStr = rowCols.map((c, idx) => padOrTruncateAnsi(c, colWidths[idx] || 0)).join(" ");
1002
+ lines.push(rowStr);
1003
+ }
1004
+ return lines;
1005
+ }
1006
+ }