@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.
@@ -1,2 +1,3 @@
1
1
  export type { MonitorSource } from "./data.js";
2
2
  export { Monitor } from "./monitor.js";
3
+ export { createTerminalNotificationWriter, type ExecutionNotification, type ExecutionNotificationWriter, } from "./notifications.js";
@@ -1 +1,2 @@
1
1
  export { Monitor } from "./monitor.js";
2
+ export { createTerminalNotificationWriter, } from "./notifications.js";
@@ -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;
@@ -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
- export declare function Monitor({ source, size, }: {
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;
@@ -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
- export function Monitor({ source, size, }) {
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 layout = monitorLayout(columns, rows);
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 (!sessions.length)
91
+ const openAgentLog = (candidates = sessions, initialSession = session) => {
92
+ if (!candidates.length)
65
93
  return;
66
94
  setLogs({
67
- sources: sessions.map((item) => ({
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, sessions.findIndex((item) => item.id === session?.id)),
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
- openAgentLog();
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
- ? detailLines(run, sessions, now, recoveryReason)
177
+ ? detailDocument
143
178
  : summaryLines(run, now, event)
144
179
  : [];
145
180
  const viewport = screen === "details" ? layout.details : layout.summary;
146
- setOffset((value) => Math.max(0, Math.min(wrapLines(lines, viewport.width).length - viewport.height, value +
147
- delta *
148
- (key.pageUp || key.pageDown ? layout.details.height : 1))));
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 status = `${data.connection}${data.lastUpdated ? ` · refreshed ${Math.max(0, Math.floor((now - data.lastUpdated) / statusRefreshIntervalMs))}s ago` : ""}`;
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 ? "v validation" : "",
200
- project ? (project.paused ? "p resume" : "p pause") : "",
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: "Details \u00B7 \u2191\u2193 scroll \u00B7 Esc back" }), _jsx(Lines, { lines: run
212
- ? detailLines(run, sessions, now, recoveryReason)
213
- : ["Run no longer available"], width: layout.details.width, height: layout.details.height, offset: offset })] })) : (_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
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 || status, columns) }), _jsx(Text, { dimColor: true, wrap: "truncate", children: cells(data.message
218
- ? status
219
- : "Runner liveness unverified · closing monitor leaves workflows running", columns) }), _jsx(Text, { color: colorFor("running"), wrap: "truncate", children: cells(screen === "dashboard"
220
- ? "Tab pane · ↑↓ select/scroll · ←→ project · Enter details"
221
- : "↑↓ scroll · PgUp/PgDn page · Esc back", columns) }), _jsx(Text, { wrap: "truncate", children: cells(controls, columns) })] }));
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
+ }
@@ -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 {};