patchrelay 0.43.0 → 0.45.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/config/patchrelay.example.json +7 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/watch/App.js +2 -1
- package/dist/cli/watch/HelpBar.js +9 -3
- package/dist/cli/watch/IssueDetailView.js +4 -4
- package/dist/cli/watch/IssueListView.js +8 -8
- package/dist/cli/watch/IssueRow.js +16 -10
- package/dist/cli/watch/StatusBar.js +15 -1
- package/dist/cli/watch/detail-rows.js +1 -1
- package/dist/codex-app-server.js +13 -0
- package/dist/config.js +2 -0
- package/dist/index.js +1 -1
- package/dist/run-orchestrator.js +28 -1
- package/dist/service.js +4 -2
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli/watch/App.js
CHANGED
|
@@ -73,6 +73,7 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
73
73
|
const filtered = useMemo(() => filterIssues(state.issues, state.filter), [state.issues, state.filter]);
|
|
74
74
|
const [frozen, setFrozen] = useState(false);
|
|
75
75
|
const width = Math.max(20, stdout?.columns ?? 80);
|
|
76
|
+
const compact = width < 90;
|
|
76
77
|
useWatchStream({ baseUrl, bearerToken, dispatch, active: !frozen });
|
|
77
78
|
useDetailStream({ baseUrl, bearerToken, issueKey: state.activeDetailKey, dispatch, active: !frozen });
|
|
78
79
|
const [promptMode, setPromptMode] = useState(false);
|
|
@@ -400,7 +401,7 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
400
401
|
: promptStatus
|
|
401
402
|
? measureRenderedTextRows(promptStatus, width)
|
|
402
403
|
: 0);
|
|
403
|
-
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt, filter: state.filter, totalCount: state.issues.length, frozen: frozen })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [state.activeDetailKey && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Issues" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { bold: true, children: state.activeDetailKey }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { dimColor: true, children: state.detailTab === "timeline" ? "Timeline" : "History" })] })), _jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, scrollOffset: state.detailScrollOffset, unreadBelow: state.detailUnreadBelow, activeRunStartedAt: state.activeRunStartedAt, activeRunId: state.activeRunId, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, detailTab: state.detailTab, rawRuns: state.rawRuns, rawFeedEvents: state.rawFeedEvents, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt, reservedRows: reservedRows, onLayoutChange: (viewportRows, contentRows) => {
|
|
404
|
+
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt, filter: state.filter, totalCount: state.issues.length, frozen: frozen, compact: compact })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [state.activeDetailKey && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Issues" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { bold: true, children: state.activeDetailKey }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { dimColor: true, children: state.detailTab === "timeline" ? "Timeline" : "History" })] })), _jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, scrollOffset: state.detailScrollOffset, unreadBelow: state.detailUnreadBelow, activeRunStartedAt: state.activeRunStartedAt, activeRunId: state.activeRunId, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, detailTab: state.detailTab, rawRuns: state.rawRuns, rawFeedEvents: state.rawFeedEvents, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt, compact: compact, reservedRows: reservedRows, onLayoutChange: (viewportRows, contentRows) => {
|
|
404
405
|
dispatch({ type: "detail-layout-updated", viewportRows, contentRows });
|
|
405
406
|
} }), promptMode && (_jsx(PromptComposer, { buffer: promptBuffer, cursor: promptCursor })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] })) : null }));
|
|
406
407
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
export function buildHelpBarText(view, follow, detailTab) {
|
|
3
|
+
export function buildHelpBarText(view, follow, detailTab, compact = false) {
|
|
4
4
|
if (view === "detail") {
|
|
5
5
|
const tabHint = detailTab === "history" ? "t: timeline" : "h: history";
|
|
6
|
+
if (compact) {
|
|
7
|
+
return "j/k: scroll f: live p: prompt y/c/o: copy r: retry s: stop esc: list q: quit";
|
|
8
|
+
}
|
|
6
9
|
return [
|
|
7
10
|
tabHint,
|
|
8
11
|
"j/k: scroll",
|
|
@@ -19,9 +22,12 @@ export function buildHelpBarText(view, follow, detailTab) {
|
|
|
19
22
|
.filter(Boolean)
|
|
20
23
|
.join(" ");
|
|
21
24
|
}
|
|
25
|
+
if (compact) {
|
|
26
|
+
return "Enter: detail Tab: filter x: pause q: quit";
|
|
27
|
+
}
|
|
22
28
|
return "Enter: detail Tab: filter";
|
|
23
29
|
}
|
|
24
|
-
export function HelpBar({ view, follow, detailTab }) {
|
|
25
|
-
const text = buildHelpBarText(view, follow, detailTab);
|
|
30
|
+
export function HelpBar({ view, follow, detailTab, compact = false }) {
|
|
31
|
+
const text = buildHelpBarText(view, follow, detailTab, compact);
|
|
26
32
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
27
33
|
}
|
|
@@ -5,7 +5,7 @@ import { HelpBar, buildHelpBarText } from "./HelpBar.js";
|
|
|
5
5
|
import { buildDetailLines } from "./detail-rows.js";
|
|
6
6
|
import { buildDetailStatusSegments, buildDetailStatusText } from "./detail-status.js";
|
|
7
7
|
import { measureRenderedTextRows } from "./layout-measure.js";
|
|
8
|
-
export function IssueDetailView({ issue, timeline, follow, scrollOffset, unreadBelow, activeRunStartedAt, activeRunId, tokenUsage, diffSummary, plan, issueContext, detailTab, rawRuns, rawFeedEvents, connected, lastServerMessageAt, reservedRows = 0, onLayoutChange, }) {
|
|
8
|
+
export function IssueDetailView({ issue, timeline, follow, scrollOffset, unreadBelow, activeRunStartedAt, activeRunId, tokenUsage, diffSummary, plan, issueContext, detailTab, rawRuns, rawFeedEvents, connected, lastServerMessageAt, reservedRows = 0, compact = false, onLayoutChange, }) {
|
|
9
9
|
const { stdout } = useStdout();
|
|
10
10
|
const width = Math.max(20, stdout?.columns ?? 80);
|
|
11
11
|
const totalRows = stdout?.rows ?? 24;
|
|
@@ -17,9 +17,9 @@ export function IssueDetailView({ issue, timeline, follow, scrollOffset, unreadB
|
|
|
17
17
|
connected,
|
|
18
18
|
lastServerMessageAt,
|
|
19
19
|
}), width);
|
|
20
|
-
const helpRows = measureRenderedTextRows(buildHelpBarText("detail", follow, detailTab), width);
|
|
20
|
+
const helpRows = measureRenderedTextRows(buildHelpBarText("detail", follow, detailTab, compact), width);
|
|
21
21
|
return statusRows + helpRows;
|
|
22
|
-
}, [activeRunStartedAt, connected, detailTab, follow, lastServerMessageAt, unreadBelow, width]);
|
|
22
|
+
}, [activeRunStartedAt, connected, detailTab, follow, lastServerMessageAt, unreadBelow, width, compact]);
|
|
23
23
|
const viewportRows = Math.max(4, totalRows - reservedRows - footerRows);
|
|
24
24
|
const lines = useMemo(() => {
|
|
25
25
|
if (!issue) {
|
|
@@ -60,7 +60,7 @@ export function IssueDetailView({ issue, timeline, follow, scrollOffset, unreadB
|
|
|
60
60
|
const start = Math.min(scrollOffset, maxOffset);
|
|
61
61
|
const visibleLines = lines.slice(start, start + viewportRows);
|
|
62
62
|
const fillerCount = Math.max(0, viewportRows - visibleLines.length);
|
|
63
|
-
return (_jsxs(Box, { flexDirection: "column", children: [visibleLines.map((line) => (_jsx(RenderedLine, { line: line }, line.key))), Array.from({ length: fillerCount }, (_, index) => (_jsx(Text, { children: " " }, `detail-fill-${index}`))), _jsx(DetailStatusStrip, { follow: follow, unreadBelow: unreadBelow, activeRunStartedAt: activeRunStartedAt, connected: connected, lastServerMessageAt: lastServerMessageAt }), _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab })] }));
|
|
63
|
+
return (_jsxs(Box, { flexDirection: "column", children: [visibleLines.map((line) => (_jsx(RenderedLine, { line: line }, line.key))), Array.from({ length: fillerCount }, (_, index) => (_jsx(Text, { children: " " }, `detail-fill-${index}`))), _jsx(DetailStatusStrip, { follow: follow, unreadBelow: unreadBelow, activeRunStartedAt: activeRunStartedAt, connected: connected, lastServerMessageAt: lastServerMessageAt }), _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab, compact: compact })] }));
|
|
64
64
|
}
|
|
65
65
|
function RenderedLine({ line }) {
|
|
66
66
|
if (line.segments.length === 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useReducer } from "react";
|
|
3
3
|
import { Box, Text, useStdout } from "ink";
|
|
4
4
|
import { IssueRow, estimateIssueRowHeight } from "./IssueRow.js";
|
|
@@ -6,11 +6,11 @@ import { StatusBar } from "./StatusBar.js";
|
|
|
6
6
|
import { HelpBar } from "./HelpBar.js";
|
|
7
7
|
const FIXED_COLS = 8;
|
|
8
8
|
const CHROME_ROWS = 4;
|
|
9
|
-
export function computeVisibleWindow(issues, selectedIndex, maxRows, cols, titleWidth) {
|
|
9
|
+
export function computeVisibleWindow(issues, selectedIndex, maxRows, cols, titleWidth, compact) {
|
|
10
10
|
if (issues.length === 0)
|
|
11
11
|
return { start: 0, end: 0 };
|
|
12
12
|
const clampedSelected = Math.max(0, Math.min(selectedIndex, issues.length - 1));
|
|
13
|
-
const heights = issues.map((issue, index) => estimateIssueRowHeight(issue, index === clampedSelected, cols, titleWidth));
|
|
13
|
+
const heights = issues.map((issue, index) => estimateIssueRowHeight(issue, index === clampedSelected, cols, titleWidth, compact));
|
|
14
14
|
let start = clampedSelected;
|
|
15
15
|
let end = clampedSelected + 1;
|
|
16
16
|
let usedRows = heights[clampedSelected] ?? 1;
|
|
@@ -34,12 +34,12 @@ export function computeVisibleWindow(issues, selectedIndex, maxRows, cols, title
|
|
|
34
34
|
}
|
|
35
35
|
return { start, end };
|
|
36
36
|
}
|
|
37
|
-
export function IssueListView({ issues, allIssues, selectedIndex, connected, lastServerMessageAt, filter, totalCount, frozen, }) {
|
|
37
|
+
export function IssueListView({ issues, allIssues, selectedIndex, connected, lastServerMessageAt, filter, totalCount, frozen, compact = false, }) {
|
|
38
38
|
const { stdout } = useStdout();
|
|
39
39
|
const cols = stdout?.columns ?? 80;
|
|
40
40
|
const rows = stdout?.rows ?? 24;
|
|
41
|
-
const titleWidth = Math.max(0, cols - FIXED_COLS);
|
|
42
|
-
const maxVisibleRows = Math.max(1, rows - CHROME_ROWS);
|
|
41
|
+
const titleWidth = Math.max(0, cols - (compact ? 24 : FIXED_COLS));
|
|
42
|
+
const maxVisibleRows = Math.max(1, rows - (compact ? 3 : CHROME_ROWS));
|
|
43
43
|
// Periodic refresh for elapsed times
|
|
44
44
|
const [, tick] = useReducer((c) => c + 1, 0);
|
|
45
45
|
useEffect(() => {
|
|
@@ -48,9 +48,9 @@ export function IssueListView({ issues, allIssues, selectedIndex, connected, las
|
|
|
48
48
|
const id = setInterval(tick, 5000);
|
|
49
49
|
return () => clearInterval(id);
|
|
50
50
|
}, [frozen]);
|
|
51
|
-
const { start: startIndex, end: endIndex } = computeVisibleWindow(issues, selectedIndex, maxVisibleRows, cols, titleWidth);
|
|
51
|
+
const { start: startIndex, end: endIndex } = computeVisibleWindow(issues, selectedIndex, maxVisibleRows, cols, titleWidth, compact);
|
|
52
52
|
const visible = issues.slice(startIndex, endIndex);
|
|
53
53
|
const hiddenAbove = startIndex;
|
|
54
54
|
const hiddenBelow = Math.max(0, issues.length - endIndex);
|
|
55
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { issues: issues, totalCount: totalCount, filter: filter, connected: connected, lastServerMessageAt: lastServerMessageAt, allIssues: allIssues, frozen: frozen ?? false }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No issues match the current filter." })) : (_jsxs(_Fragment, { children: [hiddenAbove > 0 &&
|
|
55
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { issues: issues, totalCount: totalCount, filter: filter, connected: connected, lastServerMessageAt: lastServerMessageAt, allIssues: allIssues, frozen: frozen ?? false, compact: compact }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No issues match the current filter." })) : (_jsxs(_Fragment, { children: [hiddenAbove > 0 && _jsx(Text, { dimColor: true, children: compact ? `↑${hiddenAbove}` : ` ${hiddenAbove} more above` }), visible.map((issue, i) => (_jsx(IssueRow, { issue: issue, selected: startIndex + i === selectedIndex, titleWidth: titleWidth, compact: compact }, issue.issueKey ?? `${issue.projectId}-${startIndex + i}`))), hiddenBelow > 0 && _jsx(Text, { dimColor: true, children: compact ? `↓${hiddenBelow}` : ` ${hiddenBelow} more below` })] })) }), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "list", compact: compact }) })] }));
|
|
56
56
|
}
|
|
@@ -67,7 +67,7 @@ function stageLabel(issue) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
// ─── Context facts (what matters right now) ─────────────────────
|
|
70
|
-
function buildFacts(issue, selected) {
|
|
70
|
+
function buildFacts(issue, selected, compact = false) {
|
|
71
71
|
const facts = [];
|
|
72
72
|
const rereviewNeeded = isRereviewNeeded(issue);
|
|
73
73
|
// PR number
|
|
@@ -112,7 +112,7 @@ function buildFacts(issue, selected) {
|
|
|
112
112
|
if (issue.blockedByCount > 0) {
|
|
113
113
|
facts.push({ text: `waiting on ${issue.blockedByKeys.join(", ")}`, color: "yellow" });
|
|
114
114
|
}
|
|
115
|
-
return facts;
|
|
115
|
+
return compact ? facts.slice(0, 3) : facts;
|
|
116
116
|
}
|
|
117
117
|
// ─── What's blocking progress ───────────────────────────────────
|
|
118
118
|
function blockerText(issue) {
|
|
@@ -153,34 +153,40 @@ function blockerText(issue) {
|
|
|
153
153
|
return null;
|
|
154
154
|
}
|
|
155
155
|
// ─── Render ─────────────────────────────────────────────────────
|
|
156
|
-
export function IssueRow({ issue, selected, titleWidth }) {
|
|
156
|
+
export function IssueRow({ issue, selected, titleWidth, compact = false, }) {
|
|
157
157
|
const key = issue.issueKey ?? issue.projectId;
|
|
158
158
|
const tw = titleWidth ?? 60;
|
|
159
159
|
const title = issue.title ? truncate(issue.title, tw) : "";
|
|
160
|
-
const detail = selected ? summarizeIssueStatusNote(issue.statusNote) : undefined;
|
|
161
160
|
const session = sessionDisplay(issue);
|
|
162
|
-
const facts = buildFacts(issue, selected);
|
|
163
|
-
const blocker = selected ? blockerText(issue)
|
|
161
|
+
const facts = buildFacts(issue, selected, compact);
|
|
162
|
+
const blocker = compact || !selected ? null : blockerText(issue);
|
|
163
|
+
const detail = compact || !selected ? undefined : summarizeIssueStatusNote(issue.statusNote);
|
|
164
164
|
const isTerminal = TERMINAL_STATES.has(effectiveState(issue));
|
|
165
165
|
// Terminal issues: compact single line
|
|
166
166
|
if (isTerminal && !selected) {
|
|
167
167
|
return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: ` ${key}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(issue.updatedAt).padStart(4)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: session.color, children: session.label })] }));
|
|
168
168
|
}
|
|
169
|
+
if (compact) {
|
|
170
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: selected ? "blueBright" : "gray", children: selected ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: ` ${key}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(issue.updatedAt).padStart(4)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: session.color, children: session.label }), facts.length > 0 && _jsx(Text, { dimColor: true, children: ` · ` }), facts.map((fact, i) => (_jsxs(Text, { children: [i > 0 ? _jsx(Text, { dimColor: true, children: ` ` }) : null, _jsx(Text, { color: fact.color ?? "white", dimColor: !fact.color, children: fact.text })] }, i)))] }));
|
|
171
|
+
}
|
|
169
172
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: detail ? 1 : 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "blueBright" : "gray", children: selected ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: ` ${key}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(issue.updatedAt).padStart(4)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: session.color, children: session.label }), facts.length > 0 && (_jsx(Text, { dimColor: true, children: ` \u00b7 ` })), facts.map((fact, i) => (_jsxs(Text, { children: [i > 0 ? _jsx(Text, { dimColor: true, children: ` \u00b7 ` }) : null, _jsx(Text, { color: fact.color ?? "white", dimColor: !fact.color, children: fact.text })] }, i)))] }), title ? (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: title }) })) : null, blocker ? (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: "yellow", children: blocker }) })) : null, detail ? (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: detail }) })) : null, selected && issue.factoryState && issue.sessionState ? (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, children: `Debug stage: ${stageLabel(issue)}` }) })) : null] }));
|
|
170
173
|
}
|
|
171
|
-
export function estimateIssueRowHeight(issue, selected, cols, titleWidth) {
|
|
174
|
+
export function estimateIssueRowHeight(issue, selected, cols, titleWidth, compact = false) {
|
|
172
175
|
const width = Math.max(20, cols);
|
|
173
176
|
const key = issue.issueKey ?? issue.projectId;
|
|
174
177
|
const tw = titleWidth ?? 60;
|
|
175
178
|
const title = issue.title ? truncate(issue.title, tw) : "";
|
|
176
|
-
const detail = selected ? summarizeIssueStatusNote(issue.statusNote)
|
|
179
|
+
const detail = compact || !selected ? undefined : summarizeIssueStatusNote(issue.statusNote);
|
|
177
180
|
const session = sessionDisplay(issue);
|
|
178
|
-
const facts = buildFacts(issue, selected);
|
|
179
|
-
const blocker = selected ? blockerText(issue)
|
|
181
|
+
const facts = buildFacts(issue, selected, compact);
|
|
182
|
+
const blocker = compact || !selected ? null : blockerText(issue);
|
|
180
183
|
const isTerminal = TERMINAL_STATES.has(effectiveState(issue));
|
|
181
184
|
if (isTerminal && !selected) {
|
|
182
185
|
return 1;
|
|
183
186
|
}
|
|
187
|
+
if (compact) {
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
184
190
|
const line1Parts = [
|
|
185
191
|
`${selected ? "\u25b8" : " "} ${key}`,
|
|
186
192
|
relativeTime(issue.updatedAt).padStart(4),
|
|
@@ -8,7 +8,7 @@ const FILTER_LABELS = {
|
|
|
8
8
|
"active": "active",
|
|
9
9
|
"non-done": "in progress",
|
|
10
10
|
};
|
|
11
|
-
export function StatusBar({ issues, totalCount, filter, connected, lastServerMessageAt, allIssues, frozen, }) {
|
|
11
|
+
export function StatusBar({ issues, totalCount, filter, connected, lastServerMessageAt, allIssues, frozen, compact = false, }) {
|
|
12
12
|
const showing = filter === "all" ? `${totalCount} issues` : `${issues.length}/${totalCount} issues`;
|
|
13
13
|
const aggregateSource = filter === "all" ? allIssues : issues;
|
|
14
14
|
const agg = computeAggregates(aggregateSource);
|
|
@@ -17,5 +17,19 @@ export function StatusBar({ issues, totalCount, filter, connected, lastServerMes
|
|
|
17
17
|
const intervention = aggregateSource.filter((i) => i.sessionState === "failed" || i.factoryState === "failed" || i.factoryState === "escalated").length;
|
|
18
18
|
const running = aggregateSource.filter((i) => i.sessionState === "running").length;
|
|
19
19
|
const idle = aggregateSource.filter((i) => i.sessionState === "idle").length;
|
|
20
|
+
if (compact) {
|
|
21
|
+
const compactParts = [
|
|
22
|
+
withPr > 0 ? `p${withPr}` : null,
|
|
23
|
+
running > 0 ? `r${running}` : null,
|
|
24
|
+
waitingInput > 0 ? `w${waitingInput}` : null,
|
|
25
|
+
intervention > 0 ? `x${intervention}` : null,
|
|
26
|
+
agg.blocked > 0 ? `b${agg.blocked}` : null,
|
|
27
|
+
agg.ready > 0 ? `q${agg.ready}` : null,
|
|
28
|
+
agg.failed > 0 ? `f${agg.failed}` : null,
|
|
29
|
+
agg.done > 0 ? `d${agg.done}` : null,
|
|
30
|
+
frozen ? "frozen" : null,
|
|
31
|
+
].filter(Boolean);
|
|
32
|
+
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: "patchrelay" }), _jsx(Text, { dimColor: true, children: showing }), _jsxs(Text, { dimColor: true, children: ["[", FILTER_LABELS[filter][0], "]"] }), compactParts.length > 0 ? _jsx(Text, { dimColor: true, children: compactParts.join(" ") }) : null] }), _jsx(FreshnessBadge, { connected: connected, lastServerMessageAt: lastServerMessageAt })] }));
|
|
33
|
+
}
|
|
20
34
|
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: showing }), _jsxs(Text, { dimColor: true, children: ["[", FILTER_LABELS[filter], "]"] }), _jsx(Text, { dimColor: true, children: "|" }), running > 0 && _jsxs(Text, { color: "cyan", children: [running, " running"] }), idle > 0 && _jsxs(Text, { color: "blueBright", children: [idle, " idle"] }), agg.ready > 0 && _jsxs(Text, { color: "blueBright", children: [agg.ready, " ready"] }), agg.blocked > 0 && _jsxs(Text, { color: "yellow", children: [agg.blocked, " blocked"] }), withPr > 0 && _jsxs(Text, { dimColor: true, children: [withPr, " PRs"] }), waitingInput > 0 && _jsxs(Text, { color: "yellow", children: [waitingInput, " needs input"] }), intervention > 0 && _jsxs(Text, { color: "red", children: [intervention, " needs help"] }), agg.done > 0 && _jsxs(Text, { color: "green", children: [agg.done, " done"] }), agg.failed > 0 && _jsxs(Text, { color: "red", children: [agg.failed, " failed"] }), frozen && _jsx(Text, { color: "magenta", children: "frozen" })] }), _jsx(FreshnessBadge, { connected: connected, lastServerMessageAt: lastServerMessageAt })] }));
|
|
21
35
|
}
|
|
@@ -342,7 +342,7 @@ function renderHistoryRunLines(run, index, width, gutter) {
|
|
|
342
342
|
run.commandCount ? `${run.commandCount} cmds` : null,
|
|
343
343
|
run.fileChangeCount ? `${run.fileChangeCount} files` : null,
|
|
344
344
|
].filter((value) => Boolean(value));
|
|
345
|
-
const lines = renderTextLines(`${gutter}${run.status === "completed" ? "✓" : run.status === "failed" ? "✗" : run.status === "running" ? "
|
|
345
|
+
const lines = renderTextLines(`${gutter}${run.status === "completed" ? "✓" : run.status === "failed" ? "✗" : run.status === "running" ? "▶" : "•"} #${index + 1} (${RUN_LABELS[run.runType] ?? run.runType})${duration ? ` ${duration}` : ""}${stats.length ? ` ${stats.join(", ")}` : ""}`, {
|
|
346
346
|
key: `history-run-${run.id}`,
|
|
347
347
|
width,
|
|
348
348
|
style: { color: statusColor },
|
package/dist/codex-app-server.js
CHANGED
|
@@ -43,6 +43,16 @@ export class CodexAppServerClient extends EventEmitter {
|
|
|
43
43
|
this.logger = logger;
|
|
44
44
|
this.spawnProcess = spawnProcess;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Update runtime codex settings used by future thread/thread-fork calls.
|
|
48
|
+
* This allows service config changes to take effect without restarting.
|
|
49
|
+
*/
|
|
50
|
+
setRuntimeConfig(config) {
|
|
51
|
+
this.config = {
|
|
52
|
+
...this.config,
|
|
53
|
+
...config,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
46
56
|
isStarted() {
|
|
47
57
|
return this.started;
|
|
48
58
|
}
|
|
@@ -149,6 +159,7 @@ export class CodexAppServerClient extends EventEmitter {
|
|
|
149
159
|
serviceName: this.config.serviceName ?? "patchrelay",
|
|
150
160
|
model: this.config.model ?? null,
|
|
151
161
|
modelProvider: this.config.modelProvider ?? null,
|
|
162
|
+
reasoningEffort: this.config.reasoningEffort ?? null,
|
|
152
163
|
baseInstructions: this.config.baseInstructions ?? null,
|
|
153
164
|
developerInstructions: this.config.developerInstructions ?? null,
|
|
154
165
|
experimentalRawEvents: this.config.experimentalRawEvents ?? false,
|
|
@@ -164,6 +175,7 @@ export class CodexAppServerClient extends EventEmitter {
|
|
|
164
175
|
sandbox: this.config.sandboxMode,
|
|
165
176
|
model: this.config.model ?? null,
|
|
166
177
|
modelProvider: this.config.modelProvider ?? null,
|
|
178
|
+
reasoningEffort: this.config.reasoningEffort ?? null,
|
|
167
179
|
baseInstructions: this.config.baseInstructions ?? null,
|
|
168
180
|
developerInstructions: this.config.developerInstructions ?? null,
|
|
169
181
|
};
|
|
@@ -181,6 +193,7 @@ export class CodexAppServerClient extends EventEmitter {
|
|
|
181
193
|
sandbox: overrides?.sandboxMode ?? this.config.sandboxMode,
|
|
182
194
|
model: overrides?.model ?? this.config.model ?? null,
|
|
183
195
|
modelProvider: overrides?.modelProvider ?? this.config.modelProvider ?? null,
|
|
196
|
+
reasoningEffort: overrides?.reasoningEffort ?? this.config.reasoningEffort ?? null,
|
|
184
197
|
baseInstructions: overrides?.baseInstructions ?? this.config.baseInstructions ?? null,
|
|
185
198
|
developerInstructions: overrides?.developerInstructions ?? this.config.developerInstructions ?? null,
|
|
186
199
|
};
|
package/dist/config.js
CHANGED
|
@@ -138,6 +138,7 @@ const configSchema = z.object({
|
|
|
138
138
|
request_timeout_ms: z.number().int().positive().default(30000),
|
|
139
139
|
model: z.string().optional(),
|
|
140
140
|
model_provider: z.string().optional(),
|
|
141
|
+
reasoning_effort: z.enum(["low", "medium", "high"]).optional(),
|
|
141
142
|
service_name: z.string().default("patchrelay"),
|
|
142
143
|
base_instructions: z.string().optional(),
|
|
143
144
|
developer_instructions: z.string().optional(),
|
|
@@ -544,6 +545,7 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
|
|
|
544
545
|
requestTimeoutMs: parsed.runner.codex.request_timeout_ms,
|
|
545
546
|
...(parsed.runner.codex.model ? { model: parsed.runner.codex.model } : {}),
|
|
546
547
|
...(parsed.runner.codex.model_provider ? { modelProvider: parsed.runner.codex.model_provider } : {}),
|
|
548
|
+
...(parsed.runner.codex.reasoning_effort ? { reasoningEffort: parsed.runner.codex.reasoning_effort } : {}),
|
|
547
549
|
...(parsed.runner.codex.service_name ? { serviceName: parsed.runner.codex.service_name } : {}),
|
|
548
550
|
...(parsed.runner.codex.base_instructions ? { baseInstructions: parsed.runner.codex.base_instructions } : {}),
|
|
549
551
|
...(parsed.runner.codex.developer_instructions
|
package/dist/index.js
CHANGED
|
@@ -38,7 +38,7 @@ async function main() {
|
|
|
38
38
|
db.runMigrations();
|
|
39
39
|
const codex = new CodexAppServerClient(config.runner.codex, logger);
|
|
40
40
|
const linearProvider = new DatabaseBackedLinearClientProvider(config, db, logger);
|
|
41
|
-
const service = new PatchRelayService(config, db, codex, linearProvider, logger);
|
|
41
|
+
const service = new PatchRelayService(config, db, codex, linearProvider, logger, configPath);
|
|
42
42
|
await service.start();
|
|
43
43
|
const app = await buildHttpServer(config, service, logger);
|
|
44
44
|
try {
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -16,6 +16,7 @@ import { RunReconciler } from "./run-reconciler.js";
|
|
|
16
16
|
import { RunRecoveryService } from "./run-recovery-service.js";
|
|
17
17
|
import { RunWakePlanner } from "./run-wake-planner.js";
|
|
18
18
|
import { getRemainingZombieRecoveryDelayMs } from "./zombie-recovery.js";
|
|
19
|
+
import { loadConfig } from "./config.js";
|
|
19
20
|
function lowerCaseFirst(value) {
|
|
20
21
|
return value ? `${value.slice(0, 1).toLowerCase()}${value.slice(1)}` : value;
|
|
21
22
|
}
|
|
@@ -37,6 +38,7 @@ export class RunOrchestrator {
|
|
|
37
38
|
enqueueIssue;
|
|
38
39
|
logger;
|
|
39
40
|
feed;
|
|
41
|
+
configPath;
|
|
40
42
|
worktreeManager;
|
|
41
43
|
/** Tracks last probe-failure feed event per issue to avoid spamming the operator feed. */
|
|
42
44
|
queueHealthMonitor;
|
|
@@ -54,6 +56,7 @@ export class RunOrchestrator {
|
|
|
54
56
|
runNotificationHandler;
|
|
55
57
|
runReconciler;
|
|
56
58
|
mergedLinearCompletionReconciler;
|
|
59
|
+
codexRuntimeConfig;
|
|
57
60
|
threadPorts = {
|
|
58
61
|
readThreadWithRetry: (threadId, maxRetries) => this.readThreadWithRetry(threadId, maxRetries),
|
|
59
62
|
};
|
|
@@ -70,7 +73,7 @@ export class RunOrchestrator {
|
|
|
70
73
|
};
|
|
71
74
|
activeSessionLeases;
|
|
72
75
|
botIdentity;
|
|
73
|
-
constructor(config, db, codex, linearProvider, enqueueIssue, logger, feed) {
|
|
76
|
+
constructor(config, db, codex, linearProvider, enqueueIssue, logger, feed, configPath) {
|
|
74
77
|
this.config = config;
|
|
75
78
|
this.db = db;
|
|
76
79
|
this.codex = codex;
|
|
@@ -78,7 +81,9 @@ export class RunOrchestrator {
|
|
|
78
81
|
this.enqueueIssue = enqueueIssue;
|
|
79
82
|
this.logger = logger;
|
|
80
83
|
this.feed = feed;
|
|
84
|
+
this.configPath = configPath;
|
|
81
85
|
this.worktreeManager = new WorktreeManager(config);
|
|
86
|
+
this.codexRuntimeConfig = config.runner.codex;
|
|
82
87
|
this.linearSync = new LinearSessionSync(config, db, linearProvider, logger, feed);
|
|
83
88
|
this.leaseService = new IssueSessionLeaseService(db, logger, this.workerId, this.threadPorts.readThreadWithRetry);
|
|
84
89
|
this.activeSessionLeases = this.leaseService.activeSessionLeases;
|
|
@@ -100,6 +105,27 @@ export class RunOrchestrator {
|
|
|
100
105
|
enqueueIssue: (projectId, issueId) => this.enqueueIssue(projectId, issueId),
|
|
101
106
|
}, logger, feed);
|
|
102
107
|
}
|
|
108
|
+
async refreshCodexRuntimeConfig() {
|
|
109
|
+
if (!this.configPath) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const freshConfig = loadConfig(this.configPath, { profile: "service" });
|
|
114
|
+
if (this.codexRuntimeConfig.model === freshConfig.runner.codex.model &&
|
|
115
|
+
this.codexRuntimeConfig.modelProvider === freshConfig.runner.codex.modelProvider &&
|
|
116
|
+
this.codexRuntimeConfig.reasoningEffort === freshConfig.runner.codex.reasoningEffort) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.codexRuntimeConfig = freshConfig.runner.codex;
|
|
120
|
+
this.codex.setRuntimeConfig(this.codexRuntimeConfig);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
this.logger.warn({
|
|
124
|
+
error: error instanceof Error ? error.message : String(error),
|
|
125
|
+
configPath: this.configPath,
|
|
126
|
+
}, "Failed to reload patchrelay runtime config before run; using previous codex configuration");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
103
129
|
resolveRunWake(issue) {
|
|
104
130
|
return this.runWakePlanner.resolveRunWake(issue);
|
|
105
131
|
}
|
|
@@ -111,6 +137,7 @@ export class RunOrchestrator {
|
|
|
111
137
|
}
|
|
112
138
|
// ─── Run ────────────────────────────────────────────────────────
|
|
113
139
|
async run(item) {
|
|
140
|
+
await this.refreshCodexRuntimeConfig();
|
|
114
141
|
const project = this.config.projects.find((p) => p.id === item.projectId);
|
|
115
142
|
if (!project)
|
|
116
143
|
return;
|
package/dist/service.js
CHANGED
|
@@ -17,6 +17,7 @@ export class PatchRelayService {
|
|
|
17
17
|
db;
|
|
18
18
|
codex;
|
|
19
19
|
logger;
|
|
20
|
+
configPath;
|
|
20
21
|
linearProvider;
|
|
21
22
|
orchestrator;
|
|
22
23
|
githubAppTokenManager;
|
|
@@ -29,17 +30,18 @@ export class PatchRelayService {
|
|
|
29
30
|
issueActions;
|
|
30
31
|
startupRecovery;
|
|
31
32
|
trackedIssueListQuery;
|
|
32
|
-
constructor(config, db, codex, linearProvider, logger) {
|
|
33
|
+
constructor(config, db, codex, linearProvider, logger, configPath) {
|
|
33
34
|
this.config = config;
|
|
34
35
|
this.db = db;
|
|
35
36
|
this.codex = codex;
|
|
36
37
|
this.logger = logger;
|
|
38
|
+
this.configPath = configPath;
|
|
37
39
|
this.linearProvider = toLinearClientProvider(linearProvider);
|
|
38
40
|
this.feed = new OperatorEventFeed(db.operatorFeed);
|
|
39
41
|
let enqueueIssue = () => {
|
|
40
42
|
throw new Error("Service runtime enqueueIssue is not initialized");
|
|
41
43
|
};
|
|
42
|
-
this.orchestrator = new RunOrchestrator(config, db, codex, this.linearProvider, (projectId, issueId) => enqueueIssue(projectId, issueId), logger, this.feed);
|
|
44
|
+
this.orchestrator = new RunOrchestrator(config, db, codex, this.linearProvider, (projectId, issueId) => enqueueIssue(projectId, issueId), logger, this.feed, this.configPath);
|
|
43
45
|
this.webhookHandler = new WebhookHandler(config, db, this.linearProvider, codex, (projectId, issueId) => enqueueIssue(projectId, issueId), logger, this.feed);
|
|
44
46
|
this.githubWebhookHandler = new GitHubWebhookHandler(config, db, this.linearProvider, (projectId, issueId) => enqueueIssue(projectId, issueId), logger, codex, this.feed);
|
|
45
47
|
const runtime = new ServiceRuntime(codex, logger, this.orchestrator, { listIssuesReadyForExecution: () => db.listIssuesReadyForExecution() }, this.webhookHandler, {
|