santree 0.5.5 → 0.6.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/commands/dashboard.js +228 -67
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/helpers/squirrel.d.ts +2 -0
- package/dist/commands/helpers/squirrel.js +12 -0
- package/dist/commands/worktree/commit.d.ts +9 -1
- package/dist/commands/worktree/commit.js +58 -14
- package/dist/lib/ai.d.ts +26 -0
- package/dist/lib/ai.js +53 -0
- package/dist/lib/claude-todos.d.ts +37 -0
- package/dist/lib/claude-todos.js +98 -0
- package/dist/lib/dashboard/DetailPanel.js +99 -9
- package/dist/lib/dashboard/IssueList.js +2 -0
- package/dist/lib/dashboard/MultilineTextArea.js +24 -11
- package/dist/lib/dashboard/Overlays.d.ts +5 -0
- package/dist/lib/dashboard/Overlays.js +76 -3
- package/dist/lib/dashboard/ReviewDetailPanel.d.ts +7 -0
- package/dist/lib/dashboard/ReviewDetailPanel.js +269 -77
- package/dist/lib/dashboard/ReviewList.js +12 -15
- package/dist/lib/dashboard/data.js +158 -7
- package/dist/lib/dashboard/types.d.ts +45 -10
- package/dist/lib/dashboard/types.js +40 -7
- package/dist/lib/diff-parse.d.ts +25 -0
- package/dist/lib/diff-parse.js +60 -0
- package/dist/lib/git.d.ts +22 -0
- package/dist/lib/git.js +41 -0
- package/dist/lib/github.d.ts +6 -0
- package/dist/lib/github.js +29 -0
- package/dist/lib/open-url.d.ts +10 -0
- package/dist/lib/open-url.js +20 -0
- package/dist/lib/squirrel-loader.d.ts +9 -0
- package/dist/lib/squirrel-loader.js +322 -0
- package/dist/lib/trackers/index.d.ts +13 -0
- package/dist/lib/trackers/index.js +19 -0
- package/package.json +1 -1
- package/prompts/fill-commit.njk +79 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
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 Spinner from "ink-spinner";
|
|
4
|
-
import TextInput from "ink-text-input";
|
|
5
4
|
import { MultilineTextArea } from "./MultilineTextArea.js";
|
|
6
5
|
export function CommitOverlay({ width, height, branch, ticketId, gitStatus, phase, message, error, dispatch, onSubmit, }) {
|
|
7
6
|
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { bold: true, color: "cyan", children: "Commit & Push" }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.min(width, 50)) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "branch: " }), _jsx(Text, { children: branch })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "ticket: " }), _jsx(Text, { children: ticketId })] }), _jsx(Text, { children: " " }), gitStatus ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Changes:" }), gitStatus
|
|
@@ -19,11 +18,85 @@ export function CommitOverlay({ width, height, branch, ticketId, gitStatus, phas
|
|
|
19
18
|
color = "yellow";
|
|
20
19
|
}
|
|
21
20
|
return (_jsxs(Text, { color: color, children: [" ", line] }, i));
|
|
22
|
-
}), gitStatus.split("\n").length > 8 && (_jsxs(Text, { dimColor: true, children: [" +", gitStatus.split("\n").length - 8, " more"] }))] })) : null, _jsx(Text, { children: " " }), phase === "confirm-stage" && (_jsxs(Text, { children: ["Stage all changes?", " ", _jsx(Text, { color: "cyan", bold: true, children: "y" }), "/", _jsx(Text, { color: "cyan", bold: true, children: "n" })] })), phase === "awaiting-message" && (_jsxs(
|
|
21
|
+
}), gitStatus.split("\n").length > 8 && (_jsxs(Text, { dimColor: true, children: [" +", gitStatus.split("\n").length - 8, " more"] }))] })) : null, _jsx(Text, { children: " " }), phase === "confirm-stage" && (_jsxs(Text, { children: ["Stage all changes?", " ", _jsx(Text, { color: "cyan", bold: true, children: "y" }), "/", _jsx(Text, { color: "cyan", bold: true, children: "n" })] })), phase === "choose-mode" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "How do you want to write the message?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "f" }), " ", "Fill \u2014 let Claude draft a short message"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "m" }), " ", "Manual \u2014 type it yourself"] })] })), phase === "filling" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Drafting commit message with Claude..."] })), phase === "awaiting-message" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Edit commit message" }), _jsx(Text, { children: " " }), _jsx(MultilineTextArea, { value: message, onChange: (v) => dispatch({ type: "COMMIT_MESSAGE", message: v }), onSubmit: () => onSubmit(message), onCancel: () => dispatch({ type: "COMMIT_CANCEL" }), width: width, height: Math.max(3, Math.min(6, height - 12)), placeholder: "(empty)" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "Ctrl+D" }), " commit · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+O" }), " editor · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+G" }), " cancel"] })] })), phase === "committing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Committing..."] })), phase === "pushing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Pushing..."] })), phase === "done" && (_jsx(Text, { color: "green", bold: true, children: "Committed and pushed!" })), phase === "error" && _jsx(Text, { color: "red", children: error }), phase !== "awaiting-message" && phase !== "done" && phase !== "error" && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }))] }));
|
|
23
22
|
}
|
|
24
23
|
export function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, dispatch, }) {
|
|
25
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { bold: true, color: "cyan", children: "Create Pull Request" }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.min(width, 50)) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "branch: " }), _jsx(Text, { children: branch })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "ticket: " }), _jsx(Text, { children: ticketId })] }), _jsx(Text, { children: " " }), phase === "choose-mode" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "How do you want to create this PR?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "f" }), " ", "Fill \u2014 use AI to fill the PR template"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "w" }), " ", "Web \u2014 open in browser to edit manually"] })] })), phase === "pushing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Pushing branch..."] })), phase === "filling" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Filling PR template with AI..."] })), phase === "review" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Edit PR description" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "title: " }), _jsx(Text, { children: title })] }), _jsx(Text, { children: " " }), _jsx(MultilineTextArea, { value: body ?? "", onChange: (v) => dispatch({ type: "PR_CREATE_BODY_CHANGE", body: v }), onSubmit: () => dispatch({ type: "PR_CREATE_CONFIRM" }), onCancel: () => dispatch({ type: "PR_CREATE_CANCEL" }), width: width, height: Math.max(6, height - 10), placeholder: "(empty PR body)" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "Ctrl+D" }), " send · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+O" }), " editor · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { bold: true, color: "cyan", children: "Create Pull Request" }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.min(width, 50)) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "branch: " }), _jsx(Text, { children: branch })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "ticket: " }), _jsx(Text, { children: ticketId })] }), _jsx(Text, { children: " " }), phase === "choose-mode" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "How do you want to create this PR?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "f" }), " ", "Fill \u2014 use AI to fill the PR template"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "w" }), " ", "Web \u2014 open in browser to edit manually"] })] })), phase === "pushing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Pushing branch..."] })), phase === "filling" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Filling PR template with AI..."] })), phase === "review" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Edit PR description" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "title: " }), _jsx(Text, { children: title })] }), _jsx(Text, { children: " " }), _jsx(MultilineTextArea, { value: body ?? "", onChange: (v) => dispatch({ type: "PR_CREATE_BODY_CHANGE", body: v }), onSubmit: () => dispatch({ type: "PR_CREATE_CONFIRM" }), onCancel: () => dispatch({ type: "PR_CREATE_CANCEL" }), width: width, height: Math.max(6, height - 10), placeholder: "(empty PR body)" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "Ctrl+D" }), " send · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+O" }), " editor · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+G" }), " cancel"] })] })), phase === "confirm" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Create this PR?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "title: " }), _jsx(Text, { children: title })] }), _jsx(Text, { children: " " }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [(body ?? "")
|
|
26
25
|
.split("\n")
|
|
27
26
|
.slice(0, Math.max(4, height - 12))
|
|
28
27
|
.map((line, i) => (_jsx(Text, { wrap: "truncate", children: line || " " }, i))), (body ?? "").split("\n").length > Math.max(4, height - 12) && (_jsxs(Text, { dimColor: true, children: ["\u2026+", (body ?? "").split("\n").length - Math.max(4, height - 12), " more lines"] }))] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " / ", _jsx(Text, { color: "green", bold: true, children: "Enter" }), " create ", _jsx(Text, { color: "yellow", bold: true, children: "e" }), " keep editing ", _jsx(Text, { color: "cyan", bold: true, children: "w" }), " open in browser ", _jsx(Text, { color: "red", bold: true, children: "ESC" }), " cancel"] })] })), phase === "creating" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Creating PR..."] })), phase === "done" && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "green", bold: true, children: "PR created!" }), url ? _jsx(Text, { dimColor: true, children: url }) : null] })), phase === "error" && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "red", children: error }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "w" }), " ", "open in browser ESC cancel"] })] })), phase !== "review" && phase !== "confirm" && phase !== "error" && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }))] }));
|
|
29
28
|
}
|
|
29
|
+
const LEGEND = [
|
|
30
|
+
{
|
|
31
|
+
title: "Issue list",
|
|
32
|
+
rows: [
|
|
33
|
+
{ glyph: "▎", color: "red", meaning: "Urgent (P1) priority" },
|
|
34
|
+
{ glyph: "▎", color: "yellow", meaning: "High (P2) priority" },
|
|
35
|
+
{ glyph: "●", color: "green", meaning: "State: started / In Progress" },
|
|
36
|
+
{ glyph: "●", color: "blue", meaning: "State: unstarted / In Review" },
|
|
37
|
+
{ glyph: "●", color: "gray", meaning: "State: backlog / orphaned" },
|
|
38
|
+
{ glyph: "●", color: "magenta", meaning: "State: main repo (your non-worktree checkout)" },
|
|
39
|
+
{ glyph: "✓", color: "green", meaning: "WT column: worktree exists" },
|
|
40
|
+
{ glyph: "·", color: "gray", meaning: "WT column: no worktree" },
|
|
41
|
+
{ glyph: "✓", color: "green", meaning: "CI column: all checks passing" },
|
|
42
|
+
{ glyph: "✗", color: "red", meaning: "CI column: a check is failing" },
|
|
43
|
+
{ glyph: "●", color: "yellow", meaning: "CI column: checks pending / running" },
|
|
44
|
+
{ glyph: "·", color: "gray", meaning: "CI column: no PR or no checks" },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: "Detail panel — Worktree",
|
|
49
|
+
rows: [
|
|
50
|
+
{ glyph: "● dirty", color: "yellow", meaning: "Uncommitted changes" },
|
|
51
|
+
{ glyph: "✓ clean", color: "green", meaning: "Working tree clean" },
|
|
52
|
+
{ glyph: "↑ N", color: "cyan", meaning: "N commits ahead of base" },
|
|
53
|
+
{ glyph: "↓ N behind", color: "yellow", meaning: "Main repo: N commits to pull from origin" },
|
|
54
|
+
{ glyph: "◆", color: "red", meaning: "Session needs input (permission prompt)" },
|
|
55
|
+
{ glyph: "◆", color: "green", meaning: "Session active (Claude is working)" },
|
|
56
|
+
{ glyph: "◆", color: "yellow", meaning: "Session idle (waiting for prompt)" },
|
|
57
|
+
{ glyph: "◇", color: "cyan", meaning: "Session id stored, no live signal" },
|
|
58
|
+
{ glyph: "◇", color: "gray", meaning: "No session" },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
title: "Detail panel — Tasks (Claude todos)",
|
|
63
|
+
rows: [
|
|
64
|
+
{ glyph: "◐", color: "yellow", meaning: "Task in progress" },
|
|
65
|
+
{ glyph: "◯", color: "gray", meaning: "Task pending" },
|
|
66
|
+
{ glyph: "✓", color: "green", meaning: "Task completed" },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
title: "Section icons",
|
|
71
|
+
rows: [
|
|
72
|
+
{ glyph: "⎇", color: "cyan", meaning: "Worktree / Branch" },
|
|
73
|
+
{ glyph: "◉", color: "cyan", meaning: "Pull Request" },
|
|
74
|
+
{ glyph: "✓", color: "cyan", meaning: "Checks" },
|
|
75
|
+
{ glyph: "★", color: "cyan", meaning: "Reviews" },
|
|
76
|
+
{ glyph: "⎈", color: "cyan", meaning: "Tasks (Claude todos)" },
|
|
77
|
+
{ glyph: "◎", color: "cyan", meaning: "Linked tracker ticket (review tab)" },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
export function HelpOverlay({ width, height }) {
|
|
82
|
+
const lines = [];
|
|
83
|
+
for (const section of LEGEND) {
|
|
84
|
+
lines.push({ text: section.title, bold: true });
|
|
85
|
+
for (const row of section.rows) {
|
|
86
|
+
lines.push({
|
|
87
|
+
text: "",
|
|
88
|
+
segments: [
|
|
89
|
+
{ text: " " },
|
|
90
|
+
{ text: row.glyph.padEnd(3, " "), color: row.color, bold: true },
|
|
91
|
+
{ text: " " },
|
|
92
|
+
{ text: row.meaning, dim: true },
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
lines.push({ text: "" });
|
|
97
|
+
}
|
|
98
|
+
// Trim trailing blank
|
|
99
|
+
if (lines[lines.length - 1]?.text === "")
|
|
100
|
+
lines.pop();
|
|
101
|
+
return (_jsx(Box, { width: width, height: height, flexDirection: "column", alignItems: "center", justifyContent: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Dashboard glyph reference" }), _jsx(Text, { children: " " }), lines.map((line, i) => (_jsx(Box, { children: line.segments ? (_jsx(Text, { children: line.segments.map((seg, j) => (_jsx(Text, { color: seg.color, bold: seg.bold, dimColor: seg.dim, children: seg.text }, j))) })) : (_jsx(Text, { bold: line.bold, dimColor: line.dim, children: line.text || " " })) }, i))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Press ? or Esc to close" })] }) }));
|
|
102
|
+
}
|
|
@@ -10,6 +10,13 @@ export type ReviewActionItem = {
|
|
|
10
10
|
label: string;
|
|
11
11
|
color: string;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Action footer for the reviews tab. The factory mirrors `buildIssueActions`
|
|
15
|
+
* over in DetailPanel — same shape so the dashboard's action-row renderer
|
|
16
|
+
* doesn't need a per-tab branch. Disabled-state semantics: when an action
|
|
17
|
+
* doesn't apply (no ticket, no worktree), we omit the entry rather than
|
|
18
|
+
* dimming it, matching the issues tab's convention.
|
|
19
|
+
*/
|
|
13
20
|
export declare function buildReviewActions(item: EnrichedReviewPR): ReviewActionItem[];
|
|
14
21
|
export default function ReviewDetailPanel({ item, scrollOffset, height, width }: Props): import("react/jsx-runtime").JSX.Element;
|
|
15
22
|
export {};
|
|
@@ -18,6 +18,27 @@ function relativeTime(dateStr) {
|
|
|
18
18
|
const months = Math.floor(days / 30);
|
|
19
19
|
return `${months}mo ago`;
|
|
20
20
|
}
|
|
21
|
+
function stateColor(type) {
|
|
22
|
+
switch (type) {
|
|
23
|
+
case "started":
|
|
24
|
+
return "green";
|
|
25
|
+
case "unstarted":
|
|
26
|
+
return "blue";
|
|
27
|
+
case "backlog":
|
|
28
|
+
return "gray";
|
|
29
|
+
case "orphaned":
|
|
30
|
+
return "gray";
|
|
31
|
+
default:
|
|
32
|
+
return "yellow";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Action footer for the reviews tab. The factory mirrors `buildIssueActions`
|
|
37
|
+
* over in DetailPanel — same shape so the dashboard's action-row renderer
|
|
38
|
+
* doesn't need a per-tab branch. Disabled-state semantics: when an action
|
|
39
|
+
* doesn't apply (no ticket, no worktree), we omit the entry rather than
|
|
40
|
+
* dimming it, matching the issues tab's convention.
|
|
41
|
+
*/
|
|
21
42
|
export function buildReviewActions(item) {
|
|
22
43
|
const items = [];
|
|
23
44
|
if (item.worktree) {
|
|
@@ -27,7 +48,11 @@ export function buildReviewActions(item) {
|
|
|
27
48
|
else {
|
|
28
49
|
items.push({ key: "w", label: "Checkout", color: "cyan" });
|
|
29
50
|
}
|
|
30
|
-
items.push({ key: "
|
|
51
|
+
items.push({ key: "v", label: "View diff", color: "cyan" });
|
|
52
|
+
if (item.ticket) {
|
|
53
|
+
items.push({ key: "o", label: "Open ticket", color: "gray" });
|
|
54
|
+
}
|
|
55
|
+
items.push({ key: "p", label: "Open PR", color: "gray" });
|
|
31
56
|
if (item.worktree) {
|
|
32
57
|
items.push({ key: "d", label: "Remove", color: "red" });
|
|
33
58
|
}
|
|
@@ -37,104 +62,249 @@ export default function ReviewDetailPanel({ item, scrollOffset, height, width })
|
|
|
37
62
|
if (!item) {
|
|
38
63
|
return (_jsx(Box, { width: width, height: height, justifyContent: "center", alignItems: "center", children: _jsx(Text, { dimColor: true, children: "No PR selected" }) }));
|
|
39
64
|
}
|
|
40
|
-
const { pr } = item;
|
|
65
|
+
const { pr, ticket, worktree, checks, reviews, comments } = item;
|
|
41
66
|
const lines = [];
|
|
42
|
-
const rule = "
|
|
67
|
+
const rule = "─".repeat(width);
|
|
68
|
+
const ruleLine = { text: rule, dim: true };
|
|
43
69
|
// ── Hero ──────────────────────────────────────────────────────────
|
|
44
70
|
lines.push({ text: `#${pr.number} ${pr.title}`, bold: true });
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
// Labeled metadata block — author + lines changed live here (the list
|
|
72
|
+
// strips them to keep navigation tight). Two-column "label value" rows
|
|
73
|
+
// keep the eye scannable.
|
|
74
|
+
const labelWidth = 9;
|
|
75
|
+
const lbl = (s) => s.padEnd(labelWidth);
|
|
76
|
+
// Display name first; login is the unique handle, parenthesized so the
|
|
77
|
+
// reviewer can still recognize someone they only know by username.
|
|
78
|
+
const authorSegs = [
|
|
79
|
+
{ text: " " },
|
|
80
|
+
{ text: lbl("Author"), dim: true },
|
|
81
|
+
{ text: item.authorName ?? pr.author.login, color: "cyan" },
|
|
82
|
+
];
|
|
83
|
+
if (item.authorName) {
|
|
84
|
+
authorSegs.push({ text: " " });
|
|
85
|
+
authorSegs.push({ text: `@${pr.author.login}`, dim: true });
|
|
86
|
+
}
|
|
87
|
+
if (pr.isDraft) {
|
|
88
|
+
authorSegs.push({ text: " " });
|
|
89
|
+
authorSegs.push({ text: "draft", color: "yellow" });
|
|
90
|
+
}
|
|
91
|
+
lines.push({ text: "", segments: authorSegs });
|
|
92
|
+
const changeSegs = [{ text: " " }, { text: lbl("Changed"), dim: true }];
|
|
93
|
+
if (item.additions > 0 || item.deletions > 0 || item.changedFiles > 0) {
|
|
94
|
+
changeSegs.push({ text: `+${item.additions}`, color: "green" });
|
|
95
|
+
changeSegs.push({ text: " " });
|
|
96
|
+
changeSegs.push({ text: `−${item.deletions}`, color: "red" });
|
|
97
|
+
changeSegs.push({ text: " " });
|
|
98
|
+
changeSegs.push({
|
|
99
|
+
text: `${item.changedFiles} file${item.changedFiles === 1 ? "" : "s"}`,
|
|
100
|
+
dim: true,
|
|
55
101
|
});
|
|
56
102
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
lines.push({ text: rule, dim: true });
|
|
60
|
-
lines.push({ text: "BRANCH", dim: true });
|
|
61
|
-
lines.push({ text: ` ${item.branch}` });
|
|
62
|
-
if (item.baseBranch) {
|
|
63
|
-
lines.push({ text: ` base: ${item.baseBranch}`, dim: true });
|
|
64
|
-
}
|
|
103
|
+
else {
|
|
104
|
+
changeSegs.push({ text: "—", dim: true });
|
|
65
105
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
106
|
+
lines.push({ text: "", segments: changeSegs });
|
|
107
|
+
lines.push({
|
|
108
|
+
text: "",
|
|
109
|
+
segments: [
|
|
110
|
+
{ text: " " },
|
|
111
|
+
{ text: lbl("Updated"), dim: true },
|
|
112
|
+
{ text: relativeTime(pr.updatedAt) },
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
// ── Linked Ticket ─────────────────────────────────────────────────
|
|
116
|
+
// Only rendered when the active tracker resolved an issue from the PR's
|
|
117
|
+
// branch — gives the reviewer the same "why does this PR exist" context
|
|
118
|
+
// the issues tab has always had.
|
|
119
|
+
if (ticket) {
|
|
120
|
+
lines.push(ruleLine);
|
|
121
|
+
const tc = stateColor(ticket.state.type);
|
|
122
|
+
const ticketHeader = [
|
|
123
|
+
{ text: "◎ ", color: "cyan", bold: true },
|
|
124
|
+
{ text: ticket.identifier, bold: true },
|
|
125
|
+
{ text: " " },
|
|
126
|
+
{ text: ticket.title },
|
|
127
|
+
];
|
|
128
|
+
lines.push({ text: "", segments: ticketHeader });
|
|
129
|
+
const ticketStatus = [
|
|
130
|
+
{ text: " ● ", color: tc },
|
|
131
|
+
{ text: ticket.state.name, color: tc },
|
|
132
|
+
{ text: " · ", dim: true },
|
|
133
|
+
{ text: ticket.priorityLabel },
|
|
134
|
+
];
|
|
135
|
+
if (ticket.labels.length > 0) {
|
|
136
|
+
ticketStatus.push({ text: " · ", dim: true });
|
|
137
|
+
ticketStatus.push({ text: ticket.labels.join(", "), dim: true });
|
|
138
|
+
}
|
|
139
|
+
lines.push({ text: "", segments: ticketStatus });
|
|
140
|
+
if (ticket.description) {
|
|
141
|
+
lines.push({ text: "" });
|
|
142
|
+
for (const dLine of ticket.description.trimEnd().split("\n")) {
|
|
143
|
+
lines.push({ text: dLine });
|
|
144
|
+
}
|
|
78
145
|
}
|
|
79
|
-
|
|
80
|
-
|
|
146
|
+
}
|
|
147
|
+
// ── Pull Request ──────────────────────────────────────────────────
|
|
148
|
+
lines.push(ruleLine);
|
|
149
|
+
const prDraft = pr.isDraft ? " · draft" : "";
|
|
150
|
+
lines.push({
|
|
151
|
+
text: "",
|
|
152
|
+
segments: [
|
|
153
|
+
{ text: "◉ ", color: "cyan", bold: true },
|
|
154
|
+
{ text: "Pull Request", bold: true },
|
|
155
|
+
{ text: " " },
|
|
156
|
+
{ text: `#${pr.number}`, color: "green", bold: true },
|
|
157
|
+
{ text: " " },
|
|
158
|
+
{ text: "OPEN", color: "green" },
|
|
159
|
+
{ text: prDraft, dim: true },
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
if (pr.url) {
|
|
163
|
+
lines.push({ text: ` ${pr.url}`, dim: true });
|
|
164
|
+
}
|
|
165
|
+
// ── Branch ────────────────────────────────────────────────────────
|
|
166
|
+
if (item.branch) {
|
|
167
|
+
lines.push(ruleLine);
|
|
168
|
+
lines.push({
|
|
169
|
+
text: "",
|
|
170
|
+
segments: [
|
|
171
|
+
{ text: "⎇ ", color: "cyan", bold: true },
|
|
172
|
+
{ text: "Branch", bold: true },
|
|
173
|
+
{ text: " " },
|
|
174
|
+
{ text: item.branch },
|
|
175
|
+
...(item.baseBranch
|
|
176
|
+
? [
|
|
177
|
+
{ text: " " },
|
|
178
|
+
{ text: "→", dim: true },
|
|
179
|
+
{ text: " " },
|
|
180
|
+
{ text: item.baseBranch, dim: true },
|
|
181
|
+
]
|
|
182
|
+
: []),
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// ── Worktree ──────────────────────────────────────────────────────
|
|
187
|
+
if (worktree) {
|
|
188
|
+
lines.push(ruleLine);
|
|
189
|
+
const dirty = worktree.dirty;
|
|
190
|
+
lines.push({
|
|
191
|
+
text: "",
|
|
192
|
+
segments: [
|
|
193
|
+
{ text: "⎇ ", color: "cyan", bold: true },
|
|
194
|
+
{ text: "Worktree", bold: true },
|
|
195
|
+
{ text: " " },
|
|
196
|
+
{
|
|
197
|
+
text: dirty ? "● dirty" : "✓ clean",
|
|
198
|
+
color: dirty ? "yellow" : "green",
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
lines.push({ text: ` ${worktree.path}`, dim: true });
|
|
203
|
+
if (worktree.commitsAhead > 0) {
|
|
204
|
+
lines.push({
|
|
205
|
+
text: "",
|
|
206
|
+
segments: [{ text: " " }, { text: `↑ ${worktree.commitsAhead} ahead`, color: "cyan" }],
|
|
207
|
+
});
|
|
81
208
|
}
|
|
82
209
|
}
|
|
83
|
-
// ── Description
|
|
210
|
+
// ── Description (PR body) ─────────────────────────────────────────
|
|
84
211
|
if (item.body) {
|
|
85
|
-
lines.push(
|
|
86
|
-
lines.push({
|
|
87
|
-
|
|
212
|
+
lines.push(ruleLine);
|
|
213
|
+
lines.push({
|
|
214
|
+
text: "",
|
|
215
|
+
segments: [
|
|
216
|
+
{ text: "✎ ", color: "cyan", bold: true },
|
|
217
|
+
{ text: "Description", bold: true },
|
|
218
|
+
],
|
|
219
|
+
});
|
|
88
220
|
for (const line of item.body.trimEnd().split("\n")) {
|
|
89
221
|
lines.push({ text: line });
|
|
90
222
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
223
|
+
}
|
|
224
|
+
// ── Checks ────────────────────────────────────────────────────────
|
|
225
|
+
if (checks && checks.length > 0) {
|
|
226
|
+
const passing = checks.filter((c) => c.bucket === "pass");
|
|
227
|
+
const failing = checks.filter((c) => c.bucket === "fail");
|
|
228
|
+
const pending = checks.filter((c) => c.bucket !== "pass" && c.bucket !== "fail");
|
|
229
|
+
const headerColor = failing.length > 0 ? "red" : pending.length > 0 ? "yellow" : "green";
|
|
230
|
+
lines.push(ruleLine);
|
|
231
|
+
const headerSegs = [
|
|
232
|
+
{ text: "✓ ", color: "cyan", bold: true },
|
|
233
|
+
{ text: "Checks", bold: true },
|
|
234
|
+
{ text: " " },
|
|
235
|
+
{ text: `${passing.length}/${checks.length} passing`, color: headerColor },
|
|
236
|
+
];
|
|
237
|
+
if (failing.length > 0) {
|
|
238
|
+
headerSegs.push({ text: " · ", dim: true });
|
|
239
|
+
headerSegs.push({ text: `${failing.length} failing`, color: "red" });
|
|
240
|
+
}
|
|
241
|
+
if (pending.length > 0) {
|
|
242
|
+
headerSegs.push({ text: " · ", dim: true });
|
|
243
|
+
headerSegs.push({ text: `${pending.length} pending`, color: "yellow" });
|
|
244
|
+
}
|
|
245
|
+
lines.push({ text: "", segments: headerSegs });
|
|
246
|
+
// Order: failing first (most important), then pending, then passing.
|
|
247
|
+
for (const check of failing) {
|
|
248
|
+
const desc = check.description ? ` — ${check.description}` : "";
|
|
249
|
+
lines.push({ text: ` ✗ ${check.name}${desc}`, color: "red" });
|
|
250
|
+
}
|
|
251
|
+
for (const check of pending) {
|
|
252
|
+
lines.push({ text: ` ● ${check.name}`, color: "yellow" });
|
|
253
|
+
}
|
|
254
|
+
for (const check of passing) {
|
|
255
|
+
lines.push({ text: ` ✓ ${check.name}`, color: "green" });
|
|
109
256
|
}
|
|
110
257
|
}
|
|
111
|
-
// ── Reviews
|
|
112
|
-
if (
|
|
113
|
-
lines.push(
|
|
114
|
-
lines.push({
|
|
115
|
-
|
|
116
|
-
|
|
258
|
+
// ── Reviews ───────────────────────────────────────────────────────
|
|
259
|
+
if (reviews && reviews.length > 0) {
|
|
260
|
+
lines.push(ruleLine);
|
|
261
|
+
lines.push({
|
|
262
|
+
text: "",
|
|
263
|
+
segments: [
|
|
264
|
+
{ text: "★ ", color: "cyan", bold: true },
|
|
265
|
+
{ text: "Reviews", bold: true },
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
for (const review of reviews) {
|
|
117
269
|
const rc = review.state === "APPROVED"
|
|
118
270
|
? "green"
|
|
119
271
|
: review.state === "CHANGES_REQUESTED"
|
|
120
272
|
? "red"
|
|
121
273
|
: "yellow";
|
|
122
|
-
lines.push({
|
|
274
|
+
lines.push({
|
|
275
|
+
text: "",
|
|
276
|
+
segments: [
|
|
277
|
+
{ text: ` ${review.author.login}` },
|
|
278
|
+
{ text: " " },
|
|
279
|
+
{ text: review.state, color: rc },
|
|
280
|
+
],
|
|
281
|
+
});
|
|
123
282
|
}
|
|
124
283
|
}
|
|
125
|
-
// ── Comments
|
|
126
|
-
if (
|
|
127
|
-
lines.push(
|
|
128
|
-
lines.push({
|
|
284
|
+
// ── Comments ──────────────────────────────────────────────────────
|
|
285
|
+
if (comments && comments.length > 0) {
|
|
286
|
+
lines.push(ruleLine);
|
|
287
|
+
lines.push({
|
|
288
|
+
text: "",
|
|
289
|
+
segments: [
|
|
290
|
+
{ text: "● ", color: "cyan", bold: true },
|
|
291
|
+
{ text: "Comments", bold: true },
|
|
292
|
+
{ text: " " },
|
|
293
|
+
{ text: `${comments.length}`, dim: true },
|
|
294
|
+
],
|
|
295
|
+
});
|
|
129
296
|
// Show last 5 comments
|
|
130
|
-
const recent =
|
|
297
|
+
const recent = comments.slice(-5);
|
|
131
298
|
for (const comment of recent) {
|
|
132
299
|
lines.push({ text: "" });
|
|
133
300
|
lines.push({
|
|
134
|
-
text:
|
|
135
|
-
|
|
301
|
+
text: "",
|
|
302
|
+
segments: [
|
|
303
|
+
{ text: ` ${comment.author}`, color: "cyan" },
|
|
304
|
+
{ text: " " },
|
|
305
|
+
{ text: relativeTime(comment.createdAt), dim: true },
|
|
306
|
+
],
|
|
136
307
|
});
|
|
137
|
-
// Truncate long comments to ~4 lines
|
|
138
308
|
const bodyLines = comment.body.trimEnd().split("\n");
|
|
139
309
|
const maxLines = 4;
|
|
140
310
|
for (let i = 0; i < Math.min(bodyLines.length, maxLines); i++) {
|
|
@@ -145,7 +315,7 @@ export default function ReviewDetailPanel({ item, scrollOffset, height, width })
|
|
|
145
315
|
}
|
|
146
316
|
}
|
|
147
317
|
}
|
|
148
|
-
// ──
|
|
318
|
+
// ── Render with scroll handling ───────────────────────────────────
|
|
149
319
|
const totalLines = lines.length;
|
|
150
320
|
const canScroll = totalLines > height;
|
|
151
321
|
const contentRows = canScroll ? height - 2 : height;
|
|
@@ -155,9 +325,31 @@ export default function ReviewDetailPanel({ item, scrollOffset, height, width })
|
|
|
155
325
|
if (canScroll) {
|
|
156
326
|
const atTop = clampedOffset === 0;
|
|
157
327
|
const atBottom = clampedOffset + contentRows >= totalLines;
|
|
158
|
-
scrollArrow = atTop ? "
|
|
328
|
+
scrollArrow = atTop ? "↓ scroll" : atBottom ? "↑ scroll" : "↑↓ scroll";
|
|
159
329
|
}
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
330
|
+
// Pre-truncate to the panel width — Ink's `wrap="truncate"` on segments
|
|
331
|
+
// inside flex rows is unreliable and lets long URLs/branch names spill onto
|
|
332
|
+
// the next row, shifting everything down. Mirrors DetailPanel's clampers.
|
|
333
|
+
const clamp = (text) => text.length > width ? text.slice(0, Math.max(0, width - 1)) + "…" : text;
|
|
334
|
+
const clampSegments = (segs) => {
|
|
335
|
+
let remaining = width;
|
|
336
|
+
const out = [];
|
|
337
|
+
for (const seg of segs) {
|
|
338
|
+
if (remaining <= 0)
|
|
339
|
+
break;
|
|
340
|
+
if (seg.text.length <= remaining) {
|
|
341
|
+
out.push(seg);
|
|
342
|
+
remaining -= seg.text.length;
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
out.push({
|
|
346
|
+
...seg,
|
|
347
|
+
text: seg.text.slice(0, Math.max(0, remaining - 1)) + "…",
|
|
348
|
+
});
|
|
349
|
+
remaining = 0;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
};
|
|
354
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, overflowX: "hidden", children: [visible.map((line, i) => (_jsx(Box, { children: line.segments ? (_jsx(Text, { children: clampSegments(line.segments).map((seg, j) => (_jsx(Text, { color: seg.color, bold: seg.bold, dimColor: seg.dim, children: seg.text }, j))) })) : (_jsx(Text, { color: line.color, bold: line.bold, dimColor: line.dim, children: line.text ? clamp(line.text) : " " })) }, i))), scrollArrow && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: " " }) })), scrollArrow && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: scrollArrow }) }))] }));
|
|
163
355
|
}
|
|
@@ -4,32 +4,33 @@ function checksIndicator(checks) {
|
|
|
4
4
|
if (!checks || checks.length === 0)
|
|
5
5
|
return { text: "-", color: "gray" };
|
|
6
6
|
if (checks.some((c) => c.bucket === "fail"))
|
|
7
|
-
return { text: "
|
|
7
|
+
return { text: "✗", color: "red" };
|
|
8
8
|
if (checks.every((c) => c.bucket === "pass"))
|
|
9
|
-
return { text: "
|
|
10
|
-
return { text: "
|
|
9
|
+
return { text: "✓", color: "green" };
|
|
10
|
+
return { text: "●", color: "yellow" };
|
|
11
11
|
}
|
|
12
12
|
const HEADER_ROWS = 1;
|
|
13
13
|
export function getReviewListRowCount(flatReviews) {
|
|
14
14
|
return HEADER_ROWS + flatReviews.length;
|
|
15
15
|
}
|
|
16
16
|
export default function ReviewList({ flatReviews, selectedIndex, scrollOffset, height, width, selectionBg = "#1e3a5f", }) {
|
|
17
|
-
// Keymap footer lives in the dashboard's global CommandBar
|
|
17
|
+
// Keymap footer lives in the dashboard's global CommandBar — use the full
|
|
18
18
|
// pane height for the list so we don't render two stacked keymap rows.
|
|
19
19
|
const listHeight = height;
|
|
20
|
+
// Per the user request, the list keeps only the columns that aid
|
|
21
|
+
// navigation: PR number, title, CI. Author + lines-changed live in the
|
|
22
|
+
// detail panel where they have room to be readable.
|
|
20
23
|
const numColWidth = 6;
|
|
21
|
-
const authorColWidth = 12;
|
|
22
|
-
const changesColWidth = 10;
|
|
23
24
|
const checksColWidth = 2;
|
|
24
|
-
const fixedWidth = 2 + numColWidth + 1 +
|
|
25
|
-
const titleMaxWidth = Math.max(width - fixedWidth, 10);
|
|
25
|
+
const fixedWidth = 2 + numColWidth + 1 + checksColWidth;
|
|
26
|
+
const titleMaxWidth = Math.max(width - fixedWidth - 1, 10);
|
|
26
27
|
const totalRows = HEADER_ROWS + flatReviews.length;
|
|
27
28
|
const visibleStart = scrollOffset;
|
|
28
29
|
const visibleEnd = Math.min(visibleStart + listHeight, totalRows);
|
|
29
30
|
const rows = [];
|
|
30
31
|
for (let rowIdx = visibleStart; rowIdx < visibleEnd; rowIdx++) {
|
|
31
32
|
if (rowIdx === 0) {
|
|
32
|
-
rows.push(_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: "#".padEnd(numColWidth) }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: "".padEnd(titleMaxWidth) }), _jsx(Text, { dimColor: true, children: "
|
|
33
|
+
rows.push(_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: "#".padEnd(numColWidth) }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: "".padEnd(titleMaxWidth) }), _jsx(Text, { dimColor: true, children: "ci".padStart(checksColWidth) })] }, "col-header"));
|
|
33
34
|
continue;
|
|
34
35
|
}
|
|
35
36
|
const flatIndex = rowIdx - HEADER_ROWS;
|
|
@@ -40,14 +41,10 @@ export default function ReviewList({ flatReviews, selectedIndex, scrollOffset, h
|
|
|
40
41
|
const selected = flatIndex === selectedIndex;
|
|
41
42
|
const cursor = selected ? ">" : " ";
|
|
42
43
|
const num = `#${pr.number}`;
|
|
43
|
-
const title = pr.title.length > titleMaxWidth ? pr.title.slice(0, titleMaxWidth - 1) + "
|
|
44
|
-
const author = pr.author.login.length > authorColWidth
|
|
45
|
-
? pr.author.login.slice(0, authorColWidth - 1) + "\u2026"
|
|
46
|
-
: pr.author.login;
|
|
47
|
-
const changes = `+${item.additions} -${item.deletions}`;
|
|
44
|
+
const title = pr.title.length > titleMaxWidth ? pr.title.slice(0, titleMaxWidth - 1) + "…" : pr.title;
|
|
48
45
|
const ci = checksIndicator(item.checks);
|
|
49
46
|
const bg = selected ? selectionBg : undefined;
|
|
50
|
-
rows.push(_jsxs(Box, { width: width, children: [_jsxs(Text, { backgroundColor: bg, bold: selected, children: [cursor, " "] }), _jsx(Text, { backgroundColor: bg, color: pr.isDraft ? "gray" : "green", children: num.padEnd(numColWidth) }), _jsx(Text, { backgroundColor: bg, children: " " }), _jsx(Text, { backgroundColor: bg, bold: selected, children: title.padEnd(titleMaxWidth) }), _jsx(Text, { backgroundColor: bg,
|
|
47
|
+
rows.push(_jsxs(Box, { width: width, children: [_jsxs(Text, { backgroundColor: bg, bold: selected, children: [cursor, " "] }), _jsx(Text, { backgroundColor: bg, color: pr.isDraft ? "gray" : "green", children: num.padEnd(numColWidth) }), _jsx(Text, { backgroundColor: bg, children: " " }), _jsx(Text, { backgroundColor: bg, bold: selected, children: title.padEnd(titleMaxWidth) }), _jsx(Text, { backgroundColor: bg, color: ci.color, children: ci.text.padStart(checksColWidth) })] }, `${pr.number}`));
|
|
51
48
|
}
|
|
52
49
|
return (_jsx(Box, { flexDirection: "column", width: width, height: height, children: _jsx(Box, { flexDirection: "column", height: listHeight, children: rows }) }));
|
|
53
50
|
}
|