santree 0.5.0 → 0.5.1

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,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, 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 { useEffect, useReducer, useCallback, useRef, useState } from "react";
3
3
  import { Text, Box, useInput, useStdout, useApp } from "ink";
4
4
  import Spinner from "ink-spinner";
@@ -28,7 +28,7 @@ import ReviewList from "../lib/dashboard/ReviewList.js";
28
28
  import ReviewDetailPanel, { buildReviewActions } from "../lib/dashboard/ReviewDetailPanel.js";
29
29
  import { CommitOverlay, PrCreateOverlay } from "../lib/dashboard/Overlays.js";
30
30
  import { MultilineTextArea } from "../lib/dashboard/MultilineTextArea.js";
31
- import DiffOverlay, { flattenTreeFiles, computeDiffLayout } from "../lib/dashboard/DiffOverlay.js";
31
+ import DiffOverlay, { flattenTreeFiles, computeDiffLayout, clampDiffLeftWidth, DIFF_DIVIDER_WIDTH, } from "../lib/dashboard/DiffOverlay.js";
32
32
  import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, getLatestVersion, getCachedLatestVersion, getLatestVersionFor, getCachedLatestVersionFor, isUpdateAvailable, } from "../lib/version.js";
33
33
  export const description = "Interactive dashboard of your Linear issues";
34
34
  const execAsync = promisify(exec);
@@ -193,11 +193,15 @@ function Tab({ active, label, mode }) {
193
193
  * Single-line global keymap shown at the bottom-left of the dashboard. The
194
194
  * `E workspace` hint only appears when the action is meaningful
195
195
  * (`SANTREE_EDITOR` is `code`/`cursor` and a `.code-workspace` file exists in
196
- * the repo root).
196
+ * the repo root). When the diff overlay is active, the keymap switches to
197
+ * diff-specific bindings since the global ones don't apply.
197
198
  */
