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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supipowers",
3
- "version": "0.7.11",
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
  },
@@ -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 { AgentGridWidget, createAgentGridFactory } from "../orchestrator/agent-grid.js";
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
- notifyInfo(ctx, `Resuming run: ${manifest.id}`);
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
- // Mount agent grid widget for live progress visualization
180
- let widget: AgentGridWidget | undefined;
181
- if (ctx.hasUI) {
182
- const widgetReady = new Promise<AgentGridWidget>((resolve) => {
183
- ctx.ui.setWidget("supi-agents", createAgentGridFactory((w) => {
184
- widget = w;
185
- for (const task of plan.tasks) {
186
- w.addTask(task.id, task.name);
187
- }
188
- resolve(w);
189
- }));
190
- });
191
- // Wait for TUI to instantiate the widget before dispatching agents
192
- widget = await widgetReady;
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
- widget,
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
- widget,
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
- // Ensure widget interval timer is cleaned up even on exception
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 { AgentGridWidget } from "./agent-grid.js";
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
- widget?: AgentGridWidget;
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, widget } = options;
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
- widget?.setStatus(task.id, "running");
46
+ progress?.setStatus(task.id, "running");
47
47
 
48
48
  try {
49
- const result = await executeSubAgent(pi, prompt, task, config, ctx, widget);
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
- widget?.setStatus(task.id, "done");
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
- widget?.setStatus(task.id, "done_with_concerns", agentResult.concerns);
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
- widget?.setStatus(task.id, "blocked", agentResult.output);
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
- widget?.setStatus(task.id, "blocked", errorMsg);
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
- widget?: AgentGridWidget,
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 (widget) {
164
- widget.setThinking(task.id, preview);
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 (widget) {
176
- widget.addActivity(task.id, formatToolAction(event.toolName, args));
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
- widget?.addFileChanged(task.id);
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.widget?.setStatus(task.id, "reviewing");
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.widget?.setStatus(task.id, "reviewing");
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, widget } =
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
- widget?.setStatus(task.id, "running");
438
+ progress?.setStatus(task.id, "running");
438
439
 
439
440
  try {
440
- const result = await executeSubAgent(pi, prompt, task, config, ctx, widget);
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>();