patchrelay 0.21.1 → 0.23.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
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useReducer } from "react";
|
|
2
3
|
import { Box, Text, useStdout } from "ink";
|
|
3
4
|
import { IssueRow } from "./IssueRow.js";
|
|
4
5
|
import { StatusBar } from "./StatusBar.js";
|
|
5
6
|
import { HelpBar } from "./HelpBar.js";
|
|
6
|
-
|
|
7
|
+
// selector(2) + key(10) + status(13) + pr(7) + ago(4) + gaps = ~36
|
|
8
|
+
const FIXED_COLS = 40;
|
|
7
9
|
const CHROME_ROWS = 4;
|
|
8
10
|
export function IssueListView({ issues, allIssues, selectedIndex, connected, filter, totalCount }) {
|
|
9
11
|
const { stdout } = useStdout();
|
|
@@ -11,6 +13,9 @@ export function IssueListView({ issues, allIssues, selectedIndex, connected, fil
|
|
|
11
13
|
const rows = stdout?.rows ?? 24;
|
|
12
14
|
const titleWidth = Math.max(0, cols - FIXED_COLS);
|
|
13
15
|
const maxVisible = Math.max(1, rows - CHROME_ROWS);
|
|
16
|
+
// Periodic refresh for elapsed times
|
|
17
|
+
const [, tick] = useReducer((c) => c + 1, 0);
|
|
18
|
+
useEffect(() => { const id = setInterval(tick, 5000); return () => clearInterval(id); }, []);
|
|
14
19
|
let startIndex = 0;
|
|
15
20
|
if (issues.length > maxVisible) {
|
|
16
21
|
startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), issues.length - maxVisible));
|
|
@@ -83,13 +83,27 @@ function truncate(text, max) {
|
|
|
83
83
|
return "";
|
|
84
84
|
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
85
85
|
}
|
|
86
|
+
function formatStatus(issue) {
|
|
87
|
+
const state = STATE_SHORT[issue.factoryState] ?? issue.factoryState;
|
|
88
|
+
const run = issue.activeRunType ?? issue.latestRunType;
|
|
89
|
+
const runLabel = run ? (RUN_SHORT[run] ?? run) : "";
|
|
90
|
+
const status = issue.activeRunType ? "running" : issue.latestRunStatus;
|
|
91
|
+
const statusSym = status ? (STATUS_SHORT[status] ?? "") : "";
|
|
92
|
+
// If run type matches state, just show state + status symbol
|
|
93
|
+
if (runLabel && state === (STATE_SHORT[issue.factoryState] ?? issue.factoryState)) {
|
|
94
|
+
if (issue.activeRunType)
|
|
95
|
+
return `${state} ${statusSym}`;
|
|
96
|
+
if (status)
|
|
97
|
+
return `${state} ${statusSym}`;
|
|
98
|
+
}
|
|
99
|
+
return state;
|
|
100
|
+
}
|
|
86
101
|
export function IssueRow({ issue, selected, titleWidth }) {
|
|
87
102
|
const key = issue.issueKey ?? issue.projectId;
|
|
88
|
-
const
|
|
89
|
-
const run = formatRun(issue);
|
|
103
|
+
const status = formatStatus(issue);
|
|
90
104
|
const pr = formatPr(issue);
|
|
91
105
|
const ago = relativeTime(issue.updatedAt);
|
|
92
106
|
const tw = titleWidth ?? 30;
|
|
93
107
|
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: ` ${
|
|
108
|
+
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: ` ${status.padEnd(12)}` }), _jsx(Text, { dimColor: true, children: ` ${pr.padEnd(6)}` }), _jsx(Text, { dimColor: true, children: ` ${ago.padStart(3)}` }), title ? _jsx(Text, { dimColor: true, children: ` ${title}` }) : null] }));
|
|
95
109
|
}
|
|
@@ -39,9 +39,10 @@ function cleanCommand(raw) {
|
|
|
39
39
|
function renderCommand(item) {
|
|
40
40
|
const cmd = cleanCommand(item.command ?? "?");
|
|
41
41
|
const exitCode = item.exitCode;
|
|
42
|
-
const exitLabel = exitCode !== undefined
|
|
42
|
+
const exitLabel = exitCode !== undefined && exitCode !== 0 ? ` exit:${exitCode}` : "";
|
|
43
43
|
const duration = item.durationMs !== undefined ? ` ${(item.durationMs / 1000).toFixed(1)}s` : "";
|
|
44
|
-
|
|
44
|
+
const suffix = `${exitLabel}${duration}`;
|
|
45
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "$ " }), _jsx(Text, { children: cmd }), exitLabel && _jsx(Text, { color: "red", children: exitLabel }), !exitLabel && suffix && _jsx(Text, { dimColor: true, children: suffix })] }), item.output && item.status === "inProgress" && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", item.output.split("\n").filter(Boolean).at(-1) ?? ""] }))] }));
|
|
45
46
|
}
|
|
46
47
|
function renderFileChange(item) {
|
|
47
48
|
const count = item.changes?.length ?? 0;
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { Box, Static, Text, useStdout } from "ink";
|
|
3
4
|
import { TimelineRow } from "./TimelineRow.js";
|
|
4
|
-
const
|
|
5
|
+
const ACTIVE_TAIL = 8;
|
|
6
|
+
function isFinalized(entry) {
|
|
7
|
+
if (entry.kind === "item" && entry.item?.status === "inProgress")
|
|
8
|
+
return false;
|
|
9
|
+
if (entry.kind === "run-start")
|
|
10
|
+
return false; // keep run-start in active area until run ends
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
5
13
|
export function Timeline({ entries, follow }) {
|
|
6
14
|
const { stdout } = useStdout();
|
|
7
15
|
const rows = stdout?.rows ?? 24;
|
|
8
|
-
const
|
|
9
|
-
//
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
const maxActive = Math.max(ACTIVE_TAIL, rows - 12);
|
|
17
|
+
// Split: finalized entries go to Static (terminal scrollback), active entries re-render
|
|
18
|
+
const splitIndex = useMemo(() => {
|
|
19
|
+
if (!follow)
|
|
20
|
+
return 0; // follow OFF: everything in active area (re-renders)
|
|
21
|
+
// Find the boundary: keep the last maxActive entries in the active area
|
|
22
|
+
return Math.max(0, entries.length - maxActive);
|
|
23
|
+
}, [entries.length, follow, maxActive]);
|
|
24
|
+
const finalized = entries.slice(0, splitIndex);
|
|
25
|
+
const active = entries.slice(splitIndex);
|
|
14
26
|
if (entries.length === 0) {
|
|
15
27
|
return _jsx(Text, { dimColor: true, children: "No timeline events yet." });
|
|
16
28
|
}
|
|
17
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", children: [finalized.length > 0 && (_jsx(Static, { items: finalized, children: (entry) => _jsx(TimelineRow, { entry: entry }, entry.id) })), active.map((entry) => (_jsx(TimelineRow, { entry: entry }, entry.id)))] }));
|
|
18
30
|
}
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -275,6 +275,13 @@ export class RunOrchestrator {
|
|
|
275
275
|
}
|
|
276
276
|
// Emit ephemeral progress activity to Linear for notable in-flight events
|
|
277
277
|
this.maybeEmitProgressActivity(notification, run);
|
|
278
|
+
// Sync codex plan to Linear session when it updates
|
|
279
|
+
if (notification.method === "turn/plan/updated") {
|
|
280
|
+
const issue = this.db.getIssue(run.projectId, run.linearIssueId);
|
|
281
|
+
if (issue) {
|
|
282
|
+
void this.syncLinearSessionWithCodexPlan(issue, notification.params);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
278
285
|
if (notification.method !== "turn/completed")
|
|
279
286
|
return;
|
|
280
287
|
const thread = await this.readThreadWithRetry(threadId);
|
|
@@ -687,6 +694,44 @@ export class RunOrchestrator {
|
|
|
687
694
|
this.logger.warn({ issueKey: issue.issueKey, error: msg }, "Failed to update Linear plan");
|
|
688
695
|
}
|
|
689
696
|
}
|
|
697
|
+
async syncLinearSessionWithCodexPlan(issue, params) {
|
|
698
|
+
if (!issue.agentSessionId)
|
|
699
|
+
return;
|
|
700
|
+
const plan = params.plan;
|
|
701
|
+
if (!Array.isArray(plan))
|
|
702
|
+
return;
|
|
703
|
+
const STATUS_MAP = {
|
|
704
|
+
pending: "pending",
|
|
705
|
+
inProgress: "inProgress",
|
|
706
|
+
completed: "completed",
|
|
707
|
+
};
|
|
708
|
+
const steps = plan.map((entry) => {
|
|
709
|
+
const e = entry;
|
|
710
|
+
const step = typeof e.step === "string" ? e.step : String(e.step ?? "");
|
|
711
|
+
const status = typeof e.status === "string" ? (STATUS_MAP[e.status] ?? "pending") : "pending";
|
|
712
|
+
return { content: step, status };
|
|
713
|
+
});
|
|
714
|
+
// Prepend a "Prepare workspace" completed step and append a "Merge" pending step
|
|
715
|
+
// to frame the codex plan within the PatchRelay lifecycle
|
|
716
|
+
const fullPlan = [
|
|
717
|
+
{ content: "Prepare workspace", status: "completed" },
|
|
718
|
+
...steps,
|
|
719
|
+
{ content: "Merge", status: "pending" },
|
|
720
|
+
];
|
|
721
|
+
try {
|
|
722
|
+
const linear = await this.linearProvider.forProject(issue.projectId);
|
|
723
|
+
if (!linear?.updateAgentSession)
|
|
724
|
+
return;
|
|
725
|
+
await linear.updateAgentSession({
|
|
726
|
+
agentSessionId: issue.agentSessionId,
|
|
727
|
+
plan: fullPlan,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
732
|
+
this.logger.warn({ issueKey: issue.issueKey, error: msg }, "Failed to sync codex plan to Linear");
|
|
733
|
+
}
|
|
734
|
+
}
|
|
690
735
|
async readThreadWithRetry(threadId, maxRetries = 3) {
|
|
691
736
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
692
737
|
try {
|