198
- function CommandBar({ showWorkspace }) {
199
+ function CommandBar({ showWorkspace, mode = "default", }) {
199
200
  const dot = _jsx(Text, { dimColor: true, children: " · " });
200
201
  const Key = ({ k }) => (_jsx(Text, { color: "cyan", bold: true, children: k }));
202
+ if (mode === "diff") {
203
+ return (_jsxs(Text, { children: [_jsx(Key, { k: "j/k" }), _jsx(Text, { dimColor: true, children: " file" }), dot, _jsx(Key, { k: "\u21E7\u2191\u2193" }), _jsx(Text, { dimColor: true, children: " scroll" }), dot, _jsx(Key, { k: "g/G" }), _jsx(Text, { dimColor: true, children: " top/bot" }), dot, _jsx(Key, { k: "q" }), _jsx(Text, { dimColor: true, children: " close" })] }));
204
+ }
201
205
  return (_jsxs(Text, { children: [_jsx(Key, { k: "j/k" }), _jsx(Text, { dimColor: true, children: " nav" }), dot, _jsx(Key, { k: "\u21E7\u2191\u2193" }), _jsx(Text, { dimColor: true, children: " scroll" }), dot, _jsx(Key, { k: "1/2" }), _jsx(Text, { dimColor: true, children: " tabs" }), showWorkspace ? (_jsxs(_Fragment, { children: [dot, _jsx(Key, { k: "E" }), _jsx(Text, { dimColor: true, children: " workspace" })] })) : null, dot, _jsx(Key, { k: "R" }), _jsx(Text, { dimColor: true, children: " refresh" }), dot, _jsx(Key, { k: "q" }), _jsx(Text, { dimColor: true, children: " quit" })] }));
202
206
  }
203
207
  // ── Component ─────────────────────────────────────────────────────────
@@ -218,7 +222,7 @@ export default function Dashboard() {
218
222
  const repoRootRef = useRef(null);
219
223
  const stateRef = useRef(state);
220
224
  stateRef.current = state;
221
- const draggingRef = useRef(false);
225
+ const draggingRef = useRef(null);
222
226
  const [termSize, setTermSize] = useState({
223
227
  columns: stdout?.columns ?? 80,
224
228
  rows: stdout?.rows ?? 24,
@@ -264,6 +268,13 @@ export default function Dashboard() {
264
268
  const leftWidthRef = useRef(leftWidth);
265
269
  leftWidthRef.current = leftWidth;
266
270
  const rightWidth = innerWidth - leftWidth - separatorWidth;
271
+ // Diff overlay's left pane width — null means "use the default formula"
272
+ // (computed inside computeDiffLayout). Becomes a number once the user drags
273
+ // the divider, and persists across overlay open/close while the dashboard
274
+ // session is alive.
275
+ const [diffLeftWidth, setDiffLeftWidth] = useState(null);
276
+ const diffLeftWidthRef = useRef(diffLeftWidth);
277
+ diffLeftWidthRef.current = diffLeftWidth;
267
278
  // Header (1) + tab strip (1) + 2 borders + command bar (1, inside box) = 5 rows
268
279
  const contentHeight = Math.max(3, rows - 5);
269
280
  const LIST_FOOTER_HEIGHT = 0;
@@ -333,11 +344,20 @@ export default function Dashboard() {
333
344
  const sepW = 3;
334
345
  // Release — stop dragging
335
346
  if (isRelease && draggingRef.current) {
336
- draggingRef.current = false;
347
+ draggingRef.current = null;
337
348
  return;
338
349
  }
339
350
  // Drag — resize if actively dragging
340
351
  if (isDrag && draggingRef.current) {
352
+ if (draggingRef.current === "diff") {
353
+ // DiffOverlay starts at abs col 2 with width=innerWidth; its
354
+ // 1-col divider sits at relative col (leftWidth+1) → abs col
355
+ // (leftWidth+2). Setting newLeft = col - 2 keeps it under the
356
+ // cursor; clampDiffLeftWidth enforces pane minimums.
357
+ const innerW = Math.max(40, cols - 2);
358
+ setDiffLeftWidth(clampDiffLeftWidth(col - 2, innerW));
359
+ return;
360
+ }
341
361
  // col is 1-based; outer border consumes col 1, so left pane spans cols 2..(lw+1).
342
362
  // Setting newLeft = col - 1 keeps the divider at the user's cursor.
343
363
  const newLeft = Math.max(minW, Math.min(col - 1, cols - 2 - sepW - minW));
@@ -361,12 +381,15 @@ export default function Dashboard() {
361
381
  files: s.diffFiles,
362
382
  fileIndex: s.diffFileIndex,
363
383
  fileScrollOffset: s.diffFileScrollOffset,
384
+ leftWidthOverride: diffLeftWidthRef.current ?? undefined,
364
385
  });
365
386
  // Body's first line is at absolute row 6 (title + tab + top border + overlay title + rule)
366
387
  const bodyRow = row - 6;
367
388
  if (bodyRow < 0 || bodyRow >= layout.bodyHeight)
368
389
  return;
369
- if (col <= layout.leftWidth) {
390
+ // DiffOverlay starts at abs col 2; left pane occupies abs cols
391
+ // 2..(leftWidth+1).
392
+ if (col <= layout.leftWidth + 1) {
370
393
  const maxIdx = s.diffFiles.length - 1;
371
394
  if (maxIdx < 0)
372
395
  return;
@@ -414,7 +437,7 @@ export default function Dashboard() {
414
437
  }
415
438
  if (!isPress)
416
439
  return;
417
- // Diff overlay click: select file row in left pane
440
+ // Diff overlay click: drag divider, or select file row in left pane
418
441
  {
419
442
  const s = stateRef.current;
420
443
  if (s.overlay === "diff") {
@@ -427,8 +450,17 @@ export default function Dashboard() {
427
450
  files: s.diffFiles,
428
451
  fileIndex: s.diffFileIndex,
429
452
  fileScrollOffset: s.diffFileScrollOffset,
453
+ leftWidthOverride: diffLeftWidthRef.current ?? undefined,
430
454
  });
431
- if (col > layout.leftWidth)
455
+ // Divider sits at abs col leftWidth+2 (DiffOverlay starts at
456
+ // abs col 2; divider at relative col leftWidth+1). Allow ±1
457
+ // tolerance — a 1-col target is hard to hit precisely.
458
+ const diffDivAbsCol = layout.leftWidth + 2;
459
+ if (col >= diffDivAbsCol - 1 && col <= diffDivAbsCol - 1 + DIFF_DIVIDER_WIDTH + 1) {
460
+ draggingRef.current = "diff";
461
+ return;
462
+ }
463
+ if (col > layout.leftWidth + 1)
432
464
  return;
433
465
  const bodyRow = row - 6;
434
466
  if (bodyRow < 0 || bodyRow >= layout.bodyHeight)
@@ -447,7 +479,7 @@ export default function Dashboard() {
447
479
  const divStart = lw + 2;
448
480
  const divEnd = lw + 1 + sepW;
449
481
  if (col >= divStart && col <= divEnd) {
450
- draggingRef.current = true;
482
+ draggingRef.current = "main";
451
483
  return;
452
484
  }
453
485
  // Left-click press: select item in left pane (cols 2..lw+1)
@@ -1292,6 +1324,7 @@ export default function Dashboard() {
1292
1324
  files: state.diffFiles,
1293
1325
  fileIndex: state.diffFileIndex,
1294
1326
  fileScrollOffset: state.diffFileScrollOffset,
1327
+ leftWidthOverride: diffLeftWidth ?? undefined,
1295
1328
  });
1296
1329
  const totalLines = state.diffContent ? state.diffContent.split("\n").length : 0;
1297
1330
  const maxScroll = Math.max(0, totalLines - layout.bodyHeight);
@@ -1851,17 +1884,22 @@ export default function Dashboard() {
1851
1884
  const defaultBranch = getDefaultBranch();
1852
1885
  const label = branch === defaultBranch ? `${branch} (default)` : branch;
1853
1886
  return (_jsx(Text, { children: _jsxs(Text, { color: selected ? "cyan" : undefined, bold: selected, children: [selected ? "> " : " ", label] }) }, branch));
1854
- }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to select, ESC to cancel" })] }) })) : state.overlay === "confirm-delete" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Remove worktree?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: selectedIssue?.worktree?.branch ?? "" }), selectedIssue?.worktree?.dirty && (_jsx(Text, { color: "yellow", children: "Warning: worktree has uncommitted changes" })), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " Confirm"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "n" }), " Cancel"] })] }) })) : state.overlay === "diff" ? (_jsx(DiffOverlay, { width: innerWidth, height: contentHeight, ticketId: state.diffTicketId ?? "", baseBranch: state.diffBaseBranch ?? "", files: state.diffFiles, fileIndex: state.diffFileIndex, fileScrollOffset: state.diffFileScrollOffset, content: state.diffContent, contentScrollOffset: state.diffContentScrollOffset, loadingFiles: state.diffLoadingFiles, loadingContent: state.diffLoadingContent, error: state.diffError, selectionBg: theme.selectionBg })) : state.overlay === "confirm-setup" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Run setup script?" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: ".santree/init.sh" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " Run setup"] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", bold: true, children: "n" }), " Skip"] })] }) })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: leftWidth, children: state.activeTab === "reviews" ? (_jsx(ReviewList, { flatReviews: state.flatReviews, selectedIndex: state.reviewSelectedIndex, scrollOffset: state.reviewListScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) : state.flatIssues.length === 0 ? (_jsx(Box, { width: leftWidth, height: contentHeight, justifyContent: "center", alignItems: "center", children: _jsx(Text, { color: "yellow", children: "No active issues" }) })) : (_jsx(IssueList, { groups: state.groups, flatIssues: state.flatIssues, selectedIndex: state.selectedIndex, scrollOffset: state.listScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) }), _jsx(Box, { flexDirection: "column", width: 3, children: Array.from({ length: contentHeight }).map((_, i) => (_jsx(Text, { dimColor: true, children: " │ " }, i))) }), _jsx(Box, { width: rightWidth, children: state.activeTab === "reviews" && state.creatingForTicket ? (_jsxs(Box, { flexDirection: "column", width: rightWidth, height: contentHeight, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Setting up worktree for ", state.creatingForTicket, "..."] }), state.creationLogs
1887
+ }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to select, ESC to cancel" })] }) })) : state.overlay === "confirm-delete" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Remove worktree?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: selectedIssue?.worktree?.branch ?? "" }), selectedIssue?.worktree?.dirty && (_jsx(Text, { color: "yellow", children: "Warning: worktree has uncommitted changes" })), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " Confirm"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "n" }), " Cancel"] })] }) })) : state.overlay === "diff" ? (_jsx(DiffOverlay, { width: innerWidth, height: contentHeight, ticketId: state.diffTicketId ?? "", baseBranch: state.diffBaseBranch ?? "", files: state.diffFiles, fileIndex: state.diffFileIndex, fileScrollOffset: state.diffFileScrollOffset, content: state.diffContent, contentScrollOffset: state.diffContentScrollOffset, loadingFiles: state.diffLoadingFiles, loadingContent: state.diffLoadingContent, error: state.diffError, selectionBg: theme.selectionBg, leftWidthOverride: diffLeftWidth ?? undefined })) : state.overlay === "confirm-setup" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Run setup script?" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: ".santree/init.sh" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " Run setup"] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", bold: true, children: "n" }), " Skip"] })] }) })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: leftWidth, children: state.activeTab === "reviews" ? (_jsx(ReviewList, { flatReviews: state.flatReviews, selectedIndex: state.reviewSelectedIndex, scrollOffset: state.reviewListScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) : state.flatIssues.length === 0 ? (_jsx(Box, { width: leftWidth, height: contentHeight, justifyContent: "center", alignItems: "center", children: _jsx(Text, { color: "yellow", children: "No active issues" }) })) : (_jsx(IssueList, { groups: state.groups, flatIssues: state.flatIssues, selectedIndex: state.selectedIndex, scrollOffset: state.listScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) }), _jsx(Box, { flexDirection: "column", width: 3, children: Array.from({ length: contentHeight }).map((_, i) => (_jsx(Text, { dimColor: true, children: " │ " }, i))) }), _jsx(Box, { width: rightWidth, children: state.activeTab === "reviews" && state.creatingForTicket ? (_jsxs(Box, { flexDirection: "column", width: rightWidth, height: contentHeight, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Setting up worktree for ", state.creatingForTicket, "..."] }), state.creationLogs
1855
1888
  .split("\n")
