santree 0.5.6 → 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 +210 -33
- 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 +14 -1
- package/dist/lib/dashboard/Overlays.d.ts +5 -0
- package/dist/lib/dashboard/Overlays.js +75 -2
- 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 -5
- package/dist/lib/dashboard/types.js +40 -0
- 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.
|
|
@@ -644,6 +656,29 @@ export default function Dashboard() {
|
|
|
644
656
|
process.stdout.write("\x1b[?1002h\x1b[?1006h");
|
|
645
657
|
};
|
|
646
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
|
|
@@ -1681,15 +1813,55 @@ export default function Dashboard() {
|
|
|
1681
1813
|
const ri = state.flatReviews[state.reviewSelectedIndex];
|
|
1682
1814
|
if (!ri)
|
|
1683
1815
|
return;
|
|
1684
|
-
// 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.
|
|
1685
1820
|
if (input === "o") {
|
|
1686
|
-
if (ri.
|
|
1687
|
-
|
|
1688
|
-
|
|
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)) {
|
|
1689
1835
|
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened PR in browser" });
|
|
1690
1836
|
}
|
|
1691
1837
|
return;
|
|
1692
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
|
+
}
|
|
1693
1865
|
// Create worktree from PR branch (checkout for local testing)
|
|
1694
1866
|
if (input === "w") {
|
|
1695
1867
|
if (ri.worktree) {
|
|
@@ -1898,7 +2070,10 @@ export default function Dashboard() {
|
|
|
1898
2070
|
const windowName = di.issue.identifier;
|
|
1899
2071
|
const sessionId = di.worktree.sessionId;
|
|
1900
2072
|
const bin = resolveAgentBinary();
|
|
1901
|
-
const
|
|
2073
|
+
const sessionCwd = di.worktree.sessionCwd ?? di.worktree.path;
|
|
2074
|
+
const resumeCmd = sessionId && bin
|
|
2075
|
+
? `cd ${shellEscape(sessionCwd)} && ${bin} --resume ${sessionId}`
|
|
2076
|
+
: null;
|
|
1902
2077
|
const worktreePath = di.worktree.path;
|
|
1903
2078
|
void (async () => {
|
|
1904
2079
|
const selected = await mux.selectWindow(windowName);
|
|
@@ -1931,9 +2106,9 @@ export default function Dashboard() {
|
|
|
1931
2106
|
dispatch({ type: "SET_ACTION_MESSAGE", message: "No issue URL available" });
|
|
1932
2107
|
return;
|
|
1933
2108
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2109
|
+
if (openUrl(di.issue.url)) {
|
|
2110
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened in browser" });
|
|
2111
|
+
}
|
|
1937
2112
|
return;
|
|
1938
2113
|
}
|
|
1939
2114
|
// Open PR
|
|
@@ -1942,9 +2117,9 @@ export default function Dashboard() {
|
|
|
1942
2117
|
dispatch({ type: "SET_ACTION_MESSAGE", message: "No PR to open" });
|
|
1943
2118
|
return;
|
|
1944
2119
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2120
|
+
if (openUrl(di.pr.url)) {
|
|
2121
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened PR in browser" });
|
|
2122
|
+
}
|
|
1948
2123
|
return;
|
|
1949
2124
|
}
|
|
1950
2125
|
// Create PR
|
|
@@ -2025,7 +2200,9 @@ export default function Dashboard() {
|
|
|
2025
2200
|
}
|
|
2026
2201
|
dispatch({
|
|
2027
2202
|
type: "COMMIT_START",
|
|
2028
|
-
|
|
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,
|
|
2029
2206
|
worktreePath: di.worktree.path,
|
|
2030
2207
|
branch: di.worktree.branch,
|
|
2031
2208
|
gitStatus: di.worktree.gitStatus,
|
|
@@ -2094,14 +2271,14 @@ export default function Dashboard() {
|
|
|
2094
2271
|
});
|
|
2095
2272
|
// ── Render ─────────────────────────────────────────────────────────
|
|
2096
2273
|
if (state.loading) {
|
|
2097
|
-
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..." }) }) }));
|
|
2098
2275
|
}
|
|
2099
2276
|
if (state.error) {
|
|
2100
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" })] }) }));
|
|
2101
2278
|
}
|
|
2102
2279
|
const selectedIssue = state.flatIssues[state.selectedIndex] ?? null;
|
|
2103
2280
|
const selectedReview = state.flatReviews[state.reviewSelectedIndex] ?? null;
|
|
2104
|
-
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: " " }), _jsx(MultilineTextArea, { value: state.contextInputValue, onChange: (v) => dispatch({ type: "CONTEXT_INPUT_CHANGE", value: v }), onSubmit: () => {
|
|
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: () => {
|
|
2105
2282
|
const mode = state.contextInputMode;
|
|
2106
2283
|
const ctx = state.contextInputValue;
|
|
2107
2284
|
dispatch({ type: "CONTEXT_INPUT_DONE" });
|
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 {};
|
|
@@ -3,12 +3,19 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { Text, Box, useInput, useApp } from "ink";
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
5
|
import Spinner from "ink-spinner";
|
|
6
|
+
import { z } from "zod";
|
|
6
7
|
import { exec } from "child_process";
|
|
7
8
|
import { promisify } from "util";
|
|
8
|
-
import { findRepoRoot, getCurrentBranch, extractTicketId, getGitStatus, getStagedDiffStat, hasStagedChanges, hasUnstagedChanges, } from "../../lib/git.js";
|
|
9
|
+
import { findRepoRoot, findMainRepoRoot, getCurrentBranch, extractTicketId, getGitStatus, getStagedDiffStat, getStagedDiffContent, hasStagedChanges, hasUnstagedChanges, } from "../../lib/git.js";
|
|
10
|
+
import { fillCommitMessage } from "../../lib/ai.js";
|
|
11
|
+
import { getIssueTracker } from "../../lib/trackers/index.js";
|
|
12
|
+
import { renderTicket } from "../../lib/prompts.js";
|
|
9
13
|
export const description = "Stage and commit changes";
|
|
14
|
+
export const options = z.object({
|
|
15
|
+
fill: z.boolean().optional().describe("Use AI to draft a short commit message"),
|
|
16
|
+
});
|
|
10
17
|
const execAsync = promisify(exec);
|
|
11
|
-
export default function Commit() {
|
|
18
|
+
export default function Commit({ options }) {
|
|
12
19
|
const { exit } = useApp();
|
|
13
20
|
const [status, setStatus] = useState("loading");
|
|
14
21
|
const [message, setMessage] = useState("");
|
|
@@ -25,10 +32,9 @@ export default function Commit() {
|
|
|
25
32
|
stageAndContinue();
|
|
26
33
|
}
|
|
27
34
|
else if (input === "n" || input === "N" || key.escape) {
|
|
28
|
-
if (hasStagedChanges()) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
setCommitInput(prefix);
|
|
35
|
+
if (hasStagedChanges() && repoRoot && branch) {
|
|
36
|
+
// Respect --fill even when the user declines to stage more.
|
|
37
|
+
void openMessagePhase({ repoRoot, branch, ticketId });
|
|
32
38
|
}
|
|
33
39
|
else {
|
|
34
40
|
setStatus("no-changes");
|
|
@@ -39,19 +45,59 @@ export default function Commit() {
|
|
|
39
45
|
}
|
|
40
46
|
});
|
|
41
47
|
async function stageAndContinue() {
|
|
48
|
+
if (!repoRoot || !branch)
|
|
49
|
+
return;
|
|
42
50
|
try {
|
|
43
|
-
await execAsync("git add -A", { cwd: repoRoot
|
|
51
|
+
await execAsync("git add -A", { cwd: repoRoot });
|
|
44
52
|
setGitStatus(getGitStatus());
|
|
45
53
|
setDiffStat(getStagedDiffStat());
|
|
46
|
-
|
|
47
|
-
const prefix = ticketId ? `[${ticketId}] ` : "";
|
|
48
|
-
setCommitInput(prefix);
|
|
54
|
+
await openMessagePhase({ repoRoot, branch, ticketId });
|
|
49
55
|
}
|
|
50
56
|
catch (e) {
|
|
51
57
|
setStatus("error");
|
|
52
58
|
setMessage(`Failed to stage changes: ${e}`);
|
|
53
59
|
}
|
|
54
60
|
}
|
|
61
|
+
// Routes to either the AI-fill phase or straight to the bare input.
|
|
62
|
+
// Takes context as args so callers in init() (where state isn't yet
|
|
63
|
+
// propagated from the just-fired setStates) can hand in fresh values.
|
|
64
|
+
async function openMessagePhase(ctx) {
|
|
65
|
+
const prefix = ctx.ticketId ? `[${ctx.ticketId}] ` : "";
|
|
66
|
+
if (options.fill) {
|
|
67
|
+
setStatus("filling");
|
|
68
|
+
setMessage("Drafting commit message with Claude...");
|
|
69
|
+
const drafted = await draftWithAI(ctx);
|
|
70
|
+
// Whether Claude succeeds or not, fall through to the input —
|
|
71
|
+
// the user can edit or type from scratch.
|
|
72
|
+
setCommitInput(drafted ?? prefix);
|
|
73
|
+
setStatus("awaiting-message");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
setCommitInput(prefix);
|
|
77
|
+
setStatus("awaiting-message");
|
|
78
|
+
}
|
|
79
|
+
async function draftWithAI(ctx) {
|
|
80
|
+
const diffContent = getStagedDiffContent(ctx.repoRoot);
|
|
81
|
+
if (!diffContent.trim())
|
|
82
|
+
return null;
|
|
83
|
+
// Pull ticket context if we can — the prompt uses it to ground the
|
|
84
|
+
// summary in the requested change rather than the literal diff.
|
|
85
|
+
let ticketContent;
|
|
86
|
+
const mainRoot = findMainRepoRoot();
|
|
87
|
+
if (ctx.ticketId && mainRoot) {
|
|
88
|
+
const tracker = getIssueTracker(mainRoot);
|
|
89
|
+
const result = await tracker.getIssue(ctx.ticketId, mainRoot);
|
|
90
|
+
if (result.ok) {
|
|
91
|
+
ticketContent = renderTicket(result.value, tracker.displayName);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return fillCommitMessage({
|
|
95
|
+
branch: ctx.branch,
|
|
96
|
+
ticketId: ctx.ticketId,
|
|
97
|
+
ticketContent,
|
|
98
|
+
diffContent,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
55
101
|
async function handleCommitSubmit(value) {
|
|
56
102
|
const trimmed = value.trim();
|
|
57
103
|
if (!trimmed) {
|
|
@@ -128,9 +174,7 @@ export default function Commit() {
|
|
|
128
174
|
}
|
|
129
175
|
else if (staged) {
|
|
130
176
|
setDiffStat(getStagedDiffStat());
|
|
131
|
-
|
|
132
|
-
const prefix = ticket ? `[${ticket}] ` : "";
|
|
133
|
-
setCommitInput(prefix);
|
|
177
|
+
await openMessagePhase({ repoRoot: root, branch: currentBranch, ticketId: ticket });
|
|
134
178
|
}
|
|
135
179
|
else {
|
|
136
180
|
setStatus("no-changes");
|
|
@@ -140,7 +184,7 @@ export default function Commit() {
|
|
|
140
184
|
}
|
|
141
185
|
init();
|
|
142
186
|
}, []);
|
|
143
|
-
const isLoading = status === "loading" || status === "committing" || status === "pushing";
|
|
187
|
+
const isLoading = status === "loading" || status === "filling" || status === "committing" || status === "pushing";
|
|
144
188
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\uD83D\uDCBE Commit" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : status === "done" ? "green" : "blue", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] }))] }), gitStatus && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "Changes:" }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, width: "100%", children: [gitStatus
|
|
145
189
|
.split("\n")
|
|
146
190
|
.slice(0, 10)
|
package/dist/lib/ai.d.ts
CHANGED
|
@@ -75,7 +75,33 @@ export interface RunAgentResult {
|
|
|
75
75
|
export declare function runAgent(prompt: string, opts?: {
|
|
76
76
|
allowedTools?: string[];
|
|
77
77
|
}): RunAgentResult;
|
|
78
|
+
/**
|
|
79
|
+
* Async version of runAgent. Use this from Ink renderers — spawnSync
|
|
80
|
+
* blocks Node's event loop, freezing the UI (no spinner animation, no
|
|
81
|
+
* keystroke processing) for the entire duration of Claude's generation.
|
|
82
|
+
* spawn() lets the loop run during the call.
|
|
83
|
+
*/
|
|
84
|
+
export declare function runAgentAsync(prompt: string, opts?: {
|
|
85
|
+
allowedTools?: string[];
|
|
86
|
+
}): Promise<RunAgentResult>;
|
|
78
87
|
/**
|
|
79
88
|
* Clean up cached image downloads for an issue identifier on the active tracker.
|
|
80
89
|
*/
|
|
81
90
|
export declare function cleanupImages(ticketId: string): void;
|
|
91
|
+
export interface FillCommitOpts {
|
|
92
|
+
branch: string;
|
|
93
|
+
ticketId: string | null;
|
|
94
|
+
ticketContent?: string;
|
|
95
|
+
diffContent: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Generate a short imperative commit message from a staged diff.
|
|
99
|
+
* Async so callers (the Ink dashboard, the CLI commit flow) keep the
|
|
100
|
+
* event loop turning during Claude's ~5–30s generation — using the sync
|
|
101
|
+
* runAgent here freezes the renderer.
|
|
102
|
+
*
|
|
103
|
+
* Returns the trimmed message string (no quotes, no preamble) on success,
|
|
104
|
+
* or null if Claude failed. Caller is responsible for ensuring the diff
|
|
105
|
+
* is non-empty.
|
|
106
|
+
*/
|
|
107
|
+
export declare function fillCommitMessage(opts: FillCommitOpts): Promise<string | null>;
|