patchrelay 0.20.4 → 0.20.6

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.20.4",
4
- "commit": "0dfb01b26781",
5
- "builtAt": "2026-03-25T22:05:26.938Z"
3
+ "version": "0.20.6",
4
+ "commit": "a427b2793bf2",
5
+ "builtAt": "2026-03-26T08:34:14.765Z"
6
6
  }
@@ -159,5 +159,5 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
159
159
  }
160
160
  }
161
161
  });
162
- 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 })) }));
162
+ 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 }), 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 })) }));
163
163
  }
@@ -35,62 +35,17 @@ function planStepColor(status) {
35
35
  return "yellow";
36
36
  return "white";
37
37
  }
38
- const SIDEBAR_STATE_COLORS = {
39
- delegated: "blue", preparing: "blue",
40
- implementing: "yellow", awaiting_input: "yellow",
41
- pr_open: "cyan",
42
- changes_requested: "magenta", repairing_ci: "magenta", repairing_queue: "magenta",
43
- awaiting_queue: "green", done: "green",
44
- failed: "red", escalated: "red",
45
- };
46
- function CompactSidebar({ issues, activeKey }) {
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
- }) }));
53
- }
54
- const PRIORITY_LABELS = {
55
- 1: { label: "urgent", color: "red" },
56
- 2: { label: "high", color: "yellow" },
57
- 3: { label: "medium", color: "cyan" },
58
- 4: { label: "low", color: "" },
59
- };
60
- function ContextPanel({ issue, ctx }) {
61
- const parts = [];
62
- if (ctx.priority != null && ctx.priority > 0) {
63
- const p = PRIORITY_LABELS[ctx.priority];
64
- parts.push(p ? `${p.label}` : `p${ctx.priority}`);
65
- }
66
- if (issue.prNumber) {
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);
73
- }
74
- if (ctx.runCount > 0)
75
- parts.push(`${ctx.runCount} runs`);
76
- const retries = [
77
- ctx.ciRepairAttempts > 0 ? `ci:${ctx.ciRepairAttempts}` : "",
78
- ctx.queueRepairAttempts > 0 ? `q:${ctx.queueRepairAttempts}` : "",
79
- ctx.reviewFixAttempts > 0 ? `rev:${ctx.reviewFixAttempts}` : "",
80
- ].filter(Boolean).join(" ");
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" : ""] }))] }));
84
- }
85
- function DetailPanel({ issue, timeline, follow, activeRunStartedAt, tokenUsage, diffSummary, plan, issueContext, }) {
38
+ export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, tokenUsage, diffSummary, plan, issueContext, }) {
86
39
  if (!issue) {
87
- return _jsx(Text, { color: "red", children: "Issue not found." });
40
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "Issue not found." }), _jsx(HelpBar, { view: "detail", follow: follow })] }));
88
41
  }
89
42
  const key = issue.issueKey ?? issue.projectId;
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 }) })] }));
91
- }
92
- export function IssueDetailView(props) {
93
- const { allIssues, activeDetailKey, follow, ...detailProps } = props;
94
- const showSidebar = allIssues.length > 1;
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 }) })] }));
43
+ const meta = [];
44
+ if (tokenUsage)
45
+ meta.push(`${formatTokens(tokenUsage.inputTokens)} in / ${formatTokens(tokenUsage.outputTokens)} out`);
46
+ if (diffSummary && diffSummary.filesChanged > 0)
47
+ meta.push(`${diffSummary.filesChanged}f +${diffSummary.linesAdded} -${diffSummary.linesRemoved}`);
48
+ if (issueContext?.runCount)
49
+ meta.push(`${issueContext.runCount} runs`);
50
+ 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: ["#", issue.prNumber] }), activeRunStartedAt && _jsx(ElapsedTime, { startedAt: activeRunStartedAt }), meta.length > 0 && _jsx(Text, { dimColor: true, children: meta.join(" ") }), follow && _jsx(Text, { color: "yellow", children: "follow" })] }), issue.title && _jsx(Text, { children: issue.title }), 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 }) }), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "detail", follow: follow }) })] }));
96
51
  }
