@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.
- package/README.md +37 -6
- package/dist/src/adapters/agents.js +6 -3
- package/dist/src/adapters/hosting.js +16 -10
- package/dist/src/adapters/sdk-protocol.d.ts +3 -3
- package/dist/src/adapters/sdk-protocol.js +9 -7
- package/dist/src/attribution.d.ts +9 -0
- package/dist/src/attribution.js +57 -0
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +80 -25
- package/dist/src/config.d.ts +24 -30
- package/dist/src/config.js +32 -27
- package/dist/src/defaults.d.ts +2 -0
- package/dist/src/defaults.js +2 -0
- package/dist/src/domain.d.ts +31 -4
- package/dist/src/domain.js +10 -3
- package/dist/src/evidence.d.ts +54 -0
- package/dist/src/evidence.js +214 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/invocation.d.ts +25 -0
- package/dist/src/invocation.js +166 -0
- package/dist/src/operations.d.ts +8 -2
- package/dist/src/operations.js +100 -136
- package/dist/src/prompts.d.ts +28 -0
- package/dist/src/prompts.js +63 -0
- package/dist/src/recovery.d.ts +19 -0
- package/dist/src/recovery.js +99 -0
- package/dist/src/runner.d.ts +7 -0
- package/dist/src/runner.js +170 -18
- package/dist/src/runtime/process.d.ts +2 -0
- package/dist/src/runtime/process.js +41 -12
- package/dist/src/store.d.ts +21 -2
- package/dist/src/store.js +122 -1
- package/dist/src/tui/actions.d.ts +16 -0
- package/dist/src/tui/actions.js +23 -0
- package/dist/src/tui/constants.d.ts +6 -0
- package/dist/src/tui/constants.js +3 -0
- package/dist/src/{tui-data.d.ts → tui/data.d.ts} +10 -7
- package/dist/src/tui/data.js +146 -0
- package/dist/src/tui/dialogs.d.ts +17 -0
- package/dist/src/tui/dialogs.js +149 -0
- package/dist/src/tui/format.d.ts +7 -0
- package/dist/src/tui/format.js +62 -0
- package/dist/src/tui/index.d.ts +3 -0
- package/dist/src/tui/index.js +2 -0
- package/dist/src/tui/layout.d.ts +25 -0
- package/dist/src/tui/layout.js +36 -0
- package/dist/src/tui/log-file.d.ts +26 -0
- package/dist/src/tui/log-file.js +156 -0
- package/dist/src/tui/log.d.ts +11 -0
- package/dist/src/tui/log.js +90 -0
- package/dist/src/tui/monitor.d.ts +10 -0
- package/dist/src/tui/monitor.js +284 -0
- package/dist/src/tui/notifications.d.ts +23 -0
- package/dist/src/tui/notifications.js +104 -0
- package/dist/src/tui/text.d.ts +3 -0
- package/dist/src/tui/text.js +10 -0
- package/dist/src/tui/use-log-controller.d.ts +27 -0
- package/dist/src/tui/use-log-controller.js +192 -0
- package/dist/src/tui/views.d.ts +25 -0
- package/dist/src/tui/views.js +327 -0
- package/dist/src/workspace.js +21 -8
- package/docs/api.md +132 -8
- package/docs/architecture.md +21 -4
- package/docs/configuration.md +181 -8
- package/docs/database.md +7 -0
- package/docs/operations.md +160 -5
- package/docs/providers.md +58 -2
- package/docs/releases.md +34 -79
- package/examples/config.ts +6 -6
- package/examples/run.ts +4 -1
- package/package.json +4 -2
- package/dist/src/tui-data.js +0 -89
- package/dist/src/tui.d.ts +0 -5
- package/dist/src/tui.js +0 -69
- package/docs/acceptance.md +0 -35
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { recoveryUnavailable } from "../recovery.js";
|
|
2
|
+
/** Display eligibility only; the runner still validates command admission. */
|
|
3
|
+
export function actionAvailability({ run, project, projectRuns, pending, }) {
|
|
4
|
+
const recoveryReason = run
|
|
5
|
+
? (recoveryUnavailable(run) ??
|
|
6
|
+
project?.blocked ??
|
|
7
|
+
(projectRuns.some((item) => item.taskKey === run.taskKey && item.attempt > run.attempt)
|
|
8
|
+
? "A newer attempt has superseded this run"
|
|
9
|
+
: undefined))
|
|
10
|
+
: undefined;
|
|
11
|
+
return {
|
|
12
|
+
recoveryReason,
|
|
13
|
+
available: {
|
|
14
|
+
stop: Boolean(run && !pending && ["queued", "running"].includes(run.outcome)),
|
|
15
|
+
retry: Boolean(run &&
|
|
16
|
+
!pending &&
|
|
17
|
+
["failed", "blocked", "cancelled"].includes(run.outcome) &&
|
|
18
|
+
!projectRuns.some((item) => item.taskKey === run.taskKey &&
|
|
19
|
+
["queued", "running"].includes(item.outcome))),
|
|
20
|
+
recover: Boolean(run && !pending && !recoveryReason),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { RunRecord } from "
|
|
2
|
-
import type { EventRecord, InvocationRecord, ProjectState, Store } from "
|
|
1
|
+
import type { RunRecord } from "../domain.js";
|
|
2
|
+
import type { EventRecord, InvocationRecord, ProjectState, Store } from "../store.js";
|
|
3
|
+
import { type ExecutionNotificationWriter } from "./notifications.js";
|
|
3
4
|
export interface MonitorSource {
|
|
4
5
|
projects: Store["projects"];
|
|
5
6
|
runs: Store["runs"];
|
|
@@ -8,18 +9,20 @@ export interface MonitorSource {
|
|
|
8
9
|
request: Store["request"];
|
|
9
10
|
commands: Store["commands"];
|
|
10
11
|
}
|
|
12
|
+
export type MonitorAction = "pause" | "resume" | "stop" | "retry" | "recover";
|
|
11
13
|
export declare function useMonitorData(source: MonitorSource, selection: {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}): {
|
|
14
|
+
projectId?: string;
|
|
15
|
+
runId?: string;
|
|
16
|
+
}, notificationWriter?: ExecutionNotificationWriter): {
|
|
15
17
|
projects: ProjectState[];
|
|
16
18
|
project: ProjectState | undefined;
|
|
17
19
|
projectRuns: RunRecord[];
|
|
18
20
|
run: RunRecord | undefined;
|
|
19
21
|
sessions: InvocationRecord[];
|
|
20
22
|
events: EventRecord[];
|
|
23
|
+
connection: string;
|
|
24
|
+
lastUpdated: number | undefined;
|
|
21
25
|
message: string;
|
|
22
|
-
setMessage: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
23
26
|
pending: string | undefined;
|
|
24
|
-
action: (kind:
|
|
27
|
+
action: (kind: MonitorAction, target: string) => Promise<void>;
|
|
25
28
|
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { tuiRefreshIntervalMs } from "./constants.js";
|
|
3
|
+
import { ExecutionNotificationObserver, } from "./notifications.js";
|
|
4
|
+
export function useMonitorData(source, selection, notificationWriter) {
|
|
5
|
+
const [projects, setProjects] = useState([]);
|
|
6
|
+
const [runs, setRuns] = useState([]);
|
|
7
|
+
const [detail, setDetail] = useState({ sessions: [], events: [] });
|
|
8
|
+
const [connection, setConnection] = useState("Connecting…");
|
|
9
|
+
const [lastUpdated, setLastUpdated] = useState();
|
|
10
|
+
const [message, setMessage] = useState("");
|
|
11
|
+
const [pending, setPending] = useState();
|
|
12
|
+
const pendingRef = useRef(undefined);
|
|
13
|
+
const mounted = useRef(true);
|
|
14
|
+
const notificationObserver = useRef(notificationWriter
|
|
15
|
+
? new ExecutionNotificationObserver(notificationWriter)
|
|
16
|
+
: undefined).current;
|
|
17
|
+
const project = projects.find((item) => item.id === selection.projectId) ?? projects[0];
|
|
18
|
+
const projectRuns = runs.filter((item) => item.projectId === project?.id);
|
|
19
|
+
const run = projectRuns.find((item) => item.id === selection.runId) ?? projectRuns[0];
|
|
20
|
+
const selectedRunId = run?.id;
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
mounted.current = true;
|
|
23
|
+
return () => {
|
|
24
|
+
mounted.current = false;
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
let closed = false;
|
|
29
|
+
let busy = false;
|
|
30
|
+
let cursor = 0;
|
|
31
|
+
let history = [];
|
|
32
|
+
const update = async () => {
|
|
33
|
+
if (busy)
|
|
34
|
+
return;
|
|
35
|
+
busy = true;
|
|
36
|
+
try {
|
|
37
|
+
const [nextProjects, nextRuns] = await Promise.all([
|
|
38
|
+
source.projects(),
|
|
39
|
+
source.runs(),
|
|
40
|
+
]);
|
|
41
|
+
if (closed)
|
|
42
|
+
return;
|
|
43
|
+
setProjects(nextProjects);
|
|
44
|
+
setRuns(nextRuns);
|
|
45
|
+
notificationObserver?.observe(nextRuns);
|
|
46
|
+
if (selectedRunId) {
|
|
47
|
+
const [sessions, nextEvents] = await Promise.all([
|
|
48
|
+
source.invocations(selectedRunId),
|
|
49
|
+
source.events(cursor, selectedRunId),
|
|
50
|
+
]);
|
|
51
|
+
if (closed)
|
|
52
|
+
return;
|
|
53
|
+
const fresh = nextEvents.filter((event) => event.sequence > cursor);
|
|
54
|
+
if (fresh.length)
|
|
55
|
+
cursor = Math.max(...fresh.map((event) => event.sequence));
|
|
56
|
+
history = [
|
|
57
|
+
...history,
|
|
58
|
+
...fresh.filter((event) => event.runId === selectedRunId && event.kind === "step"),
|
|
59
|
+
];
|
|
60
|
+
setDetail({ runId: selectedRunId, sessions, events: history });
|
|
61
|
+
}
|
|
62
|
+
setConnection("Database connected");
|
|
63
|
+
setLastUpdated(Date.now());
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (!closed)
|
|
67
|
+
setConnection(`Connection error: ${String(error)}`);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
busy = false;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
void update();
|
|
74
|
+
const timer = setInterval(() => void update(), tuiRefreshIntervalMs);
|
|
75
|
+
return () => {
|
|
76
|
+
closed = true;
|
|
77
|
+
clearInterval(timer);
|
|
78
|
+
};
|
|
79
|
+
}, [source, selectedRunId, notificationObserver]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!pending || pending === "submitting")
|
|
82
|
+
return;
|
|
83
|
+
let closed = false;
|
|
84
|
+
let busy = false;
|
|
85
|
+
const update = async () => {
|
|
86
|
+
if (busy)
|
|
87
|
+
return;
|
|
88
|
+
busy = true;
|
|
89
|
+
try {
|
|
90
|
+
const command = (await source.commands()).find((item) => item.id === pending);
|
|
91
|
+
if (closed || !command || command.status === "pending")
|
|
92
|
+
return;
|
|
93
|
+
setMessage(`${command.kind}: ${command.status}${command.error ? ` — ${command.error}` : ""}`);
|
|
94
|
+
pendingRef.current = undefined;
|
|
95
|
+
setPending(undefined);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (!closed)
|
|
99
|
+
setMessage(`Command pending; cannot read acknowledgement: ${String(error)}`);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
busy = false;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
void update();
|
|
106
|
+
const timer = setInterval(() => void update(), tuiRefreshIntervalMs);
|
|
107
|
+
return () => {
|
|
108
|
+
closed = true;
|
|
109
|
+
clearInterval(timer);
|
|
110
|
+
};
|
|
111
|
+
}, [source, pending]);
|
|
112
|
+
const action = async (kind, target) => {
|
|
113
|
+
if (pendingRef.current)
|
|
114
|
+
return;
|
|
115
|
+
pendingRef.current = "submitting";
|
|
116
|
+
setPending("submitting");
|
|
117
|
+
setMessage(`${kind}: pending — waiting for runner`);
|
|
118
|
+
try {
|
|
119
|
+
const id = await source.request(kind, target);
|
|
120
|
+
if (!mounted.current)
|
|
121
|
+
return;
|
|
122
|
+
pendingRef.current = id;
|
|
123
|
+
setPending(id);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (!mounted.current)
|
|
127
|
+
return;
|
|
128
|
+
pendingRef.current = undefined;
|
|
129
|
+
setPending(undefined);
|
|
130
|
+
setMessage(`${kind}: submission failed — ${String(error)}`);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
projects,
|
|
135
|
+
project,
|
|
136
|
+
projectRuns,
|
|
137
|
+
run,
|
|
138
|
+
sessions: detail.runId === selectedRunId ? detail.sessions : [],
|
|
139
|
+
events: detail.runId === selectedRunId ? detail.events : [],
|
|
140
|
+
connection,
|
|
141
|
+
lastUpdated,
|
|
142
|
+
message,
|
|
143
|
+
pending,
|
|
144
|
+
action,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -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,25 @@
|
|
|
1
|
+
/** Pane content sizes shared by rendering and keyboard scrolling. */
|
|
2
|
+
export declare function monitorLayout(columns: number, rows: number, compactChrome?: boolean): {
|
|
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, compactChrome = false) {
|
|
6
|
+
const wide = columns >= 110;
|
|
7
|
+
const height = Math.max(1, rows - (compactChrome ? 4 : 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
|
+
}
|