santree 0.6.3 → 0.7.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.
@@ -15,6 +15,7 @@ import { getInstalledClaudeVersion } from "../lib/version.js";
15
15
  import { extractTicketId, getStagedDiffContent } from "../lib/git.js";
16
16
  import { getMultiplexer } from "../lib/multiplexer/index.js";
17
17
  import { shellEscape } from "../lib/multiplexer/types.js";
18
+ import Spinner from "ink-spinner";
18
19
  import SquirrelLoader from "../lib/squirrel-loader.js";
19
20
  import { getPRTemplate } from "../lib/github.js";
20
21
  import { renderPrompt, renderDiff, renderTicket } from "../lib/prompts.js";
@@ -399,7 +400,8 @@ export default function Dashboard() {
399
400
  }
400
401
  try {
401
402
  // Re-detect terminal theme alongside data fetch so light↔dark
402
- // switches propagate within one refresh cycle (≤30s). Skip the
403
+ // switches propagate within one refresh cycle (≤5min, or sooner
404
+ // on a manual `R`). Skip the
403
405
  // OSC 11 query when a text-input overlay is active — the
404
406
  // terminal's response would otherwise leak into the user's
405
407
  // commit/PR/context message via Ink's stdin handler.
@@ -647,16 +649,21 @@ export default function Dashboard() {
647
649
  await refresh(true);
648
650
  };
649
651
  init();
650
- // Auto-refresh every 30s. While the diff overlay is open, also bump
651
- // the diff refresh tick so new/removed files (created or deleted
652
- // outside the dashboard) eventually show up. Stage/unstage already
653
- // patch XY in place, so this is purely about file-set drift.
652
+ // Auto-refresh every 5 minutes. Each refresh fans out into several
653
+ // `gh pr view`/`gh pr checks` calls per worktree-PR plus the reviews
654
+ // tab, all on the GraphQL API (5000-point/hour budget) a 30s cadence
655
+ // drained it within the hour when the dashboard was left open. Press
656
+ // `R` for an on-demand refresh between cycles. While the diff overlay
657
+ // is open, also bump the diff refresh tick so new/removed files
658
+ // (created or deleted outside the dashboard) eventually show up.
659
+ // Stage/unstage already patch XY in place, so this is purely about
660
+ // file-set drift.
654
661
  refreshTimerRef.current = setInterval(() => {
655
662
  refresh();
656
663
  if (stateRef.current.overlay === "diff") {
657
664
  dispatch({ type: "DIFF_REFRESH_FILES" });
658
665
  }
659
- }, 30_000);
666
+ }, 300_000);
660
667
  return () => {
661
668
  if (refreshTimerRef.current)
662
669
  clearInterval(refreshTimerRef.current);
@@ -1567,7 +1574,8 @@ export default function Dashboard() {
1567
1574
  try {
1568
1575
  const bodyFile = path.join(os.tmpdir(), `santree-pr-${Date.now()}.md`);
1569
1576
  fs.writeFileSync(bodyFile, s.prCreateBody);
1570
- const { stdout } = await execAsync(`gh pr create --title "${s.prCreateTitle.replace(/"/g, '\\"')}" --base "${base}" --head "${s.prCreateBranch}" --body-file "${bodyFile}"`, { cwd });
1577
+ const draftFlag = s.prCreateDraft ? " --draft" : "";
1578
+ const { stdout } = await execAsync(`gh pr create --title "${s.prCreateTitle.replace(/"/g, '\\"')}" --base "${base}" --head "${s.prCreateBranch}" --body-file "${bodyFile}"${draftFlag}`, { cwd });
1571
1579
  try {
1572
1580
  fs.unlinkSync(bodyFile);
1573
1581
  }
@@ -1589,8 +1597,25 @@ export default function Dashboard() {
1589
1597
  return;
1590
1598
  const base = getBaseBranch(s.prCreateBranch);
1591
1599
  const cwd = s.prCreateWorktreePath;
1600
+ // Carry the edited title/body into GitHub's compose page so the browser
1601
+ // opens pre-filled (gh passes them as URL query params). Without this,
1602
+ // the fill→"open in browser" path would drop everything the user just
1603
+ // reviewed. Note: very long bodies can be truncated by GitHub's URL
1604
+ // length limit — gh's documented `--web` behavior; the editable compose
1605
+ // page is the fallback. Draft selection lives in the browser dropdown,
1606
+ // since `gh --web` doesn't accept `--draft`.
1607
+ let bodyFile = null;
1592
1608
  try {
1593
- await execAsync(`gh pr create --web --base "${base}" --head "${s.prCreateBranch}"`, { cwd });
1609
+ let cmd = `gh pr create --web --base "${base}" --head "${s.prCreateBranch}"`;
1610
+ if (s.prCreateTitle) {
1611
+ cmd += ` --title "${s.prCreateTitle.replace(/"/g, '\\"')}"`;
1612
+ }
1613
+ if (s.prCreateBody) {
1614
+ bodyFile = path.join(os.tmpdir(), `santree-pr-${Date.now()}.md`);
1615
+ fs.writeFileSync(bodyFile, s.prCreateBody);
1616
+ cmd += ` --body-file "${bodyFile}"`;
1617
+ }
1618
+ await execAsync(cmd, { cwd });
1594
1619
  dispatch({ type: "PR_CREATE_DONE", url: "" });
1595
1620
  setTimeout(() => {
1596
1621
  dispatch({ type: "PR_CREATE_CANCEL" });
@@ -1601,6 +1626,14 @@ export default function Dashboard() {
1601
1626
  const msg = e?.stderr?.trim() || e?.message || "Failed to open in browser";
1602
1627
  dispatch({ type: "PR_CREATE_ERROR", error: msg });
1603
1628
  }
1629
+ finally {
1630
+ if (bodyFile) {
1631
+ try {
1632
+ fs.unlinkSync(bodyFile);
1633
+ }
1634
+ catch { }
1635
+ }
1636
+ }
1604
1637
  }, [refresh]);
1605
1638
  // ── Keyboard ──────────────────────────────────────────────────────
1606
1639
  useInput((input, key) => {
@@ -1682,6 +1715,10 @@ export default function Dashboard() {
1682
1715
  confirmPrCreate();
1683
1716
  return;
1684
1717
  }
1718
+ if (input === "d") {
1719
+ dispatch({ type: "PR_CREATE_TOGGLE_DRAFT" });
1720
+ return;
1721
+ }
1685
1722
  if (input === "e") {
1686
1723
  dispatch({ type: "PR_CREATE_EDIT" });
1687
1724
  return;
@@ -2688,7 +2725,7 @@ export default function Dashboard() {
2688
2725
  : state.flatIssues[state.selectedIndex]) ?? null;
2689
2726
  const selectedReview = state.flatReviews[state.reviewSelectedIndex] ?? null;
2690
2727
  const activeTracker = getIssueTracker(repoRootRef.current);
2691
- return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "santree" }), _jsxs(Text, { dimColor: true, children: [" ", "v", version] }), updateAvailable && latestVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ v", latestVersion, " available — `santree update`"] })) : null, CLAUDE_VERSION ? (_jsxs(Text, { dimColor: true, children: [" · claude ", CLAUDE_VERSION] })) : null, claudeUpdateAvailable && latestClaudeVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ ", latestClaudeVersion] })) : null, state.refreshing ? _jsx(Text, { dimColor: true, children: " · refreshing" }) : null, state.actionMessage ? (_jsxs(Text, { color: "yellow", children: [" · ", state.actionMessage] })) : null] }), _jsxs(Box, { paddingX: 1, children: [_jsx(Tab, { active: state.activeTab === "issues", label: `1 Issues (${state.flatIssues.length})`, mode: theme.mode }), _jsx(Text, { children: " " }), _jsx(Tab, { active: state.activeTab === "trees", label: `2 Trees (${state.flatTrees.length})`, mode: theme.mode }), _jsx(Text, { children: " " }), _jsx(Tab, { active: state.activeTab === "reviews", label: `3 Reviews (${state.flatReviews.length})`, mode: theme.mode })] }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", flexDirection: "column", children: [state.overlay === "help" ? (_jsx(HelpOverlay, { width: innerWidth, height: contentHeight })) : state.overlay === "tracker-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: state.trackerSelectPhase === "linear-org" ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Select a Linear workspace:" }), _jsx(Text, { children: " " }), state.trackerSelectOrgs.map((org, i) => {
2728
+ return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "santree" }), _jsxs(Text, { dimColor: true, children: [" ", "v", version] }), updateAvailable && latestVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ v", latestVersion, " available — `santree update`"] })) : null, CLAUDE_VERSION ? (_jsxs(Text, { dimColor: true, children: [" · claude ", CLAUDE_VERSION] })) : null, claudeUpdateAvailable && latestClaudeVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ ", latestClaudeVersion] })) : null, state.refreshing ? (_jsxs(Text, { dimColor: true, children: [" · ", _jsx(Spinner, { type: "dots" }), " refreshing"] })) : null, state.actionMessage ? (_jsxs(Text, { color: "yellow", children: [" · ", state.actionMessage] })) : null] }), _jsxs(Box, { paddingX: 1, children: [_jsx(Tab, { active: state.activeTab === "issues", label: `1 Issues (${state.flatIssues.length})`, mode: theme.mode }), _jsx(Text, { children: " " }), _jsx(Tab, { active: state.activeTab === "trees", label: `2 Trees (${state.flatTrees.length})`, mode: theme.mode }), _jsx(Text, { children: " " }), _jsx(Tab, { active: state.activeTab === "reviews", label: `3 Reviews (${state.flatReviews.length})`, mode: theme.mode })] }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", flexDirection: "column", children: [state.overlay === "help" ? (_jsx(HelpOverlay, { width: innerWidth, height: contentHeight })) : state.overlay === "tracker-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: state.trackerSelectPhase === "linear-org" ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Select a Linear workspace:" }), _jsx(Text, { children: " " }), state.trackerSelectOrgs.map((org, i) => {
2692
2729
  const sel = i === state.trackerSelectIndex;
2693
2730
  return (_jsxs(Text, { color: sel ? "cyan" : undefined, bold: sel, children: [sel ? "> " : " ", org.name, " (", org.slug, ")"] }, org.slug));
2694
2731
  }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to link, ESC to go back" })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Select an issue tracker for this repo:" }), _jsx(Text, { children: " " }), [
@@ -2721,7 +2758,7 @@ export default function Dashboard() {
2721
2758
  : 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
2722
2759
  .split("\n")
2723
2760
  .slice(-(contentHeight - 1))
2724
- .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.activeTab === "trees"
2761
+ .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, draft: state.prCreateDraft, dispatch: dispatch })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.activeTab === "trees"
2725
2762
  ? state.treeDetailScrollOffset
