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,8 +1,7 @@
|
|
|
1
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
|
-
import
|
|
5
|
-
import { exec, execSync, spawn } from "child_process";
|
|
4
|
+
import { exec, spawn } from "child_process";
|
|
6
5
|
import { promisify } from "util";
|
|
7
6
|
import { createRequire } from "module";
|
|
8
7
|
import * as fs from "fs";
|
|
@@ -11,13 +10,17 @@ const require = createRequire(import.meta.url);
|
|
|
11
10
|
const { version } = require("../../package.json");
|
|
12
11
|
import { findMainRepoRoot, createWorktree, getDefaultBranch, getBaseBranch, hasInitScript, getInitScriptPath, removeWorktree, getDiffTool, getWorktreeStatus, stageFile, unstageFile, stageAll, unstageAll, discardFile, } from "../lib/git.js";
|
|
13
12
|
import { run, spawnAsync } from "../lib/exec.js";
|
|
14
|
-
import { resolveAgentBinary } from "../lib/ai.js";
|
|
13
|
+
import { resolveAgentBinary, fillCommitMessage } from "../lib/ai.js";
|
|
15
14
|
import { getInstalledClaudeVersion } from "../lib/version.js";
|
|
16
|
-
import { extractTicketId } from "../lib/git.js";
|
|
15
|
+
import { extractTicketId, getStagedDiffContent } from "../lib/git.js";
|
|
17
16
|
import { getMultiplexer } from "../lib/multiplexer/index.js";
|
|
17
|
+
import { shellEscape } from "../lib/multiplexer/types.js";
|
|
18
|
+
import SquirrelLoader from "../lib/squirrel-loader.js";
|
|
18
19
|
import { getPRTemplate } from "../lib/github.js";
|
|
19
20
|
import { renderPrompt, renderDiff, renderTicket } from "../lib/prompts.js";
|
|
20
21
|
import { getIssueTracker } from "../lib/trackers/index.js";
|
|
22
|
+
import { openUrl } from "../lib/open-url.js";
|
|
23
|
+
import { parseUnifiedDiff } from "../lib/diff-parse.js";
|
|
21
24
|
import * as os from "os";
|
|
22
25
|
import { initialState, reducer } from "../lib/dashboard/types.js";
|
|
23
26
|
import { loadDashboardData, loadReviewsData } from "../lib/dashboard/data.js";
|
|
@@ -26,7 +29,7 @@ import { detectTerminalTheme, getThemeForMode, } from "../lib/dashboard/theme.js
|
|
|
26
29
|
import DetailPanel, { buildIssueActions } from "../lib/dashboard/DetailPanel.js";
|
|
27
30
|
import ReviewList from "../lib/dashboard/ReviewList.js";
|
|
28
31
|
import ReviewDetailPanel, { buildReviewActions } from "../lib/dashboard/ReviewDetailPanel.js";
|
|
29
|
-
import { CommitOverlay, PrCreateOverlay } from "../lib/dashboard/Overlays.js";
|
|
32
|
+
import { CommitOverlay, PrCreateOverlay, HelpOverlay } from "../lib/dashboard/Overlays.js";
|
|
30
33
|
import { MultilineTextArea } from "../lib/dashboard/MultilineTextArea.js";
|
|
31
34
|
import DiffOverlay, { flattenTreeFiles, computeDiffLayout, clampDiffLeftWidth, DIFF_DIVIDER_WIDTH, } from "../lib/dashboard/DiffOverlay.js";
|
|
32
35
|
import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, getLatestVersion, getCachedLatestVersion, getLatestVersionFor, getCachedLatestVersionFor, isUpdateAvailable, } from "../lib/version.js";
|
|
@@ -259,7 +262,7 @@ function CommandBar({ showWorkspace, mode = "default", }) {
|
|
|
259
262
|
if (mode === "diff") {
|
|
260
263
|
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: "\u2423" }), _jsx(Text, { dimColor: true, children: " stage" }), dot, _jsx(Key, { k: "a" }), _jsx(Text, { dimColor: true, children: " all" }), dot, _jsx(Key, { k: "d" }), _jsx(Text, { dimColor: true, children: " discard" }), dot, _jsx(Key, { k: "e" }), _jsx(Text, { dimColor: true, children: " edit" }), dot, _jsx(Key, { k: "q" }), _jsx(Text, { dimColor: true, children: " close" })] }));
|
|
261
264
|
}
|
|
262
|
-
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" })] }));
|
|
265
|
+
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: "?" }), _jsx(Text, { dimColor: true, children: " help" }), dot, _jsx(Key, { k: "q" }), _jsx(Text, { dimColor: true, children: " quit" })] }));
|
|
263
266
|
}
|
|
264
267
|
// ── Component ─────────────────────────────────────────────────────────
|
|
265
268
|
export default function Dashboard() {
|
|
@@ -346,14 +349,23 @@ export default function Dashboard() {
|
|
|
346
349
|
}
|
|
347
350
|
repoRootRef.current = repoRoot;
|
|
348
351
|
try {
|
|
349
|
-
// Re-detect terminal theme alongside data fetch so light↔dark
|
|
350
|
-
// propagate within one refresh cycle (≤30s).
|
|
352
|
+
// Re-detect terminal theme alongside data fetch so light↔dark
|
|
353
|
+
// switches propagate within one refresh cycle (≤30s). Skip the
|
|
354
|
+
// OSC 11 query when a text-input overlay is active — the
|
|
355
|
+
// terminal's response would otherwise leak into the user's
|
|
356
|
+
// commit/PR/context message via Ink's stdin handler.
|
|
357
|
+
const overlay = stateRef.current.overlay;
|
|
358
|
+
const inTextInput = overlay === "context-input" ||
|
|
359
|
+
(overlay === "pr-create" && stateRef.current.prCreatePhase === "review") ||
|
|
360
|
+
(overlay === "commit" && stateRef.current.commitPhase === "awaiting-message");
|
|
361
|
+
const themeP = inTextInput ? Promise.resolve(null) : detectTerminalTheme();
|
|
351
362
|
const [data, reviewData, themeMode] = await Promise.all([
|
|
352
363
|
loadDashboardData(repoRoot),
|
|
353
364
|
loadReviewsData(repoRoot),
|
|
354
|
-
|
|
365
|
+
themeP,
|
|
355
366
|
]);
|
|
356
|
-
|
|
367
|
+
if (themeMode !== null)
|
|
368
|
+
setTheme(getThemeForMode(themeMode));
|
|
357
369
|
// Workspace file presence — only meaningful when the editor consumes
|
|
358
370
|
// `.code-workspace` files. Cheap directory read; recomputed each cycle
|
|
359
371
|
// in case the user adds/removes one.
|
|
@@ -629,21 +641,44 @@ export default function Dashboard() {
|
|
|
629
641
|
}
|
|
630
642
|
}, [state.reviewSelectedIndex, state.flatReviews, contentHeight, state.reviewListScrollOffset]);
|
|
631
643
|
// ── Mouse tracking pause ─────────────────────────────────────────
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
// review); restore when that phase ends.
|
|
644
|
+
// With SGR mouse tracking on, every click emits `\x1b[<btn;col;rowM` —
|
|
645
|
+
// these escape sequences leak into text inputs as garbage characters
|
|
646
|
+
// (and into MultilineTextArea, the leading ESC fires key.escape).
|
|
647
|
+
// Disable tracking while any text-input overlay is mounted; restore on exit.
|
|
637
648
|
useEffect(() => {
|
|
638
|
-
const needsMouseOff =
|
|
639
|
-
(state.overlay === "pr-create" && state.prCreatePhase === "review")
|
|
649
|
+
const needsMouseOff = state.overlay === "context-input" ||
|
|
650
|
+
(state.overlay === "pr-create" && state.prCreatePhase === "review") ||
|
|
651
|
+
(state.overlay === "commit" && state.commitPhase === "awaiting-message");
|
|
640
652
|
if (!needsMouseOff)
|
|
641
653
|
return;
|
|
642
654
|
process.stdout.write("\x1b[?1002l\x1b[?1006l");
|
|
643
655
|
return () => {
|
|
644
656
|
process.stdout.write("\x1b[?1002h\x1b[?1006h");
|
|
645
657
|
};
|
|
646
|
-
}, [state.overlay, state.
|
|
658
|
+
}, [state.overlay, state.prCreatePhase, state.commitPhase]);
|
|
659
|
+
// ── Diff overlay: load file list when opened (gh pr diff path) ────
|
|
660
|
+
// Reviews-tab PRs without a local worktree shell out to `gh pr diff <n>`,
|
|
661
|
+
// parse the unified blob into per-file records, and stash the per-file
|
|
662
|
+
// content for the content-loader effect below to read synchronously.
|
|
663
|
+
useEffect(() => {
|
|
664
|
+
if (state.overlay !== "diff" || state.diffPRNumber == null)
|
|
665
|
+
return;
|
|
666
|
+
const prNumber = state.diffPRNumber;
|
|
667
|
+
void (async () => {
|
|
668
|
+
try {
|
|
669
|
+
const { stdout } = await execAsync(`gh pr diff ${prNumber}`, {
|
|
670
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
671
|
+
});
|
|
672
|
+
const { files, contentByPath } = parseUnifiedDiff(stdout);
|
|
673
|
+
const ordered = flattenTreeFiles(files);
|
|
674
|
+
dispatch({ type: "DIFF_PR_LOADED", files: ordered, contentByPath });
|
|
675
|
+
}
|
|
676
|
+
catch (err) {
|
|
677
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
678
|
+
dispatch({ type: "DIFF_FILES_ERROR", error: msg });
|
|
679
|
+
}
|
|
680
|
+
})();
|
|
681
|
+
}, [state.overlay, state.diffPRNumber]);
|
|
647
682
|
// ── Diff overlay: load file list when opened ──────────────────────
|
|
648
683
|
// Resolves merge-base against the configured base branch so upstream-only
|
|
649
684
|
// changes (commits on master we haven't pulled) are excluded — same semantics
|
|
@@ -702,6 +737,26 @@ export default function Dashboard() {
|
|
|
702
737
|
}
|
|
703
738
|
})();
|
|
704
739
|
}, [state.overlay, state.diffWorktreePath, state.diffBaseBranch, state.diffRefreshTick]);
|
|
740
|
+
// ── Diff overlay: select content for current file (gh pr diff path) ─
|
|
741
|
+
// Per-file slices were parsed up front by the file-list effect above —
|
|
742
|
+
// just pull the right entry out of the map. No subprocess per file.
|
|
743
|
+
useEffect(() => {
|
|
744
|
+
if (state.overlay !== "diff" || state.diffPRNumber == null)
|
|
745
|
+
return;
|
|
746
|
+
const file = state.diffFiles[state.diffFileIndex];
|
|
747
|
+
if (!file) {
|
|
748
|
+
dispatch({ type: "DIFF_CONTENT_LOADED", content: "" });
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const content = state.diffPRContentByPath[file.path] ?? "";
|
|
752
|
+
dispatch({ type: "DIFF_CONTENT_LOADED", content });
|
|
753
|
+
}, [
|
|
754
|
+
state.overlay,
|
|
755
|
+
state.diffPRNumber,
|
|
756
|
+
state.diffFileIndex,
|
|
757
|
+
state.diffFiles,
|
|
758
|
+
state.diffPRContentByPath,
|
|
759
|
+
]);
|
|
705
760
|
// ── Diff overlay: load content for selected file ──────────────────
|
|
706
761
|
// If SANTREE_DIFF_TOOL is set, pipe `git diff` output through the tool so
|
|
707
762
|
// the user's preferred renderer (delta, diff-so-fancy, etc.) handles
|
|
@@ -765,7 +820,15 @@ export default function Dashboard() {
|
|
|
765
820
|
const windowName = di.issue.identifier;
|
|
766
821
|
const sessionId = di.worktree?.sessionId;
|
|
767
822
|
const bin = resolveAgentBinary();
|
|
768
|
-
|
|
823
|
+
// `claude --resume` is cwd-scoped — it only finds the session under
|
|
824
|
+
// the encoded path of the current cwd. Project conventions (direnv,
|
|
825
|
+
// shell init) sometimes leave the tmux window in a subdir, so we
|
|
826
|
+
// prepend `cd <sessionCwd>` (resolved by `findClaudeSessionCwd`)
|
|
827
|
+
// to guarantee the resume runs from where the session was created.
|
|
828
|
+
const sessionCwd = di.worktree?.sessionCwd ?? di.worktree?.path;
|
|
829
|
+
const resumeCmd = sessionId && bin && sessionCwd
|
|
830
|
+
? `cd ${shellEscape(sessionCwd)} && ${bin} --resume ${sessionId}`
|
|
831
|
+
: null;
|
|
769
832
|
const contextArg = contextFile ? ` --context-file "${contextFile}"` : "";
|
|
770
833
|
const workCmd = mode === "plan" ? `st worktree work --plan${contextArg}` : `st worktree work${contextArg}`;
|
|
771
834
|
const cmd = resumeCmd ?? workCmd;
|
|
@@ -1024,13 +1087,12 @@ export default function Dashboard() {
|
|
|
1024
1087
|
// ── Commit flow ──────────────────────────────────────────────────
|
|
1025
1088
|
const handleStageAll = useCallback(async () => {
|
|
1026
1089
|
const wtPath = stateRef.current.commitWorktreePath;
|
|
1027
|
-
const ticketId = stateRef.current.commitTicketId;
|
|
1028
1090
|
if (!wtPath)
|
|
1029
1091
|
return;
|
|
1030
1092
|
try {
|
|
1031
1093
|
await execAsync("git add -A", { cwd: wtPath });
|
|
1032
|
-
|
|
1033
|
-
dispatch({ type: "COMMIT_PHASE", phase: "
|
|
1094
|
+
// After staging, ask whether to draft with AI or write manually.
|
|
1095
|
+
dispatch({ type: "COMMIT_PHASE", phase: "choose-mode" });
|
|
1034
1096
|
}
|
|
1035
1097
|
catch (e) {
|
|
1036
1098
|
dispatch({
|
|
@@ -1039,6 +1101,46 @@ export default function Dashboard() {
|
|
|
1039
1101
|
});
|
|
1040
1102
|
}
|
|
1041
1103
|
}, []);
|
|
1104
|
+
const handleFillCommit = useCallback(async () => {
|
|
1105
|
+
const s = stateRef.current;
|
|
1106
|
+
const wtPath = s.commitWorktreePath;
|
|
1107
|
+
const branch = s.commitBranch;
|
|
1108
|
+
const ticketId = s.commitTicketId;
|
|
1109
|
+
if (!wtPath || !branch)
|
|
1110
|
+
return;
|
|
1111
|
+
dispatch({ type: "COMMIT_PHASE", phase: "filling" });
|
|
1112
|
+
const diffContent = getStagedDiffContent(wtPath);
|
|
1113
|
+
const fallbackPrefix = ticketId ? `[${ticketId}] ` : "";
|
|
1114
|
+
if (!diffContent.trim()) {
|
|
1115
|
+
dispatch({ type: "COMMIT_MESSAGE", message: fallbackPrefix });
|
|
1116
|
+
dispatch({ type: "COMMIT_PHASE", phase: "awaiting-message" });
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
// Pull ticket context so the AI message is grounded in the requested
|
|
1120
|
+
// change rather than just the literal diff.
|
|
1121
|
+
let ticketContent;
|
|
1122
|
+
const mainRoot = repoRootRef.current;
|
|
1123
|
+
if (ticketId && mainRoot) {
|
|
1124
|
+
try {
|
|
1125
|
+
const tracker = getIssueTracker(mainRoot);
|
|
1126
|
+
const result = await tracker.getIssue(ticketId, mainRoot);
|
|
1127
|
+
if (result.ok) {
|
|
1128
|
+
ticketContent = renderTicket(result.value, tracker.displayName);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
catch {
|
|
1132
|
+
// non-fatal — the prompt works with diff alone
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const drafted = await fillCommitMessage({
|
|
1136
|
+
branch,
|
|
1137
|
+
ticketId,
|
|
1138
|
+
ticketContent,
|
|
1139
|
+
diffContent,
|
|
1140
|
+
});
|
|
1141
|
+
dispatch({ type: "COMMIT_MESSAGE", message: drafted ?? fallbackPrefix });
|
|
1142
|
+
dispatch({ type: "COMMIT_PHASE", phase: "awaiting-message" });
|
|
1143
|
+
}, []);
|
|
1042
1144
|
const handleCommitSubmit = useCallback(async (value) => {
|
|
1043
1145
|
const s = stateRef.current;
|
|
1044
1146
|
if (!s.commitWorktreePath || !s.commitBranch)
|
|
@@ -1048,9 +1150,10 @@ export default function Dashboard() {
|
|
|
1048
1150
|
dispatch({ type: "COMMIT_ERROR", error: "Empty commit message" });
|
|
1049
1151
|
return;
|
|
1050
1152
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1153
|
+
// Auto-prefix with `[TICKET]` only when there's a real ticket
|
|
1154
|
+
// AND the user hasn't already typed it.
|
|
1155
|
+
const tid = s.commitTicketId;
|
|
1156
|
+
const msg = tid && !trimmed.includes(`[${tid}]`) ? `[${tid}] ${trimmed}` : trimmed;
|
|
1054
1157
|
dispatch({ type: "COMMIT_PHASE", phase: "committing" });
|
|
1055
1158
|
try {
|
|
1056
1159
|
await execAsync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, {
|
|
@@ -1263,8 +1366,25 @@ export default function Dashboard() {
|
|
|
1263
1366
|
if (state.actionMessage && input !== "q") {
|
|
1264
1367
|
dispatch({ type: "SET_ACTION_MESSAGE", message: null });
|
|
1265
1368
|
}
|
|
1369
|
+
// Help overlay — toggleable from anywhere except text-input
|
|
1370
|
+
// overlays. ? opens, ? again or Esc closes.
|
|
1371
|
+
if (state.overlay === "help") {
|
|
1372
|
+
if (input === "?" || key.escape) {
|
|
1373
|
+
dispatch({ type: "SET_OVERLAY", overlay: null });
|
|
1374
|
+
}
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
if (input === "?" && state.overlay === null) {
|
|
1378
|
+
dispatch({ type: "SET_OVERLAY", overlay: "help" });
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1266
1381
|
// Commit overlay
|
|
1267
1382
|
if (state.overlay === "commit") {
|
|
1383
|
+
// awaiting-message is owned by MultilineTextArea (Ctrl+D submit,
|
|
1384
|
+
// Ctrl+G cancel) — escape there is handled inside the component,
|
|
1385
|
+
// so we don't intercept any keys at this phase.
|
|
1386
|
+
if (state.commitPhase === "awaiting-message")
|
|
1387
|
+
return;
|
|
1268
1388
|
if (key.escape) {
|
|
1269
1389
|
dispatch({ type: "COMMIT_CANCEL" });
|
|
1270
1390
|
return;
|
|
@@ -1280,8 +1400,20 @@ export default function Dashboard() {
|
|
|
1280
1400
|
}
|
|
1281
1401
|
return;
|
|
1282
1402
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1403
|
+
if (state.commitPhase === "choose-mode") {
|
|
1404
|
+
if (input === "f") {
|
|
1405
|
+
handleFillCommit();
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (input === "m") {
|
|
1409
|
+
const tid = state.commitTicketId;
|
|
1410
|
+
dispatch({ type: "COMMIT_MESSAGE", message: tid ? `[${tid}] ` : "" });
|
|
1411
|
+
dispatch({ type: "COMMIT_PHASE", phase: "awaiting-message" });
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
// committing / pushing / done / error: swallow
|
|
1285
1417
|
return;
|
|
1286
1418
|
}
|
|
1287
1419
|
// PR create overlay
|
|
@@ -1396,29 +1528,10 @@ export default function Dashboard() {
|
|
|
1396
1528
|
}
|
|
1397
1529
|
return;
|
|
1398
1530
|
}
|
|
1399
|
-
// Context-input overlay
|
|
1400
|
-
//
|
|
1401
|
-
//
|
|
1402
|
-
// Review phase: outer handles y/n/e/ESC.
|
|
1531
|
+
// Context-input overlay: MultilineTextArea owns useInput (outer is
|
|
1532
|
+
// disabled via isActive below). Submit launches directly; cancel
|
|
1533
|
+
// closes the overlay.
|
|
1403
1534
|
if (state.overlay === "context-input") {
|
|
1404
|
-
if (state.contextInputPhase === "review") {
|
|
1405
|
-
if (input === "y" || key.return) {
|
|
1406
|
-
const mode = state.contextInputMode;
|
|
1407
|
-
const ctx = state.contextInputValue;
|
|
1408
|
-
dispatch({ type: "CONTEXT_INPUT_DONE" });
|
|
1409
|
-
if (mode)
|
|
1410
|
-
doWork(mode, ctx);
|
|
1411
|
-
return;
|
|
1412
|
-
}
|
|
1413
|
-
if (input === "n" || input === "e") {
|
|
1414
|
-
dispatch({ type: "CONTEXT_INPUT_EDIT" });
|
|
1415
|
-
return;
|
|
1416
|
-
}
|
|
1417
|
-
if (key.escape) {
|
|
1418
|
-
dispatch({ type: "CONTEXT_INPUT_DONE" });
|
|
1419
|
-
return;
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
1535
|
return;
|
|
1423
1536
|
}
|
|
1424
1537
|
// Diff overlay
|
|
@@ -1700,15 +1813,55 @@ export default function Dashboard() {
|
|
|
1700
1813
|
const ri = state.flatReviews[state.reviewSelectedIndex];
|
|
1701
1814
|
if (!ri)
|
|
1702
1815
|
return;
|
|
1703
|
-
// Open
|
|
1816
|
+
// Open linked ticket in browser (only when one is associated).
|
|
1817
|
+
// Aligns with the issues tab: `[o]` always opens the ticket; `[p]`
|
|
1818
|
+
// opens the PR. The previous behavior — `[o]` opens the PR — is
|
|
1819
|
+
// the only intentional muscle-memory break in this redesign.
|
|
1704
1820
|
if (input === "o") {
|
|
1705
|
-
if (ri.
|
|
1706
|
-
|
|
1707
|
-
|
|
1821
|
+
if (!ri.ticket?.url) {
|
|
1822
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "No linked ticket" });
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
if (openUrl(ri.ticket.url)) {
|
|
1826
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened ticket in browser" });
|
|
1827
|
+
}
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
// Open PR in browser
|
|
1831
|
+
if (input === "p") {
|
|
1832
|
+
if (!ri.pr.url)
|
|
1833
|
+
return;
|
|
1834
|
+
if (openUrl(ri.pr.url)) {
|
|
1708
1835
|
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened PR in browser" });
|
|
1709
1836
|
}
|
|
1710
1837
|
return;
|
|
1711
1838
|
}
|
|
1839
|
+
// View diff (inline overlay).
|
|
1840
|
+
// - With a local worktree: reuse the issues-tab path (git diff
|
|
1841
|
+
// against merge-base, full XY/staging support).
|
|
1842
|
+
// - Without a worktree: parse `gh pr diff <n>` once, render the
|
|
1843
|
+
// same DiffOverlay in read-only mode.
|
|
1844
|
+
if (input === "v") {
|
|
1845
|
+
const ticketLabel = ri.ticket?.identifier ?? `#${ri.pr.number}`;
|
|
1846
|
+
if (ri.worktree) {
|
|
1847
|
+
const baseBranch = getBaseBranch(ri.worktree.branch);
|
|
1848
|
+
dispatch({
|
|
1849
|
+
type: "DIFF_OPEN",
|
|
1850
|
+
ticketId: ticketLabel,
|
|
1851
|
+
worktreePath: ri.worktree.path,
|
|
1852
|
+
baseBranch,
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
else {
|
|
1856
|
+
dispatch({
|
|
1857
|
+
type: "DIFF_OPEN_PR",
|
|
1858
|
+
label: ticketLabel,
|
|
1859
|
+
prNumber: ri.pr.number,
|
|
1860
|
+
baseBranch: ri.baseBranch ?? "main",
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1712
1865
|
// Create worktree from PR branch (checkout for local testing)
|
|
1713
1866
|
if (input === "w") {
|
|
1714
1867
|
if (ri.worktree) {
|
|
@@ -1917,7 +2070,10 @@ export default function Dashboard() {
|
|
|
1917
2070
|
const windowName = di.issue.identifier;
|
|
1918
2071
|
const sessionId = di.worktree.sessionId;
|
|
1919
2072
|
const bin = resolveAgentBinary();
|
|
1920
|
-
const
|
|
2073
|
+
const sessionCwd = di.worktree.sessionCwd ?? di.worktree.path;
|
|
2074
|
+
const resumeCmd = sessionId && bin
|
|
2075
|
+
? `cd ${shellEscape(sessionCwd)} && ${bin} --resume ${sessionId}`
|
|
2076
|
+
: null;
|
|
1921
2077
|
const worktreePath = di.worktree.path;
|
|
1922
2078
|
void (async () => {
|
|
1923
2079
|
const selected = await mux.selectWindow(windowName);
|
|
@@ -1950,9 +2106,9 @@ export default function Dashboard() {
|
|
|
1950
2106
|
dispatch({ type: "SET_ACTION_MESSAGE", message: "No issue URL available" });
|
|
1951
2107
|
return;
|
|
1952
2108
|
}
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2109
|
+
if (openUrl(di.issue.url)) {
|
|
2110
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened in browser" });
|
|
2111
|
+
}
|
|
1956
2112
|
return;
|
|
1957
2113
|
}
|
|
1958
2114
|
// Open PR
|
|
@@ -1961,9 +2117,9 @@ export default function Dashboard() {
|
|
|
1961
2117
|
dispatch({ type: "SET_ACTION_MESSAGE", message: "No PR to open" });
|
|
1962
2118
|
return;
|
|
1963
2119
|
}
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2120
|
+
if (openUrl(di.pr.url)) {
|
|
2121
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened PR in browser" });
|
|
2122
|
+
}
|
|
1967
2123
|
return;
|
|
1968
2124
|
}
|
|
1969
2125
|
// Create PR
|
|
@@ -2044,7 +2200,9 @@ export default function Dashboard() {
|
|
|
2044
2200
|
}
|
|
2045
2201
|
dispatch({
|
|
2046
2202
|
type: "COMMIT_START",
|
|
2047
|
-
|
|
2203
|
+
// Main-row commits don't carry a ticket prefix — only real
|
|
2204
|
+
// tracker tickets do.
|
|
2205
|
+
ticketId: di.issue.state.type === "main" ? null : di.issue.identifier,
|
|
2048
2206
|
worktreePath: di.worktree.path,
|
|
2049
2207
|
branch: di.worktree.branch,
|
|
2050
2208
|
gitStatus: di.worktree.gitStatus,
|
|
@@ -2107,23 +2265,26 @@ export default function Dashboard() {
|
|
|
2107
2265
|
return;
|
|
2108
2266
|
}
|
|
2109
2267
|
}, {
|
|
2110
|
-
isActive:
|
|
2268
|
+
isActive: state.overlay !== "context-input" &&
|
|
2111
2269
|
(state.overlay !== "pr-create" || state.prCreatePhase !== "review") &&
|
|
2112
2270
|
(state.overlay !== "commit" || state.commitPhase !== "awaiting-message"),
|
|
2113
2271
|
});
|
|
2114
2272
|
// ── Render ─────────────────────────────────────────────────────────
|
|
2115
2273
|
if (state.loading) {
|
|
2116
|
-
return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children:
|
|
2274
|
+
return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsx(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, children: _jsx(SquirrelLoader, { text: "Loading dashboard..." }) }) }));
|
|
2117
2275
|
}
|
|
2118
2276
|
if (state.error) {
|
|
2119
2277
|
return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsxs(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, flexDirection: "column", children: [_jsxs(Text, { color: "red", bold: true, children: ["Error: ", state.error] }), _jsx(Text, { dimColor: true, children: "Press R to retry or q to quit" })] }) }));
|
|
2120
2278
|
}
|
|
2121
2279
|
const selectedIssue = state.flatIssues[state.selectedIndex] ?? null;
|
|
2122
2280
|
const selectedReview = state.flatReviews[state.reviewSelectedIndex] ?? null;
|
|
2123
|
-
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 === "reviews", label: `2 Reviews (${state.flatReviews.length})`, mode: theme.mode })] }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", flexDirection: "column", children: [state.overlay === "mode-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Select mode:" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "p" }), " Plan"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "i" }), " Implement"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }) })) : state.overlay === "context-input" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", paddingX: 2, width: Math.min(columns - 8, 100), children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Extra context for ", state.contextInputMode] }), _jsx(Text, { dimColor: true, children: "Optional \u2014 appended to the prompt before launching Claude" }), _jsx(Text, { children: " " }),
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2281
|
+
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 === "reviews", label: `2 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 === "mode-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Select mode:" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "p" }), " Plan"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "i" }), " Implement"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }) })) : state.overlay === "context-input" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", paddingX: 2, width: Math.min(columns - 8, 100), children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Extra context for ", state.contextInputMode] }), _jsx(Text, { dimColor: true, children: "Optional \u2014 appended to the prompt before launching Claude" }), _jsx(Text, { children: " " }), _jsx(MultilineTextArea, { value: state.contextInputValue, onChange: (v) => dispatch({ type: "CONTEXT_INPUT_CHANGE", value: v }), onSubmit: () => {
|
|
2282
|
+
const mode = state.contextInputMode;
|
|
2283
|
+
const ctx = state.contextInputValue;
|
|
2284
|
+
dispatch({ type: "CONTEXT_INPUT_DONE" });
|
|
2285
|
+
if (mode)
|
|
2286
|
+
doWork(mode, ctx);
|
|
2287
|
+
}, onCancel: () => dispatch({ type: "CONTEXT_INPUT_DONE" }), width: Math.min(columns - 8, 100), height: 10, placeholder: "Type or paste extra context\u2026" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "Ctrl+D" }), " launch · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+O" }), " editor · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+G" }), " cancel"] })] }) })) : state.overlay === "base-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Select base branch:" }), _jsx(Text, { children: " " }), state.baseSelectOptions.map((branch, idx) => {
|
|
2127
2288
|
const selected = idx === state.baseSelectIndex;
|
|
2128
2289
|
const defaultBranch = getDefaultBranch();
|
|
2129
2290
|
const label = branch === defaultBranch ? `${branch} (default)` : branch;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import Spinner from "ink-spinner";
|
|
4
3
|
import { useEffect, useState } from "react";
|
|
4
|
+
import SquirrelLoader from "../lib/squirrel-loader.js";
|
|
5
5
|
import { exec, execSync } from "child_process";
|
|
6
6
|
import { promisify } from "util";
|
|
7
7
|
import { createRequire } from "module";
|
|
@@ -608,7 +608,7 @@ export default function Doctor() {
|
|
|
608
608
|
runChecks();
|
|
609
609
|
}, []);
|
|
610
610
|
if (loading) {
|
|
611
|
-
return (
|
|
611
|
+
return (_jsx(Box, { paddingY: 1, children: _jsx(SquirrelLoader, { text: "Checking system requirements..." }) }));
|
|
612
612
|
}
|
|
613
613
|
const requiredMissing = tools.filter((t) => t.required && (!t.installed || t.hint));
|
|
614
614
|
const optionalMissing = tools.filter((t) => !t.required && !t.installed);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, useApp, useInput } from "ink";
|
|
3
|
+
import SquirrelLoader from "../../lib/squirrel-loader.js";
|
|
4
|
+
export const description = "Render the spinning squirrel until you Ctrl+C (debug helper)";
|
|
5
|
+
export default function Squirrel() {
|
|
6
|
+
const { exit } = useApp();
|
|
7
|
+
useInput((_, key) => {
|
|
8
|
+
if (key.escape)
|
|
9
|
+
exit();
|
|
10
|
+
});
|
|
11
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "center", paddingY: 1, children: _jsx(SquirrelLoader, { text: "Press Esc or Ctrl+C to exit" }) }));
|
|
12
|
+
}
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
export declare const description = "Stage and commit changes";
|
|
2
|
-
export
|
|
3
|
+
export declare const options: z.ZodObject<{
|
|
4
|
+
fill: z.ZodOptional<z.ZodBoolean>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
type Props = {
|
|
7
|
+
options: z.infer<typeof options>;
|
|
8
|
+
};
|
|
9
|
+
export default function Commit({ options }: Props): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|