patchrelay 0.13.0 → 0.14.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/dist/build-info.json +3 -3
- package/dist/cli/watch/App.js +8 -4
- package/dist/cli/watch/HelpBar.js +1 -1
- package/dist/cli/watch/IssueDetailView.js +9 -2
- package/dist/cli/watch/IssueListView.js +2 -3
- package/dist/cli/watch/IssueRow.js +26 -9
- package/dist/cli/watch/StatusBar.js +8 -3
- package/dist/cli/watch/use-detail-stream.js +36 -3
- package/dist/cli/watch/watch-state.js +26 -2
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli/watch/App.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useReducer } from "react";
|
|
2
|
+
import { useReducer, useMemo } from "react";
|
|
3
3
|
import { Box, useApp, useInput } from "ink";
|
|
4
|
-
import { watchReducer, initialWatchState } from "./watch-state.js";
|
|
4
|
+
import { watchReducer, initialWatchState, filterIssues } from "./watch-state.js";
|
|
5
5
|
import { useWatchStream } from "./use-watch-stream.js";
|
|
6
6
|
import { useDetailStream } from "./use-detail-stream.js";
|
|
7
7
|
import { IssueListView } from "./IssueListView.js";
|
|
@@ -12,6 +12,7 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
12
12
|
...initialWatchState,
|
|
13
13
|
...(initialIssueKey ? { view: "detail", activeDetailKey: initialIssueKey } : {}),
|
|
14
14
|
});
|
|
15
|
+
const filtered = useMemo(() => filterIssues(state.issues, state.filter), [state.issues, state.filter]);
|
|
15
16
|
useWatchStream({ baseUrl, bearerToken, dispatch });
|
|
16
17
|
useDetailStream({ baseUrl, bearerToken, issueKey: state.activeDetailKey, dispatch });
|
|
17
18
|
useInput((input, key) => {
|
|
@@ -27,11 +28,14 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
27
28
|
dispatch({ type: "select", index: state.selectedIndex - 1 });
|
|
28
29
|
}
|
|
29
30
|
else if (key.return) {
|
|
30
|
-
const issue =
|
|
31
|
+
const issue = filtered[state.selectedIndex];
|
|
31
32
|
if (issue?.issueKey) {
|
|
32
33
|
dispatch({ type: "enter-detail", issueKey: issue.issueKey });
|
|
33
34
|
}
|
|
34
35
|
}
|
|
36
|
+
else if (key.tab) {
|
|
37
|
+
dispatch({ type: "cycle-filter" });
|
|
38
|
+
}
|
|
35
39
|
}
|
|
36
40
|
else if (state.view === "detail") {
|
|
37
41
|
if (key.escape || key.backspace || key.delete) {
|
|
@@ -39,5 +43,5 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
});
|
|
42
|
-
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { state: state })) : (_jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), thread: state.thread })) }));
|
|
46
|
+
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, selectedIndex: state.selectedIndex, connected: state.connected, filter: state.filter, totalCount: state.issues.length })) : (_jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), thread: state.thread, report: state.report })) }));
|
|
43
47
|
}
|
|
@@ -2,6 +2,6 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
export function HelpBar({ view }) {
|
|
4
4
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: view === "list"
|
|
5
|
-
? "j/k: navigate Enter: detail q: quit"
|
|
5
|
+
? "j/k: navigate Enter: detail Tab: filter q: quit"
|
|
6
6
|
: "Esc: back q: quit" }) }));
|
|
7
7
|
}
|
|
@@ -2,10 +2,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { ThreadView } from "./ThreadView.js";
|
|
4
4
|
import { HelpBar } from "./HelpBar.js";
|
|
5
|
-
|
|
5
|
+
function truncate(text, max) {
|
|
6
|
+
const line = text.replace(/\n/g, " ").trim();
|
|
7
|
+
return line.length > max ? `${line.slice(0, max - 3)}...` : line;
|
|
8
|
+
}
|
|
9
|
+
function ReportView({ report }) {
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { dimColor: true, children: "Latest run:" }), _jsx(Text, { bold: true, children: report.runType }), _jsx(Text, { color: report.status === "completed" ? "green" : "red", children: report.status })] }), report.summary && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Summary:" }), _jsx(Text, { wrap: "wrap", children: truncate(report.summary, 300) })] })), report.commands.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Commands (", report.commands.length, "):"] }), report.commands.slice(-10).map((cmd, i) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: cmd.exitCode === 0 ? "green" : cmd.exitCode !== undefined ? "red" : "white", children: cmd.exitCode === 0 ? "\u2713" : cmd.exitCode !== undefined ? "\u2717" : " " }), _jsx(Text, { dimColor: true, children: "$ " }), _jsx(Text, { children: truncate(cmd.command, 60) }), cmd.durationMs !== undefined && _jsxs(Text, { dimColor: true, children: [" ", (cmd.durationMs / 1000).toFixed(1), "s"] })] }, `cmd-${i}`)))] })), _jsxs(Box, { marginTop: 1, gap: 2, children: [report.fileChanges > 0 && _jsxs(Text, { dimColor: true, children: [report.fileChanges, " file change", report.fileChanges !== 1 ? "s" : ""] }), report.toolCalls > 0 && _jsxs(Text, { dimColor: true, children: [report.toolCalls, " tool call", report.toolCalls !== 1 ? "s" : ""] }), report.assistantMessages.length > 0 && _jsxs(Text, { dimColor: true, children: [report.assistantMessages.length, " message", report.assistantMessages.length !== 1 ? "s" : ""] })] })] }));
|
|
11
|
+
}
|
|
12
|
+
export function IssueDetailView({ issue, thread, report }) {
|
|
6
13
|
if (!issue) {
|
|
7
14
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "Issue not found." }), _jsx(HelpBar, { view: "detail" })] }));
|
|
8
15
|
}
|
|
9
16
|
const key = issue.issueKey ?? issue.projectId;
|
|
10
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: key }), _jsx(Text, { color: "cyan", children: issue.factoryState }), issue.activeRunType && _jsx(Text, { color: "yellow", children: issue.activeRunType }), issue.prNumber !== undefined && _jsxs(Text, { dimColor: true, children: ["PR #", issue.prNumber] })] }), issue.title && _jsx(Text, { dimColor: true, children: issue.title }), _jsx(Text, { dimColor: true, children: "─".repeat(72) }), thread ? (_jsx(ThreadView, { thread: thread })) : (_jsx(Text, { dimColor: true, children: "
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: key }), _jsx(Text, { color: "cyan", children: issue.factoryState }), issue.activeRunType && _jsx(Text, { color: "yellow", children: issue.activeRunType }), issue.prNumber !== undefined && _jsxs(Text, { dimColor: true, children: ["PR #", issue.prNumber] })] }), issue.title && _jsx(Text, { dimColor: true, children: issue.title }), _jsx(Text, { dimColor: true, children: "─".repeat(72) }), thread ? (_jsx(ThreadView, { thread: thread })) : report ? (_jsx(ReportView, { report: report })) : (_jsx(Text, { dimColor: true, children: "Loading..." })), _jsx(Text, { dimColor: true, children: "─".repeat(72) }), _jsx(HelpBar, { view: "detail" })] }));
|
|
11
18
|
}
|
|
@@ -3,7 +3,6 @@ import { Box, Text } from "ink";
|
|
|
3
3
|
import { IssueRow } from "./IssueRow.js";
|
|
4
4
|
import { StatusBar } from "./StatusBar.js";
|
|
5
5
|
import { HelpBar } from "./HelpBar.js";
|
|
6
|
-
export function IssueListView({
|
|
7
|
-
|
|
8
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { issues: issues, connected: connected }), _jsx(Text, { dimColor: true, children: "─".repeat(72) }), issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No tracked issues." })) : (_jsx(Box, { flexDirection: "column", children: issues.map((issue, index) => (_jsx(IssueRow, { issue: issue, selected: index === selectedIndex }, issue.issueKey ?? `${issue.projectId}-${index}`))) })), _jsx(Text, { dimColor: true, children: "─".repeat(72) }), _jsx(HelpBar, { view: "list" })] }));
|
|
6
|
+
export function IssueListView({ issues, selectedIndex, connected, filter, totalCount }) {
|
|
7
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { issues: issues, totalCount: totalCount, filter: filter, connected: connected }), _jsx(Text, { dimColor: true, children: "─".repeat(72) }), issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No issues match the current filter." })) : (_jsx(Box, { flexDirection: "column", children: issues.map((issue, index) => (_jsx(IssueRow, { issue: issue, selected: index === selectedIndex }, issue.issueKey ?? `${issue.projectId}-${index}`))) })), _jsx(Text, { dimColor: true, children: "─".repeat(72) }), _jsx(HelpBar, { view: "list" })] }));
|
|
9
8
|
}
|
|
@@ -21,16 +21,31 @@ function stateColor(state) {
|
|
|
21
21
|
function formatPr(issue) {
|
|
22
22
|
if (!issue.prNumber)
|
|
23
23
|
return "";
|
|
24
|
-
const parts = [
|
|
24
|
+
const parts = [`#${issue.prNumber}`];
|
|
25
25
|
if (issue.prReviewState === "approved")
|
|
26
|
-
parts.push("
|
|
26
|
+
parts.push("\u2713");
|
|
27
27
|
else if (issue.prReviewState === "changes_requested")
|
|
28
|
-
parts.push("
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
parts.push("\u2717");
|
|
29
|
+
return parts.join("");
|
|
30
|
+
}
|
|
31
|
+
function relativeTime(iso) {
|
|
32
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
33
|
+
if (ms < 0)
|
|
34
|
+
return "now";
|
|
35
|
+
const seconds = Math.floor(ms / 1000);
|
|
36
|
+
if (seconds < 60)
|
|
37
|
+
return `${seconds}s`;
|
|
38
|
+
const minutes = Math.floor(seconds / 60);
|
|
39
|
+
if (minutes < 60)
|
|
40
|
+
return `${minutes}m`;
|
|
41
|
+
const hours = Math.floor(minutes / 60);
|
|
42
|
+
if (hours < 24)
|
|
43
|
+
return `${hours}h`;
|
|
44
|
+
const days = Math.floor(hours / 24);
|
|
45
|
+
return `${days}d`;
|
|
46
|
+
}
|
|
47
|
+
function truncate(text, max) {
|
|
48
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
34
49
|
}
|
|
35
50
|
export function IssueRow({ issue, selected }) {
|
|
36
51
|
const key = issue.issueKey ?? issue.projectId;
|
|
@@ -38,5 +53,7 @@ export function IssueRow({ issue, selected }) {
|
|
|
38
53
|
const run = issue.activeRunType ?? issue.latestRunType;
|
|
39
54
|
const runStatus = issue.activeRunType ? "running" : issue.latestRunStatus;
|
|
40
55
|
const pr = formatPr(issue);
|
|
41
|
-
|
|
56
|
+
const ago = relativeTime(issue.updatedAt);
|
|
57
|
+
const title = issue.title ? truncate(issue.title, 40) : "";
|
|
58
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: selected ? "blueBright" : "white", bold: selected, children: selected ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: key.padEnd(10) }), _jsx(Text, { color: stateColor(state), children: state.padEnd(18) }), _jsx(Text, { dimColor: true, children: run ? `${run}:${runStatus ?? "?"}`.padEnd(22) : "".padEnd(22) }), pr ? _jsx(Text, { dimColor: true, children: pr.padEnd(6) }) : _jsx(Text, { dimColor: true, children: "".padEnd(6) }), _jsx(Text, { dimColor: true, children: ago.padStart(4) }), title ? _jsxs(Text, { dimColor: true, children: [" ", title] }) : null] }));
|
|
42
59
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const FILTER_LABELS = {
|
|
4
|
+
"all": "all",
|
|
5
|
+
"active": "active",
|
|
6
|
+
"non-done": "in progress",
|
|
7
|
+
};
|
|
8
|
+
export function StatusBar({ issues, totalCount, filter, connected }) {
|
|
9
|
+
const showing = filter === "all" ? `${totalCount} issues` : `${issues.length}/${totalCount} issues`;
|
|
10
|
+
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: showing }), _jsxs(Text, { dimColor: true, children: [" [", FILTER_LABELS[filter], "]"] })] }), _jsx(Text, { color: connected ? "green" : "red", children: connected ? "\u25cf connected" : "\u25cb disconnected" })] }));
|
|
6
11
|
}
|
|
@@ -29,15 +29,48 @@ async function rehydrate(baseUrl, issueKey, headers, signal, dispatch) {
|
|
|
29
29
|
return;
|
|
30
30
|
const data = await response.json();
|
|
31
31
|
const threadData = data.thread;
|
|
32
|
-
if (
|
|
32
|
+
if (threadData) {
|
|
33
|
+
dispatch({ type: "thread-snapshot", thread: materializeThread(threadData) });
|
|
33
34
|
return;
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
}
|
|
36
|
+
// No active thread — fall back to latest run report
|
|
37
|
+
await rehydrateFromReport(baseUrl, issueKey, headers, signal, dispatch);
|
|
36
38
|
}
|
|
37
39
|
catch {
|
|
38
40
|
// Rehydration is best-effort — SSE stream will provide updates
|
|
39
41
|
}
|
|
40
42
|
}
|
|
43
|
+
async function rehydrateFromReport(baseUrl, issueKey, headers, signal, dispatch) {
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(`/api/issues/${encodeURIComponent(issueKey)}/report`, baseUrl);
|
|
46
|
+
const response = await fetch(url, { headers, signal });
|
|
47
|
+
if (!response.ok)
|
|
48
|
+
return;
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
const latest = data.runs?.[0];
|
|
51
|
+
if (!latest)
|
|
52
|
+
return;
|
|
53
|
+
const report = {
|
|
54
|
+
runType: latest.run.runType,
|
|
55
|
+
status: latest.run.status,
|
|
56
|
+
summary: typeof latest.summary?.latestAssistantMessage === "string"
|
|
57
|
+
? latest.summary.latestAssistantMessage
|
|
58
|
+
: latest.report?.assistantMessages.at(-1),
|
|
59
|
+
commands: latest.report?.commands.map((c) => ({
|
|
60
|
+
command: c.command,
|
|
61
|
+
...(typeof c.exitCode === "number" ? { exitCode: c.exitCode } : {}),
|
|
62
|
+
...(typeof c.durationMs === "number" ? { durationMs: c.durationMs } : {}),
|
|
63
|
+
})) ?? [],
|
|
64
|
+
fileChanges: latest.report?.fileChanges.length ?? 0,
|
|
65
|
+
toolCalls: latest.report?.toolCalls.length ?? 0,
|
|
66
|
+
assistantMessages: latest.report?.assistantMessages ?? [],
|
|
67
|
+
};
|
|
68
|
+
dispatch({ type: "report-snapshot", report });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Report fetch is best-effort
|
|
72
|
+
}
|
|
73
|
+
}
|
|
41
74
|
async function streamCodexEvents(baseUrl, issueKey, baseHeaders, signal, dispatch) {
|
|
42
75
|
try {
|
|
43
76
|
const url = new URL("/api/watch", baseUrl);
|
|
@@ -5,7 +5,27 @@ export const initialWatchState = {
|
|
|
5
5
|
view: "list",
|
|
6
6
|
activeDetailKey: null,
|
|
7
7
|
thread: null,
|
|
8
|
+
report: null,
|
|
9
|
+
filter: "non-done",
|
|
8
10
|
};
|
|
11
|
+
const TERMINAL_FACTORY_STATES = new Set(["done", "failed"]);
|
|
12
|
+
export function filterIssues(issues, filter) {
|
|
13
|
+
switch (filter) {
|
|
14
|
+
case "all":
|
|
15
|
+
return issues;
|
|
16
|
+
case "active":
|
|
17
|
+
return issues.filter((i) => i.activeRunType !== undefined);
|
|
18
|
+
case "non-done":
|
|
19
|
+
return issues.filter((i) => !TERMINAL_FACTORY_STATES.has(i.factoryState));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function nextFilter(filter) {
|
|
23
|
+
switch (filter) {
|
|
24
|
+
case "non-done": return "active";
|
|
25
|
+
case "active": return "all";
|
|
26
|
+
case "all": return "non-done";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
9
29
|
export function watchReducer(state, action) {
|
|
10
30
|
switch (action.type) {
|
|
11
31
|
case "connected":
|
|
@@ -26,13 +46,17 @@ export function watchReducer(state, action) {
|
|
|
26
46
|
selectedIndex: Math.max(0, Math.min(action.index, state.issues.length - 1)),
|
|
27
47
|
};
|
|
28
48
|
case "enter-detail":
|
|
29
|
-
return { ...state, view: "detail", activeDetailKey: action.issueKey, thread: null };
|
|
49
|
+
return { ...state, view: "detail", activeDetailKey: action.issueKey, thread: null, report: null };
|
|
30
50
|
case "exit-detail":
|
|
31
|
-
return { ...state, view: "list", activeDetailKey: null, thread: null };
|
|
51
|
+
return { ...state, view: "list", activeDetailKey: null, thread: null, report: null };
|
|
32
52
|
case "thread-snapshot":
|
|
33
53
|
return { ...state, thread: action.thread };
|
|
54
|
+
case "report-snapshot":
|
|
55
|
+
return { ...state, report: action.report };
|
|
34
56
|
case "codex-notification":
|
|
35
57
|
return applyCodexNotification(state, action.method, action.params);
|
|
58
|
+
case "cycle-filter":
|
|
59
|
+
return { ...state, filter: nextFilter(state.filter), selectedIndex: 0 };
|
|
36
60
|
}
|
|
37
61
|
}
|
|
38
62
|
// ─── Feed Event Application ───────────────────────────────────────
|