pi-workflow-engine 1.1.0 → 1.3.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 +199 -85
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +5 -2
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +4 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +3 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +18 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +34 -7
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +9 -3
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +51 -8
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +14 -0
- package/.pi/extensions/pi-workflow-engine/src/codemode-slots.ts +73 -0
- package/.pi/extensions/pi-workflow-engine/src/config-file.ts +52 -0
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +127 -58
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +112 -31
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +75 -18
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +5 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +7 -1
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +22 -4
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +9 -47
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +32 -4
- package/.pi/extensions/pi-workflow-engine/src/prompt-sections.ts +15 -0
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +4 -4
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/types.ts +54 -9
- package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +1 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +9 -3
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +69 -18
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +17 -6
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +8 -3
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +16 -8
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +82 -23
- package/.pi/extensions/pi-workflow-engine/src/workflow-catalog.ts +79 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +6 -2
- package/.pi/extensions/pi-workflow-engine/src/workflow-input.ts +13 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +21 -4
- package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +26 -6
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +69 -13
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +23 -4
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +27 -1
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-seal.ts +83 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +30 -4
- package/.pi/extensions/pi-workflow-engine/src/workflow-script-file.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-settings.ts +147 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-tool-request.ts +61 -0
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +59 -22
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +7 -4
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +7 -4
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +7 -4
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +12 -4
- package/README.md +11 -3
- package/USAGE.md +124 -23
- package/package.json +13 -4
- package/skills/workflow-authoring/SKILL.md +162 -0
|
@@ -16,14 +16,23 @@ import type { WorkflowRunState } from "./workflow-run-record.ts";
|
|
|
16
16
|
|
|
17
17
|
export type { WorkflowLaneItemStatus } from "./progress-types.ts";
|
|
18
18
|
|
|
19
|
-
/** A
|
|
20
|
-
export type WorkflowRef = string;
|
|
19
|
+
/** A registered workflow by name, or an inline-format script file under the workflow cwd. */
|
|
20
|
+
export type WorkflowRef = string | { readonly scriptPath: string };
|
|
21
|
+
|
|
22
|
+
/** One declared phase of a workflow. */
|
|
23
|
+
export interface WorkflowPhaseMeta {
|
|
24
|
+
title: string;
|
|
25
|
+
/** Profile for agents that pass this title as `phase` and set neither `model` nor `profile`. */
|
|
26
|
+
profile?: WorkflowModelProfileName;
|
|
27
|
+
}
|
|
21
28
|
|
|
22
29
|
/** Metadata every workflow module must export. */
|
|
23
30
|
export interface WorkflowMeta {
|
|
24
31
|
name: string;
|
|
25
32
|
description: string;
|
|
26
|
-
|
|
33
|
+
/** When a host model should choose this workflow; listed in the system prompt's workflow catalog. */
|
|
34
|
+
whenToUse?: string;
|
|
35
|
+
phases?: WorkflowPhaseMeta[];
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
export interface WorkflowRunStats {
|
|
@@ -39,12 +48,16 @@ export interface WorkflowRunMetadata {
|
|
|
39
48
|
readonly resumedFromRunId?: string;
|
|
40
49
|
readonly journalPath: string;
|
|
41
50
|
readonly recordPath: string;
|
|
51
|
+
/** The inline run's persisted script; absent for registered workflows or when the write failed. */
|
|
52
|
+
readonly scriptPath?: string;
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
export interface WorkflowBackgroundOrigin {
|
|
45
56
|
/** Stable pi session id used to route and deduplicate completion delivery. */
|
|
46
57
|
readonly sessionId: string;
|
|
47
58
|
readonly requestedAt: number;
|
|
59
|
+
/** Deliver the completion with a new agent turn, because Dynamax was active when the run started. */
|
|
60
|
+
readonly wakeOnComplete?: boolean;
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
export interface WorkflowRunOptions {
|
|
@@ -101,6 +114,8 @@ export interface WorkflowProgressSource {
|
|
|
101
114
|
snapshot(): WorkflowProgressSnapshot;
|
|
102
115
|
/** Durable lifecycle state of the run, so a finished snapshot can tell completed from failed. */
|
|
103
116
|
readonly state?: () => WorkflowRunState;
|
|
117
|
+
/** Skip one running agent by its progress row id; false when it is no longer running. */
|
|
118
|
+
readonly skipAgent?: (agentId: number) => boolean;
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
/** Options for a single `agent()` call. */
|
|
@@ -226,12 +241,14 @@ export interface WorkflowApi {
|
|
|
226
241
|
*/
|
|
227
242
|
classify(context: ClassifierContext, opts?: ClassifyOptions): Promise<Record<string, ClassifierAnswer>>;
|
|
228
243
|
/**
|
|
229
|
-
* Run another
|
|
244
|
+
* Run another workflow inline as a sub-step and return its result: a registered workflow by name, or
|
|
245
|
+
* an inline-format script file via `{ scriptPath }` relative to the workflow cwd. The child shares
|
|
230
246
|
* this run's concurrency cap, abort signal, and perf sink. Nests one level only: calling
|
|
231
|
-
* `workflow()` from within a sub-workflow rejects. Rejects on unknown name
|
|
232
|
-
* a missing sub-workflow gracefully.
|
|
247
|
+
* `workflow()` from within a sub-workflow rejects. Rejects on an unknown name, an unreadable
|
|
248
|
+
* scriptPath, or a script compile error — catch it to handle a missing sub-workflow gracefully.
|
|
249
|
+
* `args` is the child's input: a string or any JSON value, read there as `api.input` and `api.args`.
|
|
233
250
|
*/
|
|
234
|
-
workflow(ref: WorkflowRef, args?:
|
|
251
|
+
workflow(ref: WorkflowRef, args?: unknown): Promise<unknown>;
|
|
235
252
|
/**
|
|
236
253
|
* Run every thunk concurrently and wait for all (a barrier). Recoverable thunk
|
|
237
254
|
* failures become null slots by default; settled mode retains serialisable errors.
|
|
@@ -248,8 +265,13 @@ export interface WorkflowApi {
|
|
|
248
265
|
log(message: string): void;
|
|
249
266
|
/** Emit a structured progress event for native workflow UI surfaces. */
|
|
250
267
|
progress(event: WorkflowProgressEvent): void;
|
|
251
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* The invocation's arguments as text: the string passed after the workflow name, a non-string tool or
|
|
270
|
+
* `workflow()` argument as JSON text, or "" when none was given.
|
|
271
|
+
*/
|
|
252
272
|
args: string;
|
|
273
|
+
/** The invocation's arguments verbatim: the slash command's string, or the tool or `workflow()` JSON value. */
|
|
274
|
+
input: unknown;
|
|
253
275
|
/** Working directory of the host session (typically the repo root). */
|
|
254
276
|
cwd: string;
|
|
255
277
|
/**
|
|
@@ -258,14 +280,35 @@ export interface WorkflowApi {
|
|
|
258
280
|
* `agent()` throws `WorkflowBudgetExceededError` once the ceiling is reached.
|
|
259
281
|
*/
|
|
260
282
|
budget: WorkflowBudget;
|
|
283
|
+
/**
|
|
284
|
+
* The run's live-agent limit. `remaining()` is live: admissions left before `agent()` throws
|
|
285
|
+
* `WorkflowAgentLimitError` (journal replays take none). Reserve what later stages need before a fan-out.
|
|
286
|
+
*/
|
|
287
|
+
agents: WorkflowAgentCapacity;
|
|
261
288
|
/** Abort signal for the run, propagated from the host. */
|
|
262
289
|
signal: AbortSignal | undefined;
|
|
263
290
|
}
|
|
264
291
|
|
|
292
|
+
export interface WorkflowAgentCapacity {
|
|
293
|
+
readonly max: number;
|
|
294
|
+
remaining(): number;
|
|
295
|
+
}
|
|
296
|
+
|
|
265
297
|
export type WorkflowRun = (api: WorkflowApi) => Promise<unknown>;
|
|
266
298
|
|
|
299
|
+
/**
|
|
300
|
+
* `file` workflows resolve by name from `path` under `root`. Their `fingerprint` hashes the `root` tree,
|
|
301
|
+
* revalidated on resume because imports may load lazily, unless `selfContained` marks a compiled saved
|
|
302
|
+
* script: it can import nothing, so the hash of its own content is its whole identity.
|
|
303
|
+
*/
|
|
267
304
|
export type WorkflowSourceIdentity =
|
|
268
|
-
| {
|
|
305
|
+
| {
|
|
306
|
+
readonly kind: "file";
|
|
307
|
+
readonly path: string;
|
|
308
|
+
readonly root: string;
|
|
309
|
+
readonly fingerprint: string;
|
|
310
|
+
readonly selfContained?: true;
|
|
311
|
+
}
|
|
269
312
|
| { readonly kind: "fingerprint"; readonly fingerprint: string }
|
|
270
313
|
| { readonly kind: "unverifiable"; readonly reason: string };
|
|
271
314
|
|
|
@@ -279,4 +322,6 @@ export interface WorkflowModule {
|
|
|
279
322
|
export interface LoadedWorkflow extends WorkflowModule {
|
|
280
323
|
readonly source: WorkflowSourceIdentity;
|
|
281
324
|
readonly isolatedWorktreeBaseline?: WorktreeBaseline;
|
|
325
|
+
/** An inline run's script source, persisted beside its run record so the run can be read and relaunched. */
|
|
326
|
+
readonly inlineScript?: string;
|
|
282
327
|
}
|
|
@@ -77,6 +77,7 @@ export interface WorkflowStatusCounts {
|
|
|
77
77
|
readonly running: number;
|
|
78
78
|
readonly done: number;
|
|
79
79
|
readonly failed: number;
|
|
80
|
+
readonly skipped: number;
|
|
80
81
|
readonly total: number;
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -96,7 +97,7 @@ export function formatWorkflowInspection(inspection: WorkflowInspectionSource):
|
|
|
96
97
|
`Workflow inspector: ${inspection.name}`,
|
|
97
98
|
`Run: ${snapshot.runId}`,
|
|
98
99
|
`Phase: ${snapshot.currentPhase}`,
|
|
99
|
-
`Agents: ${counts.running} running, ${counts.queued} queued, ${counts.done} done, ${counts.failed} failed`,
|
|
100
|
+
`Agents: ${counts.running} running, ${counts.queued} queued, ${counts.done} done, ${counts.failed} failed${counts.skipped > 0 ? `, ${counts.skipped} skipped` : ""}`,
|
|
100
101
|
];
|
|
101
102
|
const usage = formatWorkflowUsageLine(snapshot.usage);
|
|
102
103
|
if (usage) lines.push(usage);
|
|
@@ -109,7 +110,7 @@ export function statusTextFromCounts(snapshot: WorkflowProgressSnapshot, counts:
|
|
|
109
110
|
if (snapshot.doneAt !== undefined) return undefined;
|
|
110
111
|
|
|
111
112
|
const displayName = snapshot.title === "code-review" ? "review" : snapshot.title;
|
|
112
|
-
const progress = counts.total > 0 ? ` ${counts
|
|
113
|
+
const progress = counts.total > 0 ? ` ${finishedAgents(counts)}/${counts.total}` : "";
|
|
113
114
|
return [
|
|
114
115
|
`${theme.fg("accent", GLYPH.workflow)} ${theme.fg("accent", displayName)}`,
|
|
115
116
|
theme.fg("muted", `${snapshot.currentPhase}${progress}`),
|
|
@@ -117,8 +118,13 @@ export function statusTextFromCounts(snapshot: WorkflowProgressSnapshot, counts:
|
|
|
117
118
|
].join(dot(theme));
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
/** Agents that will not run again: done, failed, or skipped. */
|
|
122
|
+
export function finishedAgents(counts: WorkflowStatusCounts): number {
|
|
123
|
+
return counts.done + counts.failed + counts.skipped;
|
|
124
|
+
}
|
|
125
|
+
|
|
120
126
|
export function countAgents(phases: readonly PhaseSnapshot[]): WorkflowStatusCounts {
|
|
121
|
-
const counts = { queued: 0, running: 0, done: 0, failed: 0, total: 0 };
|
|
127
|
+
const counts = { queued: 0, running: 0, done: 0, failed: 0, skipped: 0, total: 0 };
|
|
122
128
|
for (const phase of phases) {
|
|
123
129
|
for (const agent of phase.agents) {
|
|
124
130
|
counts[agent.status]++;
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
truncatePath,
|
|
26
26
|
type ThemeColor,
|
|
27
27
|
} from "./kit.ts";
|
|
28
|
-
import { formatDuration } from "./workflow-format.ts";
|
|
28
|
+
import { countAgents, finishedAgents, formatDuration } from "./workflow-format.ts";
|
|
29
29
|
import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./workflow-viewer-layout.ts";
|
|
30
30
|
|
|
31
31
|
/** What the inspector knows about a run beyond its progress snapshot. */
|
|
@@ -40,6 +40,12 @@ export interface WorkflowInspectorOutcome {
|
|
|
40
40
|
|
|
41
41
|
type OutcomeSource = WorkflowInspectorOutcome | (() => WorkflowInspectorOutcome | undefined);
|
|
42
42
|
|
|
43
|
+
/** Actions on a live run. A retained or finished run has none, so its inspector is read-only. */
|
|
44
|
+
export interface WorkflowInspectorControls {
|
|
45
|
+
/** Skip one running agent by its progress row id; false when it is no longer running. */
|
|
46
|
+
readonly skipAgent?: (agentId: number) => boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
/** The tables need about 80 columns, so narrow terminals give the inspector nearly full width instead of 80%. */
|
|
44
50
|
export const WORKFLOW_INSPECTOR_OVERLAY_OPTIONS = {
|
|
45
51
|
overlay: true,
|
|
@@ -51,9 +57,10 @@ export async function showWorkflowInspector(
|
|
|
51
57
|
ui: Pick<ExtensionContext["ui"], "custom">,
|
|
52
58
|
snapshot: () => WorkflowProgressSnapshot,
|
|
53
59
|
outcome?: OutcomeSource,
|
|
60
|
+
controls?: WorkflowInspectorControls,
|
|
54
61
|
): Promise<void> {
|
|
55
62
|
await ui.custom<void>(
|
|
56
|
-
(...[tui, theme, , done]) => new WorkflowInspector(snapshot, tui, theme, () => done(undefined), outcome),
|
|
63
|
+
(...[tui, theme, , done]) => new WorkflowInspector(snapshot, tui, theme, () => done(undefined), outcome, controls),
|
|
57
64
|
WORKFLOW_INSPECTOR_OVERLAY_OPTIONS,
|
|
58
65
|
);
|
|
59
66
|
}
|
|
@@ -113,6 +120,12 @@ export class WorkflowInspector {
|
|
|
113
120
|
private readonly cursorIndexes = new Map<TabId, number>();
|
|
114
121
|
private readonly scrollOffsets = new Map<TabId, number>();
|
|
115
122
|
private readonly expanded = new Set<string>();
|
|
123
|
+
/** Agents whose skip was sent; they keep running until their session winds down. */
|
|
124
|
+
private readonly skipRequested = new Set<number>();
|
|
125
|
+
/** Agent armed by a first skip press; any other key disarms it. */
|
|
126
|
+
private pendingSkip: number | undefined;
|
|
127
|
+
/** One-keypress footer warning, as in the runs browser. */
|
|
128
|
+
private notice: string | undefined;
|
|
116
129
|
private refreshTimer: ReturnType<typeof setInterval> | undefined;
|
|
117
130
|
|
|
118
131
|
constructor(
|
|
@@ -121,6 +134,7 @@ export class WorkflowInspector {
|
|
|
121
134
|
private readonly theme: Theme,
|
|
122
135
|
private readonly close: () => void,
|
|
123
136
|
private readonly outcomeSource?: OutcomeSource,
|
|
137
|
+
private readonly controls?: WorkflowInspectorControls,
|
|
124
138
|
) {}
|
|
125
139
|
|
|
126
140
|
handleInput(data: string): void {
|
|
@@ -128,6 +142,10 @@ export class WorkflowInspector {
|
|
|
128
142
|
this.close();
|
|
129
143
|
return;
|
|
130
144
|
}
|
|
145
|
+
const armedSkip = this.pendingSkip;
|
|
146
|
+
const hadNotice = this.notice !== undefined;
|
|
147
|
+
this.pendingSkip = undefined;
|
|
148
|
+
this.notice = undefined;
|
|
131
149
|
const tabs = this.tabs(this.outcome());
|
|
132
150
|
const digit = TAB_DIGITS.findIndex((candidate) => matchesKey(data, candidate));
|
|
133
151
|
if (matchesKey(data, "tab") || matchesKey(data, "right")) this.tabIndex = (this.tabIndex + 1) % tabs.length;
|
|
@@ -142,7 +160,8 @@ export class WorkflowInspector {
|
|
|
142
160
|
else if (matchesKey(data, "g") || matchesKey(data, "home")) this.move(-Infinity);
|
|
143
161
|
else if (data === "G" || matchesKey(data, "shift+g") || matchesKey(data, "end")) this.move(Infinity);
|
|
144
162
|
else if (matchesKey(data, "enter") || matchesKey(data, "return")) this.toggleExpanded();
|
|
145
|
-
else
|
|
163
|
+
else if (matchesKey(data, "s")) this.skipSelected(armedSkip);
|
|
164
|
+
else if (!hadNotice) return;
|
|
146
165
|
this.tui.requestRender();
|
|
147
166
|
}
|
|
148
167
|
|
|
@@ -175,11 +194,12 @@ export class WorkflowInspector {
|
|
|
175
194
|
const title = `${this.theme.fg("accent", GLYPH.workflow)} ${this.theme.bold(snapshot.title)}`;
|
|
176
195
|
// Border chrome around the two labels: `╭─ ` + title + ` ` … ` ` + right + ` ─╮`, plus one fill cell.
|
|
177
196
|
const rightRoom = Math.max(0, width - visibleWidth(title) - 9);
|
|
197
|
+
const skippable = this.skippableAgent(active, snapshot, status) !== undefined;
|
|
178
198
|
return frame(
|
|
179
199
|
[tabLine, "", ...visible, ...Array.from({ length: viewRows - visible.length }, () => "")],
|
|
180
200
|
width,
|
|
181
201
|
this.theme,
|
|
182
|
-
{ title, right: this.headerStatus(snapshot, status, rightRoom), footer: this.footer(active, rows, viewRows, offset, below, width) },
|
|
202
|
+
{ title, right: this.headerStatus(snapshot, status, rightRoom), footer: this.footer(active, rows, viewRows, offset, below, width, skippable) },
|
|
183
203
|
);
|
|
184
204
|
}
|
|
185
205
|
|
|
@@ -250,6 +270,29 @@ export class WorkflowInspector {
|
|
|
250
270
|
else this.expanded.add(key);
|
|
251
271
|
}
|
|
252
272
|
|
|
273
|
+
/** Skipping ends live work, so like stopping a run in the runs browser it takes a second press on the same agent. */
|
|
274
|
+
private skipSelected(armed: number | undefined): void {
|
|
275
|
+
const snapshot = this.snapshotProvider();
|
|
276
|
+
const agent = this.skippableAgent(this.activeTab(), snapshot, runStatus(snapshot, this.outcome()?.state));
|
|
277
|
+
if (!agent) return;
|
|
278
|
+
if (armed !== agent.id) {
|
|
279
|
+
this.pendingSkip = agent.id;
|
|
280
|
+
this.notice = `press s again to skip ${agent.label}`;
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (this.controls?.skipAgent?.(agent.id)) this.skipRequested.add(agent.id);
|
|
284
|
+
else this.notice = `${agent.label} is no longer running`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The selected agent when it can be skipped: a running row of a live run whose source accepts skips. */
|
|
288
|
+
private skippableAgent(tab: TabId, snapshot: WorkflowProgressSnapshot, status: RunStatus): AgentRowSnapshot | undefined {
|
|
289
|
+
if (!this.controls?.skipAgent || tab !== "agents" || !status.live) return undefined;
|
|
290
|
+
const agents = snapshot.phases.flatMap((phase) => phase.agents);
|
|
291
|
+
const selected = this.selectedKey("agents", agents.map(agentKey));
|
|
292
|
+
const agent = agents.find((candidate) => agentKey(candidate) === selected);
|
|
293
|
+
return agent?.status === "running" && !this.skipRequested.has(agent.id) ? agent : undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
253
296
|
/** Resolves the cursor by key so live rows inserted above it do not move the selection. */
|
|
254
297
|
private cursorIndex(tab: TabId, keys: readonly string[]): number {
|
|
255
298
|
const byKey = keys.indexOf(this.cursorKeys.get(tab) ?? "");
|
|
@@ -299,27 +342,29 @@ export class WorkflowInspector {
|
|
|
299
342
|
/** State first; lower-priority parts (run id, then elapsed) drop whole rather than truncating. */
|
|
300
343
|
private headerStatus(snapshot: WorkflowProgressSnapshot, status: RunStatus, room: number): string {
|
|
301
344
|
const th = this.theme;
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const degraded = !status.live && failed > 0 && status.color === "success";
|
|
345
|
+
const counts = countAgents(snapshot.phases);
|
|
346
|
+
// A run that finished around failed or skipped agents is complete but not clean.
|
|
347
|
+
const degraded = !status.live && counts.failed + counts.skipped > 0 && status.color === "success";
|
|
306
348
|
const state = degraded ? th.fg("warning", `${GLYPH.warning} ${status.word}`) : th.fg(status.color, `${status.glyph} ${status.word}`);
|
|
307
|
-
const failures = failed > 0 ? th.fg("error", `${failed} failed`) : undefined;
|
|
349
|
+
const failures = counts.failed > 0 ? th.fg("error", `${counts.failed} failed`) : undefined;
|
|
350
|
+
const skips = counts.skipped > 0 ? th.fg("warning", `${counts.skipped} skipped`) : undefined;
|
|
308
351
|
const runId = th.fg("dim", `run ${snapshot.runId.slice(0, 8)}`);
|
|
309
352
|
if (status.live) {
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
return fitParts([state, th.fg("muted", progress), failures, th.fg("muted", formatDuration(Date.now() - snapshot.startedAt)), runId], room, th);
|
|
353
|
+
const progress = counts.total > 0 ? `${snapshot.currentPhase} ${finishedAgents(counts)}/${counts.total}` : snapshot.currentPhase;
|
|
354
|
+
return fitParts([state, th.fg("muted", progress), failures, skips, th.fg("muted", formatDuration(Date.now() - snapshot.startedAt)), runId], room, th);
|
|
313
355
|
}
|
|
314
356
|
const elapsed = snapshot.doneAt !== undefined ? th.fg("muted", formatDuration(snapshot.doneAt - snapshot.startedAt)) : undefined;
|
|
315
|
-
return fitParts([state, failures, elapsed, runId], room, th);
|
|
357
|
+
return fitParts([state, failures, skips, elapsed, runId], room, th);
|
|
316
358
|
}
|
|
317
359
|
|
|
318
|
-
private footer(tab: TabId, rows: readonly Row[], viewRows: number, offset: number, below: number, width: number): string {
|
|
360
|
+
private footer(tab: TabId, rows: readonly Row[], viewRows: number, offset: number, below: number, width: number, skippable: boolean): string {
|
|
361
|
+
if (this.notice) return this.theme.fg("warning", `${GLYPH.warning} ${this.notice}`);
|
|
319
362
|
const clipped = rows.length > viewRows;
|
|
320
363
|
const pairs: (readonly [string, string])[] = [["tab", "sections"]];
|
|
321
364
|
if (LIST_TABS.has(tab) && selectableKeys(rows).length > 0) pairs.push(["↑↓", "move"], ["enter", "expand"]);
|
|
322
365
|
else if (clipped) pairs.push(["↑↓", "scroll"]);
|
|
366
|
+
// Like the runs browser's action keys, skip is listed only while the selection can take it.
|
|
367
|
+
if (skippable) pairs.push(["s", "skip"]);
|
|
323
368
|
pairs.push(["q", "close"]);
|
|
324
369
|
const hints = keyHints(pairs, this.theme);
|
|
325
370
|
const scroll = [offset > 0 ? `↑ ${offset} more` : undefined, below > 0 ? `↓ ${below} more` : undefined].filter(Boolean).join(" ");
|
|
@@ -367,6 +412,7 @@ export class WorkflowInspector {
|
|
|
367
412
|
countNote(phase.agents, "running", "accent", th),
|
|
368
413
|
countNote(phase.agents, "queued", "dim", th),
|
|
369
414
|
countNote(phase.agents, "failed", "error", th),
|
|
415
|
+
countNote(phase.agents, "skipped", "warning", th),
|
|
370
416
|
],
|
|
371
417
|
th,
|
|
372
418
|
);
|
|
@@ -449,9 +495,8 @@ export class WorkflowInspector {
|
|
|
449
495
|
const now = Date.now();
|
|
450
496
|
const rows: Row[] = [];
|
|
451
497
|
for (const phase of phases) {
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
rows.push({ text: ` ${th.bold(th.fg("muted", phase.title))}${note}`, heading: true });
|
|
498
|
+
const notes = joinParts([countNote(phase.agents, "failed", "error", th), countNote(phase.agents, "skipped", "warning", th)], th);
|
|
499
|
+
rows.push({ text: ` ${th.bold(th.fg("muted", phase.title))}${notes ? ` ${notes}` : ""}`, heading: true });
|
|
455
500
|
for (const agent of phase.agents) {
|
|
456
501
|
const key = agentKey(agent);
|
|
457
502
|
const cursor = key === selected ? th.fg("accent", GLYPH.cursor) : " ";
|
|
@@ -460,7 +505,8 @@ export class WorkflowInspector {
|
|
|
460
505
|
const tools = agent.toolUses > 0 ? `${agent.toolUses} tool${agent.toolUses === 1 ? "" : "s"}` : "";
|
|
461
506
|
const expanded = this.expanded.has(key);
|
|
462
507
|
// Expanded rows carry the full error and tool below, so the inline note would repeat them.
|
|
463
|
-
const
|
|
508
|
+
const skipping = agent.status === "running" && this.skipRequested.has(agent.id);
|
|
509
|
+
const note = skipping ? th.fg("warning", "skipping…") : expanded ? "" : agentNote(agent, th);
|
|
464
510
|
const line = `${cursor} ${agentGlyph(agent, th)} ${label} ${padStartVisible(th.fg("muted", duration), 6)} ${fit(th.fg("dim", tools), 8)} ${note}`;
|
|
465
511
|
rows.push({ text: line.trimEnd(), key });
|
|
466
512
|
if (expanded) rows.push(...this.agentDetails(agent, phase, snapshot, width));
|
|
@@ -474,6 +520,7 @@ export class WorkflowInspector {
|
|
|
474
520
|
const fields: [string, string][] = [];
|
|
475
521
|
if (agent.error) fields.push(["error", th.fg("error", agent.error)]);
|
|
476
522
|
if (agent.status === "queued") fields.push(["status", th.fg("muted", "queued, waiting for a free agent slot")]);
|
|
523
|
+
if (agent.status === "skipped") fields.push(["status", th.fg("warning", "skipped by the user; its result was dropped")]);
|
|
477
524
|
if (agent.lastTool) {
|
|
478
525
|
const calls = agent.toolUses > 1 ? th.fg("dim", ` · last of ${agent.toolUses} tool calls`) : "";
|
|
479
526
|
fields.push(["tool", `${agent.lastTool}${calls}`]);
|
|
@@ -611,6 +658,7 @@ function phaseGlyph(phase: PhaseSnapshot, activeAndLive: boolean): { glyph: stri
|
|
|
611
658
|
if (agents.length > 0 && agents.every((agent) => agent.status === "queued")) return { glyph: GLYPH.queued, color: "dim" };
|
|
612
659
|
if (agents.some((agent) => agent.status === "queued")) return { glyph: GLYPH.running, color: "accent" };
|
|
613
660
|
if (agents.length > 0 && agents.every((agent) => agent.status === "failed")) return { glyph: GLYPH.failed, color: "error" };
|
|
661
|
+
if (agents.length > 0 && agents.every((agent) => agent.status === "skipped")) return { glyph: GLYPH.skipped, color: "warning" };
|
|
614
662
|
return { glyph: GLYPH.done, color: "success" };
|
|
615
663
|
}
|
|
616
664
|
|
|
@@ -655,6 +703,8 @@ function agentGlyph(agent: AgentRowSnapshot, theme: Theme): string {
|
|
|
655
703
|
return theme.fg("success", GLYPH.done);
|
|
656
704
|
case "failed":
|
|
657
705
|
return theme.fg("error", GLYPH.failed);
|
|
706
|
+
case "skipped":
|
|
707
|
+
return theme.fg("warning", GLYPH.skipped);
|
|
658
708
|
case "running":
|
|
659
709
|
return theme.fg("accent", GLYPH.running);
|
|
660
710
|
case "queued":
|
|
@@ -664,6 +714,7 @@ function agentGlyph(agent: AgentRowSnapshot, theme: Theme): string {
|
|
|
664
714
|
|
|
665
715
|
function agentNote(agent: AgentRowSnapshot, theme: Theme): string {
|
|
666
716
|
if (agent.status === "failed") return theme.fg("error", firstLine(agent.error ?? "failed"));
|
|
717
|
+
if (agent.status === "skipped") return theme.fg("warning", "skipped");
|
|
667
718
|
if (agent.status === "queued") return theme.fg("dim", "queued");
|
|
668
719
|
if (agent.status === "running" && agent.lastTool) return theme.fg("dim", agent.lastTool);
|
|
669
720
|
return "";
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { decodeKittyPrintable, matchesKey, visibleWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
2
|
+
import { decodeKittyPrintable, matchesKey, truncateToWidth, visibleWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
3
|
import { isRecord } from "../guards.ts";
|
|
4
4
|
import { formatCount, truncateText } from "../text.ts";
|
|
5
5
|
import {
|
|
6
6
|
availableWorkflowRunActions,
|
|
7
7
|
canRelaunchWorkflowRun,
|
|
8
8
|
formatWorkflowRunDuration,
|
|
9
|
+
hasRelaunchableWorkflowSource,
|
|
10
|
+
isInlineWorkflowRun,
|
|
9
11
|
type WorkflowRunActionContext,
|
|
10
12
|
type WorkflowRunLifecycleAction,
|
|
11
13
|
} from "../workflow-run-history.ts";
|
|
@@ -72,9 +74,12 @@ const OUTCOME_LINES = 3;
|
|
|
72
74
|
const OUTCOME_CHARS = 600;
|
|
73
75
|
const LIST_MIN = 3;
|
|
74
76
|
const REFRESH_MS = 1_000;
|
|
77
|
+
const INLINE_TAG = "inline";
|
|
75
78
|
|
|
76
79
|
interface RunCells {
|
|
77
80
|
readonly name: string;
|
|
81
|
+
/** `inline` for a run of an inline script; it stays visible however far the name is truncated. */
|
|
82
|
+
readonly tag: string;
|
|
78
83
|
readonly state: string;
|
|
79
84
|
readonly duration: string;
|
|
80
85
|
readonly age: string;
|
|
@@ -212,9 +217,12 @@ export class WorkflowRunsBrowser implements Component {
|
|
|
212
217
|
const style = STATE_STYLE[record.state];
|
|
213
218
|
const selected = index === this.cursor;
|
|
214
219
|
const cursor = selected ? this.theme.fg("accent", GLYPH.cursor) : " ";
|
|
215
|
-
const
|
|
220
|
+
const tagWidth = cells.tag ? visibleWidth(cells.tag) + 1 : 0;
|
|
221
|
+
const name = truncateToWidth(cells.name, Math.max(1, layout.name - tagWidth), "…");
|
|
222
|
+
const tag = cells.tag ? ` ${this.theme.fg("dim", cells.tag)}` : "";
|
|
223
|
+
const padding = " ".repeat(Math.max(0, layout.name - visibleWidth(name) - tagWidth));
|
|
216
224
|
const parts = [
|
|
217
|
-
`${cursor} ${this.theme.fg(style.color, style.glyph)} ${selected ? this.theme.bold(name) : name}`,
|
|
225
|
+
`${cursor} ${this.theme.fg(style.color, style.glyph)} ${selected ? this.theme.bold(name) : name}${tag}${padding}`,
|
|
218
226
|
this.theme.fg(style.color, fit(cells.state, layout.state)),
|
|
219
227
|
];
|
|
220
228
|
if (layout.duration > 0) parts.push(this.theme.fg("muted", padStartVisible(cells.duration, layout.duration)));
|
|
@@ -366,7 +374,7 @@ interface ColumnLayout {
|
|
|
366
374
|
function columnLayout(cells: readonly RunCells[], width: number): ColumnLayout {
|
|
367
375
|
const widest = (pick: (cell: RunCells) => string) => Math.max(0, ...cells.map((cell) => visibleWidth(pick(cell))));
|
|
368
376
|
let layout: ColumnLayout = {
|
|
369
|
-
name: Math.min(NAME_MAX, widest((cell) => cell.name)),
|
|
377
|
+
name: Math.min(NAME_MAX, widest((cell) => (cell.tag ? `${cell.name} ${cell.tag}` : cell.name))),
|
|
370
378
|
state: widest((cell) => cell.state),
|
|
371
379
|
duration: widest((cell) => cell.duration),
|
|
372
380
|
age: widest((cell) => cell.age),
|
|
@@ -389,6 +397,7 @@ function runCells(record: WorkflowRunRecord, now: number): RunCells {
|
|
|
389
397
|
const cost = record.usage?.totals.cost.total ?? 0;
|
|
390
398
|
return {
|
|
391
399
|
name: record.workflow.name,
|
|
400
|
+
tag: isInlineWorkflowRun(record) ? INLINE_TAG : "",
|
|
392
401
|
state: record.state,
|
|
393
402
|
duration: record.state === "queued" ? "" : formatWorkflowRunDuration(record, now),
|
|
394
403
|
age: relativeTime(record.createdAt, now),
|
|
@@ -446,8 +455,10 @@ function describeOutcome(record: WorkflowRunRecord): string | undefined {
|
|
|
446
455
|
/** Why a finished or paused run cannot be relaunched, mirroring `canRelaunchWorkflowRun`. */
|
|
447
456
|
function relaunchBlocker(record: WorkflowRunRecord): string | undefined {
|
|
448
457
|
if (record.state === "queued" || record.state === "running" || canRelaunchWorkflowRun(record)) return undefined;
|
|
449
|
-
if (record
|
|
450
|
-
return "the workflow source cannot be verified";
|
|
458
|
+
if (!hasRelaunchableWorkflowSource(record)) {
|
|
459
|
+
if (!isInlineWorkflowRun(record)) return "the workflow source cannot be verified";
|
|
460
|
+
if (record.scriptFile === undefined) return "its inline script was not retained";
|
|
461
|
+
return "its inline script has no seal from this pi install";
|
|
451
462
|
}
|
|
452
463
|
return record.options.argumentsPresent !== false ? "its arguments were not retained" : undefined;
|
|
453
464
|
}
|
|
@@ -2,6 +2,7 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
3
3
|
import { isRecord } from "../guards.ts";
|
|
4
4
|
import { readInlineWorkflowMeta } from "../inline-workflow.ts";
|
|
5
|
+
import { workflowArgsText } from "../workflow-input.ts";
|
|
5
6
|
import { GLYPH, hangingWrap, keyHints, spread } from "./kit.ts";
|
|
6
7
|
import { LinesComponent, shortRunId, type WorkflowResultHints } from "./result-layout.ts";
|
|
7
8
|
import { isWorkflowResult, workflowResultComponent } from "./workflow-result-renderer.ts";
|
|
@@ -10,7 +11,9 @@ import { isWorkflowResult, workflowResultComponent } from "./workflow-result-ren
|
|
|
10
11
|
export interface WorkflowToolCallArgs {
|
|
11
12
|
readonly name?: string;
|
|
12
13
|
readonly script?: string;
|
|
13
|
-
readonly
|
|
14
|
+
readonly scriptPath?: string;
|
|
15
|
+
/** A string or any JSON value. */
|
|
16
|
+
readonly args?: unknown;
|
|
14
17
|
readonly background?: boolean;
|
|
15
18
|
readonly resumeFromRunId?: string;
|
|
16
19
|
}
|
|
@@ -22,11 +25,13 @@ export interface WorkflowToolResultView {
|
|
|
22
25
|
|
|
23
26
|
/** A named workflow, or an inline one identified by its literal meta (`inline` until that parses). */
|
|
24
27
|
export function workflowToolTitle(args: WorkflowToolCallArgs): { readonly name: string; readonly detail?: string } {
|
|
25
|
-
const detail = collapseWhitespace(args.args
|
|
28
|
+
const detail = collapseWhitespace(workflowArgsText(args.args));
|
|
26
29
|
const named = args.name?.trim();
|
|
27
30
|
if (named) return { name: named, detail: detail || undefined };
|
|
28
31
|
const meta = args.script ? readInlineWorkflowMeta(args.script) : undefined;
|
|
29
|
-
|
|
32
|
+
// The row renders synchronously, so a script file is named by its path rather than read for its meta.
|
|
33
|
+
const fallback = meta?.description ?? args.scriptPath ?? "";
|
|
34
|
+
return { name: meta?.name ?? "inline", detail: detail || collapseWhitespace(fallback) || undefined };
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/** `▸ workflow code-review · HEAD~3 (background)`: the detail is shortened before the tags are. */
|
|
@@ -3,7 +3,7 @@ import { truncateToWidth, visibleWidth, type Component } from "@earendil-works/p
|
|
|
3
3
|
import type { AgentRowSnapshot, PhaseSnapshot, WorkflowProgressSnapshot } from "../progress-types.ts";
|
|
4
4
|
import { formatCount } from "../text.ts";
|
|
5
5
|
import { compactUsage, dot, fit, GLYPH, joinParts, keyHints, progressBar, spread, type ThemeColor } from "./kit.ts";
|
|
6
|
-
import { countAgents, formatDuration, formatElapsed, type WorkflowStatusCounts } from "./workflow-format.ts";
|
|
6
|
+
import { countAgents, finishedAgents, formatDuration, formatElapsed, type WorkflowStatusCounts } from "./workflow-format.ts";
|
|
7
7
|
|
|
8
8
|
/** pi caps string widgets at this many rows; the component keeps to the same budget. */
|
|
9
9
|
const MAX_WIDGET_LINES = 10;
|
|
@@ -64,7 +64,7 @@ export class WidthAwareWidget implements Component {
|
|
|
64
64
|
/** `◆ background · code-review 56b57550 · Find 6/10 · 12s`: one line for a run the user sent to the background. */
|
|
65
65
|
export function renderBackgroundWorkflowLine(snapshot: WorkflowProgressSnapshot, width: number, theme: Theme): string[] {
|
|
66
66
|
const counts = countAgents(snapshot.phases);
|
|
67
|
-
const progress = counts.total > 0 ? `${snapshot.currentPhase} ${counts
|
|
67
|
+
const progress = counts.total > 0 ? `${snapshot.currentPhase} ${finishedAgents(counts)}/${counts.total}` : snapshot.currentPhase;
|
|
68
68
|
const line = joinParts([
|
|
69
69
|
`${theme.fg("accent", GLYPH.workflow)} ${theme.fg("muted", "background")}`,
|
|
70
70
|
`${theme.bold(snapshot.title)} ${theme.fg("dim", snapshot.runId.slice(0, 8))}`,
|
|
@@ -92,7 +92,7 @@ function classifyPhases(snapshot: WorkflowProgressSnapshot): ClassifiedPhases {
|
|
|
92
92
|
|
|
93
93
|
function headerLine(snapshot: WorkflowProgressSnapshot, counts: WorkflowStatusCounts, width: number, now: number, theme: Theme): string {
|
|
94
94
|
const name = `${theme.fg("accent", GLYPH.workflow)} ${theme.bold(snapshot.title)}`;
|
|
95
|
-
const finished = counts
|
|
95
|
+
const finished = finishedAgents(counts);
|
|
96
96
|
const stats = joinParts([
|
|
97
97
|
counts.total > 0 ? theme.fg("text", `${finished}/${counts.total}`) : undefined,
|
|
98
98
|
theme.fg("muted", formatElapsed(now - snapshot.startedAt)),
|
|
@@ -156,8 +156,8 @@ function phaseBody(
|
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
158
|
* Spread the row budget over active phases by importance — a reserved share for failures so they always
|
|
159
|
-
* show, then running, remaining failures, done, queued — while each phase lists its chosen rows
|
|
160
|
-
*
|
|
159
|
+
* show, then running, remaining failures, skipped, done, queued — while each phase lists its chosen rows
|
|
160
|
+
* in that same status order.
|
|
161
161
|
*/
|
|
162
162
|
function planAgentRows(active: readonly PhaseSnapshot[], budget: number): Map<PhaseSnapshot, AgentRowPlan> {
|
|
163
163
|
const byStatus = (status: AgentRowSnapshot["status"]) =>
|
|
@@ -177,10 +177,11 @@ function planAgentRows(active: readonly PhaseSnapshot[], budget: number): Map<Ph
|
|
|
177
177
|
take(failed, failed.length > 0 ? Math.max(1, Math.floor(budget / 3)) : 0);
|
|
178
178
|
take(byStatus("running"));
|
|
179
179
|
take(failed);
|
|
180
|
+
take(byStatus("skipped"));
|
|
180
181
|
take(byStatus("done"));
|
|
181
182
|
take(byStatus("queued"));
|
|
182
183
|
|
|
183
|
-
const rank = { running: 0, failed: 1,
|
|
184
|
+
const rank = { running: 0, failed: 1, skipped: 2, done: 3, queued: 4 } as const;
|
|
184
185
|
const plans = new Map<PhaseSnapshot, AgentRowPlan>();
|
|
185
186
|
for (const phase of active) {
|
|
186
187
|
const shown = phase.agents.filter((agent) => chosen.has(agent)).sort((a, b) => rank[a.status] - rank[b.status]);
|
|
@@ -196,7 +197,7 @@ function agentLabelWidth(plans: ReadonlyMap<PhaseSnapshot, AgentRowPlan>, width:
|
|
|
196
197
|
|
|
197
198
|
function finishedPhaseLine(phase: PhaseSnapshot, titleWidth: number, theme: Theme): string {
|
|
198
199
|
const counts = countAgents([phase]);
|
|
199
|
-
const glyph = counts.failed === 0
|
|
200
|
+
const glyph = counts.failed === 0 && counts.skipped === 0
|
|
200
201
|
? theme.fg("success", GLYPH.done)
|
|
201
202
|
: counts.failed === counts.total ? theme.fg("error", GLYPH.failed) : theme.fg("warning", GLYPH.warning);
|
|
202
203
|
const span = phaseSpan(phase.agents);
|
|
@@ -204,17 +205,19 @@ function finishedPhaseLine(phase: PhaseSnapshot, titleWidth: number, theme: Them
|
|
|
204
205
|
counts.total > 0 ? theme.fg("muted", plural(counts.total, "agent")) : undefined,
|
|
205
206
|
span === undefined ? undefined : theme.fg("muted", shortDuration(span)),
|
|
206
207
|
counts.failed > 0 ? theme.fg("error", `${counts.failed} failed`) : undefined,
|
|
208
|
+
counts.skipped > 0 ? theme.fg("warning", `${counts.skipped} skipped`) : undefined,
|
|
207
209
|
], theme);
|
|
208
210
|
return ` ${glyph} ${theme.fg("muted", fit(phase.title, titleWidth))} ${detail}`.trimEnd();
|
|
209
211
|
}
|
|
210
212
|
|
|
211
213
|
function foldedPhasesLine(phases: readonly PhaseSnapshot[], theme: Theme): string {
|
|
212
214
|
const counts = countAgents(phases);
|
|
213
|
-
const glyph = counts.failed > 0 ? theme.fg("warning", GLYPH.warning) : theme.fg("success", GLYPH.done);
|
|
215
|
+
const glyph = counts.failed > 0 || counts.skipped > 0 ? theme.fg("warning", GLYPH.warning) : theme.fg("success", GLYPH.done);
|
|
214
216
|
const titles = phases.map((phase) => phase.title).join(" · ");
|
|
215
217
|
const detail = joinParts([
|
|
216
218
|
theme.fg("muted", plural(counts.total, "agent")),
|
|
217
219
|
counts.failed > 0 ? theme.fg("error", `${counts.failed} failed`) : undefined,
|
|
220
|
+
counts.skipped > 0 ? theme.fg("warning", `${counts.skipped} skipped`) : undefined,
|
|
218
221
|
], theme);
|
|
219
222
|
return ` ${glyph} ${theme.fg("muted", titles)} ${detail}`;
|
|
220
223
|
}
|
|
@@ -226,6 +229,7 @@ function activePhaseLine(plan: AgentRowPlan, titleWidth: number, theme: Theme):
|
|
|
226
229
|
counts.queued > 0 ? theme.fg("muted", `${counts.queued} queued`) : undefined,
|
|
227
230
|
counts.done > 0 ? theme.fg("muted", `${counts.done} done`) : undefined,
|
|
228
231
|
counts.failed > 0 ? theme.fg("error", `${counts.failed} failed`) : undefined,
|
|
232
|
+
counts.skipped > 0 ? theme.fg("warning", `${counts.skipped} skipped`) : undefined,
|
|
229
233
|
plan.hidden > 0 ? theme.fg("dim", `+${plan.hidden} more`) : undefined,
|
|
230
234
|
], theme);
|
|
231
235
|
return ` ${theme.fg("accent", GLYPH.running)} ${theme.bold(fit(plan.phase.title, titleWidth))} ${detail}`.trimEnd();
|
|
@@ -246,6 +250,8 @@ function agentGlyph(agent: AgentRowSnapshot, theme: Theme): string {
|
|
|
246
250
|
return theme.fg("error", GLYPH.failed);
|
|
247
251
|
case "done":
|
|
248
252
|
return theme.fg("success", GLYPH.done);
|
|
253
|
+
case "skipped":
|
|
254
|
+
return theme.fg("warning", GLYPH.skipped);
|
|
249
255
|
case "queued":
|
|
250
256
|
return theme.fg("dim", GLYPH.queued);
|
|
251
257
|
}
|
|
@@ -264,6 +270,8 @@ function agentDetail(agent: AgentRowSnapshot, now: number, theme: Theme): string
|
|
|
264
270
|
], theme);
|
|
265
271
|
case "failed":
|
|
266
272
|
return theme.fg("error", oneLine(agent.error ?? "failed"));
|
|
273
|
+
case "skipped":
|
|
274
|
+
return theme.fg("warning", "skipped");
|
|
267
275
|
case "queued":
|
|
268
276
|
return theme.fg("dim", "queued");
|
|
269
277
|
}
|