pi-mission-control 0.0.0-dev

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 (110) hide show
  1. package/README.md +205 -0
  2. package/agents/auditor.md +45 -0
  3. package/agents/worker.md +44 -0
  4. package/dist/index.d.ts +9 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +526 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/state.d.ts +265 -0
  9. package/dist/state.d.ts.map +1 -0
  10. package/dist/state.js +474 -0
  11. package/dist/state.js.map +1 -0
  12. package/dist/tools/add-phase.d.ts +28 -0
  13. package/dist/tools/add-phase.d.ts.map +1 -0
  14. package/dist/tools/add-phase.js +69 -0
  15. package/dist/tools/add-phase.js.map +1 -0
  16. package/dist/tools/add-task.d.ts +30 -0
  17. package/dist/tools/add-task.d.ts.map +1 -0
  18. package/dist/tools/add-task.js +85 -0
  19. package/dist/tools/add-task.js.map +1 -0
  20. package/dist/tools/index.d.ts +13 -0
  21. package/dist/tools/index.d.ts.map +1 -0
  22. package/dist/tools/index.js +16 -0
  23. package/dist/tools/index.js.map +1 -0
  24. package/dist/tools/init.d.ts +34 -0
  25. package/dist/tools/init.d.ts.map +1 -0
  26. package/dist/tools/init.js +75 -0
  27. package/dist/tools/init.js.map +1 -0
  28. package/dist/tools/mission-complete.d.ts +30 -0
  29. package/dist/tools/mission-complete.d.ts.map +1 -0
  30. package/dist/tools/mission-complete.js +85 -0
  31. package/dist/tools/mission-complete.js.map +1 -0
  32. package/dist/tools/mission-resume.d.ts +35 -0
  33. package/dist/tools/mission-resume.d.ts.map +1 -0
  34. package/dist/tools/mission-resume.js +87 -0
  35. package/dist/tools/mission-resume.js.map +1 -0
  36. package/dist/tools/scaffold.d.ts +24 -0
  37. package/dist/tools/scaffold.d.ts.map +1 -0
  38. package/dist/tools/scaffold.js +129 -0
  39. package/dist/tools/scaffold.js.map +1 -0
  40. package/dist/tools/update-phase.d.ts +33 -0
  41. package/dist/tools/update-phase.d.ts.map +1 -0
  42. package/dist/tools/update-phase.js +101 -0
  43. package/dist/tools/update-phase.js.map +1 -0
  44. package/dist/tools/update-task.d.ts +34 -0
  45. package/dist/tools/update-task.d.ts.map +1 -0
  46. package/dist/tools/update-task.js +104 -0
  47. package/dist/tools/update-task.js.map +1 -0
  48. package/dist/tui/dashboard.d.ts +146 -0
  49. package/dist/tui/dashboard.d.ts.map +1 -0
  50. package/dist/tui/dashboard.js +381 -0
  51. package/dist/tui/dashboard.js.map +1 -0
  52. package/dist/tui/header.d.ts +39 -0
  53. package/dist/tui/header.d.ts.map +1 -0
  54. package/dist/tui/header.js +62 -0
  55. package/dist/tui/header.js.map +1 -0
  56. package/dist/tui/idle-view.d.ts +44 -0
  57. package/dist/tui/idle-view.d.ts.map +1 -0
  58. package/dist/tui/idle-view.js +87 -0
  59. package/dist/tui/idle-view.js.map +1 -0
  60. package/dist/tui/index.d.ts +13 -0
  61. package/dist/tui/index.d.ts.map +1 -0
  62. package/dist/tui/index.js +15 -0
  63. package/dist/tui/index.js.map +1 -0
  64. package/dist/tui/past-runs.d.ts +49 -0
  65. package/dist/tui/past-runs.d.ts.map +1 -0
  66. package/dist/tui/past-runs.js +207 -0
  67. package/dist/tui/past-runs.js.map +1 -0
  68. package/dist/tui/phases-panel.d.ts +46 -0
  69. package/dist/tui/phases-panel.d.ts.map +1 -0
  70. package/dist/tui/phases-panel.js +161 -0
  71. package/dist/tui/phases-panel.js.map +1 -0
  72. package/dist/tui/progress-bar.d.ts +37 -0
  73. package/dist/tui/progress-bar.d.ts.map +1 -0
  74. package/dist/tui/progress-bar.js +123 -0
  75. package/dist/tui/progress-bar.js.map +1 -0
  76. package/dist/tui/styles.d.ts +8 -0
  77. package/dist/tui/styles.d.ts.map +1 -0
  78. package/dist/tui/styles.js +22 -0
  79. package/dist/tui/styles.js.map +1 -0
  80. package/dist/tui/tasks-panel.d.ts +48 -0
  81. package/dist/tui/tasks-panel.d.ts.map +1 -0
  82. package/dist/tui/tasks-panel.js +191 -0
  83. package/dist/tui/tasks-panel.js.map +1 -0
  84. package/package.json +42 -0
  85. package/skills/mission-memory/SKILL.md +88 -0
  86. package/skills/mission-orchestrator/SKILL.md +167 -0
  87. package/skills/mission-pm/SKILL.md +83 -0
  88. package/skills/mission-research/SKILL.md +66 -0
  89. package/skills/mission-tech-lead/SKILL.md +68 -0
  90. package/src/index.ts +659 -0
  91. package/src/state.ts +623 -0
  92. package/src/tools/add-phase.ts +98 -0
  93. package/src/tools/add-task.ts +121 -0
  94. package/src/tools/index.ts +18 -0
  95. package/src/tools/init.ts +109 -0
  96. package/src/tools/mission-complete.ts +118 -0
  97. package/src/tools/mission-resume.ts +119 -0
  98. package/src/tools/scaffold.ts +167 -0
  99. package/src/tools/update-phase.ts +140 -0
  100. package/src/tools/update-task.ts +145 -0
  101. package/src/tui/dashboard.ts +441 -0
  102. package/src/tui/header.ts +85 -0
  103. package/src/tui/idle-view.ts +114 -0
  104. package/src/tui/index.ts +20 -0
  105. package/src/tui/past-runs.ts +261 -0
  106. package/src/tui/phases-panel.ts +199 -0
  107. package/src/tui/progress-bar.ts +152 -0
  108. package/src/tui/styles.ts +27 -0
  109. package/src/tui/tasks-panel.ts +228 -0
  110. package/templates/state.json +5 -0
