patchrelay 0.32.1 → 0.32.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.32.1",
4
- "commit": "c48953273955",
5
- "builtAt": "2026-04-01T21:29:18.620Z"
3
+ "version": "0.32.2",
4
+ "commit": "a1095c2643be",
5
+ "builtAt": "2026-04-01T21:58:19.644Z"
6
6
  }
@@ -4,15 +4,15 @@ import { Box, Text, useStdout } from "ink";
4
4
  import { IssueRow } from "./IssueRow.js";
5
5
  import { StatusBar } from "./StatusBar.js";
6
6
  import { HelpBar } from "./HelpBar.js";
7
- // selector(2) + key(10) + status(13) + pr(7) + ago(4) + gaps = ~36
8
- const FIXED_COLS = 40;
7
+ const FIXED_COLS = 8;
9
8
  const CHROME_ROWS = 4;
9
+ const ISSUE_ROW_HEIGHT = 4;
10
10
  export function IssueListView({ issues, allIssues, selectedIndex, connected, lastServerMessageAt, filter, totalCount, frozen, }) {
11
11
  const { stdout } = useStdout();
12
12
  const cols = stdout?.columns ?? 80;
13
13
  const rows = stdout?.rows ?? 24;
14
14
  const titleWidth = Math.max(0, cols - FIXED_COLS);
15
- const maxVisible = Math.max(1, rows - CHROME_ROWS);
15
+ const maxVisible = Math.max(1, Math.floor((rows - CHROME_ROWS) / ISSUE_ROW_HEIGHT));
16
16
  // Periodic refresh for elapsed times
17
17
  const [, tick] = useReducer((c) => c + 1, 0);
18
18
  useEffect(() => {
@@ -1,6 +1,7 @@
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
  import { summarizeIssueStatusNote } from "./issue-status-note.js";
4
+ import { progressBar, relativeTime, truncate } from "./format-utils.js";
4
5
  const STATE_COLORS = {
5
6
  blocked: "yellow",
6
7
  ready: "blueBright",
@@ -20,16 +21,16 @@ const STATE_SHORT = {
20
21
  blocked: "blocked",
21
22
  ready: "ready",
22
23
  delegated: "delegated",
23
- implementing: "impl",
24
+ implementing: "implementing",
24
25
  pr_open: "pr open",
25
- changes_requested: "review fix",
26
- repairing_ci: "ci fix",
27
- awaiting_queue: "await queue",
28
- repairing_queue: "merge fix",
26
+ changes_requested: "review changes",
27
+ repairing_ci: "repairing checks",
28
+ awaiting_queue: "queued for merge",
29
+ repairing_queue: "repairing merge queue",
29
30
  done: "done",
30
31
  failed: "failed",
31
32
  escalated: "escalated",
32
- awaiting_input: "await input",
33
+ awaiting_input: "awaiting input",
33
34
  };
34
35
  const STATUS_SHORT = {
35
36
  running: "\u25b8",
@@ -40,115 +41,205 @@ const STATUS_SHORT = {
40
41
  function stateColor(state) {
41
42
  return STATE_COLORS[state] ?? "white";
42
43
  }
43
- function formatPr(issue) {
44
- if (!issue.prNumber)
45
- return "";
46
- const parts = [`#${issue.prNumber}`];
47
- const review = formatReviewState(issue.prReviewState);
48
- const checks = formatCheckState(issue.prCheckStatus);
49
- const merge = formatMergeState(issue);
50
- if (review)
51
- parts.push(review);
52
- if (checks)
53
- parts.push(checks);
54
- if (merge)
55
- parts.push(merge);
56
- return parts.join("");
44
+ const TERMINAL_STATES = new Set(["done", "failed", "escalated", "awaiting_input"]);
45
+ function formatStatus(issue) {
46
+ const effectiveState = issue.blockedByCount > 0 && !issue.activeRunType
47
+ ? "blocked"
48
+ : issue.readyForExecution && !issue.activeRunType
49
+ ? "ready"
50
+ : issue.factoryState;
51
+ const state = STATE_SHORT[effectiveState] ?? effectiveState;
52
+ // Terminal states: just the label, no run symbol
53
+ if (TERMINAL_STATES.has(issue.factoryState))
54
+ return state;
55
+ // Active/in-progress: show run status symbol
56
+ const status = issue.activeRunType ? "running" : issue.latestRunStatus;
57
+ const statusSym = status ? (STATUS_SHORT[status] ?? "") : "";
58
+ if (statusSym)
59
+ return `${state} ${statusSym}`;
60
+ return state;
57
61
  }
58
- function formatReviewState(reviewState) {
62
+ function buildStatusChips(issue) {
63
+ const effectiveState = issue.blockedByCount > 0 && !issue.activeRunType
64
+ ? "blocked"
65
+ : issue.readyForExecution && !issue.activeRunType
66
+ ? "ready"
67
+ : issue.factoryState;
68
+ const chips = [{
69
+ text: `${stateIcon(effectiveState)} ${STATE_SHORT[effectiveState] ?? effectiveState}`,
70
+ color: stateColor(effectiveState),
71
+ }];
72
+ if (issue.prNumber !== undefined) {
73
+ chips.push({ text: `PR #${issue.prNumber}`, color: "cyan" });
74
+ }
75
+ const reviewChip = buildReviewChip(issue.prReviewState);
76
+ if (reviewChip)
77
+ chips.push(reviewChip);
78
+ const checkChip = buildCheckChip(issue.prCheckStatus);
79
+ if (checkChip)
80
+ chips.push(checkChip);
81
+ const checksProgressChip = buildChecksProgressChip(issue);
82
+ if (checksProgressChip)
83
+ chips.push(checksProgressChip);
84
+ const mergeChip = buildMergeChip(issue);
85
+ if (mergeChip)
86
+ chips.push(mergeChip);
87
+ if (issue.blockedByCount > 0) {
88
+ chips.push({
89
+ text: `blocked by ${issue.blockedByKeys.join(", ")}`,
90
+ color: "yellow",
91
+ });
92
+ }
93
+ return chips;
94
+ }
95
+ function stateIcon(state) {
96
+ switch (state) {
97
+ case "implementing":
98
+ case "repairing_ci":
99
+ case "repairing_queue":
100
+ return "\u25b8";
101
+ case "awaiting_queue":
102
+ return "\u25a4";
103
+ case "done":
104
+ return "\u2713";
105
+ case "failed":
106
+ case "escalated":
107
+ return "\u2717";
108
+ case "blocked":
109
+ return "!";
110
+ case "ready":
111
+ return "+";
112
+ default:
113
+ return "\u2022";
114
+ }
115
+ }
116
+ function buildReviewChip(reviewState) {
59
117
  switch (reviewState) {
60
118
  case "approved":
61
- return "rev:+";
119
+ return { text: "\u2713 review approved", color: "green" };
62
120
  case "changes_requested":
63
- return "rev:x";
121
+ return { text: "\u2717 changes requested", color: "yellow" };
64
122
  case "commented":
65
- return "rev:c";
123
+ return { text: "\u2022 review commented", color: "yellow" };
66
124
  case "dismissed":
67
- return "rev:-";
125
+ return { text: "\u2013 review dismissed", color: "yellow" };
68
126
  default:
69
127
  return null;
70
128
  }
71
129
  }
72
- function formatCheckState(checkState) {
130
+ function buildCheckChip(checkState) {
73
131
  switch (checkState) {
74
132
  case "passed":
75
133
  case "success":
76
- return "ci:+";
134
+ return { text: "\u2713 checks passed", color: "green" };
77
135
  case "failed":
78
136
  case "failure":
79
- return "ci:x";
137
+ return { text: "\u2717 checks failed", color: "red" };
80
138
  case "pending":
81
139
  case "in_progress":
82
140
  case "queued":
83
- return "ci:…";
141
+ return { text: "\u25cf checks running", color: "yellow" };
84
142
  default:
85
143
  return null;
86
144
  }
87
145
  }
88
- function formatMergeState(issue) {
89
- if (!issue.prNumber)
146
+ function buildChecksProgressChip(issue) {
147
+ const summary = issue.prChecksSummary;
148
+ if (!summary || summary.total <= 0)
149
+ return null;
150
+ const text = summary.failed > 0
151
+ ? `checks ${summary.completed}/${summary.total} failed`
152
+ : summary.pending > 0
153
+ ? `checks ${summary.completed}/${summary.total} running`
154
+ : `checks ${summary.completed}/${summary.total} passed`;
155
+ const color = summary.failed > 0 ? "red" : summary.pending > 0 ? "yellow" : "green";
156
+ return { text, color };
157
+ }
158
+ function buildMergeChip(issue) {
159
+ if (issue.prNumber === undefined)
90
160
  return null;
91
161
  switch (issue.factoryState) {
92
162
  case "awaiting_queue":
93
- return "queue";
163
+ return { text: "\u25a4 queued for merge", color: "cyan" };
94
164
  case "repairing_queue":
95
- return "mq-fix";
165
+ return { text: "! merge queue repair", color: "yellow" };
96
166
  case "done":
97
- return "merged";
167
+ return { text: "\u2713 merged", color: "green" };
98
168
  case "pr_open":
99
- if (issue.prReviewState === "approved" && issue.prCheckStatus === "passed")
100
- return "ready";
101
- return "open";
169
+ if (issue.prReviewState === "approved" && issue.prCheckStatus === "passed") {
170
+ return { text: "\u2713 merge ready", color: "green" };
171
+ }
172
+ return { text: "\u2022 PR open", color: "cyan" };
102
173
  default:
103
174
  return null;
104
175
  }
105
176
  }
106
- function relativeTime(iso) {
107
- const ms = Date.now() - new Date(iso).getTime();
108
- if (ms < 0)
109
- return "now";
110
- const seconds = Math.floor(ms / 1000);
111
- if (seconds < 60)
112
- return `${seconds}s`;
113
- const minutes = Math.floor(seconds / 60);
114
- if (minutes < 60)
115
- return `${minutes}m`;
116
- const hours = Math.floor(minutes / 60);
117
- if (hours < 24)
118
- return `${hours}h`;
119
- const days = Math.floor(hours / 24);
120
- return `${days}d`;
121
- }
122
- function truncate(text, max) {
123
- if (max <= 0)
124
- return "";
125
- return text.length > max ? text.slice(0, max) : text;
177
+ function buildPrimaryBlocker(issue) {
178
+ if (issue.blockedByCount > 0) {
179
+ return {
180
+ text: `Waiting on ${issue.blockedByKeys.join(", ")}`,
181
+ color: "yellow",
182
+ };
183
+ }
184
+ if (issue.prCheckStatus === "failed") {
185
+ const failedCheck = issue.latestFailureCheckName ?? "PR checks";
186
+ return {
187
+ text: `${failedCheck} failed`,
188
+ color: "red",
189
+ };
190
+ }
191
+ if (issue.prReviewState === "changes_requested") {
192
+ return {
193
+ text: "Review changes requested",
194
+ color: "yellow",
195
+ };
196
+ }
197
+ if (issue.prNumber !== undefined && !issue.prReviewState && issue.factoryState !== "done") {
198
+ return {
199
+ text: "Waiting for review approval",
200
+ color: "yellow",
201
+ };
202
+ }
203
+ if (issue.factoryState === "awaiting_queue") {
204
+ return {
205
+ text: "Waiting for merge queue turn",
206
+ color: "yellow",
207
+ };
208
+ }
209
+ return null;
126
210
  }
127
- const TERMINAL_STATES = new Set(["done", "failed", "escalated", "awaiting_input"]);
128
- function formatStatus(issue) {
129
- const effectiveState = issue.blockedByCount > 0 && !issue.activeRunType
130
- ? "blocked"
131
- : issue.readyForExecution && !issue.activeRunType
132
- ? "ready"
133
- : issue.factoryState;
134
- const state = STATE_SHORT[effectiveState] ?? effectiveState;
135
- // Terminal states: just the label, no run symbol
136
- if (TERMINAL_STATES.has(issue.factoryState))
137
- return state;
138
- // Active/in-progress: show run status symbol
139
- const status = issue.activeRunType ? "running" : issue.latestRunStatus;
140
- const statusSym = status ? (STATUS_SHORT[status] ?? "") : "";
141
- if (statusSym)
142
- return `${state} ${statusSym}`;
143
- return state;
211
+ function buildPipelineProgress(issue) {
212
+ switch (issue.factoryState) {
213
+ case "delegated":
214
+ return { current: 1, total: 4, label: "delegated" };
215
+ case "implementing":
216
+ return { current: 1, total: 4, label: "implementing" };
217
+ case "pr_open":
218
+ case "changes_requested":
219
+ case "repairing_ci":
220
+ return { current: 2, total: 4, label: "pr checks" };
221
+ case "awaiting_queue":
222
+ case "repairing_queue":
223
+ return { current: 3, total: 4, label: "merge queue" };
224
+ case "done":
225
+ return { current: 4, total: 4, label: "merged" };
226
+ case "failed":
227
+ case "escalated":
228
+ case "awaiting_input":
229
+ return { current: 4, total: 4, label: "stopped" };
230
+ default:
231
+ return { current: 1, total: 4, label: "queued" };
232
+ }
144
233
  }
145
234
  export function IssueRow({ issue, selected, titleWidth }) {
146
235
  const key = issue.issueKey ?? issue.projectId;
147
- const status = formatStatus(issue);
148
- const pr = formatPr(issue);
149
236
  const ago = relativeTime(issue.updatedAt);
150
- const tw = titleWidth ?? 30;
237
+ const tw = titleWidth ?? 40;
151
238
  const title = issue.title ? truncate(issue.title, tw) : "";
152
239
  const detail = selected ? summarizeIssueStatusNote(issue.statusNote) : undefined;
153
- return (_jsxs(Box, { flexDirection: "column", marginBottom: detail ? 1 : 0, children: [_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.blockedByCount > 0 && !issue.activeRunType ? "blocked" : issue.readyForExecution && !issue.activeRunType ? "ready" : issue.factoryState), children: ` ${status.padEnd(12)}` }), _jsx(Text, { dimColor: true, children: ` ${pr.padEnd(26)}` }), _jsx(Text, { dimColor: true, children: ` ${ago.padStart(3)}` }), title ? _jsx(Text, { dimColor: true, children: ` ${title}` }) : null] }), detail ? (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: detail }) })) : null] }));
240
+ const status = formatStatus(issue);
241
+ const chips = buildStatusChips(issue);
242
+ const blocker = buildPrimaryBlocker(issue);
243
+ const pipeline = buildPipelineProgress(issue);
244
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: detail ? 1 : 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "blueBright" : "white", bold: selected, children: selected ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: ` ${key}` }), _jsx(Text, { dimColor: true, children: ` ${ago}` }), _jsx(Text, { dimColor: true, children: ` ${status}` })] }), _jsx(Box, { paddingLeft: 2, flexWrap: "wrap", children: title ? _jsx(Text, { children: title }) : null }), _jsx(Box, { paddingLeft: 2, flexWrap: "wrap", children: chips.map((chip, index) => (_jsx(Box, { marginRight: 1, children: _jsxs(Text, { color: chip.color, children: ["[", chip.text, "]"] }) }, `${key}-chip-${index}`))) }), _jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: progressBar(pipeline.current, pipeline.total, 8) }), _jsx(Text, { dimColor: true, children: pipeline.label }), blocker ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "|" }), _jsx(Text, { color: blocker.color, children: blocker.text })] })) : null] }), detail ? (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: detail }) })) : null] }));
154
245
  }
