@mingchuno/agent-workflows 0.1.0 → 0.2.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 (70) hide show
  1. package/README.md +32 -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/cli.d.ts +1 -1
  7. package/dist/src/cli.js +40 -19
  8. package/dist/src/config.d.ts +22 -22
  9. package/dist/src/config.js +31 -26
  10. package/dist/src/defaults.d.ts +2 -0
  11. package/dist/src/defaults.js +2 -0
  12. package/dist/src/domain.d.ts +24 -4
  13. package/dist/src/domain.js +10 -3
  14. package/dist/src/evidence.d.ts +54 -0
  15. package/dist/src/evidence.js +214 -0
  16. package/dist/src/index.d.ts +1 -0
  17. package/dist/src/index.js +1 -0
  18. package/dist/src/invocation.d.ts +24 -0
  19. package/dist/src/invocation.js +163 -0
  20. package/dist/src/operations.d.ts +7 -2
  21. package/dist/src/operations.js +76 -134
  22. package/dist/src/prompts.d.ts +28 -0
  23. package/dist/src/prompts.js +63 -0
  24. package/dist/src/recovery.d.ts +19 -0
  25. package/dist/src/recovery.js +99 -0
  26. package/dist/src/runner.d.ts +5 -0
  27. package/dist/src/runner.js +145 -18
  28. package/dist/src/runtime/process.d.ts +2 -0
  29. package/dist/src/runtime/process.js +41 -12
  30. package/dist/src/store.d.ts +21 -2
  31. package/dist/src/store.js +122 -1
  32. package/dist/src/tui/actions.d.ts +16 -0
  33. package/dist/src/tui/actions.js +23 -0
  34. package/dist/src/tui/constants.d.ts +6 -0
  35. package/dist/src/tui/constants.js +3 -0
  36. package/dist/src/{tui-data.d.ts → tui/data.d.ts} +8 -6
  37. package/dist/src/tui/data.js +141 -0
  38. package/dist/src/tui/dialogs.d.ts +17 -0
  39. package/dist/src/tui/dialogs.js +149 -0
  40. package/dist/src/tui/format.d.ts +7 -0
  41. package/dist/src/tui/format.js +62 -0
  42. package/dist/src/tui/index.d.ts +2 -0
  43. package/dist/src/tui/index.js +1 -0
  44. package/dist/src/tui/layout.d.ts +25 -0
  45. package/dist/src/tui/layout.js +36 -0
  46. package/dist/src/tui/log-file.d.ts +26 -0
  47. package/dist/src/tui/log-file.js +156 -0
  48. package/dist/src/tui/log.d.ts +11 -0
  49. package/dist/src/tui/log.js +90 -0
  50. package/dist/src/tui/monitor.d.ts +8 -0
  51. package/dist/src/tui/monitor.js +222 -0
  52. package/dist/src/tui/text.d.ts +3 -0
  53. package/dist/src/tui/text.js +10 -0
  54. package/dist/src/tui/use-log-controller.d.ts +27 -0
  55. package/dist/src/tui/use-log-controller.js +192 -0
  56. package/dist/src/tui/views.d.ts +17 -0
  57. package/dist/src/tui/views.js +97 -0
  58. package/docs/api.md +119 -6
  59. package/docs/architecture.md +21 -4
  60. package/docs/configuration.md +137 -5
  61. package/docs/database.md +7 -0
  62. package/docs/operations.md +117 -2
  63. package/docs/providers.md +58 -2
  64. package/docs/releases.md +34 -79
  65. package/examples/config.ts +2 -2
  66. package/package.json +4 -2
  67. package/dist/src/tui-data.js +0 -89
  68. package/dist/src/tui.d.ts +0 -5
  69. package/dist/src/tui.js +0 -69
  70. package/docs/acceptance.md +0 -35
@@ -1,5 +1,5 @@
1
- import type { RunRecord } from "./domain.js";
2
- import type { EventRecord, InvocationRecord, ProjectState, Store } from "./store.js";
1
+ import type { RunRecord } from "../domain.js";
2
+ import type { EventRecord, InvocationRecord, ProjectState, Store } from "../store.js";
3
3
  export interface MonitorSource {
4
4
  projects: Store["projects"];
5
5
  runs: Store["runs"];
@@ -8,9 +8,10 @@ export interface MonitorSource {
8
8
  request: Store["request"];
9
9
  commands: Store["commands"];
10
10
  }
11
+ export type MonitorAction = "pause" | "resume" | "stop" | "retry" | "recover";
11
12
  export declare function useMonitorData(source: MonitorSource, selection: {
12
- projectIndex: number;
13
- runIndex: number;
13
+ projectId?: string;
14
+ runId?: string;
14
15
  }): {
15
16
  projects: ProjectState[];
16
17
  project: ProjectState | undefined;
@@ -18,8 +19,9 @@ export declare function useMonitorData(source: MonitorSource, selection: {
18
19
  run: RunRecord | undefined;
19
20
  sessions: InvocationRecord[];
20
21
  events: EventRecord[];
22
+ connection: string;
23
+ lastUpdated: number | undefined;
21
24
  message: string;
22
- setMessage: import("react").Dispatch<import("react").SetStateAction<string>>;
23
25
  pending: string | undefined;
24
- action: (kind: "pause" | "resume" | "stop" | "retry", target: string) => Promise<void>;
26
+ action: (kind: MonitorAction, target: string) => Promise<void>;
25
27
  };
@@ -0,0 +1,141 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { tuiRefreshIntervalMs } from "./constants.js";
3
+ export function useMonitorData(source, selection) {
4
+ const [projects, setProjects] = useState([]);
5
+ const [runs, setRuns] = useState([]);
6
+ const [detail, setDetail] = useState({ sessions: [], events: [] });
7
+ const [connection, setConnection] = useState("Connecting…");
8
+ const [lastUpdated, setLastUpdated] = useState();
9
+ const [message, setMessage] = useState("");
10
+ const [pending, setPending] = useState();
11
+ const pendingRef = useRef(undefined);
12
+ const mounted = useRef(true);
13
+ const project = projects.find((item) => item.id === selection.projectId) ?? projects[0];
14
+ const projectRuns = runs.filter((item) => item.projectId === project?.id);
15
+ const run = projectRuns.find((item) => item.id === selection.runId) ?? projectRuns[0];
16
+ const selectedRunId = run?.id;
17
+ useEffect(() => {
18
+ mounted.current = true;
19
+ return () => {
20
+ mounted.current = false;
21
+ };
22
+ }, []);
23
+ useEffect(() => {
24
+ let closed = false;
25
+ let busy = false;
26
+ let cursor = 0;
27
+ let history = [];
28
+ const update = async () => {
29
+ if (busy)
30
+ return;
31
+ busy = true;
32
+ try {
33
+ const [nextProjects, nextRuns] = await Promise.all([
34
+ source.projects(),
35
+ source.runs(),
36
+ ]);
37
+ if (closed)
38
+ return;
39
+ setProjects(nextProjects);
40
+ setRuns(nextRuns);
41
+ if (selectedRunId) {
42
+ const [sessions, nextEvents] = await Promise.all([
43
+ source.invocations(selectedRunId),
44
+ source.events(cursor, selectedRunId),
45
+ ]);
46
+ if (closed)
47
+ return;
48
+ const fresh = nextEvents.filter((event) => event.sequence > cursor);
49
+ if (fresh.length)
50
+ cursor = Math.max(...fresh.map((event) => event.sequence));
51
+ history = [
52
+ ...history,
53
+ ...fresh.filter((event) => event.runId === selectedRunId && event.kind === "step"),
54
+ ];
55
+ setDetail({ runId: selectedRunId, sessions, events: history });
56
+ }
57
+ setConnection("Database connected");
58
+ setLastUpdated(Date.now());
59
+ }
60
+ catch (error) {
61
+ if (!closed)
62
+ setConnection(`Connection error: ${String(error)}`);
63
+ }
64
+ finally {
65
+ busy = false;
66
+ }
67
+ };
68
+ void update();
69
+ const timer = setInterval(() => void update(), tuiRefreshIntervalMs);
70
+ return () => {
71
+ closed = true;
72
+ clearInterval(timer);
73
+ };
74
+ }, [source, selectedRunId]);
75
+ useEffect(() => {
76
+ if (!pending || pending === "submitting")
77
+ return;
78
+ let closed = false;
79
+ let busy = false;
80
+ const update = async () => {
81
+ if (busy)
82
+ return;
83
+ busy = true;
84
+ try {
85
+ const command = (await source.commands()).find((item) => item.id === pending);
86
+ if (closed || !command || command.status === "pending")
87
+ return;
88
+ setMessage(`${command.kind}: ${command.status}${command.error ? ` — ${command.error}` : ""}`);
89
+ pendingRef.current = undefined;
90
+ setPending(undefined);
91
+ }
92
+ catch (error) {
93
+ if (!closed)
94
+ setMessage(`Command pending; cannot read acknowledgement: ${String(error)}`);
95
+ }
96
+ finally {
97
+ busy = false;
98
+ }
99
+ };
100
+ void update();
101
+ const timer = setInterval(() => void update(), tuiRefreshIntervalMs);
102
+ return () => {
103
+ closed = true;
104
+ clearInterval(timer);
105
+ };
106
+ }, [source, pending]);
107
+ const action = async (kind, target) => {
108
+ if (pendingRef.current)
109
+ return;
110
+ pendingRef.current = "submitting";
111
+ setPending("submitting");
112
+ setMessage(`${kind}: pending — waiting for runner`);
113
+ try {
114
+ const id = await source.request(kind, target);
115
+ if (!mounted.current)
116
+ return;
117
+ pendingRef.current = id;
118
+ setPending(id);
119
+ }
120
+ catch (error) {
121
+ if (!mounted.current)
122
+ return;
123
+ pendingRef.current = undefined;
124
+ setPending(undefined);
125
+ setMessage(`${kind}: submission failed — ${String(error)}`);
126
+ }
127
+ };
128
+ return {
129
+ projects,
130
+ project,
131
+ projectRuns,
132
+ run,
133
+ sessions: detail.runId === selectedRunId ? detail.sessions : [],
134
+ events: detail.runId === selectedRunId ? detail.events : [],
135
+ connection,
136
+ lastUpdated,
137
+ message,
138
+ pending,
139
+ action,
140
+ };
141
+ }
@@ -0,0 +1,17 @@
1
+ type DialogSize = {
2
+ columns: number;
3
+ rows: number;
4
+ };
5
+ export declare function HelpDialog({ columns, rows, onClose, initialPage, }: DialogSize & {
6
+ onClose: () => void;
7
+ initialPage?: number;
8
+ }): import("react").JSX.Element;
9
+ export declare function ConfirmDialog({ columns, rows, title, subject, description, available, onCancel, onConfirm, }: DialogSize & {
10
+ title: string;
11
+ subject: string;
12
+ description: string;
13
+ available: boolean;
14
+ onCancel: () => void;
15
+ onConfirm: () => void;
16
+ }): import("react").JSX.Element;
17
+ export {};
@@ -0,0 +1,149 @@
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 { colorFor, wrapLines } from "./format.js";
5
+ import { Lines } from "./views.js";
6
+ const dialogHorizontalMargin = 8;
7
+ const dialogVerticalMargin = 4;
8
+ const dialogContentWidthOffset = 6;
9
+ const dialogChromeHeight = 11;
10
+ function Dialog({ columns, rows, width, height, title, children, }) {
11
+ return (_jsx(Box, { width: columns, height: rows, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { width: width, height: height, borderStyle: "round", borderColor: colorFor("running"), paddingX: 2, paddingY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: title }), _jsx(Box, { height: 1, flexShrink: 0 }), children] }) }));
12
+ }
13
+ const helpPages = [
14
+ {
15
+ title: "Navigation",
16
+ rows: [
17
+ ["Tab", "Next pane"],
18
+ ["Shift+Tab", "Previous pane"],
19
+ ["↑ / ↓", "Select a run or session"],
20
+ ["← / →", "Select project"],
21
+ ["Enter", "Open run details"],
22
+ ["Esc", "Return from details"],
23
+ ["↑ / ↓", "Scroll summary or details"],
24
+ ["PgUp / PgDn", "Page through details"],
25
+ ["?", "Open keyboard shortcuts"],
26
+ ["q / Ctrl-C", "Close monitor; workflows continue"],
27
+ ],
28
+ },
29
+ {
30
+ title: "Workflow",
31
+ rows: [
32
+ ["p", "Pause or resume project intake"],
33
+ ["s", "Stop selected run"],
34
+ ["r", "Retry as a new run"],
35
+ ["c", "Recover failed publication"],
36
+ ["[ / ]", "Inspect previous or next step event"],
37
+ ["End", "Follow latest step event"],
38
+ ["a", "Focus agent sessions"],
39
+ ["l", "Open selected agent log"],
40
+ ["v", "Open validation logs"],
41
+ ],
42
+ note: "Stop, retry and recovery open a confirmation dialog.",
43
+ },
44
+ {
45
+ title: "Logs",
46
+ rows: [
47
+ ["↑ / ↓", "Scroll records"],
48
+ ["j / k", "Scroll down or up"],
49
+ ["PgUp / PgDn", "Scroll one page"],
50
+ ["← / →", "Pan long lines"],
51
+ ["Home / g", "Go to first page"],
52
+ ["End / G", "Go to last page"],
53
+ ["f", "Resume live follow"],
54
+ ["R", "Toggle readable or raw text"],
55
+ ["Tab", "Select next log"],
56
+ ["Esc", "Return to previous view"],
57
+ ],
58
+ },
59
+ {
60
+ title: "Search",
61
+ rows: [
62
+ ["/", "Start a literal search"],
63
+ ["Enter", "Apply search"],
64
+ ["n", "Next matching record"],
65
+ ["N", "Previous matching record"],
66
+ ["Esc", "Dismiss search before leaving logs"],
67
+ ],
68
+ note: "Search covers the whole selected log and wraps. Lowercase ignores case; uppercase makes it case-sensitive. Search pauses live follow.",
69
+ },
70
+ {
71
+ title: "Timing",
72
+ rows: [],
73
+ note: "Execution duration includes preparation and waits within one execution.\n\nQueue waiting and gaps before recovery are shown separately.\n\nDatabase connectivity does not establish runner liveness.",
74
+ },
75
+ ];
76
+ export function HelpDialog({ columns, rows, onClose, initialPage = 0, }) {
77
+ const [page, setPage] = useState(initialPage);
78
+ const [offset, setOffset] = useState(0);
79
+ const width = Math.min(86, columns - dialogHorizontalMargin);
80
+ const height = Math.min(23, rows - dialogVerticalMargin);
81
+ const bodyHeight = height - dialogChromeHeight;
82
+ const current = helpPages[page];
83
+ const content = [
84
+ ...current.rows.map(([key, description]) => `${key.padEnd(17)}${description}`),
85
+ ...(current.note ? ["", current.note] : []),
86
+ ];
87
+ const maxOffset = Math.max(0, wrapLines(content, width - dialogContentWidthOffset).length - bodyHeight);
88
+ const changePage = (delta) => {
89
+ setPage((value) => (value + delta + helpPages.length) % helpPages.length);
90
+ setOffset(0);
91
+ };
92
+ useInput((input, key) => {
93
+ if (key.ctrl || key.meta || key.eventType === "release")
94
+ return;
95
+ if (key.escape || input === "?")
96
+ onClose();
97
+ else if (key.tab)
98
+ changePage(key.shift ? -1 : 1);
99
+ else if (key.leftArrow)
100
+ changePage(-1);
101
+ else if (key.rightArrow)
102
+ changePage(1);
103
+ else if (key.upArrow || key.pageUp)
104
+ setOffset((value) => Math.max(0, Math.min(value, maxOffset) - (key.pageUp ? bodyHeight : 1)));
105
+ else if (key.downArrow || key.pageDown)
106
+ setOffset((value) => Math.min(maxOffset, value + (key.pageDown ? bodyHeight : 1)));
107
+ });
108
+ return (_jsxs(Dialog, { columns: columns, rows: rows, width: width, height: height, title: "Keyboard shortcuts", children: [_jsx(Box, { gap: 1, children: helpPages.map((item, index) => (_jsxs(Text, { bold: index === page, inverse: index === page, children: [index === page ? ">" : "", item.title] }, item.title))) }), _jsx(Box, { height: 1, flexShrink: 0 }), _jsx(Lines, { lines: content, width: width - dialogContentWidthOffset, height: bodyHeight, offset: offset }), _jsx(Box, { height: 1, flexShrink: 0 }), _jsxs(Text, { dimColor: true, children: [page + 1, "/", helpPages.length, maxOffset
109
+ ? ` · ↑↓ scroll (${Math.min(offset, maxOffset) + 1}/${maxOffset + 1})`
110
+ : ""] }), _jsx(Text, { color: colorFor("running"), children: "Tab / \u2190\u2192 page \u00B7 Esc close" })] }));
111
+ }
112
+ export function ConfirmDialog({ columns, rows, title, subject, description, available, onCancel, onConfirm, }) {
113
+ const [selected, setSelected] = useState("cancel");
114
+ const [offset, setOffset] = useState(0);
115
+ const width = Math.min(78, columns - dialogHorizontalMargin);
116
+ const content = [
117
+ subject,
118
+ "",
119
+ description,
120
+ ...(!available
121
+ ? ["", "This action is no longer available. Cancel to refresh the view."]
122
+ : []),
123
+ ];
124
+ const contentLines = wrapLines(content, width - dialogContentWidthOffset).length;
125
+ const height = Math.min(rows - dialogVerticalMargin, contentLines + dialogChromeHeight);
126
+ const bodyHeight = height - dialogChromeHeight;
127
+ const maxOffset = Math.max(0, contentLines - bodyHeight);
128
+ useInput((input, key) => {
129
+ if (key.ctrl || key.meta || key.eventType === "release")
130
+ return;
131
+ if (key.escape || input === "n")
132
+ onCancel();
133
+ else if (key.tab || key.leftArrow || key.rightArrow)
134
+ setSelected((value) => (value === "cancel" ? "confirm" : "cancel"));
135
+ else if (key.return) {
136
+ if (selected === "cancel")
137
+ onCancel();
138
+ else if (available)
139
+ onConfirm();
140
+ }
141
+ else if (key.upArrow)
142
+ setOffset((value) => Math.max(0, Math.min(value, maxOffset) - 1));
143
+ else if (key.downArrow)
144
+ setOffset((value) => Math.min(maxOffset, value + 1));
145
+ });
146
+ return (_jsxs(Dialog, { columns: columns, rows: rows, width: width, height: height, title: title, children: [_jsx(Lines, { lines: content, width: width - dialogContentWidthOffset, height: bodyHeight, offset: offset }), _jsx(Box, { height: 1, flexShrink: 0 }), _jsxs(Box, { gap: 3, justifyContent: "flex-end", children: [_jsx(Text, { bold: selected === "cancel", inverse: selected === "cancel", children: selected === "cancel" ? "> Cancel " : " Cancel " }), _jsxs(Text, { bold: selected === "confirm", inverse: selected === "confirm", dimColor: !available, children: [selected === "confirm" ? "> Confirm " : " Confirm ", !available ? "(unavailable)" : ""] })] }), _jsx(Box, { height: 1, flexShrink: 0 }), _jsx(Text, { color: colorFor("running"), children: "Tab switch \u00B7 Enter select \u00B7 Esc cancel" }), _jsx(Text, { dimColor: true, children: maxOffset
147
+ ? "↑↓ scroll message"
148
+ : "Only Enter on Confirm submits the action." })] }));
149
+ }
@@ -0,0 +1,7 @@
1
+ import type { ExecutionRecord, RunRecord } from "../domain.js";
2
+ export declare function duration(start?: string, end?: string, now?: number): string;
3
+ export declare function executionDuration(execution: ExecutionRecord | undefined, now: number): string;
4
+ export declare function elapsedRun(run: RunRecord, now: number): string;
5
+ export declare function colorFor(outcome: string): string | undefined;
6
+ export declare function cells(text: string, width: number, offset?: number): string;
7
+ export declare function wrapLines(lines: string[], width: number): string[];
@@ -0,0 +1,62 @@
1
+ import stringWidth from "string-width";
2
+ import wrapAnsi from "wrap-ansi";
3
+ import { terminalText } from "./text.js";
4
+ const millisecondsPerSecond = 1_000;
5
+ const secondsPerMinute = 60;
6
+ const secondsPerHour = 60 * secondsPerMinute;
7
+ const secondsPerDay = 24 * secondsPerHour;
8
+ export function duration(start, end, now = Date.now()) {
9
+ if (!start || !Number.isFinite(Date.parse(start)))
10
+ return "—";
11
+ const finish = end ? Date.parse(end) : now;
12
+ if (!Number.isFinite(finish))
13
+ return "—";
14
+ const seconds = Math.max(0, Math.floor((finish - Date.parse(start)) / millisecondsPerSecond));
15
+ const days = Math.floor(seconds / secondsPerDay);
16
+ const hours = Math.floor((seconds % secondsPerDay) / secondsPerHour);
17
+ const minutes = Math.floor((seconds % secondsPerHour) / secondsPerMinute);
18
+ return `${days ? `${days}d ` : ""}${hours ? `${hours}h ` : ""}${minutes}m ${seconds % secondsPerMinute}s`;
19
+ }
20
+ export function executionDuration(execution, now) {
21
+ if (!execution ||
22
+ (!execution.finishedAt &&
23
+ !["queued", "running"].includes(execution.outcome)))
24
+ return "—";
25
+ return duration(execution.startedAt, execution.finishedAt, now);
26
+ }
27
+ export function elapsedRun(run, now) {
28
+ const latest = run.executions?.at(-1);
29
+ if (!["queued", "running"].includes(run.outcome) && !latest?.finishedAt)
30
+ return "—";
31
+ return duration(run.createdAt, latest?.finishedAt, now);
32
+ }
33
+ export function colorFor(outcome) {
34
+ if (process.env.NO_COLOR || process.env.TERM === "dumb")
35
+ return undefined;
36
+ if (["failed", "cancelled"].includes(outcome))
37
+ return "red";
38
+ if (["blocked", "queued"].includes(outcome))
39
+ return "yellow";
40
+ if (["completed", "no-change"].includes(outcome))
41
+ return "green";
42
+ return "cyan";
43
+ }
44
+ export function cells(text, width, offset = 0) {
45
+ let result = "";
46
+ let position = 0;
47
+ for (const { segment } of new Intl.Segmenter().segment(terminalText(text).replaceAll("\n", " ").replaceAll("\t", " "))) {
48
+ const size = stringWidth(segment);
49
+ if (position >= offset && position + size <= offset + width)
50
+ result += segment;
51
+ position += size;
52
+ if (position >= offset + width)
53
+ break;
54
+ }
55
+ return result;
56
+ }
57
+ export function wrapLines(lines, width) {
58
+ return lines.flatMap((line) => wrapAnsi(terminalText(line).replaceAll("\t", " "), Math.max(1, width), {
59
+ hard: true,
60
+ trim: false,
61
+ }).split("\n"));
62
+ }
@@ -0,0 +1,2 @@
1
+ export type { MonitorSource } from "./data.js";
2
+ export { Monitor } from "./monitor.js";
@@ -0,0 +1 @@
1
+ export { Monitor } from "./monitor.js";
@@ -0,0 +1,25 @@
1
+ /** Pane content sizes shared by rendering and keyboard scrolling. */
2
+ export declare function monitorLayout(columns: number, rows: number): {
3
+ wide: boolean;
4
+ height: number;
5
+ paneWidth: number;
6
+ summaryWidth: number;
7
+ summaryPanelHeight: number;
8
+ sessionsPanelHeight: number;
9
+ details: {
10
+ width: number;
11
+ height: number;
12
+ };
13
+ runs: {
14
+ width: number;
15
+ height: number;
16
+ };
17
+ summary: {
18
+ width: number;
19
+ height: number;
20
+ };
21
+ sessions: {
22
+ width: number;
23
+ height: number;
24
+ };
25
+ };
@@ -0,0 +1,36 @@
1
+ import { screenChromeRows } from "./constants.js";
2
+ const panelHorizontalChrome = 4;
3
+ const panelVerticalChrome = 3;
4
+ /** Pane content sizes shared by rendering and keyboard scrolling. */
5
+ export function monitorLayout(columns, rows) {
6
+ const wide = columns >= 110;
7
+ const height = Math.max(1, rows - screenChromeRows);
8
+ const paneWidth = wide ? Math.floor(columns * 0.43) : columns;
9
+ const summaryWidth = wide ? columns - paneWidth : columns;
10
+ const summaryPanelHeight = wide ? height - 6 : height;
11
+ const sessionsPanelHeight = wide ? 6 : height;
12
+ return {
13
+ wide,
14
+ height,
15
+ paneWidth,
16
+ summaryWidth,
17
+ summaryPanelHeight,
18
+ sessionsPanelHeight,
19
+ details: {
20
+ width: columns - panelHorizontalChrome,
21
+ height: height - panelVerticalChrome,
22
+ },
23
+ runs: {
24
+ width: paneWidth - panelHorizontalChrome,
25
+ height: height - panelVerticalChrome,
26
+ },
27
+ summary: {
28
+ width: summaryWidth - panelHorizontalChrome,
29
+ height: summaryPanelHeight - panelVerticalChrome,
30
+ },
31
+ sessions: {
32
+ width: summaryWidth - panelHorizontalChrome,
33
+ height: sessionsPanelHeight - panelVerticalChrome,
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,26 @@
1
+ export declare function presentLogLine(line: string, raw: boolean): string;
2
+ /** Sparse byte offsets index the file; only requested pages hold decoded text. */
3
+ export declare class LogFile {
4
+ readonly path: string;
5
+ private checkpoints;
6
+ private size;
7
+ private completeLines;
8
+ private tail;
9
+ private identity;
10
+ private modified;
11
+ revision: number;
12
+ private refreshTask;
13
+ constructor(path: string);
14
+ get count(): number;
15
+ refresh(signal: AbortSignal): Promise<void>;
16
+ private updateIndex;
17
+ private lines;
18
+ page(start: number, count: number, signal: AbortSignal): Promise<string[]>;
19
+ search(options: {
20
+ query: string;
21
+ from: number;
22
+ direction: 1 | -1;
23
+ raw: boolean;
24
+ signal: AbortSignal;
25
+ }): Promise<number | undefined>;
26
+ }
@@ -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;