patchrelay 0.20.0 → 0.20.2
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 +38 -9
- package/dist/cli/watch/FeedView.js +4 -9
- package/dist/cli/watch/IssueDetailView.js +24 -38
- package/dist/cli/watch/IssueListView.js +7 -2
- package/dist/cli/watch/IssueRow.js +43 -6
- package/dist/cli/watch/ItemLine.js +3 -0
- package/dist/cli/watch/TimelineRow.js +24 -41
- package/dist/cli/watch/format-utils.js +43 -0
- package/dist/cli/watch/sse-parser.js +43 -0
- package/dist/cli/watch/theme.js +55 -0
- package/dist/cli/watch/timeline-builder.js +1 -1
- package/dist/http.js +1 -1
- package/dist/service.js +43 -7
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli/watch/App.js
CHANGED
|
@@ -12,12 +12,18 @@ async function postPrompt(baseUrl, issueKey, text, bearerToken) {
|
|
|
12
12
|
const headers = { "content-type": "application/json" };
|
|
13
13
|
if (bearerToken)
|
|
14
14
|
headers.authorization = `Bearer ${bearerToken}`;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(new URL(`/api/issues/${encodeURIComponent(issueKey)}/prompt`, baseUrl), {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers,
|
|
19
|
+
body: JSON.stringify({ text }),
|
|
20
|
+
signal: AbortSignal.timeout(5000),
|
|
21
|
+
});
|
|
22
|
+
return await response.json();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return { reason: "Request failed" };
|
|
26
|
+
}
|
|
21
27
|
}
|
|
22
28
|
async function postRetry(baseUrl, issueKey, bearerToken) {
|
|
23
29
|
const headers = { "content-type": "application/json" };
|
|
@@ -46,12 +52,35 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
46
52
|
void postRetry(baseUrl, state.activeDetailKey, bearerToken);
|
|
47
53
|
}
|
|
48
54
|
}, [baseUrl, bearerToken, state.activeDetailKey]);
|
|
55
|
+
const [promptStatus, setPromptStatus] = useState(null);
|
|
49
56
|
const handlePromptSubmit = useCallback(() => {
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
const text = promptBuffer.trim();
|
|
58
|
+
if (!state.activeDetailKey || !text) {
|
|
59
|
+
setPromptMode(false);
|
|
60
|
+
setPromptBuffer("");
|
|
61
|
+
return;
|
|
52
62
|
}
|
|
63
|
+
// Add synthetic userMessage to timeline immediately
|
|
64
|
+
dispatch({
|
|
65
|
+
type: "codex-notification",
|
|
66
|
+
method: "item/started",
|
|
67
|
+
params: { item: { id: `prompt-${Date.now()}`, type: "userMessage", status: "completed", text } },
|
|
68
|
+
});
|
|
53
69
|
setPromptMode(false);
|
|
54
70
|
setPromptBuffer("");
|
|
71
|
+
setPromptStatus("sending...");
|
|
72
|
+
void postPrompt(baseUrl, state.activeDetailKey, text, bearerToken).then((result) => {
|
|
73
|
+
if (result.delivered) {
|
|
74
|
+
setPromptStatus("delivered");
|
|
75
|
+
}
|
|
76
|
+
else if (result.queued) {
|
|
77
|
+
setPromptStatus("queued for next run");
|
|
78
|
+
}
|
|
79
|
+
else if (result.reason) {
|
|
80
|
+
setPromptStatus(`failed: ${result.reason}`);
|
|
81
|
+
}
|
|
82
|
+
setTimeout(() => setPromptStatus(null), 3000);
|
|
83
|
+
});
|
|
55
84
|
}, [baseUrl, bearerToken, state.activeDetailKey, promptBuffer]);
|
|
56
85
|
useInput((input, key) => {
|
|
57
86
|
if (promptMode) {
|
|
@@ -120,5 +149,5 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
120
149
|
}
|
|
121
150
|
}
|
|
122
151
|
});
|
|
123
|
-
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, filter: state.filter, totalCount: state.issues.length })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, activeRunStartedAt: state.activeRunStartedAt, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, allIssues: filtered, activeDetailKey: state.activeDetailKey }), promptMode && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "prompt> " }), _jsx(Text, { children: promptBuffer }), _jsx(Text, { dimColor: true, children: "_" })] }))] })) : (_jsx(FeedView, { events: state.feedEvents, connected: state.connected })) }));
|
|
152
|
+
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, filter: state.filter, totalCount: state.issues.length })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, activeRunStartedAt: state.activeRunStartedAt, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, allIssues: filtered, activeDetailKey: state.activeDetailKey }), promptMode && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "prompt> " }), _jsx(Text, { children: promptBuffer }), _jsx(Text, { dimColor: true, children: "_" })] })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] })) : (_jsx(FeedView, { events: state.feedEvents, connected: state.connected })) }));
|
|
124
153
|
}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { HelpBar } from "./HelpBar.js";
|
|
4
4
|
const TAIL_SIZE = 30;
|
|
5
|
-
const LEVEL_COLORS = {
|
|
6
|
-
info: "white",
|
|
7
|
-
warn: "yellow",
|
|
8
|
-
error: "red",
|
|
9
|
-
};
|
|
10
5
|
const KIND_COLORS = {
|
|
11
6
|
stage: "cyan",
|
|
12
7
|
turn: "yellow",
|
|
@@ -16,17 +11,17 @@ const KIND_COLORS = {
|
|
|
16
11
|
service: "white",
|
|
17
12
|
workflow: "cyan",
|
|
18
13
|
linear: "blue",
|
|
14
|
+
comment: "cyan",
|
|
19
15
|
};
|
|
20
16
|
function formatTime(iso) {
|
|
21
17
|
return new Date(iso).toLocaleTimeString("en-GB", { hour12: false });
|
|
22
18
|
}
|
|
23
19
|
function FeedEventRow({ event }) {
|
|
24
20
|
const kindColor = KIND_COLORS[event.kind] ?? "white";
|
|
25
|
-
|
|
26
|
-
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: formatTime(event.at) }), _jsx(Text, { color: kindColor, children: event.kind.padEnd(10) }), event.issueKey && _jsx(Text, { bold: true, children: event.issueKey.padEnd(10) }), event.stage && _jsx(Text, { color: "cyan", children: event.stage.padEnd(16) }), _jsx(Text, { color: levelColor, children: event.summary })] }));
|
|
21
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(event.at), " "] }), _jsx(Text, { color: kindColor, children: (event.status ?? event.kind).padEnd(14) }), event.issueKey && _jsx(Text, { bold: true, children: ` ${event.issueKey.padEnd(9)}` }), _jsxs(Text, { children: [" ", event.summary] })] }));
|
|
27
22
|
}
|
|
28
23
|
export function FeedView({ events, connected }) {
|
|
29
24
|
const visible = events.length > TAIL_SIZE ? events.slice(-TAIL_SIZE) : events;
|
|
30
25
|
const skipped = events.length - visible.length;
|
|
31
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: "Operator Feed" }), _jsx(Text, { color: connected ? "green" : "red", children: connected ? "\u25cf connected" : "\u25cb disconnected" })] }), _jsx(
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: "Operator Feed" }), _jsx(Text, { color: connected ? "green" : "red", children: connected ? "\u25cf connected" : "\u25cb disconnected" })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: events.length === 0 ? (_jsx(Text, { dimColor: true, children: "No feed events yet." })) : (_jsxs(_Fragment, { children: [skipped > 0 && _jsxs(Text, { dimColor: true, children: [" ... ", skipped, " earlier"] }), visible.map((event) => (_jsx(FeedEventRow, { event: event }, event.id)))] })) }), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "feed" }) })] }));
|
|
32
27
|
}
|
|
@@ -35,26 +35,23 @@ function planStepColor(status) {
|
|
|
35
35
|
return "yellow";
|
|
36
36
|
return "white";
|
|
37
37
|
}
|
|
38
|
-
// ─── Compact Issue Sidebar (#4 split-pane) ───────────────────────
|
|
39
38
|
const SIDEBAR_STATE_COLORS = {
|
|
40
39
|
delegated: "blue", preparing: "blue",
|
|
41
40
|
implementing: "yellow", awaiting_input: "yellow",
|
|
42
|
-
pr_open: "cyan",
|
|
41
|
+
pr_open: "cyan",
|
|
43
42
|
changes_requested: "magenta", repairing_ci: "magenta", repairing_queue: "magenta",
|
|
44
43
|
awaiting_queue: "green", done: "green",
|
|
45
44
|
failed: "red", escalated: "red",
|
|
46
45
|
};
|
|
47
46
|
function CompactSidebar({ issues, activeKey }) {
|
|
48
|
-
return (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
return (_jsx(Box, { flexDirection: "column", width: 24, paddingRight: 1, children: issues.map((issue) => {
|
|
48
|
+
const key = issue.issueKey ?? issue.projectId;
|
|
49
|
+
const isCurrent = key === activeKey;
|
|
50
|
+
const sc = SIDEBAR_STATE_COLORS[issue.factoryState] ?? "white";
|
|
51
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isCurrent ? "blueBright" : "white", bold: isCurrent, children: isCurrent ? "\u25b8" : " " }), _jsx(Text, { bold: isCurrent, children: key.padEnd(9) }), _jsx(Text, { color: sc, children: issue.factoryState.slice(0, 10) })] }, key));
|
|
52
|
+
}) }));
|
|
54
53
|
}
|
|
55
|
-
// ─── Issue Context Panel (#5) ────────────────────────────────────
|
|
56
54
|
const PRIORITY_LABELS = {
|
|
57
|
-
0: { label: "none", color: "" },
|
|
58
55
|
1: { label: "urgent", color: "red" },
|
|
59
56
|
2: { label: "high", color: "yellow" },
|
|
60
57
|
3: { label: "medium", color: "cyan" },
|
|
@@ -63,48 +60,37 @@ const PRIORITY_LABELS = {
|
|
|
63
60
|
function ContextPanel({ issue, ctx }) {
|
|
64
61
|
const parts = [];
|
|
65
62
|
if (ctx.priority != null && ctx.priority > 0) {
|
|
66
|
-
const p = PRIORITY_LABELS[ctx.priority]
|
|
67
|
-
parts.push(
|
|
68
|
-
}
|
|
69
|
-
if (ctx.estimate != null) {
|
|
70
|
-
parts.push({ label: "estimate", value: String(ctx.estimate), color: "" });
|
|
71
|
-
}
|
|
72
|
-
if (ctx.currentLinearState) {
|
|
73
|
-
parts.push({ label: "linear", value: ctx.currentLinearState, color: "" });
|
|
63
|
+
const p = PRIORITY_LABELS[ctx.priority];
|
|
64
|
+
parts.push(p ? `${p.label}` : `p${ctx.priority}`);
|
|
74
65
|
}
|
|
75
66
|
if (issue.prNumber) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
parts.push(
|
|
67
|
+
let pr = `#${issue.prNumber}`;
|
|
68
|
+
if (issue.prReviewState === "approved")
|
|
69
|
+
pr += " \u2713";
|
|
70
|
+
else if (issue.prReviewState === "changes_requested")
|
|
71
|
+
pr += " \u2717";
|
|
72
|
+
parts.push(pr);
|
|
82
73
|
}
|
|
74
|
+
if (ctx.runCount > 0)
|
|
75
|
+
parts.push(`${ctx.runCount} runs`);
|
|
83
76
|
const retries = [
|
|
84
77
|
ctx.ciRepairAttempts > 0 ? `ci:${ctx.ciRepairAttempts}` : "",
|
|
85
|
-
ctx.queueRepairAttempts > 0 ? `
|
|
86
|
-
ctx.reviewFixAttempts > 0 ? `
|
|
78
|
+
ctx.queueRepairAttempts > 0 ? `q:${ctx.queueRepairAttempts}` : "",
|
|
79
|
+
ctx.reviewFixAttempts > 0 ? `rev:${ctx.reviewFixAttempts}` : "",
|
|
87
80
|
].filter(Boolean).join(" ");
|
|
88
|
-
if (retries)
|
|
89
|
-
parts.push(
|
|
90
|
-
}
|
|
91
|
-
if (ctx.branchName) {
|
|
92
|
-
parts.push({ label: "branch", value: ctx.branchName, color: "" });
|
|
93
|
-
}
|
|
94
|
-
const hasDescription = Boolean(ctx.description);
|
|
95
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { gap: 2, flexWrap: "wrap", children: parts.map((p) => (_jsxs(Text, { dimColor: true, children: [p.label, ": ", p.color ? _jsx(Text, { color: p.color, children: p.value }) : _jsx(Text, { dimColor: true, children: p.value })] }, p.label))) }), hasDescription && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [ctx.description.slice(0, 200), ctx.description.length > 200 ? "\u2026" : ""] }))] }));
|
|
81
|
+
if (retries)
|
|
82
|
+
parts.push(retries);
|
|
83
|
+
return (_jsxs(Box, { flexDirection: "column", children: [parts.length > 0 && _jsx(Text, { dimColor: true, children: parts.join(" ") }), ctx.description && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [ctx.description.slice(0, 160), ctx.description.length > 160 ? "\u2026" : ""] }))] }));
|
|
96
84
|
}
|
|
97
|
-
// ─── Detail Panel (right side of split) ──────────────────────────
|
|
98
85
|
function DetailPanel({ issue, timeline, follow, activeRunStartedAt, tokenUsage, diffSummary, plan, issueContext, }) {
|
|
99
86
|
if (!issue) {
|
|
100
87
|
return _jsx(Text, { color: "red", children: "Issue not found." });
|
|
101
88
|
}
|
|
102
89
|
const key = issue.issueKey ?? issue.projectId;
|
|
103
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, 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: ["
|
|
90
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, 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: ["#", issue.prNumber] }), activeRunStartedAt && _jsx(ElapsedTime, { startedAt: activeRunStartedAt })] }), issue.title && _jsx(Text, { children: issue.title }), (tokenUsage || (diffSummary && diffSummary.filesChanged > 0)) && (_jsxs(Box, { gap: 2, children: [tokenUsage && _jsxs(Text, { dimColor: true, children: [formatTokens(tokenUsage.inputTokens), " in / ", formatTokens(tokenUsage.outputTokens), " out"] }), diffSummary && diffSummary.filesChanged > 0 && (_jsxs(Text, { dimColor: true, children: [diffSummary.filesChanged, "f +", diffSummary.linesAdded, " -", diffSummary.linesRemoved] })), follow && _jsx(Text, { color: "yellow", children: "follow" })] })), issueContext && _jsx(ContextPanel, { issue: issue, ctx: issueContext }), plan && plan.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: plan.map((entry, i) => (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: planStepColor(entry.status), children: ["[", planStepSymbol(entry.status), "]"] }), _jsx(Text, { children: entry.step })] }, `plan-${i}`))) })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Timeline, { entries: timeline, follow: follow }) })] }));
|
|
104
91
|
}
|
|
105
|
-
// ─── Main Detail View (split layout) ─────────────────────────────
|
|
106
92
|
export function IssueDetailView(props) {
|
|
107
93
|
const { allIssues, activeDetailKey, follow, ...detailProps } = props;
|
|
108
94
|
const showSidebar = allIssues.length > 1;
|
|
109
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [showSidebar && _jsx(CompactSidebar, { issues: allIssues, activeKey: activeDetailKey }), _jsx(DetailPanel, { ...detailProps, follow: follow })] }), _jsx(
|
|
95
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [showSidebar && _jsx(CompactSidebar, { issues: allIssues, activeKey: activeDetailKey }), _jsx(DetailPanel, { ...detailProps, follow: follow })] }), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "detail", follow: follow }) })] }));
|
|
110
96
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
2
|
+
import { Box, Text, useStdout } from "ink";
|
|
3
3
|
import { IssueRow } from "./IssueRow.js";
|
|
4
4
|
import { StatusBar } from "./StatusBar.js";
|
|
5
5
|
import { HelpBar } from "./HelpBar.js";
|
|
6
|
+
// Fixed columns: selector(2) + key(10) + state(11) + run(11) + pr(7) + ago(4) + gaps(6) = ~51
|
|
7
|
+
const FIXED_COLS = 51;
|
|
6
8
|
export function IssueListView({ issues, allIssues, selectedIndex, connected, filter, totalCount }) {
|
|
7
|
-
|
|
9
|
+
const { stdout } = useStdout();
|
|
10
|
+
const cols = stdout?.columns ?? 80;
|
|
11
|
+
const titleWidth = Math.max(0, cols - FIXED_COLS);
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { issues: issues, totalCount: totalCount, filter: filter, connected: connected, allIssues: allIssues }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No issues match the current filter." })) : (issues.map((issue, index) => (_jsx(IssueRow, { issue: issue, selected: index === selectedIndex, titleWidth: titleWidth }, issue.issueKey ?? `${issue.projectId}-${index}`)))) }), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "list" }) })] }));
|
|
8
13
|
}
|
|
@@ -14,9 +14,44 @@ const STATE_COLORS = {
|
|
|
14
14
|
escalated: "red",
|
|
15
15
|
awaiting_input: "yellow",
|
|
16
16
|
};
|
|
17
|
+
const STATE_SHORT = {
|
|
18
|
+
delegated: "queued",
|
|
19
|
+
preparing: "prep",
|
|
20
|
+
implementing: "impl",
|
|
21
|
+
pr_open: "pr open",
|
|
22
|
+
changes_requested: "changes",
|
|
23
|
+
repairing_ci: "ci fix",
|
|
24
|
+
awaiting_queue: "merging",
|
|
25
|
+
repairing_queue: "merge fix",
|
|
26
|
+
done: "done",
|
|
27
|
+
failed: "failed",
|
|
28
|
+
escalated: "escalated",
|
|
29
|
+
awaiting_input: "input",
|
|
30
|
+
};
|
|
31
|
+
const RUN_SHORT = {
|
|
32
|
+
implementation: "impl",
|
|
33
|
+
ci_repair: "ci",
|
|
34
|
+
review_fix: "review",
|
|
35
|
+
queue_repair: "merge",
|
|
36
|
+
};
|
|
37
|
+
const STATUS_SHORT = {
|
|
38
|
+
running: "\u25b8",
|
|
39
|
+
completed: "\u2713",
|
|
40
|
+
failed: "\u2717",
|
|
41
|
+
released: "\u2013",
|
|
42
|
+
};
|
|
17
43
|
function stateColor(state) {
|
|
18
44
|
return STATE_COLORS[state] ?? "white";
|
|
19
45
|
}
|
|
46
|
+
function formatRun(issue) {
|
|
47
|
+
const run = issue.activeRunType ?? issue.latestRunType;
|
|
48
|
+
if (!run)
|
|
49
|
+
return "";
|
|
50
|
+
const runLabel = RUN_SHORT[run] ?? run;
|
|
51
|
+
const status = issue.activeRunType ? "running" : issue.latestRunStatus;
|
|
52
|
+
const statusLabel = status ? STATUS_SHORT[status] ?? status : "";
|
|
53
|
+
return `${runLabel} ${statusLabel}`;
|
|
54
|
+
}
|
|
20
55
|
function formatPr(issue) {
|
|
21
56
|
if (!issue.prNumber)
|
|
22
57
|
return "";
|
|
@@ -44,15 +79,17 @@ function relativeTime(iso) {
|
|
|
44
79
|
return `${days}d`;
|
|
45
80
|
}
|
|
46
81
|
function truncate(text, max) {
|
|
82
|
+
if (max <= 0)
|
|
83
|
+
return "";
|
|
47
84
|
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
48
85
|
}
|
|
49
|
-
export function IssueRow({ issue, selected }) {
|
|
86
|
+
export function IssueRow({ issue, selected, titleWidth }) {
|
|
50
87
|
const key = issue.issueKey ?? issue.projectId;
|
|
51
|
-
const state = issue.factoryState;
|
|
52
|
-
const run = issue
|
|
53
|
-
const runStatus = issue.activeRunType ? "running" : issue.latestRunStatus;
|
|
88
|
+
const state = STATE_SHORT[issue.factoryState] ?? issue.factoryState;
|
|
89
|
+
const run = formatRun(issue);
|
|
54
90
|
const pr = formatPr(issue);
|
|
55
91
|
const ago = relativeTime(issue.updatedAt);
|
|
56
|
-
const
|
|
57
|
-
|
|
92
|
+
const tw = titleWidth ?? 30;
|
|
93
|
+
const title = issue.title ? truncate(issue.title, tw) : "";
|
|
94
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: selected ? "blueBright" : "white", bold: selected, children: selected ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: ` ${key.padEnd(9)}` }), _jsx(Text, { color: stateColor(issue.factoryState), children: ` ${state.padEnd(10)}` }), _jsx(Text, { dimColor: true, children: ` ${run.padEnd(10)}` }), _jsx(Text, { dimColor: true, children: ` ${pr.padEnd(6)}` }), _jsx(Text, { dimColor: true, children: ` ${ago.padStart(3)}` }), title ? _jsx(Text, { dimColor: true, children: ` ${title}` }) : null] }));
|
|
58
95
|
}
|
|
@@ -64,6 +64,9 @@ export function ItemLine({ item, isLast }) {
|
|
|
64
64
|
case "plan":
|
|
65
65
|
content = renderPlan(item);
|
|
66
66
|
break;
|
|
67
|
+
case "userMessage":
|
|
68
|
+
content = (_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "you: " }), _jsx(Text, { children: truncate(item.text ?? "", 120) })] }));
|
|
69
|
+
break;
|
|
67
70
|
default:
|
|
68
71
|
content = renderDefault(item);
|
|
69
72
|
break;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { ItemLine } from "./ItemLine.js";
|
|
4
4
|
function formatTime(iso) {
|
|
5
|
-
return new Date(iso).toLocaleTimeString("en-GB", { hour12: false });
|
|
5
|
+
return new Date(iso).toLocaleTimeString("en-GB", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
6
6
|
}
|
|
7
7
|
function formatDuration(startedAt, endedAt) {
|
|
8
8
|
const ms = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
@@ -10,62 +10,45 @@ function formatDuration(startedAt, endedAt) {
|
|
|
10
10
|
if (seconds < 60)
|
|
11
11
|
return `${seconds}s`;
|
|
12
12
|
const minutes = Math.floor(seconds / 60);
|
|
13
|
-
const
|
|
14
|
-
return `${minutes}m ${
|
|
15
|
-
}
|
|
16
|
-
const CHECK_SYMBOLS = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
failed: "red",
|
|
24
|
-
pending: "yellow",
|
|
13
|
+
const s = seconds % 60;
|
|
14
|
+
return `${minutes}m${s > 0 ? ` ${s}s` : ""}`;
|
|
15
|
+
}
|
|
16
|
+
const CHECK_SYMBOLS = { passed: "\u2713", failed: "\u2717", pending: "\u25cf" };
|
|
17
|
+
const CHECK_COLORS = { passed: "green", failed: "red", pending: "yellow" };
|
|
18
|
+
const RUN_LABELS = {
|
|
19
|
+
implementation: "implement",
|
|
20
|
+
ci_repair: "ci fix",
|
|
21
|
+
review_fix: "review fix",
|
|
22
|
+
queue_repair: "merge fix",
|
|
25
23
|
};
|
|
26
24
|
function FeedRow({ entry }) {
|
|
27
25
|
const feed = entry.feed;
|
|
28
|
-
const
|
|
29
|
-
return (_jsxs(Box, {
|
|
30
|
-
}
|
|
31
|
-
const RUN_TYPE_LABELS = {
|
|
32
|
-
implementation: "implementing",
|
|
33
|
-
ci_repair: "repairing checks",
|
|
34
|
-
review_fix: "addressing feedback",
|
|
35
|
-
queue_repair: "repairing merge",
|
|
36
|
-
};
|
|
37
|
-
function runLabel(runType) {
|
|
38
|
-
return RUN_TYPE_LABELS[runType] ?? runType;
|
|
26
|
+
const label = feed.status ?? feed.feedKind;
|
|
27
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { color: "cyan", children: label.padEnd(14) }), _jsxs(Text, { children: [" ", feed.summary] })] }));
|
|
39
28
|
}
|
|
40
29
|
function RunStartRow({ entry }) {
|
|
41
30
|
const run = entry.run;
|
|
42
|
-
return (_jsxs(Box, {
|
|
31
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { bold: true, color: "yellow", children: (RUN_LABELS[run.runType] ?? run.runType).padEnd(14) }), _jsx(Text, { bold: true, children: " started" })] }));
|
|
43
32
|
}
|
|
44
33
|
function RunEndRow({ entry }) {
|
|
45
34
|
const run = entry.run;
|
|
46
35
|
const color = run.status === "completed" ? "green" : "red";
|
|
47
|
-
const
|
|
48
|
-
return (_jsxs(Box, {
|
|
36
|
+
const dur = run.endedAt ? ` ${formatDuration(run.startedAt, run.endedAt)}` : "";
|
|
37
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { bold: true, color: color, children: (RUN_LABELS[run.runType] ?? run.runType).padEnd(14) }), _jsxs(Text, { bold: true, color: color, children: [" ", run.status] }), dur ? _jsx(Text, { dimColor: true, children: dur }) : null] }));
|
|
49
38
|
}
|
|
50
39
|
function ItemRow({ entry }) {
|
|
51
|
-
|
|
52
|
-
return (_jsx(Box, { paddingLeft: 2, children: _jsx(ItemLine, { item: item, isLast: false }) }));
|
|
40
|
+
return (_jsx(Box, { paddingLeft: 2, children: _jsx(ItemLine, { item: entry.item, isLast: false }) }));
|
|
53
41
|
}
|
|
54
42
|
function CIChecksRow({ entry }) {
|
|
55
43
|
const ci = entry.ciChecks;
|
|
56
|
-
return (_jsxs(Box, {
|
|
44
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { color: CHECK_COLORS[ci.overall] ?? "white", children: "checks".padEnd(14) }), _jsx(Text, { children: " " }), ci.checks.map((check, i) => (_jsxs(Text, { children: [_jsx(Text, { color: CHECK_COLORS[check.status] ?? "white", children: CHECK_SYMBOLS[check.status] ?? " " }), _jsxs(Text, { dimColor: true, children: [check.name, " "] })] }, `c-${i}`)))] }));
|
|
57
45
|
}
|
|
58
46
|
export function TimelineRow({ entry }) {
|
|
59
47
|
switch (entry.kind) {
|
|
60
|
-
case "feed":
|
|
61
|
-
|
|
62
|
-
case "run-
|
|
63
|
-
|
|
64
|
-
case "
|
|
65
|
-
return _jsx(RunEndRow, { entry: entry });
|
|
66
|
-
case "item":
|
|
67
|
-
return _jsx(ItemRow, { entry: entry });
|
|
68
|
-
case "ci-checks":
|
|
69
|
-
return _jsx(CIChecksRow, { entry: entry });
|
|
48
|
+
case "feed": return _jsx(FeedRow, { entry: entry });
|
|
49
|
+
case "run-start": return _jsx(RunStartRow, { entry: entry });
|
|
50
|
+
case "run-end": return _jsx(RunEndRow, { entry: entry });
|
|
51
|
+
case "item": return _jsx(ItemRow, { entry: entry });
|
|
52
|
+
case "ci-checks": return _jsx(CIChecksRow, { entry: entry });
|
|
70
53
|
}
|
|
71
54
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Format ISO timestamp as HH:MM:SS (24h, en-GB). */
|
|
2
|
+
export function formatTime(iso) {
|
|
3
|
+
return new Date(iso).toLocaleTimeString("en-GB", { hour12: false });
|
|
4
|
+
}
|
|
5
|
+
/** Format ISO timestamp as compact relative time: "3s", "12m", "2h", "5d". */
|
|
6
|
+
export function relativeTime(iso) {
|
|
7
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
8
|
+
if (ms < 0)
|
|
9
|
+
return "now";
|
|
10
|
+
const seconds = Math.floor(ms / 1000);
|
|
11
|
+
if (seconds < 60)
|
|
12
|
+
return `${seconds}s`;
|
|
13
|
+
const minutes = Math.floor(seconds / 60);
|
|
14
|
+
if (minutes < 60)
|
|
15
|
+
return `${minutes}m`;
|
|
16
|
+
const hours = Math.floor(minutes / 60);
|
|
17
|
+
if (hours < 24)
|
|
18
|
+
return `${hours}h`;
|
|
19
|
+
const days = Math.floor(hours / 24);
|
|
20
|
+
return `${days}d`;
|
|
21
|
+
}
|
|
22
|
+
/** Format millisecond duration as "2m 30s" or "45s". */
|
|
23
|
+
export function formatDuration(ms) {
|
|
24
|
+
const seconds = Math.floor(ms / 1000);
|
|
25
|
+
if (seconds < 60)
|
|
26
|
+
return `${seconds}s`;
|
|
27
|
+
const minutes = Math.floor(seconds / 60);
|
|
28
|
+
const remainingSeconds = seconds % 60;
|
|
29
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
30
|
+
}
|
|
31
|
+
/** Format token count with k/M suffix. */
|
|
32
|
+
export function formatTokens(n) {
|
|
33
|
+
if (n >= 1_000_000)
|
|
34
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
35
|
+
if (n >= 1_000)
|
|
36
|
+
return `${(n / 1_000).toFixed(1)}k`;
|
|
37
|
+
return String(n);
|
|
38
|
+
}
|
|
39
|
+
/** Truncate text to max length with ellipsis. Collapses newlines. */
|
|
40
|
+
export function truncate(text, max) {
|
|
41
|
+
const line = text.replace(/\n/g, " ").trim();
|
|
42
|
+
return line.length > max ? `${line.slice(0, max - 1)}\u2026` : line;
|
|
43
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared SSE (Server-Sent Events) stream parser.
|
|
3
|
+
* Extracts event type + data from a ReadableStream, calls onEvent for each complete event.
|
|
4
|
+
*/
|
|
5
|
+
export async function readSSEStream(body, onEvent) {
|
|
6
|
+
const reader = body.getReader();
|
|
7
|
+
const decoder = new TextDecoder();
|
|
8
|
+
let buffer = "";
|
|
9
|
+
let eventType = "";
|
|
10
|
+
let dataLines = [];
|
|
11
|
+
while (true) {
|
|
12
|
+
const { done, value } = await reader.read();
|
|
13
|
+
if (done)
|
|
14
|
+
break;
|
|
15
|
+
buffer += decoder.decode(value, { stream: true });
|
|
16
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
17
|
+
while (newlineIndex !== -1) {
|
|
18
|
+
const rawLine = buffer.slice(0, newlineIndex);
|
|
19
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
20
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
21
|
+
if (!line) {
|
|
22
|
+
if (dataLines.length > 0) {
|
|
23
|
+
onEvent(eventType, dataLines.join("\n"));
|
|
24
|
+
dataLines = [];
|
|
25
|
+
eventType = "";
|
|
26
|
+
}
|
|
27
|
+
newlineIndex = buffer.indexOf("\n");
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (line.startsWith(":")) {
|
|
31
|
+
newlineIndex = buffer.indexOf("\n");
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (line.startsWith("event:")) {
|
|
35
|
+
eventType = line.slice(6).trim();
|
|
36
|
+
}
|
|
37
|
+
else if (line.startsWith("data:")) {
|
|
38
|
+
dataLines.push(line.slice(5).trimStart());
|
|
39
|
+
}
|
|
40
|
+
newlineIndex = buffer.indexOf("\n");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// ─── Factory State Colors ─────────────────────────────────────────
|
|
2
|
+
export const FACTORY_STATE_COLORS = {
|
|
3
|
+
delegated: "blue",
|
|
4
|
+
preparing: "blue",
|
|
5
|
+
implementing: "yellow",
|
|
6
|
+
awaiting_input: "yellow",
|
|
7
|
+
pr_open: "cyan",
|
|
8
|
+
changes_requested: "magenta",
|
|
9
|
+
repairing_ci: "magenta",
|
|
10
|
+
repairing_queue: "magenta",
|
|
11
|
+
awaiting_queue: "green",
|
|
12
|
+
done: "green",
|
|
13
|
+
failed: "red",
|
|
14
|
+
escalated: "red",
|
|
15
|
+
};
|
|
16
|
+
// ─── Item Status Symbols & Colors ─────────────────────────────────
|
|
17
|
+
export const ITEM_STATUS_SYMBOLS = {
|
|
18
|
+
completed: "\u2713",
|
|
19
|
+
failed: "\u2717",
|
|
20
|
+
declined: "\u2717",
|
|
21
|
+
inProgress: "\u25cf",
|
|
22
|
+
};
|
|
23
|
+
export const ITEM_STATUS_COLORS = {
|
|
24
|
+
completed: "green",
|
|
25
|
+
failed: "red",
|
|
26
|
+
declined: "red",
|
|
27
|
+
inProgress: "yellow",
|
|
28
|
+
};
|
|
29
|
+
// ─── CI Check Symbols & Colors ────────────────────────────────────
|
|
30
|
+
export const CHECK_SYMBOLS = {
|
|
31
|
+
passed: "\u2713",
|
|
32
|
+
failed: "\u2717",
|
|
33
|
+
pending: "\u25cf",
|
|
34
|
+
};
|
|
35
|
+
export const CHECK_COLORS = {
|
|
36
|
+
passed: "green",
|
|
37
|
+
failed: "red",
|
|
38
|
+
pending: "yellow",
|
|
39
|
+
};
|
|
40
|
+
// ─── Feed Event Colors ────────────────────────────────────────────
|
|
41
|
+
export const FEED_LEVEL_COLORS = {
|
|
42
|
+
info: "white",
|
|
43
|
+
warn: "yellow",
|
|
44
|
+
error: "red",
|
|
45
|
+
};
|
|
46
|
+
export const FEED_KIND_COLORS = {
|
|
47
|
+
stage: "cyan",
|
|
48
|
+
turn: "yellow",
|
|
49
|
+
github: "green",
|
|
50
|
+
webhook: "blue",
|
|
51
|
+
agent: "magenta",
|
|
52
|
+
service: "white",
|
|
53
|
+
workflow: "cyan",
|
|
54
|
+
linear: "blue",
|
|
55
|
+
};
|
|
@@ -303,7 +303,7 @@ export function appendCodexItemToTimeline(timeline, params, activeRunId) {
|
|
|
303
303
|
const type = typeof itemObj.type === "string" ? itemObj.type : "unknown";
|
|
304
304
|
const status = typeof itemObj.status === "string" ? itemObj.status : "inProgress";
|
|
305
305
|
const item = { id, type, status };
|
|
306
|
-
if (type === "agentMessage" && typeof itemObj.text === "string")
|
|
306
|
+
if ((type === "agentMessage" || type === "userMessage") && typeof itemObj.text === "string")
|
|
307
307
|
item.text = itemObj.text;
|
|
308
308
|
if (type === "commandExecution") {
|
|
309
309
|
const cmd = itemObj.command;
|
package/dist/http.js
CHANGED
|
@@ -323,7 +323,7 @@ export async function buildHttpServer(config, service, logger) {
|
|
|
323
323
|
if ("error" in result) {
|
|
324
324
|
return reply.code(409).send({ ok: false, reason: result.error });
|
|
325
325
|
}
|
|
326
|
-
return reply.send({ ok: true,
|
|
326
|
+
return reply.send({ ok: true, ...result });
|
|
327
327
|
});
|
|
328
328
|
app.get("/api/feed", async (request, reply) => {
|
|
329
329
|
const feedQuery = {
|
package/dist/service.js
CHANGED
|
@@ -218,22 +218,58 @@ export class PatchRelayService {
|
|
|
218
218
|
this.codex.on("notification", handler);
|
|
219
219
|
return () => { this.codex.off("notification", handler); };
|
|
220
220
|
}
|
|
221
|
-
async promptIssue(issueKey, text) {
|
|
221
|
+
async promptIssue(issueKey, text, source = "watch") {
|
|
222
222
|
const issue = this.db.getIssueByKey(issueKey);
|
|
223
223
|
if (!issue)
|
|
224
224
|
return undefined;
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
// Publish to operator feed so all clients see the prompt
|
|
226
|
+
this.feed.publish({
|
|
227
|
+
level: "info",
|
|
228
|
+
kind: "comment",
|
|
229
|
+
issueKey: issue.issueKey,
|
|
230
|
+
projectId: issue.projectId,
|
|
231
|
+
stage: issue.factoryState,
|
|
232
|
+
status: "operator_prompt",
|
|
233
|
+
summary: `Operator prompt (${source})`,
|
|
234
|
+
detail: text.slice(0, 200),
|
|
235
|
+
});
|
|
236
|
+
// If no active run, queue as pending context for the next run
|
|
237
|
+
if (!issue.activeRunId) {
|
|
238
|
+
const existing = issue.pendingRunContextJson
|
|
239
|
+
? JSON.parse(issue.pendingRunContextJson)
|
|
240
|
+
: {};
|
|
241
|
+
this.db.upsertIssue({
|
|
242
|
+
projectId: issue.projectId,
|
|
243
|
+
linearIssueId: issue.linearIssueId,
|
|
244
|
+
pendingRunContextJson: JSON.stringify({ ...existing, operatorPrompt: text }),
|
|
245
|
+
});
|
|
246
|
+
return { delivered: false, queued: true };
|
|
247
|
+
}
|
|
227
248
|
const run = this.db.getRun(issue.activeRunId);
|
|
228
|
-
if (!run?.threadId || !run.turnId)
|
|
229
|
-
return { error: "
|
|
249
|
+
if (!run?.threadId || !run.turnId) {
|
|
250
|
+
return { error: "Active run has no thread or turn yet" };
|
|
251
|
+
}
|
|
230
252
|
try {
|
|
231
|
-
await this.codex.steerTurn({
|
|
253
|
+
await this.codex.steerTurn({
|
|
254
|
+
threadId: run.threadId,
|
|
255
|
+
turnId: run.turnId,
|
|
256
|
+
input: `Operator prompt (${source}):\n\n${text}`,
|
|
257
|
+
});
|
|
232
258
|
return { delivered: true };
|
|
233
259
|
}
|
|
234
260
|
catch (error) {
|
|
261
|
+
// Turn may have completed between check and steer — queue for next run
|
|
235
262
|
const msg = error instanceof Error ? error.message : String(error);
|
|
236
|
-
|
|
263
|
+
this.logger.warn({ issueKey, error: msg }, "steerTurn failed, queuing prompt for next run");
|
|
264
|
+
const existing = issue.pendingRunContextJson
|
|
265
|
+
? JSON.parse(issue.pendingRunContextJson)
|
|
266
|
+
: {};
|
|
267
|
+
this.db.upsertIssue({
|
|
268
|
+
projectId: issue.projectId,
|
|
269
|
+
linearIssueId: issue.linearIssueId,
|
|
270
|
+
pendingRunContextJson: JSON.stringify({ ...existing, operatorPrompt: text }),
|
|
271
|
+
});
|
|
272
|
+
return { delivered: false, queued: true };
|
|
237
273
|
}
|
|
238
274
|
}
|
|
239
275
|
retryIssue(issueKey) {
|