supipowers 0.7.11 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/commands/run.ts +43 -23
- package/src/index.ts +4 -0
- package/src/orchestrator/dispatcher.ts +21 -20
- package/src/orchestrator/progress-renderer.ts +180 -0
- package/src/orchestrator/run-progress.ts +78 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supipowers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "OMP-native workflow extension inspired by supipowers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"@oh-my-pi/pi-coding-agent": "latest",
|
|
39
39
|
"@oh-my-pi/pi-tui": "latest",
|
|
40
40
|
"@sinclair/typebox": "^0.34.48",
|
|
41
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
41
42
|
"@types/node": "^22.0.0",
|
|
43
|
+
"better-sqlite3": "^12.8.0",
|
|
42
44
|
"typescript": "^5.9.3",
|
|
43
45
|
"vitest": "^4.0.0"
|
|
44
46
|
},
|
package/src/commands/run.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { buildWorktreePrompt } from "../git/worktree.js";
|
|
|
27
27
|
import { buildBranchFinishPrompt } from "../git/branch-finish.js";
|
|
28
28
|
import { detectBaseBranch } from "../git/base-branch.js";
|
|
29
29
|
import type { RunManifest, AgentResult } from "../types.js";
|
|
30
|
-
import {
|
|
30
|
+
import { RunProgressState, activeRuns } from "../orchestrator/run-progress.js";
|
|
31
31
|
|
|
32
32
|
interface ParsedRunArgs {
|
|
33
33
|
profile?: string;
|
|
@@ -90,7 +90,17 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
90
90
|
updateRun(ctx.cwd, manifest);
|
|
91
91
|
manifest = null;
|
|
92
92
|
} else {
|
|
93
|
-
|
|
93
|
+
const completedBatches = manifest.batches.filter((b) => b.status === "completed").length;
|
|
94
|
+
const totalBatches = manifest.batches.length;
|
|
95
|
+
const completedTasks = manifest.batches
|
|
96
|
+
.filter((b) => b.status === "completed")
|
|
97
|
+
.reduce((sum, b) => sum + b.taskIds.length, 0);
|
|
98
|
+
const totalTasks = manifest.batches.reduce((sum, b) => sum + b.taskIds.length, 0);
|
|
99
|
+
notifyInfo(
|
|
100
|
+
ctx,
|
|
101
|
+
`Resuming run: ${manifest.id}`,
|
|
102
|
+
`${completedTasks}/${totalTasks} tasks done, ${completedBatches}/${totalBatches} batches completed`,
|
|
103
|
+
);
|
|
94
104
|
}
|
|
95
105
|
} else {
|
|
96
106
|
// No UI — resume automatically
|
|
@@ -176,21 +186,34 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
176
186
|
const lsp = isLspAvailable(pi.getActiveTools());
|
|
177
187
|
const ctxMode = detectContextMode(pi.getActiveTools()).available;
|
|
178
188
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
// Create shared progress state and send inline progress message
|
|
190
|
+
const progress = new RunProgressState();
|
|
191
|
+
for (const task of plan.tasks) {
|
|
192
|
+
const deps = task.parallelism.type === "sequential" ? task.parallelism.dependsOn : [];
|
|
193
|
+
progress.addTask(task.id, task.name, deps);
|
|
194
|
+
}
|
|
195
|
+
// On resume, mark already-completed tasks
|
|
196
|
+
const existingResults = loadAllAgentResults(ctx.cwd, manifest!.id);
|
|
197
|
+
for (const result of existingResults) {
|
|
198
|
+
if (result.status === "done") {
|
|
199
|
+
progress.setStatus(result.taskId, "done");
|
|
200
|
+
} else if (result.status === "done_with_concerns") {
|
|
201
|
+
progress.setStatus(result.taskId, "done_with_concerns", result.concerns);
|
|
202
|
+
} else if (result.status === "blocked") {
|
|
203
|
+
progress.setStatus(result.taskId, "blocked", result.output);
|
|
204
|
+
}
|
|
193
205
|
}
|
|
206
|
+
activeRuns.set(manifest.id, progress);
|
|
207
|
+
|
|
208
|
+
// Send inline progress message — the registered renderer will display it
|
|
209
|
+
pi.sendMessage(
|
|
210
|
+
{
|
|
211
|
+
customType: "supi-run-progress",
|
|
212
|
+
content: [{ type: "text", text: "Running tasks..." }],
|
|
213
|
+
display: "custom",
|
|
214
|
+
details: { runId: manifest.id },
|
|
215
|
+
},
|
|
216
|
+
);
|
|
194
217
|
|
|
195
218
|
try {
|
|
196
219
|
for (const batch of manifest.batches) {
|
|
@@ -204,6 +227,7 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
204
227
|
`Batch ${batch.index + 1}/${manifest.batches.length}`,
|
|
205
228
|
`${batch.taskIds.length} tasks`
|
|
206
229
|
);
|
|
230
|
+
progress.batchLabel = `Batch ${batch.index + 1}/${manifest.batches.length}`;
|
|
207
231
|
|
|
208
232
|
const batchResults: AgentResult[] = [];
|
|
209
233
|
const agentPromises = batch.taskIds.map((taskId) => {
|
|
@@ -218,7 +242,7 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
218
242
|
config,
|
|
219
243
|
lspAvailable: lsp,
|
|
220
244
|
contextModeAvailable: ctxMode,
|
|
221
|
-
|
|
245
|
+
progress,
|
|
222
246
|
});
|
|
223
247
|
});
|
|
224
248
|
|
|
@@ -255,7 +279,7 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
255
279
|
config,
|
|
256
280
|
lspAvailable: lsp,
|
|
257
281
|
contextModeAvailable: ctxMode,
|
|
258
|
-
|
|
282
|
+
progress,
|
|
259
283
|
previousOutput: failed.output,
|
|
260
284
|
failureReason: failed.output,
|
|
261
285
|
});
|
|
@@ -314,11 +338,7 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
314
338
|
notifyInfo(ctx, "Run succeeded", "Follow branch finish instructions to integrate your work");
|
|
315
339
|
}
|
|
316
340
|
} finally {
|
|
317
|
-
|
|
318
|
-
widget?.dispose();
|
|
319
|
-
if (ctx.hasUI) {
|
|
320
|
-
ctx.ui.setWidget("supi-agents", undefined);
|
|
321
|
-
}
|
|
341
|
+
activeRuns.delete(manifest.id);
|
|
322
342
|
}
|
|
323
343
|
},
|
|
324
344
|
});
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { registerUpdateCommand, handleUpdate } from "./commands/update.js";
|
|
|
15
15
|
import { registerFixPrCommand } from "./commands/fix-pr.js";
|
|
16
16
|
import { loadConfig } from "./config/loader.js";
|
|
17
17
|
import { registerContextModeHooks } from "./context-mode/hooks.js";
|
|
18
|
+
import { registerProgressRenderer } from "./orchestrator/progress-renderer.js";
|
|
18
19
|
|
|
19
20
|
// TUI-only commands — intercepted at the input level to prevent
|
|
20
21
|
// message submission and "Working..." indicator
|
|
@@ -48,6 +49,9 @@ export default function supipowers(pi: ExtensionAPI): void {
|
|
|
48
49
|
registerUpdateCommand(pi);
|
|
49
50
|
registerFixPrCommand(pi);
|
|
50
51
|
|
|
52
|
+
// Register custom message renderers
|
|
53
|
+
registerProgressRenderer(pi);
|
|
54
|
+
|
|
51
55
|
// Intercept TUI-only commands at the input level — this runs BEFORE
|
|
52
56
|
// message submission, so no chat message appears and no "Working..." indicator
|
|
53
57
|
pi.on("input", (event, ctx) => {
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
notifyError,
|
|
19
19
|
notifyInfo,
|
|
20
20
|
} from "../notifications/renderer.js";
|
|
21
|
-
import type {
|
|
21
|
+
import type { RunProgressState } from "./run-progress.js";
|
|
22
22
|
|
|
23
23
|
export interface DispatchOptions {
|
|
24
24
|
pi: ExtensionAPI;
|
|
@@ -31,22 +31,22 @@ export interface DispatchOptions {
|
|
|
31
31
|
config: SupipowersConfig;
|
|
32
32
|
lspAvailable: boolean;
|
|
33
33
|
contextModeAvailable: boolean;
|
|
34
|
-
|
|
34
|
+
progress?: RunProgressState;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export async function dispatchAgent(
|
|
38
38
|
options: DispatchOptions,
|
|
39
39
|
): Promise<AgentResult> {
|
|
40
|
-
const { pi, ctx, task, planContext, config, lspAvailable, contextModeAvailable,
|
|
40
|
+
const { pi, ctx, task, planContext, config, lspAvailable, contextModeAvailable, progress } = options;
|
|
41
41
|
const startTime = Date.now();
|
|
42
42
|
|
|
43
43
|
const prompt = buildTaskPrompt(task, planContext, config, lspAvailable, contextModeAvailable);
|
|
44
44
|
|
|
45
45
|
// Initialize widget card if available
|
|
46
|
-
|
|
46
|
+
progress?.setStatus(task.id, "running");
|
|
47
47
|
|
|
48
48
|
try {
|
|
49
|
-
const result = await executeSubAgent(pi, prompt, task, config, ctx,
|
|
49
|
+
const result = await executeSubAgent(pi, prompt, task, config, ctx, progress);
|
|
50
50
|
|
|
51
51
|
const agentResult: AgentResult = {
|
|
52
52
|
taskId: task.id,
|
|
@@ -60,15 +60,15 @@ export async function dispatchAgent(
|
|
|
60
60
|
// Update widget with final status
|
|
61
61
|
switch (agentResult.status) {
|
|
62
62
|
case "done":
|
|
63
|
-
|
|
63
|
+
progress?.setStatus(task.id, "done");
|
|
64
64
|
notifySuccess(ctx, `Task ${task.id} completed`, task.name);
|
|
65
65
|
break;
|
|
66
66
|
case "done_with_concerns":
|
|
67
|
-
|
|
67
|
+
progress?.setStatus(task.id, "done_with_concerns", agentResult.concerns);
|
|
68
68
|
notifyWarning(ctx, `Task ${task.id} done with concerns`, agentResult.concerns);
|
|
69
69
|
break;
|
|
70
70
|
case "blocked":
|
|
71
|
-
|
|
71
|
+
progress?.setStatus(task.id, "blocked", agentResult.output);
|
|
72
72
|
notifyError(ctx, `Task ${task.id} blocked`, agentResult.output);
|
|
73
73
|
break;
|
|
74
74
|
}
|
|
@@ -76,7 +76,7 @@ export async function dispatchAgent(
|
|
|
76
76
|
return agentResult;
|
|
77
77
|
} catch (error) {
|
|
78
78
|
const errorMsg = `Agent error: ${error instanceof Error ? error.message : String(error)}`;
|
|
79
|
-
|
|
79
|
+
progress?.setStatus(task.id, "blocked", errorMsg);
|
|
80
80
|
|
|
81
81
|
const agentResult: AgentResult = {
|
|
82
82
|
taskId: task.id,
|
|
@@ -131,7 +131,7 @@ async function executeSubAgent(
|
|
|
131
131
|
task: PlanTask,
|
|
132
132
|
config: SupipowersConfig,
|
|
133
133
|
ctx?: NotifyCtx,
|
|
134
|
-
|
|
134
|
+
progress?: RunProgressState,
|
|
135
135
|
): Promise<SubAgentResult> {
|
|
136
136
|
const { createAgentSession } = pi.pi;
|
|
137
137
|
|
|
@@ -160,8 +160,8 @@ async function executeSubAgent(
|
|
|
160
160
|
const preview = content.split("\n").filter(Boolean).pop()?.slice(0, 80) ?? "";
|
|
161
161
|
if (preview && preview !== lastThinkingPreview) {
|
|
162
162
|
lastThinkingPreview = preview;
|
|
163
|
-
if (
|
|
164
|
-
|
|
163
|
+
if (progress) {
|
|
164
|
+
progress.setActivity(task.id, preview);
|
|
165
165
|
} else if (ctx) {
|
|
166
166
|
ctx.ui.notify(`${tag}: thinking — ${preview}`, "info");
|
|
167
167
|
}
|
|
@@ -172,8 +172,9 @@ async function executeSubAgent(
|
|
|
172
172
|
if (FILE_TOOLS.has(event.toolName) && args?.file_path) {
|
|
173
173
|
pendingToolArgs.set(event.toolCallId, args);
|
|
174
174
|
}
|
|
175
|
-
if (
|
|
176
|
-
|
|
175
|
+
if (progress) {
|
|
176
|
+
progress.setActivity(task.id, formatToolAction(event.toolName, args));
|
|
177
|
+
progress.incrementTools(task.id);
|
|
177
178
|
} else if (ctx) {
|
|
178
179
|
ctx.ui.notify(`${tag}: ${formatToolAction(event.toolName, args)}`, "info");
|
|
179
180
|
}
|
|
@@ -182,7 +183,7 @@ async function executeSubAgent(
|
|
|
182
183
|
const args = pendingToolArgs.get(event.toolCallId);
|
|
183
184
|
if (args?.file_path && !event.isError) {
|
|
184
185
|
filesChanged.add(String(args.file_path));
|
|
185
|
-
|
|
186
|
+
progress?.incrementFiles(task.id);
|
|
186
187
|
}
|
|
187
188
|
pendingToolArgs.delete(event.toolCallId);
|
|
188
189
|
}
|
|
@@ -275,7 +276,7 @@ export async function dispatchAgentWithReview(
|
|
|
275
276
|
}
|
|
276
277
|
|
|
277
278
|
// Step 2: Spec compliance review
|
|
278
|
-
options.
|
|
279
|
+
options.progress?.setStatus(task.id, "reviewing");
|
|
279
280
|
for (let attempt = 0; attempt <= maxReviewRetries; attempt++) {
|
|
280
281
|
const specReview = await dispatchSpecReview(
|
|
281
282
|
pi,
|
|
@@ -319,7 +320,7 @@ export async function dispatchAgentWithReview(
|
|
|
319
320
|
}
|
|
320
321
|
|
|
321
322
|
// Step 3: Code quality review
|
|
322
|
-
options.
|
|
323
|
+
options.progress?.setStatus(task.id, "reviewing");
|
|
323
324
|
for (let attempt = 0; attempt <= maxReviewRetries; attempt++) {
|
|
324
325
|
const qualityReview = await dispatchQualityReview(
|
|
325
326
|
pi,
|
|
@@ -422,7 +423,7 @@ async function dispatchQualityReview(
|
|
|
422
423
|
export async function dispatchFixAgent(
|
|
423
424
|
options: DispatchOptions & { previousOutput: string; failureReason: string },
|
|
424
425
|
): Promise<AgentResult> {
|
|
425
|
-
const { pi, ctx, task, config, lspAvailable, contextModeAvailable, previousOutput, failureReason,
|
|
426
|
+
const { pi, ctx, task, config, lspAvailable, contextModeAvailable, previousOutput, failureReason, progress } =
|
|
426
427
|
options;
|
|
427
428
|
const startTime = Date.now();
|
|
428
429
|
|
|
@@ -434,10 +435,10 @@ export async function dispatchFixAgent(
|
|
|
434
435
|
contextModeAvailable,
|
|
435
436
|
);
|
|
436
437
|
|
|
437
|
-
|
|
438
|
+
progress?.setStatus(task.id, "running");
|
|
438
439
|
|
|
439
440
|
try {
|
|
440
|
-
const result = await executeSubAgent(pi, prompt, task, config, ctx,
|
|
441
|
+
const result = await executeSubAgent(pi, prompt, task, config, ctx, progress);
|
|
441
442
|
return {
|
|
442
443
|
taskId: task.id,
|
|
443
444
|
status: result.status,
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// src/orchestrator/progress-renderer.ts
|
|
2
|
+
import type { TaskStatus } from "./agent-grid.js";
|
|
3
|
+
import { activeRuns, type TaskProgress } from "./run-progress.js";
|
|
4
|
+
|
|
5
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
interface Theme {
|
|
8
|
+
fg(color: string, text: string): string;
|
|
9
|
+
bold(text: string): string;
|
|
10
|
+
sep: { dot: string };
|
|
11
|
+
tree: {
|
|
12
|
+
branch: string;
|
|
13
|
+
last: string;
|
|
14
|
+
vertical: string;
|
|
15
|
+
hook: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Component {
|
|
20
|
+
render(width: number): string[];
|
|
21
|
+
invalidate(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CustomMessage<T> {
|
|
25
|
+
details: T;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface RunProgressDetails {
|
|
29
|
+
runId: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type MessageRenderer<T> = (
|
|
33
|
+
message: CustomMessage<T>,
|
|
34
|
+
options: { expanded: boolean },
|
|
35
|
+
theme: Theme,
|
|
36
|
+
) => Component | undefined;
|
|
37
|
+
|
|
38
|
+
interface ExtensionAPI {
|
|
39
|
+
registerMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Constants ──────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
45
|
+
|
|
46
|
+
interface StatusConfig {
|
|
47
|
+
color: string;
|
|
48
|
+
icon: (frame: number) => string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const STATUS_CONFIG: Record<TaskStatus, StatusConfig> = {
|
|
52
|
+
pending: { color: "dim", icon: () => "○" },
|
|
53
|
+
running: { color: "accent", icon: (f) => SPINNER_FRAMES[f % SPINNER_FRAMES.length] },
|
|
54
|
+
reviewing: { color: "warning", icon: () => "◎" },
|
|
55
|
+
done: { color: "success", icon: () => "✓" },
|
|
56
|
+
done_with_concerns: { color: "warning", icon: () => "⚠" },
|
|
57
|
+
blocked: { color: "error", icon: () => "✗" },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ── Helper: format elapsed time ────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function formatElapsed(startedAt: number, completedAt?: number): string {
|
|
63
|
+
const ms = (completedAt ?? Date.now()) - startedAt;
|
|
64
|
+
const s = Math.floor(ms / 1000);
|
|
65
|
+
if (s < 60) return `${s}s`;
|
|
66
|
+
const m = Math.floor(s / 60);
|
|
67
|
+
const rem = s % 60;
|
|
68
|
+
return rem > 0 ? `${m}m${rem}s` : `${m}m`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Inline Progress Component ──────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
class InlineProgressComponent implements Component {
|
|
74
|
+
#runId: string;
|
|
75
|
+
#theme: Theme;
|
|
76
|
+
#spinnerFrame = 0;
|
|
77
|
+
|
|
78
|
+
constructor(runId: string, theme: Theme) {
|
|
79
|
+
this.#runId = runId;
|
|
80
|
+
this.#theme = theme;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
render(_width: number): string[] {
|
|
84
|
+
// Advance spinner on every render call (called each repaint)
|
|
85
|
+
this.#spinnerFrame = (this.#spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
86
|
+
|
|
87
|
+
const state = activeRuns.get(this.#runId);
|
|
88
|
+
if (!state || state.tasks.size === 0) return [];
|
|
89
|
+
|
|
90
|
+
const tasks = [...state.tasks.values()];
|
|
91
|
+
const lines: string[] = [];
|
|
92
|
+
const tree = this.#theme.tree;
|
|
93
|
+
const sep = this.#theme.sep.dot;
|
|
94
|
+
|
|
95
|
+
tasks.forEach((task, idx) => {
|
|
96
|
+
const isLast = idx === tasks.length - 1;
|
|
97
|
+
const prefix = isLast ? tree.last : tree.branch;
|
|
98
|
+
const cfg = STATUS_CONFIG[task.status];
|
|
99
|
+
const icon = this.#theme.fg(cfg.color, cfg.icon(this.#spinnerFrame));
|
|
100
|
+
const taskLabel = this.#theme.fg(cfg.color, `T${task.taskId}`);
|
|
101
|
+
const name = this.#theme.bold(task.name);
|
|
102
|
+
|
|
103
|
+
const parts: string[] = [`${prefix} ${icon} ${taskLabel} ${sep} ${name}`];
|
|
104
|
+
this.#appendTaskMeta(task, parts, sep);
|
|
105
|
+
|
|
106
|
+
// Show dependencies for pending tasks
|
|
107
|
+
if (task.status === "pending" && task.dependsOn.length > 0) {
|
|
108
|
+
const depLabels = task.dependsOn.map((d) => `T${d}`).join(", ");
|
|
109
|
+
parts.push(` ${sep} `);
|
|
110
|
+
parts.push(this.#theme.fg("dim", `Depends on ${depLabels}`));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
lines.push(parts.join(""));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Summary line
|
|
117
|
+
const summary = state.summary;
|
|
118
|
+
const summaryParts: string[] = [];
|
|
119
|
+
if (state.batchLabel) summaryParts.push(this.#theme.fg("muted", state.batchLabel));
|
|
120
|
+
if (summary.done > 0) summaryParts.push(this.#theme.fg("success", `${summary.done} done`));
|
|
121
|
+
if (summary.running > 0) summaryParts.push(this.#theme.fg("accent", `${summary.running} running`));
|
|
122
|
+
if (summary.pending > 0) summaryParts.push(this.#theme.fg("dim", `${summary.pending} pending`));
|
|
123
|
+
if (summary.blocked > 0) summaryParts.push(this.#theme.fg("error", `${summary.blocked} blocked`));
|
|
124
|
+
|
|
125
|
+
if (summaryParts.length > 0) {
|
|
126
|
+
const indent = ` `; // align under tree
|
|
127
|
+
lines.push(`${indent}${summaryParts.join(` ${sep} `)}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return lines;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
invalidate(): void {
|
|
134
|
+
// No cache to bust — we render fresh each time
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Private ────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
#appendTaskMeta(task: TaskProgress, parts: string[], sep: string): void {
|
|
140
|
+
const isActive = task.status === "running" || task.status === "reviewing";
|
|
141
|
+
const isDone =
|
|
142
|
+
task.status === "done" ||
|
|
143
|
+
task.status === "done_with_concerns" ||
|
|
144
|
+
task.status === "blocked";
|
|
145
|
+
|
|
146
|
+
if (task.toolCount > 0) {
|
|
147
|
+
parts.push(` ${sep} `);
|
|
148
|
+
parts.push(this.#theme.fg("muted", `${task.toolCount} tools`));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (isDone && task.filesChanged > 0) {
|
|
152
|
+
parts.push(` ${sep} `);
|
|
153
|
+
parts.push(this.#theme.fg("muted", `${task.filesChanged} files`));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (isDone) {
|
|
157
|
+
const elapsed = formatElapsed(task.startedAt, task.completedAt);
|
|
158
|
+
parts.push(` ${sep} `);
|
|
159
|
+
parts.push(this.#theme.fg("muted", elapsed));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (isActive && task.currentActivity) {
|
|
163
|
+
parts.push(` ${sep} `);
|
|
164
|
+
parts.push(this.#theme.fg("dim", task.currentActivity));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Registration ───────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
export function registerProgressRenderer(pi: ExtensionAPI): void {
|
|
172
|
+
pi.registerMessageRenderer<RunProgressDetails>(
|
|
173
|
+
"supi-run-progress",
|
|
174
|
+
(message, _options, theme) => {
|
|
175
|
+
const { runId } = message.details;
|
|
176
|
+
if (!runId) return undefined;
|
|
177
|
+
return new InlineProgressComponent(runId, theme);
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/orchestrator/run-progress.ts
|
|
2
|
+
import type { TaskStatus } from "./agent-grid.js";
|
|
3
|
+
|
|
4
|
+
export interface TaskProgress {
|
|
5
|
+
taskId: number;
|
|
6
|
+
name: string;
|
|
7
|
+
status: TaskStatus;
|
|
8
|
+
currentActivity: string;
|
|
9
|
+
toolCount: number;
|
|
10
|
+
filesChanged: number;
|
|
11
|
+
startedAt: number;
|
|
12
|
+
completedAt?: number;
|
|
13
|
+
errorReason?: string;
|
|
14
|
+
concerns?: string;
|
|
15
|
+
dependsOn: number[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Shared state store for a single run — written by dispatcher, read by renderer */
|
|
19
|
+
export class RunProgressState {
|
|
20
|
+
readonly tasks = new Map<number, TaskProgress>();
|
|
21
|
+
batchLabel = "";
|
|
22
|
+
|
|
23
|
+
addTask(taskId: number, name: string, dependsOn: number[] = []): void {
|
|
24
|
+
this.tasks.set(taskId, {
|
|
25
|
+
taskId,
|
|
26
|
+
name,
|
|
27
|
+
status: "pending",
|
|
28
|
+
currentActivity: "",
|
|
29
|
+
toolCount: 0,
|
|
30
|
+
filesChanged: 0,
|
|
31
|
+
startedAt: Date.now(),
|
|
32
|
+
dependsOn,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setStatus(taskId: number, status: TaskStatus, reason?: string): void {
|
|
37
|
+
const task = this.tasks.get(taskId);
|
|
38
|
+
if (!task) return;
|
|
39
|
+
task.status = status;
|
|
40
|
+
if (status === "blocked") task.errorReason = reason;
|
|
41
|
+
if (status === "done_with_concerns") task.concerns = reason;
|
|
42
|
+
if (status === "done" || status === "done_with_concerns" || status === "blocked") {
|
|
43
|
+
task.completedAt = Date.now();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setActivity(taskId: number, activity: string): void {
|
|
48
|
+
const task = this.tasks.get(taskId);
|
|
49
|
+
if (!task) return;
|
|
50
|
+
task.currentActivity = activity;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
incrementTools(taskId: number): void {
|
|
54
|
+
const task = this.tasks.get(taskId);
|
|
55
|
+
if (!task) return;
|
|
56
|
+
task.toolCount++;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
incrementFiles(taskId: number): void {
|
|
60
|
+
const task = this.tasks.get(taskId);
|
|
61
|
+
if (!task) return;
|
|
62
|
+
task.filesChanged++;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get summary() {
|
|
66
|
+
const all = [...this.tasks.values()];
|
|
67
|
+
return {
|
|
68
|
+
total: all.length,
|
|
69
|
+
done: all.filter((t) => t.status === "done" || t.status === "done_with_concerns").length,
|
|
70
|
+
running: all.filter((t) => t.status === "running" || t.status === "reviewing").length,
|
|
71
|
+
blocked: all.filter((t) => t.status === "blocked").length,
|
|
72
|
+
pending: all.filter((t) => t.status === "pending").length,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Module-level store: runId → state. Renderer reads from here. */
|
|
78
|
+
export const activeRuns = new Map<string, RunProgressState>();
|