@@ -0,0 +1,441 @@
1
+ /**
2
+ * Mission Control TUI Dashboard
3
+ *
4
+ * Main dashboard component that composes:
5
+ * - Header (run ID, timer, phase)
6
+ * - Phases panel (left)
7
+ * - Tasks panel (right)
8
+ * - Progress bar (bottom)
9
+ *
10
+ * Features:
11
+ * - Keyboard navigation (Up/Down/Left/Right)
12
+ * - Esc to close
13
+ * - Enter to init new mission when idle
14
+ * - Timed refresh for clock/file state
15
+ * - Overlay support via ctx.ui.custom(..., { overlay: true })
16
+ */
17
+
18
+ import type { Theme } from "@mariozechner/pi-coding-agent";
19
+ import type { TUI } from "@mariozechner/pi-tui";
20
+ import { matchesKey, Key, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
21
+ import type { Run, State, Phase, Task } from "../state.js";
22
+ import { readState, readRun, listRuns as listAllRuns, type Run as RunType } from "../state.js";
23
+ import { HeaderComponent, formatElapsedTime } from "./header.js";
24
+ import { PhasesPanelComponent } from "./phases-panel.js";
25
+ import { TasksPanelComponent } from "./tasks-panel.js";
26
+ import { ProgressBarComponent } from "./progress-bar.js";
27
+ import { IdleViewComponent } from "./idle-view.js";
28
+ import { PastRunsComponent, extractPastRuns } from "./past-runs.js";
29
+
30
+ // Panel focus states
31
+ type PanelFocus = "phases" | "tasks";
32
+ type DashboardMode = "idle" | "active";
33
+
34
+ export interface DashboardProps {
35
+ onClose: () => void;
36
+ onInitMission?: () => void;
37
+ }
38
+
39
+ /**
40
+ * Mission Control Dashboard Component
41
+ *
42
+ * Usage:
43
+ * ```typescript
44
+ * const handle = ctx.ui.custom(
45
+ * (tui, theme, keybindings, done) => new MissionDashboard({ onClose: done }),
46
+ * { overlay: true }
47
+ * );
48
+ * ```
49
+ */
50
+ export class MissionDashboard {
51
+ // Sub-components
52
+ private header: HeaderComponent;
53
+ private phasesPanel: PhasesPanelComponent;
54
+ private tasksPanel: TasksPanelComponent;
55
+ private progressBar: ProgressBarComponent;
56
+ private idleView: IdleViewComponent;
57
+ private pastRuns: PastRunsComponent;
58
+
59
+ // State
60
+ private mode: DashboardMode = "idle";
61
+ private run: Run | null = null;
62
+ private state: State | null = null;
63
+ private refreshInterval: NodeJS.Timeout | null = null;
64
+ private cachedWidth?: number;
65
+ private cachedLines?: string[];
66
+ private tui: TUI | null = null;
67
+ private theme: Theme | null = null;
68
+
69
+ // Callbacks
70
+ private onClose: () => void;
71
+ private onInitMission?: () => void;
72
+
73
+ constructor(props: DashboardProps) {
74
+ this.onClose = props.onClose;
75
+ this.onInitMission = props.onInitMission;
76
+
77
+ // Initialize sub-components
78
+ this.header = new HeaderComponent(null, "idle", true, true);
79
+ this.phasesPanel = new PhasesPanelComponent([], true);
80
+ this.tasksPanel = new TasksPanelComponent(null, false);
81
+ this.progressBar = new ProgressBarComponent(null, "");
82
+
83
+ this.idleView = new IdleViewComponent({
84
+ onInitMission: () => this.handleInitMission(),
85
+ onClose: () => this.onClose()
86
+ });
87
+
88
+ this.pastRuns = new PastRunsComponent();
89
+
90
+ // Initial data load
91
+ this.refreshData();
92
+ }
93
+
94
+ /**
95
+ * Start the refresh interval for clock/timer updates
96
+ */
97
+ startRefresh(tui: TUI): void {
98
+ this.tui = tui;
99
+
100
+ if (this.refreshInterval) {
101
+ clearInterval(this.refreshInterval);
102
+ }
103
+
104
+ // Refresh every second for timer updates
105
+ this.refreshInterval = setInterval(() => {
106
+ this.refreshData();
107
+ tui.requestRender();
108
+ }, 1000);
109
+ }
110
+
111
+ /**
112
+ * Stop the refresh interval
113
+ */
114
+ stopRefresh(): void {
115
+ if (this.refreshInterval) {
116
+ clearInterval(this.refreshInterval);
117
+ this.refreshInterval = null;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Refresh data from files
123
+ */
124
+ refreshData(): void {
125
+ // Read state
126
+ this.state = readState();
127
+
128
+ // Determine mode and load run data
129
+ if (this.state.active_run_id) {
130
+ this.mode = "active";
131
+ const runData = readRun(this.state.active_run_id);
132
+
133
+ if (runData) {
134
+ const previousSelectedPhaseId = this.phasesPanel.getSelectedPhase()?.id;
135
+
136
+ this.run = runData;
137
+ this.header.update(this.run, this.state.current_phase, true, true);
138
+ this.progressBar.update(this.run, this.state.current_status_message);
139
+
140
+ const selectedPhaseIndex = previousSelectedPhaseId
141
+ ? runData.phases.findIndex((phase) => phase.id === previousSelectedPhaseId)
142
+ : undefined;
143
+
144
+ if (selectedPhaseIndex !== undefined && selectedPhaseIndex >= 0) {
145
+ this.phasesPanel.update(runData.phases, selectedPhaseIndex);
146
+ } else {
147
+ this.phasesPanel.update(runData.phases);
148
+ }
149
+
150
+ const selectedPhase = this.phasesPanel.getSelectedPhase();
151
+ this.tasksPanel.update(selectedPhase ?? null);
152
+ }
153
+ } else {
154
+ this.mode = "idle";
155
+ this.run = null;
156
+ this.header.update(null, "idle", true, true);
157
+
158
+ // Update idle view with past runs
159
+ const allRuns = listAllRuns()
160
+ .filter(r => r.run !== null)
161
+ .map(r => r.run!);
162
+
163
+ this.idleView.update(allRuns, true, true);
164
+
165
+ const items = extractPastRuns(allRuns);
166
+ this.pastRuns.updateRuns(items);
167
+ }
168
+
169
+ this.invalidate();
170
+ }
171
+
172
+ /**
173
+ * Handle init mission action (Enter in idle mode)
174
+ */
175
+ private handleInitMission(): void {
176
+ if (this.onInitMission) {
177
+ this.onInitMission();
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Handle keyboard input
183
+ */
184
+ handleInput(data: string): void {
185
+ // Global keys
186
+ if (matchesKey(data, Key.escape)) {
187
+ this.onClose();
188
+ return;
189
+ }
190
+
191
+ const isEnter = matchesKey(data, Key.enter);
192
+ const isUp = matchesKey(data, Key.up);
193
+ const isDown = matchesKey(data, Key.down);
194
+ const isLeft = matchesKey(data, Key.left);
195
+ const isRight = matchesKey(data, Key.right);
196
+
197
+ if (this.mode === "idle") {
198
+ // Idle mode: pass to idle view
199
+ this.idleView.handleInput(data, isEnter, false, isUp, isDown);
200
+ this.requestRender();
201
+ } else {
202
+ // Active mode: handle panel navigation
203
+ if (isLeft) {
204
+ this.phasesPanel.setFocused(true);
205
+ this.tasksPanel.setFocused(false);
206
+ this.requestRender();
207
+ } else if (isRight) {
208
+ this.phasesPanel.setFocused(false);
209
+ this.tasksPanel.setFocused(true);
210
+ this.requestRender();
211
+ } else if (isUp) {
212
+ if (this.phasesPanel.isFocused()) {
213
+ this.phasesPanel.navigateUp();
214
+ // Update tasks panel to show tasks for selected phase
215
+ const phase = this.phasesPanel.getSelectedPhase();
216
+ this.tasksPanel.update(phase ?? null);
217
+ } else {
218
+ this.tasksPanel.navigateUp();
219
+ }
220
+ this.requestRender();
221
+ } else if (isDown) {
222
+ if (this.phasesPanel.isFocused()) {
223
+ this.phasesPanel.navigateDown();
224
+ // Update tasks panel to show tasks for selected phase
225
+ const phase = this.phasesPanel.getSelectedPhase();
226
+ this.tasksPanel.update(phase ?? null);
227
+ } else {
228
+ this.tasksPanel.navigateDown();
229
+ }
230
+ this.requestRender();
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Request a re-render from TUI
237
+ */
238
+ private requestRender(): void {
239
+ if (this.tui) {
240
+ this.tui.requestRender();
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Render the dashboard
246
+ */
247
+ render(width: number, theme: Theme): string[] {
248
+ this.theme = theme;
249
+
250
+ // Use cached if dimensions match
251
+ if (this.cachedLines && this.cachedWidth === width) {
252
+ return this.cachedLines;
253
+ }
254
+
255
+ if (this.mode === "idle") {
256
+ this.cachedLines = this.renderIdle(width, theme);
257
+ } else {
258
+ this.cachedLines = this.renderActive(width, theme);
259
+ }
260
+
261
+ this.cachedWidth = width;
262
+ return this.cachedLines;
263
+ }
264
+
265
+ private renderShortcutLegend(width: number, theme: Theme, mode: DashboardMode): string[] {
266
+ const items = mode === "idle"
267
+ ? [
268
+ { key: "[ ENTER ]", label: "Init Mission" },
269
+ { key: "[ ↑/↓ ]", label: "Past Runs" },
270
+ { key: "[ ←/→ ]", label: "Panels" },
271
+ { key: "[ ESC ]", label: "Close" },
272
+ ]
273
+ : [
274
+ { key: "[ ↑/↓ ]", label: "Lists" },
275
+ { key: "[ ←/→ ]", label: "Panels" },
276
+ { key: "[ ESC ]", label: "Close" },
277
+ ];
278
+
279
+ const gap = 2;
280
+ const cellWidth = Math.max(12, Math.floor((width - gap * (items.length - 1)) / items.length));
281
+ const formatCell = (text: string) => text.padEnd(cellWidth, " ");
282
+ const keysLine = items
283
+ .map((item) => theme.fg("text", theme.bold(formatCell(item.key))))
284
+ .join(" ".repeat(gap));
285
+ const labelsLine = items
286
+ .map((item) => theme.fg("dim", formatCell(item.label)))
287
+ .join(" ".repeat(gap));
288
+
289
+ return [truncateToWidth(keysLine, width), truncateToWidth(labelsLine, width)];
290
+ }
291
+
292
+ /**
293
+ * Render idle mode
294
+ */
295
+ private renderIdle(width: number, theme: Theme): string[] {
296
+ const contentHeight = 12;
297
+ const lines: string[] = [];
298
+
299
+ lines.push(...this.header.render(width, theme));
300
+ lines.push(theme.fg("border", "─".repeat(width)));
301
+ lines.push(...this.renderShortcutLegend(width, theme, "idle"));
302
+ lines.push(theme.fg("dim", "Elapsed: --:--:--"));
303
+ lines.push("");
304
+ lines.push(...this.idleView.render(width, contentHeight, theme));
305
+ lines.push("");
306
+ lines.push(...this.progressBar.render(width, theme));
307
+
308
+ return lines;
309
+ }
310
+
311
+ /**
312
+ * Render active mode with a simple 40/60 split
313
+ */
314
+ private renderActive(width: number, theme: Theme): string[] {
315
+ const lines: string[] = [];
316
+ const contentHeight = 12;
317
+
318
+ lines.push(...this.header.render(width, theme));
319
+ lines.push(theme.fg("border", "─".repeat(width)));
320
+ lines.push(...this.renderShortcutLegend(width, theme, "active"));
321
+ lines.push(theme.fg("dim", `Elapsed: ${this.run ? formatElapsedTime(this.run.started_at) : "--:--:--"}`));
322
+ lines.push("");
323
+
324
+ const gap = 4;
325
+ const availableWidth = Math.max(16, width - gap);
326
+ let leftWidth: number;
327
+ let rightWidth: number;
328
+
329
+ if (availableWidth <= 44) {
330
+ leftWidth = Math.max(8, Math.floor(availableWidth * 0.4));
331
+ rightWidth = Math.max(8, availableWidth - leftWidth);
332
+ } else {
333
+ leftWidth = Math.floor(availableWidth * 0.4);
334
+ rightWidth = availableWidth - leftWidth;
335
+ }
336
+
337
+ const phasesLines = this.phasesPanel.render(leftWidth, contentHeight, theme);
338
+ const tasksLines = this.tasksPanel.render(rightWidth, contentHeight, theme);
339
+
340
+ for (let i = 0; i < contentHeight; i++) {
341
+ const leftLine = phasesLines[i] ?? "";
342
+ const rightLine = tasksLines[i] ?? "";
343
+ const leftPadding = Math.max(0, leftWidth - visibleWidth(leftLine));
344
+ lines.push(truncateToWidth(leftLine + " ".repeat(leftPadding + gap) + rightLine, width));
345
+ }
346
+
347
+ lines.push("");
348
+ lines.push(...this.progressBar.render(width, theme));
349
+ return lines;
350
+ }
351
+
352
+ /**
353
+ * Invalidate cached render output
354
+ */
355
+ invalidate(): void {
356
+ this.cachedWidth = undefined;
357
+ this.cachedLines = undefined;
358
+
359
+ this.header.invalidate();
360
+ this.phasesPanel.invalidate();
361
+ this.tasksPanel.invalidate();
362
+ this.progressBar.invalidate();
363
+ this.idleView.invalidate();
364
+ this.pastRuns.invalidate();
365
+ }
366
+
367
+ /**
368
+ * Get current mode
369
+ */
370
+ getMode(): DashboardMode {
371
+ return this.mode;
372
+ }
373
+
374
+ /**
375
+ * Get active run (if any)
376
+ */
377
+ getRun(): Run | null {
378
+ return this.run;
379
+ }
380
+
381
+ /**
382
+ * Dispose of resources
383
+ */
384
+ dispose(): void {
385
+ this.stopRefresh();
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Open the Mission Control dashboard
391
+ *
392
+ * Usage in extension:
393
+ * ```typescript
394
+ * pi.registerCommand("mission", {
395
+ * handler: async (_args, ctx) => {
396
+ * const result = await ctx.ui.custom<string | null>(
397
+ * (tui, theme, _kb, done) => {
398
+ * const dashboard = new MissionDashboard({
399
+ * onClose: () => done(null),
400
+ * onInitMission: () => {
401
+ * // Trigger mission_init tool
402
+ * done("init");
403
+ * }
404
+ * });
405
+ * dashboard.startRefresh(tui);
406
+ * return dashboard;
407
+ * },
408
+ * { overlay: true }
409
+ * );
410
+ *
411
+ * if (result === "init") {
412
+ * // Handle init action
413
+ * }
414
+ * }
415
+ * });
416
+ * ```
417
+ */
418
+ export async function openMissionControl(
419
+ ctx: { ui: { custom: (factory: (tui: TUI, theme: Theme, keybindings: unknown, done: (result: string | null) => void) => MissionDashboard, options: { overlay: boolean }) => Promise<string | null>; notify: (message: string, type: "info" | "warning" | "error") => void } }
420
+ ): Promise<void> {
421
+ await ctx.ui.custom(
422
+ (tui: TUI, theme: Theme, _keybindings: unknown, done: (result: string | null) => void) => {
423
+ const dashboard = new MissionDashboard({
424
+ onClose: () => {
425
+ dashboard.dispose();
426
+ done(null);
427
+ },
428
+ onInitMission: () => {
429
+ dashboard.dispose();
430
+ done("init");
431
+ }
432
+ });
433
+
434
+ dashboard.startRefresh(tui);
435
+ return dashboard;
436
+ },
437
+ { overlay: true }
438
+ );
439
+ }
440
+
441
+ export default MissionDashboard;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Mission Control TUI Header Component
3
+ *
4
+ * Displays:
5
+ * - Title with icon
6
+ * - Run ID
7
+ * - Mission timer (elapsed time from run.started_at)
8
+ * - Compact legend for keyboard navigation
9
+ */
10
+
11
+ import type { Theme } from "@mariozechner/pi-coding-agent";
12
+ import type { Run } from "../state.js";
13
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
14
+
15
+ export interface HeaderProps {
16
+ run: Run | null;
17
+ currentPhase: string;
18
+ agentsReady: boolean;
19
+ skillsReady: boolean;
20
+ }
21
+
22
+ /**
23
+ * Format elapsed time as HH:MM:SS
24
+ */
25
+ export function formatElapsedTime(startedAt: string): string {
26
+ const start = new Date(startedAt).getTime();
27
+ const now = Date.now();
28
+ const elapsed = Math.floor((now - start) / 1000);
29
+
30
+ const hours = Math.floor(elapsed / 3600);
31
+ const minutes = Math.floor((elapsed % 3600) / 60);
32
+ const seconds = elapsed % 60;
33
+
34
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
35
+ }
36
+
37
+ /**
38
+ * Render the header component
39
+ * Returns an array of strings (one per line)
40
+ */
41
+ export function renderHeader(
42
+ width: number,
43
+ props: HeaderProps,
44
+ theme: Theme
45
+ ): string[] {
46
+ const title = theme.fg("accent", theme.bold("Mission Control"));
47
+ const agents = props.agentsReady ? "[√] Agents" : "[ ] Agents";
48
+ const skills = props.skillsReady ? "[√] Skills" : "[ ] Skills";
49
+ const status = `${theme.fg(props.agentsReady ? "success" : "muted", agents)} ${theme.fg(props.skillsReady ? "success" : "muted", skills)}`;
50
+ const padding = Math.max(1, width - visibleWidth(title) - visibleWidth(status));
51
+ return [truncateToWidth(title + " ".repeat(padding) + status, width)];
52
+ }
53
+
54
+ /**
55
+ * Header component class for dashboard integration
56
+ */
57
+ export class HeaderComponent {
58
+ private props: HeaderProps;
59
+ private cachedWidth?: number;
60
+ private cachedLines?: string[];
61
+
62
+ constructor(run: Run | null, currentPhase: string, agentsReady = true, skillsReady = true) {
63
+ this.props = { run, currentPhase, agentsReady, skillsReady };
64
+ }
65
+
66
+ update(run: Run | null, currentPhase: string, agentsReady = true, skillsReady = true): void {
67
+ this.props = { run, currentPhase, agentsReady, skillsReady };
68
+ this.invalidate();
69
+ }
70
+
71
+ render(width: number, theme: Theme): string[] {
72
+ if (this.cachedLines && this.cachedWidth === width) {
73
+ return this.cachedLines;
74
+ }
75
+
76
+ this.cachedLines = renderHeader(width, this.props, theme);
77
+ this.cachedWidth = width;
78
+ return this.cachedLines;
79
+ }
80
+
81
+ invalidate(): void {
82
+ this.cachedWidth = undefined;
83
+ this.cachedLines = undefined;
84
+ }
85
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Mission Control TUI Idle View
3
+ *
4
+ * Displays when no mission is active:
5
+ * - Agents/Skills readiness
6
+ * - Past runs list (if any exist)
7
+ *
8
+ * Keyboard:
9
+ * - Enter: Trigger init new mission
10
+ * - Esc: Close view
11
+ * - Up/Down: Navigate past runs
12
+ */
13
+
14
+ import type { Theme } from "@mariozechner/pi-coding-agent";
15
+ import type { Run } from "../state.js";
16
+ import { PastRunsComponent, extractPastRuns } from "./past-runs.js";
17
+ import { truncateToWidth } from "@mariozechner/pi-tui";
18
+
19
+ export interface IdleViewProps {
20
+ pastRuns: Run[];
21
+ agentsReady: boolean;
22
+ skillsReady: boolean;
23
+ }
24
+
25
+ export interface IdleViewCallbacks {
26
+ onInitMission: () => void;
27
+ onClose: () => void;
28
+ }
29
+
30
+ /**
31
+ * Render the idle view
32
+ */
33
+ export function renderIdleView(
34
+ width: number,
35
+ height: number,
36
+ props: IdleViewProps,
37
+ callbacks: IdleViewCallbacks,
38
+ theme: Theme
39
+ ): string[] {
40
+ void props;
41
+ void callbacks;
42
+ return new Array(height).fill("").map((_line, index) => (index === 0 ? truncateToWidth(theme.fg("dim", "No active mission"), width) : ""));
43
+ }
44
+
45
+ /**
46
+ * Idle view component class
47
+ *
48
+ * Composes PastRunsComponent and adds the idle-specific UI
49
+ */
50
+ export class IdleViewComponent {
51
+ private pastRuns: PastRunsComponent;
52
+ private props: { agentsReady: boolean; skillsReady: boolean };
53
+ private callbacks: IdleViewCallbacks;
54
+ private showRuns: boolean = false;
55
+
56
+ constructor(callbacks: IdleViewCallbacks) {
57
+ this.callbacks = callbacks;
58
+ this.pastRuns = new PastRunsComponent();
59
+ this.props = { agentsReady: true, skillsReady: true };
60
+ }
61
+
62
+ update(pastRuns: Run[], agentsReady = true, skillsReady = true): void {
63
+ this.props.agentsReady = agentsReady;
64
+ this.props.skillsReady = skillsReady;
65
+
66
+ const items = extractPastRuns(pastRuns);
67
+ this.pastRuns.updateRuns(items);
68
+ this.showRuns = items.length > 0;
69
+ }
70
+
71
+ handleInput(data: string, isEnter: boolean, isEscape: boolean, isUp: boolean, isDown: boolean): void {
72
+ if (isEnter) {
73
+ this.callbacks.onInitMission();
74
+ } else if (isEscape) {
75
+ this.callbacks.onClose();
76
+ } else if (this.showRuns) {
77
+ if (isUp) {
78
+ this.pastRuns.navigateUp();
79
+ } else if (isDown) {
80
+ this.pastRuns.navigateDown();
81
+ }
82
+ }
83
+ }
84
+
85
+ render(width: number, height: number, theme: Theme): string[] {
86
+ const lines: string[] = [];
87
+ const { agentsReady, skillsReady } = this.props;
88
+
89
+ const readiness = agentsReady && skillsReady
90
+ ? theme.fg("dim", "Ready to initialize a mission")
91
+ : theme.fg("muted", "Waiting for Mission Control resources");
92
+ lines.push(truncateToWidth(readiness, width));
93
+ lines.push("");
94
+
95
+ if (this.showRuns) {
96
+ const runsHeight = Math.min(this.pastRuns['props'].runs.length + 4, Math.max(4, height - 2));
97
+ const runsLines = this.pastRuns.render(width, runsHeight, theme);
98
+ lines.push(...runsLines);
99
+ } else {
100
+ lines.push(truncateToWidth(theme.fg("muted", "No past runs yet."), width));
101
+ }
102
+
103
+ // Pad to height
104
+ while (lines.length < height) {
105
+ lines.push("");
106
+ }
107
+
108
+ return lines.slice(0, height);
109
+ }
110
+
111
+ invalidate(): void {
112
+ this.pastRuns.invalidate();
113
+ }
114
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Mission Control TUI Components
3
+ *
4
+ * Exports all TUI components for the Mission Control dashboard.
5
+ */
6
+
7
+ // Individual components
8
+ export { HeaderComponent, renderHeader, formatElapsedTime } from "./header.js";
9
+ export { PhasesPanelComponent, renderPhasesPanel } from "./phases-panel.js";
10
+ export { TasksPanelComponent, renderTasksPanel } from "./tasks-panel.js";
11
+ export { ProgressBarComponent, renderProgressBarComponent } from "./progress-bar.js";
12
+ export { PastRunsComponent, renderPastRuns, extractPastRuns } from "./past-runs.js";
13
+ export { IdleViewComponent, renderIdleView } from "./idle-view.js";
14
+
15
+ // Main dashboard
16
+ export {
17
+ MissionDashboard,
18
+ openMissionControl,
19
+ type DashboardProps
20
+ } from "./dashboard.js";