@tt-a1i/openpi 0.3.1 → 0.5.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/README.md +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* name 5/5 agents · 31m18s · done
|
|
6
6
|
* description
|
|
7
7
|
* ╭ Phases ────────────╮ ╭ Gather · 3 agents ──────────────────────────────╮
|
|
8
|
-
* │ ❯
|
|
9
|
-
* │
|
|
8
|
+
* │ ❯ ✓ Gather 3/3 │ │ ✓ CodeRabbit feedback gpt-5 · 7%/372k 5m37s│
|
|
9
|
+
* │ ⠿ Verify 1/1 │ │ ⠿ Other bot feedback gpt-5 · 9%/372k 4m43s│
|
|
10
10
|
* ╰────────────────────╯ ╰─────────────────────────────────────────────────╯
|
|
11
11
|
* up/down select · right enter · left back · s save report
|
|
12
12
|
*/
|
|
@@ -14,56 +14,59 @@
|
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import {
|
|
17
|
-
getAgentDir,
|
|
18
17
|
type ExtensionContext,
|
|
18
|
+
getAgentDir,
|
|
19
19
|
type KeybindingsManager,
|
|
20
20
|
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { type TUI, truncateToWidth } from "@earendil-works/pi-tui";
|
|
22
|
+
import { AgentSessionPage } from "../shared/agent-session-page.ts";
|
|
23
|
+
import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
|
|
24
|
+
import { contextPercent } from "../shared/context-utilization.ts";
|
|
21
25
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} from "
|
|
26
|
+
panelFrame,
|
|
27
|
+
type ScreenHint,
|
|
28
|
+
screenTitleLine,
|
|
29
|
+
hintLine as sharedHintLine,
|
|
30
|
+
} from "../shared/screen-chrome.ts";
|
|
31
|
+
import { SPINNER_INTERVAL_MS, spinnerFrame } from "../shared/spinner.ts";
|
|
32
|
+
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
29
33
|
import { isAcceptanceLedger } from "./acceptance.ts";
|
|
34
|
+
import { projectWorkflowGraph } from "./graph-projection.ts";
|
|
35
|
+
import {
|
|
36
|
+
classifyInterruptedInvocation,
|
|
37
|
+
decodeInvocationRecord,
|
|
38
|
+
} from "./invocation-ledger.ts";
|
|
30
39
|
import {
|
|
40
|
+
type AgentRecord,
|
|
41
|
+
type AgentUsage,
|
|
31
42
|
agentContext,
|
|
43
|
+
aggregateUsage,
|
|
32
44
|
countStates,
|
|
33
45
|
formatElapsed,
|
|
34
46
|
formatUsage,
|
|
35
|
-
aggregateUsage,
|
|
36
47
|
isWorkflowRunId,
|
|
37
48
|
MAX_LOG_TEXT,
|
|
49
|
+
type PhaseGroup,
|
|
38
50
|
phaseGroups,
|
|
39
|
-
resultJson,
|
|
40
51
|
resolveWorkflowRunTarget,
|
|
52
|
+
resultJson,
|
|
41
53
|
sanitizeLine,
|
|
42
54
|
shortenHome,
|
|
43
|
-
|
|
55
|
+
stateGlyph,
|
|
44
56
|
statusColor,
|
|
57
|
+
statusGlyph,
|
|
45
58
|
statusWord,
|
|
46
|
-
SQUARE,
|
|
47
59
|
type Theme,
|
|
48
|
-
type AgentRecord,
|
|
49
|
-
type AgentUsage,
|
|
50
|
-
type PhaseGroup,
|
|
51
60
|
type TranscriptEntry,
|
|
52
61
|
type WorkflowDetails,
|
|
53
62
|
type WorkflowLogEntry,
|
|
54
63
|
workflowGraphRecords,
|
|
55
64
|
} from "./model.ts";
|
|
56
|
-
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
57
|
-
import { projectWorkflowGraph } from "./graph-projection.ts";
|
|
58
|
-
import {
|
|
59
|
-
classifyInterruptedInvocation,
|
|
60
|
-
decodeInvocationRecord,
|
|
61
|
-
} from "./invocation-ledger.ts";
|
|
62
65
|
import { writeFileAtomic } from "./serialization.ts";
|
|
66
|
+
import { WorkflowTranscriptAdapter } from "./transcript.ts";
|
|
63
67
|
|
|
64
68
|
const NOTICE_TTL_MS = 4000;
|
|
65
69
|
const MIN_HEIGHT = 10;
|
|
66
|
-
const TRANSCRIPT_SCROLL_STEP = 20;
|
|
67
70
|
|
|
68
71
|
function wrapSelection(index: number, delta: number, length: number): number {
|
|
69
72
|
if (length === 0) return 0;
|
|
@@ -80,6 +83,80 @@ function runsDir(): string {
|
|
|
80
83
|
return path.join(getAgentDir(), "workflows");
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
/** Every persisted run id on disk; empty when no runs directory exists. */
|
|
87
|
+
export function listPersistedRunIds(): string[] {
|
|
88
|
+
try {
|
|
89
|
+
return fs.readdirSync(runsDir()).filter(isWorkflowRunId);
|
|
90
|
+
} catch {
|
|
91
|
+
// No runs yet.
|
|
92
|
+
}
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Hydrate the result/transcript side artifacts referenced by workflow.json. */
|
|
97
|
+
function hydrateRunArtifacts(runId: string, details: WorkflowDetails) {
|
|
98
|
+
const runDir = path.join(runsDir(), runId);
|
|
99
|
+
if (details.resultArtifact) {
|
|
100
|
+
try {
|
|
101
|
+
details.result = JSON.parse(
|
|
102
|
+
fs.readFileSync(
|
|
103
|
+
path.join(runDir, path.basename(details.resultArtifact)),
|
|
104
|
+
"utf8",
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
} catch {
|
|
108
|
+
// Keep the compact compatibility marker from workflow.json.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (details.transcriptArtifact) {
|
|
112
|
+
try {
|
|
113
|
+
const transcripts = JSON.parse(
|
|
114
|
+
fs.readFileSync(
|
|
115
|
+
path.join(runDir, path.basename(details.transcriptArtifact)),
|
|
116
|
+
"utf8",
|
|
117
|
+
),
|
|
118
|
+
) as Record<string, unknown>;
|
|
119
|
+
for (const agent of details.agents) {
|
|
120
|
+
agent.transcript = normalizeTranscript(
|
|
121
|
+
transcripts[String(agent.index)],
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Older or partially written artifacts simply lack transcripts.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface ReadPersistedRunOptions {
|
|
131
|
+
/** Hydrate result and transcript side artifacts referenced by workflow.json. */
|
|
132
|
+
hydrateArtifacts?: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The single read entry for a persisted workflow.json: parse, normalize
|
|
137
|
+
* (including runs written by older tooling), and optionally hydrate the side
|
|
138
|
+
* artifacts. Unreadable or invalid runs read as undefined. Stale "running"
|
|
139
|
+
* reconciliation stays with callers so selection filters can run on the
|
|
140
|
+
* recorded timestamps first (`recoverStaleWorkflowDetails`).
|
|
141
|
+
*/
|
|
142
|
+
export function readPersistedWorkflowDetails(
|
|
143
|
+
runId: string,
|
|
144
|
+
options: ReadPersistedRunOptions = {},
|
|
145
|
+
): WorkflowDetails | undefined {
|
|
146
|
+
let details: WorkflowDetails | undefined;
|
|
147
|
+
try {
|
|
148
|
+
const raw: unknown = JSON.parse(
|
|
149
|
+
fs.readFileSync(path.join(runsDir(), runId, "workflow.json"), "utf8"),
|
|
150
|
+
);
|
|
151
|
+
details = normalizePersistedWorkflowDetails(runId, raw);
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
if (!details) return undefined;
|
|
156
|
+
if (options.hydrateArtifacts) hydrateRunArtifacts(runId, details);
|
|
157
|
+
return details;
|
|
158
|
+
}
|
|
159
|
+
|
|
83
160
|
function isWorktreeCleanup(
|
|
84
161
|
value: unknown,
|
|
85
162
|
): value is NonNullable<AgentRecord["worktreeCleanup"]> {
|
|
@@ -117,6 +194,45 @@ function normalizeUsage(value: unknown): AgentUsage {
|
|
|
117
194
|
};
|
|
118
195
|
}
|
|
119
196
|
|
|
197
|
+
function normalizeDelivery(value: unknown): WorkflowDetails["delivery"] {
|
|
198
|
+
if (!value || typeof value !== "object") return undefined;
|
|
199
|
+
const record = value as Record<string, unknown>;
|
|
200
|
+
const state = record.state;
|
|
201
|
+
if (
|
|
202
|
+
state !== "none" &&
|
|
203
|
+
state !== "held-for-inline" &&
|
|
204
|
+
state !== "pending" &&
|
|
205
|
+
state !== "delivered" &&
|
|
206
|
+
state !== "consumed-inline"
|
|
207
|
+
) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
if (typeof record.id !== "string" || record.id.length === 0) return undefined;
|
|
211
|
+
const attempts =
|
|
212
|
+
typeof record.attempts === "number" &&
|
|
213
|
+
Number.isSafeInteger(record.attempts) &&
|
|
214
|
+
record.attempts >= 0
|
|
215
|
+
? record.attempts
|
|
216
|
+
: 0;
|
|
217
|
+
const updatedAt =
|
|
218
|
+
typeof record.updatedAt === "number" && Number.isFinite(record.updatedAt)
|
|
219
|
+
? record.updatedAt
|
|
220
|
+
: 0;
|
|
221
|
+
return {
|
|
222
|
+
id: sanitizeLine(record.id, 256),
|
|
223
|
+
state,
|
|
224
|
+
attempts,
|
|
225
|
+
updatedAt,
|
|
226
|
+
...(typeof record.deliveredAt === "number" &&
|
|
227
|
+
Number.isFinite(record.deliveredAt)
|
|
228
|
+
? { deliveredAt: record.deliveredAt }
|
|
229
|
+
: {}),
|
|
230
|
+
...(typeof record.lastError === "string"
|
|
231
|
+
? { lastError: sanitizeLine(record.lastError, 2_000) }
|
|
232
|
+
: {}),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
120
236
|
function normalizeTranscript(value: unknown): TranscriptEntry[] {
|
|
121
237
|
if (!Array.isArray(value)) return [];
|
|
122
238
|
const transcript: TranscriptEntry[] = [];
|
|
@@ -140,6 +256,10 @@ function normalizeTranscript(value: unknown): TranscriptEntry[] {
|
|
|
140
256
|
typeof entry.name === "string"
|
|
141
257
|
? sanitizeLine(entry.name, 160) || undefined
|
|
142
258
|
: undefined,
|
|
259
|
+
toolCallId:
|
|
260
|
+
typeof entry.toolCallId === "string"
|
|
261
|
+
? sanitizeLine(entry.toolCallId, 1_024) || undefined
|
|
262
|
+
: undefined,
|
|
143
263
|
isError: entry.isError === true,
|
|
144
264
|
timestamp:
|
|
145
265
|
typeof entry.timestamp === "number" ? entry.timestamp : undefined,
|
|
@@ -148,6 +268,35 @@ function normalizeTranscript(value: unknown): TranscriptEntry[] {
|
|
|
148
268
|
return transcript;
|
|
149
269
|
}
|
|
150
270
|
|
|
271
|
+
function normalizeAgentState(value: unknown): AgentRecord["state"] {
|
|
272
|
+
switch (value) {
|
|
273
|
+
case "done":
|
|
274
|
+
case "completed":
|
|
275
|
+
return "done";
|
|
276
|
+
case "error":
|
|
277
|
+
case "failed":
|
|
278
|
+
return "error";
|
|
279
|
+
case "running":
|
|
280
|
+
case "uncertain":
|
|
281
|
+
return value;
|
|
282
|
+
default:
|
|
283
|
+
return "uncertain";
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function normalizeWorkflowStatus(value: unknown): WorkflowDetails["status"] {
|
|
288
|
+
switch (value) {
|
|
289
|
+
case "running":
|
|
290
|
+
case "completed":
|
|
291
|
+
case "failed":
|
|
292
|
+
case "aborted":
|
|
293
|
+
case "uncertain":
|
|
294
|
+
return value;
|
|
295
|
+
default:
|
|
296
|
+
return "uncertain";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
151
300
|
/** Leniently normalize a workflow.json (including runs from older tooling). */
|
|
152
301
|
export function normalizePersistedWorkflowDetails(
|
|
153
302
|
runId: string,
|
|
@@ -163,12 +312,7 @@ export function normalizePersistedWorkflowDetails(
|
|
|
163
312
|
for (const item of rawAgents) {
|
|
164
313
|
if (!item || typeof item !== "object") continue;
|
|
165
314
|
const a = item as Record<string, unknown>;
|
|
166
|
-
const state =
|
|
167
|
-
a.state === "error" || a.state === "failed"
|
|
168
|
-
? "error"
|
|
169
|
-
: a.state === "running"
|
|
170
|
-
? "running"
|
|
171
|
-
: "done";
|
|
315
|
+
const state = normalizeAgentState(a.state);
|
|
172
316
|
const index = typeof a.index === "number" ? a.index : agents.length + 1;
|
|
173
317
|
const decodedInvocation = decodeInvocationRecord(a.invocation);
|
|
174
318
|
const invocation =
|
|
@@ -205,6 +349,11 @@ export function normalizePersistedWorkflowDetails(
|
|
|
205
349
|
...(typeof a.resultRef === "string" && a.resultRef
|
|
206
350
|
? { resultRef: sanitizeLine(a.resultRef, 256) }
|
|
207
351
|
: {}),
|
|
352
|
+
...(typeof a.resultArtifact === "string" &&
|
|
353
|
+
a.resultArtifact.startsWith("agent-results/") &&
|
|
354
|
+
!a.resultArtifact.includes("..")
|
|
355
|
+
? { resultArtifact: sanitizeLine(a.resultArtifact, 256) }
|
|
356
|
+
: {}),
|
|
208
357
|
label:
|
|
209
358
|
typeof a.label === "string"
|
|
210
359
|
? sanitizeLine(a.label, 160) || `agent-${index}`
|
|
@@ -279,15 +428,23 @@ export function normalizePersistedWorkflowDetails(
|
|
|
279
428
|
logs.push({
|
|
280
429
|
at: typeof entry.at === "number" ? entry.at : startedAt,
|
|
281
430
|
text: sanitizeLine(entry.text, MAX_LOG_TEXT),
|
|
431
|
+
...(entry.kind === "pipeline-drop"
|
|
432
|
+
? { kind: "pipeline-drop" as const }
|
|
433
|
+
: {}),
|
|
282
434
|
});
|
|
283
435
|
}
|
|
284
436
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
437
|
+
let status = normalizeWorkflowStatus(record.status);
|
|
438
|
+
if (status !== "running") {
|
|
439
|
+
for (const agent of agents) {
|
|
440
|
+
if (agent.state !== "running" && agent.state !== "uncertain") continue;
|
|
441
|
+
status = "uncertain";
|
|
442
|
+
agent.state = "uncertain";
|
|
443
|
+
agent.error =
|
|
444
|
+
agent.error ??
|
|
445
|
+
"Persisted terminal workflow contained an agent without terminal evidence";
|
|
446
|
+
}
|
|
447
|
+
}
|
|
291
448
|
|
|
292
449
|
return {
|
|
293
450
|
runId,
|
|
@@ -306,6 +463,7 @@ export function normalizePersistedWorkflowDetails(
|
|
|
306
463
|
? sanitizeLine(meta.description, 2_000) || undefined
|
|
307
464
|
: undefined,
|
|
308
465
|
background: record.background === true,
|
|
466
|
+
delivery: normalizeDelivery(record.delivery),
|
|
309
467
|
status,
|
|
310
468
|
startedAt,
|
|
311
469
|
finishedAt:
|
|
@@ -351,13 +509,36 @@ export function recoverStaleWorkflowDetails(
|
|
|
351
509
|
recoveredAt = Date.now(),
|
|
352
510
|
): WorkflowDetails {
|
|
353
511
|
if (details.status !== "running") return details;
|
|
354
|
-
details.status = "
|
|
512
|
+
details.status = "uncertain";
|
|
355
513
|
details.finishedAt = details.finishedAt ?? recoveredAt;
|
|
356
|
-
details.error =
|
|
514
|
+
details.error =
|
|
515
|
+
details.error ??
|
|
516
|
+
"Workflow owner was lost; completion and external side effects are uncertain";
|
|
517
|
+
if (!details.delivery) {
|
|
518
|
+
details.delivery = {
|
|
519
|
+
id: `workflow:${details.runId}`,
|
|
520
|
+
state: "pending",
|
|
521
|
+
attempts: 0,
|
|
522
|
+
updatedAt: recoveredAt,
|
|
523
|
+
lastError: "Migrated a pre-delivery run after its owner disappeared",
|
|
524
|
+
};
|
|
525
|
+
} else if (
|
|
526
|
+
details.delivery.state !== "delivered" &&
|
|
527
|
+
details.delivery.state !== "consumed-inline"
|
|
528
|
+
) {
|
|
529
|
+
details.delivery = {
|
|
530
|
+
...details.delivery,
|
|
531
|
+
state: "pending",
|
|
532
|
+
updatedAt: recoveredAt,
|
|
533
|
+
lastError: "Recovered after the workflow owner process ended",
|
|
534
|
+
};
|
|
535
|
+
}
|
|
357
536
|
for (const agent of details.agents) {
|
|
358
537
|
if (agent.state !== "running") continue;
|
|
359
|
-
agent.state = "
|
|
360
|
-
agent.error =
|
|
538
|
+
agent.state = "uncertain";
|
|
539
|
+
agent.error =
|
|
540
|
+
agent.error ??
|
|
541
|
+
"Workflow owner was lost before this agent produced terminal evidence";
|
|
361
542
|
agent.finishedAt = details.finishedAt;
|
|
362
543
|
}
|
|
363
544
|
details.graph = projectWorkflowGraph(workflowGraphRecords(details.agents));
|
|
@@ -388,70 +569,36 @@ export function loadRunEntries(
|
|
|
388
569
|
referencedRunIds: ReadonlySet<string>,
|
|
389
570
|
/** Hide runs untouched by the current request; live runs always show. */
|
|
390
571
|
startedSince = 0,
|
|
572
|
+
/** Bounded settled projections used only if canonical disk state is unreadable. */
|
|
573
|
+
retained: ReadonlyMap<string, WorkflowDetails> = new Map(),
|
|
391
574
|
): RunEntry[] {
|
|
392
|
-
let names: string[] = [];
|
|
393
|
-
try {
|
|
394
|
-
names = fs.readdirSync(runsDir()).filter(isWorkflowRunId);
|
|
395
|
-
} catch {
|
|
396
|
-
// No runs yet.
|
|
397
|
-
}
|
|
398
575
|
const entries: RunEntry[] = [];
|
|
399
|
-
|
|
576
|
+
const runIds = new Set([...listPersistedRunIds(), ...retained.keys()]);
|
|
577
|
+
for (const runId of runIds) {
|
|
400
578
|
const live = active.get(runId);
|
|
401
579
|
if (live) {
|
|
402
580
|
entries.push({ runId, details: live, live: true });
|
|
403
581
|
continue;
|
|
404
582
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
try {
|
|
422
|
-
details.result = JSON.parse(
|
|
423
|
-
fs.readFileSync(
|
|
424
|
-
path.join(runDir, path.basename(details.resultArtifact)),
|
|
425
|
-
"utf8",
|
|
426
|
-
),
|
|
427
|
-
);
|
|
428
|
-
} catch {
|
|
429
|
-
// Keep the compact compatibility marker from workflow.json.
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
if (details.transcriptArtifact) {
|
|
433
|
-
try {
|
|
434
|
-
const transcripts = JSON.parse(
|
|
435
|
-
fs.readFileSync(
|
|
436
|
-
path.join(runDir, path.basename(details.transcriptArtifact)),
|
|
437
|
-
"utf8",
|
|
438
|
-
),
|
|
439
|
-
) as Record<string, unknown>;
|
|
440
|
-
for (const agent of details.agents) {
|
|
441
|
-
agent.transcript = normalizeTranscript(
|
|
442
|
-
transcripts[String(agent.index)],
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
} catch {
|
|
446
|
-
// Older or partially written artifacts simply lack transcripts.
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
recoverStaleWorkflowDetails(details);
|
|
450
|
-
entries.push({ runId, details, live: false });
|
|
451
|
-
}
|
|
452
|
-
} catch {
|
|
453
|
-
// Skip unreadable runs.
|
|
583
|
+
const persisted = readPersistedWorkflowDetails(runId, {
|
|
584
|
+
hydrateArtifacts: true,
|
|
585
|
+
});
|
|
586
|
+
const retainedDetails = retained.get(runId);
|
|
587
|
+
const details = persisted ?? retainedDetails;
|
|
588
|
+
if (!details) continue;
|
|
589
|
+
const fromRetention =
|
|
590
|
+
persisted === undefined && retainedDetails !== undefined;
|
|
591
|
+
const touchedAt = Math.max(details.startedAt, details.finishedAt ?? 0);
|
|
592
|
+
if (
|
|
593
|
+
touchedAt < startedSince ||
|
|
594
|
+
(!fromRetention &&
|
|
595
|
+
details.sessionId !== sessionId &&
|
|
596
|
+
!referencedRunIds.has(runId))
|
|
597
|
+
) {
|
|
598
|
+
continue;
|
|
454
599
|
}
|
|
600
|
+
recoverStaleWorkflowDetails(details);
|
|
601
|
+
entries.push({ runId, details, live: false });
|
|
455
602
|
}
|
|
456
603
|
return entries.sort((a, b) => b.details.startedAt - a.details.startedAt);
|
|
457
604
|
}
|
|
@@ -472,13 +619,13 @@ export function workflowGraphSummary(
|
|
|
472
619
|
}
|
|
473
620
|
|
|
474
621
|
export function buildWorkflowReport(details: WorkflowDetails): string {
|
|
475
|
-
const { done, failed } = countStates(details);
|
|
622
|
+
const { done, failed, uncertain } = countStates(details);
|
|
476
623
|
const lines: string[] = [
|
|
477
624
|
`# Workflow ${details.name ?? details.runId}`,
|
|
478
625
|
"",
|
|
479
626
|
`- Run: ${details.runId}`,
|
|
480
627
|
`- Status: ${statusWord(details.status)}`,
|
|
481
|
-
`- Agents: ${done}/${details.agents.length} ok${failed ? `, ${failed} failed` : ""}`,
|
|
628
|
+
`- Agents: ${done}/${details.agents.length} ok${failed ? `, ${failed} failed` : ""}${uncertain ? `, ${uncertain} uncertain` : ""}`,
|
|
482
629
|
`- Elapsed: ${formatElapsed(details.startedAt, details.finishedAt)}`,
|
|
483
630
|
];
|
|
484
631
|
const totals = formatUsage(aggregateUsage(details.agents));
|
|
@@ -498,7 +645,9 @@ export function buildWorkflowReport(details: WorkflowDetails): string {
|
|
|
498
645
|
? "ok"
|
|
499
646
|
: agent.state === "error"
|
|
500
647
|
? "FAILED"
|
|
501
|
-
: "
|
|
648
|
+
: agent.state === "uncertain"
|
|
649
|
+
? "UNCERTAIN"
|
|
650
|
+
: "running";
|
|
502
651
|
const stats = [
|
|
503
652
|
agent.model,
|
|
504
653
|
agentContext(agent),
|
|
@@ -565,24 +714,24 @@ export class WorkflowDashboard {
|
|
|
565
714
|
private phaseIndex = 0;
|
|
566
715
|
private agentIndex = 0;
|
|
567
716
|
private detailFocus: DetailFocus = "phases";
|
|
568
|
-
private
|
|
569
|
-
private transcriptRowCount = 0;
|
|
570
|
-
private transcriptViewportSize = 1;
|
|
717
|
+
private transcriptPage?: AgentSessionPage;
|
|
571
718
|
private current?: RunEntry;
|
|
572
719
|
private openedDirectly = false;
|
|
573
720
|
private notice?: string;
|
|
574
721
|
private noticeAt = 0;
|
|
575
722
|
private disposed = false;
|
|
576
|
-
private timer
|
|
723
|
+
private timer?: ReturnType<typeof setInterval>;
|
|
577
724
|
private tui: TUI;
|
|
578
725
|
private theme: Theme;
|
|
579
726
|
private keybindings: KeybindingsManager;
|
|
580
727
|
private getActive: () => Map<string, WorkflowDetails>;
|
|
728
|
+
private getRetained: () => ReadonlyMap<string, WorkflowDetails>;
|
|
581
729
|
private sessionId: string;
|
|
582
730
|
private referencedRunIds: ReadonlySet<string>;
|
|
583
731
|
private startedSince: number;
|
|
584
732
|
private close: () => void;
|
|
585
733
|
private onAbort?: (runId: string) => boolean;
|
|
734
|
+
private initialToolsExpanded: boolean;
|
|
586
735
|
|
|
587
736
|
constructor(
|
|
588
737
|
tui: TUI,
|
|
@@ -595,16 +744,20 @@ export class WorkflowDashboard {
|
|
|
595
744
|
close: () => void,
|
|
596
745
|
initialRunId?: string,
|
|
597
746
|
onAbort?: (runId: string) => boolean,
|
|
747
|
+
getRetained: () => ReadonlyMap<string, WorkflowDetails> = () => new Map(),
|
|
748
|
+
initialToolsExpanded = false,
|
|
598
749
|
) {
|
|
599
750
|
this.tui = tui;
|
|
600
751
|
this.theme = theme;
|
|
601
752
|
this.keybindings = keybindings;
|
|
602
753
|
this.getActive = getActive;
|
|
754
|
+
this.getRetained = getRetained;
|
|
603
755
|
this.sessionId = sessionId;
|
|
604
756
|
this.referencedRunIds = referencedRunIds;
|
|
605
757
|
this.startedSince = startedSince;
|
|
606
758
|
this.close = close;
|
|
607
759
|
this.onAbort = onAbort;
|
|
760
|
+
this.initialToolsExpanded = initialToolsExpanded;
|
|
608
761
|
this.refresh();
|
|
609
762
|
if (initialRunId) {
|
|
610
763
|
const resolution = resolveWorkflowRunTarget(
|
|
@@ -624,25 +777,44 @@ export class WorkflowDashboard {
|
|
|
624
777
|
this.noticeAt = Date.now();
|
|
625
778
|
}
|
|
626
779
|
}
|
|
780
|
+
this.refreshTimer();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
private refreshTimer() {
|
|
784
|
+
const active = Boolean(
|
|
785
|
+
this.notice ||
|
|
786
|
+
(this.view === "list"
|
|
787
|
+
? this.entries.some(
|
|
788
|
+
(entry) => entry.live && entry.details.status === "running",
|
|
789
|
+
)
|
|
790
|
+
: this.view === "detail"
|
|
791
|
+
? this.current?.live && this.current.details.status === "running"
|
|
792
|
+
: this.current?.live && this.selectedAgent()?.state === "running"),
|
|
793
|
+
);
|
|
794
|
+
if (!active) {
|
|
795
|
+
if (this.timer) clearInterval(this.timer);
|
|
796
|
+
this.timer = undefined;
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (this.timer) return;
|
|
627
800
|
this.timer = setInterval(() => {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
) {
|
|
633
|
-
this.refresh();
|
|
634
|
-
this.tui.requestRender();
|
|
635
|
-
}
|
|
636
|
-
}, 500);
|
|
801
|
+
this.refresh();
|
|
802
|
+
this.tui.requestRender();
|
|
803
|
+
this.refreshTimer();
|
|
804
|
+
}, SPINNER_INTERVAL_MS);
|
|
637
805
|
}
|
|
638
806
|
|
|
639
807
|
dispose() {
|
|
640
808
|
if (this.disposed) return;
|
|
641
809
|
this.disposed = true;
|
|
642
|
-
clearInterval(this.timer);
|
|
810
|
+
if (this.timer) clearInterval(this.timer);
|
|
811
|
+
this.timer = undefined;
|
|
812
|
+
this.transcriptPage = undefined;
|
|
643
813
|
}
|
|
644
814
|
|
|
645
|
-
invalidate() {
|
|
815
|
+
invalidate() {
|
|
816
|
+
this.transcriptPage?.invalidate();
|
|
817
|
+
}
|
|
646
818
|
|
|
647
819
|
private refresh() {
|
|
648
820
|
const selected = this.entries[this.listIndex]?.runId;
|
|
@@ -651,6 +823,7 @@ export class WorkflowDashboard {
|
|
|
651
823
|
this.sessionId,
|
|
652
824
|
this.referencedRunIds,
|
|
653
825
|
this.startedSince,
|
|
826
|
+
this.getRetained(),
|
|
654
827
|
);
|
|
655
828
|
if (selected) {
|
|
656
829
|
const index = this.entries.findIndex((e) => e.runId === selected);
|
|
@@ -707,11 +880,12 @@ export class WorkflowDashboard {
|
|
|
707
880
|
const target = path.join(runsDir(), entry.runId, "report.md");
|
|
708
881
|
try {
|
|
709
882
|
writeFileAtomic(target, buildWorkflowReport(entry.details));
|
|
710
|
-
this.
|
|
883
|
+
this.setNotice(`saved ${shortenHome(target)}`);
|
|
711
884
|
} catch (error) {
|
|
712
|
-
this.
|
|
885
|
+
this.setNotice(
|
|
886
|
+
`save failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
887
|
+
);
|
|
713
888
|
}
|
|
714
|
-
this.noticeAt = Date.now();
|
|
715
889
|
}
|
|
716
890
|
|
|
717
891
|
/** Request cancellation of a run by id, surfacing the outcome as a notice. */
|
|
@@ -730,6 +904,7 @@ export class WorkflowDashboard {
|
|
|
730
904
|
private setNotice(text: string) {
|
|
731
905
|
this.notice = text;
|
|
732
906
|
this.noticeAt = Date.now();
|
|
907
|
+
this.refreshTimer();
|
|
733
908
|
}
|
|
734
909
|
|
|
735
910
|
handleInput(data: string) {
|
|
@@ -811,57 +986,27 @@ export class WorkflowDashboard {
|
|
|
811
986
|
} else if (left || cancel) {
|
|
812
987
|
this.detailFocus = "phases";
|
|
813
988
|
} else if ((right || confirm) && this.selectedAgent()) {
|
|
814
|
-
this.
|
|
815
|
-
this.view = "transcript";
|
|
989
|
+
this.openTranscriptPage();
|
|
816
990
|
}
|
|
817
991
|
}
|
|
818
992
|
if (data === "s") this.saveReport();
|
|
819
993
|
if (data === "x") this.abortRun(this.current);
|
|
820
994
|
} else {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
);
|
|
825
|
-
const scrollStep =
|
|
826
|
-
data === "j" || data === "k" ? TRANSCRIPT_SCROLL_STEP : 1;
|
|
827
|
-
const pageStep = Math.max(1, this.transcriptViewportSize - 2);
|
|
828
|
-
if (up) {
|
|
829
|
-
this.transcriptScroll = Math.max(0, this.transcriptScroll - scrollStep);
|
|
830
|
-
} else if (down) {
|
|
831
|
-
this.transcriptScroll = Math.min(
|
|
832
|
-
maxScroll,
|
|
833
|
-
this.transcriptScroll + scrollStep,
|
|
834
|
-
);
|
|
835
|
-
} else if (matchesKey(data, Key.ctrl("u"))) {
|
|
836
|
-
this.transcriptScroll = Math.max(0, this.transcriptScroll - pageStep);
|
|
837
|
-
} else if (matchesKey(data, Key.ctrl("d"))) {
|
|
838
|
-
this.transcriptScroll = Math.min(
|
|
839
|
-
maxScroll,
|
|
840
|
-
this.transcriptScroll + pageStep,
|
|
841
|
-
);
|
|
842
|
-
} else if (data === "g") {
|
|
843
|
-
this.transcriptScroll = 0;
|
|
844
|
-
} else if (data === "G") {
|
|
845
|
-
this.transcriptScroll = maxScroll;
|
|
846
|
-
} else if (cancel || left) {
|
|
847
|
-
this.view = "detail";
|
|
848
|
-
this.detailFocus = "agents";
|
|
849
|
-
}
|
|
995
|
+
this.transcriptPage?.handleInput(data);
|
|
996
|
+
this.refreshTimer();
|
|
997
|
+
return;
|
|
850
998
|
}
|
|
999
|
+
this.refreshTimer();
|
|
851
1000
|
this.tui.requestRender();
|
|
852
1001
|
}
|
|
853
1002
|
|
|
854
1003
|
render(width: number): string[] {
|
|
1004
|
+
if (this.view === "transcript" && this.transcriptPage) {
|
|
1005
|
+
return this.transcriptPage.render(width);
|
|
1006
|
+
}
|
|
855
1007
|
const height = Math.max(MIN_HEIGHT, this.tui.terminal.rows - 1);
|
|
856
1008
|
let lines: string[];
|
|
857
|
-
if (this.view === "
|
|
858
|
-
lines = this.renderTranscript(
|
|
859
|
-
this.current.details,
|
|
860
|
-
this.selectedAgent()!,
|
|
861
|
-
width,
|
|
862
|
-
height,
|
|
863
|
-
);
|
|
864
|
-
} else if (this.view === "detail" && this.current) {
|
|
1009
|
+
if (this.view === "detail" && this.current) {
|
|
865
1010
|
lines = this.renderDetail(this.current.details, width, height);
|
|
866
1011
|
} else {
|
|
867
1012
|
lines = this.renderList(width, height);
|
|
@@ -869,15 +1014,51 @@ export class WorkflowDashboard {
|
|
|
869
1014
|
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
870
1015
|
}
|
|
871
1016
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1017
|
+
private openTranscriptPage() {
|
|
1018
|
+
const transcriptAdapter = new WorkflowTranscriptAdapter();
|
|
1019
|
+
this.view = "transcript";
|
|
1020
|
+
this.transcriptPage = new AgentSessionPage(
|
|
1021
|
+
this.tui,
|
|
1022
|
+
this.theme,
|
|
1023
|
+
this.keybindings,
|
|
1024
|
+
{
|
|
1025
|
+
getState: () => {
|
|
1026
|
+
const details = this.current?.details;
|
|
1027
|
+
const agent = this.selectedAgent();
|
|
1028
|
+
if (!details || !agent) return undefined;
|
|
1029
|
+
return {
|
|
1030
|
+
id: agent.callId ?? `agent-${agent.index}`,
|
|
1031
|
+
title: agent.label,
|
|
1032
|
+
status: agent.state,
|
|
1033
|
+
document: transcriptAdapter.document(
|
|
1034
|
+
agent.transcript,
|
|
1035
|
+
agent.worktreePath,
|
|
1036
|
+
),
|
|
1037
|
+
metadata: [
|
|
1038
|
+
`${details.name ?? details.runId} · ${agent.phase ?? "unphased"}`,
|
|
1039
|
+
agent.model,
|
|
1040
|
+
agentContext(agent),
|
|
1041
|
+
agent.acceptance
|
|
1042
|
+
? `acceptance:${agent.acceptance.status}`
|
|
1043
|
+
: undefined,
|
|
1044
|
+
formatElapsed(agent.startedAt, agent.finishedAt),
|
|
1045
|
+
],
|
|
1046
|
+
errorText: agent.error,
|
|
1047
|
+
emptyText:
|
|
1048
|
+
"transcript unavailable (this run predates transcript capture)",
|
|
1049
|
+
};
|
|
1050
|
+
},
|
|
1051
|
+
close: () => {
|
|
1052
|
+
this.transcriptPage = undefined;
|
|
1053
|
+
this.view = "detail";
|
|
1054
|
+
this.detailFocus = "agents";
|
|
1055
|
+
this.refreshTimer();
|
|
1056
|
+
this.tui.requestRender();
|
|
1057
|
+
},
|
|
1058
|
+
},
|
|
1059
|
+
{ toolsExpanded: this.initialToolsExpanded },
|
|
1060
|
+
);
|
|
1061
|
+
this.tui.requestRender();
|
|
881
1062
|
}
|
|
882
1063
|
|
|
883
1064
|
/** Bordered panel with a title in the top border, padded to exact height. */
|
|
@@ -887,23 +1068,7 @@ export class WorkflowDashboard {
|
|
|
887
1068
|
width: number,
|
|
888
1069
|
height: number,
|
|
889
1070
|
): string[] {
|
|
890
|
-
|
|
891
|
-
const inner = Math.max(0, width - 2);
|
|
892
|
-
const border = (s: string) => theme.fg("borderMuted", s);
|
|
893
|
-
const titleText = truncateToWidth(` ${title} `, Math.max(0, inner - 2));
|
|
894
|
-
const dashes = Math.max(0, inner - visibleWidth(titleText) - 1);
|
|
895
|
-
const lines: string[] = [
|
|
896
|
-
border("╭─") + titleText + border("─".repeat(dashes) + "╮"),
|
|
897
|
-
];
|
|
898
|
-
const bodyHeight = Math.max(0, height - 2);
|
|
899
|
-
for (let i = 0; i < bodyHeight; i++) {
|
|
900
|
-
const row = rows[i] ?? "";
|
|
901
|
-
const clipped = truncateToWidth(row, inner, "…");
|
|
902
|
-
const pad = Math.max(0, inner - visibleWidth(clipped));
|
|
903
|
-
lines.push(border("│") + clipped + " ".repeat(pad) + border("│"));
|
|
904
|
-
}
|
|
905
|
-
lines.push(border("╰" + "─".repeat(inner) + "╯"));
|
|
906
|
-
return lines;
|
|
1071
|
+
return panelFrame(this.theme, { label: title, rows, width, height });
|
|
907
1072
|
}
|
|
908
1073
|
|
|
909
1074
|
/** Scroll window keeping `selected` visible. */
|
|
@@ -924,25 +1089,21 @@ export class WorkflowDashboard {
|
|
|
924
1089
|
return this.keybindings.getKeys(binding).join("/") || "unbound";
|
|
925
1090
|
}
|
|
926
1091
|
|
|
927
|
-
private hintLine(
|
|
928
|
-
|
|
929
|
-
if (this.notice)
|
|
930
|
-
return truncateToWidth(theme.fg("accent", ` ${this.notice}`), width);
|
|
931
|
-
return truncateToWidth(theme.fg("dim", ` ${hint}`), width);
|
|
1092
|
+
private hintLine(hints: readonly ScreenHint[], width: number): string {
|
|
1093
|
+
return sharedHintLine(this.theme, hints, width, this.notice);
|
|
932
1094
|
}
|
|
933
1095
|
|
|
934
1096
|
private renderList(width: number, height: number): string[] {
|
|
935
1097
|
const theme = this.theme;
|
|
936
1098
|
const lines: string[] = [];
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
"
|
|
941
|
-
`${this.entries.length} run${this.entries.length === 1 ? "" : "s"}
|
|
1099
|
+
lines.push(
|
|
1100
|
+
screenTitleLine(
|
|
1101
|
+
theme,
|
|
1102
|
+
"Workflows",
|
|
1103
|
+
`${this.entries.length} run${this.entries.length === 1 ? "" : "s"}`,
|
|
1104
|
+
width,
|
|
942
1105
|
),
|
|
943
|
-
width,
|
|
944
1106
|
);
|
|
945
|
-
lines.push(header);
|
|
946
1107
|
|
|
947
1108
|
const panelHeight = height - 2;
|
|
948
1109
|
const bodyHeight = Math.max(0, panelHeight - 2);
|
|
@@ -957,7 +1118,7 @@ export class WorkflowDashboard {
|
|
|
957
1118
|
),
|
|
958
1119
|
);
|
|
959
1120
|
lines.push(
|
|
960
|
-
this.hintLine(
|
|
1121
|
+
this.hintLine([[this.keys("tui.select.cancel"), "close"]], width),
|
|
961
1122
|
);
|
|
962
1123
|
return lines;
|
|
963
1124
|
}
|
|
@@ -976,22 +1137,30 @@ export class WorkflowDashboard {
|
|
|
976
1137
|
const label = selected
|
|
977
1138
|
? theme.fg("accent", name)
|
|
978
1139
|
: theme.fg("text", name);
|
|
979
|
-
const { done, failed } = countStates(d);
|
|
1140
|
+
const { done, failed, uncertain } = countStates(d);
|
|
980
1141
|
const settled = done + failed;
|
|
981
1142
|
const right =
|
|
982
1143
|
theme.fg(
|
|
983
1144
|
"dim",
|
|
984
|
-
`${settled}/${d.agents.length} agents · ${formatElapsed(d.startedAt, d.finishedAt)} · `,
|
|
1145
|
+
`${settled}/${d.agents.length} agents${uncertain ? ` · ${uncertain} uncertain` : ""} · ${formatElapsed(d.startedAt, d.finishedAt)} · `,
|
|
985
1146
|
) +
|
|
986
1147
|
theme.fg(statusColor(d.status), statusWord(d.status)) +
|
|
987
1148
|
" ";
|
|
988
|
-
const left = ` ${marker} ${
|
|
989
|
-
return
|
|
1149
|
+
const left = ` ${marker} ${statusGlyph(d.status, theme, Date.now())} ${label} ${theme.fg("dim", d.runId)}`;
|
|
1150
|
+
return fitNavigationSides(left, right, width - 2);
|
|
990
1151
|
});
|
|
991
1152
|
lines.push(...this.panel("Runs", rows, width, panelHeight));
|
|
992
1153
|
lines.push(
|
|
993
1154
|
this.hintLine(
|
|
994
|
-
|
|
1155
|
+
[
|
|
1156
|
+
[
|
|
1157
|
+
`${this.keys("tui.select.up")}/${this.keys("tui.select.down")}`,
|
|
1158
|
+
"select",
|
|
1159
|
+
],
|
|
1160
|
+
[this.keys("tui.select.confirm"), "open"],
|
|
1161
|
+
["x", "stop"],
|
|
1162
|
+
[this.keys("tui.select.cancel"), "close"],
|
|
1163
|
+
],
|
|
995
1164
|
width,
|
|
996
1165
|
),
|
|
997
1166
|
);
|
|
@@ -1006,28 +1175,37 @@ export class WorkflowDashboard {
|
|
|
1006
1175
|
const theme = this.theme;
|
|
1007
1176
|
const lines: string[] = [];
|
|
1008
1177
|
|
|
1009
|
-
const { done, failed } = countStates(d);
|
|
1178
|
+
const { done, failed, uncertain } = countStates(d);
|
|
1010
1179
|
const settled = done + failed;
|
|
1180
|
+
// Same language as the transcript card: the glyph carries the state, the
|
|
1181
|
+
// status word only shows for terminal states.
|
|
1011
1182
|
const right =
|
|
1012
1183
|
theme.fg(
|
|
1013
1184
|
"dim",
|
|
1014
|
-
`${settled}/${d.agents.length} agents · ${formatElapsed(d.startedAt, d.finishedAt)}
|
|
1185
|
+
`${settled}/${d.agents.length} agents${uncertain ? ` · ${uncertain} uncertain` : ""} · ${formatElapsed(d.startedAt, d.finishedAt)}`,
|
|
1015
1186
|
) +
|
|
1016
|
-
|
|
1017
|
-
|
|
1187
|
+
(d.status === "running"
|
|
1188
|
+
? " "
|
|
1189
|
+
: theme.fg("dim", " · ") +
|
|
1190
|
+
theme.fg(statusColor(d.status), statusWord(d.status)) +
|
|
1191
|
+
" ");
|
|
1018
1192
|
lines.push(
|
|
1019
|
-
|
|
1020
|
-
|
|
1193
|
+
fitNavigationSides(
|
|
1194
|
+
` ${statusGlyph(d.status, theme, Date.now())} ${theme.bold(theme.fg("accent", d.name ?? d.runId))}`,
|
|
1021
1195
|
right,
|
|
1022
1196
|
width,
|
|
1023
1197
|
),
|
|
1024
1198
|
);
|
|
1025
1199
|
const totals = formatUsage(aggregateUsage(d.agents));
|
|
1026
|
-
|
|
1200
|
+
// A graph with no edges is a flat swarm — "N nodes · 0 edges" is noise.
|
|
1201
|
+
const graphSummary =
|
|
1202
|
+
d.graph && d.graph.edges.length > 0
|
|
1203
|
+
? workflowGraphSummary(d.graph)
|
|
1204
|
+
: undefined;
|
|
1027
1205
|
const subRight = [graphSummary, totals].filter(Boolean).join(" · ");
|
|
1028
1206
|
const subLeft = " " + theme.fg("muted", d.description ?? d.runId);
|
|
1029
1207
|
lines.push(
|
|
1030
|
-
|
|
1208
|
+
fitNavigationSides(
|
|
1031
1209
|
subLeft,
|
|
1032
1210
|
subRight ? theme.fg("dim", `${subRight} `) : " ",
|
|
1033
1211
|
width,
|
|
@@ -1064,7 +1242,7 @@ export class WorkflowDashboard {
|
|
|
1064
1242
|
const groupDone = group.agents.filter(
|
|
1065
1243
|
(a) => a.state !== "running",
|
|
1066
1244
|
).length;
|
|
1067
|
-
const square =
|
|
1245
|
+
const square = groupGlyph(group, theme);
|
|
1068
1246
|
const title =
|
|
1069
1247
|
selected && this.detailFocus === "phases"
|
|
1070
1248
|
? theme.fg("accent", group.title)
|
|
@@ -1073,7 +1251,11 @@ export class WorkflowDashboard {
|
|
|
1073
1251
|
group.agents.length > 0
|
|
1074
1252
|
? theme.fg("dim", `${groupDone}/${group.agents.length} `)
|
|
1075
1253
|
: theme.fg("dim", "- ");
|
|
1076
|
-
return
|
|
1254
|
+
return fitNavigationSides(
|
|
1255
|
+
` ${marker} ${square} ${title}`,
|
|
1256
|
+
counts,
|
|
1257
|
+
sidebarInner,
|
|
1258
|
+
);
|
|
1077
1259
|
});
|
|
1078
1260
|
|
|
1079
1261
|
// Right: agents in the selected phase.
|
|
@@ -1085,6 +1267,10 @@ export class WorkflowDashboard {
|
|
|
1085
1267
|
0,
|
|
1086
1268
|
...selectedGroup.agents.map((a) => a.label.length),
|
|
1087
1269
|
);
|
|
1270
|
+
const models = new Set(
|
|
1271
|
+
selectedGroup.agents.map((a) => a.model).filter(Boolean),
|
|
1272
|
+
);
|
|
1273
|
+
const mixedModels = models.size > 1;
|
|
1088
1274
|
const agentWindow = this.windowed(
|
|
1089
1275
|
selectedGroup.agents,
|
|
1090
1276
|
this.agentIndex,
|
|
@@ -1097,9 +1283,18 @@ export class WorkflowDashboard {
|
|
|
1097
1283
|
selected && this.detailFocus === "agents"
|
|
1098
1284
|
? theme.fg("accent", "❯")
|
|
1099
1285
|
: " ";
|
|
1286
|
+
// The model repeats on every row when the run is homogeneous; only
|
|
1287
|
+
// mixed fleets earn a per-row model. Context occupancy matters while
|
|
1288
|
+
// an agent runs; once settled, its cost is the elapsed on the right.
|
|
1289
|
+
const percent = contextPercent({
|
|
1290
|
+
tokens: agent.usage.contextTokens,
|
|
1291
|
+
contextWindow: agent.contextWindow,
|
|
1292
|
+
});
|
|
1100
1293
|
const stats = [
|
|
1101
|
-
agent.model,
|
|
1102
|
-
|
|
1294
|
+
mixedModels ? agent.model : undefined,
|
|
1295
|
+
agent.state === "running" && percent !== undefined
|
|
1296
|
+
? `${percent}%`
|
|
1297
|
+
: undefined,
|
|
1103
1298
|
agent.acceptance
|
|
1104
1299
|
? `acceptance:${agent.acceptance.status}`
|
|
1105
1300
|
: undefined,
|
|
@@ -1110,16 +1305,16 @@ export class WorkflowDashboard {
|
|
|
1110
1305
|
selected && this.detailFocus === "agents"
|
|
1111
1306
|
? theme.fg("accent", agent.label.padEnd(Math.min(maxLabel, 40)))
|
|
1112
1307
|
: theme.fg("text", agent.label.padEnd(Math.min(maxLabel, 40)));
|
|
1113
|
-
const left = ` ${marker} ${
|
|
1308
|
+
const left = ` ${marker} ${stateGlyph(agent.state, theme, Date.now())} ${label}${stats ? ` ${theme.fg("dim", stats)}` : ""}`;
|
|
1114
1309
|
const right = theme.fg(
|
|
1115
1310
|
"dim",
|
|
1116
1311
|
`${formatElapsed(agent.startedAt, agent.finishedAt)} `,
|
|
1117
1312
|
);
|
|
1118
|
-
agentRows.push(
|
|
1313
|
+
agentRows.push(fitNavigationSides(left, right, agentsInner));
|
|
1119
1314
|
if (agent.error) {
|
|
1120
1315
|
agentRows.push(
|
|
1121
1316
|
truncateToWidth(
|
|
1122
|
-
` ${theme.fg("error",
|
|
1317
|
+
` ${theme.fg("error", displayError(agent.error))}`,
|
|
1123
1318
|
agentsInner,
|
|
1124
1319
|
"…",
|
|
1125
1320
|
),
|
|
@@ -1170,136 +1365,57 @@ export class WorkflowDashboard {
|
|
|
1170
1365
|
);
|
|
1171
1366
|
}
|
|
1172
1367
|
|
|
1173
|
-
const
|
|
1368
|
+
const hints: ScreenHint[] =
|
|
1174
1369
|
this.detailFocus === "phases"
|
|
1175
|
-
?
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
const contentWidth = Math.max(8, width - 4);
|
|
1200
|
-
const styled = theme.fg(
|
|
1201
|
-
entry.role === "thinking" ? "dim" : entry.isError ? "error" : "text",
|
|
1202
|
-
sanitizeTerminalText(entry.text),
|
|
1203
|
-
);
|
|
1204
|
-
for (const line of wrapTextWithAnsi(styled, contentWidth)) {
|
|
1205
|
-
rows.push(` ${line}`);
|
|
1206
|
-
}
|
|
1207
|
-
rows.push("");
|
|
1208
|
-
}
|
|
1209
|
-
return rows;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
private renderTranscript(
|
|
1213
|
-
details: WorkflowDetails,
|
|
1214
|
-
agent: AgentRecord,
|
|
1215
|
-
width: number,
|
|
1216
|
-
height: number,
|
|
1217
|
-
): string[] {
|
|
1218
|
-
const theme = this.theme;
|
|
1219
|
-
const lines: string[] = [];
|
|
1220
|
-
const right = theme.fg(
|
|
1221
|
-
"dim",
|
|
1222
|
-
[
|
|
1223
|
-
agent.model,
|
|
1224
|
-
agentContext(agent),
|
|
1225
|
-
agent.acceptance ? `acceptance:${agent.acceptance.status}` : undefined,
|
|
1226
|
-
formatElapsed(agent.startedAt, agent.finishedAt),
|
|
1227
|
-
]
|
|
1228
|
-
.filter(Boolean)
|
|
1229
|
-
.join(" · ") + " ",
|
|
1230
|
-
);
|
|
1231
|
-
lines.push(
|
|
1232
|
-
this.split(
|
|
1233
|
-
` ${stateSquare(agent.state, theme)} ${theme.bold(theme.fg("accent", agent.label))}`,
|
|
1234
|
-
right,
|
|
1235
|
-
width,
|
|
1236
|
-
),
|
|
1237
|
-
);
|
|
1238
|
-
lines.push(
|
|
1239
|
-
this.split(
|
|
1240
|
-
` ${theme.fg("muted", `${details.name ?? details.runId} · ${agent.phase ?? "unphased"}`)}`,
|
|
1241
|
-
theme.fg("dim", `${agent.transcript.length} entries `),
|
|
1242
|
-
width,
|
|
1243
|
-
),
|
|
1244
|
-
);
|
|
1245
|
-
|
|
1246
|
-
const panelHeight = height - 3;
|
|
1247
|
-
const bodyHeight = Math.max(1, panelHeight - 2);
|
|
1248
|
-
const rows = this.transcriptRows(agent, width - 2);
|
|
1249
|
-
this.transcriptRowCount = rows.length;
|
|
1250
|
-
this.transcriptViewportSize = bodyHeight;
|
|
1251
|
-
const maxScroll = Math.max(0, rows.length - bodyHeight);
|
|
1252
|
-
this.transcriptScroll = Math.min(this.transcriptScroll, maxScroll);
|
|
1253
|
-
const visible = rows.slice(
|
|
1254
|
-
this.transcriptScroll,
|
|
1255
|
-
this.transcriptScroll + bodyHeight,
|
|
1256
|
-
);
|
|
1257
|
-
const position =
|
|
1258
|
-
rows.length > bodyHeight
|
|
1259
|
-
? `Transcript · ${this.transcriptScroll + 1}-${Math.min(rows.length, this.transcriptScroll + bodyHeight)}/${rows.length}`
|
|
1260
|
-
: "Transcript";
|
|
1261
|
-
lines.push(...this.panel(position, visible, width, panelHeight));
|
|
1262
|
-
lines.push(
|
|
1263
|
-
this.hintLine(
|
|
1264
|
-
"j/k scroll · ctrl-u/d page · g/G top/bottom · h/left/esc back",
|
|
1265
|
-
width,
|
|
1266
|
-
),
|
|
1267
|
-
);
|
|
1370
|
+
? [
|
|
1371
|
+
["j/k", "select phase"],
|
|
1372
|
+
[
|
|
1373
|
+
`l/${this.keys("tui.editor.cursorRight")}/${this.keys("tui.select.confirm")}`,
|
|
1374
|
+
"agents",
|
|
1375
|
+
],
|
|
1376
|
+
[this.keys("tui.select.cancel"), "back"],
|
|
1377
|
+
["x", "stop"],
|
|
1378
|
+
["s", "save report"],
|
|
1379
|
+
]
|
|
1380
|
+
: [
|
|
1381
|
+
["j/k", "select agent"],
|
|
1382
|
+
[
|
|
1383
|
+
`h/${this.keys("tui.editor.cursorLeft")}/${this.keys("tui.select.cancel")}`,
|
|
1384
|
+
"phases",
|
|
1385
|
+
],
|
|
1386
|
+
[
|
|
1387
|
+
`l/${this.keys("tui.editor.cursorRight")}/${this.keys("tui.select.confirm")}`,
|
|
1388
|
+
"details",
|
|
1389
|
+
],
|
|
1390
|
+
["x", "stop"],
|
|
1391
|
+
["s", "save report"],
|
|
1392
|
+
];
|
|
1393
|
+
lines.push(this.hintLine(hints, width));
|
|
1268
1394
|
return lines;
|
|
1269
1395
|
}
|
|
1270
1396
|
}
|
|
1271
1397
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
entry: TranscriptEntry,
|
|
1283
|
-
): "accent" | "success" | "dim" | "warning" | "error" | "muted" {
|
|
1284
|
-
if (entry.isError) return "error";
|
|
1285
|
-
if (entry.role === "user") return "accent";
|
|
1286
|
-
if (entry.role === "assistant") return "success";
|
|
1287
|
-
if (entry.role === "thinking") return "dim";
|
|
1288
|
-
if (entry.role === "tool") return "warning";
|
|
1289
|
-
return "muted";
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
function statusSquareFor(details: WorkflowDetails, theme: Theme): string {
|
|
1293
|
-
return theme.fg(statusColor(details.status), SQUARE);
|
|
1398
|
+
/**
|
|
1399
|
+
* Agent errors often arrive as an HTTP status plus a JSON body
|
|
1400
|
+
* (`429: {"message":"user rate limit exceeded …"}`); the panel row keeps the
|
|
1401
|
+
* status code and the message, dropping the braces and quotes.
|
|
1402
|
+
*/
|
|
1403
|
+
function displayError(error: string) {
|
|
1404
|
+
const clean = sanitizeLine(error, 2_000);
|
|
1405
|
+
const match = clean.match(/^(\d{3})[:\s]*\{\s*"message"\s*:\s*"([^"]+)"/);
|
|
1406
|
+
if (match) return `${match[1]}: ${match[2]}`;
|
|
1407
|
+
return clean;
|
|
1294
1408
|
}
|
|
1295
1409
|
|
|
1296
|
-
function
|
|
1297
|
-
if (group.agents.length === 0) return theme.fg("dim",
|
|
1410
|
+
function groupGlyph(group: PhaseGroup, theme: Theme) {
|
|
1411
|
+
if (group.agents.length === 0) return theme.fg("dim", "○");
|
|
1298
1412
|
if (group.agents.some((a) => a.state === "running"))
|
|
1299
|
-
return theme.fg("warning",
|
|
1413
|
+
return theme.fg("warning", spinnerFrame(Date.now()));
|
|
1414
|
+
if (group.agents.some((a) => a.state === "uncertain"))
|
|
1415
|
+
return theme.fg("warning", "?");
|
|
1300
1416
|
if (group.agents.some((a) => a.state === "error"))
|
|
1301
|
-
return theme.fg("error",
|
|
1302
|
-
return theme.fg("success",
|
|
1417
|
+
return theme.fg("error", "✗");
|
|
1418
|
+
return theme.fg("success", "✓");
|
|
1303
1419
|
}
|
|
1304
1420
|
|
|
1305
1421
|
/** Open the dashboard as a full-screen overlay. */
|
|
@@ -1309,10 +1425,11 @@ export async function showWorkflowDashboard(
|
|
|
1309
1425
|
initialRunId?: string,
|
|
1310
1426
|
startedSince = 0,
|
|
1311
1427
|
onAbort?: (runId: string) => boolean,
|
|
1312
|
-
)
|
|
1428
|
+
getRetained?: () => ReadonlyMap<string, WorkflowDetails>,
|
|
1429
|
+
) {
|
|
1313
1430
|
await ctx.ui.custom<void>(
|
|
1314
1431
|
(tui, theme, keybindings, done) => {
|
|
1315
|
-
const dashboard
|
|
1432
|
+
const dashboard = new WorkflowDashboard(
|
|
1316
1433
|
tui,
|
|
1317
1434
|
theme,
|
|
1318
1435
|
keybindings,
|
|
@@ -1326,12 +1443,14 @@ export async function showWorkflowDashboard(
|
|
|
1326
1443
|
},
|
|
1327
1444
|
initialRunId,
|
|
1328
1445
|
onAbort,
|
|
1446
|
+
getRetained,
|
|
1447
|
+
ctx.ui.getToolsExpanded(),
|
|
1329
1448
|
);
|
|
1330
1449
|
return dashboard;
|
|
1331
1450
|
},
|
|
1332
1451
|
{
|
|
1333
1452
|
overlay: true,
|
|
1334
|
-
overlayOptions: { anchor: "
|
|
1453
|
+
overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" },
|
|
1335
1454
|
},
|
|
1336
1455
|
);
|
|
1337
1456
|
}
|