patchrelay 0.41.3 → 0.41.5

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.3",
4
- "commit": "091d6739b8e1",
5
- "builtAt": "2026-04-12T23:46:11.014Z"
3
+ "version": "0.41.5",
4
+ "commit": "8cba4e264b7e",
5
+ "builtAt": "2026-04-13T11:03:19.900Z"
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"]);
@@ -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
+ }
@@ -120,6 +120,7 @@ export class DesiredStageRecorder {
120
120
  triggerEvent: params.normalized.triggerEvent,
121
121
  delegated,
122
122
  });
123
+ const terminalRunRelease = effectiveRunRelease.release && terminal;
123
124
  const commitIssueUpdate = () => {
124
125
  const record = this.db.issues.upsertIssue({
125
126
  projectId: params.project.id,
@@ -148,6 +149,7 @@ export class DesiredStageRecorder {
148
149
  ...(clearPending ? { pendingRunType: null, pendingRunContextJson: null } : {}),
149
150
  ...(agentSessionId !== undefined ? { agentSessionId } : {}),
150
151
  ...(effectiveRunRelease.release ? { activeRunId: null } : {}),
152
+ ...(terminalRunRelease ? { factoryState: "done", pendingRunType: null, pendingRunContextJson: null } : {}),
151
153
  ...(blockerPausedImplementation ? { factoryState: "delegated" } : {}),
152
154
  ...(undelegation.factoryState ? { factoryState: undelegation.factoryState } : {}),
153
155
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.41.3",
3
+ "version": "0.41.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {