@jameslovespancakes/pi-plus 1.0.20 → 1.0.21
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 +21 -3
- package/package.json +1 -1
- package/src/domains/workflows/index.ts +56 -104
- package/src/domains/workflows/runtime/advisory-challenge.ts +3 -3
- package/src/domains/workflows/runtime/agent-attempt.ts +3 -3
- package/src/domains/workflows/runtime/agent-options.ts +18 -0
- package/src/domains/workflows/runtime/agent-runner-types.ts +7 -3
- package/src/domains/workflows/runtime/agent-runner.ts +14 -6
- package/src/domains/workflows/runtime/agent-session.ts +34 -3
- package/src/domains/workflows/runtime/cancellation.ts +5 -0
- package/src/domains/workflows/runtime/engine.ts +19 -40
- package/src/domains/workflows/runtime/journal.ts +4 -4
- package/src/domains/workflows/runtime/live-agent.ts +37 -0
- package/src/domains/workflows/runtime/model-profiles.ts +2 -6
- package/src/domains/workflows/runtime/progress-types.ts +3 -1
- package/src/domains/workflows/runtime/progress.ts +54 -14
- package/src/domains/workflows/runtime/review/review-fix-workflow.ts +3 -3
- package/src/domains/workflows/runtime/types.ts +16 -15
- package/src/domains/workflows/runtime/ui/agent-transcript.ts +59 -0
- package/src/domains/workflows/runtime/ui/workflow-format.ts +6 -2
- package/src/domains/workflows/runtime/ui/workflow-inspector.ts +75 -61
- package/src/domains/workflows/runtime/ui/workflow-widget.ts +26 -66
- package/src/domains/workflows/runtime/workflow-advisory-utils.ts +5 -5
- package/src/domains/workflows/runtime/{background-workflows.ts → workflow-lifecycle.ts} +91 -75
- package/src/domains/workflows/runtime/workflow-management.ts +66 -0
- package/src/domains/workflows/runtime/workflow-run-controller.ts +16 -20
- package/src/domains/workflows/runtime/{workflow-run-background.ts → workflow-run-delivery.ts} +3 -3
- package/src/domains/workflows/runtime/workflow-run-record.ts +7 -4
- package/src/domains/workflows/workflows/code-review.ts +1 -1
- package/src/domains/workflows/workflows/diagnose.ts +1 -1
- package/src/domains/workflows/workflows/perf-review.ts +1 -1
- package/src/domains/workflows/workflows/refactor-scout.ts +1 -1
- package/src/domains/workflows/workflows/research.ts +4 -4
- package/src/domains/workflows/runtime/background-workflow-tool.ts +0 -75
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { CustomEditor, getSelectListTheme, type KeybindingsManager, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { AgentTranscriptView } from "./agent-transcript.ts";
|
|
2
3
|
import {
|
|
3
4
|
Box,
|
|
4
|
-
|
|
5
|
+
Editor,
|
|
5
6
|
Markdown,
|
|
6
7
|
matchesKey,
|
|
7
8
|
Text,
|
|
@@ -16,21 +17,25 @@ import {
|
|
|
16
17
|
import type { AgentChatMessage, AgentRowSnapshot, WorkflowProgressSnapshot } from "../progress-types.ts";
|
|
17
18
|
import type { WorkflowProgressSource } from "../types.ts";
|
|
18
19
|
import { unknownErrorMessage } from "../unknown-error.ts";
|
|
19
|
-
import {
|
|
20
|
+
import { agentModelName, thinkingLabel } from "./workflow-widget.ts";
|
|
20
21
|
import { formatDuration, statusIcon, truncateDisplay } from "./workflow-format.ts";
|
|
21
22
|
import {
|
|
22
23
|
centerWorkflowViewerViewport,
|
|
23
24
|
fitWorkflowViewerRow,
|
|
24
25
|
fitWorkflowViewerRows,
|
|
25
|
-
workflowViewerHeight,
|
|
26
26
|
} from "./workflow-viewer-layout.ts";
|
|
27
27
|
|
|
28
|
+
export const WORKFLOW_INSPECTOR_OVERLAY_OPTIONS = {
|
|
29
|
+
overlay: true,
|
|
30
|
+
overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%", margin: 0 },
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
28
33
|
export interface WorkflowInspectorOutcome {
|
|
29
34
|
readonly label: string;
|
|
30
35
|
readonly text: string;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
type WorkflowInspectorLiveSource = Pick<WorkflowProgressSource, "conversation" | "followUp">;
|
|
38
|
+
type WorkflowInspectorLiveSource = Pick<WorkflowProgressSource, "conversation" | "followUp" | "stopAgent" | "transcript">;
|
|
34
39
|
|
|
35
40
|
interface BoardAgent {
|
|
36
41
|
readonly agent: AgentRowSnapshot;
|
|
@@ -45,6 +50,7 @@ interface BoardRow {
|
|
|
45
50
|
/** Modal board for inspecting workflow agents and sending targeted follow-ups. */
|
|
46
51
|
export class WorkflowInspector implements Focusable {
|
|
47
52
|
private selected = 0;
|
|
53
|
+
private width = 0;
|
|
48
54
|
private detailAgentId: number | undefined;
|
|
49
55
|
private detailScroll = 0;
|
|
50
56
|
private clickRows = new Map<number, number>();
|
|
@@ -52,7 +58,8 @@ export class WorkflowInspector implements Focusable {
|
|
|
52
58
|
private followUpError: string | undefined;
|
|
53
59
|
private sending = false;
|
|
54
60
|
private _focused = false;
|
|
55
|
-
private readonly input:
|
|
61
|
+
private readonly input: Editor;
|
|
62
|
+
private readonly transcriptViews = new Map<number, AgentTranscriptView>();
|
|
56
63
|
private readonly snapshotProvider: () => WorkflowProgressSnapshot;
|
|
57
64
|
private readonly tui: Pick<TUI, "requestRender" | "terminal">;
|
|
58
65
|
private readonly theme: Theme;
|
|
@@ -67,6 +74,7 @@ export class WorkflowInspector implements Focusable {
|
|
|
67
74
|
close: () => void,
|
|
68
75
|
outcome?: WorkflowInspectorOutcome,
|
|
69
76
|
live?: WorkflowInspectorLiveSource,
|
|
77
|
+
keybindings?: KeybindingsManager,
|
|
70
78
|
) {
|
|
71
79
|
this.snapshotProvider = snapshotProvider;
|
|
72
80
|
this.tui = tui;
|
|
@@ -74,11 +82,10 @@ export class WorkflowInspector implements Focusable {
|
|
|
74
82
|
this.close = close;
|
|
75
83
|
this.outcome = outcome;
|
|
76
84
|
this.live = live;
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
});
|
|
85
|
+
const editorTheme = { borderColor: (text: string) => this.theme.fg("border", text), selectList: getSelectListTheme() };
|
|
86
|
+
this.input = keybindings
|
|
87
|
+
? new CustomEditor(tui as TUI, editorTheme, keybindings)
|
|
88
|
+
: new Editor(tui as TUI, editorTheme);
|
|
82
89
|
this.input.onSubmit = (value) => void this.submitFollowUp(value);
|
|
83
90
|
}
|
|
84
91
|
|
|
@@ -93,14 +100,16 @@ export class WorkflowInspector implements Focusable {
|
|
|
93
100
|
|
|
94
101
|
handleInput(data: string): void {
|
|
95
102
|
if (matchesKey(data, "escape") || (data === "q" && this.detailAgentId === undefined)) {
|
|
96
|
-
this.
|
|
103
|
+
if (this.detailAgentId !== undefined) this.closeDetails();
|
|
104
|
+
else this.close();
|
|
97
105
|
return;
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
if (this.detailAgentId !== undefined) {
|
|
101
109
|
if (matchesKey(data, "pageUp")) this.scrollChat(6);
|
|
102
110
|
else if (matchesKey(data, "pageDown")) this.scrollChat(-6);
|
|
103
|
-
else if (matchesKey(data, "backspace") && this.input.
|
|
111
|
+
else if (matchesKey(data, "backspace") && this.input.getText().length === 0) this.closeDetails();
|
|
112
|
+
else if (matchesKey(data, "alt+enter") && this.canMessageSelectedAgent()) void this.submitFollowUp(this.input.getText(), false);
|
|
104
113
|
else if (this.canMessageSelectedAgent()) this.input.handleInput(data);
|
|
105
114
|
this.tui.requestRender();
|
|
106
115
|
return;
|
|
@@ -113,6 +122,7 @@ export class WorkflowInspector implements Focusable {
|
|
|
113
122
|
else if (matchesKey(data, "pageDown")) this.select(this.selected + 8, count);
|
|
114
123
|
else if (matchesKey(data, "home") || data === "g") this.select(0, count);
|
|
115
124
|
else if (matchesKey(data, "end") || data === "G") this.select(count - 1, count);
|
|
125
|
+
else if (data === "x" || data === "X") this.stopSelectedAgent();
|
|
116
126
|
else if (matchesKey(data, "return") || matchesKey(data, "enter") || data === " ") this.openDetails(count);
|
|
117
127
|
}
|
|
118
128
|
|
|
@@ -123,7 +133,7 @@ export class WorkflowInspector implements Focusable {
|
|
|
123
133
|
return { handled: true, focus: true, render: true };
|
|
124
134
|
}
|
|
125
135
|
if (event.type === "click" && event.button === "left" && event.y === this.inputRow && this.canMessageSelectedAgent()) {
|
|
126
|
-
const result = this.input.handleMouse({ ...event, y: 0 });
|
|
136
|
+
const result = this.input.handleMouse({ ...event, y: event.y - (this.inputRow ?? 0) });
|
|
127
137
|
return { handled: true, focus: true, render: true, ...result };
|
|
128
138
|
}
|
|
129
139
|
return undefined;
|
|
@@ -143,6 +153,8 @@ export class WorkflowInspector implements Focusable {
|
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
render(width: number): string[] {
|
|
156
|
+
this.width = width;
|
|
157
|
+
if (!this.canInspect() && this.detailAgentId !== undefined) this.closeDetails();
|
|
146
158
|
const outerWidth = Math.max(4, width);
|
|
147
159
|
const innerWidth = Math.max(1, outerWidth - 4);
|
|
148
160
|
const snapshot = this.snapshotProvider();
|
|
@@ -154,7 +166,7 @@ export class WorkflowInspector implements Focusable {
|
|
|
154
166
|
: agents.find((entry) => entry.agent.id === this.detailAgentId);
|
|
155
167
|
if (this.detailAgentId !== undefined && !selected) this.closeDetails();
|
|
156
168
|
|
|
157
|
-
const totalHeight =
|
|
169
|
+
const totalHeight = Math.max(3, this.tui.terminal.rows - 1);
|
|
158
170
|
const innerHeight = Math.max(1, totalHeight - 2);
|
|
159
171
|
const interior = selected
|
|
160
172
|
? this.chatInterior(selected, innerWidth, innerHeight)
|
|
@@ -196,7 +208,7 @@ export class WorkflowInspector implements Focusable {
|
|
|
196
208
|
` ${this.columns(width)}`,
|
|
197
209
|
...body,
|
|
198
210
|
this.theme.fg("dim", "─".repeat(width)),
|
|
199
|
-
` ${this.theme.fg("dim", `↑↓ select ·
|
|
211
|
+
` ${this.followUpError ? this.theme.fg("error", this.followUpError) : this.theme.fg("dim", `↑↓ select · ${this.canInspect() ? "enter inspect" : "resize to inspect (80×24)"} · X stop · esc back`)}`,
|
|
200
212
|
], height);
|
|
201
213
|
}
|
|
202
214
|
|
|
@@ -207,35 +219,36 @@ export class WorkflowInspector implements Focusable {
|
|
|
207
219
|
): string[] {
|
|
208
220
|
this.clickRows.clear();
|
|
209
221
|
const { agent } = entry;
|
|
210
|
-
const
|
|
222
|
+
const canMessage = agent.status === "running" && this.live !== undefined;
|
|
223
|
+
this.input.focused = this._focused && canMessage;
|
|
224
|
+
const editorLines = canMessage ? this.input.render(Math.max(1, width - 1))
|
|
225
|
+
: [this.theme.fg("dim", agent.status === "queued" ? "Agent has not started yet." : "Agent has finished.")];
|
|
226
|
+
const transcript = this.live?.transcript?.(agent.id);
|
|
227
|
+
const queue = [...(transcript?.steering ?? []), ...(transcript?.followUp ?? [])];
|
|
228
|
+
const queueLines = queue.length ? [this.theme.fg("dim", truncateDisplay(`Queued: ${queue.join(" · ")}`, width))] : [];
|
|
229
|
+
const bodyHeight = Math.max(0, height - 5 - editorLines.length - queueLines.length);
|
|
211
230
|
const rows = this.chatRows(agent, width);
|
|
212
231
|
const maxStart = Math.max(0, rows.length - bodyHeight);
|
|
213
232
|
const start = Math.max(0, maxStart - this.detailScroll);
|
|
214
233
|
const body = fitWorkflowViewerRows(rows.slice(start, start + bodyHeight), bodyHeight);
|
|
215
234
|
const elapsed = agent.startedAt === undefined ? "queued" : formatDuration((agent.doneAt ?? Date.now()) - agent.startedAt);
|
|
216
235
|
const details = [
|
|
217
|
-
|
|
218
|
-
|
|
236
|
+
agentModelName(agent),
|
|
237
|
+
thinkingLabel(agent.thinkingLevel),
|
|
219
238
|
elapsed,
|
|
220
|
-
`${agent.toolUses} tool${agent.toolUses === 1 ? "" : "s"}`,
|
|
221
239
|
].join(this.theme.fg("dim", " · "));
|
|
222
|
-
|
|
223
|
-
this.input.focused = this._focused && canMessage;
|
|
224
|
-
this.inputRow = canMessage ? height - 1 : undefined;
|
|
225
|
-
const inputLine = canMessage
|
|
226
|
-
? this.input.render(Math.max(1, width - 1))[0] ?? ""
|
|
227
|
-
: this.theme.fg("dim", agent.status === "queued" ? "Agent has not started yet." : "Agent has finished.");
|
|
240
|
+
this.inputRow = canMessage ? 4 + bodyHeight + queueLines.length : undefined;
|
|
228
241
|
const help = this.followUpError
|
|
229
242
|
? this.theme.fg("error", truncateDisplay(this.followUpError, Math.max(1, width - 1)))
|
|
230
|
-
: this.theme.fg("dim", "enter
|
|
243
|
+
: this.theme.fg("dim", "enter steer · alt+enter queue · /model · /thinking · esc back");
|
|
231
244
|
|
|
232
245
|
return fitWorkflowViewerRows([
|
|
233
246
|
` ${statusIcon(agent.status, this.theme)} ${this.theme.fg("accent", this.theme.bold(agent.label))}`,
|
|
234
247
|
` ${details}`,
|
|
235
248
|
this.theme.fg("dim", "─".repeat(width)),
|
|
236
249
|
...body,
|
|
237
|
-
|
|
238
|
-
|
|
250
|
+
...queueLines,
|
|
251
|
+
...editorLines,
|
|
239
252
|
` ${help}`,
|
|
240
253
|
], height);
|
|
241
254
|
}
|
|
@@ -250,8 +263,23 @@ export class WorkflowInspector implements Focusable {
|
|
|
250
263
|
this.tui.requestRender();
|
|
251
264
|
}
|
|
252
265
|
|
|
266
|
+
private canInspect(): boolean {
|
|
267
|
+
return this.width >= 80 && this.tui.terminal.rows >= 24;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private stopSelectedAgent(): void {
|
|
271
|
+
const agent = this.agents()[this.selected]?.agent;
|
|
272
|
+
if (!agent || !this.live?.stopAgent) return;
|
|
273
|
+
try {
|
|
274
|
+
this.live.stopAgent(agent.id);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
this.followUpError = unknownErrorMessage(error);
|
|
277
|
+
}
|
|
278
|
+
this.tui.requestRender();
|
|
279
|
+
}
|
|
280
|
+
|
|
253
281
|
private openDetails(count: number): void {
|
|
254
|
-
if (count === 0) return;
|
|
282
|
+
if (count === 0 || !this.canInspect()) return;
|
|
255
283
|
const entry = this.agents()[this.selected];
|
|
256
284
|
if (!entry) return;
|
|
257
285
|
this.detailAgentId = entry.agent.id;
|
|
@@ -265,7 +293,7 @@ export class WorkflowInspector implements Focusable {
|
|
|
265
293
|
this.detailAgentId = undefined;
|
|
266
294
|
this.detailScroll = 0;
|
|
267
295
|
this.followUpError = undefined;
|
|
268
|
-
this.input.
|
|
296
|
+
this.input.setText("");
|
|
269
297
|
this.input.focused = false;
|
|
270
298
|
this.tui.requestRender();
|
|
271
299
|
}
|
|
@@ -280,20 +308,20 @@ export class WorkflowInspector implements Focusable {
|
|
|
280
308
|
return this.agents().some((entry) => entry.agent.id === this.detailAgentId && entry.agent.status === "running");
|
|
281
309
|
}
|
|
282
310
|
|
|
283
|
-
private async submitFollowUp(value: string): Promise<void> {
|
|
311
|
+
private async submitFollowUp(value: string, steer = true): Promise<void> {
|
|
284
312
|
const message = value.trim();
|
|
285
313
|
const agentId = this.detailAgentId;
|
|
286
314
|
if (!message || agentId === undefined || !this.live || this.sending) return;
|
|
287
315
|
this.sending = true;
|
|
288
316
|
this.followUpError = undefined;
|
|
289
|
-
this.input.
|
|
317
|
+
this.input.setText("");
|
|
290
318
|
this.tui.requestRender();
|
|
291
319
|
try {
|
|
292
|
-
await this.live.followUp(agentId, message);
|
|
320
|
+
await this.live.followUp(agentId, message, steer);
|
|
293
321
|
this.detailScroll = 0;
|
|
294
322
|
} catch (error) {
|
|
295
323
|
this.followUpError = unknownErrorMessage(error);
|
|
296
|
-
this.input.
|
|
324
|
+
this.input.setText(message);
|
|
297
325
|
} finally {
|
|
298
326
|
this.sending = false;
|
|
299
327
|
this.tui.requestRender();
|
|
@@ -319,6 +347,12 @@ export class WorkflowInspector implements Focusable {
|
|
|
319
347
|
}
|
|
320
348
|
|
|
321
349
|
private chatRows(agent: AgentRowSnapshot, width: number): string[] {
|
|
350
|
+
const transcript = this.live?.transcript?.(agent.id);
|
|
351
|
+
if (transcript) {
|
|
352
|
+
let view = this.transcriptViews.get(agent.id);
|
|
353
|
+
if (!view) { view = new AgentTranscriptView(); this.transcriptViews.set(agent.id, view); }
|
|
354
|
+
return view.render(transcript, width, this.tui as TUI, transcript.cwd ?? process.cwd());
|
|
355
|
+
}
|
|
322
356
|
const messages = this.live?.conversation(agent.id) ?? [];
|
|
323
357
|
if (messages.length === 0) return [` ${this.theme.fg("dim", "Waiting for agent activity…")}`];
|
|
324
358
|
return messages.flatMap((message) => this.chatMessageRows(message, width));
|
|
@@ -381,8 +415,8 @@ export class WorkflowInspector implements Focusable {
|
|
|
381
415
|
const modelWidth = Math.max(12, Math.floor(width * 0.28));
|
|
382
416
|
const activityWidth = Math.max(8, width - taskWidth - modelWidth - 7);
|
|
383
417
|
const task = this.cell(agent.label, taskWidth, agent.status === "failed" ? "error" : "text");
|
|
384
|
-
const model = this.cell(
|
|
385
|
-
const activity = this.cell(
|
|
418
|
+
const model = this.cell(agentModelName(agent), modelWidth, "muted");
|
|
419
|
+
const activity = this.cell(thinkingLabel(agent.thinkingLevel), activityWidth, "dim");
|
|
386
420
|
const row = `${marker} ${statusIcon(agent.status, this.theme)} ${task} ${model} ${activity}`;
|
|
387
421
|
return selected ? this.theme.bg("selectedBg", fitWorkflowViewerRow(row, width)) : fitWorkflowViewerRow(row, width);
|
|
388
422
|
}
|
|
@@ -390,21 +424,15 @@ export class WorkflowInspector implements Focusable {
|
|
|
390
424
|
private columns(width: number): string {
|
|
391
425
|
const taskWidth = Math.max(12, Math.floor(width * 0.36));
|
|
392
426
|
const modelWidth = Math.max(12, Math.floor(width * 0.28));
|
|
393
|
-
return this.theme.fg("dim", ` ${"Task".padEnd(taskWidth + 2)}${"Model".padEnd(modelWidth + 1)}
|
|
427
|
+
return this.theme.fg("dim", ` ${"Task".padEnd(taskWidth + 2)}${"Model".padEnd(modelWidth + 1)}Thinking`);
|
|
394
428
|
}
|
|
395
429
|
|
|
396
430
|
private summary(snapshot: WorkflowProgressSnapshot, agents: readonly BoardAgent[]): string {
|
|
397
|
-
const
|
|
398
|
-
for (const { agent } of agents) counts[agent.status]++;
|
|
431
|
+
const done = agents.filter(({ agent }) => agent.status === "done").length;
|
|
399
432
|
const parts = [
|
|
400
|
-
|
|
401
|
-
`${counts.running} running`,
|
|
402
|
-
`${counts.queued} queued`,
|
|
403
|
-
this.theme.fg("success", `${counts.done} done`),
|
|
404
|
-
...(counts.failed > 0 ? [this.theme.fg("error", `${counts.failed} failed`)] : []),
|
|
433
|
+
`${done}/${agents.length} done`,
|
|
405
434
|
formatDuration((snapshot.doneAt ?? Date.now()) - snapshot.startedAt),
|
|
406
|
-
|
|
407
|
-
].filter(Boolean);
|
|
435
|
+
];
|
|
408
436
|
return parts.join(this.theme.fg("dim", " · "));
|
|
409
437
|
}
|
|
410
438
|
|
|
@@ -419,17 +447,3 @@ export class WorkflowInspector implements Focusable {
|
|
|
419
447
|
return `${this.theme.fg("border", "│")} ${fitted}${padding} ${this.theme.fg("border", "│")}`;
|
|
420
448
|
}
|
|
421
449
|
}
|
|
422
|
-
|
|
423
|
-
function shortModel(model: string): string {
|
|
424
|
-
return model.replace(/^openai-codex\//, "codex/").replace(/^anthropic\//, "");
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function agentActivity(agent: AgentRowSnapshot): string {
|
|
428
|
-
if (agent.status === "queued") return "queued";
|
|
429
|
-
if (agent.status === "failed") return agent.error ?? "failed";
|
|
430
|
-
const parts: string[] = [];
|
|
431
|
-
if (agent.lastTool) parts.push(agent.lastTool);
|
|
432
|
-
if (agent.toolUses > 0) parts.push(`${agent.toolUses} tool${agent.toolUses === 1 ? "" : "s"}`);
|
|
433
|
-
if (agent.startedAt !== undefined) parts.push(formatDuration((agent.doneAt ?? Date.now()) - agent.startedAt));
|
|
434
|
-
return parts.join(" · ") || agent.status;
|
|
435
|
-
}
|
|
@@ -1,78 +1,38 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
3
|
import type { AgentRowSnapshot, WorkflowProgressSnapshot } from "../progress-types.ts";
|
|
3
|
-
import { formatWorkflowUsageLine } from "../usage.ts";
|
|
4
4
|
import { formatDuration, statusIcon } from "./workflow-format.ts";
|
|
5
5
|
import { toDisplayLine } from "./display-text.ts";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export function renderWorkflowWidgetLines(snapshot: WorkflowProgressSnapshot, theme: Theme): string[] {
|
|
11
|
-
const agents = snapshot.phases.flatMap((phase) => phase.agents);
|
|
12
|
-
const ordered = [...agents].sort((left, right) => statusOrder(left.status) - statusOrder(right.status));
|
|
13
|
-
const counts = countAgents(agents);
|
|
14
|
-
const elapsed = formatDuration((snapshot.doneAt ?? Date.now()) - snapshot.startedAt);
|
|
15
|
-
const active = counts.running + counts.queued;
|
|
16
|
-
const icon = active > 0 ? theme.fg("accent", "●") : theme.fg("success", "✓");
|
|
17
|
-
const summary = [
|
|
18
|
-
`${counts.running} running`,
|
|
19
|
-
`${counts.queued} queued`,
|
|
20
|
-
`${counts.done}/${counts.total} done`,
|
|
21
|
-
...(counts.failed > 0 ? [`${counts.failed} failed`] : []),
|
|
22
|
-
elapsed,
|
|
23
|
-
].join(` ${theme.fg("dim", "·")} `);
|
|
24
|
-
const lines = [
|
|
25
|
-
`${icon} ${theme.bold(snapshot.title)} ${theme.fg("dim", `· ${snapshot.currentPhase} · ${summary}`)}`,
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
const footer = footerLine(snapshot, theme);
|
|
29
|
-
const bodyLimit = Math.max(0, MAX_WIDGET_LINES - lines.length - 1);
|
|
30
|
-
for (const agent of ordered.slice(0, bodyLimit)) lines.push(agentLine(agent, theme));
|
|
31
|
-
const hidden = ordered.length - Math.min(ordered.length, bodyLimit);
|
|
32
|
-
if (hidden > 0 && lines.length < MAX_WIDGET_LINES) {
|
|
33
|
-
lines.push(theme.fg("dim", ` +${hidden} more · /workflow`));
|
|
34
|
-
} else if (footer && lines.length < MAX_WIDGET_LINES) {
|
|
35
|
-
lines.push(footer);
|
|
36
|
-
}
|
|
37
|
-
return lines.map((line) => line.replace(/[\r\n]+/g, " "));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface AgentCounts {
|
|
41
|
-
queued: number;
|
|
42
|
-
running: number;
|
|
43
|
-
done: number;
|
|
44
|
-
failed: number;
|
|
45
|
-
total: number;
|
|
7
|
+
export function thinkingLabel(level?: string): string {
|
|
8
|
+
if (!level) return "—";
|
|
9
|
+
return level === "xhigh" ? "XHigh" : level[0].toUpperCase() + level.slice(1);
|
|
46
10
|
}
|
|
47
11
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
for (const agent of agents) counts[agent.status]++;
|
|
51
|
-
return counts;
|
|
12
|
+
export function agentModelName(agent: AgentRowSnapshot): string {
|
|
13
|
+
return toDisplayLine(agent.modelName ?? agent.model ?? "—", 48);
|
|
52
14
|
}
|
|
53
15
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const parts = [usage, latest ? toDisplayLine(latest, 100) : undefined, "/workflow"].filter(Boolean);
|
|
73
|
-
return theme.fg("dim", ` ${parts.join(" · ")}`);
|
|
16
|
+
/** Compact live board; detailed activity is available through /workflow. */
|
|
17
|
+
export function renderWorkflowWidgetLines(snapshot: WorkflowProgressSnapshot, theme: Theme): string[] {
|
|
18
|
+
const agents = snapshot.phases.flatMap((phase) => phase.agents);
|
|
19
|
+
const ordered = [...agents].sort((a, b) => order(a.status) - order(b.status));
|
|
20
|
+
const done = agents.filter((agent) => agent.status === "done").length;
|
|
21
|
+
const elapsed = formatDuration((snapshot.doneAt ?? Date.now()) - snapshot.startedAt);
|
|
22
|
+
const shown = ordered.slice(0, 7);
|
|
23
|
+
const names = shown.map((agent) => toDisplayLine(agent.label, 32));
|
|
24
|
+
const models = shown.map(agentModelName);
|
|
25
|
+
const nameWidth = Math.max(0, ...names.map(visibleWidth));
|
|
26
|
+
const modelWidth = Math.max(0, ...models.map(visibleWidth));
|
|
27
|
+
const pad = (text: string, width: number) => text + " ".repeat(Math.max(0, width - visibleWidth(text)));
|
|
28
|
+
const lines = [`${theme.bold(snapshot.title)} ${theme.fg("dim", `· ${done}/${agents.length} done · ${elapsed}`)}`];
|
|
29
|
+
shown.forEach((agent, index) => lines.push(
|
|
30
|
+
` ${statusIcon(agent.status, theme)} ${pad(names[index], nameWidth)} ${theme.fg("muted", pad(models[index], modelWidth))} ${theme.fg("dim", thinkingLabel(agent.thinkingLevel))}`,
|
|
31
|
+
));
|
|
32
|
+
if (agents.length > shown.length) lines.push(theme.fg("dim", ` +${agents.length - shown.length} more · /workflow`));
|
|
33
|
+
return lines;
|
|
74
34
|
}
|
|
75
35
|
|
|
76
|
-
function
|
|
77
|
-
return
|
|
36
|
+
function order(status: AgentRowSnapshot["status"]): number {
|
|
37
|
+
return status === "running" || status === "stopping" ? 0 : status === "queued" ? 1 : 2;
|
|
78
38
|
}
|
|
@@ -50,7 +50,7 @@ export function publishVerifiedKeptProgress(
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export interface LensVerificationPipelineOptions {
|
|
53
|
-
api: Pick<WorkflowApi, "agent" | "parallel" | "phase" | "progress" | "log">;
|
|
53
|
+
api: Pick<WorkflowApi, "agent" | "modelProfile" | "parallel" | "phase" | "progress" | "log">;
|
|
54
54
|
lenses: readonly AdvisoryLens[];
|
|
55
55
|
perLens: number;
|
|
56
56
|
finderPhase?: "Find" | "Hypothesize";
|
|
@@ -77,7 +77,7 @@ export async function runLensVerificationPipeline(
|
|
|
77
77
|
const result = await api.agent(finderPrompt(lens), {
|
|
78
78
|
phase: finderPhase, label: `${finderPhase.toLowerCase()}:${lens.label}`,
|
|
79
79
|
tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS,
|
|
80
|
-
|
|
80
|
+
...api.modelProfile("small"), schema: AdvisoryCandidatesSchema,
|
|
81
81
|
});
|
|
82
82
|
const raw = identifyCandidates(result.candidates.slice(0, perLens), lens.label);
|
|
83
83
|
rawCandidates += raw.length;
|
|
@@ -105,7 +105,7 @@ export async function runLensVerificationPipeline(
|
|
|
105
105
|
const judged = await api.agent(`${verifierPrompt(candidate)}\nCandidate record: ${JSON.stringify(candidate)}`, {
|
|
106
106
|
phase: "Verify", label: `verify:${location.file.split("/").pop() ?? location.file}`,
|
|
107
107
|
tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS,
|
|
108
|
-
|
|
108
|
+
...api.modelProfile("small"), schema: AdvisoryVerdictSchema,
|
|
109
109
|
});
|
|
110
110
|
recordVerdictProgress(api.progress, candidate, judged, () => { refuted += 1; });
|
|
111
111
|
return { ...candidate, verdict: judged.verdict, evidence: judged.evidence, confidence: judged.confidence };
|
|
@@ -115,7 +115,7 @@ export async function runLensVerificationPipeline(
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
export async function synthesizeAdvisoryReport(
|
|
118
|
-
api: Pick<WorkflowApi, "agent" | "parallel" | "phase">,
|
|
118
|
+
api: Pick<WorkflowApi, "agent" | "modelProfile" | "parallel" | "phase">,
|
|
119
119
|
prompt: string,
|
|
120
120
|
ranked: readonly AdvisoryVerified[],
|
|
121
121
|
coverage: AdvisoryStageCoverage[],
|
|
@@ -123,7 +123,7 @@ export async function synthesizeAdvisoryReport(
|
|
|
123
123
|
api.phase("Synthesize");
|
|
124
124
|
const [report] = await collectAdvisoryStage(api, "Synthesize", [{ id: "synthesize", run: () => api.agent(
|
|
125
125
|
SYNTHESIS_ID_INSTRUCTIONS + prompt,
|
|
126
|
-
{ phase: "Synthesize", label: "synthesize", tools: [],
|
|
126
|
+
{ phase: "Synthesize", label: "synthesize", tools: [], ...api.modelProfile("medium"), resume: "read-only", schema: AdvisorySynthesisSchema },
|
|
127
127
|
) }], coverage);
|
|
128
128
|
return resolveAdvisorySynthesis(report, ranked, {
|
|
129
129
|
impact: "Impact not restated by verification.",
|