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,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission Control TUI Past Runs List
|
|
3
|
+
*
|
|
4
|
+
* Displays archived/completed runs in idle state:
|
|
5
|
+
* - Run ID with date
|
|
6
|
+
* - Status color
|
|
7
|
+
* - Completion percentage
|
|
8
|
+
* - Keyboard selection
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import type { Run, RunStatus } from "../state.js";
|
|
13
|
+
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
14
|
+
import { missionSuccess, missionWarning } from "./styles.js";
|
|
15
|
+
|
|
16
|
+
const ICON_SELECTED = "> ";
|
|
17
|
+
const ICON_UNSELECTED = " ";
|
|
18
|
+
|
|
19
|
+
export interface PastRunItem {
|
|
20
|
+
runId: string;
|
|
21
|
+
status: RunStatus;
|
|
22
|
+
startedAt: string;
|
|
23
|
+
completedTasks: number;
|
|
24
|
+
totalTasks: number;
|
|
25
|
+
durationSeconds?: number; // Total duration if run finished
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PastRunsProps {
|
|
29
|
+
runs: PastRunItem[];
|
|
30
|
+
selectedIndex: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Apply Mission Control status colors for past runs.
|
|
35
|
+
*/
|
|
36
|
+
function styleStatus(status: RunStatus, text: string, theme: Theme): string {
|
|
37
|
+
switch (status) {
|
|
38
|
+
case "done": return missionSuccess(text);
|
|
39
|
+
case "failed": return theme.fg("error", text);
|
|
40
|
+
case "in_progress": return missionWarning(text);
|
|
41
|
+
case "paused": return missionWarning(text);
|
|
42
|
+
default: return theme.fg("muted", text);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format date as numeric Y-M-D 24h format
|
|
48
|
+
*/
|
|
49
|
+
function formatDate(isoDate: string): string {
|
|
50
|
+
try {
|
|
51
|
+
const date = new Date(isoDate);
|
|
52
|
+
const y = date.getFullYear();
|
|
53
|
+
const m = (date.getMonth() + 1).toString().padStart(2, "0");
|
|
54
|
+
const d = date.getDate().toString().padStart(2, "0");
|
|
55
|
+
const h = date.getHours().toString().padStart(2, "0");
|
|
56
|
+
const min = date.getMinutes().toString().padStart(2, "0");
|
|
57
|
+
return `${y}-${m}-${d} ${h}:${min}`;
|
|
58
|
+
} catch {
|
|
59
|
+
return isoDate;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Format duration in HH:MM:SS
|
|
65
|
+
*/
|
|
66
|
+
function formatDuration(seconds: number): string {
|
|
67
|
+
const h = Math.floor(seconds / 3600).toString().padStart(2, "0");
|
|
68
|
+
const m = Math.floor((seconds % 3600) / 60).toString().padStart(2, "0");
|
|
69
|
+
const s = (seconds % 60).toString().padStart(2, "0");
|
|
70
|
+
return `${h}:${m}:${s}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extract runs from Run objects
|
|
75
|
+
*/
|
|
76
|
+
export function extractPastRuns(runs: Run[]): PastRunItem[] {
|
|
77
|
+
return runs.map(run => {
|
|
78
|
+
let completed = 0;
|
|
79
|
+
let total = 0;
|
|
80
|
+
|
|
81
|
+
for (const phase of run.phases) {
|
|
82
|
+
for (const task of phase.tasks) {
|
|
83
|
+
if (task.status === "removed") continue;
|
|
84
|
+
total++;
|
|
85
|
+
if (task.status === "done") {
|
|
86
|
+
completed++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Calculate duration if run has finished
|
|
92
|
+
let durationSeconds: number | undefined;
|
|
93
|
+
if (run.finish_at) {
|
|
94
|
+
const start = new Date(run.started_at).getTime();
|
|
95
|
+
const end = new Date(run.finish_at).getTime();
|
|
96
|
+
durationSeconds = Math.round((end - start) / 1000);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
runId: run.run_id,
|
|
101
|
+
status: run.status,
|
|
102
|
+
startedAt: run.started_at,
|
|
103
|
+
completedTasks: completed,
|
|
104
|
+
totalTasks: total,
|
|
105
|
+
durationSeconds
|
|
106
|
+
};
|
|
107
|
+
}).sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Render a single past run line
|
|
112
|
+
* Format: "> run-id (YYYY-MM-DD HH:MM) [duration] 45% (5/11)"
|
|
113
|
+
*/
|
|
114
|
+
function renderPastRunLine(
|
|
115
|
+
run: PastRunItem,
|
|
116
|
+
index: number,
|
|
117
|
+
selectedIndex: number,
|
|
118
|
+
width: number,
|
|
119
|
+
theme: Theme
|
|
120
|
+
): string {
|
|
121
|
+
const isSelected = index === selectedIndex;
|
|
122
|
+
|
|
123
|
+
const date = formatDate(run.startedAt);
|
|
124
|
+
const percent = run.totalTasks > 0
|
|
125
|
+
? Math.round((run.completedTasks / run.totalTasks) * 100)
|
|
126
|
+
: 0;
|
|
127
|
+
|
|
128
|
+
// Include duration if available
|
|
129
|
+
const durationStr = run.durationSeconds ? ` [${formatDuration(run.durationSeconds)}]` : "";
|
|
130
|
+
|
|
131
|
+
const selectIndicator = isSelected ? ICON_SELECTED : ICON_UNSELECTED;
|
|
132
|
+
const baseText = `${selectIndicator}${run.runId} (${date})${durationStr} ${percent}% (${run.completedTasks}/${run.totalTasks})`;
|
|
133
|
+
|
|
134
|
+
// Apply styling
|
|
135
|
+
let styled: string;
|
|
136
|
+
if (isSelected) {
|
|
137
|
+
styled = theme.fg("text", theme.bold(baseText));
|
|
138
|
+
} else {
|
|
139
|
+
styled = styleStatus(run.status, baseText, theme);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return truncateToWidth(styled, width);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Render the past runs list
|
|
147
|
+
*/
|
|
148
|
+
export function renderPastRuns(
|
|
149
|
+
width: number,
|
|
150
|
+
height: number,
|
|
151
|
+
props: PastRunsProps,
|
|
152
|
+
theme: Theme
|
|
153
|
+
): string[] {
|
|
154
|
+
const { runs, selectedIndex } = props;
|
|
155
|
+
const lines: string[] = [];
|
|
156
|
+
|
|
157
|
+
// Header
|
|
158
|
+
const header = theme.fg("text", theme.bold("PAST RUNS"));
|
|
159
|
+
lines.push(truncateToWidth(header, width));
|
|
160
|
+
lines.push(""); // spacer
|
|
161
|
+
|
|
162
|
+
if (runs.length === 0) {
|
|
163
|
+
const emptyMsg = theme.fg("muted", " No past runs");
|
|
164
|
+
lines.push(truncateToWidth(emptyMsg, width));
|
|
165
|
+
} else {
|
|
166
|
+
// Calculate visible range
|
|
167
|
+
const availableHeight = height - 2;
|
|
168
|
+
let startIdx = 0;
|
|
169
|
+
let endIdx = runs.length;
|
|
170
|
+
|
|
171
|
+
if (runs.length > availableHeight) {
|
|
172
|
+
const halfHeight = Math.floor(availableHeight / 2);
|
|
173
|
+
startIdx = Math.max(0, selectedIndex - halfHeight);
|
|
174
|
+
endIdx = Math.min(runs.length, startIdx + availableHeight);
|
|
175
|
+
|
|
176
|
+
if (endIdx - startIdx < availableHeight) {
|
|
177
|
+
startIdx = Math.max(0, endIdx - availableHeight);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Render visible runs
|
|
182
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
183
|
+
const run = runs[i];
|
|
184
|
+
const line = renderPastRunLine(run, i, selectedIndex, width, theme);
|
|
185
|
+
lines.push(line);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Pad to height
|
|
190
|
+
while (lines.length < height) {
|
|
191
|
+
lines.push("");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return lines.slice(0, height);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Past runs component class
|
|
199
|
+
*/
|
|
200
|
+
export class PastRunsComponent {
|
|
201
|
+
private props: PastRunsProps;
|
|
202
|
+
private cachedWidth?: number;
|
|
203
|
+
private cachedHeight?: number;
|
|
204
|
+
private cachedLines?: string[];
|
|
205
|
+
|
|
206
|
+
constructor(runs: PastRunItem[] = []) {
|
|
207
|
+
this.props = {
|
|
208
|
+
runs,
|
|
209
|
+
selectedIndex: 0
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
updateRuns(runs: PastRunItem[], selectedIndex?: number): void {
|
|
214
|
+
this.props.runs = runs;
|
|
215
|
+
if (selectedIndex !== undefined) {
|
|
216
|
+
this.props.selectedIndex = Math.max(0, Math.min(selectedIndex, runs.length - 1));
|
|
217
|
+
} else if (this.props.selectedIndex >= runs.length && runs.length > 0) {
|
|
218
|
+
this.props.selectedIndex = runs.length - 1;
|
|
219
|
+
}
|
|
220
|
+
this.invalidate();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getSelectedIndex(): number {
|
|
224
|
+
return this.props.selectedIndex;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getSelectedRun(): PastRunItem | undefined {
|
|
228
|
+
return this.props.runs[this.props.selectedIndex];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
navigateUp(): void {
|
|
232
|
+
if (this.props.selectedIndex > 0) {
|
|
233
|
+
this.props.selectedIndex--;
|
|
234
|
+
this.invalidate();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
navigateDown(): void {
|
|
239
|
+
if (this.props.selectedIndex < this.props.runs.length - 1) {
|
|
240
|
+
this.props.selectedIndex++;
|
|
241
|
+
this.invalidate();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
render(width: number, height: number, theme: Theme): string[] {
|
|
246
|
+
if (this.cachedLines && this.cachedWidth === width && this.cachedHeight === height) {
|
|
247
|
+
return this.cachedLines;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.cachedLines = renderPastRuns(width, height, this.props, theme);
|
|
251
|
+
this.cachedWidth = width;
|
|
252
|
+
this.cachedHeight = height;
|
|
253
|
+
return this.cachedLines;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
invalidate(): void {
|
|
257
|
+
this.cachedWidth = undefined;
|
|
258
|
+
this.cachedHeight = undefined;
|
|
259
|
+
this.cachedLines = undefined;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission Control TUI Phases Panel
|
|
3
|
+
*
|
|
4
|
+
* Left panel displaying:
|
|
5
|
+
* - List of phases with status icons
|
|
6
|
+
* - Navigation indicator (arrow) for selected phase
|
|
7
|
+
* - Keyboard navigation support
|
|
8
|
+
*
|
|
9
|
+
* Icons (Nerd Font):
|
|
10
|
+
* - Done: \uf058 () check
|
|
11
|
+
* - In Progress: \uf192 () dot-circle
|
|
12
|
+
* - Pending: \uf096 () empty box
|
|
13
|
+
* - Selected: \uf054 () arrow
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
17
|
+
import type { Phase } from "../state.js";
|
|
18
|
+
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
19
|
+
import { missionSuccess, missionWarning, strike } from "./styles.js";
|
|
20
|
+
|
|
21
|
+
export interface PhasesPanelProps {
|
|
22
|
+
phases: Phase[];
|
|
23
|
+
selectedIndex: number;
|
|
24
|
+
focused: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render a single phase line
|
|
29
|
+
* Styling: selected=white bold, running=orange/warning, unselected non-running=gray/muted
|
|
30
|
+
*/
|
|
31
|
+
function renderPhaseLine(
|
|
32
|
+
phase: Phase,
|
|
33
|
+
index: number,
|
|
34
|
+
selectedIndex: number,
|
|
35
|
+
width: number,
|
|
36
|
+
theme: Theme,
|
|
37
|
+
focused: boolean
|
|
38
|
+
): string {
|
|
39
|
+
const isSelected = index === selectedIndex;
|
|
40
|
+
const isRunning = phase.status === "in_progress";
|
|
41
|
+
const baseText = `Phase ${index + 1}: ${phase.name}`;
|
|
42
|
+
|
|
43
|
+
let styled: string;
|
|
44
|
+
if (phase.status === "removed") {
|
|
45
|
+
const removedText = strike(baseText);
|
|
46
|
+
styled = isSelected && focused
|
|
47
|
+
? theme.fg("text", theme.bold(removedText))
|
|
48
|
+
: theme.fg("muted", removedText);
|
|
49
|
+
} else if (isSelected && focused) {
|
|
50
|
+
styled = theme.fg("text", theme.bold(baseText));
|
|
51
|
+
} else if (isRunning) {
|
|
52
|
+
styled = missionWarning(baseText);
|
|
53
|
+
} else if (phase.status === "done") {
|
|
54
|
+
styled = missionSuccess(baseText);
|
|
55
|
+
} else {
|
|
56
|
+
styled = theme.fg("muted", baseText);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Truncate to fit
|
|
60
|
+
return truncateToWidth(styled, width);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Render the phases panel
|
|
65
|
+
*/
|
|
66
|
+
export function renderPhasesPanel(
|
|
67
|
+
width: number,
|
|
68
|
+
height: number,
|
|
69
|
+
props: PhasesPanelProps,
|
|
70
|
+
theme: Theme
|
|
71
|
+
): string[] {
|
|
72
|
+
const { phases, selectedIndex, focused } = props;
|
|
73
|
+
const lines: string[] = [];
|
|
74
|
+
|
|
75
|
+
// Header
|
|
76
|
+
const header = theme.fg("text", theme.bold("PHASES"));
|
|
77
|
+
lines.push(truncateToWidth(header, width));
|
|
78
|
+
|
|
79
|
+
if (phases.length === 0) {
|
|
80
|
+
const emptyMsg = theme.fg("muted", " No phases yet");
|
|
81
|
+
lines.push(truncateToWidth(emptyMsg, width));
|
|
82
|
+
} else {
|
|
83
|
+
// Calculate visible range if list is taller than available height
|
|
84
|
+
const availableHeight = height - 1;
|
|
85
|
+
let startIdx = 0;
|
|
86
|
+
let endIdx = phases.length;
|
|
87
|
+
|
|
88
|
+
if (phases.length > availableHeight) {
|
|
89
|
+
// Scroll to keep selected item visible
|
|
90
|
+
const halfHeight = Math.floor(availableHeight / 2);
|
|
91
|
+
startIdx = Math.max(0, selectedIndex - halfHeight);
|
|
92
|
+
endIdx = Math.min(phases.length, startIdx + availableHeight);
|
|
93
|
+
|
|
94
|
+
// Adjust start if we're near the end
|
|
95
|
+
if (endIdx - startIdx < availableHeight) {
|
|
96
|
+
startIdx = Math.max(0, endIdx - availableHeight);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Render visible phases
|
|
101
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
102
|
+
const phase = phases[i];
|
|
103
|
+
const line = renderPhaseLine(phase, i, selectedIndex, width, theme, focused);
|
|
104
|
+
lines.push(line);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Pad to height
|
|
109
|
+
while (lines.length < height) {
|
|
110
|
+
lines.push("");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return lines.slice(0, height);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Phases panel component class
|
|
118
|
+
*/
|
|
119
|
+
export class PhasesPanelComponent {
|
|
120
|
+
private props: PhasesPanelProps;
|
|
121
|
+
private cachedWidth?: number;
|
|
122
|
+
private cachedHeight?: number;
|
|
123
|
+
private cachedLines?: string[];
|
|
124
|
+
|
|
125
|
+
constructor(phases: Phase[] = [], focused = false) {
|
|
126
|
+
this.props = {
|
|
127
|
+
phases,
|
|
128
|
+
selectedIndex: 0,
|
|
129
|
+
focused
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
update(phases: Phase[], selectedIndex?: number): void {
|
|
134
|
+
this.props.phases = phases;
|
|
135
|
+
if (selectedIndex !== undefined) {
|
|
136
|
+
// Clamp to valid range
|
|
137
|
+
this.props.selectedIndex = Math.max(0, Math.min(selectedIndex, phases.length - 1));
|
|
138
|
+
} else if (this.props.selectedIndex >= phases.length && phases.length > 0) {
|
|
139
|
+
this.props.selectedIndex = phases.length - 1;
|
|
140
|
+
}
|
|
141
|
+
this.invalidate();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
setFocused(focused: boolean): void {
|
|
145
|
+
this.props.focused = focused;
|
|
146
|
+
this.invalidate();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
isFocused(): boolean {
|
|
150
|
+
return this.props.focused;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getSelectedIndex(): number {
|
|
154
|
+
return this.props.selectedIndex;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getSelectedPhase(): Phase | undefined {
|
|
158
|
+
return this.props.phases[this.props.selectedIndex];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
navigateUp(): void {
|
|
162
|
+
if (this.props.selectedIndex > 0) {
|
|
163
|
+
this.props.selectedIndex--;
|
|
164
|
+
this.invalidate();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
navigateDown(): void {
|
|
169
|
+
if (this.props.selectedIndex < this.props.phases.length - 1) {
|
|
170
|
+
this.props.selectedIndex++;
|
|
171
|
+
this.invalidate();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
selectFirstInProgress(): void {
|
|
176
|
+
const idx = this.props.phases.findIndex(p => p.status === "in_progress");
|
|
177
|
+
if (idx !== -1) {
|
|
178
|
+
this.props.selectedIndex = idx;
|
|
179
|
+
this.invalidate();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
render(width: number, height: number, theme: Theme): string[] {
|
|
184
|
+
if (this.cachedLines && this.cachedWidth === width && this.cachedHeight === height) {
|
|
185
|
+
return this.cachedLines;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.cachedLines = renderPhasesPanel(width, height, this.props, theme);
|
|
189
|
+
this.cachedWidth = width;
|
|
190
|
+
this.cachedHeight = height;
|
|
191
|
+
return this.cachedLines;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
invalidate(): void {
|
|
195
|
+
this.cachedWidth = undefined;
|
|
196
|
+
this.cachedHeight = undefined;
|
|
197
|
+
this.cachedLines = undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission Control TUI Progress Bar
|
|
3
|
+
*
|
|
4
|
+
* Bottom bar displaying:
|
|
5
|
+
* - Visual progress bar using block characters
|
|
6
|
+
* - Percentage completion
|
|
7
|
+
* - Task count (completed/total)
|
|
8
|
+
*
|
|
9
|
+
* Block characters for progress bar:
|
|
10
|
+
* - Full: \u2588 (█)
|
|
11
|
+
* - Empty: \u2591 (░)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
15
|
+
import type { Run } from "../state.js";
|
|
16
|
+
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
17
|
+
|
|
18
|
+
// Block characters for progress bar
|
|
19
|
+
const BLOCK_FULL = "\u2588"; // █
|
|
20
|
+
const BLOCK_EMPTY = "\u2591"; // ░
|
|
21
|
+
|
|
22
|
+
export interface ProgressBarProps {
|
|
23
|
+
run: Run | null;
|
|
24
|
+
statusMessage: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Count completed and total tasks across all phases
|
|
29
|
+
*/
|
|
30
|
+
function countTasks(run: Run): { completed: number; total: number; phaseCompleted: number; phaseTotal: number } {
|
|
31
|
+
let completed = 0;
|
|
32
|
+
let total = 0;
|
|
33
|
+
let phaseCompleted = 0;
|
|
34
|
+
let phaseTotal = run.phases.length;
|
|
35
|
+
|
|
36
|
+
for (const phase of run.phases) {
|
|
37
|
+
if (phase.status === "done") {
|
|
38
|
+
phaseCompleted++;
|
|
39
|
+
}
|
|
40
|
+
for (const task of phase.tasks) {
|
|
41
|
+
if (task.status === "removed") continue;
|
|
42
|
+
total++;
|
|
43
|
+
if (task.status === "done") {
|
|
44
|
+
completed++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { completed, total, phaseCompleted, phaseTotal };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Render a progress bar string
|
|
54
|
+
*/
|
|
55
|
+
function renderProgressBar(percent: number, width: number): string {
|
|
56
|
+
if (width < 3) return "";
|
|
57
|
+
|
|
58
|
+
const filled = Math.round((percent / 100) * width);
|
|
59
|
+
const empty = width - filled;
|
|
60
|
+
|
|
61
|
+
return BLOCK_FULL.repeat(filled) + BLOCK_EMPTY.repeat(empty);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Render the progress bar component
|
|
66
|
+
*/
|
|
67
|
+
export function renderProgressBarComponent(
|
|
68
|
+
width: number,
|
|
69
|
+
props: ProgressBarProps,
|
|
70
|
+
theme: Theme
|
|
71
|
+
): string[] {
|
|
72
|
+
const { run } = props;
|
|
73
|
+
const lines: string[] = [];
|
|
74
|
+
|
|
75
|
+
if (!run || run.phases.length === 0) {
|
|
76
|
+
const suffix = "[0/0 Tasks]";
|
|
77
|
+
const progressBar = renderProgressBar(0, Math.max(0, width - suffix.length - 4));
|
|
78
|
+
lines.push(truncateToWidth(theme.fg("muted", `[${progressBar}] ${suffix}`), width));
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Count tasks
|
|
83
|
+
const { completed, total } = countTasks(run);
|
|
84
|
+
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
85
|
+
const suffix = `[${completed}/${total} Tasks]`;
|
|
86
|
+
const barWidth = Math.max(0, width - suffix.length - 4);
|
|
87
|
+
const progressBar = renderProgressBar(percent, barWidth);
|
|
88
|
+
lines.push(truncateToWidth(theme.fg("accent", `[${progressBar}] ${suffix}`), width));
|
|
89
|
+
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Progress bar component class
|
|
95
|
+
*/
|
|
96
|
+
export class ProgressBarComponent {
|
|
97
|
+
private props: ProgressBarProps;
|
|
98
|
+
private cachedWidth?: number;
|
|
99
|
+
private cachedLines?: string[];
|
|
100
|
+
|
|
101
|
+
constructor(run: Run | null = null, statusMessage = "") {
|
|
102
|
+
this.props = { run, statusMessage };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
update(run: Run | null, statusMessage?: string): void {
|
|
106
|
+
this.props.run = run;
|
|
107
|
+
if (statusMessage !== undefined) {
|
|
108
|
+
this.props.statusMessage = statusMessage;
|
|
109
|
+
}
|
|
110
|
+
this.invalidate();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
updateStatus(message: string): void {
|
|
114
|
+
this.props.statusMessage = message;
|
|
115
|
+
this.invalidate();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getCompletionPercent(): number {
|
|
119
|
+
const { run } = this.props;
|
|
120
|
+
if (!run) return 0;
|
|
121
|
+
|
|
122
|
+
let completed = 0;
|
|
123
|
+
let total = 0;
|
|
124
|
+
|
|
125
|
+
for (const phase of run.phases) {
|
|
126
|
+
for (const task of phase.tasks) {
|
|
127
|
+
if (task.status === "removed") continue;
|
|
128
|
+
total++;
|
|
129
|
+
if (task.status === "done") {
|
|
130
|
+
completed++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
render(width: number, theme: Theme): string[] {
|
|
139
|
+
if (this.cachedLines && this.cachedWidth === width) {
|
|
140
|
+
return this.cachedLines;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.cachedLines = renderProgressBarComponent(width, this.props, theme);
|
|
144
|
+
this.cachedWidth = width;
|
|
145
|
+
return this.cachedLines;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
invalidate(): void {
|
|
149
|
+
this.cachedWidth = undefined;
|
|
150
|
+
this.cachedLines = undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission Control TUI custom styles.
|
|
3
|
+
* Uses stronger dark orange / dark green tones than the default theme tokens.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ANSI_FG_RESET = "\x1b[39m";
|
|
7
|
+
const ANSI_STRIKE_ON = "\x1b[9m";
|
|
8
|
+
const ANSI_STRIKE_OFF = "\x1b[29m";
|
|
9
|
+
|
|
10
|
+
const DARK_GREEN = { r: 46, g: 125, b: 50 }; // #2E7D32
|
|
11
|
+
const DARK_ORANGE = { r: 198, g: 94, b: 0 }; // #C65E00
|
|
12
|
+
|
|
13
|
+
function rgb(text: string, color: { r: number; g: number; b: number }): string {
|
|
14
|
+
return `\x1b[38;2;${color.r};${color.g};${color.b}m${text}${ANSI_FG_RESET}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function missionSuccess(text: string): string {
|
|
18
|
+
return rgb(text, DARK_GREEN);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function missionWarning(text: string): string {
|
|
22
|
+
return rgb(text, DARK_ORANGE);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function strike(text: string): string {
|
|
26
|
+
return `${ANSI_STRIKE_ON}${text}${ANSI_STRIKE_OFF}`;
|
|
27
|
+
}
|