1856
1889
  .slice(-(contentHeight - 1))
1857
- .map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl, body: state.prCreateBody, title: state.prCreateTitle, dispatch: dispatch })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] })), _jsxs(Box, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview }) })] })] })] }));
1890
+ .map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl, body: state.prCreateBody, title: state.prCreateTitle, dispatch: dispatch })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] })), _jsxs(Box, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: state.overlay === "diff" ? "diff" : "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay }) })] })] })] }));
1858
1891
  }
1859
1892
  /**
1860
1893
  * Renders the per-issue action key hints (Resume / Editor / View diff / …)
1861
1894
  * lifted out of the detail panels so they sit on the same row as the global
1862
1895
  * command bar. Empty when nothing is selected.
1863
1896
  */
1864
- function ActionRow({ activeTab, selectedIssue, selectedReview, }) {
1897
+ function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, }) {
1898
+ // During the diff overlay, none of the per-issue actions apply (View diff
1899
+ // is what got us here, Commit/PR/etc. need the detail panel context). Keep
1900
+ // the row blank so the diff-specific CommandBar reads cleanly.
1901
+ if (overlay === "diff")
1902
+ return _jsx(Text, { children: " " });
1865
1903
  const items = activeTab === "reviews"
1866
1904
  ? selectedReview
1867
1905
  ? buildReviewActions(selectedReview)
@@ -14,6 +14,11 @@ interface Props {
14
14
  error: string | null;
15
15
  /** Theme-adapted selection background. Falls back to dark navy. */
16
16
  selectionBg?: string;
17
+ /**
18
+ * User-set left pane width (from divider drag). Falls back to the default
19
+ * formula when undefined. Always clamped against pane minimums.
20
+ */
21
+ leftWidthOverride?: number;
17
22
  }
18
23
  interface RenderedRow {
19
24
  prefix: string;
@@ -39,12 +44,18 @@ export interface DiffLayout {
39
44
  * Shared between DiffOverlay (rendering) and the dashboard mouse handler
40
45
  * (mapping click coords back to file indices).
41
46
  */
47
+ export declare const DIFF_LEFT_MIN = 20;
48
+ export declare const DIFF_RIGHT_MIN = 20;
49
+ export declare const DIFF_DIVIDER_WIDTH = 1;
50
+ export declare function defaultDiffLeftWidth(width: number): number;
51
+ export declare function clampDiffLeftWidth(leftWidth: number, width: number): number;
42
52
  export declare function computeDiffLayout(opts: {
43
53
  width: number;
44
54
  height: number;
45
55
  files: DiffFile[];
46
56
  fileIndex: number;
47
57
  fileScrollOffset: number;
58
+ leftWidthOverride?: number;
48
59
  }): DiffLayout;
49
- export default function DiffOverlay({ width, height, ticketId, baseBranch, files, fileIndex, fileScrollOffset, content, contentScrollOffset, loadingFiles, loadingContent, error, selectionBg, }: Props): import("react/jsx-runtime").JSX.Element;
60
+ export default function DiffOverlay({ width, height, ticketId, baseBranch, files, fileIndex, fileScrollOffset, content, contentScrollOffset, loadingFiles, loadingContent, error, selectionBg, leftWidthOverride, }: Props): import("react/jsx-runtime").JSX.Element;
50
61
  export {};
@@ -112,12 +112,24 @@ export function flattenTreeFiles(files) {
112
112
  * Shared between DiffOverlay (rendering) and the dashboard mouse handler
113
113
  * (mapping click coords back to file indices).
114
114
  */
115
+ export const DIFF_LEFT_MIN = 20;
116
+ export const DIFF_RIGHT_MIN = 20;
117
+ export const DIFF_DIVIDER_WIDTH = 1;
118
+ export function defaultDiffLeftWidth(width) {
119
+ return Math.min(48, Math.max(24, Math.floor(width * 0.32)));
120
+ }
121
+ export function clampDiffLeftWidth(leftWidth, width) {
122
+ const max = Math.max(DIFF_LEFT_MIN, width - DIFF_DIVIDER_WIDTH - DIFF_RIGHT_MIN);
123
+ return Math.max(DIFF_LEFT_MIN, Math.min(leftWidth, max));
124
+ }
115
125
  export function computeDiffLayout(opts) {
116
126
  const headerHeight = 2;
117
- const footerHeight = 1;
118
- const bodyHeight = Math.max(3, opts.height - headerHeight - footerHeight);
119
- const leftWidth = Math.min(48, Math.max(24, Math.floor(opts.width * 0.32)));
120
- const rightWidth = Math.max(20, opts.width - leftWidth - 1);
127
+ // Keymap footer lives in the dashboard's global CommandBar — don't reserve
128
+ // a row here or we'd render two stacked keymap rows.
129
+ const bodyHeight = Math.max(3, opts.height - headerHeight);
130
+ const requestedLeft = opts.leftWidthOverride ?? defaultDiffLeftWidth(opts.width);
131
+ const leftWidth = clampDiffLeftWidth(requestedLeft, opts.width);
132
+ const rightWidth = Math.max(DIFF_RIGHT_MIN, opts.width - leftWidth - DIFF_DIVIDER_WIDTH);
121
133
  const rows = [];
122
134
  const tree = buildTree(opts.files);
123
135
  renderTree(tree, 0, rows, { value: 0 });
@@ -194,8 +206,15 @@ function truncateVisible(s, max) {
194
206
  return out + "…";
195
207
  }
196
208
  // ── Component ─────────────────────────────────────────────────────────
197
- export default function DiffOverlay({ width, height, ticketId, baseBranch, files, fileIndex, fileScrollOffset, content, contentScrollOffset, loadingFiles, loadingContent, error, selectionBg = "#1e3a5f", }) {
198
- const layout = computeDiffLayout({ width, height, files, fileIndex, fileScrollOffset });
209
+ export default function DiffOverlay({ width, height, ticketId, baseBranch, files, fileIndex, fileScrollOffset, content, contentScrollOffset, loadingFiles, loadingContent, error, selectionBg = "#1e3a5f", leftWidthOverride, }) {
210
+ const layout = computeDiffLayout({
211
+ width,
212
+ height,
213
+ files,
214
+ fileIndex,
215
+ fileScrollOffset,
216
+ leftWidthOverride,
217
+ });
199
218
  const { bodyHeight, leftWidth, rightWidth, rows, effectiveScroll, selectedRowIdx } = layout;
200
219
  const visibleRows = rows.slice(effectiveScroll, effectiveScroll + bodyHeight);
201
220
  // Right pane: split content into lines and slice for scroll. If the content
@@ -239,5 +258,5 @@ export default function DiffOverlay({ width, height, ticketId, baseBranch, files
239
258
  // so usable column count is rightWidth - 1.
240
259
  const cell = truncateVisible(line.text || " ", Math.max(1, rightWidth - 1));
241
260
  return (_jsx(Text, { color: line.color, bold: line.bold, dimColor: line.dim, children: cell }, i));
242
- })) })] }), _jsx(Box, { flexShrink: 0, width: width, children: _jsx(Text, { dimColor: true, wrap: "truncate", children: "j/k file \u2022 J/K scroll \u2022 g/G top/bot \u2022 click select \u2022 wheel scroll \u2022 q close" }) })] }));
261
+ })) })] })] }));
243
262
  }
