@mingchuno/agent-workflows 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -15
- package/dist/src/attribution.d.ts +9 -0
- package/dist/src/attribution.js +57 -0
- package/dist/src/cli-config.d.ts +2 -0
- package/dist/src/cli-config.js +35 -0
- package/dist/src/cli.js +50 -31
- package/dist/src/config.d.ts +2 -8
- package/dist/src/config.js +1 -1
- package/dist/src/domain.d.ts +7 -0
- package/dist/src/invocation.d.ts +4 -3
- package/dist/src/invocation.js +6 -3
- package/dist/src/operations.d.ts +1 -0
- package/dist/src/operations.js +25 -3
- package/dist/src/runner.d.ts +2 -0
- package/dist/src/runner.js +28 -3
- package/dist/src/tui/data.d.ts +2 -1
- package/dist/src/tui/data.js +7 -2
- package/dist/src/tui/index.d.ts +1 -0
- package/dist/src/tui/index.js +1 -0
- package/dist/src/tui/layout.d.ts +1 -1
- package/dist/src/tui/layout.js +2 -2
- package/dist/src/tui/monitor.d.ts +3 -1
- package/dist/src/tui/monitor.js +93 -31
- package/dist/src/tui/notifications.d.ts +23 -0
- package/dist/src/tui/notifications.js +104 -0
- package/dist/src/tui/views.d.ts +10 -2
- package/dist/src/tui/views.js +272 -42
- package/dist/src/workspace.js +21 -8
- package/docs/api.md +25 -10
- package/docs/configuration.md +78 -27
- package/docs/database.md +2 -18
- package/docs/operations.md +54 -22
- package/docs/providers.md +2 -2
- package/examples/config.ts +4 -4
- package/examples/run.ts +4 -1
- package/package.json +1 -1
- package/docs/architecture.md +0 -41
package/dist/src/tui/index.d.ts
CHANGED
package/dist/src/tui/index.js
CHANGED
package/dist/src/tui/layout.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Pane content sizes shared by rendering and keyboard scrolling. */
|
|
2
|
-
export declare function monitorLayout(columns: number, rows: number): {
|
|
2
|
+
export declare function monitorLayout(columns: number, rows: number, compactChrome?: boolean): {
|
|
3
3
|
wide: boolean;
|
|
4
4
|
height: number;
|
|
5
5
|
paneWidth: number;
|
package/dist/src/tui/layout.js
CHANGED
|
@@ -2,9 +2,9 @@ import { screenChromeRows } from "./constants.js";
|
|
|
2
2
|
const panelHorizontalChrome = 4;
|
|
3
3
|
const panelVerticalChrome = 3;
|
|
4
4
|
/** Pane content sizes shared by rendering and keyboard scrolling. */
|
|
5
|
-
export function monitorLayout(columns, rows) {
|
|
5
|
+
export function monitorLayout(columns, rows, compactChrome = false) {
|
|
6
6
|
const wide = columns >= 110;
|
|
7
|
-
const height = Math.max(1, rows - screenChromeRows);
|
|
7
|
+
const height = Math.max(1, rows - (compactChrome ? 4 : screenChromeRows));
|
|
8
8
|
const paneWidth = wide ? Math.floor(columns * 0.43) : columns;
|
|
9
9
|
const summaryWidth = wide ? columns - paneWidth : columns;
|
|
10
10
|
const summaryPanelHeight = wide ? height - 6 : height;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type MonitorSource } from "./data.js";
|
|
2
|
-
|
|
2
|
+
import type { ExecutionNotificationWriter } from "./notifications.js";
|
|
3
|
+
export declare function Monitor({ source, size, notificationWriter, }: {
|
|
3
4
|
source: MonitorSource;
|
|
4
5
|
size?: {
|
|
5
6
|
columns: number;
|
|
6
7
|
rows: number;
|
|
7
8
|
};
|
|
9
|
+
notificationWriter?: ExecutionNotificationWriter;
|
|
8
10
|
}): import("react").JSX.Element;
|
package/dist/src/tui/monitor.js
CHANGED
|
@@ -8,7 +8,8 @@ import { ConfirmDialog, HelpDialog } from "./dialogs.js";
|
|
|
8
8
|
import { cells, colorFor, wrapLines } from "./format.js";
|
|
9
9
|
import { monitorLayout } from "./layout.js";
|
|
10
10
|
import { LogViewer } from "./log.js";
|
|
11
|
-
import { detailLines, Lines, RunList, summaryLines } from "./views.js";
|
|
11
|
+
import { detailLines, Lines, RunList, summaryLines, wrapDetailLines, } from "./views.js";
|
|
12
|
+
const notificationNoticeDurationMs = 2000;
|
|
12
13
|
const focuses = ["runs", "summary", "sessions"];
|
|
13
14
|
const statusRefreshIntervalMs = 1_000;
|
|
14
15
|
const actionDescriptions = {
|
|
@@ -16,12 +17,35 @@ const actionDescriptions = {
|
|
|
16
17
|
retry: "Create a new run and branch; execute the workflow again.",
|
|
17
18
|
recover: "Continue the failed publication step using completed work.",
|
|
18
19
|
};
|
|
19
|
-
|
|
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
|
+
export function Monitor({ source, size, notificationWriter, }) {
|
|
20
44
|
const window = useWindowSize();
|
|
21
45
|
const { columns, rows } = size ?? window;
|
|
22
46
|
const { exit } = useApp();
|
|
23
47
|
const [selection, setSelection] = useState({});
|
|
24
|
-
const data = useMonitorData(source, selection);
|
|
48
|
+
const data = useMonitorData(source, selection, notificationWriter);
|
|
25
49
|
const { projects, project, projectRuns, run, sessions, events } = data;
|
|
26
50
|
const [focus, setFocus] = useState("runs");
|
|
27
51
|
const [screen, setScreen] = useState("dashboard");
|
|
@@ -32,9 +56,13 @@ export function Monitor({ source, size, }) {
|
|
|
32
56
|
const [confirmation, setConfirmation] = useState();
|
|
33
57
|
const [logs, setLogs] = useState();
|
|
34
58
|
const [now, setNow] = useState(Date.now());
|
|
35
|
-
const
|
|
59
|
+
const [notificationNoticeUntil] = useState(() => Date.now() + notificationNoticeDurationMs);
|
|
60
|
+
const showNotificationNotice = Boolean(notificationWriter) && now < notificationNoticeUntil;
|
|
61
|
+
const layout = monitorLayout(columns, rows - (showNotificationNotice ? 1 : 0), screen === "details");
|
|
36
62
|
const { wide, height, paneWidth, summaryWidth } = layout;
|
|
37
63
|
const session = sessions.find((item) => item.id === sessionId) ?? sessions[0];
|
|
64
|
+
const executionSessions = currentExecutionSessions(run, sessions, events);
|
|
65
|
+
const detailLogSession = latestSession(executionSessions);
|
|
38
66
|
const event = events.find((item) => item.sequence === stepSequence) ?? events.at(-1);
|
|
39
67
|
useEffect(() => {
|
|
40
68
|
const timer = setInterval(() => setNow(Date.now()), statusRefreshIntervalMs);
|
|
@@ -60,15 +88,15 @@ export function Monitor({ source, size, }) {
|
|
|
60
88
|
setFocus("runs");
|
|
61
89
|
setOffset(0);
|
|
62
90
|
};
|
|
63
|
-
const openAgentLog = () => {
|
|
64
|
-
if (!
|
|
91
|
+
const openAgentLog = (candidates = sessions, initialSession = session) => {
|
|
92
|
+
if (!candidates.length)
|
|
65
93
|
return;
|
|
66
94
|
setLogs({
|
|
67
|
-
sources:
|
|
95
|
+
sources: candidates.map((item) => ({
|
|
68
96
|
path: item.log,
|
|
69
97
|
label: `${item.step} · invocation ${item.attempt}`,
|
|
70
98
|
})),
|
|
71
|
-
initial: Math.max(0,
|
|
99
|
+
initial: Math.max(0, candidates.findIndex((item) => item.id === initialSession?.id)),
|
|
72
100
|
});
|
|
73
101
|
};
|
|
74
102
|
const { recoveryReason, available } = actionAvailability({
|
|
@@ -77,6 +105,11 @@ export function Monitor({ source, size, }) {
|
|
|
77
105
|
projectRuns,
|
|
78
106
|
pending: Boolean(data.pending),
|
|
79
107
|
});
|
|
108
|
+
const detailDocument = run
|
|
109
|
+
? detailLines(run, sessions, now, recoveryReason, layout.details.width, available)
|
|
110
|
+
: ["Run no longer available"];
|
|
111
|
+
const wrappedDetailLines = wrapDetailLines(detailDocument, layout.details.width);
|
|
112
|
+
const detailOffset = Math.min(offset, Math.max(0, wrappedDetailLines.length - layout.details.height));
|
|
80
113
|
useInput((input, key) => {
|
|
81
114
|
if (key.ctrl || key.meta || key.eventType === "release")
|
|
82
115
|
return;
|
|
@@ -106,7 +139,7 @@ export function Monitor({ source, size, }) {
|
|
|
106
139
|
setFocus(focuses[(focuses.indexOf(focus) + (key.shift ? 2 : 1)) % 3]);
|
|
107
140
|
setOffset(0);
|
|
108
141
|
}
|
|
109
|
-
if (input === "a") {
|
|
142
|
+
if (input === "a" && screen === "dashboard") {
|
|
110
143
|
setScreen("dashboard");
|
|
111
144
|
setFocus("sessions");
|
|
112
145
|
}
|
|
@@ -114,12 +147,14 @@ export function Monitor({ source, size, }) {
|
|
|
114
147
|
selectProject(-1);
|
|
115
148
|
if (key.rightArrow && screen === "dashboard")
|
|
116
149
|
selectProject(1);
|
|
117
|
-
if (key.return && run) {
|
|
150
|
+
if (key.return && run && screen === "dashboard") {
|
|
118
151
|
setScreen("details");
|
|
119
152
|
setOffset(0);
|
|
120
153
|
}
|
|
121
154
|
if (input === "l")
|
|
122
|
-
|
|
155
|
+
screen === "details"
|
|
156
|
+
? openAgentLog(executionSessions, detailLogSession)
|
|
157
|
+
: openAgentLog();
|
|
123
158
|
if (input === "v" && run?.validation?.length)
|
|
124
159
|
setLogs({
|
|
125
160
|
sources: run.validation.map((item) => ({
|
|
@@ -128,24 +163,30 @@ export function Monitor({ source, size, }) {
|
|
|
128
163
|
})),
|
|
129
164
|
initial: 0,
|
|
130
165
|
});
|
|
131
|
-
if (input === "[" || input === "]") {
|
|
166
|
+
if (screen === "dashboard" && (input === "[" || input === "]")) {
|
|
132
167
|
const index = events.findIndex((item) => item.sequence === event?.sequence);
|
|
133
168
|
setStepSequence(events[Math.max(0, Math.min(events.length - 1, index + (input === "]" ? 1 : -1)))]?.sequence);
|
|
134
169
|
}
|
|
135
|
-
if (key.end)
|
|
170
|
+
if (key.end && screen === "dashboard")
|
|
136
171
|
setStepSequence(undefined);
|
|
137
172
|
if (key.upArrow || key.downArrow || key.pageUp || key.pageDown) {
|
|
138
173
|
const delta = key.upArrow || key.pageUp ? -1 : 1;
|
|
139
174
|
if (screen === "details" || focus === "summary") {
|
|
140
175
|
const lines = run
|
|
141
176
|
? screen === "details"
|
|
142
|
-
?
|
|
177
|
+
? detailDocument
|
|
143
178
|
: summaryLines(run, now, event)
|
|
144
179
|
: [];
|
|
145
180
|
const viewport = screen === "details" ? layout.details : layout.summary;
|
|
146
|
-
setOffset((value) =>
|
|
147
|
-
|
|
148
|
-
(
|
|
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
|
+
});
|
|
149
190
|
}
|
|
150
191
|
else if (focus === "sessions") {
|
|
151
192
|
const index = sessions.findIndex((item) => item.id === session?.id);
|
|
@@ -160,7 +201,7 @@ export function Monitor({ source, size, }) {
|
|
|
160
201
|
});
|
|
161
202
|
}
|
|
162
203
|
}
|
|
163
|
-
if (input === "p" && project && !data.pending)
|
|
204
|
+
if (input === "p" && screen === "dashboard" && project && !data.pending)
|
|
164
205
|
void data.action(project.paused ? "resume" : "pause", project.id);
|
|
165
206
|
const kind = input === "s"
|
|
166
207
|
? "stop"
|
|
@@ -187,17 +228,33 @@ export function Monitor({ source, size, }) {
|
|
|
187
228
|
} }));
|
|
188
229
|
if (logs)
|
|
189
230
|
return (_jsx(LogViewer, { ...logs, columns: columns, rows: rows, onBack: () => setLogs(undefined) }));
|
|
190
|
-
const
|
|
231
|
+
const freshness = data.lastUpdated
|
|
232
|
+
? `refreshed ${Math.max(0, Math.floor((now - data.lastUpdated) / statusRefreshIntervalMs))}s ago`
|
|
233
|
+
: "freshness unavailable";
|
|
234
|
+
const status = `${data.connection} · ${freshness}`;
|
|
191
235
|
const sessionIndex = Math.max(0, sessions.findIndex((item) => item.id === session?.id));
|
|
192
236
|
const sessionLines = sessions.length
|
|
193
237
|
? sessions
|
|
194
238
|
.slice(Math.max(0, sessionIndex - 1), sessionIndex + 3)
|
|
195
239
|
.map((item) => `${item.id === session?.id ? ">" : " "} ${item.step} · invocation ${item.attempt} · ${item.outcome}`)
|
|
196
240
|
: ["No agent sessions recorded"];
|
|
241
|
+
const detailsLogControl = detailLogSession
|
|
242
|
+
? columns < 120
|
|
243
|
+
? "l current log"
|
|
244
|
+
: `l log: ${detailLogSession.step} invocation ${detailLogSession.attempt} (${detailLogSession.outcome})`
|
|
245
|
+
: "";
|
|
197
246
|
const controls = [
|
|
198
|
-
sessions.length ? "l log" : "",
|
|
199
|
-
run?.validation?.length
|
|
200
|
-
|
|
247
|
+
screen === "details" ? detailsLogControl : sessions.length ? "l log" : "",
|
|
248
|
+
run?.validation?.length
|
|
249
|
+
? columns < 120
|
|
250
|
+
? "v checks"
|
|
251
|
+
: "v validation"
|
|
252
|
+
: "",
|
|
253
|
+
screen === "dashboard" && project
|
|
254
|
+
? project.paused
|
|
255
|
+
? "p resume"
|
|
256
|
+
: "p pause"
|
|
257
|
+
: "",
|
|
201
258
|
available.stop ? "s stop" : "",
|
|
202
259
|
available.retry ? "r retry" : "",
|
|
203
260
|
available.recover ? "c recover" : "",
|
|
@@ -208,15 +265,20 @@ export function Monitor({ source, size, }) {
|
|
|
208
265
|
.join(" · ");
|
|
209
266
|
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colorFor("running"), wrap: "truncate", children: cells(`Agent Workflows · ${screen === "dashboard" ? "Monitor" : "Run details"}`, columns) }), _jsx(Text, { wrap: "truncate", children: cells(project
|
|
210
267
|
? `${project.id} · intake ${project.paused ? "paused" : "enabled"} · ${projectRuns.filter((item) => item.outcome === "queued").length} queued${project.blocked ? ` · BLOCKED: ${project.blocked}` : ""}`
|
|
211
|
-
: "No projects registered. Start the runner to populate this view.", columns) }), _jsx(Box, { height: height, flexDirection: "row", overflow: "hidden", children: screen === "details" ? (_jsxs(Box, { borderStyle: "round", width: columns, height: height, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children:
|
|
212
|
-
?
|
|
213
|
-
:
|
|
268
|
+
: "No projects registered. Start the runner to populate this view.", columns) }), _jsx(Box, { height: height, flexDirection: "row", overflow: "hidden", children: screen === "details" ? (_jsxs(Box, { borderStyle: "round", width: columns, height: height, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: wrappedDetailLines.length > layout.details.height
|
|
269
|
+
? `Details · lines ${detailOffset + 1}–${Math.min(detailOffset + layout.details.height, wrappedDetailLines.length)} of ${wrappedDetailLines.length}`
|
|
270
|
+
: "Details" }), _jsx(Lines, { lines: detailDocument, width: layout.details.width, height: layout.details.height, offset: detailOffset, outcome: run?.outcome })] })) : (_jsxs(_Fragment, { children: [(wide || focus === "runs") && (_jsxs(Box, { width: paneWidth, height: height, borderStyle: "round", borderColor: focus === "runs" ? colorFor("running") : undefined, flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, children: [focus === "runs" ? "> " : "", "Runs \u00B7 ", projectRuns.length] }), _jsx(RunList, { runs: projectRuns, selected: run?.id, width: layout.runs.width, height: layout.runs.height, now: now })] })), (wide || focus !== "runs") && (_jsxs(Box, { width: summaryWidth, height: height, flexDirection: "column", children: [(wide || focus === "summary") && (_jsxs(Box, { width: summaryWidth, height: layout.summaryPanelHeight, borderStyle: "round", borderColor: focus === "summary" ? colorFor("running") : undefined, paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [focus === "summary" ? "> " : "", "Summary \u00B7 Enter details"] }), _jsx(Lines, { lines: run
|
|
214
271
|
? summaryLines(run, now, event)
|
|
215
|
-
: ["Select a run to inspect progress"], width: layout.summary.width, height: layout.summary.height, offset: offset })] })), (wide || focus === "sessions") && (_jsxs(Box, { width: summaryWidth, height: layout.sessionsPanelHeight, borderStyle: "round", borderColor: focus === "sessions" ? colorFor("running") : undefined, paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [focus === "sessions" ? "> " : "", "Agent sessions \u00B7", " ", sessions.length, " \u00B7 l log"] }), _jsx(Lines, { lines: sessionLines, width: layout.sessions.width, height: layout.sessions.height })] }))] }))] })) }), _jsx(Text, { wrap: "truncate", color: data.connection.startsWith("Connection error")
|
|
272
|
+
: ["Select a run to inspect progress"], width: layout.summary.width, height: layout.summary.height, offset: offset })] })), (wide || focus === "sessions") && (_jsxs(Box, { width: summaryWidth, height: layout.sessionsPanelHeight, borderStyle: "round", borderColor: focus === "sessions" ? colorFor("running") : undefined, paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [focus === "sessions" ? "> " : "", "Agent sessions \u00B7", " ", sessions.length, " \u00B7 l log"] }), _jsx(Lines, { lines: sessionLines, width: layout.sessions.width, height: layout.sessions.height })] }))] }))] })) }), showNotificationNotice && (_jsx(Text, { color: colorFor("running"), wrap: "truncate", children: cells("Notifications enabled; delivery is best-effort and depends on terminal settings", columns) })), _jsx(Text, { wrap: "truncate", color: data.connection.startsWith("Connection error")
|
|
216
273
|
? colorFor("failed")
|
|
217
|
-
: undefined, children: cells(data.message
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
274
|
+
: undefined, children: cells(`${data.message ? `${data.message} · ` : ""}${columns < 120 ? status.replace("Database ", "DB ") : status} · runner liveness unverified · closing leaves workflows running`, columns) }), _jsx(Text, { color: colorFor("running"), wrap: "truncate", children: cells([
|
|
275
|
+
screen === "dashboard"
|
|
276
|
+
? "Tab pane · ↑↓ select/scroll · ←→ project · Enter details"
|
|
277
|
+
: columns < 120
|
|
278
|
+
? "↑↓/Pg · Esc"
|
|
279
|
+
: "↑↓ scroll · PgUp/PgDn page · Esc back",
|
|
280
|
+
controls,
|
|
281
|
+
]
|
|
282
|
+
.filter(Boolean)
|
|
283
|
+
.join(" · "), columns) })] }));
|
|
222
284
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RunRecord } from "../domain.js";
|
|
2
|
+
declare const terminalOutcomes: readonly ["completed", "failed", "blocked", "cancelled", "no-change", "ineligible"];
|
|
3
|
+
export type TerminalOutcome = (typeof terminalOutcomes)[number];
|
|
4
|
+
export interface ExecutionNotification {
|
|
5
|
+
project: string;
|
|
6
|
+
issue: string;
|
|
7
|
+
outcome: TerminalOutcome;
|
|
8
|
+
}
|
|
9
|
+
export interface ExecutionNotificationWriter {
|
|
10
|
+
notify(notification: ExecutionNotification): void;
|
|
11
|
+
}
|
|
12
|
+
export declare class ExecutionNotificationObserver {
|
|
13
|
+
private readonly writer;
|
|
14
|
+
private seeded;
|
|
15
|
+
private readonly observedTerminalExecutionIds;
|
|
16
|
+
constructor(writer: ExecutionNotificationWriter);
|
|
17
|
+
observe(runs: RunRecord[]): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function createTerminalNotificationWriter({ write, tmux, }?: {
|
|
20
|
+
write?: (value: string) => void | Promise<void>;
|
|
21
|
+
tmux?: boolean;
|
|
22
|
+
}): ExecutionNotificationWriter;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { write as writeFileDescriptor } from "node:fs";
|
|
2
|
+
const terminalOutcomes = [
|
|
3
|
+
"completed",
|
|
4
|
+
"failed",
|
|
5
|
+
"blocked",
|
|
6
|
+
"cancelled",
|
|
7
|
+
"no-change",
|
|
8
|
+
"ineligible",
|
|
9
|
+
];
|
|
10
|
+
const terminalOutcomeSet = new Set(terminalOutcomes);
|
|
11
|
+
const whitespace = /\s+/gu;
|
|
12
|
+
const maximumPayloadBytes = 256;
|
|
13
|
+
const notificationPrefix = "agent-workflows: ";
|
|
14
|
+
export class ExecutionNotificationObserver {
|
|
15
|
+
writer;
|
|
16
|
+
seeded = false;
|
|
17
|
+
observedTerminalExecutionIds = new Set();
|
|
18
|
+
constructor(writer) {
|
|
19
|
+
this.writer = writer;
|
|
20
|
+
}
|
|
21
|
+
observe(runs) {
|
|
22
|
+
for (const run of runs) {
|
|
23
|
+
for (const execution of run.executions ?? []) {
|
|
24
|
+
if (!this.seeded) {
|
|
25
|
+
if (isTerminal(execution.outcome))
|
|
26
|
+
this.observedTerminalExecutionIds.add(execution.id);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (this.observedTerminalExecutionIds.has(execution.id) ||
|
|
30
|
+
!isTerminal(execution.outcome))
|
|
31
|
+
continue;
|
|
32
|
+
this.observedTerminalExecutionIds.add(execution.id);
|
|
33
|
+
try {
|
|
34
|
+
this.writer.notify({
|
|
35
|
+
project: run.projectId,
|
|
36
|
+
issue: `#${run.issue.number}`,
|
|
37
|
+
outcome: execution.outcome,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Execution notifications are advisory and never affect monitoring.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
this.seeded = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function createTerminalNotificationWriter({ write = writeTerminal, tmux = Boolean(process.env.TMUX), } = {}) {
|
|
49
|
+
return {
|
|
50
|
+
notify(notification) {
|
|
51
|
+
const suffix = ` · ${sanitize(notification.issue)} · ${notification.outcome}`;
|
|
52
|
+
const projectBytes = Math.max(0, maximumPayloadBytes -
|
|
53
|
+
Buffer.byteLength(notificationPrefix + suffix, "utf8"));
|
|
54
|
+
const message = `${notificationPrefix}${truncateUtf8(sanitize(notification.project), projectBytes)}${suffix}`;
|
|
55
|
+
const osc = `\u001b]9;${message}\u001b\\`;
|
|
56
|
+
const sequence = tmux
|
|
57
|
+
? `\u001bPtmux;${osc.replaceAll("\u001b", "\u001b\u001b")}\u001b\\`
|
|
58
|
+
: osc;
|
|
59
|
+
try {
|
|
60
|
+
const pending = write(sequence);
|
|
61
|
+
if (pending)
|
|
62
|
+
void pending.catch(() => undefined);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Execution notifications are advisory and never affect monitoring.
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function isTerminal(outcome) {
|
|
71
|
+
return terminalOutcomeSet.has(outcome);
|
|
72
|
+
}
|
|
73
|
+
function sanitize(value) {
|
|
74
|
+
return [...value]
|
|
75
|
+
.filter((character) => {
|
|
76
|
+
const codePoint = character.codePointAt(0);
|
|
77
|
+
return !(codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f));
|
|
78
|
+
})
|
|
79
|
+
.join("")
|
|
80
|
+
.replace(whitespace, " ")
|
|
81
|
+
.trim();
|
|
82
|
+
}
|
|
83
|
+
function writeTerminal(value) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
writeFileDescriptor(process.stdout.fd, value, (error) => {
|
|
86
|
+
if (error)
|
|
87
|
+
reject(error);
|
|
88
|
+
else
|
|
89
|
+
resolve();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function truncateUtf8(value, maximumBytes) {
|
|
94
|
+
let result = "";
|
|
95
|
+
let bytes = 0;
|
|
96
|
+
for (const character of value) {
|
|
97
|
+
const characterBytes = Buffer.byteLength(character, "utf8");
|
|
98
|
+
if (bytes + characterBytes > maximumBytes)
|
|
99
|
+
break;
|
|
100
|
+
result += character;
|
|
101
|
+
bytes += characterBytes;
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
package/dist/src/tui/views.d.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { RunRecord } from "../domain.js";
|
|
2
2
|
import type { EventRecord, InvocationRecord } from "../store.js";
|
|
3
|
+
type AvailableActions = {
|
|
4
|
+
stop: boolean;
|
|
5
|
+
retry: boolean;
|
|
6
|
+
recover: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function wrapDetailLines(lines: string[], width: number): string[];
|
|
3
9
|
export declare function summaryLines(run: RunRecord, now: number, event?: EventRecord): string[];
|
|
4
|
-
export declare function detailLines(run: RunRecord, sessions: InvocationRecord[], now: number, recoveryReason?: string | undefined): string[];
|
|
5
|
-
export declare function Lines({ lines, width, height, offset, }: {
|
|
10
|
+
export declare function detailLines(run: RunRecord, sessions: InvocationRecord[], now: number, recoveryReason?: string | undefined, width?: number, available?: AvailableActions): string[];
|
|
11
|
+
export declare function Lines({ lines, width, height, offset, outcome, }: {
|
|
6
12
|
lines: string[];
|
|
7
13
|
width: number;
|
|
8
14
|
height: number;
|
|
9
15
|
offset?: number;
|
|
16
|
+
outcome?: string;
|
|
10
17
|
}): import("react").JSX.Element;
|
|
11
18
|
export declare function RunList({ runs, selected, width, height, now, }: {
|
|
12
19
|
runs: RunRecord[];
|
|
@@ -15,3 +22,4 @@ export declare function RunList({ runs, selected, width, height, now, }: {
|
|
|
15
22
|
height: number;
|
|
16
23
|
now: number;
|
|
17
24
|
}): import("react").JSX.Element;
|
|
25
|
+
export {};
|