2726
2763
  : state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay, trackerName: activeTracker.displayName, canMutate: activeTracker.canMutate === true }) })] })) })] })] }));
2727
2764
  }
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  function stateColor(type) {
4
4
  switch (type) {
@@ -144,7 +144,11 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
144
144
  const contentRows = height - 1;
145
145
  const startIdx = Math.max(0, logLines.length - contentRows);
146
146
  const visible = logLines.slice(startIdx, startIdx + contentRows);
147
- return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Setting up worktree for ", creatingForTicket, "..."] }), visible.map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] }));
147
+ // Setup-script output is arbitrary-length; clamp each line to the pane
148
+ // width so long lines truncate instead of wrapping/overflowing into the
149
+ // left pane. Ink's default soft-wrap would push the box past `height`.
150
+ const clampLine = (s) => s.length > width ? s.slice(0, Math.max(0, width - 1)) + "…" : s;
151
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { color: "yellow", bold: true, children: clampLine(`Setting up worktree for ${creatingForTicket}...`) }), visible.map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: clampLine(line) }) }, i)))] }));
148
152
  }
149
153
  if (!issue) {
150
154
  return (_jsx(Box, { width: width, height: height, justifyContent: "center", alignItems: "center", children: _jsx(Text, { dimColor: true, children: "No issue selected" }) }));
@@ -10,6 +10,8 @@ interface Props {
10
10
  selectionBg?: string;
11
11
  }
12
12
  export type ListRow = {
13
+ kind: "spacer";
14
+ } | {
13
15
  kind: "header";
14
16
  name: string;
15
17
  count: number;
@@ -64,6 +64,11 @@ export function buildIssueListRows(groups, flatIssues) {
64
64
  }
65
65
  groups.forEach((group, gi) => {
66
66
  const totalIssues = group.statusGroups.reduce((sum, sg) => sum + sg.issues.length, 0);
67
+ // Blank line between projects. Modelled as a real row (not a render-only
68
+ // `marginTop`) so `rows[]` indices line up 1:1 with rendered rows —
69
+ // otherwise the dashboard's click→row mapping drifts by one per project.
70
+ if (gi > 0)
71
+ rows.push({ kind: "spacer" });
67
72
  rows.push({ kind: "header", name: group.name, count: totalIssues, isFirst: gi === 0 });
68
73
  for (const sg of group.statusGroups) {
69
74
  rows.push({ kind: "status-header", name: sg.name, type: sg.type, count: sg.issues.length });
@@ -86,6 +91,9 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
86
91
  const visible = rows.slice(scrollOffset, scrollOffset + height);
87
92
  const titleMaxWidth = Math.max(width - LEFT_FIXED - 1 /* leading space */ - RIGHT_FIXED - TITLE_GAP, 10);
88
93
  return (_jsx(Box, { flexDirection: "column", width: width, height: height, children: _jsx(Box, { flexDirection: "column", height: height, children: visible.map((row, i) => {
94
+ if (row.kind === "spacer") {
95
+ return _jsx(Box, { height: 1 }, `sp-${i}`);
96
+ }
89
97
  if (row.kind === "header") {
90
98
  // On the first project header, also render the WT/CI column
91
99
  // labels right-aligned to the worktree/CI glyph columns —
@@ -97,7 +105,7 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
97
105
  const labelPad = row.isFirst
98
106
  ? Math.max(2, width - namePart.length - labelText.length)
99
107
  : 0;
100
- return (_jsxs(Box, { marginTop: i === 0 ? 0 : 1, children: [_jsx(Text, { bold: true, children: row.name }), _jsxs(Text, { dimColor: true, children: [" ", row.count] }), row.isFirst && _jsx(Text, { dimColor: true, children: `${" ".repeat(labelPad)}${labelText}` })] }, `h-${i}`));
108
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: row.name }), _jsxs(Text, { dimColor: true, children: [" ", row.count] }), row.isFirst && _jsx(Text, { dimColor: true, children: `${" ".repeat(labelPad)}${labelText}` })] }, `h-${i}`));
101
109
  }
102
110
  if (row.kind === "status-header") {
103
111
  return (_jsxs(Box, { children: [_jsxs(Text, { color: stateColor(row.type, row.name), children: [" ", row.name] }), _jsxs(Text, { dimColor: true, children: [" ", row.count] })] }, `sh-${i}`));
@@ -22,9 +22,10 @@ interface PrCreateOverlayProps {
22
22
  url: string | null;
23
23
  body: string | null;
24
24
  title: string | null;
25
+ draft: boolean;
25
26
  dispatch: React.Dispatch<DashboardAction>;
26
27
  }
27
- export declare function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, dispatch, }: PrCreateOverlayProps): import("react/jsx-runtime").JSX.Element;
28
+ export declare function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, draft, dispatch, }: PrCreateOverlayProps): import("react/jsx-runtime").JSX.Element;
28
29
  interface HelpOverlayProps {
29
30
  width: number;
30
31
  height: number;
@@ -20,11 +20,11 @@ export function CommitOverlay({ width, height, branch, ticketId, gitStatus, phas
20
20
  return (_jsxs(Text, { color: color, children: [" ", line] }, i));
21
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" })] }))] }));
22
22
  }
23
- export function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, dispatch, }) {
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 ?? "")
23
+ export function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, draft, dispatch, }) {
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: [_jsxs(Text, { bold: true, children: ["Create this PR?", " ", _jsxs(Text, { color: draft ? "yellow" : "green", children: ["(", draft ? "draft" : "ready for review", ")"] })] }), _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 ?? "")
25
25
  .split("\n")
26
26
  .slice(0, Math.max(4, height - 12))
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" })] }))] }));
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: "d" }), ` ${draft ? "mark ready" : "mark draft"} `, _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" })] }))] }));
28
28
  }
