santree 0.5.6 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -599
- package/dist/commands/dashboard.js +267 -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";
|
|
@@ -134,6 +137,45 @@ function runPipedDiff(cwd, gitArgs, tool, themeMode) {
|
|
|
134
137
|
});
|
|
135
138
|
});
|
|
136
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Pipe an in-memory unified diff string through the configured pager. Used by
|
|
142
|
+
* the reviews-tab PR diff path, which already has per-file content from
|
|
143
|
+
* `gh pr diff` parsed into a map — no `git diff` to spawn here.
|
|
144
|
+
*/
|
|
145
|
+
function runPagerOnString(input, tool, themeMode) {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const pagerArgs = tool === "delta" ? [themeMode === "light" ? "--light" : "--dark"] : [];
|
|
148
|
+
const pagerEnv = tool === "delta"
|
|
149
|
+
? {
|
|
150
|
+
...process.env,
|
|
151
|
+
GIT_CONFIG_PARAMETERS: "'delta.hyperlinks=false' 'delta.line-numbers=false'",
|
|
152
|
+
}
|
|
153
|
+
: process.env;
|
|
154
|
+
const pager = spawn(tool, pagerArgs, {
|
|
155
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
156
|
+
env: pagerEnv,
|
|
157
|
+
});
|
|
158
|
+
let out = "";
|
|
159
|
+
let err = "";
|
|
160
|
+
pager.stdout.on("data", (d) => {
|
|
161
|
+
out += d.toString();
|
|
162
|
+
});
|
|
163
|
+
pager.stderr.on("data", (d) => {
|
|
164
|
+
err += d.toString();
|
|
165
|
+
});
|
|
166
|
+
pager.on("error", reject);
|
|
167
|
+
pager.on("close", (code) => {
|
|
168
|
+
if (code !== 0 && !out) {
|
|
169
|
+
reject(new Error(err || `${tool} exited with code ${code}`));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
resolve(splitCombinedSgr(out));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
pager.stdin.write(input);
|
|
176
|
+
pager.stdin.end();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
137
179
|
function parseNameStatus(raw) {
|
|
138
180
|
const files = [];
|
|
139
181
|
for (const line of raw.split("\n")) {
|
|
@@ -259,7 +301,7 @@ function CommandBar({ showWorkspace, mode = "default", }) {
|
|
|
259
301
|
if (mode === "diff") {
|
|
260
302
|
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
303
|
}
|
|
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" })] }));
|
|
304
|
+
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
305
|
}
|
|
264
306
|
// ── Component ─────────────────────────────────────────────────────────
|
|
265
307
|
export default function Dashboard() {
|
|
@@ -346,14 +388,23 @@ export default function Dashboard() {
|
|
|
346
388
|
}
|
|
347
389
|
repoRootRef.current = repoRoot;
|
|
348
390
|
try {
|
|
349
|
-
// Re-detect terminal theme alongside data fetch so light↔dark
|
|
350
|
-
// propagate within one refresh cycle (≤30s).
|
|
391
|
+
// Re-detect terminal theme alongside data fetch so light↔dark
|
|
392
|
+
// switches propagate within one refresh cycle (≤30s). Skip the
|
|
393
|
+
// OSC 11 query when a text-input overlay is active — the
|
|
394
|
+
// terminal's response would otherwise leak into the user's
|
|
395
|
+
// commit/PR/context message via Ink's stdin handler.
|
|
396
|
+
const overlay = stateRef.current.overlay;
|
|
397
|
+
const inTextInput = overlay === "context-input" ||
|
|
398
|
+
(overlay === "pr-create" && stateRef.current.prCreatePhase === "review") ||
|
|
399
|
+
(overlay === "commit" && stateRef.current.commitPhase === "awaiting-message");
|
|
400
|
+
const themeP = inTextInput ? Promise.resolve(null) : detectTerminalTheme();
|
|
351
401
|
const [data, reviewData, themeMode] = await Promise.all([
|
|
352
402
|
loadDashboardData(repoRoot),
|
|
353
403
|
loadReviewsData(repoRoot),
|
|
354
|
-
|
|
404
|
+
themeP,
|
|
355
405
|
]);
|
|
356
|
-
|
|
406
|
+
if (themeMode !== null)
|
|
407
|
+
setTheme(getThemeForMode(themeMode));
|
|
357
408
|
// Workspace file presence — only meaningful when the editor consumes
|
|
358
409
|
// `.code-workspace` files. Cheap directory read; recomputed each cycle
|
|
359
410
|
// in case the user adds/removes one.
|
|
@@ -644,6 +695,29 @@ export default function Dashboard() {
|
|
|
644
695
|
process.stdout.write("\x1b[?1002h\x1b[?1006h");
|
|
645
696
|
};
|
|
646
697
|
}, [state.overlay, state.prCreatePhase, state.commitPhase]);
|
|
698
|
+
// ── Diff overlay: load file list when opened (gh pr diff path) ────
|
|
699
|
+
// Reviews-tab PRs without a local worktree shell out to `gh pr diff <n>`,
|
|
700
|
+
// parse the unified blob into per-file records, and stash the per-file
|
|
701
|
+
// content for the content-loader effect below to read synchronously.
|
|
702
|
+
useEffect(() => {
|
|
703
|
+
if (state.overlay !== "diff" || state.diffPRNumber == null)
|
|
704
|
+
return;
|
|
705
|
+
const prNumber = state.diffPRNumber;
|
|
706
|
+
void (async () => {
|
|
707
|
+
try {
|
|
708
|
+
const { stdout } = await execAsync(`gh pr diff ${prNumber}`, {
|
|
709
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
710
|
+
});
|
|
711
|
+
const { files, contentByPath } = parseUnifiedDiff(stdout);
|
|
712
|
+
const ordered = flattenTreeFiles(files);
|
|
713
|
+
dispatch({ type: "DIFF_PR_LOADED", files: ordered, contentByPath });
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
717
|
+
dispatch({ type: "DIFF_FILES_ERROR", error: msg });
|
|
718
|
+
}
|
|
719
|
+
})();
|
|
720
|
+
}, [state.overlay, state.diffPRNumber]);
|
|
647
721
|
// ── Diff overlay: load file list when opened ──────────────────────
|
|
648
722
|
// Resolves merge-base against the configured base branch so upstream-only
|
|
649
723
|
// changes (commits on master we haven't pulled) are excluded — same semantics
|
|
@@ -702,6 +776,44 @@ export default function Dashboard() {
|
|
|
702
776
|
}
|
|
703
777
|
})();
|
|
704
778
|
}, [state.overlay, state.diffWorktreePath, state.diffBaseBranch, state.diffRefreshTick]);
|
|
779
|
+
// ── Diff overlay: select content for current file (gh pr diff path) ─
|
|
780
|
+
// Per-file slices were parsed up front by the file-list effect above —
|
|
781
|
+
// just pull the right entry out of the map. When SANTREE_DIFF_TOOL is set,
|
|
782
|
+
// pipe the slice through the pager so reviews-tab diffs get the same
|
|
783
|
+
// syntax highlighting as worktree diffs.
|
|
784
|
+
useEffect(() => {
|
|
785
|
+
if (state.overlay !== "diff" || state.diffPRNumber == null)
|
|
786
|
+
return;
|
|
787
|
+
const file = state.diffFiles[state.diffFileIndex];
|
|
788
|
+
if (!file) {
|
|
789
|
+
dispatch({ type: "DIFF_CONTENT_LOADED", content: "" });
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const raw = state.diffPRContentByPath[file.path] ?? "";
|
|
793
|
+
const tool = getDiffTool();
|
|
794
|
+
if (!tool || !raw) {
|
|
795
|
+
dispatch({ type: "DIFF_CONTENT_LOADED", content: raw });
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
dispatch({ type: "DIFF_CONTENT_LOADING" });
|
|
799
|
+
void (async () => {
|
|
800
|
+
try {
|
|
801
|
+
const content = await runPagerOnString(raw, tool, theme.mode);
|
|
802
|
+
dispatch({ type: "DIFF_CONTENT_LOADED", content });
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
806
|
+
dispatch({ type: "DIFF_CONTENT_LOADED", content: `Error rendering diff: ${msg}` });
|
|
807
|
+
}
|
|
808
|
+
})();
|
|
809
|
+
}, [
|
|
810
|
+
state.overlay,
|
|
811
|
+
state.diffPRNumber,
|
|
812
|
+
state.diffFileIndex,
|
|
813
|
+
state.diffFiles,
|
|
814
|
+
state.diffPRContentByPath,
|
|
815
|
+
theme.mode,
|
|
816
|
+
]);
|
|
705
817
|
// ── Diff overlay: load content for selected file ──────────────────
|
|
706
818
|
// If SANTREE_DIFF_TOOL is set, pipe `git diff` output through the tool so
|
|
707
819
|
// the user's preferred renderer (delta, diff-so-fancy, etc.) handles
|
|
@@ -765,7 +877,15 @@ export default function Dashboard() {
|
|
|
765
877
|
const windowName = di.issue.identifier;
|
|
766
878
|
const sessionId = di.worktree?.sessionId;
|
|
767
879
|
const bin = resolveAgentBinary();
|
|
768
|
-
|
|
880
|
+
// `claude --resume` is cwd-scoped — it only finds the session under
|
|
881
|
+
// the encoded path of the current cwd. Project conventions (direnv,
|
|
882
|
+
// shell init) sometimes leave the tmux window in a subdir, so we
|
|
883
|
+
// prepend `cd <sessionCwd>` (resolved by `findClaudeSessionCwd`)
|
|
884
|
+
// to guarantee the resume runs from where the session was created.
|
|
885
|
+
const sessionCwd = di.worktree?.sessionCwd ?? di.worktree?.path;
|
|
886
|
+
const resumeCmd = sessionId && bin && sessionCwd
|
|
887
|
+
? `cd ${shellEscape(sessionCwd)} && ${bin} --resume ${sessionId}`
|
|
888
|
+
: null;
|
|
769
889
|
const contextArg = contextFile ? ` --context-file "${contextFile}"` : "";
|
|
770
890
|
const workCmd = mode === "plan" ? `st worktree work --plan${contextArg}` : `st worktree work${contextArg}`;
|
|
771
891
|
const cmd = resumeCmd ?? workCmd;
|
|
@@ -1024,13 +1144,12 @@ export default function Dashboard() {
|
|
|
1024
1144
|
// ── Commit flow ──────────────────────────────────────────────────
|
|
1025
1145
|
const handleStageAll = useCallback(async () => {
|
|
1026
1146
|
const wtPath = stateRef.current.commitWorktreePath;
|
|
1027
|
-
const ticketId = stateRef.current.commitTicketId;
|
|
1028
1147
|
if (!wtPath)
|
|
1029
1148
|
return;
|
|
1030
1149
|
try {
|
|
1031
1150
|
await execAsync("git add -A", { cwd: wtPath });
|
|
1032
|
-
|
|
1033
|
-
dispatch({ type: "COMMIT_PHASE", phase: "
|
|
1151
|
+
// After staging, ask whether to draft with AI or write manually.
|
|
1152
|
+
dispatch({ type: "COMMIT_PHASE", phase: "choose-mode" });
|
|
1034
1153
|
}
|
|
1035
1154
|
catch (e) {
|
|
1036
1155
|
dispatch({
|
|
@@ -1039,6 +1158,46 @@ export default function Dashboard() {
|
|
|
1039
1158
|
});
|
|
1040
1159
|
}
|
|
1041
1160
|
}, []);
|
|
1161
|
+
const handleFillCommit = useCallback(async () => {
|
|
1162
|
+
const s = stateRef.current;
|
|
1163
|
+
const wtPath = s.commitWorktreePath;
|
|
1164
|
+
const branch = s.commitBranch;
|
|
1165
|
+
const ticketId = s.commitTicketId;
|
|
1166
|
+
if (!wtPath || !branch)
|
|
1167
|
+
return;
|
|
1168
|
+
dispatch({ type: "COMMIT_PHASE", phase: "filling" });
|
|
1169
|
+
const diffContent = getStagedDiffContent(wtPath);
|
|
1170
|
+
const fallbackPrefix = ticketId ? `[${ticketId}] ` : "";
|
|
1171
|
+
if (!diffContent.trim()) {
|
|
1172
|
+
dispatch({ type: "COMMIT_MESSAGE", message: fallbackPrefix });
|
|
1173
|
+
dispatch({ type: "COMMIT_PHASE", phase: "awaiting-message" });
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
// Pull ticket context so the AI message is grounded in the requested
|
|
1177
|
+
// change rather than just the literal diff.
|
|
1178
|
+
let ticketContent;
|
|
1179
|
+
const mainRoot = repoRootRef.current;
|
|
1180
|
+
if (ticketId && mainRoot) {
|
|
1181
|
+
try {
|
|
1182
|
+
const tracker = getIssueTracker(mainRoot);
|
|
1183
|
+
const result = await tracker.getIssue(ticketId, mainRoot);
|
|
1184
|
+
if (result.ok) {
|
|
1185
|
+
ticketContent = renderTicket(result.value, tracker.displayName);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
catch {
|
|
1189
|
+
// non-fatal — the prompt works with diff alone
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const drafted = await fillCommitMessage({
|
|
1193
|
+
branch,
|
|
1194
|
+
ticketId,
|
|
1195
|
+
ticketContent,
|
|
1196
|
+
diffContent,
|
|
1197
|
+
});
|
|
1198
|
+
dispatch({ type: "COMMIT_MESSAGE", message: drafted ?? fallbackPrefix });
|
|
1199
|
+
dispatch({ type: "COMMIT_PHASE", phase: "awaiting-message" });
|
|
1200
|
+
}, []);
|
|
1042
1201
|
const handleCommitSubmit = useCallback(async (value) => {
|
|
1043
1202
|
const s = stateRef.current;
|
|
1044
1203
|
if (!s.commitWorktreePath || !s.commitBranch)
|
|
@@ -1048,9 +1207,10 @@ export default function Dashboard() {
|
|
|
1048
1207
|
dispatch({ type: "COMMIT_ERROR", error: "Empty commit message" });
|
|
1049
1208
|
return;
|
|
1050
1209
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1210
|
+
// Auto-prefix with `[TICKET]` only when there's a real ticket
|
|
1211
|
+
// AND the user hasn't already typed it.
|
|
1212
|
+
const tid = s.commitTicketId;
|
|
1213
|
+
const msg = tid && !trimmed.includes(`[${tid}]`) ? `[${tid}] ${trimmed}` : trimmed;
|
|
1054
1214
|
dispatch({ type: "COMMIT_PHASE", phase: "committing" });
|
|
1055
1215
|
try {
|
|
1056
1216
|
await execAsync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, {
|
|
@@ -1263,8 +1423,25 @@ export default function Dashboard() {
|
|
|
1263
1423
|
if (state.actionMessage && input !== "q") {
|
|
1264
1424
|
dispatch({ type: "SET_ACTION_MESSAGE", message: null });
|
|
1265
1425
|
}
|
|
1426
|
+
// Help overlay — toggleable from anywhere except text-input
|
|
1427
|
+
// overlays. ? opens, ? again or Esc closes.
|
|
1428
|
+
if (state.overlay === "help") {
|
|
1429
|
+
if (input === "?" || key.escape) {
|
|
1430
|
+
dispatch({ type: "SET_OVERLAY", overlay: null });
|
|
1431
|
+
}
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
if (input === "?" && state.overlay === null) {
|
|
1435
|
+
dispatch({ type: "SET_OVERLAY", overlay: "help" });
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1266
1438
|
// Commit overlay
|
|
1267
1439
|
if (state.overlay === "commit") {
|
|
1440
|
+
// awaiting-message is owned by MultilineTextArea (Ctrl+D submit,
|
|
1441
|
+
// Ctrl+G cancel) — escape there is handled inside the component,
|
|
1442
|
+
// so we don't intercept any keys at this phase.
|
|
1443
|
+
if (state.commitPhase === "awaiting-message")
|
|
1444
|
+
return;
|
|
1268
1445
|
if (key.escape) {
|
|
1269
1446
|
dispatch({ type: "COMMIT_CANCEL" });
|
|
1270
1447
|
return;
|
|
@@ -1280,8 +1457,20 @@ export default function Dashboard() {
|
|
|
1280
1457
|
}
|
|
1281
1458
|
return;
|
|
1282
1459
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1460
|
+
if (state.commitPhase === "choose-mode") {
|
|
1461
|
+
if (input === "f") {
|
|
1462
|
+
handleFillCommit();
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
if (input === "m") {
|
|
1466
|
+
const tid = state.commitTicketId;
|
|
1467
|
+
dispatch({ type: "COMMIT_MESSAGE", message: tid ? `[${tid}] ` : "" });
|
|
1468
|
+
dispatch({ type: "COMMIT_PHASE", phase: "awaiting-message" });
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
// committing / pushing / done / error: swallow
|
|
1285
1474
|
return;
|
|
1286
1475
|
}
|
|
1287
1476
|
// PR create overlay
|
|
@@ -1681,15 +1870,55 @@ export default function Dashboard() {
|
|
|
1681
1870
|
const ri = state.flatReviews[state.reviewSelectedIndex];
|
|
1682
1871
|
if (!ri)
|
|
1683
1872
|
return;
|
|
1684
|
-
// Open
|
|
1873
|
+
// Open linked ticket in browser (only when one is associated).
|
|
1874
|
+
// Aligns with the issues tab: `[o]` always opens the ticket; `[p]`
|
|
1875
|
+
// opens the PR. The previous behavior — `[o]` opens the PR — is
|
|
1876
|
+
// the only intentional muscle-memory break in this redesign.
|
|
1685
1877
|
if (input === "o") {
|
|
1686
|
-
if (ri.
|
|
1687
|
-
|
|
1688
|
-
|
|
1878
|
+
if (!ri.ticket?.url) {
|
|
1879
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "No linked ticket" });
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (openUrl(ri.ticket.url)) {
|
|
1883
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened ticket in browser" });
|
|
1884
|
+
}
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
// Open PR in browser
|
|
1888
|
+
if (input === "p") {
|
|
1889
|
+
if (!ri.pr.url)
|
|
1890
|
+
return;
|
|
1891
|
+
if (openUrl(ri.pr.url)) {
|
|
1689
1892
|
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened PR in browser" });
|
|
1690
1893
|
}
|
|
1691
1894
|
return;
|
|
1692
1895
|
}
|
|
1896
|
+
// View diff (inline overlay).
|
|
1897
|
+
// - With a local worktree: reuse the issues-tab path (git diff
|
|
1898
|
+
// against merge-base, full XY/staging support).
|
|
1899
|
+
// - Without a worktree: parse `gh pr diff <n>` once, render the
|
|
1900
|
+
// same DiffOverlay in read-only mode.
|
|
1901
|
+
if (input === "v") {
|
|
1902
|
+
const ticketLabel = ri.ticket?.identifier ?? `#${ri.pr.number}`;
|
|
1903
|
+
if (ri.worktree) {
|
|
1904
|
+
const baseBranch = getBaseBranch(ri.worktree.branch);
|
|
1905
|
+
dispatch({
|
|
1906
|
+
type: "DIFF_OPEN",
|
|
1907
|
+
ticketId: ticketLabel,
|
|
1908
|
+
worktreePath: ri.worktree.path,
|
|
1909
|
+
baseBranch,
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
else {
|
|
1913
|
+
dispatch({
|
|
1914
|
+
type: "DIFF_OPEN_PR",
|
|
1915
|
+
label: ticketLabel,
|
|
1916
|
+
prNumber: ri.pr.number,
|
|
1917
|
+
baseBranch: ri.baseBranch ?? "main",
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1693
1922
|
// Create worktree from PR branch (checkout for local testing)
|
|
1694
1923
|
if (input === "w") {
|
|
1695
1924
|
if (ri.worktree) {
|
|
@@ -1898,7 +2127,10 @@ export default function Dashboard() {
|
|
|
1898
2127
|
const windowName = di.issue.identifier;
|
|
1899
2128
|
const sessionId = di.worktree.sessionId;
|
|
1900
2129
|
const bin = resolveAgentBinary();
|
|
1901
|
-
const
|
|
2130
|
+
const sessionCwd = di.worktree.sessionCwd ?? di.worktree.path;
|
|
2131
|
+
const resumeCmd = sessionId && bin
|
|
2132
|
+
? `cd ${shellEscape(sessionCwd)} && ${bin} --resume ${sessionId}`
|
|
2133
|
+
: null;
|
|
1902
2134
|
const worktreePath = di.worktree.path;
|
|
1903
2135
|
void (async () => {
|
|
1904
2136
|
const selected = await mux.selectWindow(windowName);
|
|
@@ -1931,9 +2163,9 @@ export default function Dashboard() {
|
|
|
1931
2163
|
dispatch({ type: "SET_ACTION_MESSAGE", message: "No issue URL available" });
|
|
1932
2164
|
return;
|
|
1933
2165
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2166
|
+
if (openUrl(di.issue.url)) {
|
|
2167
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened in browser" });
|
|
2168
|
+
}
|
|
1937
2169
|
return;
|
|
1938
2170
|
}
|
|
1939
2171
|
// Open PR
|
|
@@ -1942,9 +2174,9 @@ export default function Dashboard() {
|
|
|
1942
2174
|
dispatch({ type: "SET_ACTION_MESSAGE", message: "No PR to open" });
|
|
1943
2175
|
return;
|
|
1944
2176
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2177
|
+
if (openUrl(di.pr.url)) {
|
|
2178
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened PR in browser" });
|
|
2179
|
+
}
|
|
1948
2180
|
return;
|
|
1949
2181
|
}
|
|
1950
2182
|
// Create PR
|
|
@@ -2025,7 +2257,9 @@ export default function Dashboard() {
|
|
|
2025
2257
|
}
|
|
2026
2258
|
dispatch({
|
|
2027
2259
|
type: "COMMIT_START",
|
|
2028
|
-
|
|
2260
|
+
// Main-row commits don't carry a ticket prefix — only real
|
|
2261
|
+
// tracker tickets do.
|
|
2262
|
+
ticketId: di.issue.state.type === "main" ? null : di.issue.identifier,
|
|
2029
2263
|
worktreePath: di.worktree.path,
|
|
2030
2264
|
branch: di.worktree.branch,
|
|
2031
2265
|
gitStatus: di.worktree.gitStatus,
|
|
@@ -2094,14 +2328,14 @@ export default function Dashboard() {
|
|
|
2094
2328
|
});
|
|
2095
2329
|
// ── Render ─────────────────────────────────────────────────────────
|
|
2096
2330
|
if (state.loading) {
|
|
2097
|
-
return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children:
|
|
2331
|
+
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
2332
|
}
|
|
2099
2333
|
if (state.error) {
|
|
2100
2334
|
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
2335
|
}
|
|
2102
2336
|
const selectedIssue = state.flatIssues[state.selectedIndex] ?? null;
|
|
2103
2337
|
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: () => {
|
|
2338
|
+
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
2339
|
const mode = state.contextInputMode;
|
|
2106
2340
|
const ctx = state.contextInputValue;
|
|
2107
2341
|
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)
|