@mingchuno/agent-workflows 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/adapters/agents.js +9 -0
- package/dist/src/cli.js +14 -1
- package/dist/src/config.d.ts +10 -0
- package/dist/src/config.js +9 -11
- package/dist/src/domain.d.ts +6 -0
- package/dist/src/invocation.d.ts +2 -4
- package/dist/src/invocation.js +204 -107
- package/dist/src/operations.d.ts +1 -2
- package/dist/src/operations.js +18 -33
- package/dist/src/recovery.d.ts +20 -2
- package/dist/src/recovery.js +49 -1
- package/dist/src/runner.d.ts +6 -2
- package/dist/src/runner.js +81 -83
- package/dist/src/runtime/process.d.ts +1 -0
- package/dist/src/runtime/process.js +6 -2
- package/dist/src/store.d.ts +4 -2
- package/dist/src/store.js +69 -51
- package/dist/src/tui/data.d.ts +1 -1
- package/dist/src/tui/dialogs.js +1 -0
- package/dist/src/tui/monitor-navigation.d.ts +76 -0
- package/dist/src/tui/monitor-navigation.js +187 -0
- package/dist/src/tui/monitor.js +70 -183
- package/dist/src/tui/projection.d.ts +22 -0
- package/dist/src/tui/projection.js +49 -0
- package/dist/src/validation-selection.d.ts +6 -0
- package/dist/src/validation-selection.js +29 -0
- package/dist/src/workspace.js +3 -2
- package/docs/api.md +1 -1
- package/docs/configuration.md +33 -0
- package/docs/operations.md +4 -3
- package/package.json +1 -1
- package/dist/src/tui/actions.d.ts +0 -16
- package/dist/src/tui/actions.js +0 -23
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Key } from "ink";
|
|
2
|
+
import type { RunRecord } from "../domain.js";
|
|
3
|
+
import type { EventRecord, InvocationRecord, ProjectState } from "../store.js";
|
|
4
|
+
import type { MonitorAction } from "./data.js";
|
|
5
|
+
import type { LogSource } from "./log.js";
|
|
6
|
+
type Focus = "runs" | "summary" | "sessions";
|
|
7
|
+
type RunAction = "stop" | "retry" | "retry-refresh" | "recover";
|
|
8
|
+
type Selection = {
|
|
9
|
+
projectId?: string;
|
|
10
|
+
runId?: string;
|
|
11
|
+
};
|
|
12
|
+
type Modal = {
|
|
13
|
+
type: "help";
|
|
14
|
+
} | {
|
|
15
|
+
type: "confirmation";
|
|
16
|
+
kind: RunAction;
|
|
17
|
+
runId: string;
|
|
18
|
+
title: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: "logs";
|
|
21
|
+
sources: LogSource[];
|
|
22
|
+
initial: number;
|
|
23
|
+
};
|
|
24
|
+
export interface MonitorNavigation {
|
|
25
|
+
selection: Selection;
|
|
26
|
+
/** Actual displayed run, which may fall back from a requested selection. */
|
|
27
|
+
displayedRunId?: string;
|
|
28
|
+
screen: "dashboard" | "details";
|
|
29
|
+
focus: Focus;
|
|
30
|
+
sessionId?: string;
|
|
31
|
+
stepSequence?: number;
|
|
32
|
+
offset: number;
|
|
33
|
+
modal?: Modal;
|
|
34
|
+
}
|
|
35
|
+
export declare const initialNavigation: MonitorNavigation;
|
|
36
|
+
interface NavigationContext {
|
|
37
|
+
terminalIsLargeEnough: boolean;
|
|
38
|
+
projects: ProjectState[];
|
|
39
|
+
project?: ProjectState;
|
|
40
|
+
runs: RunRecord[];
|
|
41
|
+
run?: RunRecord;
|
|
42
|
+
sessions: InvocationRecord[];
|
|
43
|
+
session?: InvocationRecord;
|
|
44
|
+
events: EventRecord[];
|
|
45
|
+
event?: EventRecord;
|
|
46
|
+
logSessions: InvocationRecord[];
|
|
47
|
+
logSession?: InvocationRecord;
|
|
48
|
+
available: Record<"stop" | "retry" | "recover", boolean>;
|
|
49
|
+
pending: boolean;
|
|
50
|
+
scrollMaximum: number;
|
|
51
|
+
pageSize: number;
|
|
52
|
+
}
|
|
53
|
+
export type NavigationEvent = {
|
|
54
|
+
type: "selection";
|
|
55
|
+
selection: Selection;
|
|
56
|
+
} | {
|
|
57
|
+
type: "dismiss";
|
|
58
|
+
} | {
|
|
59
|
+
type: "key";
|
|
60
|
+
input: string;
|
|
61
|
+
key: Key;
|
|
62
|
+
context: NavigationContext;
|
|
63
|
+
};
|
|
64
|
+
interface Transition {
|
|
65
|
+
state: MonitorNavigation;
|
|
66
|
+
effect?: {
|
|
67
|
+
type: "exit";
|
|
68
|
+
} | {
|
|
69
|
+
type: "command";
|
|
70
|
+
kind: MonitorAction;
|
|
71
|
+
target: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Pure navigation transitions; the monitor owns rendering and command effects. */
|
|
75
|
+
export declare function transitionNavigation(state: MonitorNavigation, event: NavigationEvent): Transition;
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
export const initialNavigation = {
|
|
2
|
+
selection: {},
|
|
3
|
+
screen: "dashboard",
|
|
4
|
+
focus: "runs",
|
|
5
|
+
offset: 0,
|
|
6
|
+
};
|
|
7
|
+
const focuses = ["runs", "summary", "sessions"];
|
|
8
|
+
/** Pure navigation transitions; the monitor owns rendering and command effects. */
|
|
9
|
+
export function transitionNavigation(state, event) {
|
|
10
|
+
if (event.type === "dismiss")
|
|
11
|
+
return { state: { ...state, modal: undefined } };
|
|
12
|
+
if (event.type === "selection")
|
|
13
|
+
return { state: synchronizeSelection(state, event.selection) };
|
|
14
|
+
const { input, key, context } = event;
|
|
15
|
+
if (key.ctrl || key.meta || key.eventType === "release")
|
|
16
|
+
return { state };
|
|
17
|
+
// Dialogs/logs own keyboard input, except when hidden by the resize screen.
|
|
18
|
+
if (state.modal && context.terminalIsLargeEnough)
|
|
19
|
+
return { state };
|
|
20
|
+
if (input === "q")
|
|
21
|
+
return { state, effect: { type: "exit" } };
|
|
22
|
+
if (!context.terminalIsLargeEnough)
|
|
23
|
+
return { state };
|
|
24
|
+
if (key.escape)
|
|
25
|
+
return {
|
|
26
|
+
state: { ...state, screen: "dashboard", focus: "runs", offset: 0 },
|
|
27
|
+
};
|
|
28
|
+
if (input === "?")
|
|
29
|
+
return { state: { ...state, modal: { type: "help" } } };
|
|
30
|
+
if (key.upArrow || key.downArrow || key.pageUp || key.pageDown)
|
|
31
|
+
return { state: moveVertically(state, key, context) };
|
|
32
|
+
if (input === "l")
|
|
33
|
+
return { state: openAgentLog(state, context) };
|
|
34
|
+
if (input === "v" && context.run?.validation?.length)
|
|
35
|
+
return {
|
|
36
|
+
state: {
|
|
37
|
+
...state,
|
|
38
|
+
modal: {
|
|
39
|
+
type: "logs",
|
|
40
|
+
sources: context.run.validation.map((check) => ({
|
|
41
|
+
path: check.log,
|
|
42
|
+
label: `${check.command} · exit ${check.exitCode}`,
|
|
43
|
+
})),
|
|
44
|
+
initial: 0,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const kind = input === "s"
|
|
49
|
+
? "stop"
|
|
50
|
+
: input === "r"
|
|
51
|
+
? "retry"
|
|
52
|
+
: input === "R"
|
|
53
|
+
? "retry-refresh"
|
|
54
|
+
: input === "c"
|
|
55
|
+
? "recover"
|
|
56
|
+
: undefined;
|
|
57
|
+
if (kind &&
|
|
58
|
+
context.run &&
|
|
59
|
+
context.available[kind === "retry-refresh" ? "retry" : kind])
|
|
60
|
+
return {
|
|
61
|
+
state: {
|
|
62
|
+
...state,
|
|
63
|
+
modal: {
|
|
64
|
+
type: "confirmation",
|
|
65
|
+
kind,
|
|
66
|
+
runId: context.run.id,
|
|
67
|
+
title: `#${context.run.issue.number} ${context.run.issue.title}`,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
if (state.screen === "details")
|
|
72
|
+
return { state };
|
|
73
|
+
return dashboardInput(state, input, key, context);
|
|
74
|
+
}
|
|
75
|
+
function synchronizeSelection(state, selection) {
|
|
76
|
+
const sameRun = state.displayedRunId === selection.runId;
|
|
77
|
+
if (sameRun &&
|
|
78
|
+
state.selection.projectId === selection.projectId &&
|
|
79
|
+
state.selection.runId === selection.runId)
|
|
80
|
+
return state;
|
|
81
|
+
if (sameRun)
|
|
82
|
+
return { ...state, selection };
|
|
83
|
+
return {
|
|
84
|
+
...state,
|
|
85
|
+
selection,
|
|
86
|
+
displayedRunId: selection.runId,
|
|
87
|
+
sessionId: undefined,
|
|
88
|
+
stepSequence: undefined,
|
|
89
|
+
offset: 0,
|
|
90
|
+
modal: state.modal?.type === "confirmation" ? undefined : state.modal,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function dashboardInput(state, input, key, context) {
|
|
94
|
+
if (key.tab)
|
|
95
|
+
return {
|
|
96
|
+
state: {
|
|
97
|
+
...state,
|
|
98
|
+
focus: focuses[(focuses.indexOf(state.focus) + (key.shift ? 2 : 1)) % 3],
|
|
99
|
+
offset: 0,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
if (input === "a")
|
|
103
|
+
return { state: { ...state, focus: "sessions" } };
|
|
104
|
+
if (key.leftArrow || key.rightArrow) {
|
|
105
|
+
const next = adjacent(context.projects, context.projects.findIndex((item) => item.id === context.project?.id), key.leftArrow ? -1 : 1);
|
|
106
|
+
return {
|
|
107
|
+
state: {
|
|
108
|
+
...state,
|
|
109
|
+
selection: { projectId: next?.id },
|
|
110
|
+
focus: "runs",
|
|
111
|
+
offset: 0,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (key.return && context.run)
|
|
116
|
+
return { state: { ...state, screen: "details", offset: 0 } };
|
|
117
|
+
if (input === "[" || input === "]") {
|
|
118
|
+
const next = adjacent(context.events, context.events.findIndex((item) => item.sequence === context.event?.sequence), input === "]" ? 1 : -1);
|
|
119
|
+
return { state: { ...state, stepSequence: next?.sequence } };
|
|
120
|
+
}
|
|
121
|
+
if (key.end)
|
|
122
|
+
return { state: { ...state, stepSequence: undefined } };
|
|
123
|
+
if (input === "p" && context.project && !context.pending)
|
|
124
|
+
return {
|
|
125
|
+
state,
|
|
126
|
+
effect: {
|
|
127
|
+
type: "command",
|
|
128
|
+
kind: context.project.paused ? "resume" : "pause",
|
|
129
|
+
target: context.project.id,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
return { state };
|
|
133
|
+
}
|
|
134
|
+
function moveVertically(state, key, context) {
|
|
135
|
+
const delta = key.upArrow || key.pageUp ? -1 : 1;
|
|
136
|
+
if (state.screen === "details" || state.focus === "summary") {
|
|
137
|
+
const current = state.screen === "details"
|
|
138
|
+
? Math.min(state.offset, context.scrollMaximum)
|
|
139
|
+
: state.offset;
|
|
140
|
+
const distance = key.pageUp || key.pageDown ? context.pageSize : 1;
|
|
141
|
+
return {
|
|
142
|
+
...state,
|
|
143
|
+
offset: Math.max(0, Math.min(context.scrollMaximum, current + delta * distance)),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (state.focus === "sessions") {
|
|
147
|
+
const next = adjacent(context.sessions, context.sessions.findIndex((item) => item.id === context.session?.id), delta);
|
|
148
|
+
return { ...state, sessionId: next?.id };
|
|
149
|
+
}
|
|
150
|
+
const next = adjacent(context.runs, context.runs.findIndex((item) => item.id === context.run?.id), delta);
|
|
151
|
+
return {
|
|
152
|
+
...state,
|
|
153
|
+
selection: { projectId: context.project?.id, runId: next?.id },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function openAgentLog(state, context) {
|
|
157
|
+
const currentExecutionId = context.run?.executions?.at(-1)?.id ?? context.run?.id;
|
|
158
|
+
const stageSources = (context.run?.stageLogs ?? [])
|
|
159
|
+
.filter((item) => item.executionId === currentExecutionId)
|
|
160
|
+
.map((item) => ({
|
|
161
|
+
path: item.path,
|
|
162
|
+
label: `${item.step} · stage diagnostic`,
|
|
163
|
+
}));
|
|
164
|
+
const sources = [
|
|
165
|
+
...stageSources,
|
|
166
|
+
...context.logSessions.map((session) => ({
|
|
167
|
+
path: session.log,
|
|
168
|
+
label: `${session.step} · invocation ${session.attempt}`,
|
|
169
|
+
})),
|
|
170
|
+
];
|
|
171
|
+
if (!sources.length)
|
|
172
|
+
return state;
|
|
173
|
+
return {
|
|
174
|
+
...state,
|
|
175
|
+
modal: {
|
|
176
|
+
type: "logs",
|
|
177
|
+
sources,
|
|
178
|
+
initial: context.logSession
|
|
179
|
+
? Math.max(0, stageSources.length +
|
|
180
|
+
context.logSessions.findIndex((session) => session.id === context.logSession?.id))
|
|
181
|
+
: 0,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function adjacent(items, index, delta) {
|
|
186
|
+
return items[Math.max(0, Math.min(items.length - 1, index + delta))];
|
|
187
|
+
}
|
package/dist/src/tui/monitor.js
CHANGED
|
@@ -1,233 +1,114 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useApp, useInput, useWindowSize } from "ink";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import { actionAvailability } from "./actions.js";
|
|
5
4
|
import { minimumTerminalSize } from "./constants.js";
|
|
6
5
|
import { useMonitorData } from "./data.js";
|
|
7
6
|
import { ConfirmDialog, HelpDialog } from "./dialogs.js";
|
|
8
7
|
import { cells, colorFor, wrapLines } from "./format.js";
|
|
9
8
|
import { monitorLayout } from "./layout.js";
|
|
10
9
|
import { LogViewer } from "./log.js";
|
|
10
|
+
import { initialNavigation, transitionNavigation, } from "./monitor-navigation.js";
|
|
11
|
+
import { projectRunProjection } from "./projection.js";
|
|
11
12
|
import { detailLines, Lines, RunList, summaryLines, wrapDetailLines, } from "./views.js";
|
|
12
13
|
const notificationNoticeDurationMs = 2000;
|
|
13
|
-
const focuses = ["runs", "summary", "sessions"];
|
|
14
14
|
const statusRefreshIntervalMs = 1_000;
|
|
15
15
|
const actionDescriptions = {
|
|
16
16
|
stop: "Cancel this run and wait for its active local work to stop.",
|
|
17
17
|
retry: "Create a new run and branch; execute the workflow again.",
|
|
18
|
+
"retry-refresh": "Create a new run using the current hosted issue and validation selection.",
|
|
18
19
|
recover: "Continue the failed publication step using completed work.",
|
|
19
20
|
};
|
|
20
|
-
function currentExecutionSessions(run, sessions, events) {
|
|
21
|
-
const current = run?.executions?.at(-1);
|
|
22
|
-
if (!current || (run?.executions?.length ?? 0) <= 1)
|
|
23
|
-
return sessions;
|
|
24
|
-
const currentStepIds = new Set(events.flatMap((event) => {
|
|
25
|
-
const payload = event.payload;
|
|
26
|
-
return payload.executionId === current.id &&
|
|
27
|
-
typeof payload.stepId === "number"
|
|
28
|
-
? [payload.stepId]
|
|
29
|
-
: [];
|
|
30
|
-
}));
|
|
31
|
-
if (currentStepIds.size)
|
|
32
|
-
return sessions.filter((item) => currentStepIds.has(item.stepId));
|
|
33
|
-
const executionCreatedAt = Date.parse(current.createdAt);
|
|
34
|
-
if (!Number.isFinite(executionCreatedAt))
|
|
35
|
-
return [];
|
|
36
|
-
return sessions.filter((item) => Date.parse(item.startedAt) >= executionCreatedAt);
|
|
37
|
-
}
|
|
38
|
-
function latestSession(sessions) {
|
|
39
|
-
const byRecency = [...sessions].sort((left, right) => Date.parse(left.startedAt) - Date.parse(right.startedAt));
|
|
40
|
-
return (byRecency.filter((item) => item.outcome === "running").at(-1) ??
|
|
41
|
-
byRecency.at(-1));
|
|
42
|
-
}
|
|
43
21
|
export function Monitor({ source, size, notificationWriter, }) {
|
|
44
22
|
const window = useWindowSize();
|
|
45
23
|
const { columns, rows } = size ?? window;
|
|
46
24
|
const { exit } = useApp();
|
|
47
|
-
const [
|
|
25
|
+
const [navigation, setNavigation] = useState(initialNavigation);
|
|
26
|
+
const { selection, focus, screen, sessionId, stepSequence, offset, modal } = navigation;
|
|
27
|
+
const confirmation = modal?.type === "confirmation" ? modal : undefined;
|
|
28
|
+
const logs = modal?.type === "logs" ? modal : undefined;
|
|
48
29
|
const data = useMonitorData(source, selection, notificationWriter);
|
|
49
30
|
const { projects, project, projectRuns, run, sessions, events } = data;
|
|
50
|
-
const
|
|
51
|
-
const [screen, setScreen] = useState("dashboard");
|
|
52
|
-
const [sessionId, setSessionId] = useState();
|
|
53
|
-
const [stepSequence, setStepSequence] = useState();
|
|
54
|
-
const [offset, setOffset] = useState(0);
|
|
55
|
-
const [helpOpen, setHelpOpen] = useState(false);
|
|
56
|
-
const [confirmation, setConfirmation] = useState();
|
|
57
|
-
const [logs, setLogs] = useState();
|
|
31
|
+
const dismissModal = () => setNavigation((current) => transitionNavigation(current, { type: "dismiss" }).state);
|
|
58
32
|
const [now, setNow] = useState(Date.now());
|
|
59
33
|
const [notificationNoticeUntil] = useState(() => Date.now() + notificationNoticeDurationMs);
|
|
60
34
|
const showNotificationNotice = Boolean(notificationWriter) && now < notificationNoticeUntil;
|
|
61
35
|
const layout = monitorLayout(columns, rows - (showNotificationNotice ? 1 : 0), screen === "details");
|
|
62
36
|
const { wide, height, paneWidth, summaryWidth } = layout;
|
|
63
37
|
const session = sessions.find((item) => item.id === sessionId) ?? sessions[0];
|
|
64
|
-
const executionSessions
|
|
65
|
-
|
|
38
|
+
const { executionSessions, detailLogSession, recoveryReason, available } = projectRunProjection({
|
|
39
|
+
run,
|
|
40
|
+
project,
|
|
41
|
+
projectRuns,
|
|
42
|
+
sessions,
|
|
43
|
+
events,
|
|
44
|
+
pending: Boolean(data.pending),
|
|
45
|
+
});
|
|
66
46
|
const event = events.find((item) => item.sequence === stepSequence) ?? events.at(-1);
|
|
67
47
|
useEffect(() => {
|
|
68
48
|
const timer = setInterval(() => setNow(Date.now()), statusRefreshIntervalMs);
|
|
69
49
|
return () => clearInterval(timer);
|
|
70
50
|
}, []);
|
|
71
51
|
useEffect(() => {
|
|
72
|
-
// Pin default selections
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
: { projectId: project?.id, runId: run?.id }
|
|
52
|
+
// Pin default selections and reset run-specific navigation in one transition.
|
|
53
|
+
setNavigation((current) => transitionNavigation(current, {
|
|
54
|
+
type: "selection",
|
|
55
|
+
selection: { projectId: project?.id, runId: run?.id },
|
|
56
|
+
}).state);
|
|
76
57
|
}, [project?.id, run?.id]);
|
|
77
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: reset navigation when the selected run identity changes.
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
setSessionId(undefined);
|
|
80
|
-
setStepSequence(undefined);
|
|
81
|
-
setOffset(0);
|
|
82
|
-
setConfirmation(undefined);
|
|
83
|
-
}, [run?.id]);
|
|
84
|
-
const selectProject = (delta) => {
|
|
85
|
-
const index = projects.findIndex((item) => item.id === project?.id);
|
|
86
|
-
const next = projects[Math.max(0, Math.min(projects.length - 1, index + delta))];
|
|
87
|
-
setSelection({ projectId: next?.id });
|
|
88
|
-
setFocus("runs");
|
|
89
|
-
setOffset(0);
|
|
90
|
-
};
|
|
91
|
-
const openAgentLog = (candidates = sessions, initialSession = session) => {
|
|
92
|
-
if (!candidates.length)
|
|
93
|
-
return;
|
|
94
|
-
setLogs({
|
|
95
|
-
sources: candidates.map((item) => ({
|
|
96
|
-
path: item.log,
|
|
97
|
-
label: `${item.step} · invocation ${item.attempt}`,
|
|
98
|
-
})),
|
|
99
|
-
initial: Math.max(0, candidates.findIndex((item) => item.id === initialSession?.id)),
|
|
100
|
-
});
|
|
101
|
-
};
|
|
102
|
-
const { recoveryReason, available } = actionAvailability({
|
|
103
|
-
run,
|
|
104
|
-
project,
|
|
105
|
-
projectRuns,
|
|
106
|
-
pending: Boolean(data.pending),
|
|
107
|
-
});
|
|
108
58
|
const detailDocument = run
|
|
109
59
|
? detailLines(run, sessions, now, recoveryReason, layout.details.width, available)
|
|
110
60
|
: ["Run no longer available"];
|
|
111
61
|
const wrappedDetailLines = wrapDetailLines(detailDocument, layout.details.width);
|
|
112
62
|
const detailOffset = Math.min(offset, Math.max(0, wrappedDetailLines.length - layout.details.height));
|
|
63
|
+
const terminalIsLargeEnough = columns >= minimumTerminalSize.columns && rows >= minimumTerminalSize.rows;
|
|
113
64
|
useInput((input, key) => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
65
|
+
const viewport = screen === "details" ? layout.details : layout.summary;
|
|
66
|
+
const scrollLines = run
|
|
67
|
+
? screen === "details"
|
|
68
|
+
? wrappedDetailLines
|
|
69
|
+
: wrapLines(summaryLines(run, now, event), viewport.width)
|
|
70
|
+
: [];
|
|
71
|
+
const navigationEvent = {
|
|
72
|
+
type: "key",
|
|
73
|
+
input,
|
|
74
|
+
key,
|
|
75
|
+
context: {
|
|
76
|
+
terminalIsLargeEnough,
|
|
77
|
+
projects,
|
|
78
|
+
project,
|
|
79
|
+
runs: projectRuns,
|
|
80
|
+
run,
|
|
81
|
+
sessions,
|
|
82
|
+
session,
|
|
83
|
+
events,
|
|
84
|
+
event,
|
|
85
|
+
logSessions: screen === "details" ? executionSessions : sessions,
|
|
86
|
+
logSession: screen === "details" ? detailLogSession : session,
|
|
87
|
+
available,
|
|
88
|
+
pending: Boolean(data.pending),
|
|
89
|
+
scrollMaximum: Math.max(0, scrollLines.length - viewport.height),
|
|
90
|
+
pageSize: layout.details.height,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const { effect } = transitionNavigation(navigation, navigationEvent);
|
|
94
|
+
setNavigation((current) => transitionNavigation(current, navigationEvent).state);
|
|
95
|
+
if (effect?.type === "exit")
|
|
123
96
|
exit();
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!terminalIsLargeEnough)
|
|
127
|
-
return;
|
|
128
|
-
if (key.escape) {
|
|
129
|
-
setScreen("dashboard");
|
|
130
|
-
setFocus("runs");
|
|
131
|
-
setOffset(0);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (input === "?") {
|
|
135
|
-
setHelpOpen(true);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if (key.tab && screen === "dashboard") {
|
|
139
|
-
setFocus(focuses[(focuses.indexOf(focus) + (key.shift ? 2 : 1)) % 3]);
|
|
140
|
-
setOffset(0);
|
|
141
|
-
}
|
|
142
|
-
if (input === "a" && screen === "dashboard") {
|
|
143
|
-
setScreen("dashboard");
|
|
144
|
-
setFocus("sessions");
|
|
145
|
-
}
|
|
146
|
-
if (key.leftArrow && screen === "dashboard")
|
|
147
|
-
selectProject(-1);
|
|
148
|
-
if (key.rightArrow && screen === "dashboard")
|
|
149
|
-
selectProject(1);
|
|
150
|
-
if (key.return && run && screen === "dashboard") {
|
|
151
|
-
setScreen("details");
|
|
152
|
-
setOffset(0);
|
|
153
|
-
}
|
|
154
|
-
if (input === "l")
|
|
155
|
-
screen === "details"
|
|
156
|
-
? openAgentLog(executionSessions, detailLogSession)
|
|
157
|
-
: openAgentLog();
|
|
158
|
-
if (input === "v" && run?.validation?.length)
|
|
159
|
-
setLogs({
|
|
160
|
-
sources: run.validation.map((item) => ({
|
|
161
|
-
path: item.log,
|
|
162
|
-
label: `${item.command} · exit ${item.exitCode}`,
|
|
163
|
-
})),
|
|
164
|
-
initial: 0,
|
|
165
|
-
});
|
|
166
|
-
if (screen === "dashboard" && (input === "[" || input === "]")) {
|
|
167
|
-
const index = events.findIndex((item) => item.sequence === event?.sequence);
|
|
168
|
-
setStepSequence(events[Math.max(0, Math.min(events.length - 1, index + (input === "]" ? 1 : -1)))]?.sequence);
|
|
169
|
-
}
|
|
170
|
-
if (key.end && screen === "dashboard")
|
|
171
|
-
setStepSequence(undefined);
|
|
172
|
-
if (key.upArrow || key.downArrow || key.pageUp || key.pageDown) {
|
|
173
|
-
const delta = key.upArrow || key.pageUp ? -1 : 1;
|
|
174
|
-
if (screen === "details" || focus === "summary") {
|
|
175
|
-
const lines = run
|
|
176
|
-
? screen === "details"
|
|
177
|
-
? detailDocument
|
|
178
|
-
: summaryLines(run, now, event)
|
|
179
|
-
: [];
|
|
180
|
-
const viewport = screen === "details" ? layout.details : layout.summary;
|
|
181
|
-
setOffset((value) => {
|
|
182
|
-
const maximum = Math.max(0, (screen === "details"
|
|
183
|
-
? wrapDetailLines(lines, viewport.width)
|
|
184
|
-
: wrapLines(lines, viewport.width)).length - viewport.height);
|
|
185
|
-
const current = screen === "details" ? Math.min(value, maximum) : value;
|
|
186
|
-
return Math.max(0, Math.min(maximum, current +
|
|
187
|
-
delta *
|
|
188
|
-
(key.pageUp || key.pageDown ? layout.details.height : 1)));
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
else if (focus === "sessions") {
|
|
192
|
-
const index = sessions.findIndex((item) => item.id === session?.id);
|
|
193
|
-
setSessionId(sessions[Math.max(0, Math.min(sessions.length - 1, index + delta))]
|
|
194
|
-
?.id);
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
const index = projectRuns.findIndex((item) => item.id === run?.id);
|
|
198
|
-
setSelection({
|
|
199
|
-
projectId: project?.id,
|
|
200
|
-
runId: projectRuns[Math.max(0, Math.min(projectRuns.length - 1, index + delta))]?.id,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (input === "p" && screen === "dashboard" && project && !data.pending)
|
|
205
|
-
void data.action(project.paused ? "resume" : "pause", project.id);
|
|
206
|
-
const kind = input === "s"
|
|
207
|
-
? "stop"
|
|
208
|
-
: input === "r"
|
|
209
|
-
? "retry"
|
|
210
|
-
: input === "c"
|
|
211
|
-
? "recover"
|
|
212
|
-
: undefined;
|
|
213
|
-
if (kind && run && available[kind])
|
|
214
|
-
setConfirmation({
|
|
215
|
-
kind,
|
|
216
|
-
runId: run.id,
|
|
217
|
-
title: `#${run.issue.number} ${run.issue.title}`,
|
|
218
|
-
});
|
|
97
|
+
if (effect?.type === "command")
|
|
98
|
+
void data.action(effect.kind, effect.target);
|
|
219
99
|
});
|
|
220
|
-
if (
|
|
100
|
+
if (!terminalIsLargeEnough)
|
|
221
101
|
return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsx(Text, { children: cells(`Resize terminal to at least ${minimumTerminalSize.columns}×${minimumTerminalSize.rows}. q closes monitor.`, columns) }) }));
|
|
222
|
-
if (
|
|
223
|
-
return
|
|
102
|
+
if (modal?.type === "help")
|
|
103
|
+
return _jsx(HelpDialog, { columns: columns, rows: rows, onClose: dismissModal });
|
|
224
104
|
if (confirmation)
|
|
225
|
-
return (_jsx(ConfirmDialog, { columns: columns, rows: rows, title: `Confirm ${confirmation.kind}`, subject: confirmation.title, description: actionDescriptions[confirmation.kind], available: run?.id === confirmation.runId &&
|
|
105
|
+
return (_jsx(ConfirmDialog, { columns: columns, rows: rows, title: `Confirm ${confirmation.kind}`, subject: confirmation.title, description: actionDescriptions[confirmation.kind], available: run?.id === confirmation.runId &&
|
|
106
|
+
available[confirmation.kind === "retry-refresh" ? "retry" : confirmation.kind], onCancel: dismissModal, onConfirm: () => {
|
|
226
107
|
void data.action(confirmation.kind, confirmation.runId);
|
|
227
|
-
|
|
108
|
+
dismissModal();
|
|
228
109
|
} }));
|
|
229
110
|
if (logs)
|
|
230
|
-
return (_jsx(LogViewer, { ...logs, columns: columns, rows: rows, onBack:
|
|
111
|
+
return (_jsx(LogViewer, { ...logs, columns: columns, rows: rows, onBack: dismissModal }));
|
|
231
112
|
const freshness = data.lastUpdated
|
|
232
113
|
? `refreshed ${Math.max(0, Math.floor((now - data.lastUpdated) / statusRefreshIntervalMs))}s ago`
|
|
233
114
|
: "freshness unavailable";
|
|
@@ -242,9 +123,15 @@ export function Monitor({ source, size, notificationWriter, }) {
|
|
|
242
123
|
? columns < 120
|
|
243
124
|
? "l current log"
|
|
244
125
|
: `l log: ${detailLogSession.step} invocation ${detailLogSession.attempt} (${detailLogSession.outcome})`
|
|
245
|
-
:
|
|
126
|
+
: run?.stageLogs?.some((item) => item.executionId === (run.executions?.at(-1)?.id ?? run.id))
|
|
127
|
+
? "l stage diagnostic"
|
|
128
|
+
: "";
|
|
246
129
|
const controls = [
|
|
247
|
-
screen === "details"
|
|
130
|
+
screen === "details"
|
|
131
|
+
? detailsLogControl
|
|
132
|
+
: sessions.length || run?.stageLogs?.length
|
|
133
|
+
? "l log"
|
|
134
|
+
: "",
|
|
248
135
|
run?.validation?.length
|
|
249
136
|
? columns < 120
|
|
250
137
|
? "v checks"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RunRecord } from "../domain.js";
|
|
2
|
+
import type { EventRecord, InvocationRecord, ProjectState } from "../store.js";
|
|
3
|
+
interface RunProjectionInput {
|
|
4
|
+
run?: RunRecord;
|
|
5
|
+
project?: ProjectState;
|
|
6
|
+
projectRuns: RunRecord[];
|
|
7
|
+
sessions?: InvocationRecord[];
|
|
8
|
+
events?: EventRecord[];
|
|
9
|
+
pending: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Operator facts derived from one selected Run; admission remains authoritative. */
|
|
12
|
+
export declare function projectRunProjection({ run, project, projectRuns, sessions, events, pending, }: RunProjectionInput): {
|
|
13
|
+
recoveryReason: string | undefined;
|
|
14
|
+
available: {
|
|
15
|
+
stop: boolean;
|
|
16
|
+
retry: boolean;
|
|
17
|
+
recover: boolean;
|
|
18
|
+
};
|
|
19
|
+
executionSessions: InvocationRecord[];
|
|
20
|
+
detailLogSession: InvocationRecord | undefined;
|
|
21
|
+
};
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { recoveryUnavailable } from "../recovery.js";
|
|
2
|
+
/** Operator facts derived from one selected Run; admission remains authoritative. */
|
|
3
|
+
export function projectRunProjection({ run, project, projectRuns, sessions = [], events = [], pending, }) {
|
|
4
|
+
const recoveryReason = run
|
|
5
|
+
? (recoveryUnavailable(run) ??
|
|
6
|
+
project?.blocked ??
|
|
7
|
+
(projectRuns.some((item) => item.taskKey === run.taskKey && item.attempt > run.attempt)
|
|
8
|
+
? "A newer attempt has superseded this run"
|
|
9
|
+
: undefined))
|
|
10
|
+
: undefined;
|
|
11
|
+
const executionSessions = currentExecutionSessions(run, sessions, events);
|
|
12
|
+
return {
|
|
13
|
+
recoveryReason,
|
|
14
|
+
available: {
|
|
15
|
+
stop: Boolean(run && !pending && ["queued", "running"].includes(run.outcome)),
|
|
16
|
+
retry: Boolean(run &&
|
|
17
|
+
!pending &&
|
|
18
|
+
["failed", "blocked", "cancelled"].includes(run.outcome) &&
|
|
19
|
+
!projectRuns.some((item) => item.taskKey === run.taskKey &&
|
|
20
|
+
["queued", "running"].includes(item.outcome))),
|
|
21
|
+
recover: Boolean(run && !pending && !recoveryReason),
|
|
22
|
+
},
|
|
23
|
+
executionSessions,
|
|
24
|
+
detailLogSession: latestSession(executionSessions),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function currentExecutionSessions(run, sessions, events) {
|
|
28
|
+
const current = run?.executions?.at(-1);
|
|
29
|
+
if (!current || (run?.executions?.length ?? 0) <= 1)
|
|
30
|
+
return sessions;
|
|
31
|
+
const currentStepIds = new Set(events.flatMap((event) => {
|
|
32
|
+
const payload = event.payload;
|
|
33
|
+
return payload.executionId === current.id &&
|
|
34
|
+
typeof payload.stepId === "number"
|
|
35
|
+
? [payload.stepId]
|
|
36
|
+
: [];
|
|
37
|
+
}));
|
|
38
|
+
if (currentStepIds.size)
|
|
39
|
+
return sessions.filter((item) => currentStepIds.has(item.stepId));
|
|
40
|
+
const executionCreatedAt = Date.parse(current.createdAt);
|
|
41
|
+
if (!Number.isFinite(executionCreatedAt))
|
|
42
|
+
return [];
|
|
43
|
+
return sessions.filter((item) => Date.parse(item.startedAt) >= executionCreatedAt);
|
|
44
|
+
}
|
|
45
|
+
function latestSession(sessions) {
|
|
46
|
+
const byRecency = [...sessions].sort((left, right) => Date.parse(left.startedAt) - Date.parse(right.startedAt));
|
|
47
|
+
return (byRecency.filter((item) => item.outcome === "running").at(-1) ??
|
|
48
|
+
byRecency.at(-1));
|
|
49
|
+
}
|