@mingchuno/agent-workflows 0.1.0 → 0.3.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.
Files changed (76) hide show
  1. package/README.md +37 -6
  2. package/dist/src/adapters/agents.js +6 -3
  3. package/dist/src/adapters/hosting.js +16 -10
  4. package/dist/src/adapters/sdk-protocol.d.ts +3 -3
  5. package/dist/src/adapters/sdk-protocol.js +9 -7
  6. package/dist/src/attribution.d.ts +9 -0
  7. package/dist/src/attribution.js +57 -0
  8. package/dist/src/cli.d.ts +1 -1
  9. package/dist/src/cli.js +80 -25
  10. package/dist/src/config.d.ts +24 -30
  11. package/dist/src/config.js +32 -27
  12. package/dist/src/defaults.d.ts +2 -0
  13. package/dist/src/defaults.js +2 -0
  14. package/dist/src/domain.d.ts +31 -4
  15. package/dist/src/domain.js +10 -3
  16. package/dist/src/evidence.d.ts +54 -0
  17. package/dist/src/evidence.js +214 -0
  18. package/dist/src/index.d.ts +1 -0
  19. package/dist/src/index.js +1 -0
  20. package/dist/src/invocation.d.ts +25 -0
  21. package/dist/src/invocation.js +166 -0
  22. package/dist/src/operations.d.ts +8 -2
  23. package/dist/src/operations.js +100 -136
  24. package/dist/src/prompts.d.ts +28 -0
  25. package/dist/src/prompts.js +63 -0
  26. package/dist/src/recovery.d.ts +19 -0
  27. package/dist/src/recovery.js +99 -0
  28. package/dist/src/runner.d.ts +7 -0
  29. package/dist/src/runner.js +170 -18
  30. package/dist/src/runtime/process.d.ts +2 -0
  31. package/dist/src/runtime/process.js +41 -12
  32. package/dist/src/store.d.ts +21 -2
  33. package/dist/src/store.js +122 -1
  34. package/dist/src/tui/actions.d.ts +16 -0
  35. package/dist/src/tui/actions.js +23 -0
  36. package/dist/src/tui/constants.d.ts +6 -0
  37. package/dist/src/tui/constants.js +3 -0
  38. package/dist/src/{tui-data.d.ts → tui/data.d.ts} +10 -7
  39. package/dist/src/tui/data.js +146 -0
  40. package/dist/src/tui/dialogs.d.ts +17 -0
  41. package/dist/src/tui/dialogs.js +149 -0
  42. package/dist/src/tui/format.d.ts +7 -0
  43. package/dist/src/tui/format.js +62 -0
  44. package/dist/src/tui/index.d.ts +3 -0
  45. package/dist/src/tui/index.js +2 -0
  46. package/dist/src/tui/layout.d.ts +25 -0
  47. package/dist/src/tui/layout.js +36 -0
  48. package/dist/src/tui/log-file.d.ts +26 -0
  49. package/dist/src/tui/log-file.js +156 -0
  50. package/dist/src/tui/log.d.ts +11 -0
  51. package/dist/src/tui/log.js +90 -0
  52. package/dist/src/tui/monitor.d.ts +10 -0
  53. package/dist/src/tui/monitor.js +284 -0
  54. package/dist/src/tui/notifications.d.ts +23 -0
  55. package/dist/src/tui/notifications.js +104 -0
  56. package/dist/src/tui/text.d.ts +3 -0
  57. package/dist/src/tui/text.js +10 -0
  58. package/dist/src/tui/use-log-controller.d.ts +27 -0
  59. package/dist/src/tui/use-log-controller.js +192 -0
  60. package/dist/src/tui/views.d.ts +25 -0
  61. package/dist/src/tui/views.js +327 -0
  62. package/dist/src/workspace.js +21 -8
  63. package/docs/api.md +132 -8
  64. package/docs/architecture.md +21 -4
  65. package/docs/configuration.md +181 -8
  66. package/docs/database.md +7 -0
  67. package/docs/operations.md +160 -5
  68. package/docs/providers.md +58 -2
  69. package/docs/releases.md +34 -79
  70. package/examples/config.ts +6 -6
  71. package/examples/run.ts +4 -1
  72. package/package.json +4 -2
  73. package/dist/src/tui-data.js +0 -89
  74. package/dist/src/tui.d.ts +0 -5
  75. package/dist/src/tui.js +0 -69
  76. package/docs/acceptance.md +0 -35
