@entelligentsia/forgecli 1.0.10 → 1.0.14

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 (167) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/CHANGELOG-forge-plugin.md +150 -0
  3. package/dist/bin/forge.js +0 -0
  4. package/dist/extensions/forgecli/config-layer.d.ts +16 -0
  5. package/dist/extensions/forgecli/config-layer.js +5 -0
  6. package/dist/extensions/forgecli/config-layer.js.map +1 -1
  7. package/dist/extensions/forgecli/dashboard/component.d.ts +102 -0
  8. package/dist/extensions/forgecli/dashboard/component.js +882 -0
  9. package/dist/extensions/forgecli/dashboard/component.js.map +1 -0
  10. package/dist/extensions/forgecli/dashboard/register.d.ts +2 -0
  11. package/dist/extensions/forgecli/dashboard/register.js +45 -0
  12. package/dist/extensions/forgecli/dashboard/register.js.map +1 -0
  13. package/dist/extensions/forgecli/dashboard/view-model.d.ts +35 -0
  14. package/dist/extensions/forgecli/dashboard/view-model.js +54 -0
  15. package/dist/extensions/forgecli/dashboard/view-model.js.map +1 -0
  16. package/dist/extensions/forgecli/fix-bug.js +72 -7
  17. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  18. package/dist/extensions/forgecli/forge-cli-schema.json +4 -0
  19. package/dist/extensions/forgecli/forge-commands.js +1 -0
  20. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  21. package/dist/extensions/forgecli/forge-init/phase4-register.js +53 -0
  22. package/dist/extensions/forgecli/forge-init/phase4-register.js.map +1 -1
  23. package/dist/extensions/forgecli/forge-subagent.js +6 -4
  24. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  25. package/dist/extensions/forgecli/index.js +5 -0
  26. package/dist/extensions/forgecli/index.js.map +1 -1
  27. package/dist/extensions/forgecli/lib/halt-advisor.d.ts +54 -0
  28. package/dist/extensions/forgecli/lib/halt-advisor.js +90 -0
  29. package/dist/extensions/forgecli/lib/halt-advisor.js.map +1 -0
  30. package/dist/extensions/forgecli/migration-engine.js +25 -12
  31. package/dist/extensions/forgecli/migration-engine.js.map +1 -1
  32. package/dist/extensions/forgecli/orchestrator-status-bar.d.ts +25 -0
  33. package/dist/extensions/forgecli/orchestrator-status-bar.js +183 -0
  34. package/dist/extensions/forgecli/orchestrator-status-bar.js.map +1 -0
  35. package/dist/extensions/forgecli/orchestrator-tree.d.ts +96 -0
  36. package/dist/extensions/forgecli/orchestrator-tree.js +390 -0
  37. package/dist/extensions/forgecli/orchestrator-tree.js.map +1 -0
  38. package/dist/extensions/forgecli/project-orientation.js +12 -8
  39. package/dist/extensions/forgecli/project-orientation.js.map +1 -1
  40. package/dist/extensions/forgecli/regenerate.d.ts +16 -0
  41. package/dist/extensions/forgecli/regenerate.js +110 -0
  42. package/dist/extensions/forgecli/regenerate.js.map +1 -1
  43. package/dist/extensions/forgecli/run-sprint.js +33 -3
  44. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  45. package/dist/extensions/forgecli/run-task.d.ts +32 -0
  46. package/dist/extensions/forgecli/run-task.js +185 -12
  47. package/dist/extensions/forgecli/run-task.js.map +1 -1
  48. package/dist/extensions/forgecli/thread-switcher.js +105 -764
  49. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  50. package/dist/extensions/forgecli/viewport-events.js +32 -0
  51. package/dist/extensions/forgecli/viewport-events.js.map +1 -1
  52. package/dist/forge-payload/.base-pack/commands/fix-bug.md +1 -1
  53. package/dist/forge-payload/.base-pack/commands/run-sprint.md +1 -1
  54. package/dist/forge-payload/.base-pack/commands/run-task.md +1 -1
  55. package/dist/forge-payload/.base-pack/personas/architect.md +1 -1
  56. package/dist/forge-payload/.base-pack/personas/bug-fixer.md +1 -1
  57. package/dist/forge-payload/.base-pack/personas/collator.md +3 -3
  58. package/dist/forge-payload/.base-pack/personas/engineer.md +1 -1
  59. package/dist/forge-payload/.base-pack/personas/librarian.md +1 -1
  60. package/dist/forge-payload/.base-pack/personas/orchestrator.md +1 -1
  61. package/dist/forge-payload/.base-pack/personas/product-manager.md +1 -1
  62. package/dist/forge-payload/.base-pack/personas/qa-engineer.md +1 -1
  63. package/dist/forge-payload/.base-pack/personas/supervisor.md +1 -1
  64. package/dist/forge-payload/.base-pack/workflows/_fragments/event-emission-schema.md +1 -1
  65. package/dist/forge-payload/.base-pack/workflows/_fragments/friction-emit.md +1 -1
  66. package/dist/forge-payload/.base-pack/workflows/_fragments/iron-laws.md +1 -1
  67. package/dist/forge-payload/.base-pack/workflows/_fragments/progress-reporting.md +2 -2
  68. package/dist/forge-payload/.base-pack/workflows/_fragments/store-cli-verbs.md +11 -2
  69. package/dist/forge-payload/.base-pack/workflows/architect_approve.md +6 -7
  70. package/dist/forge-payload/.base-pack/workflows/architect_review_sprint_completion.md +2 -2
  71. package/dist/forge-payload/.base-pack/workflows/architect_sprint_intake.md +2 -2
  72. package/dist/forge-payload/.base-pack/workflows/architect_sprint_plan.md +5 -5
  73. package/dist/forge-payload/.base-pack/workflows/collator_agent.md +4 -6
  74. package/dist/forge-payload/.base-pack/workflows/commit_task.md +5 -6
  75. package/dist/forge-payload/.base-pack/workflows/enhance.md +5 -5
  76. package/dist/forge-payload/.base-pack/workflows/implement_plan.md +6 -7
  77. package/dist/forge-payload/.base-pack/workflows/migrate_structural.md +12 -13
  78. package/dist/forge-payload/.base-pack/workflows/plan_task.md +5 -6
  79. package/dist/forge-payload/.base-pack/workflows/review_code.md +8 -8
  80. package/dist/forge-payload/.base-pack/workflows/review_plan.md +8 -8
  81. package/dist/forge-payload/.base-pack/workflows/sprint_retrospective.md +3 -3
  82. package/dist/forge-payload/.base-pack/workflows/triage.md +12 -9
  83. package/dist/forge-payload/.base-pack/workflows/update_implementation.md +2 -2
  84. package/dist/forge-payload/.base-pack/workflows/update_plan.md +2 -2
  85. package/dist/forge-payload/.base-pack/workflows/validate_task.md +5 -6
  86. package/dist/forge-payload/.base-pack/workflows-js/wfl-fix-bug.js +490 -0
  87. package/dist/forge-payload/.base-pack/workflows-js/wfl-run-sprint.js +416 -0
  88. package/dist/forge-payload/.base-pack/workflows-js/wfl-run-task.js +608 -0
  89. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  90. package/dist/forge-payload/.schemas/config.schema.json +2 -3
  91. package/dist/forge-payload/.schemas/enum-catalog.json +2 -2
  92. package/dist/forge-payload/.schemas/event.schema.json +16 -0
  93. package/dist/forge-payload/.schemas/migrations.json +236 -0
  94. package/dist/forge-payload/commands/health.md +29 -0
  95. package/dist/forge-payload/commands/rebuild.md +143 -15
  96. package/dist/forge-payload/commands/update.md +28 -27
  97. package/dist/forge-payload/hooks/preflight-session.cjs +99 -0
  98. package/dist/forge-payload/init/phases/phase-3-materialize.md +18 -5
  99. package/dist/forge-payload/integrity.json +7 -6
  100. package/dist/forge-payload/meta/fragments/tool-discipline.md +1 -1
  101. package/dist/forge-payload/meta/personas/meta-architect.md +1 -1
  102. package/dist/forge-payload/meta/personas/meta-bug-fixer.md +1 -1
  103. package/dist/forge-payload/meta/personas/meta-collator.md +7 -7
  104. package/dist/forge-payload/meta/personas/meta-engineer.md +1 -1
  105. package/dist/forge-payload/meta/personas/meta-orchestrator.md +1 -1
  106. package/dist/forge-payload/meta/personas/meta-supervisor.md +1 -1
  107. package/dist/forge-payload/meta/tool-specs/store-cli.spec.md +1 -1
  108. package/dist/forge-payload/meta/workflows/_fragments/event-emission-schema.md +1 -1
  109. package/dist/forge-payload/meta/workflows/_fragments/friction-emit.md +1 -1
  110. package/dist/forge-payload/meta/workflows/_fragments/iron-laws.md +1 -1
  111. package/dist/forge-payload/meta/workflows/_fragments/progress-reporting.md +2 -2
  112. package/dist/forge-payload/meta/workflows/_fragments/store-cli-verbs.md +11 -2
  113. package/dist/forge-payload/meta/workflows/meta-approve.md +6 -7
  114. package/dist/forge-payload/meta/workflows/meta-bug-triage.md +12 -9
  115. package/dist/forge-payload/meta/workflows/meta-collate.md +5 -7
  116. package/dist/forge-payload/meta/workflows/meta-commit.md +5 -6
  117. package/dist/forge-payload/meta/workflows/meta-enhance.md +5 -5
  118. package/dist/forge-payload/meta/workflows/meta-fix-bug.md +35 -11
  119. package/dist/forge-payload/meta/workflows/meta-implement.md +15 -7
  120. package/dist/forge-payload/meta/workflows/meta-migrate.md +13 -14
  121. package/dist/forge-payload/meta/workflows/meta-new-sprint.md +3 -3
  122. package/dist/forge-payload/meta/workflows/meta-orchestrate.md +138 -39
  123. package/dist/forge-payload/meta/workflows/meta-plan-sprint.md +6 -6
  124. package/dist/forge-payload/meta/workflows/meta-plan-task.md +12 -6
  125. package/dist/forge-payload/meta/workflows/meta-retro.md +4 -4
  126. package/dist/forge-payload/meta/workflows/meta-retrospective.md +4 -4
  127. package/dist/forge-payload/meta/workflows/meta-review-implementation.md +8 -8
  128. package/dist/forge-payload/meta/workflows/meta-review-plan.md +8 -8
  129. package/dist/forge-payload/meta/workflows/meta-review-sprint-completion.md +3 -3
  130. package/dist/forge-payload/meta/workflows/meta-sprint-intake.md +3 -3
  131. package/dist/forge-payload/meta/workflows/meta-sprint-plan.md +6 -6
  132. package/dist/forge-payload/meta/workflows/meta-update-implementation.md +2 -2
  133. package/dist/forge-payload/meta/workflows/meta-update-plan.md +2 -2
  134. package/dist/forge-payload/meta/workflows/meta-validate.md +5 -6
  135. package/dist/forge-payload/schemas/config.schema.json +2 -3
  136. package/dist/forge-payload/schemas/enum-catalog.json +2 -2
  137. package/dist/forge-payload/schemas/event.schema.json +16 -0
  138. package/dist/forge-payload/schemas/structure-manifest.json +75 -73
  139. package/dist/forge-payload/skills/refresh-kb-links/SKILL.md +14 -7
  140. package/dist/forge-payload/tools/banners.cjs +29 -10
  141. package/dist/forge-payload/tools/check-structure.cjs +88 -7
  142. package/dist/forge-payload/tools/collate.cjs +16 -2
  143. package/dist/forge-payload/tools/manage-config.cjs +5 -7
  144. package/dist/forge-payload/tools/parse-gates.cjs +73 -1
  145. package/dist/forge-payload/tools/postflight-gate.cjs +252 -0
  146. package/dist/forge-payload/tools/preflight-gate.cjs +47 -0
  147. package/dist/forge-payload/tools/substitute-placeholders.cjs +5 -4
  148. package/dist/forge-payload/tools/verify-phase.cjs +17 -0
  149. package/package.json +1 -1
  150. package/dist/bin/forgecli.d.ts +0 -2
  151. package/dist/bin/forgecli.js +0 -6
  152. package/dist/bin/forgecli.js.map +0 -1
  153. package/dist/extensions/forgecli/config-tui/index.d.ts +0 -5
  154. package/dist/extensions/forgecli/config-tui/index.js +0 -5
  155. package/dist/extensions/forgecli/config-tui/index.js.map +0 -1
  156. package/dist/extensions/forgecli/loaders/persona-skill-loader.d.ts +0 -45
  157. package/dist/extensions/forgecli/loaders/persona-skill-loader.js +0 -227
  158. package/dist/extensions/forgecli/loaders/persona-skill-loader.js.map +0 -1
  159. package/dist/extensions/forgecli/loaders/template-render.d.ts +0 -20
  160. package/dist/extensions/forgecli/loaders/template-render.js +0 -85
  161. package/dist/extensions/forgecli/loaders/template-render.js.map +0 -1
  162. package/dist/extensions/forgecli/loaders/workflow-loader.d.ts +0 -41
  163. package/dist/extensions/forgecli/loaders/workflow-loader.js +0 -164
  164. package/dist/extensions/forgecli/loaders/workflow-loader.js.map +0 -1
  165. package/dist/forge-payload/.base-pack/workflows/fix_bug.md +0 -446
  166. package/dist/forge-payload/.base-pack/workflows/orchestrate_task.md +0 -928
  167. package/dist/forge-payload/.base-pack/workflows/run_sprint.md +0 -225
