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.
- package/README.md +205 -0
- package/agents/auditor.md +45 -0
- package/agents/worker.md +44 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +526 -0
- package/dist/index.js.map +1 -0
- package/dist/state.d.ts +265 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +474 -0
- package/dist/state.js.map +1 -0
- package/dist/tools/add-phase.d.ts +28 -0
- package/dist/tools/add-phase.d.ts.map +1 -0
- package/dist/tools/add-phase.js +69 -0
- package/dist/tools/add-phase.js.map +1 -0
- package/dist/tools/add-task.d.ts +30 -0
- package/dist/tools/add-task.d.ts.map +1 -0
- package/dist/tools/add-task.js +85 -0
- package/dist/tools/add-task.js.map +1 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +16 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/init.d.ts +34 -0
- package/dist/tools/init.d.ts.map +1 -0
- package/dist/tools/init.js +75 -0
- package/dist/tools/init.js.map +1 -0
- package/dist/tools/mission-complete.d.ts +30 -0
- package/dist/tools/mission-complete.d.ts.map +1 -0
- package/dist/tools/mission-complete.js +85 -0
- package/dist/tools/mission-complete.js.map +1 -0
- package/dist/tools/mission-resume.d.ts +35 -0
- package/dist/tools/mission-resume.d.ts.map +1 -0
- package/dist/tools/mission-resume.js +87 -0
- package/dist/tools/mission-resume.js.map +1 -0
- package/dist/tools/scaffold.d.ts +24 -0
- package/dist/tools/scaffold.d.ts.map +1 -0
- package/dist/tools/scaffold.js +129 -0
- package/dist/tools/scaffold.js.map +1 -0
- package/dist/tools/update-phase.d.ts +33 -0
- package/dist/tools/update-phase.d.ts.map +1 -0
- package/dist/tools/update-phase.js +101 -0
- package/dist/tools/update-phase.js.map +1 -0
- package/dist/tools/update-task.d.ts +34 -0
- package/dist/tools/update-task.d.ts.map +1 -0
- package/dist/tools/update-task.js +104 -0
- package/dist/tools/update-task.js.map +1 -0
- package/dist/tui/dashboard.d.ts +146 -0
- package/dist/tui/dashboard.d.ts.map +1 -0
- package/dist/tui/dashboard.js +381 -0
- package/dist/tui/dashboard.js.map +1 -0
- package/dist/tui/header.d.ts +39 -0
- package/dist/tui/header.d.ts.map +1 -0
- package/dist/tui/header.js +62 -0
- package/dist/tui/header.js.map +1 -0
- package/dist/tui/idle-view.d.ts +44 -0
- package/dist/tui/idle-view.d.ts.map +1 -0
- package/dist/tui/idle-view.js +87 -0
- package/dist/tui/idle-view.js.map +1 -0
- package/dist/tui/index.d.ts +13 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +15 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/past-runs.d.ts +49 -0
- package/dist/tui/past-runs.d.ts.map +1 -0
- package/dist/tui/past-runs.js +207 -0
- package/dist/tui/past-runs.js.map +1 -0
- package/dist/tui/phases-panel.d.ts +46 -0
- package/dist/tui/phases-panel.d.ts.map +1 -0
- package/dist/tui/phases-panel.js +161 -0
- package/dist/tui/phases-panel.js.map +1 -0
- package/dist/tui/progress-bar.d.ts +37 -0
- package/dist/tui/progress-bar.d.ts.map +1 -0
- package/dist/tui/progress-bar.js +123 -0
- package/dist/tui/progress-bar.js.map +1 -0
- package/dist/tui/styles.d.ts +8 -0
- package/dist/tui/styles.d.ts.map +1 -0
- package/dist/tui/styles.js +22 -0
- package/dist/tui/styles.js.map +1 -0
- package/dist/tui/tasks-panel.d.ts +48 -0
- package/dist/tui/tasks-panel.d.ts.map +1 -0
- package/dist/tui/tasks-panel.js +191 -0
- package/dist/tui/tasks-panel.js.map +1 -0
- package/package.json +42 -0
- package/skills/mission-memory/SKILL.md +88 -0
- package/skills/mission-orchestrator/SKILL.md +167 -0
- package/skills/mission-pm/SKILL.md +83 -0
- package/skills/mission-research/SKILL.md +66 -0
- package/skills/mission-tech-lead/SKILL.md +68 -0
- package/src/index.ts +659 -0
- package/src/state.ts +623 -0
- package/src/tools/add-phase.ts +98 -0
- package/src/tools/add-task.ts +121 -0
- package/src/tools/index.ts +18 -0
- package/src/tools/init.ts +109 -0
- package/src/tools/mission-complete.ts +118 -0
- package/src/tools/mission-resume.ts +119 -0
- package/src/tools/scaffold.ts +167 -0
- package/src/tools/update-phase.ts +140 -0
- package/src/tools/update-task.ts +145 -0
- package/src/tui/dashboard.ts +441 -0
- package/src/tui/header.ts +85 -0
- package/src/tui/idle-view.ts +114 -0
- package/src/tui/index.ts +20 -0
- package/src/tui/past-runs.ts +261 -0
- package/src/tui/phases-panel.ts +199 -0
- package/src/tui/progress-bar.ts +152 -0
- package/src/tui/styles.ts +27 -0
- package/src/tui/tasks-panel.ts +228 -0
- 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
|
+
}
|
package/src/tui/index.ts
ADDED
|
@@ -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";
|