@tt-a1i/openpi 0.3.1 → 0.4.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 +87 -24
- package/SETUP.md +3 -3
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/src/prompt.ts +1 -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/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +387 -88
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +18 -18
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +330 -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/sessions/index.ts +70 -55
- package/extensions/setup/index.ts +6 -6
- package/extensions/shared/activity-status.ts +6 -5
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +7 -1
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +24 -5
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/text-projection.ts +56 -0
- package/extensions/shared/tool-surface.ts +13 -6
- package/extensions/subagents/index.ts +204 -140
- package/extensions/subagents/navigation.ts +52 -23
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backends/stub.ts +7 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +620 -537
- package/extensions/subagents/src/prompt.ts +153 -38
- package/extensions/subagents/src/result-artifact.ts +142 -0
- package/extensions/subagents/src/runtime.ts +8 -5
- package/extensions/subagents/src/ui/takeover.ts +84 -109
- package/extensions/subagents/src/ui/transcript.ts +76 -42
- package/extensions/subagents/src/ui/wait-result.ts +1 -1
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/workflows/artifacts.ts +35 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +64 -0
- package/extensions/workflows/dashboard.ts +353 -173
- package/extensions/workflows/handoff.ts +62 -20
- package/extensions/workflows/index.ts +647 -387
- package/extensions/workflows/model.ts +57 -15
- package/extensions/workflows/navigation.ts +33 -14
- package/extensions/workflows/prompt.ts +104 -8
- package/extensions/workflows/replay-safety.ts +16 -6
- package/extensions/workflows/result-delivery.ts +189 -0
- package/extensions/workflows/sandbox-child.cjs +11 -0
- package/package.json +1 -1
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +7 -4
- package/skills/workflows/SKILL.md +53 -10
- 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,51 +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
21
|
import {
|
|
22
22
|
Key,
|
|
23
23
|
matchesKey,
|
|
24
|
+
type TUI,
|
|
24
25
|
truncateToWidth,
|
|
25
|
-
visibleWidth,
|
|
26
26
|
wrapTextWithAnsi,
|
|
27
|
-
type TUI,
|
|
28
27
|
} from "@earendil-works/pi-tui";
|
|
28
|
+
import { contextPercent } from "../shared/context-utilization.ts";
|
|
29
|
+
import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
|
|
30
|
+
import {
|
|
31
|
+
panelFrame,
|
|
32
|
+
type ScreenHint,
|
|
33
|
+
screenTitleLine,
|
|
34
|
+
hintLine as sharedHintLine,
|
|
35
|
+
} from "../shared/screen-chrome.ts";
|
|
36
|
+
import { SPINNER_INTERVAL_MS, spinnerFrame } from "../shared/spinner.ts";
|
|
37
|
+
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
29
38
|
import { isAcceptanceLedger } from "./acceptance.ts";
|
|
39
|
+
import { projectWorkflowGraph } from "./graph-projection.ts";
|
|
40
|
+
import {
|
|
41
|
+
classifyInterruptedInvocation,
|
|
42
|
+
decodeInvocationRecord,
|
|
43
|
+
} from "./invocation-ledger.ts";
|
|
30
44
|
import {
|
|
45
|
+
type AgentRecord,
|
|
46
|
+
type AgentUsage,
|
|
31
47
|
agentContext,
|
|
48
|
+
aggregateUsage,
|
|
32
49
|
countStates,
|
|
33
50
|
formatElapsed,
|
|
34
51
|
formatUsage,
|
|
35
|
-
aggregateUsage,
|
|
36
52
|
isWorkflowRunId,
|
|
37
53
|
MAX_LOG_TEXT,
|
|
54
|
+
type PhaseGroup,
|
|
38
55
|
phaseGroups,
|
|
39
|
-
resultJson,
|
|
40
56
|
resolveWorkflowRunTarget,
|
|
57
|
+
resultJson,
|
|
41
58
|
sanitizeLine,
|
|
42
59
|
shortenHome,
|
|
43
|
-
|
|
60
|
+
stateGlyph,
|
|
44
61
|
statusColor,
|
|
62
|
+
statusGlyph,
|
|
45
63
|
statusWord,
|
|
46
|
-
SQUARE,
|
|
47
64
|
type Theme,
|
|
48
|
-
type AgentRecord,
|
|
49
|
-
type AgentUsage,
|
|
50
|
-
type PhaseGroup,
|
|
51
65
|
type TranscriptEntry,
|
|
52
66
|
type WorkflowDetails,
|
|
53
67
|
type WorkflowLogEntry,
|
|
54
68
|
workflowGraphRecords,
|
|
55
69
|
} 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
70
|
import { writeFileAtomic } from "./serialization.ts";
|
|
63
71
|
|
|
64
72
|
const NOTICE_TTL_MS = 4000;
|
|
@@ -80,6 +88,80 @@ function runsDir(): string {
|
|
|
80
88
|
return path.join(getAgentDir(), "workflows");
|
|
81
89
|
}
|
|
82
90
|
|
|
91
|
+
/** Every persisted run id on disk; empty when no runs directory exists. */
|
|
92
|
+
export function listPersistedRunIds(): string[] {
|
|
93
|
+
try {
|
|
94
|
+
return fs.readdirSync(runsDir()).filter(isWorkflowRunId);
|
|
95
|
+
} catch {
|
|
96
|
+
// No runs yet.
|
|
97
|
+
}
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Hydrate the result/transcript side artifacts referenced by workflow.json. */
|
|
102
|
+
function hydrateRunArtifacts(runId: string, details: WorkflowDetails) {
|
|
103
|
+
const runDir = path.join(runsDir(), runId);
|
|
104
|
+
if (details.resultArtifact) {
|
|
105
|
+
try {
|
|
106
|
+
details.result = JSON.parse(
|
|
107
|
+
fs.readFileSync(
|
|
108
|
+
path.join(runDir, path.basename(details.resultArtifact)),
|
|
109
|
+
"utf8",
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
} catch {
|
|
113
|
+
// Keep the compact compatibility marker from workflow.json.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (details.transcriptArtifact) {
|
|
117
|
+
try {
|
|
118
|
+
const transcripts = JSON.parse(
|
|
119
|
+
fs.readFileSync(
|
|
120
|
+
path.join(runDir, path.basename(details.transcriptArtifact)),
|
|
121
|
+
"utf8",
|
|
122
|
+
),
|
|
123
|
+
) as Record<string, unknown>;
|
|
124
|
+
for (const agent of details.agents) {
|
|
125
|
+
agent.transcript = normalizeTranscript(
|
|
126
|
+
transcripts[String(agent.index)],
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Older or partially written artifacts simply lack transcripts.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ReadPersistedRunOptions {
|
|
136
|
+
/** Hydrate result and transcript side artifacts referenced by workflow.json. */
|
|
137
|
+
hydrateArtifacts?: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The single read entry for a persisted workflow.json: parse, normalize
|
|
142
|
+
* (including runs written by older tooling), and optionally hydrate the side
|
|
143
|
+
* artifacts. Unreadable or invalid runs read as undefined. Stale "running"
|
|
144
|
+
* reconciliation stays with callers so selection filters can run on the
|
|
145
|
+
* recorded timestamps first (`recoverStaleWorkflowDetails`).
|
|
146
|
+
*/
|
|
147
|
+
export function readPersistedWorkflowDetails(
|
|
148
|
+
runId: string,
|
|
149
|
+
options: ReadPersistedRunOptions = {},
|
|
150
|
+
): WorkflowDetails | undefined {
|
|
151
|
+
let details: WorkflowDetails | undefined;
|
|
152
|
+
try {
|
|
153
|
+
const raw: unknown = JSON.parse(
|
|
154
|
+
fs.readFileSync(path.join(runsDir(), runId, "workflow.json"), "utf8"),
|
|
155
|
+
);
|
|
156
|
+
details = normalizePersistedWorkflowDetails(runId, raw);
|
|
157
|
+
} catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
if (!details) return undefined;
|
|
161
|
+
if (options.hydrateArtifacts) hydrateRunArtifacts(runId, details);
|
|
162
|
+
return details;
|
|
163
|
+
}
|
|
164
|
+
|
|
83
165
|
function isWorktreeCleanup(
|
|
84
166
|
value: unknown,
|
|
85
167
|
): value is NonNullable<AgentRecord["worktreeCleanup"]> {
|
|
@@ -117,6 +199,45 @@ function normalizeUsage(value: unknown): AgentUsage {
|
|
|
117
199
|
};
|
|
118
200
|
}
|
|
119
201
|
|
|
202
|
+
function normalizeDelivery(value: unknown): WorkflowDetails["delivery"] {
|
|
203
|
+
if (!value || typeof value !== "object") return undefined;
|
|
204
|
+
const record = value as Record<string, unknown>;
|
|
205
|
+
const state = record.state;
|
|
206
|
+
if (
|
|
207
|
+
state !== "none" &&
|
|
208
|
+
state !== "held-for-inline" &&
|
|
209
|
+
state !== "pending" &&
|
|
210
|
+
state !== "delivered" &&
|
|
211
|
+
state !== "consumed-inline"
|
|
212
|
+
) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
if (typeof record.id !== "string" || record.id.length === 0) return undefined;
|
|
216
|
+
const attempts =
|
|
217
|
+
typeof record.attempts === "number" &&
|
|
218
|
+
Number.isSafeInteger(record.attempts) &&
|
|
219
|
+
record.attempts >= 0
|
|
220
|
+
? record.attempts
|
|
221
|
+
: 0;
|
|
222
|
+
const updatedAt =
|
|
223
|
+
typeof record.updatedAt === "number" && Number.isFinite(record.updatedAt)
|
|
224
|
+
? record.updatedAt
|
|
225
|
+
: 0;
|
|
226
|
+
return {
|
|
227
|
+
id: sanitizeLine(record.id, 256),
|
|
228
|
+
state,
|
|
229
|
+
attempts,
|
|
230
|
+
updatedAt,
|
|
231
|
+
...(typeof record.deliveredAt === "number" &&
|
|
232
|
+
Number.isFinite(record.deliveredAt)
|
|
233
|
+
? { deliveredAt: record.deliveredAt }
|
|
234
|
+
: {}),
|
|
235
|
+
...(typeof record.lastError === "string"
|
|
236
|
+
? { lastError: sanitizeLine(record.lastError, 2_000) }
|
|
237
|
+
: {}),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
120
241
|
function normalizeTranscript(value: unknown): TranscriptEntry[] {
|
|
121
242
|
if (!Array.isArray(value)) return [];
|
|
122
243
|
const transcript: TranscriptEntry[] = [];
|
|
@@ -166,9 +287,11 @@ export function normalizePersistedWorkflowDetails(
|
|
|
166
287
|
const state =
|
|
167
288
|
a.state === "error" || a.state === "failed"
|
|
168
289
|
? "error"
|
|
169
|
-
: a.state === "
|
|
170
|
-
? "
|
|
171
|
-
: "
|
|
290
|
+
: a.state === "uncertain"
|
|
291
|
+
? "uncertain"
|
|
292
|
+
: a.state === "running"
|
|
293
|
+
? "running"
|
|
294
|
+
: "done";
|
|
172
295
|
const index = typeof a.index === "number" ? a.index : agents.length + 1;
|
|
173
296
|
const decodedInvocation = decodeInvocationRecord(a.invocation);
|
|
174
297
|
const invocation =
|
|
@@ -205,6 +328,11 @@ export function normalizePersistedWorkflowDetails(
|
|
|
205
328
|
...(typeof a.resultRef === "string" && a.resultRef
|
|
206
329
|
? { resultRef: sanitizeLine(a.resultRef, 256) }
|
|
207
330
|
: {}),
|
|
331
|
+
...(typeof a.resultArtifact === "string" &&
|
|
332
|
+
a.resultArtifact.startsWith("agent-results/") &&
|
|
333
|
+
!a.resultArtifact.includes("..")
|
|
334
|
+
? { resultArtifact: sanitizeLine(a.resultArtifact, 256) }
|
|
335
|
+
: {}),
|
|
208
336
|
label:
|
|
209
337
|
typeof a.label === "string"
|
|
210
338
|
? sanitizeLine(a.label, 160) || `agent-${index}`
|
|
@@ -285,7 +413,8 @@ export function normalizePersistedWorkflowDetails(
|
|
|
285
413
|
const status =
|
|
286
414
|
record.status === "running" ||
|
|
287
415
|
record.status === "failed" ||
|
|
288
|
-
record.status === "aborted"
|
|
416
|
+
record.status === "aborted" ||
|
|
417
|
+
record.status === "uncertain"
|
|
289
418
|
? record.status
|
|
290
419
|
: "completed";
|
|
291
420
|
|
|
@@ -306,6 +435,7 @@ export function normalizePersistedWorkflowDetails(
|
|
|
306
435
|
? sanitizeLine(meta.description, 2_000) || undefined
|
|
307
436
|
: undefined,
|
|
308
437
|
background: record.background === true,
|
|
438
|
+
delivery: normalizeDelivery(record.delivery),
|
|
309
439
|
status,
|
|
310
440
|
startedAt,
|
|
311
441
|
finishedAt:
|
|
@@ -351,13 +481,36 @@ export function recoverStaleWorkflowDetails(
|
|
|
351
481
|
recoveredAt = Date.now(),
|
|
352
482
|
): WorkflowDetails {
|
|
353
483
|
if (details.status !== "running") return details;
|
|
354
|
-
details.status = "
|
|
484
|
+
details.status = "uncertain";
|
|
355
485
|
details.finishedAt = details.finishedAt ?? recoveredAt;
|
|
356
|
-
details.error =
|
|
486
|
+
details.error =
|
|
487
|
+
details.error ??
|
|
488
|
+
"Workflow owner was lost; completion and external side effects are uncertain";
|
|
489
|
+
if (!details.delivery) {
|
|
490
|
+
details.delivery = {
|
|
491
|
+
id: `workflow:${details.runId}`,
|
|
492
|
+
state: "pending",
|
|
493
|
+
attempts: 0,
|
|
494
|
+
updatedAt: recoveredAt,
|
|
495
|
+
lastError: "Migrated a pre-delivery run after its owner disappeared",
|
|
496
|
+
};
|
|
497
|
+
} else if (
|
|
498
|
+
details.delivery.state !== "delivered" &&
|
|
499
|
+
details.delivery.state !== "consumed-inline"
|
|
500
|
+
) {
|
|
501
|
+
details.delivery = {
|
|
502
|
+
...details.delivery,
|
|
503
|
+
state: "pending",
|
|
504
|
+
updatedAt: recoveredAt,
|
|
505
|
+
lastError: "Recovered after the workflow owner process ended",
|
|
506
|
+
};
|
|
507
|
+
}
|
|
357
508
|
for (const agent of details.agents) {
|
|
358
509
|
if (agent.state !== "running") continue;
|
|
359
|
-
agent.state = "
|
|
360
|
-
agent.error =
|
|
510
|
+
agent.state = "uncertain";
|
|
511
|
+
agent.error =
|
|
512
|
+
agent.error ??
|
|
513
|
+
"Workflow owner was lost before this agent produced terminal evidence";
|
|
361
514
|
agent.finishedAt = details.finishedAt;
|
|
362
515
|
}
|
|
363
516
|
details.graph = projectWorkflowGraph(workflowGraphRecords(details.agents));
|
|
@@ -389,69 +542,26 @@ export function loadRunEntries(
|
|
|
389
542
|
/** Hide runs untouched by the current request; live runs always show. */
|
|
390
543
|
startedSince = 0,
|
|
391
544
|
): RunEntry[] {
|
|
392
|
-
let names: string[] = [];
|
|
393
|
-
try {
|
|
394
|
-
names = fs.readdirSync(runsDir()).filter(isWorkflowRunId);
|
|
395
|
-
} catch {
|
|
396
|
-
// No runs yet.
|
|
397
|
-
}
|
|
398
545
|
const entries: RunEntry[] = [];
|
|
399
|
-
for (const runId of
|
|
546
|
+
for (const runId of listPersistedRunIds()) {
|
|
400
547
|
const live = active.get(runId);
|
|
401
548
|
if (live) {
|
|
402
549
|
entries.push({ runId, details: live, live: true });
|
|
403
550
|
continue;
|
|
404
551
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
details &&
|
|
416
|
-
touchedAt >= startedSince &&
|
|
417
|
-
(details.sessionId === sessionId || referencedRunIds.has(runId))
|
|
418
|
-
) {
|
|
419
|
-
const runDir = path.join(runsDir(), runId);
|
|
420
|
-
if (details.resultArtifact) {
|
|
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.
|
|
552
|
+
const details = readPersistedWorkflowDetails(runId, {
|
|
553
|
+
hydrateArtifacts: true,
|
|
554
|
+
});
|
|
555
|
+
if (!details) continue;
|
|
556
|
+
const touchedAt = Math.max(details.startedAt, details.finishedAt ?? 0);
|
|
557
|
+
if (
|
|
558
|
+
touchedAt < startedSince ||
|
|
559
|
+
(details.sessionId !== sessionId && !referencedRunIds.has(runId))
|
|
560
|
+
) {
|
|
561
|
+
continue;
|
|
454
562
|
}
|
|
563
|
+
recoverStaleWorkflowDetails(details);
|
|
564
|
+
entries.push({ runId, details, live: false });
|
|
455
565
|
}
|
|
456
566
|
return entries.sort((a, b) => b.details.startedAt - a.details.startedAt);
|
|
457
567
|
}
|
|
@@ -472,13 +582,13 @@ export function workflowGraphSummary(
|
|
|
472
582
|
}
|
|
473
583
|
|
|
474
584
|
export function buildWorkflowReport(details: WorkflowDetails): string {
|
|
475
|
-
const { done, failed } = countStates(details);
|
|
585
|
+
const { done, failed, uncertain } = countStates(details);
|
|
476
586
|
const lines: string[] = [
|
|
477
587
|
`# Workflow ${details.name ?? details.runId}`,
|
|
478
588
|
"",
|
|
479
589
|
`- Run: ${details.runId}`,
|
|
480
590
|
`- Status: ${statusWord(details.status)}`,
|
|
481
|
-
`- Agents: ${done}/${details.agents.length} ok${failed ? `, ${failed} failed` : ""}`,
|
|
591
|
+
`- Agents: ${done}/${details.agents.length} ok${failed ? `, ${failed} failed` : ""}${uncertain ? `, ${uncertain} uncertain` : ""}`,
|
|
482
592
|
`- Elapsed: ${formatElapsed(details.startedAt, details.finishedAt)}`,
|
|
483
593
|
];
|
|
484
594
|
const totals = formatUsage(aggregateUsage(details.agents));
|
|
@@ -498,7 +608,9 @@ export function buildWorkflowReport(details: WorkflowDetails): string {
|
|
|
498
608
|
? "ok"
|
|
499
609
|
: agent.state === "error"
|
|
500
610
|
? "FAILED"
|
|
501
|
-
: "
|
|
611
|
+
: agent.state === "uncertain"
|
|
612
|
+
? "UNCERTAIN"
|
|
613
|
+
: "running";
|
|
502
614
|
const stats = [
|
|
503
615
|
agent.model,
|
|
504
616
|
agentContext(agent),
|
|
@@ -633,7 +745,7 @@ export class WorkflowDashboard {
|
|
|
633
745
|
this.refresh();
|
|
634
746
|
this.tui.requestRender();
|
|
635
747
|
}
|
|
636
|
-
},
|
|
748
|
+
}, SPINNER_INTERVAL_MS);
|
|
637
749
|
}
|
|
638
750
|
|
|
639
751
|
dispose() {
|
|
@@ -869,17 +981,6 @@ export class WorkflowDashboard {
|
|
|
869
981
|
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
870
982
|
}
|
|
871
983
|
|
|
872
|
-
/** Compose `left ... right` within `width`, truncating left when needed. */
|
|
873
|
-
private split(left: string, right: string, width: number): string {
|
|
874
|
-
const rightWidth = visibleWidth(right);
|
|
875
|
-
let text = left;
|
|
876
|
-
if (visibleWidth(text) + rightWidth + 1 > width) {
|
|
877
|
-
text = truncateToWidth(text, Math.max(0, width - rightWidth - 2), "…");
|
|
878
|
-
}
|
|
879
|
-
const pad = Math.max(1, width - visibleWidth(text) - rightWidth);
|
|
880
|
-
return text + " ".repeat(pad) + right;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
984
|
/** Bordered panel with a title in the top border, padded to exact height. */
|
|
884
985
|
private panel(
|
|
885
986
|
title: string,
|
|
@@ -887,23 +988,7 @@ export class WorkflowDashboard {
|
|
|
887
988
|
width: number,
|
|
888
989
|
height: number,
|
|
889
990
|
): 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;
|
|
991
|
+
return panelFrame(this.theme, { label: title, rows, width, height });
|
|
907
992
|
}
|
|
908
993
|
|
|
909
994
|
/** Scroll window keeping `selected` visible. */
|
|
@@ -924,25 +1009,21 @@ export class WorkflowDashboard {
|
|
|
924
1009
|
return this.keybindings.getKeys(binding).join("/") || "unbound";
|
|
925
1010
|
}
|
|
926
1011
|
|
|
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);
|
|
1012
|
+
private hintLine(hints: readonly ScreenHint[], width: number): string {
|
|
1013
|
+
return sharedHintLine(this.theme, hints, width, this.notice);
|
|
932
1014
|
}
|
|
933
1015
|
|
|
934
1016
|
private renderList(width: number, height: number): string[] {
|
|
935
1017
|
const theme = this.theme;
|
|
936
1018
|
const lines: string[] = [];
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
"
|
|
941
|
-
`${this.entries.length} run${this.entries.length === 1 ? "" : "s"}
|
|
1019
|
+
lines.push(
|
|
1020
|
+
screenTitleLine(
|
|
1021
|
+
theme,
|
|
1022
|
+
"Workflows",
|
|
1023
|
+
`${this.entries.length} run${this.entries.length === 1 ? "" : "s"}`,
|
|
1024
|
+
width,
|
|
942
1025
|
),
|
|
943
|
-
width,
|
|
944
1026
|
);
|
|
945
|
-
lines.push(header);
|
|
946
1027
|
|
|
947
1028
|
const panelHeight = height - 2;
|
|
948
1029
|
const bodyHeight = Math.max(0, panelHeight - 2);
|
|
@@ -957,7 +1038,7 @@ export class WorkflowDashboard {
|
|
|
957
1038
|
),
|
|
958
1039
|
);
|
|
959
1040
|
lines.push(
|
|
960
|
-
this.hintLine(
|
|
1041
|
+
this.hintLine([[this.keys("tui.select.cancel"), "close"]], width),
|
|
961
1042
|
);
|
|
962
1043
|
return lines;
|
|
963
1044
|
}
|
|
@@ -976,22 +1057,30 @@ export class WorkflowDashboard {
|
|
|
976
1057
|
const label = selected
|
|
977
1058
|
? theme.fg("accent", name)
|
|
978
1059
|
: theme.fg("text", name);
|
|
979
|
-
const { done, failed } = countStates(d);
|
|
1060
|
+
const { done, failed, uncertain } = countStates(d);
|
|
980
1061
|
const settled = done + failed;
|
|
981
1062
|
const right =
|
|
982
1063
|
theme.fg(
|
|
983
1064
|
"dim",
|
|
984
|
-
`${settled}/${d.agents.length} agents · ${formatElapsed(d.startedAt, d.finishedAt)} · `,
|
|
1065
|
+
`${settled}/${d.agents.length} agents${uncertain ? ` · ${uncertain} uncertain` : ""} · ${formatElapsed(d.startedAt, d.finishedAt)} · `,
|
|
985
1066
|
) +
|
|
986
1067
|
theme.fg(statusColor(d.status), statusWord(d.status)) +
|
|
987
1068
|
" ";
|
|
988
|
-
const left = ` ${marker} ${
|
|
989
|
-
return
|
|
1069
|
+
const left = ` ${marker} ${statusGlyph(d.status, theme, Date.now())} ${label} ${theme.fg("dim", d.runId)}`;
|
|
1070
|
+
return fitNavigationSides(left, right, width - 2);
|
|
990
1071
|
});
|
|
991
1072
|
lines.push(...this.panel("Runs", rows, width, panelHeight));
|
|
992
1073
|
lines.push(
|
|
993
1074
|
this.hintLine(
|
|
994
|
-
|
|
1075
|
+
[
|
|
1076
|
+
[
|
|
1077
|
+
`${this.keys("tui.select.up")}/${this.keys("tui.select.down")}`,
|
|
1078
|
+
"select",
|
|
1079
|
+
],
|
|
1080
|
+
[this.keys("tui.select.confirm"), "open"],
|
|
1081
|
+
["x", "stop"],
|
|
1082
|
+
[this.keys("tui.select.cancel"), "close"],
|
|
1083
|
+
],
|
|
995
1084
|
width,
|
|
996
1085
|
),
|
|
997
1086
|
);
|
|
@@ -1006,28 +1095,37 @@ export class WorkflowDashboard {
|
|
|
1006
1095
|
const theme = this.theme;
|
|
1007
1096
|
const lines: string[] = [];
|
|
1008
1097
|
|
|
1009
|
-
const { done, failed } = countStates(d);
|
|
1098
|
+
const { done, failed, uncertain } = countStates(d);
|
|
1010
1099
|
const settled = done + failed;
|
|
1100
|
+
// Same language as the transcript card: the glyph carries the state, the
|
|
1101
|
+
// status word only shows for terminal states.
|
|
1011
1102
|
const right =
|
|
1012
1103
|
theme.fg(
|
|
1013
1104
|
"dim",
|
|
1014
|
-
`${settled}/${d.agents.length} agents · ${formatElapsed(d.startedAt, d.finishedAt)}
|
|
1105
|
+
`${settled}/${d.agents.length} agents${uncertain ? ` · ${uncertain} uncertain` : ""} · ${formatElapsed(d.startedAt, d.finishedAt)}`,
|
|
1015
1106
|
) +
|
|
1016
|
-
|
|
1017
|
-
|
|
1107
|
+
(d.status === "running"
|
|
1108
|
+
? " "
|
|
1109
|
+
: theme.fg("dim", " · ") +
|
|
1110
|
+
theme.fg(statusColor(d.status), statusWord(d.status)) +
|
|
1111
|
+
" ");
|
|
1018
1112
|
lines.push(
|
|
1019
|
-
|
|
1020
|
-
|
|
1113
|
+
fitNavigationSides(
|
|
1114
|
+
` ${statusGlyph(d.status, theme, Date.now())} ${theme.bold(theme.fg("accent", d.name ?? d.runId))}`,
|
|
1021
1115
|
right,
|
|
1022
1116
|
width,
|
|
1023
1117
|
),
|
|
1024
1118
|
);
|
|
1025
1119
|
const totals = formatUsage(aggregateUsage(d.agents));
|
|
1026
|
-
|
|
1120
|
+
// A graph with no edges is a flat swarm — "N nodes · 0 edges" is noise.
|
|
1121
|
+
const graphSummary =
|
|
1122
|
+
d.graph && d.graph.edges.length > 0
|
|
1123
|
+
? workflowGraphSummary(d.graph)
|
|
1124
|
+
: undefined;
|
|
1027
1125
|
const subRight = [graphSummary, totals].filter(Boolean).join(" · ");
|
|
1028
1126
|
const subLeft = " " + theme.fg("muted", d.description ?? d.runId);
|
|
1029
1127
|
lines.push(
|
|
1030
|
-
|
|
1128
|
+
fitNavigationSides(
|
|
1031
1129
|
subLeft,
|
|
1032
1130
|
subRight ? theme.fg("dim", `${subRight} `) : " ",
|
|
1033
1131
|
width,
|
|
@@ -1064,7 +1162,7 @@ export class WorkflowDashboard {
|
|
|
1064
1162
|
const groupDone = group.agents.filter(
|
|
1065
1163
|
(a) => a.state !== "running",
|
|
1066
1164
|
).length;
|
|
1067
|
-
const square =
|
|
1165
|
+
const square = groupGlyph(group, theme);
|
|
1068
1166
|
const title =
|
|
1069
1167
|
selected && this.detailFocus === "phases"
|
|
1070
1168
|
? theme.fg("accent", group.title)
|
|
@@ -1073,7 +1171,11 @@ export class WorkflowDashboard {
|
|
|
1073
1171
|
group.agents.length > 0
|
|
1074
1172
|
? theme.fg("dim", `${groupDone}/${group.agents.length} `)
|
|
1075
1173
|
: theme.fg("dim", "- ");
|
|
1076
|
-
return
|
|
1174
|
+
return fitNavigationSides(
|
|
1175
|
+
` ${marker} ${square} ${title}`,
|
|
1176
|
+
counts,
|
|
1177
|
+
sidebarInner,
|
|
1178
|
+
);
|
|
1077
1179
|
});
|
|
1078
1180
|
|
|
1079
1181
|
// Right: agents in the selected phase.
|
|
@@ -1085,6 +1187,10 @@ export class WorkflowDashboard {
|
|
|
1085
1187
|
0,
|
|
1086
1188
|
...selectedGroup.agents.map((a) => a.label.length),
|
|
1087
1189
|
);
|
|
1190
|
+
const models = new Set(
|
|
1191
|
+
selectedGroup.agents.map((a) => a.model).filter(Boolean),
|
|
1192
|
+
);
|
|
1193
|
+
const mixedModels = models.size > 1;
|
|
1088
1194
|
const agentWindow = this.windowed(
|
|
1089
1195
|
selectedGroup.agents,
|
|
1090
1196
|
this.agentIndex,
|
|
@@ -1097,9 +1203,18 @@ export class WorkflowDashboard {
|
|
|
1097
1203
|
selected && this.detailFocus === "agents"
|
|
1098
1204
|
? theme.fg("accent", "❯")
|
|
1099
1205
|
: " ";
|
|
1206
|
+
// The model repeats on every row when the run is homogeneous; only
|
|
1207
|
+
// mixed fleets earn a per-row model. Context occupancy matters while
|
|
1208
|
+
// an agent runs; once settled, its cost is the elapsed on the right.
|
|
1209
|
+
const percent = contextPercent({
|
|
1210
|
+
tokens: agent.usage.contextTokens,
|
|
1211
|
+
contextWindow: agent.contextWindow,
|
|
1212
|
+
});
|
|
1100
1213
|
const stats = [
|
|
1101
|
-
agent.model,
|
|
1102
|
-
|
|
1214
|
+
mixedModels ? agent.model : undefined,
|
|
1215
|
+
agent.state === "running" && percent !== undefined
|
|
1216
|
+
? `${percent}%`
|
|
1217
|
+
: undefined,
|
|
1103
1218
|
agent.acceptance
|
|
1104
1219
|
? `acceptance:${agent.acceptance.status}`
|
|
1105
1220
|
: undefined,
|
|
@@ -1110,16 +1225,16 @@ export class WorkflowDashboard {
|
|
|
1110
1225
|
selected && this.detailFocus === "agents"
|
|
1111
1226
|
? theme.fg("accent", agent.label.padEnd(Math.min(maxLabel, 40)))
|
|
1112
1227
|
: theme.fg("text", agent.label.padEnd(Math.min(maxLabel, 40)));
|
|
1113
|
-
const left = ` ${marker} ${
|
|
1228
|
+
const left = ` ${marker} ${stateGlyph(agent.state, theme, Date.now())} ${label}${stats ? ` ${theme.fg("dim", stats)}` : ""}`;
|
|
1114
1229
|
const right = theme.fg(
|
|
1115
1230
|
"dim",
|
|
1116
1231
|
`${formatElapsed(agent.startedAt, agent.finishedAt)} `,
|
|
1117
1232
|
);
|
|
1118
|
-
agentRows.push(
|
|
1233
|
+
agentRows.push(fitNavigationSides(left, right, agentsInner));
|
|
1119
1234
|
if (agent.error) {
|
|
1120
1235
|
agentRows.push(
|
|
1121
1236
|
truncateToWidth(
|
|
1122
|
-
` ${theme.fg("error",
|
|
1237
|
+
` ${theme.fg("error", displayError(agent.error))}`,
|
|
1123
1238
|
agentsInner,
|
|
1124
1239
|
"…",
|
|
1125
1240
|
),
|
|
@@ -1170,11 +1285,32 @@ export class WorkflowDashboard {
|
|
|
1170
1285
|
);
|
|
1171
1286
|
}
|
|
1172
1287
|
|
|
1173
|
-
const
|
|
1288
|
+
const hints: ScreenHint[] =
|
|
1174
1289
|
this.detailFocus === "phases"
|
|
1175
|
-
?
|
|
1176
|
-
|
|
1177
|
-
|
|
1290
|
+
? [
|
|
1291
|
+
["j/k", "select phase"],
|
|
1292
|
+
[
|
|
1293
|
+
`l/${this.keys("tui.editor.cursorRight")}/${this.keys("tui.select.confirm")}`,
|
|
1294
|
+
"agents",
|
|
1295
|
+
],
|
|
1296
|
+
[this.keys("tui.select.cancel"), "back"],
|
|
1297
|
+
["x", "stop"],
|
|
1298
|
+
["s", "save report"],
|
|
1299
|
+
]
|
|
1300
|
+
: [
|
|
1301
|
+
["j/k", "select agent"],
|
|
1302
|
+
[
|
|
1303
|
+
`h/${this.keys("tui.editor.cursorLeft")}/${this.keys("tui.select.cancel")}`,
|
|
1304
|
+
"phases",
|
|
1305
|
+
],
|
|
1306
|
+
[
|
|
1307
|
+
`l/${this.keys("tui.editor.cursorRight")}/${this.keys("tui.select.confirm")}`,
|
|
1308
|
+
"details",
|
|
1309
|
+
],
|
|
1310
|
+
["x", "stop"],
|
|
1311
|
+
["s", "save report"],
|
|
1312
|
+
];
|
|
1313
|
+
lines.push(this.hintLine(hints, width));
|
|
1178
1314
|
return lines;
|
|
1179
1315
|
}
|
|
1180
1316
|
|
|
@@ -1191,14 +1327,30 @@ export class WorkflowDashboard {
|
|
|
1191
1327
|
}
|
|
1192
1328
|
|
|
1193
1329
|
for (const entry of agent.transcript) {
|
|
1330
|
+
// Tool calls are one-liners: the arrow shows direction, the accent name
|
|
1331
|
+
// says what ran, and the arguments collapse to a single compact line
|
|
1332
|
+
// instead of a vertical JSON block.
|
|
1333
|
+
if (entry.role === "tool") {
|
|
1334
|
+
const name = entry.name ? sanitizeLine(entry.name, 160) : "unknown";
|
|
1335
|
+
const args = compactInlineJson(entry.text);
|
|
1336
|
+
rows.push(
|
|
1337
|
+
` ${theme.fg("muted", "→")} ${theme.fg("accent", name)}${args ? theme.fg("dim", ` ${args}`) : ""}`,
|
|
1338
|
+
);
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1194
1341
|
const label = transcriptLabel(entry);
|
|
1195
1342
|
const color = transcriptColor(entry);
|
|
1343
|
+
const marker = entry.role === "toolResult" ? "←" : "●";
|
|
1196
1344
|
rows.push(
|
|
1197
|
-
` ${theme.fg(color,
|
|
1345
|
+
` ${theme.fg(color, marker)} ${theme.bold(theme.fg(color, label))}`,
|
|
1198
1346
|
);
|
|
1199
1347
|
const contentWidth = Math.max(8, width - 4);
|
|
1200
1348
|
const styled = theme.fg(
|
|
1201
|
-
entry.role === "thinking"
|
|
1349
|
+
entry.role === "thinking" || entry.role === "toolResult"
|
|
1350
|
+
? "dim"
|
|
1351
|
+
: entry.isError
|
|
1352
|
+
? "error"
|
|
1353
|
+
: "text",
|
|
1202
1354
|
sanitizeTerminalText(entry.text),
|
|
1203
1355
|
);
|
|
1204
1356
|
for (const line of wrapTextWithAnsi(styled, contentWidth)) {
|
|
@@ -1229,14 +1381,14 @@ export class WorkflowDashboard {
|
|
|
1229
1381
|
.join(" · ") + " ",
|
|
1230
1382
|
);
|
|
1231
1383
|
lines.push(
|
|
1232
|
-
|
|
1233
|
-
` ${
|
|
1384
|
+
fitNavigationSides(
|
|
1385
|
+
` ${stateGlyph(agent.state, theme, Date.now())} ${theme.bold(theme.fg("accent", agent.label))}`,
|
|
1234
1386
|
right,
|
|
1235
1387
|
width,
|
|
1236
1388
|
),
|
|
1237
1389
|
);
|
|
1238
1390
|
lines.push(
|
|
1239
|
-
|
|
1391
|
+
fitNavigationSides(
|
|
1240
1392
|
` ${theme.fg("muted", `${details.name ?? details.runId} · ${agent.phase ?? "unphased"}`)}`,
|
|
1241
1393
|
theme.fg("dim", `${agent.transcript.length} entries `),
|
|
1242
1394
|
width,
|
|
@@ -1261,7 +1413,12 @@ export class WorkflowDashboard {
|
|
|
1261
1413
|
lines.push(...this.panel(position, visible, width, panelHeight));
|
|
1262
1414
|
lines.push(
|
|
1263
1415
|
this.hintLine(
|
|
1264
|
-
|
|
1416
|
+
[
|
|
1417
|
+
["j/k", "scroll"],
|
|
1418
|
+
["ctrl-u/d", "page"],
|
|
1419
|
+
["g/G", "top/bottom"],
|
|
1420
|
+
["h/left/esc", "back"],
|
|
1421
|
+
],
|
|
1265
1422
|
width,
|
|
1266
1423
|
),
|
|
1267
1424
|
);
|
|
@@ -1269,13 +1426,38 @@ export class WorkflowDashboard {
|
|
|
1269
1426
|
}
|
|
1270
1427
|
}
|
|
1271
1428
|
|
|
1429
|
+
/**
|
|
1430
|
+
* Agent errors often arrive as an HTTP status plus a JSON body
|
|
1431
|
+
* (`429: {"message":"user rate limit exceeded …"}`); the panel row keeps the
|
|
1432
|
+
* status code and the message, dropping the braces and quotes.
|
|
1433
|
+
*/
|
|
1434
|
+
function displayError(error: string) {
|
|
1435
|
+
const clean = sanitizeLine(error, 2_000);
|
|
1436
|
+
const match = clean.match(/^(\d{3})[:\s]*\{\s*"message"\s*:\s*"([^"]+)"/);
|
|
1437
|
+
if (match) return `${match[1]}: ${match[2]}`;
|
|
1438
|
+
return clean;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1272
1441
|
function transcriptLabel(entry: TranscriptEntry): string {
|
|
1273
|
-
if (entry.role === "user") return "
|
|
1274
|
-
if (entry.role === "assistant") return "
|
|
1275
|
-
if (entry.role === "thinking") return "
|
|
1442
|
+
if (entry.role === "user") return "user";
|
|
1443
|
+
if (entry.role === "assistant") return "assistant";
|
|
1444
|
+
if (entry.role === "thinking") return "thinking";
|
|
1276
1445
|
const name = entry.name ? sanitizeLine(entry.name, 160) : "unknown";
|
|
1277
|
-
|
|
1278
|
-
|
|
1446
|
+
return name;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* Tool arguments arrive pretty-printed over many lines; the transcript shows
|
|
1451
|
+
* them inline. Non-JSON text passes through flattened.
|
|
1452
|
+
*/
|
|
1453
|
+
function compactInlineJson(text: string) {
|
|
1454
|
+
const flat = sanitizeTerminalText(text).trim();
|
|
1455
|
+
if (!flat) return "";
|
|
1456
|
+
try {
|
|
1457
|
+
return JSON.stringify(JSON.parse(flat));
|
|
1458
|
+
} catch {
|
|
1459
|
+
return flat.replace(/\s+/g, " ");
|
|
1460
|
+
}
|
|
1279
1461
|
}
|
|
1280
1462
|
|
|
1281
1463
|
function transcriptColor(
|
|
@@ -1289,17 +1471,15 @@ function transcriptColor(
|
|
|
1289
1471
|
return "muted";
|
|
1290
1472
|
}
|
|
1291
1473
|
|
|
1292
|
-
function
|
|
1293
|
-
return theme.fg(
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
function groupSquare(group: PhaseGroup, theme: Theme): string {
|
|
1297
|
-
if (group.agents.length === 0) return theme.fg("dim", SQUARE);
|
|
1474
|
+
function groupGlyph(group: PhaseGroup, theme: Theme) {
|
|
1475
|
+
if (group.agents.length === 0) return theme.fg("dim", "○");
|
|
1298
1476
|
if (group.agents.some((a) => a.state === "running"))
|
|
1299
|
-
return theme.fg("warning",
|
|
1477
|
+
return theme.fg("warning", spinnerFrame(Date.now()));
|
|
1478
|
+
if (group.agents.some((a) => a.state === "uncertain"))
|
|
1479
|
+
return theme.fg("warning", "?");
|
|
1300
1480
|
if (group.agents.some((a) => a.state === "error"))
|
|
1301
|
-
return theme.fg("error",
|
|
1302
|
-
return theme.fg("success",
|
|
1481
|
+
return theme.fg("error", "✗");
|
|
1482
|
+
return theme.fg("success", "✓");
|
|
1303
1483
|
}
|
|
1304
1484
|
|
|
1305
1485
|
/** Open the dashboard as a full-screen overlay. */
|
|
@@ -1309,10 +1489,10 @@ export async function showWorkflowDashboard(
|
|
|
1309
1489
|
initialRunId?: string,
|
|
1310
1490
|
startedSince = 0,
|
|
1311
1491
|
onAbort?: (runId: string) => boolean,
|
|
1312
|
-
)
|
|
1492
|
+
) {
|
|
1313
1493
|
await ctx.ui.custom<void>(
|
|
1314
1494
|
(tui, theme, keybindings, done) => {
|
|
1315
|
-
const dashboard
|
|
1495
|
+
const dashboard = new WorkflowDashboard(
|
|
1316
1496
|
tui,
|
|
1317
1497
|
theme,
|
|
1318
1498
|
keybindings,
|