pi-workflow-engine 0.11.0 → 0.13.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-challenge.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +13 -5
- 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 +26 -4
- 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 +82 -10
- 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 +13 -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 +127 -41
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +24 -0
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +120 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +43 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +1 -1
- 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 +52 -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 +7 -3
- 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 +151 -178
- 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 +47 -3
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +65 -162
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +48 -149
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +28 -77
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +28 -77
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +72 -141
- package/USAGE.md +270 -17
- package/package.json +1 -1
|
@@ -3,9 +3,12 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
|
3
3
|
import type { WorkflowBudget } from "./budget.ts";
|
|
4
4
|
import type { Pipeline, WorkflowParallel } from "./concurrency.ts";
|
|
5
5
|
import type { PerfSink, PerfSnapshot } from "./perf.ts";
|
|
6
|
-
import type { WorkflowProgressSnapshot } from "./progress.ts";
|
|
6
|
+
import type { WorkflowLaneItemStatus, WorkflowProgressSnapshot } from "./progress-types.ts";
|
|
7
7
|
import type { WorkflowUsageSnapshot } from "./usage.ts";
|
|
8
8
|
import type { WorktreeBaseline } from "./worktree.ts";
|
|
9
|
+
import type { WorkflowModelProfileName } from "./model-profiles.ts";
|
|
10
|
+
|
|
11
|
+
export type { WorkflowLaneItemStatus } from "./progress-types.ts";
|
|
9
12
|
|
|
10
13
|
/** A reference to a registered workflow by name. */
|
|
11
14
|
export type WorkflowRef = string;
|
|
@@ -29,6 +32,13 @@ export interface WorkflowRunMetadata {
|
|
|
29
32
|
readonly runId: string;
|
|
30
33
|
readonly resumedFromRunId?: string;
|
|
31
34
|
readonly journalPath: string;
|
|
35
|
+
readonly recordPath: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WorkflowBackgroundOrigin {
|
|
39
|
+
/** Stable pi session id used to route and deduplicate completion delivery. */
|
|
40
|
+
readonly sessionId: string;
|
|
41
|
+
readonly requestedAt: number;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
export interface WorkflowRunOptions {
|
|
@@ -36,12 +46,30 @@ export interface WorkflowRunOptions {
|
|
|
36
46
|
perf?: boolean;
|
|
37
47
|
concurrency?: number;
|
|
38
48
|
parallelSubmissionLimit?: number;
|
|
39
|
-
/**
|
|
49
|
+
/** Maximum number of agent calls that may reach a live model session during this run. */
|
|
50
|
+
maxAgents?: number;
|
|
51
|
+
/** Maximum live duration of one agent call in milliseconds. */
|
|
52
|
+
agentTimeoutMs?: number;
|
|
53
|
+
/** Retry count for transient provider/session failures. Defaults to zero. */
|
|
54
|
+
agentRetries?: number;
|
|
55
|
+
/** Explicitly schedule resumable background runs after recognized provider usage windows. */
|
|
56
|
+
autoResumeOnUsageLimit?: boolean;
|
|
57
|
+
/** Maximum recognized usage-limit pauses before automatic scheduling stops. */
|
|
58
|
+
usageLimitMaxAttempts?: number;
|
|
59
|
+
/** Maximum delay accepted from a provider reset hint. */
|
|
60
|
+
usageLimitMaxDelayMs?: number;
|
|
61
|
+
/** Internal count carried into a scheduler-created successor run. */
|
|
62
|
+
usageLimitAttempt?: number;
|
|
63
|
+
/** Output-token ceiling across recorded subagent attempts. Omit for no limit (budget.total === null). */
|
|
40
64
|
budget?: number;
|
|
41
65
|
/** Internal/test override for the generated run id. Omit to generate a new id. */
|
|
42
66
|
runId?: string;
|
|
67
|
+
/** Internal origin metadata for an explicitly backgrounded tool invocation. */
|
|
68
|
+
background?: WorkflowBackgroundOrigin;
|
|
43
69
|
/** Replay completed agent results from this prior run id when call and execution context still match. */
|
|
44
70
|
resumeFromRunId?: string;
|
|
71
|
+
/** Explicitly allow resume to ignore only a workflow-source fingerprint mismatch. */
|
|
72
|
+
resumeEditedWorkflow?: boolean;
|
|
45
73
|
resultViewer?: "open" | "skip";
|
|
46
74
|
/** Additional abort signal to compose with the host context signal. */
|
|
47
75
|
signal?: AbortSignal;
|
|
@@ -66,12 +94,11 @@ export interface WorkflowProgressSource {
|
|
|
66
94
|
}
|
|
67
95
|
|
|
68
96
|
/** Options for a single `agent()` call. */
|
|
69
|
-
export type
|
|
70
|
-
|
|
71
|
-
export type AgentToolHint = "search";
|
|
97
|
+
export type AgentToolHint = "search" | "external-search";
|
|
72
98
|
export type AgentResumePolicy = "read-only" | "off";
|
|
73
99
|
|
|
74
100
|
export interface IsolatedAgentResult<T> {
|
|
101
|
+
readonly baselineOid?: string;
|
|
75
102
|
readonly result: T;
|
|
76
103
|
readonly patch: string;
|
|
77
104
|
readonly changed: boolean;
|
|
@@ -96,12 +123,15 @@ export interface AgentOptions<S extends TSchema = TSchema> {
|
|
|
96
123
|
/** Phase to group this agent under in the progress tree. */
|
|
97
124
|
phase?: string;
|
|
98
125
|
/**
|
|
99
|
-
* Optional model id.
|
|
100
|
-
*
|
|
126
|
+
* Optional model id. Overrides profile routing; when both model and profile are
|
|
127
|
+
* omitted, inherit the host model. Explicit refs are strict: bare ids resolve as
|
|
128
|
+
* Anthropic shorthand; use "provider/id" for other providers.
|
|
101
129
|
*/
|
|
102
130
|
model?: string;
|
|
103
|
-
/** Reasoning effort for this agent. */
|
|
131
|
+
/** Reasoning effort for this agent. Overrides the profile's configured effort. */
|
|
104
132
|
thinkingLevel?: ThinkingLevel;
|
|
133
|
+
/** Exact configured model route to use when model/thinkingLevel do not override it. */
|
|
134
|
+
profile?: WorkflowModelProfileName;
|
|
105
135
|
/**
|
|
106
136
|
* Stable identity hint for resume replay. Use this for repeated logical calls
|
|
107
137
|
* with identical prompts/options, e.g. `${stage}:${item.id}`.
|
|
@@ -123,14 +153,18 @@ export interface AgentOptions<S extends TSchema = TSchema> {
|
|
|
123
153
|
resumeInputs?: readonly string[];
|
|
124
154
|
/** Run this agent in a disposable git worktree and return its patch with the result. */
|
|
125
155
|
isolation?: "worktree";
|
|
156
|
+
/** Apply a captured candidate to a fresh isolated baseline before evaluation. */
|
|
157
|
+
candidatePatch?: { readonly baselineOid: string; readonly patch: string };
|
|
126
158
|
/** Allowlist of concrete tool names the agent may use (e.g. ["read", "bash"]). */
|
|
127
159
|
tools?: string[];
|
|
128
160
|
/**
|
|
129
|
-
* Dynamically include installed tools matching semantic categories.
|
|
130
|
-
*
|
|
131
|
-
*
|
|
161
|
+
* Dynamically include installed tools matching semantic categories. "search"
|
|
162
|
+
* matches local grep/find/code-search tools; "external-search" matches installed
|
|
163
|
+
* web search, browsing, and URL extraction tools.
|
|
132
164
|
*/
|
|
133
165
|
toolHints?: readonly AgentToolHint[];
|
|
166
|
+
/** Fail before prompting unless every semantic tool hint matched an installed tool. */
|
|
167
|
+
requireToolHints?: boolean;
|
|
134
168
|
/**
|
|
135
169
|
* Skill names to expose to this subagent. Subagents receive no skills by default.
|
|
136
170
|
* When omitted, clear prompt text such as `/skill:name`, `include skill name`, or
|
|
@@ -138,7 +172,10 @@ export interface AgentOptions<S extends TSchema = TSchema> {
|
|
|
138
172
|
* prompt inference for prompts that mention skills only as subject matter.
|
|
139
173
|
*/
|
|
140
174
|
skills?: readonly string[];
|
|
141
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* typebox schema. The agent must call the validated `final_answer` tool; bounded
|
|
177
|
+
* repair exhaustion throws `WorkflowStructuredOutputError` instead of returning null.
|
|
178
|
+
*/
|
|
142
179
|
schema?: S;
|
|
143
180
|
}
|
|
144
181
|
|
|
@@ -151,11 +188,11 @@ export interface WorkflowApi {
|
|
|
151
188
|
agent<S extends TSchema>(
|
|
152
189
|
prompt: string,
|
|
153
190
|
opts: AgentOptions<S> & { schema: S; isolation: "worktree" },
|
|
154
|
-
): Promise<IsolatedAgentResult<Static<S
|
|
191
|
+
): Promise<IsolatedAgentResult<Static<S>>>;
|
|
155
192
|
/** Run a text subagent in a disposable worktree and return its final text plus patch. */
|
|
156
193
|
agent(prompt: string, opts: AgentOptions & { isolation: "worktree" }): Promise<IsolatedAgentResult<string>>;
|
|
157
|
-
/** Run a subagent and return
|
|
158
|
-
agent<S extends TSchema>(prompt: string, opts: AgentOptions<S> & { schema: S }): Promise<Static<S
|
|
194
|
+
/** Run a subagent and return validated structured output; rejects with a recoverable typed error on repair exhaustion. */
|
|
195
|
+
agent<S extends TSchema>(prompt: string, opts: AgentOptions<S> & { schema: S }): Promise<Static<S>>;
|
|
159
196
|
/** Run a subagent and return its final assistant text. */
|
|
160
197
|
agent(prompt: string, opts?: AgentOptions): Promise<string>;
|
|
161
198
|
/**
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { sliceByColumn, visibleWidth, type EditorComponent } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export type DynamaxEffect = "shine" | "static" | "off";
|
|
4
|
+
|
|
5
|
+
export interface DynamaxAnimationScheduler {
|
|
6
|
+
now(): number;
|
|
7
|
+
schedule(callback: () => void, delayMs: number): () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DynamaxEditorDecoration {
|
|
11
|
+
readonly editor: EditorComponent;
|
|
12
|
+
dispose(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DynamaxEditorDecorationOptions {
|
|
16
|
+
effect: Exclude<DynamaxEffect, "off">;
|
|
17
|
+
scheduler?: DynamaxAnimationScheduler;
|
|
18
|
+
isActive?: () => boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DYNAMAX_RENDER_PATTERN = /(^|[^A-Za-z0-9_])(dynamax)(?=[^A-Za-z0-9_]|$)/gi;
|
|
22
|
+
const DYNAMAX_COLORS = [
|
|
23
|
+
[233, 137, 115],
|
|
24
|
+
[228, 186, 103],
|
|
25
|
+
[141, 192, 122],
|
|
26
|
+
[102, 194, 179],
|
|
27
|
+
[121, 157, 207],
|
|
28
|
+
[157, 134, 195],
|
|
29
|
+
[206, 130, 172],
|
|
30
|
+
] as const;
|
|
31
|
+
const RESET_FOREGROUND = "\x1b[39m";
|
|
32
|
+
const DYNAMAX_EFFECT_ENV = "PI_DYNAMAX_EFFECT";
|
|
33
|
+
export const DYNAMAX_ANIMATION_FRAME_MS = 135;
|
|
34
|
+
const DYNAMAX_SHINE_FRAME_COUNT = DYNAMAX_COLORS.length;
|
|
35
|
+
const DEFAULT_DYNAMAX_ANIMATION_SCHEDULER: DynamaxAnimationScheduler = {
|
|
36
|
+
now: () => performance.now(),
|
|
37
|
+
schedule(callback, delayMs) {
|
|
38
|
+
const timer = setTimeout(callback, delayMs);
|
|
39
|
+
return () => clearTimeout(timer);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function resolveDynamaxEffect(env: NodeJS.ProcessEnv = process.env): DynamaxEffect {
|
|
44
|
+
const configured = env[DYNAMAX_EFFECT_ENV]?.trim().toLowerCase();
|
|
45
|
+
if (configured === "shine" || configured === "static" || configured === "off") return configured;
|
|
46
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== "") return "off";
|
|
47
|
+
return "shine";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function highlightDynamaxTokens(line: string, shinePosition?: number): string {
|
|
51
|
+
const { visibleText, rawPositions } = mapVisibleCharacters(line);
|
|
52
|
+
const insertions = new Map<number, string>();
|
|
53
|
+
for (const match of visibleText.matchAll(DYNAMAX_RENDER_PATTERN)) {
|
|
54
|
+
const token = match[2]!;
|
|
55
|
+
const visibleStart = match.index! + match[1]!.length;
|
|
56
|
+
for (let index = 0; index < token.length; index++) {
|
|
57
|
+
const rawPosition = rawPositions[visibleStart + index]!;
|
|
58
|
+
const color = DYNAMAX_COLORS[index % DYNAMAX_COLORS.length]!;
|
|
59
|
+
const shine = shinePosition === undefined ? (index === 0 ? 0.45 : 0) : shineFactor(index, shinePosition);
|
|
60
|
+
insertions.set(rawPosition, foregroundColor(color, shine));
|
|
61
|
+
}
|
|
62
|
+
const restorePosition = rawPositions[visibleStart + token.length] ?? line.length;
|
|
63
|
+
insertions.set(restorePosition, foregroundRestoreAt(line, restorePosition));
|
|
64
|
+
}
|
|
65
|
+
if (insertions.size === 0) return line;
|
|
66
|
+
|
|
67
|
+
let highlighted = "";
|
|
68
|
+
for (let rawPosition = 0; rawPosition <= line.length; rawPosition++) {
|
|
69
|
+
highlighted += insertions.get(rawPosition) ?? "";
|
|
70
|
+
if (rawPosition < line.length) highlighted += line[rawPosition];
|
|
71
|
+
}
|
|
72
|
+
return highlighted;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function decorateDynamaxEditor(
|
|
76
|
+
editor: EditorComponent,
|
|
77
|
+
requestRender: () => void,
|
|
78
|
+
options: DynamaxEditorDecorationOptions,
|
|
79
|
+
): DynamaxEditorDecoration {
|
|
80
|
+
const { effect } = options;
|
|
81
|
+
const originalRender = editor.render;
|
|
82
|
+
const scheduler = options.scheduler ?? DEFAULT_DYNAMAX_ANIMATION_SCHEDULER;
|
|
83
|
+
const isActive = options.isActive ?? (() => true);
|
|
84
|
+
let disposed = false;
|
|
85
|
+
let cancelScheduled: (() => void) | undefined;
|
|
86
|
+
let lastSignature: string | undefined;
|
|
87
|
+
let animationStartedAt: number | undefined;
|
|
88
|
+
|
|
89
|
+
const cancelAnimation = (): void => {
|
|
90
|
+
cancelScheduled?.();
|
|
91
|
+
cancelScheduled = undefined;
|
|
92
|
+
animationStartedAt = undefined;
|
|
93
|
+
};
|
|
94
|
+
const scheduleRender = (delayMs: number): void => {
|
|
95
|
+
if (cancelScheduled || disposed || !isActive()) return;
|
|
96
|
+
cancelScheduled = scheduler.schedule(() => {
|
|
97
|
+
cancelScheduled = undefined;
|
|
98
|
+
if (disposed || !isActive()) return;
|
|
99
|
+
requestRender();
|
|
100
|
+
}, Math.max(0, delayMs));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const decoratedRender = (width: number): string[] => {
|
|
104
|
+
const lines = originalRender.call(editor, width);
|
|
105
|
+
const signature = visibleDynamaxSignature(lines);
|
|
106
|
+
if (signature !== lastSignature) {
|
|
107
|
+
cancelAnimation();
|
|
108
|
+
lastSignature = signature;
|
|
109
|
+
if (signature && effect === "shine" && isActive()) animationStartedAt = scheduler.now();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let shinePosition: number | undefined;
|
|
113
|
+
if (animationStartedAt !== undefined && signature && effect === "shine" && isActive()) {
|
|
114
|
+
const now = scheduler.now();
|
|
115
|
+
const elapsed = Math.max(0, now - animationStartedAt);
|
|
116
|
+
const frame = Math.floor(elapsed / DYNAMAX_ANIMATION_FRAME_MS);
|
|
117
|
+
if (frame < DYNAMAX_SHINE_FRAME_COUNT) {
|
|
118
|
+
shinePosition = frame;
|
|
119
|
+
const nextFrameAt = animationStartedAt + (frame + 1) * DYNAMAX_ANIMATION_FRAME_MS;
|
|
120
|
+
scheduleRender(nextFrameAt - now);
|
|
121
|
+
} else {
|
|
122
|
+
cancelAnimation();
|
|
123
|
+
}
|
|
124
|
+
} else if (!signature || effect !== "shine" || !isActive()) {
|
|
125
|
+
cancelAnimation();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return lines.map((line) => highlightDynamaxTokens(line, shinePosition));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
editor.render = decoratedRender;
|
|
132
|
+
return {
|
|
133
|
+
editor,
|
|
134
|
+
dispose() {
|
|
135
|
+
if (disposed) return;
|
|
136
|
+
disposed = true;
|
|
137
|
+
cancelAnimation();
|
|
138
|
+
if (editor.render === decoratedRender) editor.render = originalRender;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function shineFactor(index: number, shinePosition: number): number {
|
|
144
|
+
const distance = Math.abs(index - shinePosition);
|
|
145
|
+
if (distance === 0) return 0.7;
|
|
146
|
+
if (distance === 1) return 0.3;
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function foregroundColor(color: readonly [number, number, number], shine: number): string {
|
|
151
|
+
const red = Math.round(color[0] + (255 - color[0]) * shine);
|
|
152
|
+
const green = Math.round(color[1] + (255 - color[1]) * shine);
|
|
153
|
+
const blue = Math.round(color[2] + (255 - color[2]) * shine);
|
|
154
|
+
return `\x1b[38;2;${red};${green};${blue}m`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function mapVisibleCharacters(line: string): { visibleText: string; rawPositions: number[] } {
|
|
158
|
+
let visibleText = "";
|
|
159
|
+
const rawPositions: number[] = [];
|
|
160
|
+
let rawPosition = 0;
|
|
161
|
+
while (rawPosition < line.length) {
|
|
162
|
+
const controlLength = terminalControlSequenceLength(line, rawPosition);
|
|
163
|
+
if (controlLength > 0) {
|
|
164
|
+
rawPosition += controlLength;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
visibleText += line[rawPosition];
|
|
168
|
+
rawPositions.push(rawPosition);
|
|
169
|
+
rawPosition += 1;
|
|
170
|
+
}
|
|
171
|
+
return { visibleText, rawPositions };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function terminalControlSequenceLength(line: string, position: number): number {
|
|
175
|
+
if (line[position] !== "\x1b") return 0;
|
|
176
|
+
const introducer = line[position + 1];
|
|
177
|
+
if (introducer === "[") {
|
|
178
|
+
for (let index = position + 2; index < line.length; index++) {
|
|
179
|
+
const code = line.charCodeAt(index);
|
|
180
|
+
if (code >= 0x40 && code <= 0x7e) return index - position + 1;
|
|
181
|
+
}
|
|
182
|
+
return line.length - position;
|
|
183
|
+
}
|
|
184
|
+
if (introducer === "]" || introducer === "_") {
|
|
185
|
+
for (let index = position + 2; index < line.length; index++) {
|
|
186
|
+
if (line[index] === "\x07") return index - position + 1;
|
|
187
|
+
if (line[index] === "\x1b" && line[index + 1] === "\\") return index - position + 2;
|
|
188
|
+
}
|
|
189
|
+
return line.length - position;
|
|
190
|
+
}
|
|
191
|
+
return Math.min(2, line.length - position);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function foregroundRestoreAt(line: string, rawPosition: number): string {
|
|
195
|
+
const column = visibleWidth(line.slice(0, rawPosition));
|
|
196
|
+
// The extra cell makes Pi emit the active SGR prefix even when the token ends the line.
|
|
197
|
+
const sliced = sliceByColumn(`${line} `, column, 1);
|
|
198
|
+
let controls = "";
|
|
199
|
+
let position = 0;
|
|
200
|
+
while (position < sliced.length) {
|
|
201
|
+
const controlLength = terminalControlSequenceLength(sliced, position);
|
|
202
|
+
if (controlLength === 0) break;
|
|
203
|
+
const sequence = sliced.slice(position, position + controlLength);
|
|
204
|
+
if (sequence.startsWith("\x1b[") && sequence.endsWith("m")) controls += sequence;
|
|
205
|
+
position += controlLength;
|
|
206
|
+
}
|
|
207
|
+
return `${RESET_FOREGROUND}${controls}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function visibleDynamaxSignature(lines: string[]): string {
|
|
211
|
+
const matches: string[] = [];
|
|
212
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
213
|
+
const { visibleText } = mapVisibleCharacters(lines[lineIndex]!);
|
|
214
|
+
for (const match of visibleText.matchAll(DYNAMAX_RENDER_PATTERN)) {
|
|
215
|
+
const visibleStart = match.index! + match[1]!.length;
|
|
216
|
+
matches.push(`${lineIndex}:${visibleStart}:${match[2]!}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return matches.join("|");
|
|
220
|
+
}
|
|
@@ -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;
|
|
@@ -61,15 +62,18 @@ function renderAdvisoryResult(
|
|
|
61
62
|
metadata?: WorkflowRunDisplayMetadata,
|
|
62
63
|
perf?: WorkflowPerfDetails,
|
|
63
64
|
): string {
|
|
64
|
-
const
|
|
65
|
+
const incomplete = result.status === "incomplete";
|
|
66
|
+
const icon = incomplete ? theme.fg("warning", "⚠") : theme.fg("success", "✓");
|
|
65
67
|
const title = theme.fg("accent", theme.bold(`Workflow: ${name}`));
|
|
66
68
|
const lines = [`${icon} ${title}`, theme.fg("muted", result.summary)];
|
|
69
|
+
if (result.coverage?.length) lines.push(theme.fg("dim", result.coverage.map((stage) => `${stage.stage}: ${stage.completed}/${stage.expected} complete, ${stage.failed} failed`).join(" · ")));
|
|
70
|
+
if (expanded) for (const gap of result.gaps ?? []) lines.push(theme.fg("warning", gap));
|
|
67
71
|
const stats = statsLine(result.stats, theme);
|
|
68
72
|
if (stats) lines.push(stats);
|
|
69
73
|
pushWorkflowDetailLines(lines, theme, { usage, metadata, perf });
|
|
70
74
|
|
|
71
75
|
if (result.findings.length === 0) {
|
|
72
|
-
lines.push(theme.fg("success", "No findings."));
|
|
76
|
+
lines.push(incomplete ? theme.fg("warning", "No verified findings; coverage is incomplete.") : theme.fg("success", "No findings."));
|
|
73
77
|
if (expanded && result.nextSteps.length > 0) renderNextSteps(result.nextSteps, lines, theme);
|
|
74
78
|
return lines.join("\n");
|
|
75
79
|
}
|
|
@@ -158,7 +162,7 @@ function safeJson(value: unknown): string {
|
|
|
158
162
|
try {
|
|
159
163
|
return JSON.stringify(value, null, 2) ?? String(value);
|
|
160
164
|
} catch (error) {
|
|
161
|
-
return
|
|
165
|
+
return unknownErrorMessage(error);
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
168
|
|