package/dist/service.js CHANGED
@@ -35,6 +35,38 @@ function extractStatusNote(summaryJson, reportJson) {
35
35
  }
36
36
  return undefined;
37
37
  }
38
+ function parseCiSnapshotSummary(snapshotJson) {
39
+ if (!snapshotJson)
40
+ return undefined;
41
+ try {
42
+ const snapshot = JSON.parse(snapshotJson);
43
+ const checks = Array.isArray(snapshot.checks) ? snapshot.checks : [];
44
+ if (checks.length === 0)
45
+ return undefined;
46
+ let passed = 0;
47
+ let failed = 0;
48
+ let pending = 0;
49
+ for (const check of checks) {
50
+ if (check.status === "success")
51
+ passed++;
52
+ else if (check.status === "failure")
53
+ failed++;
54
+ else
55
+ pending++;
56
+ }
57
+ return {
58
+ total: checks.length,
59
+ completed: passed + failed,
60
+ passed,
61
+ failed,
62
+ pending,
63
+ overall: snapshot.gateCheckStatus,
64
+ };
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
38
70
  export class PatchRelayService {
39
71
  config;
40
72
  db;
@@ -221,6 +253,7 @@ export class PatchRelayService {
221
253
  i.current_linear_state, i.factory_state, i.updated_at,
222
254
  i.pending_run_type,
223
255
  i.pr_number, i.pr_review_state, i.pr_check_status,
256
+ i.last_github_ci_snapshot_json,
224
257
  i.last_github_failure_source,
225
258
  i.last_github_failure_head_sha,
226
259
  i.last_github_failure_check_name,
@@ -267,6 +300,7 @@ export class PatchRelayService {
267
300
  .all();
268
301
  return rows.map((row) => {
269
302
  const failureContext = parseGitHubFailureContext(typeof row.last_github_failure_context_json === "string" ? row.last_github_failure_context_json : undefined);
303
+ const prChecksSummary = parseCiSnapshotSummary(typeof row.last_github_ci_snapshot_json === "string" ? row.last_github_ci_snapshot_json : undefined);
270
304
  const statusNote = extractStatusNote(typeof row.latest_run_summary_json === "string" ? row.latest_run_summary_json : undefined, typeof row.latest_run_report_json === "string" ? row.latest_run_report_json : undefined);
271
305
  const blockedByKeys = parseStringArray(typeof row.blocked_by_keys_json === "string" ? row.blocked_by_keys_json : undefined);
272
306
  const blockedByCount = Number(row.blocked_by_count ?? 0);
@@ -299,6 +333,7 @@ export class PatchRelayService {
299
333
  ...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
300
334
  ...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
301
335
  ...(row.pr_check_status !== null ? { prCheckStatus: String(row.pr_check_status) } : {}),
336
+ ...(prChecksSummary ? { prChecksSummary } : {}),
302
337
  ...(row.last_github_failure_source !== null ? { latestFailureSource: String(row.last_github_failure_source) } : {}),
303
338
  ...(row.last_github_failure_head_sha !== null ? { latestFailureHeadSha: String(row.last_github_failure_head_sha) } : {}),
304
339
  ...(row.last_github_failure_check_name !== null ? { latestFailureCheckName: String(row.last_github_failure_check_name) } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.32.1",
3
+ "version": "0.32.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {