patchrelay 0.41.4 → 0.41.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.41.4",
4
- "commit": "f0bf37346e5e",
5
- "builtAt": "2026-04-13T10:48:22.890Z"
3
+ "version": "0.41.6",
4
+ "commit": "24441e13272f",
5
+ "builtAt": "2026-04-13T17:33:37.051Z"
6
6
  }
@@ -1,18 +1,45 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useReducer } from "react";
3
3
  import { Box, Text, useStdout } from "ink";
4
- import { IssueRow } from "./IssueRow.js";
4
+ import { IssueRow, estimateIssueRowHeight } from "./IssueRow.js";
5
5
  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
- const ISSUE_ROW_HEIGHT = 4;
9
+ export function computeVisibleWindow(issues, selectedIndex, maxRows, cols, titleWidth) {
10
+ if (issues.length === 0)
11
+ return { start: 0, end: 0 };
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));
14
+ let start = clampedSelected;
15
+ let end = clampedSelected + 1;
16
+ let usedRows = heights[clampedSelected] ?? 1;
17
+ while (true) {
18
+ const canAddAbove = start > 0 && usedRows + (heights[start - 1] ?? 1) <= maxRows;
19
+ const canAddBelow = end < issues.length && usedRows + (heights[end] ?? 1) <= maxRows;
20
+ if (!canAddAbove && !canAddBelow)
21
+ break;
22
+ const aboveDistance = clampedSelected - start;
23
+ const belowDistance = end - 1 - clampedSelected;
24
+ const preferAbove = canAddAbove && (!canAddBelow || aboveDistance <= belowDistance);
25
+ if (preferAbove) {
26
+ start -= 1;
27
+ usedRows += heights[start] ?? 1;
28
+ continue;
29
+ }
30
+ if (canAddBelow) {
31
+ usedRows += heights[end] ?? 1;
32
+ end += 1;
33
+ }
34
+ }
35
+ return { start, end };
36
+ }
10
37
  export function IssueListView({ issues, allIssues, selectedIndex, connected, lastServerMessageAt, filter, totalCount, frozen, }) {
11
38
  const { stdout } = useStdout();
12
39
  const cols = stdout?.columns ?? 80;
13
40
  const rows = stdout?.rows ?? 24;
14
41
  const titleWidth = Math.max(0, cols - FIXED_COLS);
15
- const maxVisible = Math.max(1, Math.floor((rows - CHROME_ROWS) / ISSUE_ROW_HEIGHT));
42
+ const maxVisibleRows = Math.max(1, rows - CHROME_ROWS);
16
43
  // Periodic refresh for elapsed times
17
44
  const [, tick] = useReducer((c) => c + 1, 0);
18
45
  useEffect(() => {
@@ -21,12 +48,9 @@ export function IssueListView({ issues, allIssues, selectedIndex, connected, las
21
48
  const id = setInterval(tick, 5000);
22
49
  return () => clearInterval(id);
23
50
  }, [frozen]);
24
- let startIndex = 0;
25
- if (issues.length > maxVisible) {
26
- startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), issues.length - maxVisible));
27
- }
28
- const visible = issues.slice(startIndex, startIndex + maxVisible);
51
+ const { start: startIndex, end: endIndex } = computeVisibleWindow(issues, selectedIndex, maxVisibleRows, cols, titleWidth);
52
+ const visible = issues.slice(startIndex, endIndex);
29
53
  const hiddenAbove = startIndex;
30
- const hiddenBelow = Math.max(0, issues.length - startIndex - maxVisible);
54
+ const hiddenBelow = Math.max(0, issues.length - endIndex);
31
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 && _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" }) })] }));
32
56
  }
@@ -3,6 +3,7 @@ import { Box, Text } from "ink";
3
3
  import { hasOpenPr } from "../../pr-state.js";
4
4
  import { summarizeIssueStatusNote } from "./issue-status-note.js";
5
5
  import { relativeTime, truncate } from "./format-utils.js";
6
+ import { measureRenderedTextRows } from "./layout-measure.js";
6
7
  import { hasDisplayPrBlocker, isApprovedReviewState, isAwaitingReviewState, isChangesRequestedReviewState, isRereviewNeeded, prChecksFact, } from "./pr-status.js";
7
8
  // ─── State display ──────────────────────────────────────────────
8
9
  const TERMINAL_STATES = new Set(["done", "failed", "escalated"]);
@@ -100,7 +101,7 @@ function buildFacts(issue, selected) {
100
101
  facts.push({ text: "awaiting review", color: "yellow" });
101
102
  }
102
103
  if (issue.factoryState === "awaiting_queue") {
103
- facts.push({ text: "merge queue", color: "cyan" });
104
+ facts.push({ text: "downstream ready", color: "cyan" });
104
105
  }
105
106
  // Check status — compact
106
107
  const checksFact = prChecksFact(issue);
@@ -167,3 +168,35 @@ export function IssueRow({ issue, selected, titleWidth }) {
167
168
  }
168
169
  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] }));
169
170
  }
171
+ export function estimateIssueRowHeight(issue, selected, cols, titleWidth) {
172
+ const width = Math.max(20, cols);
173
+ const key = issue.issueKey ?? issue.projectId;
174
+ const tw = titleWidth ?? 60;
175
+ const title = issue.title ? truncate(issue.title, tw) : "";
176
+ const detail = selected ? summarizeIssueStatusNote(issue.statusNote) : undefined;
177
+ const session = sessionDisplay(issue);
178
+ const facts = buildFacts(issue, selected);
179
+ const blocker = selected ? blockerText(issue) : null;
180
+ const isTerminal = TERMINAL_STATES.has(effectiveState(issue));
181
+ if (isTerminal && !selected) {
182
+ return 1;
183
+ }
184
+ const line1Parts = [
185
+ `${selected ? "\u25b8" : " "} ${key}`,
186
+ relativeTime(issue.updatedAt).padStart(4),
187
+ session.label,
188
+ ...facts.map((fact) => fact.text),
189
+ ];
190
+ let rows = measureRenderedTextRows(line1Parts.join(" · "), width);
191
+ if (title)
192
+ rows += measureRenderedTextRows(title, Math.max(8, width - 2));
193
+ if (blocker)
194
+ rows += measureRenderedTextRows(blocker, Math.max(8, width - 2));
195
+ if (detail)
196
+ rows += measureRenderedTextRows(detail, Math.max(8, width - 4));
197
+ if (selected && issue.factoryState && issue.sessionState)
198
+ rows += 1;
199
+ if (detail)
200
+ rows += 1;
201
+ return Math.max(1, rows);
202
+ }
@@ -401,7 +401,7 @@ function buildFactSegments(issue, issueContext) {
401
401
  && (isAwaitingReviewState(issue.prReviewState) || (!issue.prReviewState && issue.factoryState === "pr_open")))
402
402
  facts.push([{ text: "awaiting review", color: "yellow" }]);
403
403
  if (issue.factoryState === "awaiting_queue")
404
- facts.push([{ text: "merge queue", color: "cyan" }]);
404
+ facts.push([{ text: "downstream ready", color: "cyan" }]);
405
405
  if (issue.waitingReason && issue.sessionState === "waiting_input")
406
406
  facts.push([{ text: issue.waitingReason, color: "yellow" }]);
407
407
  const checks = prChecksFact({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.41.4",
3
+ "version": "0.41.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {