@wichayutdew/pi-workflows 0.2.3 → 1.0.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 +66 -38
- package/dist/index.js +5903 -4782
- package/package.json +1 -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 +116 -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 +137 -135
- package/src/config/validate.ts +12 -1261
- package/src/config/validation/permissions.ts +291 -0
- package/src/config/validation/prompt.ts +19 -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 +42 -0
- package/src/engine/gate-transitions.ts +184 -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 +128 -0
- package/src/engine/state-types.ts +64 -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 +237 -2201
- 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 +174 -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 -799
- 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 +81 -0
- package/src/prompt.ts +10 -245
- 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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { truncateToWidth } from '@earendil-works/pi-tui';
|
|
2
|
+
import {
|
|
3
|
+
elapsedMs,
|
|
4
|
+
formatElapsed,
|
|
5
|
+
formatExecution,
|
|
6
|
+
formatStepName,
|
|
7
|
+
formatTimestamp,
|
|
8
|
+
inline,
|
|
9
|
+
runDisplayStatus,
|
|
10
|
+
statusBadge,
|
|
11
|
+
statusColor,
|
|
12
|
+
statusGlyph,
|
|
13
|
+
statusLabel,
|
|
14
|
+
stepTitle,
|
|
15
|
+
} from './formatting.ts';
|
|
16
|
+
import { clampRows, keyValueLines } from './layout.ts';
|
|
17
|
+
import type { WorkflowStatusSnapshot, WorkflowStatusTheme } from './types.ts';
|
|
18
|
+
|
|
19
|
+
const MAX_REASON_ROWS = 5;
|
|
20
|
+
|
|
21
|
+
/** Render the two compact lines in the workflow status header. */
|
|
22
|
+
export function renderHeaderLines(
|
|
23
|
+
theme: WorkflowStatusTheme,
|
|
24
|
+
snapshot: WorkflowStatusSnapshot,
|
|
25
|
+
width: number,
|
|
26
|
+
): Array<string> {
|
|
27
|
+
const { run } = snapshot;
|
|
28
|
+
const firstLine = [
|
|
29
|
+
statusGlyph(theme, runDisplayStatus(run), snapshot.now),
|
|
30
|
+
theme.bold(inline(run.workflowId)),
|
|
31
|
+
statusBadge(theme, run.status),
|
|
32
|
+
theme.fg('muted', '·'),
|
|
33
|
+
theme.fg(
|
|
34
|
+
'success',
|
|
35
|
+
`${run.history.length} completed attempt${run.history.length === 1 ? '' : 's'}`,
|
|
36
|
+
),
|
|
37
|
+
].join(' ');
|
|
38
|
+
|
|
39
|
+
const currentTitle = stepTitle(snapshot.workflow, run.currentStepId);
|
|
40
|
+
const visit = Math.max(1, run.visits[run.currentStepId] ?? 1);
|
|
41
|
+
const secondLine = [
|
|
42
|
+
theme.fg('muted', 'step'),
|
|
43
|
+
theme.fg('text', formatStepName(currentTitle, run.currentStepId)),
|
|
44
|
+
theme.fg(
|
|
45
|
+
'muted',
|
|
46
|
+
`· visit ${visit} · elapsed ${formatElapsed(elapsedMs(snapshot))}`,
|
|
47
|
+
),
|
|
48
|
+
].join(' ');
|
|
49
|
+
|
|
50
|
+
return [
|
|
51
|
+
truncateToWidth(firstLine, width),
|
|
52
|
+
truncateToWidth(secondLine, width),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Render the key/value summary panel for a workflow snapshot. */
|
|
57
|
+
export function renderSummaryLines(
|
|
58
|
+
theme: WorkflowStatusTheme,
|
|
59
|
+
snapshot: WorkflowStatusSnapshot,
|
|
60
|
+
width: number,
|
|
61
|
+
): Array<string> {
|
|
62
|
+
const { run, workflow } = snapshot;
|
|
63
|
+
const lines = [
|
|
64
|
+
...keyValueLines(theme, 'workflow', run.workflowId, width),
|
|
65
|
+
...keyValueLines(theme, 'run', run.runId, width),
|
|
66
|
+
...keyValueLines(
|
|
67
|
+
theme,
|
|
68
|
+
'status',
|
|
69
|
+
statusLabel(run.status),
|
|
70
|
+
width,
|
|
71
|
+
statusColor(run.status),
|
|
72
|
+
),
|
|
73
|
+
...keyValueLines(
|
|
74
|
+
theme,
|
|
75
|
+
'current',
|
|
76
|
+
formatStepName(stepTitle(workflow, run.currentStepId), run.currentStepId),
|
|
77
|
+
width,
|
|
78
|
+
),
|
|
79
|
+
...keyValueLines(
|
|
80
|
+
theme,
|
|
81
|
+
'visit',
|
|
82
|
+
String(Math.max(1, run.visits[run.currentStepId] ?? 1)),
|
|
83
|
+
width,
|
|
84
|
+
),
|
|
85
|
+
...keyValueLines(theme, 'started', formatTimestamp(run.startedAt), width),
|
|
86
|
+
...keyValueLines(
|
|
87
|
+
theme,
|
|
88
|
+
'updated',
|
|
89
|
+
`${formatTimestamp(run.updatedAt)} · ${formatElapsed(elapsedMs(snapshot))}`,
|
|
90
|
+
width,
|
|
91
|
+
),
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const execution = formatExecution(snapshot.execution);
|
|
95
|
+
if (execution) {
|
|
96
|
+
lines.push(
|
|
97
|
+
...keyValueLines(theme, 'execution', execution, width, 'accent'),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (run.pendingGate) {
|
|
101
|
+
const review = run.pendingGate.reviewId
|
|
102
|
+
? `${run.pendingGate.provider} · ${run.pendingGate.reviewId}`
|
|
103
|
+
: `${run.pendingGate.provider} · opening`;
|
|
104
|
+
lines.push(...keyValueLines(theme, 'review', review, width, 'warning'));
|
|
105
|
+
}
|
|
106
|
+
if (!workflow) {
|
|
107
|
+
lines.push(
|
|
108
|
+
...keyValueLines(
|
|
109
|
+
theme,
|
|
110
|
+
'config',
|
|
111
|
+
'workflow definition is not loaded',
|
|
112
|
+
width,
|
|
113
|
+
'warning',
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
} else if (workflow.digest !== run.workflowDigest) {
|
|
117
|
+
lines.push(
|
|
118
|
+
...keyValueLines(
|
|
119
|
+
theme,
|
|
120
|
+
'config',
|
|
121
|
+
'definition changed since this checkpoint',
|
|
122
|
+
width,
|
|
123
|
+
'warning',
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (run.pauseReason) {
|
|
128
|
+
lines.push(
|
|
129
|
+
...clampRows(
|
|
130
|
+
keyValueLines(
|
|
131
|
+
theme,
|
|
132
|
+
'reason',
|
|
133
|
+
run.pauseReason,
|
|
134
|
+
width,
|
|
135
|
+
run.status === 'aborted' ? 'error' : 'warning',
|
|
136
|
+
),
|
|
137
|
+
MAX_REASON_ROWS,
|
|
138
|
+
width,
|
|
139
|
+
theme,
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Theme, ThemeColor } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import type { TUI } from '@earendil-works/pi-tui';
|
|
3
|
+
import type { LoadedWorkflow } from '../config/types.ts';
|
|
4
|
+
import type { WorkflowRun, WorkflowRunStatus } from '../engine/state.ts';
|
|
5
|
+
|
|
6
|
+
export type WorkflowStatusExecution =
|
|
7
|
+
| {
|
|
8
|
+
readonly kind: 'main';
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
readonly kind: 'subagent';
|
|
12
|
+
readonly agent: string;
|
|
13
|
+
readonly requestId: string;
|
|
14
|
+
readonly progress: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type WorkflowStatusSnapshot = {
|
|
18
|
+
readonly run: WorkflowRun;
|
|
19
|
+
readonly workflow?: LoadedWorkflow;
|
|
20
|
+
readonly execution?: WorkflowStatusExecution;
|
|
21
|
+
readonly now: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type SnapshotProvider = () => WorkflowStatusSnapshot | undefined;
|
|
25
|
+
export type StepDisplayStatus = WorkflowRunStatus | 'failed';
|
|
26
|
+
export type StatusViewTui = Pick<TUI, 'requestRender'> & {
|
|
27
|
+
readonly terminal?: { readonly rows: number };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type PathEntry = {
|
|
31
|
+
readonly stepId: string;
|
|
32
|
+
readonly title: string;
|
|
33
|
+
readonly status: StepDisplayStatus;
|
|
34
|
+
readonly visit: number;
|
|
35
|
+
readonly outcome?: string;
|
|
36
|
+
readonly isCurrent: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type WorkflowStatusThemeColor = ThemeColor;
|
|
40
|
+
export type WorkflowStatusTheme = Pick<Theme, 'fg' | 'bg' | 'bold'>;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import {
|
|
3
|
+
Key,
|
|
4
|
+
matchesKey,
|
|
5
|
+
truncateToWidth,
|
|
6
|
+
type Component,
|
|
7
|
+
type KeyId,
|
|
8
|
+
} from '@earendil-works/pi-tui';
|
|
9
|
+
import { DEFAULT_STATUS_SHORTCUT } from '../config/types.ts';
|
|
10
|
+
import {
|
|
11
|
+
formatShortcutLabel,
|
|
12
|
+
runDisplayStatus,
|
|
13
|
+
statusGlyph,
|
|
14
|
+
statusLabel,
|
|
15
|
+
} from './formatting.ts';
|
|
16
|
+
import { padAnsi } from './layout.ts';
|
|
17
|
+
import { renderBoard, renderEmptyBoard } from './render-board.ts';
|
|
18
|
+
import type {
|
|
19
|
+
SnapshotProvider,
|
|
20
|
+
StatusViewTui,
|
|
21
|
+
WorkflowStatusTheme,
|
|
22
|
+
} from './types.ts';
|
|
23
|
+
|
|
24
|
+
const REFRESH_INTERVAL_MS = 250;
|
|
25
|
+
|
|
26
|
+
type RefreshTimer = ReturnType<typeof setInterval>;
|
|
27
|
+
|
|
28
|
+
export type WorkflowStatusViewDependencies = {
|
|
29
|
+
readonly scheduleRefresh: (
|
|
30
|
+
render: () => void,
|
|
31
|
+
intervalMilliseconds: number,
|
|
32
|
+
) => RefreshTimer;
|
|
33
|
+
readonly cancelRefresh: (timer: RefreshTimer) => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type ViewportState = {
|
|
37
|
+
readonly isClosed: boolean;
|
|
38
|
+
readonly scrollOffset: number;
|
|
39
|
+
readonly viewportRows: number;
|
|
40
|
+
readonly contentRows: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type PaginatedBoard = {
|
|
44
|
+
readonly state: ViewportState;
|
|
45
|
+
readonly lines: Array<string>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const DEFAULT_VIEW_DEPENDENCIES = {
|
|
49
|
+
scheduleRefresh: (render, intervalMilliseconds) =>
|
|
50
|
+
setInterval(render, intervalMilliseconds),
|
|
51
|
+
cancelRefresh: (timer) => {
|
|
52
|
+
clearInterval(timer);
|
|
53
|
+
},
|
|
54
|
+
} as const satisfies WorkflowStatusViewDependencies;
|
|
55
|
+
|
|
56
|
+
function initialViewportState(): ViewportState {
|
|
57
|
+
return {
|
|
58
|
+
isClosed: false,
|
|
59
|
+
scrollOffset: 0,
|
|
60
|
+
viewportRows: 0,
|
|
61
|
+
contentRows: 0,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function paginateBoard(
|
|
66
|
+
state: ViewportState,
|
|
67
|
+
lines: ReadonlyArray<string>,
|
|
68
|
+
width: number,
|
|
69
|
+
terminalRows: number | undefined,
|
|
70
|
+
statusShortcutLabel: string,
|
|
71
|
+
theme: WorkflowStatusTheme,
|
|
72
|
+
): PaginatedBoard {
|
|
73
|
+
const maximumRows =
|
|
74
|
+
terminalRows === undefined
|
|
75
|
+
? lines.length + 1
|
|
76
|
+
: Math.max(4, Math.floor(terminalRows * 0.95));
|
|
77
|
+
const contentHeight = Math.max(1, maximumRows - 1);
|
|
78
|
+
const maximumOffset = Math.max(0, lines.length - contentHeight);
|
|
79
|
+
const scrollOffset = Math.min(state.scrollOffset, maximumOffset);
|
|
80
|
+
const visible = lines.slice(scrollOffset, scrollOffset + contentHeight);
|
|
81
|
+
const first = lines.length === 0 ? 0 : scrollOffset + 1;
|
|
82
|
+
const last = Math.min(lines.length, scrollOffset + contentHeight);
|
|
83
|
+
const hint =
|
|
84
|
+
maximumOffset > 0
|
|
85
|
+
? `↑/↓ PgUp/PgDn Home/End · rows ${first}-${last}/${lines.length} · ${statusShortcutLabel} / q / Esc hide`
|
|
86
|
+
: `${statusShortcutLabel} / q / Esc hide · live refresh`;
|
|
87
|
+
return {
|
|
88
|
+
state: {
|
|
89
|
+
...state,
|
|
90
|
+
scrollOffset,
|
|
91
|
+
viewportRows: maximumRows,
|
|
92
|
+
contentRows: lines.length,
|
|
93
|
+
},
|
|
94
|
+
lines: [
|
|
95
|
+
...visible,
|
|
96
|
+
padAnsi(truncateToWidth(theme.fg('dim', hint), width, '…'), width),
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function nextScrollOffset(state: ViewportState, value: number): number {
|
|
102
|
+
const contentHeight = Math.max(1, state.viewportRows - 1);
|
|
103
|
+
const maximumOffset = Math.max(0, state.contentRows - contentHeight);
|
|
104
|
+
return Math.max(0, Math.min(value, maximumOffset));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Stateful TUI adapter around the pure workflow-status rendering functions.
|
|
109
|
+
*
|
|
110
|
+
* Timer effects are supplied explicitly so tests and alternative hosts can
|
|
111
|
+
* replace the Node.js scheduling boundary.
|
|
112
|
+
*/
|
|
113
|
+
export class WorkflowStatusView implements Component {
|
|
114
|
+
private timer: RefreshTimer | undefined;
|
|
115
|
+
private state = initialViewportState();
|
|
116
|
+
private readonly statusShortcutLabel: string;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Creates the stateful adapter around the pure status renderer.
|
|
120
|
+
*
|
|
121
|
+
* @param getSnapshot - Supplies the latest workflow status value.
|
|
122
|
+
* @param tui - TUI render-invalidation boundary.
|
|
123
|
+
* @param theme - Theme used to render the board.
|
|
124
|
+
* @param done - Callback invoked when the view closes.
|
|
125
|
+
* @param statusShortcut - Shortcut displayed in the view.
|
|
126
|
+
* @param dependencies - Injectable refresh scheduling effects.
|
|
127
|
+
*/
|
|
128
|
+
constructor(
|
|
129
|
+
private readonly getSnapshot: SnapshotProvider,
|
|
130
|
+
private readonly tui: StatusViewTui,
|
|
131
|
+
private readonly theme: WorkflowStatusTheme,
|
|
132
|
+
private readonly done: () => void,
|
|
133
|
+
private readonly statusShortcut: KeyId = DEFAULT_STATUS_SHORTCUT,
|
|
134
|
+
private readonly dependencies: WorkflowStatusViewDependencies = DEFAULT_VIEW_DEPENDENCIES,
|
|
135
|
+
) {
|
|
136
|
+
this.statusShortcutLabel = formatShortcutLabel(statusShortcut);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Start periodic live-refresh requests. */
|
|
140
|
+
start(): void {
|
|
141
|
+
this.timer = this.dependencies.scheduleRefresh(() => {
|
|
142
|
+
this.tui.requestRender();
|
|
143
|
+
}, REFRESH_INTERVAL_MS);
|
|
144
|
+
this.timer.unref();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Stop periodic live-refresh requests. */
|
|
148
|
+
dispose(): void {
|
|
149
|
+
if (this.timer) this.dependencies.cancelRefresh(this.timer);
|
|
150
|
+
this.timer = undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Satisfy the TUI component invalidation contract. */
|
|
154
|
+
invalidate(): void {}
|
|
155
|
+
|
|
156
|
+
/** Handle close and scrolling key input. */
|
|
157
|
+
handleInput(data: string): void {
|
|
158
|
+
if (
|
|
159
|
+
data === 'q' ||
|
|
160
|
+
data === 'Q' ||
|
|
161
|
+
matchesKey(data, 'escape') ||
|
|
162
|
+
matchesKey(data, 'ctrl+c') ||
|
|
163
|
+
matchesKey(data, 'ctrl+d') ||
|
|
164
|
+
matchesKey(data, this.statusShortcut)
|
|
165
|
+
) {
|
|
166
|
+
this.close();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const pageSize = Math.max(1, this.state.viewportRows - 2);
|
|
170
|
+
if (matchesKey(data, Key.down) || data === 'j') {
|
|
171
|
+
this.setScrollOffset(this.state.scrollOffset + 1);
|
|
172
|
+
} else if (matchesKey(data, Key.up) || data === 'k') {
|
|
173
|
+
this.setScrollOffset(this.state.scrollOffset - 1);
|
|
174
|
+
} else if (matchesKey(data, Key.pageDown)) {
|
|
175
|
+
this.setScrollOffset(this.state.scrollOffset + pageSize);
|
|
176
|
+
} else if (matchesKey(data, Key.pageUp)) {
|
|
177
|
+
this.setScrollOffset(this.state.scrollOffset - pageSize);
|
|
178
|
+
} else if (matchesKey(data, Key.home)) {
|
|
179
|
+
this.setScrollOffset(0);
|
|
180
|
+
} else if (matchesKey(data, Key.end)) {
|
|
181
|
+
this.setScrollOffset(Number.MAX_SAFE_INTEGER);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Render the current snapshot at the requested terminal width. */
|
|
186
|
+
render(width: number): Array<string> {
|
|
187
|
+
const viewportWidth = Math.max(1, Math.floor(width || 1));
|
|
188
|
+
const snapshot = this.getSnapshot();
|
|
189
|
+
if (viewportWidth < 12) {
|
|
190
|
+
const label = snapshot
|
|
191
|
+
? `${statusGlyph(
|
|
192
|
+
this.theme,
|
|
193
|
+
runDisplayStatus(snapshot.run),
|
|
194
|
+
snapshot.now,
|
|
195
|
+
)} ${snapshot.run.workflowId} ${statusLabel(snapshot.run.status)}`
|
|
196
|
+
: 'No workflow';
|
|
197
|
+
return [truncateToWidth(label, viewportWidth, '…', true)];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const contentWidth = viewportWidth - 2;
|
|
201
|
+
const lines = snapshot
|
|
202
|
+
? renderBoard(
|
|
203
|
+
this.theme,
|
|
204
|
+
snapshot,
|
|
205
|
+
contentWidth,
|
|
206
|
+
false,
|
|
207
|
+
this.statusShortcutLabel,
|
|
208
|
+
)
|
|
209
|
+
: renderEmptyBoard(this.theme, contentWidth);
|
|
210
|
+
const rendered = lines.map((line) =>
|
|
211
|
+
padAnsi(truncateToWidth(line, contentWidth, '…'), viewportWidth),
|
|
212
|
+
);
|
|
213
|
+
const page = paginateBoard(
|
|
214
|
+
this.state,
|
|
215
|
+
rendered,
|
|
216
|
+
viewportWidth,
|
|
217
|
+
this.tui.terminal?.rows,
|
|
218
|
+
this.statusShortcutLabel,
|
|
219
|
+
this.theme,
|
|
220
|
+
);
|
|
221
|
+
this.state = page.state;
|
|
222
|
+
return page.lines;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private setScrollOffset(value: number): void {
|
|
226
|
+
const scrollOffset = nextScrollOffset(this.state, value);
|
|
227
|
+
if (scrollOffset === this.state.scrollOffset) return;
|
|
228
|
+
this.state = { ...this.state, scrollOffset };
|
|
229
|
+
this.tui.requestRender(true);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private close(): void {
|
|
233
|
+
if (this.state.isClosed) return;
|
|
234
|
+
this.state = { ...this.state, isClosed: true };
|
|
235
|
+
this.dispose();
|
|
236
|
+
this.done();
|
|
237
|
+
this.tui.requestRender(true);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Open the live workflow status overlay using an injected snapshot provider. */
|
|
242
|
+
export async function showWorkflowStatus(
|
|
243
|
+
ctx: ExtensionContext,
|
|
244
|
+
getSnapshot: SnapshotProvider,
|
|
245
|
+
statusShortcut: KeyId = DEFAULT_STATUS_SHORTCUT,
|
|
246
|
+
dependencies: WorkflowStatusViewDependencies = DEFAULT_VIEW_DEPENDENCIES,
|
|
247
|
+
): Promise<void> {
|
|
248
|
+
await ctx.ui.custom<undefined>(
|
|
249
|
+
(tui, theme, _keybindings, done) => {
|
|
250
|
+
const view = new WorkflowStatusView(
|
|
251
|
+
getSnapshot,
|
|
252
|
+
tui,
|
|
253
|
+
theme,
|
|
254
|
+
() => {
|
|
255
|
+
done(undefined);
|
|
256
|
+
},
|
|
257
|
+
statusShortcut,
|
|
258
|
+
dependencies,
|
|
259
|
+
);
|
|
260
|
+
view.start();
|
|
261
|
+
return view;
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
overlay: true,
|
|
265
|
+
overlayOptions: {
|
|
266
|
+
anchor: 'center',
|
|
267
|
+
width: '95%',
|
|
268
|
+
maxHeight: '95%',
|
|
269
|
+
margin: 1,
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
}
|