29
29
  const LEGEND = [
30
30
  {
@@ -146,6 +146,7 @@ export interface DashboardState {
146
146
  prCreateUrl: string | null;
147
147
  prCreateBody: string | null;
148
148
  prCreateTitle: string | null;
149
+ prCreateDraft: boolean;
149
150
  setupMode: "plan" | "implement" | null;
150
151
  baseSelectOptions: string[];
151
152
  baseSelectIndex: number;
@@ -312,6 +313,8 @@ export type DashboardAction = {
312
313
  type: "PR_CREATE_CONFIRM";
313
314
  } | {
314
315
  type: "PR_CREATE_EDIT";
316
+ } | {
317
+ type: "PR_CREATE_TOGGLE_DRAFT";
315
318
  } | {
316
319
  type: "PR_CREATE_DONE";
317
320
  url: string;
@@ -49,6 +49,7 @@ export const initialState = {
49
49
  prCreateUrl: null,
50
50
  prCreateBody: null,
51
51
  prCreateTitle: null,
52
+ prCreateDraft: false,
52
53
  setupMode: null,
53
54
  baseSelectOptions: [],
54
55
  baseSelectIndex: 0,
@@ -260,6 +261,7 @@ export function reducer(state, action) {
260
261
  prCreateBranch: action.branch,
261
262
  prCreateError: null,
262
263
  prCreateUrl: null,
264
+ prCreateDraft: false,
263
265
  };
264
266
  case "PR_CREATE_PHASE":
265
267
  return { ...state, prCreatePhase: action.phase };
@@ -277,6 +279,8 @@ export function reducer(state, action) {
277
279
  return { ...state, prCreateBody: action.body };
278
280
  case "PR_CREATE_CONFIRM":
279
281
  return { ...state, prCreatePhase: "confirm" };
282
+ case "PR_CREATE_TOGGLE_DRAFT":
283
+ return { ...state, prCreateDraft: !state.prCreateDraft };
280
284
  case "PR_CREATE_EDIT":
281
285
  return { ...state, prCreatePhase: "review" };
282
286
  case "PR_CREATE_DONE":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "santree",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "Git worktree manager",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Toscanini",