@jameslovespancakes/pi-plus 1.0.11 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -22
- package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
- package/package.json +9 -4
- package/src/core/accounts/oauth-pool.ts +137 -0
- package/src/core/accounts/registry.ts +12 -25
- package/src/core/accounts/routing.ts +144 -0
- package/src/core/anthropic/client-identity.ts +9 -64
- package/src/core/anthropic/identity.ts +54 -0
- package/src/core/anthropic/oauth.ts +3 -0
- package/src/core/anthropic/quota.ts +232 -78
- package/src/core/anthropic/routing.ts +40 -115
- package/src/core/anthropic/store.ts +11 -10
- package/src/core/catalog/quality.ts +33 -15
- package/src/core/codex/quota.ts +46 -8
- package/src/core/codex/store.ts +29 -16
- package/src/core/config.ts +16 -17
- package/src/core/store.ts +13 -12
- package/src/domains/agents/format.ts +97 -0
- package/src/domains/agents/index.ts +58 -47
- package/src/domains/compact/archive.ts +140 -0
- package/src/domains/compact/chunking.ts +166 -0
- package/src/domains/compact/index.ts +452 -0
- package/src/domains/compact/jev.ts +242 -0
- package/src/domains/compact/policy.ts +255 -0
- package/src/domains/compact/types.ts +79 -0
- package/src/domains/models/provider-picker.ts +1 -1
- package/src/domains/setup/index.ts +23 -23
- package/src/domains/subscriptions/accounts-picker.ts +61 -40
- package/src/domains/subscriptions/accounts.ts +53 -36
- package/src/domains/subscriptions/footer.ts +48 -29
- package/src/domains/subscriptions/index.ts +11 -27
- package/src/domains/subscriptions/provider.ts +79 -103
- package/src/domains/subscriptions/providers/anthropic.ts +42 -22
- package/src/domains/subscriptions/providers/codex.ts +146 -113
- package/src/domains/subscriptions/providers/hosted.ts +18 -0
- package/src/domains/subscriptions/providers/oauth-pool.ts +325 -0
- package/src/domains/subscriptions/routing.ts +19 -11
- package/src/domains/workflows/LICENSE.md +21 -0
- package/src/domains/workflows/index.ts +836 -0
- package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
- package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
- package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
- package/src/domains/workflows/runtime/agent-attempt.ts +177 -0
- package/src/domains/workflows/runtime/agent-failure.ts +14 -0
- package/src/domains/workflows/runtime/agent-limits.ts +66 -0
- package/src/domains/workflows/runtime/agent-replay.ts +399 -0
- package/src/domains/workflows/runtime/agent-retry.ts +116 -0
- package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
- package/src/domains/workflows/runtime/agent-runner.ts +225 -0
- package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
- package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
- package/src/domains/workflows/runtime/agent-session.ts +382 -0
- package/src/domains/workflows/runtime/agent-skills.ts +270 -0
- package/src/domains/workflows/runtime/agent-workspace.ts +79 -0
- package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
- package/src/domains/workflows/runtime/background-workflows.ts +492 -0
- package/src/domains/workflows/runtime/budget.ts +53 -0
- package/src/domains/workflows/runtime/cancellation.ts +87 -0
- package/src/domains/workflows/runtime/command-completions.ts +36 -0
- package/src/domains/workflows/runtime/concurrency.ts +403 -0
- package/src/domains/workflows/runtime/debug.ts +3 -0
- package/src/domains/workflows/runtime/diff-capture.ts +81 -0
- package/src/domains/workflows/runtime/discovery.ts +137 -0
- package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
- package/src/domains/workflows/runtime/dynamax.ts +330 -0
- package/src/domains/workflows/runtime/engine.ts +579 -0
- package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
- package/src/domains/workflows/runtime/finalizers.ts +66 -0
- package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
- package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
- package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
- package/src/domains/workflows/runtime/journal.ts +313 -0
- package/src/domains/workflows/runtime/model-profiles.ts +310 -0
- package/src/domains/workflows/runtime/options.ts +157 -0
- package/src/domains/workflows/runtime/perf.ts +146 -0
- package/src/domains/workflows/runtime/pi-compat.ts +32 -0
- package/src/domains/workflows/runtime/process-runner.ts +260 -0
- package/src/domains/workflows/runtime/progress-types.ts +59 -0
- package/src/domains/workflows/runtime/progress.ts +400 -0
- package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
- package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
- package/src/domains/workflows/runtime/research-contract.ts +89 -0
- package/src/domains/workflows/runtime/research-evidence.ts +289 -0
- package/src/domains/workflows/runtime/resume-context.ts +751 -0
- package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
- package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
- package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
- package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
- package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
- package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
- package/src/domains/workflows/runtime/review/review-format.ts +133 -0
- package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
- package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
- package/src/domains/workflows/runtime/review/review-report.ts +42 -0
- package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
- package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
- package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
- package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
- package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
- package/src/domains/workflows/runtime/session-identity.ts +6 -0
- package/src/domains/workflows/runtime/structured-output.ts +32 -0
- package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
- package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
- package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
- package/src/domains/workflows/runtime/types.ts +255 -0
- package/src/domains/workflows/runtime/ui/display-text.ts +28 -0
- package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
- package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
- package/src/domains/workflows/runtime/ui/workflow-inspector.ts +435 -0
- package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
- package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
- package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
- package/src/domains/workflows/runtime/unknown-error.ts +8 -0
- package/src/domains/workflows/runtime/usage.ts +341 -0
- package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
- package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
- package/src/domains/workflows/runtime/workflow-module.ts +75 -0
- package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
- package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
- package/src/domains/workflows/runtime/workflow-run-history.ts +164 -0
- package/src/domains/workflows/runtime/workflow-run-record.ts +636 -0
- package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
- package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
- package/src/domains/workflows/runtime/workflows.ts +40 -0
- package/src/domains/workflows/runtime/worktree.ts +615 -0
- package/src/domains/workflows/workflows/code-review.ts +232 -0
- package/src/domains/workflows/workflows/diagnose.ts +154 -0
- package/src/domains/workflows/workflows/perf-review.ts +149 -0
- package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
- package/src/domains/workflows/workflows/research.ts +169 -0
- package/src/services/usage-service.ts +6 -30
- package/src/ui/usage-bars.ts +1 -2
- package/src/core/codex/oauth.ts +0 -129
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { AgentRowSnapshot, WorkflowLaneItemStatus, WorkflowProgressSnapshot } from "../progress-types.ts";
|
|
4
|
+
import { toDisplayLine } from "./display-text.ts";
|
|
5
|
+
import { formatWorkflowUsageLine } from "../usage.ts";
|
|
6
|
+
|
|
7
|
+
export type WorkflowDisplayStatus = WorkflowLaneItemStatus | "queued" | "done" | "failed";
|
|
8
|
+
export type WorkflowThemeColor = Parameters<Theme["fg"]>[0];
|
|
9
|
+
|
|
10
|
+
export function formatDuration(ms: number): string {
|
|
11
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0s";
|
|
12
|
+
if (ms < 1_000) return `${Math.round(ms)}ms`;
|
|
13
|
+
|
|
14
|
+
const totalSeconds = Math.floor(ms / 1_000);
|
|
15
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
16
|
+
|
|
17
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
18
|
+
const seconds = totalSeconds % 60;
|
|
19
|
+
if (minutes < 60) return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
|
|
20
|
+
|
|
21
|
+
const hours = Math.floor(minutes / 60);
|
|
22
|
+
const remainingMinutes = minutes % 60;
|
|
23
|
+
return remainingMinutes === 0 ? `${hours}h` : `${hours}h ${remainingMinutes}m`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatCount(n: number): string {
|
|
27
|
+
if (!Number.isFinite(n)) return "0";
|
|
28
|
+
const sign = n < 0 ? "-" : "";
|
|
29
|
+
const value = Math.abs(n);
|
|
30
|
+
if (value < 1_000) return `${Math.trunc(n)}`;
|
|
31
|
+
if (value < 1_000_000) return `${sign}${formatCompact(value / 1_000)}k`;
|
|
32
|
+
return `${sign}${formatCompact(value / 1_000_000)}m`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function statusIcon(status: WorkflowDisplayStatus, theme: Theme): string {
|
|
36
|
+
switch (status) {
|
|
37
|
+
case "success":
|
|
38
|
+
case "done":
|
|
39
|
+
return theme.fg("success", "✓");
|
|
40
|
+
case "warning":
|
|
41
|
+
return theme.fg("warning", "!");
|
|
42
|
+
case "error":
|
|
43
|
+
case "failed":
|
|
44
|
+
return theme.fg("error", "✗");
|
|
45
|
+
case "running":
|
|
46
|
+
return theme.fg("accent", "●");
|
|
47
|
+
case "queued":
|
|
48
|
+
case "pending":
|
|
49
|
+
return theme.fg("dim", "○");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { toDisplayLine } from "./display-text.ts";
|
|
54
|
+
|
|
55
|
+
export function truncateDisplay(text: string, width: number): string {
|
|
56
|
+
if (width <= 0) return "";
|
|
57
|
+
return truncateToWidth(text, width);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AgentDetailOptions {
|
|
61
|
+
now?: number;
|
|
62
|
+
includeQueuedStatus?: boolean;
|
|
63
|
+
/** Compact surfaces (widget) show only the ✗ icon; detail surfaces show the message. */
|
|
64
|
+
includeError?: boolean;
|
|
65
|
+
errorMaxLength?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function agentDetailParts(agent: AgentRowSnapshot, now?: number): string[];
|
|
69
|
+
export function agentDetailParts(agent: AgentRowSnapshot, options?: AgentDetailOptions): string[];
|
|
70
|
+
export function agentDetailParts(agent: AgentRowSnapshot, optionsOrNow: AgentDetailOptions | number = {}): string[] {
|
|
71
|
+
const options = typeof optionsOrNow === "number" ? { now: optionsOrNow } : optionsOrNow;
|
|
72
|
+
const now = options.now ?? Date.now();
|
|
73
|
+
const includeQueuedStatus = options.includeQueuedStatus ?? true;
|
|
74
|
+
const includeError = options.includeError ?? true;
|
|
75
|
+
const parts: string[] = [];
|
|
76
|
+
if (agent.toolUses > 0) parts.push(`${agent.toolUses} tool${agent.toolUses === 1 ? "" : "s"}`);
|
|
77
|
+
if (agent.lastTool) parts.push(toDisplayLine(agent.lastTool, 32));
|
|
78
|
+
if (agent.startedAt !== undefined) parts.push(formatDuration((agent.doneAt ?? now) - agent.startedAt));
|
|
79
|
+
else if (includeQueuedStatus && agent.status === "queued") parts.push("queued");
|
|
80
|
+
if (agent.status === "failed" && includeError && agent.error) {
|
|
81
|
+
parts.push(toDisplayLine(agent.error, options.errorMaxLength ?? 80));
|
|
82
|
+
}
|
|
83
|
+
return parts;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function agentLabelColor(agent: AgentRowSnapshot): WorkflowThemeColor {
|
|
87
|
+
return agent.status === "running" ? "text" : "muted";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface WorkflowStatusCounts {
|
|
91
|
+
readonly queued: number;
|
|
92
|
+
readonly running: number;
|
|
93
|
+
readonly done: number;
|
|
94
|
+
readonly failed: number;
|
|
95
|
+
readonly total: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface WorkflowStatusSource {
|
|
99
|
+
readonly title: string;
|
|
100
|
+
readonly doneAt?: number;
|
|
101
|
+
readonly currentPhase: string;
|
|
102
|
+
readonly counters: readonly { readonly key: string; readonly label: string; readonly value: number }[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface WorkflowInspectionSource {
|
|
106
|
+
readonly name: string;
|
|
107
|
+
readonly snapshot: WorkflowProgressSnapshot | (() => WorkflowProgressSnapshot);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function workflowInspectionSnapshot(inspection: WorkflowInspectionSource): WorkflowProgressSnapshot {
|
|
111
|
+
return typeof inspection.snapshot === "function" ? inspection.snapshot() : inspection.snapshot;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function formatWorkflowInspection(inspection: WorkflowInspectionSource): string {
|
|
115
|
+
const snapshot = workflowInspectionSnapshot(inspection);
|
|
116
|
+
const counts = countSnapshotAgents(snapshot);
|
|
117
|
+
const lines = [
|
|
118
|
+
`Workflow inspector: ${inspection.name}`,
|
|
119
|
+
`Run: ${snapshot.runId}`,
|
|
120
|
+
`Phase: ${snapshot.currentPhase}`,
|
|
121
|
+
`Agents: ${counts.running} running, ${counts.queued} queued, ${counts.done} done, ${counts.failed} failed`,
|
|
122
|
+
];
|
|
123
|
+
const usage = formatWorkflowUsageLine(snapshot.usage);
|
|
124
|
+
if (usage) lines.push(usage);
|
|
125
|
+
if (snapshot.logs.length > 0) lines.push("Recent log:", ...snapshot.logs.slice(-8).map((entry) => `- ${entry}`));
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function statusText(snapshot: WorkflowProgressSnapshot, theme: Theme): string | undefined {
|
|
130
|
+
const counts = countSnapshotAgents(snapshot);
|
|
131
|
+
return statusTextFromCounts(snapshot, counts, theme);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function statusTextFromCounts(snapshot: WorkflowStatusSource, counts: WorkflowStatusCounts, theme: Theme): string | undefined {
|
|
135
|
+
if (snapshot.doneAt !== undefined) return undefined;
|
|
136
|
+
|
|
137
|
+
const complete = counts.done + counts.failed;
|
|
138
|
+
const active = counts.running + counts.queued;
|
|
139
|
+
const kept = snapshot.counters.find((counter) => counter.key === "kept" || counter.label.toLowerCase() === "kept");
|
|
140
|
+
const displayName = snapshot.title === "code-review" ? "review" : snapshot.title;
|
|
141
|
+
|
|
142
|
+
const parts = [theme.fg("accent", displayName), theme.fg("muted", snapshot.currentPhase)];
|
|
143
|
+
if (counts.total > 0) parts.push(theme.fg("muted", `${complete}/${counts.total}`));
|
|
144
|
+
if (kept) parts.push(theme.fg("success", `${formatCount(kept.value)} kept`));
|
|
145
|
+
else if (active > 0) parts.push(theme.fg("muted", `${active} active`));
|
|
146
|
+
|
|
147
|
+
return parts.join(theme.fg("dim", " · "));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function countSnapshotAgents(snapshot: WorkflowProgressSnapshot): WorkflowStatusCounts {
|
|
151
|
+
const counts = { queued: 0, running: 0, done: 0, failed: 0, total: 0 };
|
|
152
|
+
for (const phase of snapshot.phases) {
|
|
153
|
+
for (const agent of phase.agents) {
|
|
154
|
+
counts[agent.status]++;
|
|
155
|
+
counts.total++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return counts;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function formatCompact(value: number): string {
|
|
162
|
+
if (value >= 100) return `${Math.round(value)}`;
|
|
163
|
+
const rounded = Math.round(value * 10) / 10;
|
|
164
|
+
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(1);
|
|
165
|
+
}
|