@@ -1,13 +1,22 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
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
6
  const FIXED_COLS = 51;
7
+ const CHROME_ROWS = 4;
8
8
  export function IssueListView({ issues, allIssues, selectedIndex, connected, filter, totalCount }) {
9
9
  const { stdout } = useStdout();
10
10
  const cols = stdout?.columns ?? 80;
11
+ const rows = stdout?.rows ?? 24;
11
12
  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" }) })] }));
13
+ const maxVisible = Math.max(1, rows - CHROME_ROWS);
14
+ let startIndex = 0;
15
+ if (issues.length > maxVisible) {
16
+ startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), issues.length - maxVisible));
17
+ }
18
+ const visible = issues.slice(startIndex, startIndex + maxVisible);
19
+ const hiddenAbove = startIndex;
20
+ const hiddenBelow = Math.max(0, issues.length - startIndex - maxVisible);
21
+ 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." })) : (_jsxs(_Fragment, { children: [hiddenAbove > 0 && _jsxs(Text, { dimColor: true, children: [" ", hiddenAbove, " more above"] }), visible.map((issue, i) => (_jsx(IssueRow, { issue: issue, selected: startIndex + i === selectedIndex, titleWidth: titleWidth }, issue.issueKey ?? `${issue.projectId}-${startIndex + i}`))), hiddenBelow > 0 && _jsxs(Text, { dimColor: true, children: [" ", hiddenBelow, " more below"] })] })) }), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "list" }) })] }));
13
22
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  const STATUS_SYMBOL = {
4
4
  completed: "\u2713",
@@ -23,7 +23,7 @@ function truncate(text, max) {
23
23
  return line.length > max ? `${line.slice(0, max - 3)}...` : line;
24
24
  }
25
25
  function renderAgentMessage(item) {
26
- return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "message: " }), _jsx(Text, { children: truncate(item.text ?? "", 120) })] }));
26
+ return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "message: " }), _jsx(Text, { wrap: "wrap", children: item.text ?? "" })] }));
27
27
  }
28
28
  function renderCommand(item) {
29
29
  const cmd = item.command ?? "?";
@@ -64,9 +64,13 @@ 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) })] }));
67
+ case "userMessage": {
68
+ const userText = item.text?.trim();
69
+ if (!userText)
70
+ return _jsx(_Fragment, {});
71
+ content = (_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "you: " }), _jsx(Text, { wrap: "wrap", children: userText })] }));
69
72
  break;
73
+ }
70
74
  default:
71
75
  content = renderDefault(item);
72
76
  break;
@@ -1,14 +1,16 @@
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 { TimelineRow } from "./TimelineRow.js";
4
- const FOLLOW_TAIL_SIZE = 20;
4
+ const DETAIL_CHROME_ROWS = 10;
5
5
  export function Timeline({ entries, follow }) {
6
- const visible = follow && entries.length > FOLLOW_TAIL_SIZE
7
- ? entries.slice(-FOLLOW_TAIL_SIZE)
8
- : entries;
6
+ const { stdout } = useStdout();
7
+ const rows = stdout?.rows ?? 24;
8
+ const maxVisible = Math.max(5, rows - DETAIL_CHROME_ROWS);
9
+ const tailSize = follow ? Math.min(maxVisible, entries.length) : Math.min(maxVisible, entries.length);
10
+ const visible = entries.length > tailSize ? entries.slice(-tailSize) : entries;
9
11
  const skipped = entries.length - visible.length;
10
12
  if (entries.length === 0) {
11
13
  return _jsx(Text, { dimColor: true, children: "No timeline events yet." });
12
14
  }
13
- return (_jsxs(Box, { flexDirection: "column", children: [skipped > 0 && _jsxs(Text, { dimColor: true, children: [" ... ", skipped, " earlier events"] }), visible.map((entry) => (_jsx(TimelineRow, { entry: entry }, entry.id)))] }));
15
+ return (_jsxs(Box, { flexDirection: "column", children: [skipped > 0 && _jsxs(Text, { dimColor: true, children: [" ... ", skipped, " earlier"] }), visible.map((entry) => (_jsx(TimelineRow, { entry: entry }, entry.id)))] }));
14
16
  }
