pi-workflow-engine 0.11.0 → 0.12.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/.pi/extensions/pi-workflow-engine/index.ts +255 -97
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +0 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +3 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +27 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +49 -45
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +95 -26
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +158 -203
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +48 -42
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +11 -3
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +3 -3
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +38 -0
- package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +70 -4
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +16 -130
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +9 -5
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +109 -60
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +166 -18
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +2 -2
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +12 -6
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +88 -1
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +72 -117
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +74 -38
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +11 -30
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +5 -5
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +3 -4
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
- package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
- package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/types.ts +49 -15
- package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +23 -3
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +40 -3
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +18 -42
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +10 -17
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +8 -15
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +8 -15
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +72 -141
- package/USAGE.md +211 -17
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { AgentRowSnapshot, WorkflowProgressSnapshot } from "../progress.ts";
|
|
4
|
-
import
|
|
3
|
+
import type { AgentRowSnapshot, WorkflowLaneItemStatus, WorkflowProgressSnapshot } from "../progress-types.ts";
|
|
4
|
+
import { formatWorkflowUsageLine } from "../usage.ts";
|
|
5
5
|
|
|
6
6
|
export type WorkflowDisplayStatus = WorkflowLaneItemStatus | "queued" | "done" | "failed";
|
|
7
7
|
export type WorkflowThemeColor = Parameters<Theme["fg"]>[0];
|
|
@@ -49,10 +49,6 @@ export function statusIcon(status: WorkflowDisplayStatus, theme: Theme): string
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export function badge(label: string, color: WorkflowThemeColor, theme: Theme): string {
|
|
53
|
-
return theme.fg(color, `[${label}]`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
52
|
export function truncateDisplay(text: string, width: number): string {
|
|
57
53
|
if (width <= 0) return "";
|
|
58
54
|
return truncateToWidth(text, width);
|
|
@@ -97,6 +93,30 @@ export interface WorkflowStatusSource {
|
|
|
97
93
|
readonly counters: readonly { readonly key: string; readonly label: string; readonly value: number }[];
|
|
98
94
|
}
|
|
99
95
|
|
|
96
|
+
interface WorkflowInspectionSource {
|
|
97
|
+
readonly name: string;
|
|
98
|
+
readonly snapshot: WorkflowProgressSnapshot | (() => WorkflowProgressSnapshot);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function workflowInspectionSnapshot(inspection: WorkflowInspectionSource): WorkflowProgressSnapshot {
|
|
102
|
+
return typeof inspection.snapshot === "function" ? inspection.snapshot() : inspection.snapshot;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function formatWorkflowInspection(inspection: WorkflowInspectionSource): string {
|
|
106
|
+
const snapshot = workflowInspectionSnapshot(inspection);
|
|
107
|
+
const counts = countSnapshotAgents(snapshot);
|
|
108
|
+
const lines = [
|
|
109
|
+
`Workflow inspector: ${inspection.name}`,
|
|
110
|
+
`Run: ${snapshot.runId}`,
|
|
111
|
+
`Phase: ${snapshot.currentPhase}`,
|
|
112
|
+
`Agents: ${counts.running} running, ${counts.queued} queued, ${counts.done} done, ${counts.failed} failed`,
|
|
113
|
+
];
|
|
114
|
+
const usage = formatWorkflowUsageLine(snapshot.usage);
|
|
115
|
+
if (usage) lines.push(usage);
|
|
116
|
+
if (snapshot.logs.length > 0) lines.push("Recent log:", ...snapshot.logs.slice(-8).map((entry) => `- ${entry}`));
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
export function statusText(snapshot: WorkflowProgressSnapshot, theme: Theme): string | undefined {
|
|
101
121
|
const counts = countSnapshotAgents(snapshot);
|
|
102
122
|
return statusTextFromCounts(snapshot, counts, theme);
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { matchesKey, type TUI, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { AgentRowSnapshot, WorkflowLaneItemSnapshot, WorkflowProgressSnapshot } from "../progress.ts";
|
|
3
|
+
import type { AgentRowSnapshot, WorkflowLaneItemSnapshot, WorkflowProgressSnapshot } from "../progress-types.ts";
|
|
4
|
+
import { formatWorkflowUsageLine } from "../usage.ts";
|
|
4
5
|
import { agentDetailParts, formatCount, formatDuration, statusIcon, truncateDisplay } from "./workflow-format.ts";
|
|
6
|
+
import {
|
|
7
|
+
centerWorkflowViewerViewport,
|
|
8
|
+
fitWorkflowViewerRow,
|
|
9
|
+
fitWorkflowViewerRows,
|
|
10
|
+
workflowViewerHeight,
|
|
11
|
+
} from "./workflow-viewer-layout.ts";
|
|
5
12
|
|
|
6
|
-
type Section = "Overview" | "Agents" | "Findings" | "Logs";
|
|
13
|
+
type Section = "Overview" | "Agents" | "Findings" | "Logs" | "Result";
|
|
7
14
|
|
|
8
15
|
interface InspectorLine {
|
|
9
16
|
text: string;
|
|
@@ -11,11 +18,20 @@ interface InspectorLine {
|
|
|
11
18
|
key?: string;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
const
|
|
21
|
+
const BASE_SECTIONS: readonly Section[] = ["Overview", "Agents", "Findings", "Logs"];
|
|
22
|
+
const RESULT_SECTIONS: readonly Section[] = [...BASE_SECTIONS, "Result"];
|
|
23
|
+
const MAX_RESULT_SOURCE_LINES = 200;
|
|
24
|
+
const MAX_RESULT_RENDERED_LINES = 400;
|
|
25
|
+
|
|
26
|
+
export interface WorkflowInspectorOutcome {
|
|
27
|
+
readonly label: string;
|
|
28
|
+
readonly text: string;
|
|
29
|
+
}
|
|
15
30
|
|
|
16
31
|
export class WorkflowInspector {
|
|
17
32
|
private sectionIndex = 0;
|
|
18
|
-
private
|
|
33
|
+
private contentWidth = 80;
|
|
34
|
+
private readonly selected: Record<Section, number> = { Overview: 0, Agents: 0, Findings: 0, Logs: 0, Result: 0 };
|
|
19
35
|
private readonly expanded = new Set<string>();
|
|
20
36
|
|
|
21
37
|
constructor(
|
|
@@ -23,6 +39,7 @@ export class WorkflowInspector {
|
|
|
23
39
|
private readonly tui: Pick<TUI, "requestRender" | "terminal">,
|
|
24
40
|
private readonly theme: Theme,
|
|
25
41
|
private readonly close: () => void,
|
|
42
|
+
private readonly outcome?: WorkflowInspectorOutcome,
|
|
26
43
|
) {}
|
|
27
44
|
|
|
28
45
|
handleInput(data: string): void {
|
|
@@ -32,7 +49,7 @@ export class WorkflowInspector {
|
|
|
32
49
|
}
|
|
33
50
|
|
|
34
51
|
if (matchesKey(data, "tab")) {
|
|
35
|
-
this.sectionIndex = (this.sectionIndex + 1) %
|
|
52
|
+
this.sectionIndex = (this.sectionIndex + 1) % this.sections().length;
|
|
36
53
|
this.requestRender();
|
|
37
54
|
return;
|
|
38
55
|
}
|
|
@@ -60,47 +77,52 @@ export class WorkflowInspector {
|
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
render(width: number): string[] {
|
|
63
|
-
const w = Math.max(
|
|
80
|
+
const w = Math.max(4, width);
|
|
64
81
|
const inner = Math.max(1, w - 4);
|
|
82
|
+
this.contentWidth = inner;
|
|
65
83
|
const th = this.theme;
|
|
66
84
|
const snapshot = this.snapshotProvider();
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
lines.push(th.fg("border", `╭${"─".repeat(w - 2)}╮`));
|
|
76
|
-
lines.push(this.row(` ${th.fg("accent", th.bold("Workflow Inspector"))} ${th.fg("dim", snapshot.title)}`, inner));
|
|
77
|
-
lines.push(this.row(` ${this.tabs()}`, inner));
|
|
78
|
-
lines.push(this.row(th.fg("dim", "─".repeat(inner)), inner));
|
|
79
|
-
|
|
80
|
-
for (const line of visible) lines.push(this.row(line.text, inner));
|
|
81
|
-
while (visible.length < maxBody) {
|
|
82
|
-
visible.push({ text: "" });
|
|
83
|
-
lines.push(this.row("", inner));
|
|
85
|
+
const section = this.currentSection();
|
|
86
|
+
let body = this.sectionLines(section, snapshot, inner);
|
|
87
|
+
const selectableCount = body.filter((line) => line.selectable).length;
|
|
88
|
+
const clampedSelection = Math.min(Math.max(0, selectableCount - 1), this.selected[section]);
|
|
89
|
+
if (clampedSelection !== this.selected[section]) {
|
|
90
|
+
this.selected[section] = clampedSelection;
|
|
91
|
+
body = this.sectionLines(section, snapshot, inner);
|
|
84
92
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
const innerHeight = Math.max(1, workflowViewerHeight(this.tui.terminal.rows) - 2);
|
|
94
|
+
const maxBody = Math.max(1, innerHeight - 5);
|
|
95
|
+
const viewport = centerWorkflowViewerViewport(body, maxBody, this.selectedLineIndex(body));
|
|
96
|
+
const pct = `${viewport.percentage}%`;
|
|
97
|
+
const selected = this.selected[section] + 1;
|
|
98
|
+
const count = Math.max(1, selectableCount);
|
|
99
|
+
const content = fitWorkflowViewerRows(viewport.visible.map((line) => line.text), maxBody);
|
|
100
|
+
const interior = fitWorkflowViewerRows(
|
|
101
|
+
[
|
|
102
|
+
` ${th.fg("accent", th.bold("Workflow Inspector"))} ${th.fg("dim", snapshot.title)}`,
|
|
103
|
+
` ${this.tabs()}`,
|
|
104
|
+
th.fg("dim", "─".repeat(inner)),
|
|
105
|
+
...content,
|
|
106
|
+
th.fg("dim", "─".repeat(inner)),
|
|
92
107
|
`${th.fg("dim", `${body.length} lines · ${pct} · ${selected}/${count}`)} ${th.fg("dim", "· tab sections · ↑↓ select · enter expand/collapse · q/esc close")}`,
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
],
|
|
109
|
+
innerHeight,
|
|
95
110
|
);
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
return [
|
|
112
|
+
th.fg("border", `╭${"─".repeat(Math.max(0, w - 2))}╮`),
|
|
113
|
+
...interior.map((line) => this.row(line, inner)),
|
|
114
|
+
th.fg("border", `╰${"─".repeat(Math.max(0, w - 2))}╯`),
|
|
115
|
+
].map((line) => truncateDisplay(line, w));
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
invalidate(): void {}
|
|
101
119
|
|
|
102
120
|
private currentSection(): Section {
|
|
103
|
-
return
|
|
121
|
+
return this.sections()[this.sectionIndex] ?? "Overview";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private sections(): readonly Section[] {
|
|
125
|
+
return this.outcome ? RESULT_SECTIONS : BASE_SECTIONS;
|
|
104
126
|
}
|
|
105
127
|
|
|
106
128
|
private requestRender(): void {
|
|
@@ -108,24 +130,23 @@ export class WorkflowInspector {
|
|
|
108
130
|
}
|
|
109
131
|
|
|
110
132
|
private tabs(): string {
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
return label;
|
|
133
|
+
return this.sections().map((section, index) => {
|
|
134
|
+
return index === this.sectionIndex ? this.theme.fg("accent", this.theme.bold(section)) : this.theme.fg("muted", section);
|
|
114
135
|
}).join(this.theme.fg("dim", " | "));
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
private row(content: string, innerWidth: number): string {
|
|
118
|
-
const
|
|
119
|
-
return this.theme.fg("border", "│") + " " +
|
|
139
|
+
const fitted = fitWorkflowViewerRow(content, innerWidth);
|
|
140
|
+
return this.theme.fg("border", "│") + " " + fitted + " " + this.theme.fg("border", "│");
|
|
120
141
|
}
|
|
121
142
|
|
|
122
143
|
private itemCount(section: Section): number {
|
|
123
|
-
return this.sectionLines(section, this.snapshotProvider(),
|
|
144
|
+
return this.sectionLines(section, this.snapshotProvider(), this.contentWidth).filter((line) => line.selectable).length;
|
|
124
145
|
}
|
|
125
146
|
|
|
126
147
|
private selectedKey(section: Section): string | undefined {
|
|
127
148
|
let index = -1;
|
|
128
|
-
for (const line of this.sectionLines(section, this.snapshotProvider(),
|
|
149
|
+
for (const line of this.sectionLines(section, this.snapshotProvider(), this.contentWidth)) {
|
|
129
150
|
if (!line.selectable) continue;
|
|
130
151
|
index++;
|
|
131
152
|
if (index === this.selected[section]) return line.key;
|
|
@@ -153,6 +174,8 @@ export class WorkflowInspector {
|
|
|
153
174
|
return this.findingLines(snapshot, width);
|
|
154
175
|
case "Logs":
|
|
155
176
|
return this.logLines(snapshot);
|
|
177
|
+
case "Result":
|
|
178
|
+
return this.resultLines(width);
|
|
156
179
|
}
|
|
157
180
|
}
|
|
158
181
|
|
|
@@ -162,6 +185,8 @@ export class WorkflowInspector {
|
|
|
162
185
|
{ text: `${this.theme.fg("muted", "Phase")} ${this.theme.fg("accent", snapshot.currentPhase)}`, selectable: true, key: "overview:phase" },
|
|
163
186
|
{ text: `${this.theme.fg("muted", "Elapsed")} ${elapsed}`, selectable: true, key: "overview:elapsed" },
|
|
164
187
|
];
|
|
188
|
+
const usage = formatWorkflowUsageLine(snapshot.usage);
|
|
189
|
+
if (usage) lines.push({ text: usage, selectable: true, key: "overview:usage" });
|
|
165
190
|
if (snapshot.counters.length > 0) {
|
|
166
191
|
lines.push({ text: this.theme.fg("dim", "Counters") });
|
|
167
192
|
for (const counter of snapshot.counters) {
|
|
@@ -260,9 +285,26 @@ export class WorkflowInspector {
|
|
|
260
285
|
return { text, selectable: true, key };
|
|
261
286
|
});
|
|
262
287
|
}
|
|
263
|
-
}
|
|
264
288
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
289
|
+
private resultLines(width: number): InspectorLine[] {
|
|
290
|
+
if (!this.outcome) return [{ text: this.theme.fg("dim", "No retained outcome is available for this run.") }];
|
|
291
|
+
const source = this.outcome.text.split("\n");
|
|
292
|
+
const lines: InspectorLine[] = [{ text: this.theme.fg("dim", this.outcome.label) }];
|
|
293
|
+
const shown = source.slice(0, MAX_RESULT_SOURCE_LINES);
|
|
294
|
+
for (const sourceLine of shown) {
|
|
295
|
+
const wrapped = wrapTextWithAnsi(sourceLine || " ", Math.max(10, width - 2));
|
|
296
|
+
for (const line of wrapped) {
|
|
297
|
+
if (lines.length >= MAX_RESULT_RENDERED_LINES) break;
|
|
298
|
+
const index = lines.length;
|
|
299
|
+
const selected = this.currentSection() === "Result" && this.selected.Result === index - 1;
|
|
300
|
+
const text = selected ? this.theme.bg("selectedBg", line) : line;
|
|
301
|
+
lines.push({ text, selectable: true, key: `result:${index}` });
|
|
302
|
+
}
|
|
303
|
+
if (lines.length >= MAX_RESULT_RENDERED_LINES) break;
|
|
304
|
+
}
|
|
305
|
+
if (shown.length < source.length || lines.length >= MAX_RESULT_RENDERED_LINES) {
|
|
306
|
+
lines.push({ text: this.theme.fg("dim", "… retained result truncated for display") });
|
|
307
|
+
}
|
|
308
|
+
return lines;
|
|
309
|
+
}
|
|
268
310
|
}
|
|
@@ -6,6 +6,7 @@ import { toReviewIssues } from "../review/review-issues.ts";
|
|
|
6
6
|
import { formatCount } from "./workflow-format.ts";
|
|
7
7
|
import { formatWorkflowUsageLine } from "../usage.ts";
|
|
8
8
|
import type { WorkflowPerfDetails, WorkflowResultEnvelope } from "../workflow-execution.ts";
|
|
9
|
+
import { unknownErrorMessage } from "../unknown-error.ts";
|
|
9
10
|
|
|
10
11
|
export function isWorkflowResult(value: unknown): value is WorkflowResultEnvelope {
|
|
11
12
|
if (!isRecord(value)) return false;
|
|
@@ -158,7 +159,7 @@ function safeJson(value: unknown): string {
|
|
|
158
159
|
try {
|
|
159
160
|
return JSON.stringify(value, null, 2) ?? String(value);
|
|
160
161
|
} catch (error) {
|
|
161
|
-
return
|
|
162
|
+
return unknownErrorMessage(error);
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
const VIEWPORT_HEIGHT_RATIO = 0.8;
|
|
4
|
+
const VIEWPORT_MARGIN_ROWS = 2;
|
|
5
|
+
|
|
6
|
+
export const WORKFLOW_VIEWER_OVERLAY_OPTIONS = {
|
|
7
|
+
overlay: true,
|
|
8
|
+
overlayOptions: {
|
|
9
|
+
anchor: "center",
|
|
10
|
+
width: "80%",
|
|
11
|
+
minWidth: 40,
|
|
12
|
+
maxHeight: "80%",
|
|
13
|
+
margin: 1,
|
|
14
|
+
},
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export function workflowViewerHeight(terminalRows: number): number {
|
|
18
|
+
const rows = Math.max(1, terminalRows);
|
|
19
|
+
const availableRows = Math.max(1, rows - VIEWPORT_MARGIN_ROWS);
|
|
20
|
+
const proportionalRows = Math.floor(rows * VIEWPORT_HEIGHT_RATIO);
|
|
21
|
+
return Math.min(availableRows, Math.max(3, proportionalRows));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function fitWorkflowViewerRow(text: string, width: number): string {
|
|
25
|
+
return truncateToWidth(text, Math.max(0, width), "...", true);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function fitWorkflowViewerRows(lines: readonly string[], height: number): string[] {
|
|
29
|
+
const visible = lines.slice(0, Math.max(0, height));
|
|
30
|
+
return [...visible, ...Array.from({ length: Math.max(0, height - visible.length) }, () => "")];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface WorkflowViewerViewport<T> {
|
|
34
|
+
readonly visible: readonly T[];
|
|
35
|
+
readonly percentage: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function centerWorkflowViewerViewport<T>(
|
|
39
|
+
rows: readonly T[],
|
|
40
|
+
height: number,
|
|
41
|
+
selectedIndex: number,
|
|
42
|
+
): WorkflowViewerViewport<T> {
|
|
43
|
+
const visibleHeight = Math.max(0, Math.floor(height));
|
|
44
|
+
const clampedSelection = Math.min(Math.max(0, rows.length - 1), Math.max(0, selectedIndex));
|
|
45
|
+
const maxStart = Math.max(0, rows.length - visibleHeight);
|
|
46
|
+
const start = Math.min(maxStart, Math.max(0, clampedSelection - Math.floor(visibleHeight / 2)));
|
|
47
|
+
const visible = rows.slice(start, start + visibleHeight);
|
|
48
|
+
const end = Math.min(rows.length, start + visible.length);
|
|
49
|
+
const percentage = rows.length <= visibleHeight ? 100 : Math.round((end / rows.length) * 100);
|
|
50
|
+
return { visible, percentage };
|
|
51
|
+
}
|
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { AgentRowSnapshot, PhaseSnapshot, WorkflowProgressSnapshot } from "../progress.ts";
|
|
3
|
-
import {
|
|
2
|
+
import type { AgentRowSnapshot, PhaseSnapshot, WorkflowProgressSnapshot } from "../progress-types.ts";
|
|
3
|
+
import { formatWorkflowUsageLine } from "../usage.ts";
|
|
4
|
+
import { agentDetailParts, agentLabelColor, formatCount, formatDuration, statusIcon } from "./workflow-format.ts";
|
|
4
5
|
|
|
5
|
-
const MAX_WIDGET_LINES =
|
|
6
|
-
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
7
|
-
|
|
8
|
-
export interface WorkflowWidget {
|
|
9
|
-
nextFrame(): void;
|
|
10
|
-
render(width: number, theme: Theme): string[];
|
|
11
|
-
invalidate(): void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function createWorkflowWidget(snapshotProvider: () => WorkflowProgressSnapshot): WorkflowWidget {
|
|
15
|
-
return new LiveWorkflowWidget(snapshotProvider);
|
|
16
|
-
}
|
|
6
|
+
const MAX_WIDGET_LINES = 10;
|
|
17
7
|
|
|
18
8
|
export function renderWorkflowWidgetLines(
|
|
19
9
|
snapshot: WorkflowProgressSnapshot,
|
|
20
|
-
frame: number,
|
|
21
|
-
width: number,
|
|
22
10
|
theme: Theme,
|
|
23
11
|
): string[] {
|
|
24
|
-
const safeWidth = Math.max(1, width);
|
|
25
12
|
const counts = countAgents(snapshot.phases);
|
|
26
13
|
const active = counts.running + counts.queued;
|
|
27
14
|
const elapsed = formatDuration((snapshot.doneAt ?? Date.now()) - snapshot.startedAt);
|
|
28
|
-
const
|
|
15
|
+
const activity = active > 0 ? theme.fg("accent", "●") : theme.fg("success", "✓");
|
|
29
16
|
|
|
30
17
|
const headingParts = [`${counts.done}/${counts.total} done`, elapsed];
|
|
31
18
|
if (counts.running > 0) headingParts.unshift(`${counts.running} running`);
|
|
@@ -33,10 +20,7 @@ export function renderWorkflowWidgetLines(
|
|
|
33
20
|
if (counts.failed > 0) headingParts.unshift(theme.fg("error", `${counts.failed} failed`));
|
|
34
21
|
|
|
35
22
|
const lines: string[] = [
|
|
36
|
-
|
|
37
|
-
`${spinner} ${theme.bold(snapshot.title)} ${theme.fg("dim", "·")} ${theme.fg("muted", snapshot.currentPhase)} ${theme.fg("dim", "·")} ${headingParts.join(` ${theme.fg("dim", "·")} `)}`,
|
|
38
|
-
safeWidth,
|
|
39
|
-
),
|
|
23
|
+
`${activity} ${theme.bold(snapshot.title)} ${theme.fg("dim", "·")} ${theme.fg("muted", snapshot.currentPhase)} ${theme.fg("dim", "·")} ${headingParts.join(` ${theme.fg("dim", "·")} `)}`,
|
|
40
24
|
];
|
|
41
25
|
|
|
42
26
|
const footer = footerLine(snapshot, theme);
|
|
@@ -46,53 +30,13 @@ export function renderWorkflowWidgetLines(
|
|
|
46
30
|
const visibleBodyLinesToRender = reserveHiddenLine ? body.lines.slice(0, Math.max(0, bodyBudget - 1)) : body.lines;
|
|
47
31
|
const hidden = body.hidden + (body.lines.length - visibleBodyLinesToRender.length);
|
|
48
32
|
|
|
49
|
-
|
|
33
|
+
lines.push(...visibleBodyLinesToRender);
|
|
50
34
|
if (hidden > 0 && lines.length < MAX_WIDGET_LINES) {
|
|
51
|
-
lines.push(
|
|
35
|
+
lines.push(`${theme.fg("dim", "└─")} ${theme.fg("dim", `+${hidden} more`)}`);
|
|
52
36
|
}
|
|
53
|
-
if (footer && lines.length < MAX_WIDGET_LINES) lines.push(
|
|
54
|
-
|
|
55
|
-
return lines.slice(0, MAX_WIDGET_LINES).map((line) => truncateDisplay(line, safeWidth));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
class LiveWorkflowWidget implements WorkflowWidget {
|
|
59
|
-
private frame = 0;
|
|
60
|
-
private cachedWidth: number | undefined;
|
|
61
|
-
private cachedFrame: number | undefined;
|
|
62
|
-
private cachedDoneAt: number | undefined;
|
|
63
|
-
private cachedLines: string[] | undefined;
|
|
64
|
-
|
|
65
|
-
constructor(private readonly snapshotProvider: () => WorkflowProgressSnapshot) {}
|
|
37
|
+
if (footer && lines.length < MAX_WIDGET_LINES) lines.push(footer);
|
|
66
38
|
|
|
67
|
-
|
|
68
|
-
this.frame = (this.frame + 1) % SPINNER.length;
|
|
69
|
-
this.invalidate();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
render(width: number, theme: Theme): string[] {
|
|
73
|
-
const snapshot = this.snapshotProvider();
|
|
74
|
-
if (
|
|
75
|
-
this.cachedLines &&
|
|
76
|
-
this.cachedWidth === width &&
|
|
77
|
-
this.cachedFrame === this.frame &&
|
|
78
|
-
this.cachedDoneAt === snapshot.doneAt
|
|
79
|
-
) {
|
|
80
|
-
return this.cachedLines;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
this.cachedLines = renderWorkflowWidgetLines(snapshot, this.frame, width, theme);
|
|
84
|
-
this.cachedWidth = width;
|
|
85
|
-
this.cachedFrame = this.frame;
|
|
86
|
-
this.cachedDoneAt = snapshot.doneAt;
|
|
87
|
-
return this.cachedLines;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
invalidate(): void {
|
|
91
|
-
this.cachedWidth = undefined;
|
|
92
|
-
this.cachedFrame = undefined;
|
|
93
|
-
this.cachedDoneAt = undefined;
|
|
94
|
-
this.cachedLines = undefined;
|
|
95
|
-
}
|
|
39
|
+
return lines.slice(0, MAX_WIDGET_LINES);
|
|
96
40
|
}
|
|
97
41
|
|
|
98
42
|
interface AgentCounts {
|
|
@@ -173,9 +117,10 @@ function agentLine(agent: AgentRowSnapshot, theme: Theme): string {
|
|
|
173
117
|
}
|
|
174
118
|
|
|
175
119
|
function footerLine(snapshot: WorkflowProgressSnapshot, theme: Theme): string | undefined {
|
|
120
|
+
const usage = formatWorkflowUsageLine(snapshot.usage);
|
|
176
121
|
const counters = snapshot.counters.slice(0, 4).map((counter) => `${counter.label} ${formatCount(counter.value)}`);
|
|
177
122
|
const latestLog = snapshot.logs[snapshot.logs.length - 1];
|
|
178
|
-
const parts =
|
|
123
|
+
const parts = usage ? [usage, ...counters] : counters;
|
|
179
124
|
if (latestLog) parts.push(latestLog);
|
|
180
125
|
if (parts.length === 0) return undefined;
|
|
181
126
|
return `${theme.fg("dim", "└─")} ${theme.fg("dim", parts.join(" · "))}`;
|