@wichayutdew/pi-workflows 0.3.0 → 1.0.1
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 +55 -39
- package/dist/index.js +5771 -4794
- package/examples/mr-comments.workflow.yaml +2 -0
- package/package.json +1 -1
- package/schemas/workflow.schema.json +10 -1
- package/src/command-names.ts +7 -1
- package/src/commands.ts +126 -52
- package/src/config/catalog.ts +162 -0
- package/src/config/ceiling.ts +120 -116
- package/src/config/command-conflicts.ts +4 -4
- package/src/config/diagnostics.ts +28 -0
- package/src/config/load-settings.ts +37 -0
- package/src/config/load-types.ts +53 -0
- package/src/config/load-workflows.ts +135 -0
- package/src/config/load.ts +43 -311
- package/src/config/types.ts +150 -137
- package/src/config/validate.ts +12 -1261
- package/src/config/validation/permissions.ts +312 -0
- package/src/config/validation/prompt.ts +21 -0
- package/src/config/validation/settings.ts +122 -0
- package/src/config/validation/shared.ts +129 -0
- package/src/config/validation/shortcut.ts +118 -0
- package/src/config/validation/step.ts +228 -0
- package/src/config/validation/subagent.ts +288 -0
- package/src/config/validation/workflow.ts +156 -0
- package/src/config/yaml.ts +29 -0
- package/src/digest.ts +38 -6
- package/src/engine/checkpoint.ts +13 -9
- package/src/engine/create-run.ts +43 -0
- package/src/engine/gate-transitions.ts +187 -0
- package/src/engine/reconciliation-history.ts +88 -0
- package/src/engine/resume.ts +19 -7
- package/src/engine/run-advance.ts +122 -0
- package/src/engine/run-lifecycle.ts +124 -0
- package/src/engine/run-reconciliation.ts +116 -0
- package/src/engine/run-validation.ts +129 -0
- package/src/engine/state-types.ts +66 -0
- package/src/engine/state.ts +10 -193
- package/src/engine/transition-helpers.ts +28 -0
- package/src/engine/transition-types.ts +8 -0
- package/src/engine/transitions.ts +17 -471
- package/src/harness/action-context.ts +181 -0
- package/src/harness/catalog.ts +45 -0
- package/src/harness/context-idle.ts +21 -0
- package/src/harness/core-actions.ts +299 -0
- package/src/harness/delegation-control-actions.ts +250 -0
- package/src/harness/delegation-failure.ts +215 -0
- package/src/harness/delegation-plan.ts +219 -0
- package/src/harness/delegation-recovery-validation.ts +159 -0
- package/src/harness/delegation-response-actions.ts +295 -0
- package/src/harness/delegation-retry-policy.ts +118 -0
- package/src/harness/dependencies.ts +165 -0
- package/src/harness/gate-submission-action.ts +154 -0
- package/src/harness/lifecycle-actions.ts +154 -0
- package/src/harness/pause-actions.ts +120 -0
- package/src/harness/plannotator-result-actions.ts +123 -0
- package/src/harness/prompt-gate-actions.ts +270 -0
- package/src/harness/resume-action.ts +261 -0
- package/src/harness/start-actions.ts +183 -0
- package/src/harness/status-actions.ts +191 -0
- package/src/harness/step-execution-actions.ts +283 -0
- package/src/harness/types.ts +76 -0
- package/src/harness.ts +236 -2303
- package/src/index.ts +73 -11
- package/src/integrations/plannotator-requests.ts +127 -0
- package/src/integrations/plannotator-responses.ts +141 -0
- package/src/integrations/plannotator-types.ts +44 -0
- package/src/integrations/plannotator.ts +16 -235
- package/src/integrations/prompt-gate.ts +17 -10
- package/src/integrations/subagents/child-policy-envelope.ts +119 -0
- package/src/integrations/subagents/child-policy-paths.ts +66 -0
- package/src/integrations/subagents/child-policy-sections.ts +186 -0
- package/src/integrations/subagents/child-policy-types.ts +32 -0
- package/src/integrations/subagents/child-policy-validation.ts +163 -0
- package/src/integrations/subagents/child-runtime-completion.ts +56 -0
- package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
- package/src/integrations/subagents/child-runtime-files.ts +156 -0
- package/src/integrations/subagents/child-runtime-policy.ts +83 -0
- package/src/integrations/subagents/child-runtime-types.ts +30 -0
- package/src/integrations/subagents/child-runtime.ts +140 -301
- package/src/integrations/subagents/client-delegation.ts +181 -0
- package/src/integrations/subagents/client-messages.ts +66 -0
- package/src/integrations/subagents/client-types.ts +36 -0
- package/src/integrations/subagents/client.ts +127 -219
- package/src/integrations/subagents/delegated-result.ts +31 -0
- package/src/integrations/subagents/diagnostic-format.ts +40 -0
- package/src/integrations/subagents/diagnostic-text.ts +114 -0
- package/src/integrations/subagents/diagnostic-types.ts +77 -0
- package/src/integrations/subagents/diagnostics.ts +24 -977
- package/src/integrations/subagents/failure-correlation.ts +235 -0
- package/src/integrations/subagents/failure-transcript.ts +192 -0
- package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
- package/src/integrations/subagents/protocol-events.ts +27 -0
- package/src/integrations/subagents/protocol.ts +30 -375
- package/src/integrations/subagents/replay-audit.ts +147 -0
- package/src/integrations/subagents/replay-safety.ts +82 -0
- package/src/integrations/subagents/session-diagnostics.ts +258 -0
- package/src/policy/approved-command-extraction.ts +71 -0
- package/src/policy/approved-commands.ts +15 -432
- package/src/policy/bash-authorization.ts +86 -0
- package/src/policy/bash-read-only.ts +161 -0
- package/src/policy/bash-types.ts +20 -0
- package/src/policy/bash.ts +4 -346
- package/src/policy/completion-batch.ts +21 -16
- package/src/policy/immutable-input.ts +5 -2
- package/src/policy/mcp-authorization.ts +71 -0
- package/src/policy/restricted-command.ts +141 -0
- package/src/policy/restricted-git.ts +30 -0
- package/src/policy/reviewed-artifact.ts +97 -0
- package/src/policy/reviewed-command-safety.ts +150 -0
- package/src/policy/reviewed-command-shape.ts +49 -0
- package/src/policy/reviewed-repository-contract.ts +138 -0
- package/src/policy/reviewed-repository-cwd.ts +102 -0
- package/src/policy/tool-call-authorization.ts +58 -0
- package/src/policy/tool-selection.ts +72 -0
- package/src/policy/tool-types.ts +14 -0
- package/src/policy/tools.ts +11 -150
- package/src/preflight.ts +96 -61
- package/src/prompt/main-workflow-notice.ts +41 -0
- package/src/prompt/retry-task.ts +83 -0
- package/src/prompt/step-contract.ts +101 -0
- package/src/prompt/step-sections.ts +75 -0
- package/src/prompt/step-task.ts +156 -0
- package/src/prompt/template.ts +83 -0
- package/src/prompt.ts +10 -254
- package/src/runtime/completion-tool.ts +3 -0
- package/src/runtime/main-step-completion.ts +78 -0
- package/src/runtime/main-step-lifecycle.ts +37 -0
- package/src/runtime/main-step-policy.ts +84 -0
- package/src/runtime/main-step-runtime-types.ts +64 -0
- package/src/runtime/main-step-runtime.ts +129 -200
- package/src/runtime/main-step-state.ts +85 -0
- package/src/runtime/serial-task-queue.ts +48 -13
- package/src/runtime/step-result.ts +48 -27
- package/src/workflow-list.ts +15 -6
- package/src/workflow-status/format-status.ts +52 -0
- package/src/workflow-status/formatting.ts +178 -0
- package/src/workflow-status/layout.ts +109 -0
- package/src/workflow-status/render-board.ts +101 -0
- package/src/workflow-status/render-path.ts +123 -0
- package/src/workflow-status/render-summary.ts +144 -0
- package/src/workflow-status/types.ts +40 -0
- package/src/workflow-status/view.ts +273 -0
- package/src/workflow-status.ts +20 -820
package/src/workflow-status.ts
CHANGED
|
@@ -1,820 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
StepHistoryEntry,
|
|
22
|
-
WorkflowRun,
|
|
23
|
-
WorkflowRunStatus,
|
|
24
|
-
} from './engine/state.ts';
|
|
25
|
-
|
|
26
|
-
const REFRESH_INTERVAL_MS = 250;
|
|
27
|
-
const WORKING_ICON_FRAME_MS = 250;
|
|
28
|
-
const WORKING_ICON_FRAMES = ['◐', '◓', '◑', '◒'] as const;
|
|
29
|
-
const WIDE_LAYOUT_MIN_COLUMNS = 92;
|
|
30
|
-
const MAX_PATH_ROWS = 16;
|
|
31
|
-
const MAX_REASON_ROWS = 5;
|
|
32
|
-
|
|
33
|
-
export type WorkflowStatusExecution =
|
|
34
|
-
| {
|
|
35
|
-
kind: 'main';
|
|
36
|
-
}
|
|
37
|
-
| {
|
|
38
|
-
kind: 'subagent';
|
|
39
|
-
agent: string;
|
|
40
|
-
requestId: string;
|
|
41
|
-
progress: string;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export interface WorkflowStatusSnapshot {
|
|
45
|
-
run: WorkflowRun;
|
|
46
|
-
workflow?: LoadedWorkflow;
|
|
47
|
-
execution?: WorkflowStatusExecution;
|
|
48
|
-
now: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
type SnapshotProvider = () => WorkflowStatusSnapshot | undefined;
|
|
52
|
-
type StepDisplayStatus = WorkflowRunStatus | 'completed' | 'failed';
|
|
53
|
-
type StatusViewTui = Pick<TUI, 'requestRender'> & {
|
|
54
|
-
terminal?: { rows: number };
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
interface PathEntry {
|
|
58
|
-
stepId: string;
|
|
59
|
-
title: string;
|
|
60
|
-
status: StepDisplayStatus;
|
|
61
|
-
visit: number;
|
|
62
|
-
outcome?: string;
|
|
63
|
-
current: boolean;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function formatWorkflowStatusText(
|
|
67
|
-
snapshot: WorkflowStatusSnapshot,
|
|
68
|
-
): string {
|
|
69
|
-
const { run } = snapshot;
|
|
70
|
-
const lines = [
|
|
71
|
-
`Workflow: ${run.workflowId}`,
|
|
72
|
-
`Run: ${run.runId}`,
|
|
73
|
-
`Status: ${run.status}`,
|
|
74
|
-
`Step: ${run.currentStepId}`,
|
|
75
|
-
`Completed steps: ${run.history.length}`,
|
|
76
|
-
];
|
|
77
|
-
if (run.pendingGate?.reviewId) {
|
|
78
|
-
lines.push(`Review: ${run.pendingGate.reviewId}`);
|
|
79
|
-
}
|
|
80
|
-
if (snapshot.execution?.kind === 'subagent') {
|
|
81
|
-
lines.push(
|
|
82
|
-
`Subagent: ${snapshot.execution.agent} (${snapshot.execution.requestId})`,
|
|
83
|
-
`Progress: ${snapshot.execution.progress}`,
|
|
84
|
-
);
|
|
85
|
-
} else if (snapshot.execution?.kind === 'main') {
|
|
86
|
-
lines.push('Execution: main agent');
|
|
87
|
-
}
|
|
88
|
-
if (run.pauseReason) lines.push(`Reason: ${run.pauseReason}`);
|
|
89
|
-
return lines.join('\n');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Format the full workflow status board. */
|
|
93
|
-
export function formatWorkflowStatusBoard(
|
|
94
|
-
snapshot: WorkflowStatusSnapshot,
|
|
95
|
-
width = 88,
|
|
96
|
-
theme: Theme = unstyledTheme,
|
|
97
|
-
): string[] {
|
|
98
|
-
return renderBoard(
|
|
99
|
-
theme,
|
|
100
|
-
snapshot,
|
|
101
|
-
Math.max(8, Math.floor(width)),
|
|
102
|
-
false,
|
|
103
|
-
formatShortcutLabel(DEFAULT_STATUS_SHORTCUT),
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const unstyledTheme = {
|
|
108
|
-
fg: (_color: string, value: string) => value,
|
|
109
|
-
bg: (_color: string, value: string) => value,
|
|
110
|
-
bold: (value: string) => value,
|
|
111
|
-
} as unknown as Theme;
|
|
112
|
-
|
|
113
|
-
const SHORTCUT_LABELS: Readonly<Record<string, string>> = {
|
|
114
|
-
ctrl: 'Ctrl',
|
|
115
|
-
shift: 'Shift',
|
|
116
|
-
alt: 'Alt',
|
|
117
|
-
super: 'Super',
|
|
118
|
-
escape: 'Esc',
|
|
119
|
-
esc: 'Esc',
|
|
120
|
-
enter: 'Enter',
|
|
121
|
-
return: 'Enter',
|
|
122
|
-
tab: 'Tab',
|
|
123
|
-
space: 'Space',
|
|
124
|
-
backspace: 'Backspace',
|
|
125
|
-
delete: 'Del',
|
|
126
|
-
insert: 'Ins',
|
|
127
|
-
clear: 'Clear',
|
|
128
|
-
home: 'Home',
|
|
129
|
-
end: 'End',
|
|
130
|
-
pageUp: 'PgUp',
|
|
131
|
-
pageDown: 'PgDn',
|
|
132
|
-
up: 'Up',
|
|
133
|
-
down: 'Down',
|
|
134
|
-
left: 'Left',
|
|
135
|
-
right: 'Right',
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
export function formatShortcutLabel(shortcut: KeyId): string {
|
|
139
|
-
return shortcut
|
|
140
|
-
.split('+')
|
|
141
|
-
.map((part) => {
|
|
142
|
-
const label = SHORTCUT_LABELS[part];
|
|
143
|
-
if (label) return label;
|
|
144
|
-
if (/^f\d+$/.test(part)) return part.toUpperCase();
|
|
145
|
-
return part.length === 1 ? part.toUpperCase() : part;
|
|
146
|
-
})
|
|
147
|
-
.join('+');
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export async function showWorkflowStatus(
|
|
151
|
-
ctx: ExtensionContext,
|
|
152
|
-
getSnapshot: SnapshotProvider,
|
|
153
|
-
statusShortcut: KeyId = DEFAULT_STATUS_SHORTCUT,
|
|
154
|
-
): Promise<void> {
|
|
155
|
-
await ctx.ui.custom<void>(
|
|
156
|
-
(tui, theme, _keybindings, done) => {
|
|
157
|
-
const view = new WorkflowStatusView(
|
|
158
|
-
getSnapshot,
|
|
159
|
-
tui,
|
|
160
|
-
theme,
|
|
161
|
-
done,
|
|
162
|
-
statusShortcut,
|
|
163
|
-
);
|
|
164
|
-
view.start();
|
|
165
|
-
return view;
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
overlay: true,
|
|
169
|
-
overlayOptions: {
|
|
170
|
-
anchor: 'center',
|
|
171
|
-
width: '95%',
|
|
172
|
-
maxHeight: '95%',
|
|
173
|
-
margin: 1,
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export class WorkflowStatusView implements Component {
|
|
180
|
-
private timer: ReturnType<typeof setInterval> | undefined;
|
|
181
|
-
private closed = false;
|
|
182
|
-
private scrollOffset = 0;
|
|
183
|
-
private viewportRows = 0;
|
|
184
|
-
private contentRows = 0;
|
|
185
|
-
private readonly statusShortcutLabel: string;
|
|
186
|
-
|
|
187
|
-
constructor(
|
|
188
|
-
private readonly getSnapshot: SnapshotProvider,
|
|
189
|
-
private readonly tui: StatusViewTui,
|
|
190
|
-
private readonly theme: Theme,
|
|
191
|
-
private readonly done: () => void,
|
|
192
|
-
private readonly statusShortcut: KeyId = DEFAULT_STATUS_SHORTCUT,
|
|
193
|
-
) {
|
|
194
|
-
this.statusShortcutLabel = formatShortcutLabel(statusShortcut);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
start(): void {
|
|
198
|
-
this.timer = setInterval(
|
|
199
|
-
() => this.tui.requestRender(),
|
|
200
|
-
REFRESH_INTERVAL_MS,
|
|
201
|
-
);
|
|
202
|
-
this.timer.unref?.();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
dispose(): void {
|
|
206
|
-
if (this.timer) clearInterval(this.timer);
|
|
207
|
-
this.timer = undefined;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
invalidate(): void {}
|
|
211
|
-
|
|
212
|
-
handleInput(data: string): void {
|
|
213
|
-
if (
|
|
214
|
-
data === 'q' ||
|
|
215
|
-
data === 'Q' ||
|
|
216
|
-
matchesKey(data, 'escape') ||
|
|
217
|
-
matchesKey(data, 'ctrl+c') ||
|
|
218
|
-
matchesKey(data, 'ctrl+d') ||
|
|
219
|
-
matchesKey(data, this.statusShortcut)
|
|
220
|
-
) {
|
|
221
|
-
this.close();
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
const pageSize = Math.max(1, this.viewportRows - 2);
|
|
225
|
-
if (matchesKey(data, Key.down) || data === 'j') {
|
|
226
|
-
this.scrollBy(1);
|
|
227
|
-
} else if (matchesKey(data, Key.up) || data === 'k') {
|
|
228
|
-
this.scrollBy(-1);
|
|
229
|
-
} else if (matchesKey(data, Key.pageDown)) {
|
|
230
|
-
this.scrollBy(pageSize);
|
|
231
|
-
} else if (matchesKey(data, Key.pageUp)) {
|
|
232
|
-
this.scrollBy(-pageSize);
|
|
233
|
-
} else if (matchesKey(data, Key.home)) {
|
|
234
|
-
this.setScrollOffset(0);
|
|
235
|
-
} else if (matchesKey(data, Key.end)) {
|
|
236
|
-
this.setScrollOffset(Number.MAX_SAFE_INTEGER);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
render(width: number): string[] {
|
|
241
|
-
const viewportWidth = Math.max(1, Math.floor(width || 1));
|
|
242
|
-
const snapshot = this.getSnapshot();
|
|
243
|
-
if (viewportWidth < 12) {
|
|
244
|
-
const label = snapshot
|
|
245
|
-
? `${statusGlyph(this.theme, runDisplayStatus(snapshot.run), snapshot.now)} ${snapshot.run.workflowId} ${statusLabel(snapshot.run.status)}`
|
|
246
|
-
: 'No workflow';
|
|
247
|
-
return [truncateToWidth(label, viewportWidth, '…', true)];
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const contentWidth = viewportWidth - 2;
|
|
251
|
-
const lines = snapshot
|
|
252
|
-
? renderBoard(
|
|
253
|
-
this.theme,
|
|
254
|
-
snapshot,
|
|
255
|
-
contentWidth,
|
|
256
|
-
false,
|
|
257
|
-
this.statusShortcutLabel,
|
|
258
|
-
)
|
|
259
|
-
: renderEmptyBoard(this.theme, contentWidth);
|
|
260
|
-
const rendered = lines.map((line) =>
|
|
261
|
-
padAnsi(truncateToWidth(line, contentWidth, '…'), viewportWidth),
|
|
262
|
-
);
|
|
263
|
-
return this.paginate(rendered, viewportWidth);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private paginate(lines: string[], width: number): string[] {
|
|
267
|
-
this.contentRows = lines.length;
|
|
268
|
-
const terminalRows = this.tui.terminal?.rows;
|
|
269
|
-
const maximumRows =
|
|
270
|
-
terminalRows === undefined
|
|
271
|
-
? lines.length + 1
|
|
272
|
-
: Math.max(4, Math.floor(terminalRows * 0.95));
|
|
273
|
-
this.viewportRows = maximumRows;
|
|
274
|
-
const contentHeight = Math.max(1, maximumRows - 1);
|
|
275
|
-
const maximumOffset = Math.max(0, lines.length - contentHeight);
|
|
276
|
-
this.scrollOffset = Math.min(this.scrollOffset, maximumOffset);
|
|
277
|
-
const visible = lines.slice(
|
|
278
|
-
this.scrollOffset,
|
|
279
|
-
this.scrollOffset + contentHeight,
|
|
280
|
-
);
|
|
281
|
-
const first = lines.length === 0 ? 0 : this.scrollOffset + 1;
|
|
282
|
-
const last = Math.min(lines.length, this.scrollOffset + contentHeight);
|
|
283
|
-
const hint =
|
|
284
|
-
maximumOffset > 0
|
|
285
|
-
? `↑/↓ PgUp/PgDn Home/End · rows ${first}-${last}/${lines.length} · ${this.statusShortcutLabel} / q / Esc hide`
|
|
286
|
-
: `${this.statusShortcutLabel} / q / Esc hide · live refresh`;
|
|
287
|
-
return [
|
|
288
|
-
...visible,
|
|
289
|
-
padAnsi(truncateToWidth(this.theme.fg('dim', hint), width, '…'), width),
|
|
290
|
-
];
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
private scrollBy(delta: number): void {
|
|
294
|
-
this.setScrollOffset(this.scrollOffset + delta);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
private setScrollOffset(value: number): void {
|
|
298
|
-
const contentHeight = Math.max(1, this.viewportRows - 1);
|
|
299
|
-
const maximumOffset = Math.max(0, this.contentRows - contentHeight);
|
|
300
|
-
const next = Math.max(0, Math.min(value, maximumOffset));
|
|
301
|
-
if (next === this.scrollOffset) return;
|
|
302
|
-
this.scrollOffset = next;
|
|
303
|
-
this.tui.requestRender(true);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
private close(): void {
|
|
307
|
-
if (this.closed) return;
|
|
308
|
-
this.closed = true;
|
|
309
|
-
this.dispose();
|
|
310
|
-
this.done();
|
|
311
|
-
this.tui.requestRender(true);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function renderBoard(
|
|
316
|
-
theme: Theme,
|
|
317
|
-
snapshot: WorkflowStatusSnapshot,
|
|
318
|
-
width: number,
|
|
319
|
-
showCloseHint = true,
|
|
320
|
-
statusShortcutLabel = formatShortcutLabel(DEFAULT_STATUS_SHORTCUT),
|
|
321
|
-
): string[] {
|
|
322
|
-
const header = boxed(
|
|
323
|
-
theme,
|
|
324
|
-
'✦ Workflow Status',
|
|
325
|
-
width,
|
|
326
|
-
renderHeaderLines(theme, snapshot, width - 4),
|
|
327
|
-
'borderAccent',
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
let body: string[];
|
|
331
|
-
if (width >= WIDE_LAYOUT_MIN_COLUMNS) {
|
|
332
|
-
const gap = 2;
|
|
333
|
-
const summaryWidth = Math.min(42, Math.max(36, Math.floor(width * 0.36)));
|
|
334
|
-
const pathWidth = width - summaryWidth - gap;
|
|
335
|
-
const summary = boxed(
|
|
336
|
-
theme,
|
|
337
|
-
'Run Summary',
|
|
338
|
-
summaryWidth,
|
|
339
|
-
renderSummaryLines(theme, snapshot, summaryWidth - 4),
|
|
340
|
-
);
|
|
341
|
-
const path = boxed(
|
|
342
|
-
theme,
|
|
343
|
-
'Execution Path',
|
|
344
|
-
pathWidth,
|
|
345
|
-
renderPathLines(theme, snapshot, pathWidth - 4),
|
|
346
|
-
'borderAccent',
|
|
347
|
-
);
|
|
348
|
-
body = joinPanels(summary, summaryWidth, path, pathWidth, gap);
|
|
349
|
-
} else {
|
|
350
|
-
body = [
|
|
351
|
-
...boxed(
|
|
352
|
-
theme,
|
|
353
|
-
'Run Summary',
|
|
354
|
-
width,
|
|
355
|
-
renderSummaryLines(theme, snapshot, width - 4),
|
|
356
|
-
),
|
|
357
|
-
'',
|
|
358
|
-
...boxed(
|
|
359
|
-
theme,
|
|
360
|
-
'Execution Path',
|
|
361
|
-
width,
|
|
362
|
-
renderPathLines(theme, snapshot, width - 4),
|
|
363
|
-
'borderAccent',
|
|
364
|
-
),
|
|
365
|
-
];
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const lines = [...header, '', ...body];
|
|
369
|
-
if (showCloseHint) {
|
|
370
|
-
lines.push(
|
|
371
|
-
'',
|
|
372
|
-
theme.fg('dim', `${statusShortcutLabel} / q / Esc hide · live refresh`),
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
return lines;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function renderEmptyBoard(theme: Theme, width: number): string[] {
|
|
379
|
-
return [
|
|
380
|
-
...boxed(
|
|
381
|
-
theme,
|
|
382
|
-
'✦ Workflow Status',
|
|
383
|
-
width,
|
|
384
|
-
[theme.fg('muted', 'No workflow checkpoint in this session')],
|
|
385
|
-
'borderAccent',
|
|
386
|
-
),
|
|
387
|
-
'',
|
|
388
|
-
theme.fg('dim', 'q / Esc close'),
|
|
389
|
-
];
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function renderHeaderLines(
|
|
393
|
-
theme: Theme,
|
|
394
|
-
snapshot: WorkflowStatusSnapshot,
|
|
395
|
-
width: number,
|
|
396
|
-
): string[] {
|
|
397
|
-
const { run } = snapshot;
|
|
398
|
-
const workflowName = inline(run.workflowId);
|
|
399
|
-
const status = statusBadge(theme, run.status);
|
|
400
|
-
const completed = theme.fg(
|
|
401
|
-
'success',
|
|
402
|
-
`${run.history.length} completed attempt${run.history.length === 1 ? '' : 's'}`,
|
|
403
|
-
);
|
|
404
|
-
const firstLine = [
|
|
405
|
-
statusGlyph(theme, runDisplayStatus(run), snapshot.now),
|
|
406
|
-
theme.bold(workflowName),
|
|
407
|
-
status,
|
|
408
|
-
theme.fg('muted', '·'),
|
|
409
|
-
completed,
|
|
410
|
-
].join(' ');
|
|
411
|
-
|
|
412
|
-
const currentTitle = stepTitle(snapshot.workflow, run.currentStepId);
|
|
413
|
-
const visit = Math.max(1, run.visits[run.currentStepId] ?? 1);
|
|
414
|
-
const elapsed = formatElapsed(elapsedMs(snapshot));
|
|
415
|
-
const secondLine = [
|
|
416
|
-
theme.fg('muted', 'step'),
|
|
417
|
-
theme.fg('text', formatStepName(currentTitle, run.currentStepId)),
|
|
418
|
-
theme.fg('muted', `· visit ${visit} · elapsed ${elapsed}`),
|
|
419
|
-
].join(' ');
|
|
420
|
-
|
|
421
|
-
return [
|
|
422
|
-
truncateToWidth(firstLine, width),
|
|
423
|
-
truncateToWidth(secondLine, width),
|
|
424
|
-
];
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function renderSummaryLines(
|
|
428
|
-
theme: Theme,
|
|
429
|
-
snapshot: WorkflowStatusSnapshot,
|
|
430
|
-
width: number,
|
|
431
|
-
): string[] {
|
|
432
|
-
const { run, workflow } = snapshot;
|
|
433
|
-
const lines = [
|
|
434
|
-
...keyValueLines(theme, 'workflow', run.workflowId, width),
|
|
435
|
-
...keyValueLines(theme, 'run', run.runId, width),
|
|
436
|
-
...keyValueLines(
|
|
437
|
-
theme,
|
|
438
|
-
'status',
|
|
439
|
-
statusLabel(run.status),
|
|
440
|
-
width,
|
|
441
|
-
statusColor(run.status),
|
|
442
|
-
),
|
|
443
|
-
...keyValueLines(
|
|
444
|
-
theme,
|
|
445
|
-
'current',
|
|
446
|
-
formatStepName(stepTitle(workflow, run.currentStepId), run.currentStepId),
|
|
447
|
-
width,
|
|
448
|
-
),
|
|
449
|
-
...keyValueLines(
|
|
450
|
-
theme,
|
|
451
|
-
'visit',
|
|
452
|
-
String(Math.max(1, run.visits[run.currentStepId] ?? 1)),
|
|
453
|
-
width,
|
|
454
|
-
),
|
|
455
|
-
...keyValueLines(theme, 'started', formatTimestamp(run.startedAt), width),
|
|
456
|
-
...keyValueLines(
|
|
457
|
-
theme,
|
|
458
|
-
'updated',
|
|
459
|
-
`${formatTimestamp(run.updatedAt)} · ${formatElapsed(elapsedMs(snapshot))}`,
|
|
460
|
-
width,
|
|
461
|
-
),
|
|
462
|
-
];
|
|
463
|
-
|
|
464
|
-
const execution = formatExecution(snapshot.execution);
|
|
465
|
-
if (execution) {
|
|
466
|
-
lines.push(
|
|
467
|
-
...keyValueLines(theme, 'execution', execution, width, 'accent'),
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
if (run.pendingGate) {
|
|
471
|
-
const review = run.pendingGate.reviewId
|
|
472
|
-
? `${run.pendingGate.provider} · ${run.pendingGate.reviewId}`
|
|
473
|
-
: `${run.pendingGate.provider} · opening`;
|
|
474
|
-
lines.push(...keyValueLines(theme, 'review', review, width, 'warning'));
|
|
475
|
-
}
|
|
476
|
-
if (!workflow) {
|
|
477
|
-
lines.push(
|
|
478
|
-
...keyValueLines(
|
|
479
|
-
theme,
|
|
480
|
-
'config',
|
|
481
|
-
'workflow definition is not loaded',
|
|
482
|
-
width,
|
|
483
|
-
'warning',
|
|
484
|
-
),
|
|
485
|
-
);
|
|
486
|
-
} else if (workflow.digest !== run.workflowDigest) {
|
|
487
|
-
lines.push(
|
|
488
|
-
...keyValueLines(
|
|
489
|
-
theme,
|
|
490
|
-
'config',
|
|
491
|
-
'definition changed since this checkpoint',
|
|
492
|
-
width,
|
|
493
|
-
'warning',
|
|
494
|
-
),
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
if (run.pauseReason) {
|
|
498
|
-
lines.push(
|
|
499
|
-
...clampRows(
|
|
500
|
-
keyValueLines(
|
|
501
|
-
theme,
|
|
502
|
-
'reason',
|
|
503
|
-
run.pauseReason,
|
|
504
|
-
width,
|
|
505
|
-
run.status === 'aborted' ? 'error' : 'warning',
|
|
506
|
-
),
|
|
507
|
-
MAX_REASON_ROWS,
|
|
508
|
-
width,
|
|
509
|
-
theme,
|
|
510
|
-
),
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
return lines;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function clampRows(
|
|
517
|
-
lines: string[],
|
|
518
|
-
maximum: number,
|
|
519
|
-
width: number,
|
|
520
|
-
theme: Theme,
|
|
521
|
-
): string[] {
|
|
522
|
-
if (lines.length <= maximum) return lines;
|
|
523
|
-
const visible = lines.slice(0, maximum);
|
|
524
|
-
const last = visible.at(-1) ?? '';
|
|
525
|
-
visible[maximum - 1] =
|
|
526
|
-
truncateToWidth(last, Math.max(1, width - 1), '', true) +
|
|
527
|
-
theme.fg('dim', '…');
|
|
528
|
-
return visible;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function renderPathLines(
|
|
532
|
-
theme: Theme,
|
|
533
|
-
snapshot: WorkflowStatusSnapshot,
|
|
534
|
-
width: number,
|
|
535
|
-
): string[] {
|
|
536
|
-
const entries = buildPathEntries(snapshot);
|
|
537
|
-
if (entries.length === 0) {
|
|
538
|
-
return [theme.fg('muted', 'No step attempts recorded')];
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const hidden = Math.max(0, entries.length - MAX_PATH_ROWS);
|
|
542
|
-
const visible = entries.slice(hidden);
|
|
543
|
-
const lines =
|
|
544
|
-
hidden > 0
|
|
545
|
-
? [
|
|
546
|
-
theme.fg(
|
|
547
|
-
'dim',
|
|
548
|
-
`… ${hidden} earlier attempt${hidden === 1 ? '' : 's'}`,
|
|
549
|
-
),
|
|
550
|
-
]
|
|
551
|
-
: [];
|
|
552
|
-
for (const entry of visible) {
|
|
553
|
-
const visit =
|
|
554
|
-
entry.visit > 1 ? theme.fg('dim', ` · visit ${entry.visit}`) : '';
|
|
555
|
-
const left = `${statusGlyph(theme, entry.status, snapshot.now)} ${theme.fg(
|
|
556
|
-
entry.current ? 'text' : 'muted',
|
|
557
|
-
entry.title,
|
|
558
|
-
)}${visit}`;
|
|
559
|
-
const right = entry.outcome
|
|
560
|
-
? `${statusLabel(entry.status)} · ${inline(entry.outcome)}`
|
|
561
|
-
: statusLabel(entry.status);
|
|
562
|
-
const row = joinColumns(
|
|
563
|
-
left,
|
|
564
|
-
theme.fg(statusColor(entry.status), right),
|
|
565
|
-
width,
|
|
566
|
-
Math.max(12, Math.floor(width * 0.58)),
|
|
567
|
-
);
|
|
568
|
-
lines.push(
|
|
569
|
-
entry.current
|
|
570
|
-
? theme.bg('selectedBg', padAnsi(row, width))
|
|
571
|
-
: truncateToWidth(row, width),
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
return lines;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function buildPathEntries(snapshot: WorkflowStatusSnapshot): PathEntry[] {
|
|
578
|
-
const { run, workflow } = snapshot;
|
|
579
|
-
const visits = new Map<string, number>();
|
|
580
|
-
const entries = run.history.map((entry) => {
|
|
581
|
-
const visit = (visits.get(entry.stepId) ?? 0) + 1;
|
|
582
|
-
visits.set(entry.stepId, visit);
|
|
583
|
-
return historyPathEntry(workflow, entry, visit);
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
if (run.status !== 'completed') {
|
|
587
|
-
entries.push({
|
|
588
|
-
stepId: run.currentStepId,
|
|
589
|
-
title: stepTitle(workflow, run.currentStepId),
|
|
590
|
-
status: runDisplayStatus(run),
|
|
591
|
-
visit: Math.max(
|
|
592
|
-
visits.get(run.currentStepId) ?? 0,
|
|
593
|
-
run.visits[run.currentStepId] ?? 1,
|
|
594
|
-
),
|
|
595
|
-
current: true,
|
|
596
|
-
});
|
|
597
|
-
} else if (
|
|
598
|
-
entries.length === 0 ||
|
|
599
|
-
entries.at(-1)?.stepId !== run.currentStepId
|
|
600
|
-
) {
|
|
601
|
-
entries.push({
|
|
602
|
-
stepId: run.currentStepId,
|
|
603
|
-
title: stepTitle(workflow, run.currentStepId),
|
|
604
|
-
status: 'completed',
|
|
605
|
-
visit: Math.max(1, run.visits[run.currentStepId] ?? 1),
|
|
606
|
-
current: true,
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
return entries;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function historyPathEntry(
|
|
613
|
-
workflow: LoadedWorkflow | undefined,
|
|
614
|
-
entry: StepHistoryEntry,
|
|
615
|
-
visit: number,
|
|
616
|
-
): PathEntry {
|
|
617
|
-
return {
|
|
618
|
-
stepId: entry.stepId,
|
|
619
|
-
title: stepTitle(workflow, entry.stepId),
|
|
620
|
-
status: 'completed',
|
|
621
|
-
visit,
|
|
622
|
-
outcome: entry.outcome,
|
|
623
|
-
current: false,
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function keyValueLines(
|
|
628
|
-
theme: Theme,
|
|
629
|
-
label: string,
|
|
630
|
-
rawValue: string,
|
|
631
|
-
width: number,
|
|
632
|
-
valueColor: ThemeColor = 'text',
|
|
633
|
-
): string[] {
|
|
634
|
-
const safeWidth = Math.max(1, width);
|
|
635
|
-
const labelWidth = Math.min(10, Math.max(7, label.length + 1));
|
|
636
|
-
const valueWidth = Math.max(1, safeWidth - labelWidth);
|
|
637
|
-
const value = theme.fg(valueColor, inline(rawValue));
|
|
638
|
-
const wrapped = wrapTextWithAnsi(value, valueWidth);
|
|
639
|
-
const prefix = theme.fg('muted', label.padEnd(labelWidth));
|
|
640
|
-
return (wrapped.length > 0 ? wrapped : ['']).map((line, index) =>
|
|
641
|
-
index === 0 ? `${prefix}${line}` : `${' '.repeat(labelWidth)}${line}`,
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function boxed(
|
|
646
|
-
theme: Theme,
|
|
647
|
-
title: string,
|
|
648
|
-
width: number,
|
|
649
|
-
content: string[],
|
|
650
|
-
color: ThemeColor = 'borderMuted',
|
|
651
|
-
): string[] {
|
|
652
|
-
const safeWidth = Math.max(8, Math.floor(width));
|
|
653
|
-
const bodyWidth = Math.max(1, safeWidth - 4);
|
|
654
|
-
const topLabel = `╭─ ${title} `;
|
|
655
|
-
const top = `${topLabel}${'─'.repeat(
|
|
656
|
-
Math.max(0, safeWidth - visibleWidth(topLabel) - 1),
|
|
657
|
-
)}╮`;
|
|
658
|
-
const bottom = `╰${'─'.repeat(Math.max(0, safeWidth - 2))}╯`;
|
|
659
|
-
const body = content.length > 0 ? content : [''];
|
|
660
|
-
return [
|
|
661
|
-
theme.fg(color, top),
|
|
662
|
-
...body.map(
|
|
663
|
-
(line) =>
|
|
664
|
-
`${theme.fg(color, '│')} ${padAnsi(
|
|
665
|
-
truncateToWidth(line, bodyWidth),
|
|
666
|
-
bodyWidth,
|
|
667
|
-
)} ${theme.fg(color, '│')}`,
|
|
668
|
-
),
|
|
669
|
-
theme.fg(color, bottom),
|
|
670
|
-
];
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function joinPanels(
|
|
674
|
-
left: string[],
|
|
675
|
-
leftWidth: number,
|
|
676
|
-
right: string[],
|
|
677
|
-
rightWidth: number,
|
|
678
|
-
gap: number,
|
|
679
|
-
): string[] {
|
|
680
|
-
const height = Math.max(left.length, right.length);
|
|
681
|
-
return Array.from({ length: height }, (_, index) => {
|
|
682
|
-
const leftLine = padAnsi(left[index] ?? '', leftWidth);
|
|
683
|
-
const rightLine = padAnsi(right[index] ?? '', rightWidth);
|
|
684
|
-
return `${leftLine}${' '.repeat(gap)}${rightLine}`;
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function joinColumns(
|
|
689
|
-
left: string,
|
|
690
|
-
right: string,
|
|
691
|
-
width: number,
|
|
692
|
-
leftWidth: number,
|
|
693
|
-
): string {
|
|
694
|
-
const safeLeftWidth = Math.max(1, Math.min(leftWidth, width - 2));
|
|
695
|
-
const rightWidth = Math.max(1, width - safeLeftWidth - 1);
|
|
696
|
-
return `${padAnsi(
|
|
697
|
-
truncateToWidth(left, safeLeftWidth),
|
|
698
|
-
safeLeftWidth,
|
|
699
|
-
)} ${truncateToWidth(right, rightWidth)}`;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function padAnsi(value: string, width: number): string {
|
|
703
|
-
const visible = visibleWidth(value);
|
|
704
|
-
if (visible >= width) return value;
|
|
705
|
-
return `${value}${' '.repeat(width - visible)}`;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function statusGlyph(
|
|
709
|
-
theme: Theme,
|
|
710
|
-
status: StepDisplayStatus,
|
|
711
|
-
now = Date.now(),
|
|
712
|
-
): string {
|
|
713
|
-
if (status === 'completed') return theme.fg('success', '✓');
|
|
714
|
-
if (status === 'running') {
|
|
715
|
-
return theme.fg('accent', workingIcon(now));
|
|
716
|
-
}
|
|
717
|
-
if (status === 'paused' || status === 'awaiting-gate') {
|
|
718
|
-
return theme.fg('warning', '◆');
|
|
719
|
-
}
|
|
720
|
-
if (status === 'failed' || status === 'aborted') {
|
|
721
|
-
return theme.fg('error', '✕');
|
|
722
|
-
}
|
|
723
|
-
return theme.fg('dim', '•');
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
function statusColor(status: StepDisplayStatus): ThemeColor {
|
|
727
|
-
if (status === 'completed') return 'success';
|
|
728
|
-
if (status === 'running') return 'accent';
|
|
729
|
-
if (status === 'paused' || status === 'awaiting-gate') return 'warning';
|
|
730
|
-
if (status === 'failed' || status === 'aborted') return 'error';
|
|
731
|
-
return 'dim';
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function statusLabel(status: StepDisplayStatus): string {
|
|
735
|
-
return status === 'awaiting-gate'
|
|
736
|
-
? 'AWAITING REVIEW'
|
|
737
|
-
: status.toUpperCase().replaceAll('-', ' ');
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function statusBadge(theme: Theme, status: StepDisplayStatus): string {
|
|
741
|
-
return theme.fg(statusColor(status), theme.bold(`[${statusLabel(status)}]`));
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function runDisplayStatus(run: WorkflowRun): StepDisplayStatus {
|
|
745
|
-
return run.status === 'paused' && run.failedStepId === run.currentStepId
|
|
746
|
-
? 'failed'
|
|
747
|
-
: run.status;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
export function workflowStatusIcon(run: WorkflowRun, now = Date.now()): string {
|
|
751
|
-
const status = runDisplayStatus(run);
|
|
752
|
-
if (status === 'completed') return '✓';
|
|
753
|
-
if (status === 'running') return workingIcon(now);
|
|
754
|
-
if (status === 'failed' || status === 'aborted') return '✕';
|
|
755
|
-
if (status === 'paused' || status === 'awaiting-gate') return '◆';
|
|
756
|
-
return '•';
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function workingIcon(now: number): string {
|
|
760
|
-
return WORKING_ICON_FRAMES[
|
|
761
|
-
Math.floor(now / WORKING_ICON_FRAME_MS) % WORKING_ICON_FRAMES.length
|
|
762
|
-
]!;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
function stepTitle(
|
|
766
|
-
workflow: LoadedWorkflow | undefined,
|
|
767
|
-
stepId: string,
|
|
768
|
-
): string {
|
|
769
|
-
return inline(workflow?.definition.steps[stepId]?.title ?? stepId);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function formatStepName(title: string, stepId: string): string {
|
|
773
|
-
const safeStepId = inline(stepId);
|
|
774
|
-
return title === safeStepId ? title : `${title} (${safeStepId})`;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function formatExecution(
|
|
778
|
-
execution: WorkflowStatusExecution | undefined,
|
|
779
|
-
): string | undefined {
|
|
780
|
-
if (!execution) return undefined;
|
|
781
|
-
if (execution.kind === 'main') return 'main agent';
|
|
782
|
-
return `${execution.agent} · ${execution.progress} · ${execution.requestId}`;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function inline(value: string): string {
|
|
786
|
-
return value.replace(/\s+/g, ' ').trim();
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
function elapsedMs(snapshot: WorkflowStatusSnapshot): number {
|
|
790
|
-
const { run } = snapshot;
|
|
791
|
-
const end =
|
|
792
|
-
run.status === 'running' || run.status === 'awaiting-gate'
|
|
793
|
-
? snapshot.now
|
|
794
|
-
: run.updatedAt;
|
|
795
|
-
return Math.max(0, end - run.startedAt);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
function formatElapsed(milliseconds: number): string {
|
|
799
|
-
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1_000));
|
|
800
|
-
const days = Math.floor(totalSeconds / 86_400);
|
|
801
|
-
const hours = Math.floor((totalSeconds % 86_400) / 3_600);
|
|
802
|
-
const minutes = Math.floor((totalSeconds % 3_600) / 60);
|
|
803
|
-
const seconds = totalSeconds % 60;
|
|
804
|
-
if (days > 0) return `${days}d ${hours}h`;
|
|
805
|
-
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
806
|
-
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
807
|
-
return `${seconds}s`;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
function formatTimestamp(milliseconds: number): string {
|
|
811
|
-
const value = new Date(milliseconds);
|
|
812
|
-
if (!Number.isFinite(value.getTime())) return 'unknown';
|
|
813
|
-
const year = value.getFullYear();
|
|
814
|
-
const month = String(value.getMonth() + 1).padStart(2, '0');
|
|
815
|
-
const day = String(value.getDate()).padStart(2, '0');
|
|
816
|
-
const hours = String(value.getHours()).padStart(2, '0');
|
|
817
|
-
const minutes = String(value.getMinutes()).padStart(2, '0');
|
|
818
|
-
const seconds = String(value.getSeconds()).padStart(2, '0');
|
|
819
|
-
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
820
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
formatWorkflowStatusBoard,
|
|
3
|
+
formatWorkflowStatusText,
|
|
4
|
+
} from './workflow-status/format-status.ts';
|
|
5
|
+
export {
|
|
6
|
+
formatShortcutLabel,
|
|
7
|
+
workflowStatusIcon,
|
|
8
|
+
} from './workflow-status/formatting.ts';
|
|
9
|
+
export {
|
|
10
|
+
showWorkflowStatus,
|
|
11
|
+
WorkflowStatusView,
|
|
12
|
+
type WorkflowStatusViewDependencies,
|
|
13
|
+
} from './workflow-status/view.ts';
|
|
14
|
+
export type {
|
|
15
|
+
SnapshotProvider,
|
|
16
|
+
StatusViewTui,
|
|
17
|
+
WorkflowStatusExecution,
|
|
18
|
+
WorkflowStatusSnapshot,
|
|
19
|
+
WorkflowStatusTheme,
|
|
20
|
+
} from './workflow-status/types.ts';
|