merge-steward 0.17.1 → 0.19.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/db/schema.js +1 -0
- package/dist/db/sqlite-store.js +3 -1
- package/dist/github/pr-client.js +2 -1
- package/dist/service-queue.d.ts +1 -0
- package/dist/service-queue.js +2 -1
- package/dist/types.d.ts +3 -0
- package/dist/watch/App.js +48 -7
- package/dist/watch/HelpBar.js +2 -2
- package/dist/watch/OverviewView.d.ts +2 -1
- package/dist/watch/OverviewView.js +26 -11
- package/dist/watch/ProjectDetailView.d.ts +15 -2
- package/dist/watch/ProjectDetailView.js +75 -42
- package/dist/watch/dashboard-model.d.ts +1 -0
- package/dist/watch/dashboard-model.js +3 -0
- package/package.json +1 -1
package/dist/db/schema.js
CHANGED
|
@@ -75,6 +75,7 @@ export function ensureSchema(connection) {
|
|
|
75
75
|
ensureColumn(connection, "queue_entries", "post_merge_summary", "TEXT");
|
|
76
76
|
ensureColumn(connection, "queue_entries", "post_merge_checked_at", "TEXT");
|
|
77
77
|
ensureColumn(connection, "queue_entries", "wait_detail", "TEXT");
|
|
78
|
+
ensureColumn(connection, "queue_entries", "pr_title", "TEXT");
|
|
78
79
|
connection.exec(`
|
|
79
80
|
UPDATE queue_entries
|
|
80
81
|
SET post_merge_status = 'pending'
|
package/dist/db/sqlite-store.js
CHANGED
|
@@ -29,6 +29,7 @@ function mapEntry(row) {
|
|
|
29
29
|
postMergeSha: row.post_merge_sha === null || row.post_merge_sha === undefined ? null : String(row.post_merge_sha),
|
|
30
30
|
postMergeSummary: row.post_merge_summary === null || row.post_merge_summary === undefined ? null : String(row.post_merge_summary),
|
|
31
31
|
postMergeCheckedAt: row.post_merge_checked_at === null || row.post_merge_checked_at === undefined ? null : String(row.post_merge_checked_at),
|
|
32
|
+
prTitle: row.pr_title === null || row.pr_title === undefined ? null : String(row.pr_title),
|
|
32
33
|
enqueuedAt: String(row.enqueued_at),
|
|
33
34
|
updatedAt: String(row.updated_at),
|
|
34
35
|
};
|
|
@@ -125,8 +126,9 @@ export class SqliteStore {
|
|
|
125
126
|
priority, generation, ci_run_id, ci_retries, retry_attempts,
|
|
126
127
|
max_retries, last_failed_base_sha, issue_key, wait_detail,
|
|
127
128
|
post_merge_status, post_merge_sha, post_merge_summary, post_merge_checked_at,
|
|
129
|
+
pr_title,
|
|
128
130
|
enqueued_at, updated_at)
|
|
129
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(entry.id, entry.repoId, entry.prNumber, entry.branch, entry.headSha, entry.baseSha, entry.status, entry.position, entry.priority, entry.generation, entry.ciRunId, entry.ciRetries, entry.retryAttempts, entry.maxRetries, entry.lastFailedBaseSha, entry.issueKey ?? null, entry.waitDetail ?? null, entry.postMergeStatus ?? null, entry.postMergeSha ?? null, entry.postMergeSummary ?? null, entry.postMergeCheckedAt ?? null, entry.enqueuedAt, entry.updatedAt);
|
|
131
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(entry.id, entry.repoId, entry.prNumber, entry.branch, entry.headSha, entry.baseSha, entry.status, entry.position, entry.priority, entry.generation, entry.ciRunId, entry.ciRetries, entry.retryAttempts, entry.maxRetries, entry.lastFailedBaseSha, entry.issueKey ?? null, entry.waitDetail ?? null, entry.postMergeStatus ?? null, entry.postMergeSha ?? null, entry.postMergeSummary ?? null, entry.postMergeCheckedAt ?? null, entry.prTitle ?? null, entry.enqueuedAt, entry.updatedAt);
|
|
130
132
|
this.writeEvent(entry.id, null, entry.status);
|
|
131
133
|
})();
|
|
132
134
|
}
|
package/dist/github/pr-client.js
CHANGED
|
@@ -24,13 +24,14 @@ export class GitHubPRClient {
|
|
|
24
24
|
const result = await exec("gh", [
|
|
25
25
|
"pr", "view", String(prNumber),
|
|
26
26
|
"--repo", this.repoFullName,
|
|
27
|
-
"--json", "number,headRefName,headRefOid,reviewDecision,state,mergeStateStatus",
|
|
27
|
+
"--json", "number,title,headRefName,headRefOid,reviewDecision,state,mergeStateStatus",
|
|
28
28
|
], { githubRepoFullName: this.repoFullName });
|
|
29
29
|
const data = JSON.parse(result.stdout);
|
|
30
30
|
return {
|
|
31
31
|
number: data.number,
|
|
32
32
|
branch: data.headRefName,
|
|
33
33
|
headSha: data.headRefOid,
|
|
34
|
+
...(data.title ? { title: data.title } : {}),
|
|
34
35
|
mergeable: data.state === "OPEN",
|
|
35
36
|
mergeStateStatus: data.mergeStateStatus,
|
|
36
37
|
reviewDecision: data.reviewDecision,
|
package/dist/service-queue.d.ts
CHANGED
package/dist/service-queue.js
CHANGED
|
@@ -63,6 +63,7 @@ export class MergeStewardQueueCommands {
|
|
|
63
63
|
postMergeSha: null,
|
|
64
64
|
postMergeSummary: null,
|
|
65
65
|
postMergeCheckedAt: null,
|
|
66
|
+
prTitle: params.prTitle ?? null,
|
|
66
67
|
enqueuedAt: new Date().toISOString(),
|
|
67
68
|
updatedAt: new Date().toISOString(),
|
|
68
69
|
};
|
|
@@ -161,7 +162,7 @@ export class MergeStewardQueueCommands {
|
|
|
161
162
|
return false;
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
|
-
this.enqueue({ prNumber, branch, headSha });
|
|
165
|
+
this.enqueue({ prNumber, branch, headSha, ...(status.title ? { prTitle: status.title } : {}) });
|
|
165
166
|
return true;
|
|
166
167
|
}
|
|
167
168
|
catch (error) {
|
package/dist/types.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export interface QueueEntry {
|
|
|
42
42
|
waitDetail?: string | null;
|
|
43
43
|
/** Post-merge verification status for the landed commit on main. */
|
|
44
44
|
postMergeStatus: PostMergeStatus | null;
|
|
45
|
+
/** PR title captured at admission. Falls back to branch name in the UI. */
|
|
46
|
+
prTitle?: string | null;
|
|
45
47
|
/** Commit SHA used when checking post-merge status. */
|
|
46
48
|
postMergeSha: string | null;
|
|
47
49
|
/** Optional one-line summary of post-merge checks. */
|
|
@@ -180,6 +182,7 @@ export interface PRStatus {
|
|
|
180
182
|
number: number;
|
|
181
183
|
branch: string;
|
|
182
184
|
headSha: string;
|
|
185
|
+
title?: string | undefined;
|
|
183
186
|
mergeable: boolean;
|
|
184
187
|
mergeStateStatus?: string | undefined;
|
|
185
188
|
reviewDecision?: string | undefined;
|
package/dist/watch/App.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { Box, Text, useApp, useInput } from "ink";
|
|
3
|
+
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
4
4
|
import { fetchGatewayHealth, fetchSnapshot, triggerReconcile } from "./api.js";
|
|
5
5
|
import { buildDashboard, matchRepoRef, repoSelector, stepRepo } from "./dashboard-model.js";
|
|
6
6
|
import { HelpBar } from "./HelpBar.js";
|
|
@@ -10,6 +10,7 @@ import { StatusBar } from "./StatusBar.js";
|
|
|
10
10
|
const REFRESH_INTERVAL_MS = 1_500;
|
|
11
11
|
export function App({ gatewayBaseUrl, repos, initialRepoRef, initialPrNumber }) {
|
|
12
12
|
const { exit } = useApp();
|
|
13
|
+
const { stdout } = useStdout();
|
|
13
14
|
const [repoStates, setRepoStates] = useState(() => repos.map((repo) => ({
|
|
14
15
|
...repo,
|
|
15
16
|
serviceState: "offline",
|
|
@@ -27,6 +28,7 @@ export function App({ gatewayBaseUrl, repos, initialRepoRef, initialPrNumber })
|
|
|
27
28
|
const [view, setView] = useState(initialRepoRef || initialPrNumber !== undefined ? "detail" : "list");
|
|
28
29
|
const [flashMessage, setFlashMessage] = useState(null);
|
|
29
30
|
const [pendingInitialPrNumber, setPendingInitialPrNumber] = useState(initialPrNumber ?? null);
|
|
31
|
+
const [detailScrollOffset, setDetailScrollOffset] = useState(0);
|
|
30
32
|
const model = useMemo(() => buildDashboard(repoStates), [repoStates]);
|
|
31
33
|
useEffect(() => {
|
|
32
34
|
setSelectedRepoId((current) => repoSelector(model.repos, current));
|
|
@@ -150,6 +152,9 @@ export function App({ gatewayBaseUrl, repos, initialRepoRef, initialPrNumber })
|
|
|
150
152
|
setFlashMessage(error instanceof Error ? error.message : String(error));
|
|
151
153
|
}
|
|
152
154
|
}
|
|
155
|
+
const rows = Math.max(8, stdout?.rows ?? 24);
|
|
156
|
+
const chromeRows = 1 /* status */ + 1 /* body marginTop */ + (flashMessage ? 2 : 0) + 2 /* help bar + its marginTop */;
|
|
157
|
+
const bodyRows = Math.max(2, rows - chromeRows);
|
|
153
158
|
useInput((input, key) => {
|
|
154
159
|
if (input === "q") {
|
|
155
160
|
exit();
|
|
@@ -159,22 +164,58 @@ export function App({ gatewayBaseUrl, repos, initialRepoRef, initialPrNumber })
|
|
|
159
164
|
void runReconcile(selectedRepoId);
|
|
160
165
|
return;
|
|
161
166
|
}
|
|
162
|
-
if (
|
|
167
|
+
if (view === "list") {
|
|
168
|
+
if (input === "j" || key.downArrow || input === "]") {
|
|
169
|
+
setSelectedRepoId((current) => stepRepo(model.repos, current, 1));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (input === "k" || key.upArrow || input === "[") {
|
|
173
|
+
setSelectedRepoId((current) => stepRepo(model.repos, current, -1));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (key.return)
|
|
177
|
+
setView("detail");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// detail view
|
|
181
|
+
if (input === "]") {
|
|
163
182
|
setSelectedRepoId((current) => stepRepo(model.repos, current, 1));
|
|
183
|
+
setDetailScrollOffset(0);
|
|
164
184
|
return;
|
|
165
185
|
}
|
|
166
|
-
if (input === "
|
|
186
|
+
if (input === "[") {
|
|
167
187
|
setSelectedRepoId((current) => stepRepo(model.repos, current, -1));
|
|
188
|
+
setDetailScrollOffset(0);
|
|
168
189
|
return;
|
|
169
190
|
}
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
191
|
+
if (input === "j" || key.downArrow) {
|
|
192
|
+
setDetailScrollOffset((offset) => offset + 1);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (input === "k" || key.upArrow) {
|
|
196
|
+
setDetailScrollOffset((offset) => Math.max(0, offset - 1));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (key.pageDown || input === " ") {
|
|
200
|
+
setDetailScrollOffset((offset) => offset + Math.max(1, bodyRows - 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (key.pageUp) {
|
|
204
|
+
setDetailScrollOffset((offset) => Math.max(0, offset - Math.max(1, bodyRows - 2)));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (input === "g") {
|
|
208
|
+
setDetailScrollOffset(0);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (input === "G") {
|
|
212
|
+
setDetailScrollOffset(Number.MAX_SAFE_INTEGER);
|
|
173
213
|
return;
|
|
174
214
|
}
|
|
175
215
|
if (key.escape || key.backspace || key.delete) {
|
|
176
216
|
setView("list");
|
|
217
|
+
setDetailScrollOffset(0);
|
|
177
218
|
}
|
|
178
219
|
});
|
|
179
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { connected: model.repos.some((repo) => !repo.offlineMessage), lastSnapshotReceivedAt: lastSnapshotReceivedAt, gatewayError: gatewayError }), view === "detail" ? (_jsx(ProjectDetailView, { model: model, selectedRepoId: selectedRepoId })) : (_jsx(OverviewView, { model: model, selectedRepoId: selectedRepoId, showCursor: true })), flashMessage ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: flashMessage }) })) : null, _jsx(HelpBar, { view: view })] }));
|
|
220
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { connected: model.repos.some((repo) => !repo.offlineMessage), lastSnapshotReceivedAt: lastSnapshotReceivedAt, gatewayError: gatewayError }), view === "detail" ? (_jsx(ProjectDetailView, { model: model, selectedRepoId: selectedRepoId, bodyRows: bodyRows, scrollOffset: detailScrollOffset })) : (_jsx(OverviewView, { model: model, selectedRepoId: selectedRepoId, showCursor: true, bodyRows: bodyRows })), flashMessage ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: flashMessage }) })) : null, _jsx(HelpBar, { view: view })] }));
|
|
180
221
|
}
|
package/dist/watch/HelpBar.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
export function HelpBar({ view }) {
|
|
4
4
|
const text = view === "list"
|
|
5
|
-
? "↑↓
|
|
6
|
-
: "
|
|
5
|
+
? "↑↓ repo enter detail r reconcile q quit"
|
|
6
|
+
: "[ ] repo ↑↓ scroll g/G top/end esc back r reconcile q quit";
|
|
7
7
|
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
8
8
|
}
|
|
@@ -3,6 +3,7 @@ interface ListViewProps {
|
|
|
3
3
|
model: DashboardModel;
|
|
4
4
|
selectedRepoId: string | null;
|
|
5
5
|
showCursor: boolean;
|
|
6
|
+
bodyRows: number;
|
|
6
7
|
}
|
|
7
8
|
export declare function RepoRow({ repo, selected, showCursor, width, }: {
|
|
8
9
|
repo: DashboardRepo;
|
|
@@ -14,5 +15,5 @@ export declare function pickVisibleWindow(total: number, selectedIndex: number,
|
|
|
14
15
|
start: number;
|
|
15
16
|
end: number;
|
|
16
17
|
};
|
|
17
|
-
export declare function OverviewView({ model, selectedRepoId, showCursor }: ListViewProps): React.JSX.Element;
|
|
18
|
+
export declare function OverviewView({ model, selectedRepoId, showCursor, bodyRows }: ListViewProps): React.JSX.Element;
|
|
18
19
|
export {};
|
|
@@ -45,21 +45,36 @@ export function pickVisibleWindow(total, selectedIndex, availableRows) {
|
|
|
45
45
|
}
|
|
46
46
|
return { start, end };
|
|
47
47
|
}
|
|
48
|
-
export function OverviewView({ model, selectedRepoId, showCursor }) {
|
|
48
|
+
export function OverviewView({ model, selectedRepoId, showCursor, bodyRows }) {
|
|
49
49
|
const { stdout } = useStdout();
|
|
50
|
-
const rows = Math.max(3, stdout?.rows ?? 24);
|
|
51
50
|
const width = Math.max(40, stdout?.columns ?? 80);
|
|
52
|
-
const
|
|
51
|
+
const total = model.repos.length;
|
|
53
52
|
const selectedIndex = Math.max(0, model.repos.findIndex((repo) => repo.repoId === selectedRepoId));
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
let { start, end } = pickVisibleWindow(total, selectedIndex, Math.max(1, bodyRows));
|
|
54
|
+
let needTop = start > 0;
|
|
55
|
+
let needBottom = end < total;
|
|
56
|
+
if (needTop || needBottom) {
|
|
57
|
+
const reserve = (needTop ? 1 : 0) + (needBottom ? 1 : 0);
|
|
58
|
+
const windowRows = Math.max(1, bodyRows - reserve);
|
|
59
|
+
({ start, end } = pickVisibleWindow(total, selectedIndex, windowRows));
|
|
60
|
+
needTop = start > 0;
|
|
61
|
+
needBottom = end < total;
|
|
62
|
+
}
|
|
57
63
|
const visible = model.repos.slice(start, end);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (visible.length === 0) {
|
|
64
|
+
const above = start;
|
|
65
|
+
const below = total - end;
|
|
66
|
+
if (total === 0 || bodyRows <= 0) {
|
|
62
67
|
return _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: " " }) });
|
|
63
68
|
}
|
|
64
|
-
|
|
69
|
+
const children = [];
|
|
70
|
+
if (needTop) {
|
|
71
|
+
children.push(_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: `\u2191${above} more above` }) }, "above"));
|
|
72
|
+
}
|
|
73
|
+
for (const repo of visible) {
|
|
74
|
+
children.push(_jsx(RepoRow, { repo: repo, selected: repo.repoId === selectedRepoId, showCursor: showCursor, width: width - 2 }, repo.repoId));
|
|
75
|
+
}
|
|
76
|
+
if (needBottom) {
|
|
77
|
+
children.push(_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: `\u2193${below} more below` }) }, "below"));
|
|
78
|
+
}
|
|
79
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: children.slice(0, Math.max(1, bodyRows)) }));
|
|
65
80
|
}
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import { type DashboardModel } from "./dashboard-model.ts";
|
|
1
|
+
import { type DashboardModel, type DashboardPrEntry, type DashboardRepo } from "./dashboard-model.ts";
|
|
2
2
|
interface ProjectDetailViewProps {
|
|
3
3
|
model: DashboardModel;
|
|
4
4
|
selectedRepoId: string | null;
|
|
5
|
+
bodyRows: number;
|
|
6
|
+
scrollOffset: number;
|
|
5
7
|
}
|
|
6
|
-
|
|
8
|
+
type ContentLine = {
|
|
9
|
+
kind: "blank";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "entry-header";
|
|
12
|
+
entry: DashboardPrEntry;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "summary-line";
|
|
15
|
+
text: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function buildContentLines(repo: DashboardRepo, width: number): ContentLine[];
|
|
18
|
+
export declare function clampScrollOffset(requested: number, totalLines: number, scrollArea: number): number;
|
|
19
|
+
export declare function ProjectDetailView({ model, selectedRepoId, bodyRows, scrollOffset }: ProjectDetailViewProps): React.JSX.Element;
|
|
7
20
|
export {};
|
|
@@ -3,17 +3,60 @@ import { Box, Text, useStdout } from "ink";
|
|
|
3
3
|
import { RepoRow } from "./OverviewView.js";
|
|
4
4
|
import { clipSummary } from "./dashboard-model.js";
|
|
5
5
|
const PR_ID_WIDTH = 7;
|
|
6
|
+
const PR_PHRASE_WIDTH = 20;
|
|
6
7
|
const SUMMARY_INDENT = 9;
|
|
7
|
-
function
|
|
8
|
+
function truncate(value, maxWidth) {
|
|
9
|
+
if (value.length <= maxWidth)
|
|
10
|
+
return value;
|
|
11
|
+
if (maxWidth <= 1)
|
|
12
|
+
return value.slice(0, maxWidth);
|
|
13
|
+
return `${value.slice(0, Math.max(0, maxWidth - 1))}\u2026`;
|
|
14
|
+
}
|
|
15
|
+
export function buildContentLines(repo, width) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
repo.entries.forEach((entry, index) => {
|
|
18
|
+
if (index > 0)
|
|
19
|
+
lines.push({ kind: "blank" });
|
|
20
|
+
lines.push({ kind: "entry-header", entry });
|
|
21
|
+
if (entry.summary) {
|
|
22
|
+
const summaryText = clipSummary(entry.summary, {
|
|
23
|
+
maxLines: 3,
|
|
24
|
+
width: Math.max(20, width - SUMMARY_INDENT),
|
|
25
|
+
});
|
|
26
|
+
if (summaryText) {
|
|
27
|
+
summaryText.split("\n").forEach((line) => lines.push({ kind: "summary-line", text: line }));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return lines;
|
|
32
|
+
}
|
|
33
|
+
function EntryHeaderRow({ entry, width }) {
|
|
8
34
|
const idText = `#${entry.prNumber}`.padEnd(PR_ID_WIDTH, " ");
|
|
9
|
-
const
|
|
10
|
-
|
|
35
|
+
const paddedPhrase = entry.phrase.padEnd(PR_PHRASE_WIDTH, " ");
|
|
36
|
+
const titleSpace = Math.max(0, width - (PR_ID_WIDTH + 3 + PR_PHRASE_WIDTH + 1));
|
|
37
|
+
const title = entry.title && entry.title !== entry.phrase && titleSpace >= 8
|
|
38
|
+
? truncate(entry.title, titleSpace)
|
|
11
39
|
: "";
|
|
12
|
-
return (_jsxs(Box, {
|
|
40
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: entry.color, children: idText }), _jsx(Text, { color: entry.color, children: entry.glyph }), _jsx(Text, { children: ` ${paddedPhrase}` }), title ? _jsx(Text, { dimColor: true, children: ` ${title}` }) : null] }));
|
|
41
|
+
}
|
|
42
|
+
function renderLine(line, key, width) {
|
|
43
|
+
if (line.kind === "blank")
|
|
44
|
+
return _jsx(Box, { children: _jsx(Text, { children: " " }) }, key);
|
|
45
|
+
if (line.kind === "summary-line") {
|
|
46
|
+
return (_jsxs(Box, { children: [_jsx(Text, { children: " ".repeat(SUMMARY_INDENT) }), _jsx(Text, { dimColor: true, children: line.text })] }, key));
|
|
47
|
+
}
|
|
48
|
+
return _jsx(EntryHeaderRow, { entry: line.entry, width: width }, key);
|
|
13
49
|
}
|
|
14
|
-
export function
|
|
50
|
+
export function clampScrollOffset(requested, totalLines, scrollArea) {
|
|
51
|
+
if (scrollArea <= 0 || totalLines <= scrollArea)
|
|
52
|
+
return 0;
|
|
53
|
+
// When scrolled at all, a one-row top indicator eats into the viewport, so the
|
|
54
|
+
// largest useful offset lets (scrollArea - 1) rows of content fill the remainder.
|
|
55
|
+
const maxOffset = Math.max(0, totalLines - Math.max(1, scrollArea - 1));
|
|
56
|
+
return Math.max(0, Math.min(requested, maxOffset));
|
|
57
|
+
}
|
|
58
|
+
export function ProjectDetailView({ model, selectedRepoId, bodyRows, scrollOffset }) {
|
|
15
59
|
const { stdout } = useStdout();
|
|
16
|
-
const rows = Math.max(3, stdout?.rows ?? 24);
|
|
17
60
|
const width = Math.max(40, stdout?.columns ?? 80);
|
|
18
61
|
const repo = selectedRepoId
|
|
19
62
|
? model.repos.find((candidate) => candidate.repoId === selectedRepoId) ?? model.repos[0] ?? null
|
|
@@ -21,42 +64,32 @@ export function ProjectDetailView({ model, selectedRepoId }) {
|
|
|
21
64
|
if (!repo) {
|
|
22
65
|
return _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: " " }) });
|
|
23
66
|
}
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
67
|
+
const repoHeaderRows = 1;
|
|
68
|
+
const separatorRows = bodyRows >= 3 ? 1 : 0;
|
|
69
|
+
const scrollArea = Math.max(0, bodyRows - repoHeaderRows - separatorRows);
|
|
70
|
+
const contentWidth = Math.max(20, width - 2);
|
|
71
|
+
const contentLines = buildContentLines(repo, contentWidth);
|
|
72
|
+
const offset = clampScrollOffset(scrollOffset, contentLines.length, scrollArea);
|
|
73
|
+
const children = [];
|
|
74
|
+
const couldHaveAbove = offset > 0;
|
|
75
|
+
const reserveTop = couldHaveAbove && scrollArea > 0 ? 1 : 0;
|
|
76
|
+
let remaining = scrollArea - reserveTop;
|
|
77
|
+
let tentativeEnd = Math.min(contentLines.length, offset + remaining);
|
|
78
|
+
const belowCount = contentLines.length - tentativeEnd;
|
|
79
|
+
const reserveBottom = belowCount > 0 && remaining > 0 ? 1 : 0;
|
|
80
|
+
if (reserveBottom) {
|
|
81
|
+
remaining = Math.max(0, remaining - 1);
|
|
82
|
+
tentativeEnd = Math.min(contentLines.length, offset + remaining);
|
|
37
83
|
}
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (!summaryText)
|
|
48
|
-
continue;
|
|
49
|
-
const summaryLines = summaryText.split("\n").length;
|
|
50
|
-
const isLast = i === planned.length - 1;
|
|
51
|
-
const extra = summaryLines + (isLast ? 0 : 1);
|
|
52
|
-
if (used + extra > afterRepoRow)
|
|
53
|
-
break;
|
|
54
|
-
item.includeSummary = true;
|
|
55
|
-
used += extra;
|
|
56
|
-
}
|
|
84
|
+
if (reserveTop) {
|
|
85
|
+
children.push(_jsx(Box, { children: _jsx(Text, { dimColor: true, children: ` \u2191${offset} more above` }) }, "above"));
|
|
86
|
+
}
|
|
87
|
+
for (let i = offset; i < tentativeEnd; i += 1) {
|
|
88
|
+
children.push(renderLine(contentLines[i], i, contentWidth));
|
|
89
|
+
}
|
|
90
|
+
if (reserveBottom) {
|
|
91
|
+
const hidden = contentLines.length - tentativeEnd;
|
|
92
|
+
children.push(_jsx(Box, { children: _jsx(Text, { dimColor: true, children: ` \u2193${hidden} more below` }) }, "below"));
|
|
57
93
|
}
|
|
58
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(RepoRow, { repo: repo, selected: false, showCursor: false, width: width - 2 }),
|
|
59
|
-
const isLast = index === planned.length - 1;
|
|
60
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PrEntryRow, { entry: entry, width: width, includeSummary: includeSummary }), includeSummary && !isLast ? _jsx(Box, { children: _jsx(Text, { children: " " }) }) : null] }, entry.prNumber));
|
|
61
|
-
})] }));
|
|
94
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(RepoRow, { repo: repo, selected: false, showCursor: false, width: width - 2 }), separatorRows > 0 ? _jsx(Box, { children: _jsx(Text, { children: " " }) }) : null, children] }));
|
|
62
95
|
}
|
|
@@ -151,6 +151,9 @@ function repoEntriesFromSnapshot(snapshot, cutoff) {
|
|
|
151
151
|
const summary = entrySummary(entry);
|
|
152
152
|
if (summary)
|
|
153
153
|
item.summary = summary;
|
|
154
|
+
const title = entry.prTitle?.trim() || entry.branch;
|
|
155
|
+
if (title)
|
|
156
|
+
item.title = title;
|
|
154
157
|
byPr.set(entry.prNumber, item);
|
|
155
158
|
}
|
|
156
159
|
return [...byPr.values()].sort((left, right) => {
|