@@ -9,20 +9,20 @@ function checksIndicator(checks) {
9
9
  return { text: "\u2713", color: "green" };
10
10
  return { text: "\u25cf", color: "yellow" };
11
11
  }
12
- const FOOTER_HEIGHT = 2;
13
12
  const HEADER_ROWS = 1;
14
13
  export function getReviewListRowCount(flatReviews) {
15
14
  return HEADER_ROWS + flatReviews.length;
16
15
  }
17
16
  export default function ReviewList({ flatReviews, selectedIndex, scrollOffset, height, width, selectionBg = "#1e3a5f", }) {
18
- const listHeight = height - FOOTER_HEIGHT;
17
+ // Keymap footer lives in the dashboard's global CommandBar \u2014 use the full
18
+ // pane height for the list so we don't render two stacked keymap rows.
19
+ const listHeight = height;
19
20
  const numColWidth = 6;
20
21
  const authorColWidth = 12;
21
22
  const changesColWidth = 10;
22
23
  const checksColWidth = 2;
23
24
  const fixedWidth = 2 + numColWidth + 1 + authorColWidth + 1 + changesColWidth + 1 + checksColWidth;
24
25
  const titleMaxWidth = Math.max(width - fixedWidth, 10);
25
- const footerRule = "\u2500".repeat(width);
26
26
  const totalRows = HEADER_ROWS + flatReviews.length;
27
27
  const visibleStart = scrollOffset;
28
28
  const visibleEnd = Math.min(visibleStart + listHeight, totalRows);
@@ -49,5 +49,5 @@ export default function ReviewList({ flatReviews, selectedIndex, scrollOffset, h
49
49
  const bg = selected ? selectionBg : undefined;
50
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, dimColor: true, children: author.padStart(authorColWidth) }), _jsx(Text, { backgroundColor: bg, children: " " }), _jsxs(Text, { backgroundColor: bg, children: [_jsx(Text, { color: "green", children: `+${item.additions}` }), _jsx(Text, { dimColor: true, children: "/" }), _jsx(Text, { color: "red", children: `-${item.deletions}` }), "".padStart(Math.max(0, changesColWidth - changes.length))] }), _jsx(Text, { backgroundColor: bg, children: " " }), _jsx(Text, { backgroundColor: bg, color: selected ? (ci.color === "gray" ? "gray" : ci.color) : ci.color, children: ci.text.padStart(checksColWidth) })] }, `${pr.number}`));
51
51
  }
52
- return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Box, { flexDirection: "column", height: listHeight, children: rows }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: footerRule }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "j/k" }), _jsx(Text, { color: "white", children: " Navigate" }), " ", _jsxs(Text, { color: "cyan", bold: true, children: ["Shift + ", "\u2191\u2193"] }), _jsx(Text, { color: "white", children: " Scroll detail" }), " ", _jsx(Text, { color: "cyan", bold: true, children: "o" }), _jsx(Text, { color: "white", children: " Open PR" }), " ", _jsx(Text, { color: "cyan", bold: true, children: "Tab" }), _jsx(Text, { color: "white", children: " Issues" }), " ", _jsx(Text, { color: "cyan", bold: true, children: "q" }), _jsx(Text, { color: "white", children: " Quit" })] })] })] }));
52
+ return (_jsx(Box, { flexDirection: "column", width: width, height: height, children: _jsx(Box, { flexDirection: "column", height: listHeight, children: rows }) }));
53
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "santree",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Git worktree manager",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Toscanini",