@@ -0,0 +1,882 @@
1
+ // dashboard/component.ts — Two-panel orchestrator tree dashboard.
2
+ //
3
+ // Renders as a pi-tui overlay (ctx.ui.custom) with:
4
+ // Left panel: tree browser with depth-based indentation, status glyphs,
5
+ // progress labels, and cursor highlighting.
6
+ // Right panel: detail view of the selected node — status, model, metrics,
7
+ // prompt preview, tail-buffer activity, outcome, and children.
8
+ //
9
+ // Keyboard:
10
+ // ↑/↓ move cursor in tree
11
+ // → expand orchestrator / focus leaf detail
12
+ // ← collapse orchestrator / back to tree
13
+ // Enter toggle expand on orchestrator; focus detail on leaf
14
+ // Esc close overlay
15
+ // x request cancellation (with y/n confirm)
16
+ // p (reserved for pause)
17
+ //
18
+ // Architecture (post-MVC-refactor, dashboard-mvc-audit.md V1–V7):
19
+ // DashboardController owns the TreeViewModel, subscribes to model events,
20
+ // manages the refresh timer, and handles input. DashboardComponent reads
21
+ // only from the controller — never directly from the OrchestratorTree model.
22
+ // This eliminates the boundary violations catalogued in the audit.
23
+ import { matchesKey, Key, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
24
+ import { getSessionRegistry } from "../session-registry.js";
25
+ import { buildViewModel } from "./view-model.js";
26
+ import { fmtTokenMeter } from "../viewport-renderer.js";
27
+ import { paintTailLine } from "../viewport-theme.js";
28
+ // ── Word-wrap helper ───────────────────────────────────────────────────────────
29
+ //
30
+ // Splits a line (which may contain ANSI escape sequences) into multiple
31
+ // lines at word boundaries so it fits within `maxWidth` visible columns.
32
+ // Preserves ANSI styling across wrapped lines. Falls back to character-level
33
+ // truncation for individual tokens wider than maxWidth.
34
+ function wrapLine(line, maxWidth) {
35
+ if (maxWidth <= 0)
36
+ return [line];
37
+ const visW = visibleWidth(line);
38
+ if (visW <= maxWidth)
39
+ return [line];
40
+ const segments = [];
41
+ const ANSI_RE = /(\x1b\[[0-9;]*m|\x1b\].*?(?:\x07|\x1b\\)|\x1b\[[^m]*m)/g;
42
+ let currentPrefix = "";
43
+ let lastIdx = 0;
44
+ let m;
45
+ while ((m = ANSI_RE.exec(line)) !== null) {
46
+ if (m.index > lastIdx) {
47
+ segments.push({ prefix: currentPrefix, text: line.slice(lastIdx, m.index) });
48
+ }
49
+ currentPrefix += m[0];
50
+ lastIdx = m.index + m[0].length;
51
+ }
52
+ if (lastIdx < line.length) {
53
+ segments.push({ prefix: currentPrefix, text: line.slice(lastIdx) });
54
+ }
55
+ if (segments.length === 0) {
56
+ return [truncateToWidth(line, maxWidth)];
57
+ }
58
+ const tokens = [];
59
+ for (const seg of segments) {
60
+ const words = seg.text.split(/\s+/);
61
+ for (const w of words) {
62
+ if (w.length === 0)
63
+ continue;
64
+ tokens.push({ prefix: seg.prefix, text: w, visLen: visibleWidth(w) });
65
+ }
66
+ }
67
+ // Greedy line-fill: accumulate tokens with single-space gaps until the
68
+ // next token would exceed maxWidth, then start a new line.
69
+ const lines = [];
70
+ let curLine = "";
71
+ let curLen = 0;
72
+ for (const tok of tokens) {
73
+ const gapLen = curLen > 0 ? 1 : 0; // one space between tokens
74
+ const addedLen = tok.visLen + gapLen;
75
+ if (curLen > 0 && curLen + addedLen > maxWidth) {
76
+ // Emit current line and start fresh
77
+ if (curLine.length > 0)
78
+ lines.push(curLine);
79
+ curLine = "";
80
+ curLen = 0;
81
+ }
82
+ if (curLen === 0) {
83
+ curLine = tok.prefix + tok.text;
84
+ curLen = tok.visLen;
85
+ }
86
+ else {
87
+ curLine += " " + tok.text;
88
+ curLen += 1 + tok.visLen;
89
+ }
90
+ // If a single token is wider than maxWidth, hard-break via truncation.
91
+ if (curLen > maxWidth) {
92
+ const truncated = truncateToWidth(curLine, maxWidth);
93
+ lines.push(truncated);
94
+ curLine = "";
95
+ curLen = 0;
96
+ }
97
+ }
98
+ if (curLen > 0) {
99
+ lines.push(curLine);
100
+ }
101
+ return lines.length > 0 ? lines : [line];
102
+ } // ── Refresh timer ───────────────────────────────────────────────────────────
103
+ const REFRESH_INTERVAL_MS = 1000;
104
+ // ── Controller ──────────────────────────────────────────────────────────────
105
+ export class DashboardController {
106
+ tree;
107
+ vm;
108
+ state;
109
+ onInvalidate;
110
+ refreshTimer;
111
+ _handlers;
112
+ constructor(tree, initialCursorId) {
113
+ this.tree = tree;
114
+ this.vm = buildViewModel(tree);
115
+ // Default cursor to the first active root, or empty string if tree is empty.
116
+ const firstVisible = this.vm.roots.length > 0 ? this.vm.roots[0] : "";
117
+ this.state = {
118
+ cursorId: initialCursorId ?? firstVisible,
119
+ expanded: new Set(),
120
+ focusPanel: "tree",
121
+ promptExpanded: false,
122
+ cancelTargetId: null,
123
+ detailScroll: 0,
124
+ };
125
+ this._handlers = [];
126
+ // V2 fix: subscriptions now owned by controller, not view.
127
+ // On every model event, rebuild VM then invalidate the view.
128
+ const onModelChange = () => {
129
+ this.rebuildViewModel();
130
+ this.onInvalidate?.();
131
+ };
132
+ const onTreeChange = (id) => {
133
+ this.rebuildViewModel();
134
+ this.autoExpandNewNode(id);
135
+ this.onInvalidate?.();
136
+ };
137
+ this.tree.on("change", onModelChange);
138
+ this.tree.on("tail", onModelChange);
139
+ this.tree.on("preview", onModelChange);
140
+ this.tree.on("tree", onTreeChange);
141
+ this._handlers = [
142
+ { event: "change", handler: onModelChange },
143
+ { event: "tail", handler: onModelChange },
144
+ { event: "preview", handler: onModelChange },
145
+ { event: "tree", handler: onTreeChange },
146
+ ];
147
+ // V3 fix: timer now owned by controller, not view.
148
+ this.ensureRefreshTimer();
149
+ }
150
+ setOnInvalidate(cb) {
151
+ this.onInvalidate = cb;
152
+ }
153
+ // ── ViewModel projection ────────────────────────────────────────────────
154
+ /** Rebuild the ViewModel from the live model. Called on every model event. */
155
+ rebuildViewModel() {
156
+ this.vm = buildViewModel(this.tree);
157
+ }
158
+ /** Get a node from the current ViewModel. */
159
+ getNode(id) {
160
+ return this.vm.nodes.get(id);
161
+ }
162
+ /** Get a node's children from the current ViewModel. */
163
+ getChildren(id) {
164
+ const node = this.vm.nodes.get(id);
165
+ if (!node)
166
+ return [];
167
+ return node.children
168
+ .map((cid) => this.vm.nodes.get(cid))
169
+ .filter((n) => n !== undefined);
170
+ }
171
+ /** Get the progress (completed/total leaves) for a subtree. */
172
+ getSubtreeProgress(id) {
173
+ let completed = 0;
174
+ let total = 0;
175
+ const stack = [id];
176
+ while (stack.length > 0) {
177
+ const current = stack.pop();
178
+ const node = this.vm.nodes.get(current);
179
+ if (!node)
180
+ continue;
181
+ if (node.kind === "leaf") {
182
+ total++;
183
+ if (node.status === "completed")
184
+ completed++;
185
+ }
186
+ stack.push(...node.children);
187
+ }
188
+ return { completed, total };
189
+ }
190
+ /** Aggregate token usage across all active roots. */
191
+ getAggregateUsage() {
192
+ const agg = { input: 0, output: 0, cacheRead: 0 };
193
+ for (const rootId of this.vm.roots) {
194
+ const stack = [rootId];
195
+ while (stack.length > 0) {
196
+ const current = stack.pop();
197
+ const node = this.vm.nodes.get(current);
198
+ if (!node)
199
+ continue;
200
+ agg.input += node.usage.input;
201
+ agg.output += node.usage.output;
202
+ agg.cacheRead += node.usage.cacheRead;
203
+ stack.push(...node.children);
204
+ }
205
+ }
206
+ return agg;
207
+ }
208
+ /** Aggregate compression across all active roots. */
209
+ getAggregateCompression() {
210
+ const agg = { calls: 0, tokensSaved: 0 };
211
+ for (const rootId of this.vm.roots) {
212
+ const stack = [rootId];
213
+ while (stack.length > 0) {
214
+ const current = stack.pop();
215
+ const node = this.vm.nodes.get(current);
216
+ if (!node)
217
+ continue;
218
+ if (node.compression) {
219
+ agg.calls += node.compression.calls;
220
+ agg.tokensSaved += node.compression.tokensSaved;
221
+ }
222
+ stack.push(...node.children);
223
+ }
224
+ }
225
+ return agg;
226
+ }
227
+ /** Model/provider from the first running root, if any. */
228
+ getActiveModel() {
229
+ for (const rootId of this.vm.roots) {
230
+ const node = this.vm.nodes.get(rootId);
231
+ if (node && node.model) {
232
+ return { provider: node.provider, model: node.model };
233
+ }
234
+ }
235
+ return undefined;
236
+ }
237
+ /** Check whether any active root is running or cancelling. */
238
+ hasRunningNodes() {
239
+ for (const rootId of this.vm.roots) {
240
+ const node = this.vm.nodes.get(rootId);
241
+ if (node && (node.status === "running" || node.status === "cancelling")) {
242
+ return true;
243
+ }
244
+ }
245
+ return false;
246
+ }
247
+ // ── Visible node list (DFS respecting expand state) ────────────────────
248
+ getVisibleNodes() {
249
+ const result = [];
250
+ const visit = (id) => {
251
+ const node = this.vm.nodes.get(id);
252
+ if (!node)
253
+ return;
254
+ result.push(id);
255
+ if (node.kind === "orchestrator" && this.state.expanded.has(id)) {
256
+ for (const childId of node.children) {
257
+ visit(childId);
258
+ }
259
+ }
260
+ };
261
+ for (const rootId of this.vm.roots) {
262
+ visit(rootId);
263
+ }
264
+ return result;
265
+ }
266
+ // ── Input handling ──────────────────────────────────────────────────────
267
+ handleInput(data) {
268
+ // Cancel confirmation mode takes priority.
269
+ if (this.state.cancelTargetId) {
270
+ this.handleCancelConfirm(data);
271
+ this.onInvalidate?.();
272
+ return;
273
+ }
274
+ switch (this.state.focusPanel) {
275
+ case "tree":
276
+ this.handleTreeInput(data);
277
+ break;
278
+ case "detail":
279
+ this.handleDetailInput(data);
280
+ break;
281
+ }
282
+ this.onInvalidate?.();
283
+ }
284
+ handleTreeInput(data) {
285
+ if (matchesKey(data, Key.up)) {
286
+ this.moveCursor(-1);
287
+ }
288
+ else if (matchesKey(data, Key.down)) {
289
+ this.moveCursor(1);
290
+ }
291
+ else if (matchesKey(data, Key.enter) || matchesKey(data, Key.right)) {
292
+ this.activateCursor();
293
+ }
294
+ else if (matchesKey(data, Key.left)) {
295
+ this.collapseCursor();
296
+ }
297
+ else if (data === "x") {
298
+ this.startCancel();
299
+ }
300
+ // ESC in tree panel is handled in handleInput above — it closes
301
+ // the overlay rather than navigating or setting a sentinel.
302
+ }
303
+ handleDetailInput(data) {
304
+ // ESC in detail panel is handled in handleInput above — it closes
305
+ // the overlay rather than just switching panels.
306
+ if (matchesKey(data, Key.left)) {
307
+ this.state.focusPanel = "tree";
308
+ this.state.detailScroll = 0;
309
+ }
310
+ else if (matchesKey(data, Key.down) || data === "j" || data === "J") {
311
+ this.state.detailScroll++;
312
+ }
313
+ else if (matchesKey(data, Key.up) || data === "k" || data === "K") {
314
+ this.state.detailScroll = Math.max(0, this.state.detailScroll - 1);
315
+ }
316
+ else if (matchesKey(data, Key.enter)) {
317
+ this.state.promptExpanded = !this.state.promptExpanded;
318
+ }
319
+ else if (data === "x") {
320
+ this.startCancel();
321
+ }
322
+ }
323
+ handleCancelConfirm(data) {
324
+ if (data === "y" || matchesKey(data, Key.enter)) {
325
+ if (this.state.cancelTargetId) {
326
+ this.cancelNodeAndSessions(this.state.cancelTargetId);
327
+ }
328
+ this.state.cancelTargetId = null;
329
+ }
330
+ else if (data === "n") {
331
+ this.state.cancelTargetId = null;
332
+ }
333
+ // All other keys consumed silently in cancel mode. ESC is handled
334
+ // by the view's handleInput (closes the overlay entirely).
335
+ }
336
+ /** Cancel the node in the OrchestratorTree for immediate visual
337
+ * feedback, and propagate cancellation through SessionRegistry
338
+ * for actual pipeline abort. Reads from VM; writes to model. */
339
+ cancelNodeAndSessions(nodeId) {
340
+ // Mutation: mark node as "cancelling" in the model for immediate
341
+ // visual feedback. The model emits "change", which triggers a VM
342
+ // rebuild via the controller's subscription.
343
+ this.tree.requestCancel(nodeId);
344
+ const registry = getSessionRegistry();
345
+ // Walk up the tree to find the session (task-level) node in the registry.
346
+ // Reads from the VM (pre-rebuild stale state is fine — parentId doesn't
347
+ // change on cancel, and we re-read from VM).
348
+ let currentId = nodeId;
349
+ while (currentId) {
350
+ const session = registry.getSession(currentId);
351
+ if (session && (session.status === "running" || session.status === "cancelling")) {
352
+ registry.requestCancel(currentId);
353
+ return;
354
+ }
355
+ const node = this.vm.nodes.get(currentId);
356
+ currentId = node?.parentId ?? null;
357
+ }
358
+ // No session found for this node or ancestors. If this is a
359
+ // sprint-level orchestrator, cancel all running child sessions.
360
+ const node = this.vm.nodes.get(nodeId);
361
+ if (node?.kind === "orchestrator") {
362
+ for (const childId of node.children) {
363
+ const session = registry.getSession(childId);
364
+ if (session && (session.status === "running" || session.status === "cancelling")) {
365
+ registry.requestCancel(childId);
366
+ }
367
+ }
368
+ }
369
+ }
370
+ moveCursor(delta) {
371
+ const visible = this.getVisibleNodes();
372
+ if (visible.length === 0)
373
+ return;
374
+ const idx = visible.indexOf(this.state.cursorId);
375
+ const next = Math.max(0, Math.min(visible.length - 1, idx + delta));
376
+ this.state.cursorId = visible[next];
377
+ // Auto-expand ancestors so the cursor is always visible.
378
+ this.ensureAncestorsExpanded(this.state.cursorId);
379
+ }
380
+ activateCursor() {
381
+ const node = this.vm.nodes.get(this.state.cursorId);
382
+ if (!node)
383
+ return;
384
+ if (node.kind === "orchestrator") {
385
+ this.toggleExpand(node.id);
386
+ }
387
+ else {
388
+ this.state.focusPanel = "detail";
389
+ this.state.detailScroll = 0;
390
+ }
391
+ }
392
+ toggleExpand(id) {
393
+ if (this.state.expanded.has(id)) {
394
+ this.state.expanded.delete(id);
395
+ }
396
+ else {
397
+ this.state.expanded.add(id);
398
+ }
399
+ }
400
+ collapseCursor() {
401
+ const node = this.vm.nodes.get(this.state.cursorId);
402
+ if (!node)
403
+ return;
404
+ if (node.kind === "orchestrator" && this.state.expanded.has(node.id)) {
405
+ this.state.expanded.delete(node.id);
406
+ }
407
+ else if (node.parentId) {
408
+ // Move cursor to parent.
409
+ this.state.cursorId = node.parentId;
410
+ }
411
+ }
412
+ startCancel() {
413
+ const node = this.vm.nodes.get(this.state.cursorId);
414
+ if (!node)
415
+ return;
416
+ // Can cancel any node under a running subtree — find the nearest
417
+ // cancellable ancestor (running or cancelling).
418
+ let target = null;
419
+ let current = node;
420
+ while (current) {
421
+ if (current.status === "running" || current.status === "cancelling") {
422
+ target = current;
423
+ break;
424
+ }
425
+ current = current.parentId ? this.vm.nodes.get(current.parentId) : undefined;
426
+ }
427
+ if (target) {
428
+ this.state.cancelTargetId = target.id;
429
+ }
430
+ }
431
+ ensureAncestorsExpanded(id) {
432
+ let currentId = id;
433
+ while (currentId) {
434
+ const node = this.vm.nodes.get(currentId);
435
+ if (!node)
436
+ break;
437
+ if (node.kind === "orchestrator") {
438
+ this.state.expanded.add(currentId);
439
+ }
440
+ currentId = node.parentId;
441
+ }
442
+ }
443
+ // ── Auto-expand: only expand the root and the direct parent of a new
444
+ // leaf node (phase). Intermediate orchestrator nodes (tasks) are NOT
445
+ // auto-expanded — the user expands them manually. This prevents the tree
446
+ // from exploding into a fully-expanded state on every new node.
447
+ autoExpandNewNode(id) {
448
+ const node = this.vm.nodes.get(id);
449
+ if (!node)
450
+ return;
451
+ // Auto-expand the root level only: if this node IS a root (no parent),
452
+ // expand it so it's visible. If this node is a leaf (phase), expand
453
+ // only its direct parent (the task). Intermediate orchestrators
454
+ // (tasks under a sprint) are NOT auto-expanded.
455
+ if (node.kind === "leaf" && node.parentId) {
456
+ // Leaf node (phase): expand its parent so the phase is visible,
457
+ // but only if the parent is already visible (i.e., the user
458
+ // has expanded the root).
459
+ const parent = this.vm.nodes.get(node.parentId);
460
+ if (parent && this.state.expanded.has(node.parentId) && parent.kind === "orchestrator") {
461
+ // Parent is visible and is an orchestrator — leaf will show.
462
+ // No need to expand further.
463
+ }
464
+ // Also ensure ancestors of the leaf are expanded so it's visible.
465
+ this.ensureAncestorsExpanded(node.id);
466
+ }
467
+ else if (!node.parentId) {
468
+ // Root node (sprint-level orchestrator): always expand.
469
+ this.state.expanded.add(id);
470
+ }
471
+ // Default cursor to the newest running node if no cursor set.
472
+ if (!this.state.cursorId || this.state.cursorId === "") {
473
+ this.state.cursorId = id;
474
+ }
475
+ }
476
+ // ── Refresh timer (V3 fix: owned by controller, not view) ──────────────
477
+ ensureRefreshTimer() {
478
+ if (this.refreshTimer)
479
+ return;
480
+ this.refreshTimer = setInterval(() => {
481
+ if (!this.hasRunningNodes()) {
482
+ // One last render to settle final frame, then stop timer.
483
+ this.onInvalidate?.();
484
+ this.stopRefreshTimer();
485
+ return;
486
+ }
487
+ this.onInvalidate?.();
488
+ }, REFRESH_INTERVAL_MS);
489
+ }
490
+ stopRefreshTimer() {
491
+ if (this.refreshTimer) {
492
+ clearInterval(this.refreshTimer);
493
+ this.refreshTimer = undefined;
494
+ }
495
+ }
496
+ // ── Cleanup ────────────────────────────────────────────────────────────
497
+ dispose() {
498
+ this.stopRefreshTimer();
499
+ for (const { event, handler } of this._handlers) {
500
+ this.tree.off(event, handler);
501
+ }
502
+ this._handlers = [];
503
+ }
504
+ getState() {
505
+ return this.state;
506
+ }
507
+ }
508
+ // ── View ────────────────────────────────────────────────────────────────────
509
+ export class DashboardComponent {
510
+ controller;
511
+ theme;
512
+ tui;
513
+ done;
514
+ constructor(controller, tui, theme, done) {
515
+ this.controller = controller;
516
+ this.tui = tui;
517
+ this.theme = theme;
518
+ this.done = done;
519
+ // V2 fix: subscriptions are now owned by the controller.
520
+ // The view only registers an invalidation callback for re-rendering.
521
+ this.controller.setOnInvalidate(() => this.tui.requestRender());
522
+ }
523
+ // ── Component interface ──────────────────────────────────────────────────
524
+ render(width) {
525
+ // Layout: left panel ~20% (min 22), right panel fills rest, borders.
526
+ const leftWidth = Math.max(22, Math.floor(width * 0.20));
527
+ const separatorWidth = 1;
528
+ const rightWidth = Math.max(20, width - leftWidth - separatorWidth - 3);
529
+ const contentWidth = width - 2; // minus left and right border chars
530
+ const visible = this.controller.getVisibleNodes();
531
+ const state = this.controller.getState();
532
+ const selectedNode = this.controller.getNode(state.cursorId);
533
+ // ── Left panel: tree browser ───────────────────────────────────
534
+ const leftLines = this.renderTreePanel(visible, state, leftWidth);
535
+ // ── Right panel: detail ─────────────────────────────────────────
536
+ const rightLines = selectedNode
537
+ ? this.renderDetailPanel(selectedNode, rightWidth)
538
+ : [this.theme.fg("dim", "Select a node in the tree")];
539
+ // ── Compose ─────────────────────────────────────────────────────
540
+ const termHeight = this.tui.terminal.rows;
541
+ // Deduct 3 lines for top border, bottom border, and hints footer
542
+ const contentHeight = Math.max(8, termHeight - 3);
543
+ // Compute detail panel scroll with auto-scrolling fallback for tree focus
544
+ let activeScroll = state.detailScroll;
545
+ if (state.focusPanel === "tree") {
546
+ // Auto-scroll to the bottom so live dispatches are visible immediately
547
+ activeScroll = Math.max(0, rightLines.length - contentHeight);
548
+ }
549
+ else {
550
+ // Clamp user manual scroll within valid bounds
551
+ const maxScroll = Math.max(0, rightLines.length - contentHeight);
552
+ if (activeScroll > maxScroll) {
553
+ activeScroll = maxScroll;
554
+ state.detailScroll = maxScroll; // sync controller state
555
+ }
556
+ }
557
+ const paddedLeftLines = [];
558
+ for (let i = 0; i < contentHeight; i++) {
559
+ paddedLeftLines.push(leftLines[i] ?? "");
560
+ }
561
+ const paddedRightLines = [];
562
+ for (let i = 0; i < contentHeight; i++) {
563
+ paddedRightLines.push(rightLines[i + activeScroll] ?? "");
564
+ }
565
+ const lines = [];
566
+ const border = (s) => this.theme.fg("border", s);
567
+ const accent = (s) => this.theme.fg("accent", s);
568
+ // ── Header ──────────────────────────────────────────────────────
569
+ const headerText = ` Orchestrator Dashboard `;
570
+ const headerPad = contentWidth - visibleWidth(border("")) - visibleWidth(accent(headerText));
571
+ lines.push(border("╭") +
572
+ accent(headerText) +
573
+ border("─".repeat(Math.max(0, headerPad))) +
574
+ border("╮"));
575
+ for (let i = 0; i < contentHeight; i++) {
576
+ const left = truncateToWidth(paddedLeftLines[i], leftWidth);
577
+ const right = truncateToWidth(paddedRightLines[i], rightWidth);
578
+ const lPad = leftWidth - visibleWidth(left);
579
+ const rPad = rightWidth - visibleWidth(right);
580
+ lines.push(border("│") +
581
+ left +
582
+ " ".repeat(Math.max(0, lPad)) +
583
+ border("│") +
584
+ " " +
585
+ right +
586
+ " ".repeat(Math.max(0, rPad)) +
587
+ border("│"));
588
+ }
589
+ // ── Footer: bottom border + key hints + model/token meter ────────
590
+ const dim = (s) => this.theme.fg("dim", s);
591
+ const hintsBase = state.cancelTargetId
592
+ ? " y confirm · n dismiss · esc close"
593
+ : " ↑↓ nav · → expand · ← back · ⏎ focus · x cancel · esc close";
594
+ lines.push(border("╰") + border("─".repeat(contentWidth)) + border("╯"));
595
+ // Aggregate model + token footer (mirrors ViewportFooterComponent).
596
+ const aggUsage = this.controller.getAggregateUsage();
597
+ const aggCompression = this.controller.getAggregateCompression();
598
+ const activeModel = this.controller.getActiveModel();
599
+ const meter = fmtTokenMeter(aggUsage);
600
+ const compSuffix = aggCompression.tokensSaved > 0 ? ` ⇌${aggCompression.tokensSaved}t` : "";
601
+ const modelLabel = activeModel?.provider && activeModel?.model
602
+ ? `${activeModel.provider} ${activeModel.model}`
603
+ : (activeModel?.provider ?? activeModel?.model ?? "");
604
+ let footerLine = "";
605
+ if (modelLabel && meter) {
606
+ footerLine = `${hintsBase} ${dim(modelLabel)} Σ ${meter}${compSuffix}`;
607
+ }
608
+ else if (meter) {
609
+ footerLine = `${hintsBase} Σ ${meter}${compSuffix}`;
610
+ }
611
+ else if (modelLabel) {
612
+ footerLine = `${hintsBase} ${dim(modelLabel)}`;
613
+ }
614
+ else {
615
+ footerLine = hintsBase;
616
+ }
617
+ lines.push(dim(truncateToWidth(footerLine, width)));
618
+ // ── Overlay cancel confirmation on top of dashboard content ────
619
+ if (state.cancelTargetId) {
620
+ return this.overlayCancelConfirm(lines, width, state.cancelTargetId);
621
+ }
622
+ return lines;
623
+ }
624
+ handleInput(data) {
625
+ // ESC always closes the overlay, regardless of focus panel or
626
+ // cancel-confirmation state. This takes priority over the controller's
627
+ // ESC handling (which only dismissed cancel-confirm but didn't close).
628
+ if (matchesKey(data, Key.escape)) {
629
+ this.dispose();
630
+ this.done(null);
631
+ return;
632
+ }
633
+ this.controller.handleInput(data);
634
+ }
635
+ invalidate() {
636
+ // No cached render state — pure function of VM + controller state.
637
+ }
638
+ dispose() {
639
+ this.controller.dispose();
640
+ }
641
+ // ── Tree panel renderer ─────────────────────────────────────────────────
642
+ renderTreePanel(visibleIds, state, width) {
643
+ if (visibleIds.length === 0) {
644
+ return [this.theme.fg("dim", "No session running")];
645
+ }
646
+ const lines = [];
647
+ // Header
648
+ lines.push(this.theme.bold(this.theme.fg("accent", " Phases")));
649
+ for (const id of visibleIds) {
650
+ const node = this.controller.getNode(id);
651
+ if (!node)
652
+ continue;
653
+ const isCursor = id === state.cursorId;
654
+ const depth = node.depth;
655
+ const indent = " ".repeat(depth * 2);
656
+ // Status glyph
657
+ const glyph = this.nodeGlyph(node);
658
+ // Progress label for orchestrators
659
+ let label = node.label;
660
+ if (node.kind === "orchestrator") {
661
+ const prog = this.controller.getSubtreeProgress(id);
662
+ label += ` ${prog.completed}/${prog.total}`;
663
+ }
664
+ // Expand indicator
665
+ const expandIndicator = node.kind === "orchestrator"
666
+ ? state.expanded.has(id)
667
+ ? " "
668
+ : "▸"
669
+ : " ";
670
+ // Combine
671
+ const prefix = `${indent}${isCursor ? "❯" : " "} ${glyph} ${expandIndicator} `;
672
+ const styled = isCursor
673
+ ? this.theme.bold(this.theme.fg("accent", `${prefix}${label}`))
674
+ : `${prefix}${label}`;
675
+ lines.push(truncateToWidth(styled, width));
676
+ }
677
+ return lines;
678
+ }
679
+ nodeGlyph(node) {
680
+ switch (node.status) {
681
+ case "completed":
682
+ return this.theme.fg("success", "✔");
683
+ case "running":
684
+ return this.theme.fg("accent", "●");
685
+ case "cancelling":
686
+ return this.theme.fg("warning", "⏳");
687
+ case "cancelled":
688
+ return this.theme.fg("muted", "⊘");
689
+ case "failed":
690
+ return this.theme.fg("error", "✗");
691
+ case "escalated":
692
+ return this.theme.fg("error", "▲");
693
+ case "pending":
694
+ default:
695
+ return this.theme.fg("dim", "○");
696
+ }
697
+ }
698
+ // ── Detail panel renderer ────────────────────────────────────────────────
699
+ renderDetailPanel(node, width) {
700
+ const lines = [];
701
+ const dim = (s) => this.theme.fg("dim", s);
702
+ const accent = (s) => this.theme.fg("accent", s);
703
+ const bold = (s) => this.theme.bold(s);
704
+ const success = (s) => this.theme.fg("success", s);
705
+ const warn = (s) => this.theme.fg("warning", s);
706
+ // ── Header: label + status (wrap long model strings) ──────────────────
707
+ const statusLabel = this.statusLabel(node.status);
708
+ const modelPart = node.model ? ` · ${node.provider ?? ""} ${node.model}` : "";
709
+ lines.push(...wrapLine(`${this.nodeGlyph(node)} ${bold(statusLabel)}${dim(modelPart)}`, width));
710
+ // ── Metrics line ────────────────────────────────────────────────
711
+ const metrics = this.formatMetrics(node);
712
+ if (metrics)
713
+ lines.push(...wrapLine(dim(metrics), width));
714
+ lines.push("");
715
+ // ── Orchestrator node: list children ─────────────────────────────
716
+ if (node.kind === "orchestrator" && node.children.length > 0) {
717
+ const children = this.controller.getChildren(node.id);
718
+ lines.push(...wrapLine(dim(bold(`Agents · ${children.length}`)), width));
719
+ for (const child of children) {
720
+ const cglyph = this.nodeGlyph(child);
721
+ const cmodel = child.model ? ` ${child.provider ?? ""} ${child.model}` : "";
722
+ const cmetrics = this.formatMetrics(child);
723
+ const cmetricsPart = cmetrics ? ` ${cmetrics}` : "";
724
+ lines.push(...wrapLine(` ${cglyph} ${child.label}${dim(cmodel)}${dim(cmetricsPart)}`, width));
725
+ }
726
+ lines.push("");
727
+ }
728
+ // ── Prompt preview (expandable) ─────────────────────────────────
729
+ if (node.promptPreview) {
730
+ const expandIcon = this.controller.getState().promptExpanded ? "▼" : "▶";
731
+ const lineCount = node.promptPreview.split("\n").length;
732
+ lines.push(...wrapLine(dim(`${expandIcon} Prompt · ${lineCount} lines · ⏎ expand`), width));
733
+ if (this.controller.getState().promptExpanded) {
734
+ for (const pline of node.promptPreview.split("\n").slice(0, 20)) {
735
+ // Indent wrapped prompt lines by 2 spaces
736
+ const wrapped = wrapLine(pline, Math.max(0, width - 4));
737
+ for (let i = 0; i < wrapped.length; i++) {
738
+ lines.push(dim(i === 0 ? ` ${wrapped[i]}` : ` ${wrapped[i]}`));
739
+ }
740
+ }
741
+ if (lineCount > 20) {
742
+ lines.push(dim(` … ${lineCount - 20} more lines`));
743
+ }
744
+ }
745
+ lines.push("");
746
+ }
747
+ // ── Activity: running full log of all turns ──────────────────────
748
+ if (node.kind === "leaf" && node.tailBuffer.length > 0) {
749
+ const total = node.tailBuffer.length;
750
+ lines.push(...wrapLine(dim(`Activity · ${total} log line${total === 1 ? "" : "s"}`), width));
751
+ for (const tline of node.tailBuffer) {
752
+ const painted = paintTailLine(tline, this.theme);
753
+ lines.push(...wrapLine(painted, width));
754
+ }
755
+ lines.push("");
756
+ }
757
+ // ── Outcome ─────────────────────────────────────────────────────
758
+ if (node.outcomePreview) {
759
+ lines.push(dim("Outcome"));
760
+ for (const oline of node.outcomePreview.split("\n").slice(0, 8)) {
761
+ const wrapped = wrapLine(oline, Math.max(0, width - 4));
762
+ for (const wl of wrapped) {
763
+ lines.push(` ${wl}`);
764
+ }
765
+ }
766
+ const lineCount = node.outcomePreview.split("\n").length;
767
+ if (lineCount > 8) {
768
+ lines.push(dim(` … ${lineCount - 8} more lines`));
769
+ }
770
+ }
771
+ return lines;
772
+ }
773
+ /** Render a cancel-confirmation dialog overlaid on the dashboard.
774
+ * The dashboard content stays visible above and below the dialog.
775
+ * The outer frame borders (│ on each side) are preserved across the
776
+ * dialog rows so the two-panel layout doesn't visually collapse.
777
+ * The dialog is centered and accepts y/Enter to confirm or n/Esc to
778
+ * dismiss. The overlay stays until the user decides — Esc only
779
+ * dismisses the cancel prompt, not the dashboard.
780
+ */
781
+ overlayCancelConfirm(baseLines, width, targetId) {
782
+ const node = this.controller.getNode(targetId);
783
+ const label = node?.label ?? targetId;
784
+ const dim = (s) => this.theme.fg("dim", s);
785
+ const warn = (s) => this.theme.fg("warning", s);
786
+ const bold = (s) => this.theme.bold(s);
787
+ const brd = (s) => this.theme.fg("border", s);
788
+ // Content width is the full width minus the two outer │ borders.
789
+ const contentWidth = width - 2;
790
+ // Build the dialog box (narrower than the full content area so it
791
+ // floats with padding on each side).
792
+ const dialogW = Math.min(contentWidth, 60);
793
+ const diagLines = [];
794
+ const prompt = warn(`⚠ Cancel ${bold(label)}?`);
795
+ const actions = dim("y confirm · n dismiss · esc close");
796
+ const promptW = visibleWidth(prompt);
797
+ const actionsW = visibleWidth(actions);
798
+ diagLines.push(brd("╭") + brd("─".repeat(dialogW - 2)) + brd("╮"));
799
+ diagLines.push(brd("│") + " ".repeat(dialogW - 2) + brd("│"));
800
+ {
801
+ // Place prompt and actions side by side when they fit; stack otherwise.
802
+ const sideBySide = promptW + actionsW + 4 <= dialogW - 2;
803
+ if (sideBySide) {
804
+ const contentGap = Math.max(1, dialogW - 4 - promptW - actionsW);
805
+ const contentLine = brd("│") + " " + prompt + " ".repeat(contentGap) + actions + " " + brd("│");
806
+ if (visibleWidth(contentLine) < dialogW) {
807
+ // Pad content line to full dialog width.
808
+ const pad = dialogW - 2 - visibleWidth(contentLine.slice(1, -1));
809
+ diagLines.push(brd("│") +
810
+ " " + prompt + " ".repeat(contentGap + Math.max(0, pad)) + actions +
811
+ " " + brd("│"));
812
+ }
813
+ else {
814
+ diagLines.push(contentLine);
815
+ }
816
+ }
817
+ else {
818
+ // Stacked: prompt on one line, actions on the next.
819
+ const promptPad = Math.max(0, dialogW - 2 - 1 - promptW - 1);
820
+ diagLines.push(brd("│") + " " + prompt + " ".repeat(promptPad) + brd("│"));
821
+ const actionsPad = Math.max(0, dialogW - 2 - 1 - actionsW - 1);
822
+ diagLines.push(brd("│") + " " + actions + " ".repeat(actionsPad) + brd("│"));
823
+ }
824
+ }
825
+ diagLines.push(brd("│") + " ".repeat(dialogW - 2) + brd("│"));
826
+ diagLines.push(brd("╰") + brd("─".repeat(dialogW - 2)) + brd("╯"));
827
+ // Center the dialog vertically within the content rows and
828
+ // horizontally within the content area, preserving the outer │
829
+ // borders on every row so the frame stays intact.
830
+ const totalLines = baseLines.length;
831
+ const diagH = diagLines.length;
832
+ const startRow = Math.max(0, Math.floor((totalLines - diagH) / 2));
833
+ const leftPad = Math.max(0, Math.floor((contentWidth - dialogW) / 2));
834
+ const result = [...baseLines];
835
+ for (let i = 0; i < diagH && startRow + i < result.length; i++) {
836
+ const diagLine = diagLines[i];
837
+ const diagVisW = visibleWidth(diagLine);
838
+ const rightPad = Math.max(0, contentWidth - leftPad - diagVisW);
839
+ // Preserve the outer │ borders: left border + centered dialog + right border.
840
+ result[startRow + i] =
841
+ brd("│") + " ".repeat(leftPad) + diagLine + " ".repeat(rightPad) + brd("│");
842
+ }
843
+ return result;
844
+ }
845
+ statusLabel(status) {
846
+ switch (status) {
847
+ case "completed":
848
+ return "Completed";
849
+ case "running":
850
+ return "Running";
851
+ case "cancelling":
852
+ return "Cancelling…";
853
+ case "cancelled":
854
+ return "Cancelled";
855
+ case "failed":
856
+ return "Failed";
857
+ case "escalated":
858
+ return "Escalated";
859
+ case "pending":
860
+ return "Pending";
861
+ }
862
+ }
863
+ formatMetrics(node) {
864
+ const parts = [];
865
+ if (node.usage.input || node.usage.output || node.usage.cacheRead) {
866
+ parts.push(fmtTokenMeter(node.usage));
867
+ }
868
+ if (node.metrics.toolCount) {
869
+ parts.push(`${node.metrics.toolCount} tool${node.metrics.toolCount === 1 ? "" : "s"}`);
870
+ }
871
+ if (node.metrics.errCount) {
872
+ parts.push(`${node.metrics.errCount} err`);
873
+ }
874
+ if (node.startedAt) {
875
+ const end = node.endedAt ?? Date.now();
876
+ const secs = Math.floor((end - node.startedAt) / 1000);
877
+ parts.push(secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m${String(secs % 60).padStart(2, "0")}s`);
878
+ }
879
+ return parts.join(" · ");
880
+ }
881
+ }
882
+ //# sourceMappingURL=component.js.map