@@ -0,0 +1,156 @@
1
+ import { open, stat } from "node:fs/promises";
2
+ import { matchIndex, terminalText } from "./text.js";
3
+ const chunkSize = 64 * 1024;
4
+ const checkpointInterval = 256;
5
+ const lineFeedByte = 0x0a;
6
+ export function presentLogLine(line, raw) {
7
+ if (raw)
8
+ return terminalText(line);
9
+ try {
10
+ const event = JSON.parse(line);
11
+ const body = event.item ?? event.data ?? event;
12
+ const label = body.type ?? event.type ?? "event";
13
+ const content = body.text ??
14
+ body.content ??
15
+ body.deltaContent ??
16
+ body.message ??
17
+ body.command;
18
+ const output = body.aggregated_output ?? body.output;
19
+ if (typeof body.toolName === "string")
20
+ return terminalText(`${event.type ?? "tool"}: ${body.toolName} ${JSON.stringify(body.arguments ?? body.result ?? "")}`);
21
+ if (typeof content === "string")
22
+ return terminalText(`${label}: ${content}${typeof output === "string" ? ` | ${output}` : ""}`).replaceAll("\n", " ↵ ");
23
+ }
24
+ catch {
25
+ // Validation output and incomplete JSON records remain readable as text.
26
+ }
27
+ return terminalText(line);
28
+ }
29
+ /** Sparse byte offsets index the file; only requested pages hold decoded text. */
30
+ export class LogFile {
31
+ path;
32
+ checkpoints = [0];
33
+ size = 0;
34
+ completeLines = 0;
35
+ tail = false;
36
+ identity = "";
37
+ modified = 0;
38
+ revision = 0;
39
+ refreshTask = Promise.resolve();
40
+ constructor(path) {
41
+ this.path = path;
42
+ }
43
+ get count() {
44
+ return this.completeLines + Number(this.tail);
45
+ }
46
+ refresh(signal) {
47
+ const task = this.refreshTask.then(() => this.updateIndex(signal));
48
+ this.refreshTask = task.catch(() => { });
49
+ return task;
50
+ }
51
+ async updateIndex(signal) {
52
+ signal.throwIfAborted();
53
+ const metadata = await stat(this.path);
54
+ const identity = `${metadata.dev}:${metadata.ino}`;
55
+ if (identity !== this.identity ||
56
+ metadata.size < this.size ||
57
+ (metadata.size === this.size && metadata.mtimeMs !== this.modified)) {
58
+ this.checkpoints = [0];
59
+ this.size = 0;
60
+ this.completeLines = 0;
61
+ this.tail = false;
62
+ this.revision++;
63
+ }
64
+ this.identity = identity;
65
+ this.modified = metadata.mtimeMs;
66
+ const file = await open(this.path, "r");
67
+ try {
68
+ const buffer = Buffer.alloc(chunkSize);
69
+ while (this.size < metadata.size) {
70
+ signal.throwIfAborted();
71
+ const { bytesRead } = await file.read(buffer, 0, Math.min(chunkSize, metadata.size - this.size), this.size);
72
+ signal.throwIfAborted();
73
+ if (!bytesRead)
74
+ break;
75
+ for (let i = 0; i < bytesRead; i++) {
76
+ this.tail = buffer[i] !== lineFeedByte;
77
+ if (buffer[i] === lineFeedByte) {
78
+ this.completeLines++;
79
+ if (this.completeLines % checkpointInterval === 0)
80
+ this.checkpoints[this.completeLines / checkpointInterval] =
81
+ this.size + i + 1;
82
+ }
83
+ }
84
+ this.size += bytesRead;
85
+ }
86
+ }
87
+ finally {
88
+ await file.close();
89
+ }
90
+ }
91
+ async *lines(start, signal) {
92
+ const checkpoint = Math.floor(Math.max(0, start) / checkpointInterval);
93
+ let position = this.checkpoints[checkpoint] ?? this.size;
94
+ let number = checkpoint * checkpointInterval;
95
+ const file = await open(this.path, "r");
96
+ try {
97
+ const buffer = Buffer.alloc(chunkSize);
98
+ let parts = [];
99
+ while (position < this.size) {
100
+ signal.throwIfAborted();
101
+ const { bytesRead } = await file.read(buffer, 0, Math.min(chunkSize, this.size - position), position);
102
+ if (!bytesRead)
103
+ break;
104
+ let beginning = 0;
105
+ for (let i = 0; i < bytesRead; i++) {
106
+ if (buffer[i] !== lineFeedByte)
107
+ continue;
108
+ parts.push(Buffer.from(buffer.subarray(beginning, i)));
109
+ if (number >= start)
110
+ yield {
111
+ number,
112
+ text: Buffer.concat(parts).toString("utf8").replace(/\r$/, ""),
113
+ };
114
+ number++;
115
+ parts = [];
116
+ beginning = i + 1;
117
+ }
118
+ if (beginning < bytesRead)
119
+ parts.push(Buffer.from(buffer.subarray(beginning, bytesRead)));
120
+ position += bytesRead;
121
+ }
122
+ if (parts.length && number >= start)
123
+ yield { number, text: Buffer.concat(parts).toString("utf8") };
124
+ }
125
+ finally {
126
+ await file.close();
127
+ }
128
+ }
129
+ async page(start, count, signal) {
130
+ const result = [];
131
+ for await (const line of this.lines(start, signal)) {
132
+ result.push(line.text);
133
+ if (result.length >= count)
134
+ break;
135
+ }
136
+ return result;
137
+ }
138
+ async search(options) {
139
+ const { query, from, direction, raw, signal } = options;
140
+ let first;
141
+ let last;
142
+ let previous;
143
+ for await (const line of this.lines(0, signal)) {
144
+ const text = presentLogLine(line.text, raw);
145
+ if (matchIndex(text, query) < 0)
146
+ continue;
147
+ first ??= line.number;
148
+ last = line.number;
149
+ if (line.number < from)
150
+ previous = line.number;
151
+ if (direction === 1 && line.number > from)
152
+ return line.number;
153
+ }
154
+ return direction === 1 ? first : (previous ?? last);
155
+ }
156
+ }
@@ -0,0 +1,11 @@
1
+ export interface LogSource {
2
+ path: string;
3
+ label: string;
4
+ }
5
+ export declare function LogViewer({ sources, initial, columns, rows, onBack, }: {
6
+ sources: LogSource[];
7
+ initial: number;
8
+ columns: number;
9
+ rows: number;
10
+ onBack: () => void;
11
+ }): import("react").JSX.Element;
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { useState } from "react";
4
+ import { HelpDialog } from "./dialogs.js";
5
+ import { cells, colorFor } from "./format.js";
6
+ import { presentLogLine } from "./log-file.js";
7
+ import { matchIndex } from "./text.js";
8
+ import { useLogController } from "./use-log-controller.js";
9
+ const horizontalPanColumns = 20;
10
+ export function LogViewer({ sources, initial, columns, rows, onBack, }) {
11
+ const [index, setIndex] = useState(initial);
12
+ const source = sources[index] ?? sources[0];
13
+ if (!source)
14
+ return _jsx(Text, { children: "No logs recorded. Esc back" });
15
+ return (_jsx(LogScreen, { source: source, columns: columns, rows: rows, onBack: onBack, onNext: () => setIndex((value) => (value + 1) % sources.length), multiple: sources.length > 1 }, source.path));
16
+ }
17
+ function Highlight({ text, query }) {
18
+ const at = query ? matchIndex(text, query) : -1;
19
+ if (at < 0)
20
+ return _jsx(Text, { children: text });
21
+ return (_jsxs(Text, { children: [text.slice(0, at), _jsx(Text, { inverse: true, bold: true, children: text.slice(at, at + query.length) }), text.slice(at + query.length)] }));
22
+ }
23
+ function LogScreen({ source, columns, rows, onBack, onNext, multiple, }) {
24
+ const log = useLogController(source.path, columns, rows);
25
+ const { height, following, horizontal, raw, lines, total, message, editing, draft, query, searching, position, } = log;
26
+ const [help, setHelp] = useState(false);
27
+ useInput((input, key) => {
28
+ if (key.ctrl || key.meta || key.eventType === "release")
29
+ return;
30
+ if (help)
31
+ return;
32
+ if (key.escape) {
33
+ if (!log.dismissSearch())
34
+ onBack();
35
+ return;
36
+ }
37
+ if (editing) {
38
+ if (key.return)
39
+ log.applySearch();
40
+ else if (key.backspace || key.delete)
41
+ log.eraseDraft();
42
+ else if (!key.ctrl &&
43
+ !key.meta &&
44
+ !key.upArrow &&
45
+ !key.downArrow &&
46
+ !key.leftArrow &&
47
+ !key.rightArrow &&
48
+ !key.tab)
49
+ log.appendDraft(input);
50
+ return;
51
+ }
52
+ if (input === "/")
53
+ log.beginSearch();
54
+ else if (input === "n" || input === "N")
55
+ log.repeatSearch(input === "n" ? 1 : -1);
56
+ else if (input === "f")
57
+ log.follow();
58
+ else if (input === "R")
59
+ log.toggleRaw();
60
+ else if (key.tab && multiple)
61
+ onNext();
62
+ else if (input === "?")
63
+ setHelp(true);
64
+ else if (key.upArrow || input === "k")
65
+ log.scroll(-1);
66
+ else if (key.downArrow || input === "j")
67
+ log.scroll(1);
68
+ else if (key.pageUp)
69
+ log.scroll(-height);
70
+ else if (key.pageDown || input === " ")
71
+ log.scroll(height);
72
+ else if (key.home || input === "g")
73
+ log.firstPage();
74
+ else if (key.end || input === "G")
75
+ log.lastPage();
76
+ else if (key.leftArrow)
77
+ log.pan(-horizontalPanColumns);
78
+ else if (key.rightArrow)
79
+ log.pan(horizontalPanColumns);
80
+ });
81
+ if (help)
82
+ return (_jsx(HelpDialog, { columns: columns, rows: rows, initialPage: 2, onClose: () => setHelp(false) }));
83
+ return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colorFor("running"), wrap: "truncate", children: cells(`Logs · ${source.label}`, columns) }), _jsx(Text, { wrap: "truncate", children: cells(source.path, columns) }), _jsxs(Text, { children: [following ? "LIVE FOLLOW" : "SCROLLING", " \u00B7 ", raw ? "Raw" : "Readable", " \u00B7 lines ", total ? position + 1 : 0, "\u2013", Math.min(total, position + height), "/", total, searching ? " · Searching…" : ""] }), _jsx(Box, { height: height, flexDirection: "column", overflow: "hidden", children: lines.map((line, index) => (_jsx(Text, { wrap: "truncate", children: _jsx(Highlight, { text: cells(presentLogLine(line, raw), columns, horizontal), query: query }) }, `${position + index}`))) }), _jsx(Text, { wrap: "truncate", children: cells(editing
84
+ ? `/${draft}▏`
85
+ : query
86
+ ? `Search: ${query}${message ? ` · ${message}` : ""}`
87
+ : message, columns) }), _jsx(Text, { color: colorFor("running"), wrap: "truncate", children: editing
88
+ ? "Enter search · Esc cancel"
89
+ : `/ search · n/N match · f follow · R ${raw ? "readable" : "raw"} · ${multiple ? "Tab log · " : ""}Esc back · ? help` }), _jsx(Text, { dimColor: true, wrap: "truncate", children: cells("↑↓ scroll · PgUp/PgDn page · ←→ pan · workflow continues in background", columns) })] }));
90
+ }
@@ -0,0 +1,10 @@
1
+ import { type MonitorSource } from "./data.js";
2
+ import type { ExecutionNotificationWriter } from "./notifications.js";
3
+ export declare function Monitor({ source, size, notificationWriter, }: {
4
+ source: MonitorSource;
5
+ size?: {
6
+ columns: number;
7
+ rows: number;
8
+ };
9
+ notificationWriter?: ExecutionNotificationWriter;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,284 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useApp, useInput, useWindowSize } from "ink";
3
+ import { useEffect, useState } from "react";
4
+ import { actionAvailability } from "./actions.js";
5
+ import { minimumTerminalSize } from "./constants.js";
6
+ import { useMonitorData } from "./data.js";
7
+ import { ConfirmDialog, HelpDialog } from "./dialogs.js";
8
+ import { cells, colorFor, wrapLines } from "./format.js";
9
+ import { monitorLayout } from "./layout.js";
10
+ import { LogViewer } from "./log.js";
11
+ import { detailLines, Lines, RunList, summaryLines, wrapDetailLines, } from "./views.js";
12
+ const notificationNoticeDurationMs = 2000;
13
+ const focuses = ["runs", "summary", "sessions"];
14
+ const statusRefreshIntervalMs = 1_000;
15
+ const actionDescriptions = {
16
+ stop: "Cancel this run and wait for its active local work to stop.",
17
+ retry: "Create a new run and branch; execute the workflow again.",
18
+ recover: "Continue the failed publication step using completed work.",
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, }) {
44
+ const window = useWindowSize();
45
+ const { columns, rows } = size ?? window;
46
+ const { exit } = useApp();
47
+ const [selection, setSelection] = useState({});
48
+ const data = useMonitorData(source, selection, notificationWriter);
49
+ const { projects, project, projectRuns, run, sessions, events } = data;
50
+ const [focus, setFocus] = useState("runs");
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();
58
+ const [now, setNow] = useState(Date.now());
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");
62
+ const { wide, height, paneWidth, summaryWidth } = layout;
63
+ const session = sessions.find((item) => item.id === sessionId) ?? sessions[0];
64
+ const executionSessions = currentExecutionSessions(run, sessions, events);
65
+ const detailLogSession = latestSession(executionSessions);
66
+ const event = events.find((item) => item.sequence === stepSequence) ?? events.at(-1);
67
+ useEffect(() => {
68
+ const timer = setInterval(() => setNow(Date.now()), statusRefreshIntervalMs);
69
+ return () => clearInterval(timer);
70
+ }, []);
71
+ useEffect(() => {
72
+ // Pin default selections by identity before incoming rows can reorder them.
73
+ setSelection((current) => current.projectId === project?.id && current.runId === run?.id
74
+ ? current
75
+ : { projectId: project?.id, runId: run?.id });
76
+ }, [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
+ 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));
113
+ useInput((input, key) => {
114
+ if (key.ctrl || key.meta || key.eventType === "release")
115
+ return;
116
+ const terminalIsLargeEnough = columns >= minimumTerminalSize.columns &&
117
+ rows >= minimumTerminalSize.rows;
118
+ if (logs && terminalIsLargeEnough)
119
+ return;
120
+ if (terminalIsLargeEnough && (confirmation || helpOpen))
121
+ return;
122
+ if (input === "q") {
123
+ exit();
124
+ return;
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
+ });
219
+ });
220
+ if (columns < minimumTerminalSize.columns || rows < minimumTerminalSize.rows)
221
+ 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 (helpOpen)
223
+ return (_jsx(HelpDialog, { columns: columns, rows: rows, onClose: () => setHelpOpen(false) }));
224
+ 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 && available[confirmation.kind], onCancel: () => setConfirmation(undefined), onConfirm: () => {
226
+ void data.action(confirmation.kind, confirmation.runId);
227
+ setConfirmation(undefined);
228
+ } }));
229
+ if (logs)
230
+ return (_jsx(LogViewer, { ...logs, columns: columns, rows: rows, onBack: () => setLogs(undefined) }));
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}`;
235
+ const sessionIndex = Math.max(0, sessions.findIndex((item) => item.id === session?.id));
236
+ const sessionLines = sessions.length
237
+ ? sessions
238
+ .slice(Math.max(0, sessionIndex - 1), sessionIndex + 3)
239
+ .map((item) => `${item.id === session?.id ? ">" : " "} ${item.step} · invocation ${item.attempt} · ${item.outcome}`)
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
+ : "";
246
+ const controls = [
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
+ : "",
258
+ available.stop ? "s stop" : "",
259
+ available.retry ? "r retry" : "",
260
+ available.recover ? "c recover" : "",
261
+ "? help",
262
+ "q close",
263
+ ]
264
+ .filter(Boolean)
265
+ .join(" · ");
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
267
+ ? `${project.id} · intake ${project.paused ? "paused" : "enabled"} · ${projectRuns.filter((item) => item.outcome === "queued").length} queued${project.blocked ? ` · BLOCKED: ${project.blocked}` : ""}`
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
271
+ ? summaryLines(run, now, event)
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")
273
+ ? colorFor("failed")
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) })] }));
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 {};