package/dist/http.js CHANGED
@@ -243,6 +243,8 @@ export async function buildHttpServer(config, service, logger) {
243
243
  return reply.code(401).send({ ok: false, reason: "operator_auth_required" });
244
244
  }
245
245
  });
246
+ }
247
+ if (managementRoutesEnabled) {
246
248
  app.get("/api/issues/:issueKey", async (request, reply) => {
247
249
  const issueKey = request.params.issueKey;
248
250
  const result = await service.getIssueOverview(issueKey);
@@ -296,8 +298,6 @@ export async function buildHttpServer(config, service, logger) {
296
298
  }
297
299
  return reply.send({ ok: true, ...link });
298
300
  });
299
- }
300
- if (managementRoutesEnabled) {
301
301
  app.post("/api/issues/:issueKey/retry", async (request, reply) => {
302
302
  const issueKey = request.params.issueKey;
303
303
  const result = service.retryIssue(issueKey);
@@ -546,6 +546,16 @@ export class RunOrchestrator {
546
546
  // Reactive loops (CI repair, review fix) will handle follow-up if needed.
547
547
  if (latestTurn?.status === "interrupted") {
548
548
  this.logger.warn({ issueKey: issue.issueKey, runType: run.runType, threadId: run.threadId }, "Run has interrupted turn — marking as failed");
549
+ // Interrupted runs are not real failures — undo the budget increment.
550
+ if (run.runType === "ci_repair" && issue.ciRepairAttempts > 0) {
551
+ this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, ciRepairAttempts: issue.ciRepairAttempts - 1 });
552
+ }
553
+ else if (run.runType === "queue_repair" && issue.queueRepairAttempts > 0) {
554
+ this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, queueRepairAttempts: issue.queueRepairAttempts - 1 });
555
+ }
556
+ else if (run.runType === "review_fix" && issue.reviewFixAttempts > 0) {
557
+ this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, reviewFixAttempts: issue.reviewFixAttempts - 1 });
558
+ }
549
559
  this.failRunAndClear(run, "Codex turn was interrupted");
550
560
  const failedIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? issue;
551
561
  void this.emitLinearActivity(failedIssue, buildRunFailureActivity(run.runType, "The Codex turn was interrupted."));
package/dist/service.js CHANGED
@@ -278,12 +278,27 @@ export class PatchRelayService {
278
278
  return undefined;
279
279
  if (issue.activeRunId)
280
280
  return { error: "Issue already has an active run" };
281
- const runType = "implementation";
281
+ // Infer run type from current state instead of always resetting to implementation
282
+ let runType = "implementation";
283
+ let factoryState = "delegated";
284
+ if (issue.prNumber && issue.prCheckStatus === "failed") {
285
+ runType = "ci_repair";
286
+ factoryState = "repairing_ci";
287
+ }
288
+ else if (issue.prNumber && issue.prReviewState === "changes_requested") {
289
+ runType = "review_fix";
290
+ factoryState = "changes_requested";
291
+ }
292
+ else if (issue.prNumber) {
293
+ // PR exists but no specific failure — re-run implementation
294
+ runType = "implementation";
295
+ factoryState = "implementing";
296
+ }
282
297
  this.db.upsertIssue({
283
298
  projectId: issue.projectId,
284
299
  linearIssueId: issue.linearIssueId,
285
300
  pendingRunType: runType,
286
- factoryState: "delegated",
301
+ factoryState: factoryState,
287
302
  });
288
303
  this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
289
304
  return { issueKey, runType };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.20.4",
3
+ "version": "0.20.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {