@signalridge/pi-workflows 1.8.0 → 1.10.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/CHANGELOG.md +15 -0
- package/README.md +31 -1
- package/package.json +2 -2
- package/skills/workflow-authoring/SKILL.md +1 -1
- package/skills/workflow-authoring/references/capabilities.md +1 -1
- package/skills/workflow-authoring/references/capability-details.md +1 -1
- package/skills/workflow-patterns/SKILL.md +1 -1
- package/skills/workflow-review/SKILL.md +1 -1
- package/src/engine.ts +1088 -395
- package/src/index.ts +286 -53
- package/src/inspection.ts +544 -0
- package/src/journal.ts +33 -11
- package/src/orchestrator.ts +22 -4
- package/src/runtime.ts +387 -148
- package/src/status-actions.ts +143 -0
- package/src/ui-bus.ts +243 -0
- package/src/ui-command-center.ts +1032 -0
- package/src/ui-inspection.ts +206 -0
- package/src/ui-presentation.ts +196 -0
- package/src/ui.ts +43 -265
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkflowInspection,
|
|
3
|
+
WorkflowInspectionCall,
|
|
4
|
+
WorkflowInspectionEvent,
|
|
5
|
+
WorkflowInspectionPhase,
|
|
6
|
+
WorkflowInspectionTask,
|
|
7
|
+
} from "./inspection.js";
|
|
8
|
+
import type { ScriptRun } from "./journal.js";
|
|
9
|
+
import { sanitizeDisplayText } from "./safe-text.js";
|
|
10
|
+
import { expandedText, statusMark } from "./ui-presentation.js";
|
|
11
|
+
|
|
12
|
+
/** Only the UI's consumed projection fields, checked at the adapter boundary. */
|
|
13
|
+
export interface WorkflowPresentationSnapshot {
|
|
14
|
+
readonly runId: string;
|
|
15
|
+
readonly executionId: string;
|
|
16
|
+
readonly executionGeneration?: number;
|
|
17
|
+
readonly revision?: number;
|
|
18
|
+
readonly currentPhase?: string;
|
|
19
|
+
readonly currentPhaseKey?: string;
|
|
20
|
+
readonly observationSource: WorkflowInspection["observationSource"];
|
|
21
|
+
readonly restored: boolean;
|
|
22
|
+
readonly truncated: boolean;
|
|
23
|
+
readonly logicalCalls: number;
|
|
24
|
+
readonly replayedCalls: number;
|
|
25
|
+
readonly physicalDispatches: number;
|
|
26
|
+
readonly phases: readonly PresentationPhase[];
|
|
27
|
+
readonly calls: readonly PresentationCall[];
|
|
28
|
+
readonly tasks: readonly WorkflowInspectionTask[];
|
|
29
|
+
readonly events: readonly WorkflowInspectionEvent[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type PresentationPhase = WorkflowInspectionPhase;
|
|
33
|
+
export type PresentationCall = WorkflowInspectionCall;
|
|
34
|
+
|
|
35
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
36
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
function text(value: unknown): value is string {
|
|
39
|
+
return typeof value === "string" && value.length <= 1024;
|
|
40
|
+
}
|
|
41
|
+
function optionalText(value: unknown): value is string | undefined {
|
|
42
|
+
return value === undefined || text(value);
|
|
43
|
+
}
|
|
44
|
+
function count(value: unknown): value is number {
|
|
45
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
46
|
+
}
|
|
47
|
+
function optionalCount(value: unknown): value is number | undefined {
|
|
48
|
+
return value === undefined || count(value);
|
|
49
|
+
}
|
|
50
|
+
function status(value: unknown): boolean {
|
|
51
|
+
return (
|
|
52
|
+
typeof value === "string" &&
|
|
53
|
+
["unknown", "pending", "queued", "running", "completed", "failed", "stopped", "interrupted"].includes(value)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
function phase(value: unknown): value is PresentationPhase {
|
|
57
|
+
return (
|
|
58
|
+
record(value) &&
|
|
59
|
+
text(value.id) &&
|
|
60
|
+
text(value.frameId) &&
|
|
61
|
+
text(value.title) &&
|
|
62
|
+
optionalText(value.detail) &&
|
|
63
|
+
optionalText(value.phaseKey) &&
|
|
64
|
+
typeof value.declared === "boolean" &&
|
|
65
|
+
typeof value.entered === "boolean"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
function call(value: unknown): value is PresentationCall {
|
|
69
|
+
return (
|
|
70
|
+
record(value) &&
|
|
71
|
+
text(value.id) &&
|
|
72
|
+
text(value.frameId) &&
|
|
73
|
+
Number.isInteger(value.index) &&
|
|
74
|
+
count(value.index) &&
|
|
75
|
+
(value.kind === "agent" || value.kind === "workflow" || value.kind === "checkpoint" || value.kind === "unknown") &&
|
|
76
|
+
status(value.status) &&
|
|
77
|
+
typeof value.observed === "boolean" &&
|
|
78
|
+
typeof value.journalOnly === "boolean" &&
|
|
79
|
+
[
|
|
80
|
+
"label",
|
|
81
|
+
"phase",
|
|
82
|
+
"phaseKey",
|
|
83
|
+
"agentType",
|
|
84
|
+
"tier",
|
|
85
|
+
"graphId",
|
|
86
|
+
"taskKey",
|
|
87
|
+
"taskId",
|
|
88
|
+
"agentId",
|
|
89
|
+
"attemptId",
|
|
90
|
+
"resultPreview",
|
|
91
|
+
"errorPreview",
|
|
92
|
+
].every((key) => optionalText(value[key])) &&
|
|
93
|
+
["tokenCount", "compactionCount", "physicalDispatches", "replayedAgentCount"].every((key) =>
|
|
94
|
+
optionalCount(value[key]),
|
|
95
|
+
) &&
|
|
96
|
+
(value.physicalStatus === undefined || status(value.physicalStatus)) &&
|
|
97
|
+
(value.replayed === undefined || typeof value.replayed === "boolean")
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
function task(value: unknown): value is WorkflowInspectionTask {
|
|
101
|
+
return (
|
|
102
|
+
record(value) &&
|
|
103
|
+
["id", "frameId", "graphId", "taskId"].every((key) => text(value[key])) &&
|
|
104
|
+
optionalText(value.description) &&
|
|
105
|
+
optionalText(value.phase) &&
|
|
106
|
+
optionalText(value.errorPreview) &&
|
|
107
|
+
count(value.attempt) &&
|
|
108
|
+
(status(value.status) || value.status === "skipped")
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
function event(value: unknown): value is WorkflowInspectionEvent {
|
|
112
|
+
return (
|
|
113
|
+
record(value) &&
|
|
114
|
+
text(value.id) &&
|
|
115
|
+
text(value.frameId) &&
|
|
116
|
+
typeof value.type === "string" &&
|
|
117
|
+
["phase", "task", "workflow", "quality", "control-attempt", "log"].includes(value.type) &&
|
|
118
|
+
["stage", "title", "graphId", "taskKey", "helperId", "helper", "outcome", "message"].every((key) =>
|
|
119
|
+
optionalText(value[key]),
|
|
120
|
+
) &&
|
|
121
|
+
optionalCount(value.attempt) &&
|
|
122
|
+
(value.accepted === undefined || typeof value.accepted === "boolean")
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
function array<T extends { readonly id: string }>(
|
|
126
|
+
value: unknown,
|
|
127
|
+
maximum: number,
|
|
128
|
+
validate: (item: unknown) => item is T,
|
|
129
|
+
): value is T[] {
|
|
130
|
+
if (!Array.isArray(value) || value.length > maximum || !value.every(validate)) return false;
|
|
131
|
+
const items: readonly T[] = value;
|
|
132
|
+
return items.every((item) => item.id.length > 0) && new Set(items.map((item) => item.id)).size === items.length;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Never interpret unknown adapter data as control authority or mutate its strings. */
|
|
136
|
+
export function parsePresentation(value: unknown, runId: string): WorkflowPresentationSnapshot | undefined {
|
|
137
|
+
if (
|
|
138
|
+
!record(value) ||
|
|
139
|
+
value.runId !== runId ||
|
|
140
|
+
!text(value.executionId) ||
|
|
141
|
+
!optionalCount(value.executionGeneration) ||
|
|
142
|
+
!optionalCount(value.revision) ||
|
|
143
|
+
!optionalText(value.currentPhase) ||
|
|
144
|
+
!optionalText(value.currentPhaseKey) ||
|
|
145
|
+
(value.observationSource !== "execution" && value.observationSource !== "journal-only") ||
|
|
146
|
+
typeof value.restored !== "boolean" ||
|
|
147
|
+
typeof value.truncated !== "boolean" ||
|
|
148
|
+
!count(value.logicalCalls) ||
|
|
149
|
+
!count(value.replayedCalls) ||
|
|
150
|
+
!count(value.physicalDispatches) ||
|
|
151
|
+
!array(value.phases, 256, phase) ||
|
|
152
|
+
!array(value.calls, 2048, call) ||
|
|
153
|
+
!array(value.tasks, 256, task) ||
|
|
154
|
+
!array(value.events, 128, event)
|
|
155
|
+
)
|
|
156
|
+
return undefined;
|
|
157
|
+
return {
|
|
158
|
+
runId,
|
|
159
|
+
executionId: value.executionId,
|
|
160
|
+
executionGeneration: value.executionGeneration,
|
|
161
|
+
revision: value.revision,
|
|
162
|
+
currentPhase: value.currentPhase,
|
|
163
|
+
currentPhaseKey: value.currentPhaseKey,
|
|
164
|
+
observationSource: value.observationSource,
|
|
165
|
+
restored: value.restored,
|
|
166
|
+
truncated: value.truncated,
|
|
167
|
+
logicalCalls: value.logicalCalls,
|
|
168
|
+
replayedCalls: value.replayedCalls,
|
|
169
|
+
physicalDispatches: value.physicalDispatches,
|
|
170
|
+
phases: value.phases,
|
|
171
|
+
calls: value.calls,
|
|
172
|
+
tasks: value.tasks,
|
|
173
|
+
events: value.events,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function inspectionCallLabel(call: WorkflowInspectionCall): string {
|
|
178
|
+
return (
|
|
179
|
+
`${statusMark(call.status)} Call ${call.index} · ${sanitizeDisplayText(call.label ?? call.kind, 160)} · ${call.status}` +
|
|
180
|
+
`${call.phase ? ` · phase ${sanitizeDisplayText(call.phase, 120)}` : ""}` +
|
|
181
|
+
`${call.tier ? ` · tier ${sanitizeDisplayText(call.tier, 80)}` : ""}` +
|
|
182
|
+
` · compactions ${call.compactionCount ?? 0}` +
|
|
183
|
+
`${call.replayed ? " · replayed" : ""}` +
|
|
184
|
+
`${call.journalOnly ? " · journal-only" : ""}` +
|
|
185
|
+
`${call.errorPreview ? ` · error: ${sanitizeDisplayText(call.errorPreview, 120)}` : call.resultPreview ? ` · result: ${sanitizeDisplayText(call.resultPreview, 120)}` : ""}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function inspectionCallDetail(call: WorkflowInspectionCall, run: ScriptRun): string {
|
|
190
|
+
return [
|
|
191
|
+
inspectionCallLabel(call),
|
|
192
|
+
`Frame: ${sanitizeDisplayText(call.frameId, 200)}${call.frameId === run.runId ? " (root)" : " (nested)"}`,
|
|
193
|
+
...(call.agentType ? [`Agent type: ${sanitizeDisplayText(call.agentType, 200)}`] : []),
|
|
194
|
+
...(call.agentId ? [`Agent: ${sanitizeDisplayText(call.agentId, 128)}`] : []),
|
|
195
|
+
...(call.tokenCount === undefined ? [] : [`Tokens: ${call.tokenCount}`]),
|
|
196
|
+
...(call.physicalStatus
|
|
197
|
+
? [`Physical status: ${call.physicalStatus} · dispatches ${call.physicalDispatches ?? 0}`]
|
|
198
|
+
: []),
|
|
199
|
+
...(call.taskId ? [`Task: ${sanitizeDisplayText(call.taskId, 200)}`] : []),
|
|
200
|
+
call.observed
|
|
201
|
+
? "Live callback facts (stored preview, not a conversation)."
|
|
202
|
+
: "Journal-only call; historical label/phase is unknown.",
|
|
203
|
+
...(call.errorPreview ? [`Error: ${expandedText(call.errorPreview)}`] : []),
|
|
204
|
+
...(call.resultPreview ? [expandedText(call.resultPreview)] : []),
|
|
205
|
+
].join("\n");
|
|
206
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { WorkflowEngine } from "./engine.js";
|
|
2
|
+
import type { ScriptRun } from "./journal.js";
|
|
3
|
+
import { sanitizeDisplayText, sanitizeTerminalText } from "./safe-text.js";
|
|
4
|
+
|
|
5
|
+
export type RunStatus = ScriptRun["status"];
|
|
6
|
+
export type WorkflowAction = "refresh" | "pause" | "resume" | "stop" | "back";
|
|
7
|
+
export interface WorkflowActionItem {
|
|
8
|
+
value: WorkflowAction;
|
|
9
|
+
label: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isTerminal(status: RunStatus): boolean {
|
|
13
|
+
return status === "completed" || status === "failed" || status === "stopped";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** ASCII marks occupy exactly one cell, including on ambiguous-width terminals. */
|
|
17
|
+
export function statusMark(status: string): string {
|
|
18
|
+
if (status === "completed") return "+";
|
|
19
|
+
if (status === "failed") return "!";
|
|
20
|
+
if (status === "stopped" || status === "stopping") return "x";
|
|
21
|
+
if (status === "paused" || status === "pausing" || status === "interrupted") return "=";
|
|
22
|
+
if (status === "pending" || status === "queued") return ".";
|
|
23
|
+
if (status === "skipped") return "-";
|
|
24
|
+
return status === "running" ? ">" : "?";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function elapsed(run: ScriptRun): string {
|
|
28
|
+
const ms = (isTerminal(run.status) ? run.updatedAt : Date.now()) - run.startedAt;
|
|
29
|
+
const seconds = Number.isFinite(ms) ? Math.floor(Math.max(0, ms) / 1_000) : 0;
|
|
30
|
+
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function callIndexes(run: ScriptRun): string[] {
|
|
34
|
+
return [...new Set([...Object.keys(run.callStatus), ...Object.keys(run.callResults)])]
|
|
35
|
+
.filter((id) => /^(?:0|[1-9]\d*)$/u.test(id))
|
|
36
|
+
.sort((a, b) => Number(a) - Number(b));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function tokenCount(run: ScriptRun): number | undefined {
|
|
40
|
+
const values = Object.values(run.callResults)
|
|
41
|
+
.map((result) => result.tokenCount)
|
|
42
|
+
.filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
43
|
+
return values.length ? values.reduce((total, value) => total + value, 0) : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resultText(value: unknown): string {
|
|
47
|
+
if (typeof value === "string") return value;
|
|
48
|
+
try {
|
|
49
|
+
return JSON.stringify(value, null, 2) ?? "(no stored result)";
|
|
50
|
+
} catch {
|
|
51
|
+
return "(result cannot be displayed as JSON)";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Expanded views retain line breaks, but encode controls before any theme styling. */
|
|
56
|
+
export function expandedText(value: string): string {
|
|
57
|
+
const limit = 16_000;
|
|
58
|
+
const copy = value.slice(0, limit);
|
|
59
|
+
return (
|
|
60
|
+
copy
|
|
61
|
+
.split("\n")
|
|
62
|
+
.map((line) => sanitizeTerminalText(line))
|
|
63
|
+
.join("\n") + (copy.length < value.length ? "\n[Display limited to 16000 characters]" : "")
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function formatWorkflowRunLabel(run: ScriptRun): string {
|
|
68
|
+
const phases = run.meta.phases?.length ? ` · phases ${run.meta.phases.length}` : "";
|
|
69
|
+
return `${sanitizeDisplayText(run.meta.name, 200)} · ${run.status}${phases} · ${callIndexes(run).length} calls · ${elapsed(run)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function runSummary(run: ScriptRun): string {
|
|
73
|
+
const statuses = Object.values(run.callStatus);
|
|
74
|
+
const running = statuses.filter((status) => status === "running").length;
|
|
75
|
+
const completed = statuses.filter((status) => status === "completed").length;
|
|
76
|
+
const failed = statuses.filter((status) => status === "failed").length;
|
|
77
|
+
const tokens = tokenCount(run);
|
|
78
|
+
return `${statusMark(run.status)} ${run.status} · ${elapsed(run)} · ${tokens ?? "?"} tokens · ${callIndexes(run).length} calls (${running} live / ${completed} done / ${failed} failed)`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function callLabel(run: ScriptRun, id: string): string {
|
|
82
|
+
const result = run.callResults[id];
|
|
83
|
+
const status = run.callStatus[id] ?? result?.status ?? "unknown";
|
|
84
|
+
const tier = run.callTiers?.[id]?.tier ?? result?.tier;
|
|
85
|
+
const compactions = run.compactions[id] ?? result?.compactionCount ?? 0;
|
|
86
|
+
const preview = result?.error
|
|
87
|
+
? `error: ${sanitizeDisplayText(result.error, 120)}`
|
|
88
|
+
: result?.result !== undefined
|
|
89
|
+
? `result: ${sanitizeDisplayText(resultText(result.result), 120)}`
|
|
90
|
+
: "";
|
|
91
|
+
return `${statusMark(status)} Call ${id} · ${status}${tier ? ` · tier ${sanitizeDisplayText(tier, 80)}` : ""} · compactions ${compactions}${preview ? ` · ${preview}` : ""}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function callDetail(run: ScriptRun, id: string): string {
|
|
95
|
+
const result = run.callResults[id];
|
|
96
|
+
const agentId = run.agentIds[id] ?? result?.agentId;
|
|
97
|
+
return [
|
|
98
|
+
callLabel(run, id),
|
|
99
|
+
...(agentId ? [`Agent: ${sanitizeDisplayText(agentId, 128)}`] : []),
|
|
100
|
+
...(result?.tokenCount === undefined ? [] : [`Tokens: ${result.tokenCount}`]),
|
|
101
|
+
"Stored call result preview (not a live conversation):",
|
|
102
|
+
...(result?.error ? [`Error: ${expandedText(result.error)}`] : []),
|
|
103
|
+
result?.result === undefined ? "No stored result yet." : expandedText(resultText(result.result)),
|
|
104
|
+
].join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function formatWorkflowDetail(engine: WorkflowEngine, runId: string): string {
|
|
108
|
+
const state = engine.getState(runId);
|
|
109
|
+
if (!state) return "Workflow run is no longer available.";
|
|
110
|
+
const run = state.run;
|
|
111
|
+
return [
|
|
112
|
+
`${sanitizeDisplayText(run.meta.name, 200)} [${run.status}]`,
|
|
113
|
+
`run: ${sanitizeDisplayText(run.runId, 128)}`,
|
|
114
|
+
`elapsed: ${elapsed(run)}`,
|
|
115
|
+
runSummary(run),
|
|
116
|
+
...(run.meta.phases ?? []).map(
|
|
117
|
+
(phase) => `Phase (declared, progress unknown): ${sanitizeDisplayText(phase.title, 200)}`,
|
|
118
|
+
),
|
|
119
|
+
...callIndexes(run).map((id) => callDetail(run, id)),
|
|
120
|
+
"Final script result:",
|
|
121
|
+
run.finalResult === undefined ? "No stored final result yet." : expandedText(resultText(run.finalResult)),
|
|
122
|
+
...(run.error ? [`Error: ${expandedText(run.error)}`] : []),
|
|
123
|
+
].join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** State-only availability; callers must additionally fence nonResumable/terminalIntent. */
|
|
127
|
+
export function workflowActionsFor(status: RunStatus): readonly WorkflowActionItem[] {
|
|
128
|
+
const back: WorkflowActionItem = { value: "back", label: "Back" };
|
|
129
|
+
const stop: WorkflowActionItem = { value: "stop", label: "Stop (non-resumable)" };
|
|
130
|
+
switch (status) {
|
|
131
|
+
case "pending":
|
|
132
|
+
case "pausing":
|
|
133
|
+
return [stop, back];
|
|
134
|
+
case "running":
|
|
135
|
+
return [{ value: "pause", label: "Pause" }, stop, back];
|
|
136
|
+
case "paused":
|
|
137
|
+
case "interrupted":
|
|
138
|
+
return [{ value: "resume", label: "Resume" }, stop, back];
|
|
139
|
+
case "stopping":
|
|
140
|
+
case "completed":
|
|
141
|
+
case "failed":
|
|
142
|
+
case "stopped":
|
|
143
|
+
return [back];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function actionsForRun(run: ScriptRun): readonly WorkflowActionItem[] {
|
|
148
|
+
return workflowActionsFor(run.status).filter(
|
|
149
|
+
(item) => item.value !== "resume" || (!run.nonResumable && !run.terminalIntent),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function liveWidgetLines(
|
|
154
|
+
runs: readonly ScriptRun[],
|
|
155
|
+
mode: "compact" | "detailed" = "compact",
|
|
156
|
+
maxAgentsShown = 6,
|
|
157
|
+
maxRows = 12,
|
|
158
|
+
): string[] {
|
|
159
|
+
const active = runs.filter((run) => !isTerminal(run.status));
|
|
160
|
+
if (!active.length) return [];
|
|
161
|
+
const max = Number.isFinite(maxAgentsShown) ? Math.max(1, Math.min(32, Math.floor(maxAgentsShown))) : 6;
|
|
162
|
+
const rows = Number.isFinite(maxRows) ? Math.max(1, Math.min(32, Math.floor(maxRows))) : 12;
|
|
163
|
+
const lines = [`Workflows · ${active.length} active · /workflows`];
|
|
164
|
+
if (rows === 1) return lines;
|
|
165
|
+
let shown = Math.min(max, active.length, rows - 1);
|
|
166
|
+
// When runs are hidden, reserve a dedicated overflow row before call details.
|
|
167
|
+
if (active.length > shown && shown + 2 > rows) shown = Math.max(0, shown - 1);
|
|
168
|
+
const visible = active.slice(0, shown);
|
|
169
|
+
const hiddenRuns = active.length - visible.length;
|
|
170
|
+
for (const [index, run] of visible.entries()) {
|
|
171
|
+
const runLine = lines.length;
|
|
172
|
+
lines.push(
|
|
173
|
+
` ${statusMark(run.status)} ${run.status} · ${elapsed(run)} · ${callIndexes(run).length} calls · ${sanitizeDisplayText(run.meta.name, 80)}`,
|
|
174
|
+
);
|
|
175
|
+
if (mode !== "detailed") continue;
|
|
176
|
+
const calls = callIndexes(run);
|
|
177
|
+
const live = calls.filter((id) => run.callStatus[id] === "running");
|
|
178
|
+
const liveIds = new Set(live);
|
|
179
|
+
const prioritized = [...live, ...calls.filter((id) => !liveIds.has(id))];
|
|
180
|
+
const reserved = visible.length - index - 1 + (hiddenRuns > 0 ? 1 : 0);
|
|
181
|
+
const callRows = Math.max(0, Math.min(max, rows - lines.length - reserved));
|
|
182
|
+
const shownCalls = prioritized.slice(0, callRows);
|
|
183
|
+
for (const id of shownCalls) lines.push(` ${id}: ${run.callStatus[id] ?? run.callResults[id]?.status}`);
|
|
184
|
+
const hiddenCalls = calls.length - shownCalls.length;
|
|
185
|
+
if (hiddenCalls > 0) {
|
|
186
|
+
const last = shownCalls.length ? lines.length - 1 : runLine;
|
|
187
|
+
lines[last] += ` · ${hiddenCalls} more calls`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (hiddenRuns > 0) lines.push(` ${hiddenRuns} more workflows · /workflows`);
|
|
191
|
+
return lines;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function snapshotRunLine(run: ScriptRun): string {
|
|
195
|
+
return formatWorkflowRunLabel(run);
|
|
196
|
+
}
|