santree 0.2.15 → 0.4.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/README.md +9 -0
- package/dist/commands/dashboard.js +159 -119
- package/dist/commands/doctor.js +66 -1
- package/dist/commands/helpers/text-editor.d.ts +13 -0
- package/dist/commands/helpers/text-editor.js +118 -0
- package/dist/commands/worktree/create.d.ts +1 -0
- package/dist/commands/worktree/create.js +30 -38
- package/dist/lib/ai.js +11 -8
- package/dist/lib/dashboard/MultilineTextArea.js +306 -22
- package/dist/lib/dashboard/Overlays.d.ts +2 -2
- package/dist/lib/dashboard/Overlays.js +6 -5
- package/dist/lib/dashboard/data.js +5 -5
- package/dist/lib/dashboard/external-editor.d.ts +12 -0
- package/dist/lib/dashboard/external-editor.js +74 -0
- package/dist/lib/dashboard/types.d.ts +13 -1
- package/dist/lib/dashboard/types.js +13 -0
- package/dist/lib/git.d.ts +6 -4
- package/dist/lib/git.js +8 -33
- package/dist/lib/multiplexer/cmux.d.ts +2 -0
- package/dist/lib/multiplexer/cmux.js +97 -0
- package/dist/lib/multiplexer/index.d.ts +4 -0
- package/dist/lib/multiplexer/index.js +20 -0
- package/dist/lib/multiplexer/none.d.ts +2 -0
- package/dist/lib/multiplexer/none.js +22 -0
- package/dist/lib/multiplexer/tmux.d.ts +2 -0
- package/dist/lib/multiplexer/tmux.js +82 -0
- package/dist/lib/multiplexer/types.d.ts +23 -0
- package/dist/lib/multiplexer/types.js +3 -0
- package/dist/lib/session-signal.js +5 -8
- package/package.json +1 -1
- package/shell/init.zsh.njk +45 -15
package/README.md
CHANGED
|
@@ -317,6 +317,15 @@ echo "$(date): $SANTREE_TICKET_ID waiting — $SANTREE_MESSAGE" >> /tmp/santree-
|
|
|
317
317
|
|
|
318
318
|
Make it executable: `chmod +x .santree/hooks/on-waiting.sh`
|
|
319
319
|
|
|
320
|
+
### Environment Variables
|
|
321
|
+
|
|
322
|
+
| Variable | Effect |
|
|
323
|
+
|---|---|
|
|
324
|
+
| `SANTREE_EDITOR` | Editor used by `e` (open in editor) actions in the dashboard. Defaults to `code`. Examples: `cursor`, `zed`, `code`, `nvim`. |
|
|
325
|
+
| `SANTREE_MULTIPLEXER` | Terminal multiplexer used by the dashboard and `worktree create --window`. One of `tmux`, `cmux`, `none`. If unset, auto-detects from `$TMUX` / `$CMUX_SURFACE_ID`. cmux is macOS-only and limited by [manaflow-ai/cmux#1472](https://github.com/manaflow-ai/cmux/issues/1472). |
|
|
326
|
+
|
|
327
|
+
Santree always launches Claude with `--permission-mode auto` (Claude Code's auto mode), or `plan` when invoked in plan mode. Worktree-scoped automation is the default — there is no opt-in flag.
|
|
328
|
+
|
|
320
329
|
---
|
|
321
330
|
|
|
322
331
|
## Command Options
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useReducer, useCallback, useRef, useState } from "react";
|
|
3
3
|
import { Text, Box, useInput, useStdout, useApp } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
@@ -13,6 +13,7 @@ import { findMainRepoRoot, createWorktree, getDefaultBranch, getBaseBranch, hasI
|
|
|
13
13
|
import { run, spawnAsync } from "../lib/exec.js";
|
|
14
14
|
import { resolveAgentBinary } from "../lib/ai.js";
|
|
15
15
|
import { extractTicketId } from "../lib/git.js";
|
|
16
|
+
import { getMultiplexer } from "../lib/multiplexer/index.js";
|
|
16
17
|
import { getPRTemplate } from "../lib/github.js";
|
|
17
18
|
import { renderPrompt, renderDiff, renderTicket } from "../lib/prompts.js";
|
|
18
19
|
import { getTicketContent } from "../lib/linear.js";
|
|
@@ -39,9 +40,6 @@ const CLAUDE_VERSION = (() => {
|
|
|
39
40
|
}
|
|
40
41
|
})();
|
|
41
42
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
42
|
-
function isInTmux() {
|
|
43
|
-
return !!process.env.TMUX;
|
|
44
|
-
}
|
|
45
43
|
function slugify(title) {
|
|
46
44
|
return title
|
|
47
45
|
.toLowerCase()
|
|
@@ -137,12 +135,7 @@ function ensureAltScreen() {
|
|
|
137
135
|
if (altScreenEntered)
|
|
138
136
|
return;
|
|
139
137
|
altScreenEntered = true;
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
execSync('tmux rename-window "santree"', { stdio: "ignore" });
|
|
143
|
-
}
|
|
144
|
-
catch { }
|
|
145
|
-
}
|
|
138
|
+
getMultiplexer().renameWindow("", "santree");
|
|
146
139
|
process.stdout.write("\x1b[?1049h"); // Enter alternate screen buffer
|
|
147
140
|
process.stdout.write("\x1b[?25l"); // Hide cursor
|
|
148
141
|
}
|
|
@@ -371,77 +364,95 @@ export default function Dashboard() {
|
|
|
371
364
|
// ── Mouse tracking pause ─────────────────────────────────────────
|
|
372
365
|
// The MultilineTextArea captures ESC for cancel. With SGR mouse tracking on,
|
|
373
366
|
// every click emits `\x1b[<btn;col;rowM` — Ink reads the leading ESC and fires
|
|
374
|
-
// key.escape, dismissing the overlay. Disable
|
|
375
|
-
// context-input
|
|
367
|
+
// key.escape, dismissing the overlay. Disable tracking while any overlay
|
|
368
|
+
// phase mounts a MultilineTextArea (context-input editing OR pr-create
|
|
369
|
+
// review); restore when that phase ends.
|
|
376
370
|
useEffect(() => {
|
|
377
|
-
|
|
371
|
+
const needsMouseOff = (state.overlay === "context-input" && state.contextInputPhase === "editing") ||
|
|
372
|
+
(state.overlay === "pr-create" && state.prCreatePhase === "review");
|
|
373
|
+
if (!needsMouseOff)
|
|
378
374
|
return;
|
|
379
375
|
process.stdout.write("\x1b[?1002l\x1b[?1006l");
|
|
380
376
|
return () => {
|
|
381
377
|
process.stdout.write("\x1b[?1002h\x1b[?1006h");
|
|
382
378
|
};
|
|
383
|
-
}, [state.overlay]);
|
|
379
|
+
}, [state.overlay, state.contextInputPhase, state.prCreatePhase]);
|
|
384
380
|
// ── Actions ───────────────────────────────────────────────────────
|
|
385
|
-
const launchWorkInTmux = useCallback((di, mode, worktreePath, contextFile) => {
|
|
381
|
+
const launchWorkInTmux = useCallback(async (di, mode, worktreePath, contextFile) => {
|
|
386
382
|
const windowName = di.issue.identifier;
|
|
387
383
|
const sessionId = di.worktree?.sessionId;
|
|
388
384
|
const bin = resolveAgentBinary();
|
|
389
385
|
const resumeCmd = sessionId && bin ? `${bin} --resume ${sessionId}` : null;
|
|
390
386
|
const contextArg = contextFile ? ` --context-file "${contextFile}"` : "";
|
|
391
387
|
const workCmd = mode === "plan" ? `st worktree work --plan${contextArg}` : `st worktree work${contextArg}`;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
388
|
+
const cmd = resumeCmd ?? workCmd;
|
|
389
|
+
const mux = getMultiplexer();
|
|
390
|
+
const selected = await mux.selectWindow(windowName);
|
|
391
|
+
if (selected.ok) {
|
|
392
|
+
const sent = mux.sendCommand(windowName, cmd);
|
|
393
|
+
if (sent.ok) {
|
|
394
|
+
dispatch({
|
|
395
|
+
type: "SET_ACTION_MESSAGE",
|
|
396
|
+
message: resumeCmd
|
|
397
|
+
? `Resumed session in: ${windowName}`
|
|
398
|
+
: `Launched ${mode} in: ${windowName}`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
dispatch({
|
|
403
|
+
type: "SET_ACTION_MESSAGE",
|
|
404
|
+
message: `Focused ${windowName} — run \`${cmd}\` manually (${sent.reason})`,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
403
407
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const cmd = resumeCmd ?? workCmd;
|
|
412
|
-
execSync(`tmux send-keys -t "${windowName}" "${cmd}" Enter`, { stdio: "ignore" });
|
|
408
|
+
else {
|
|
409
|
+
const created = await mux.createWindow({
|
|
410
|
+
name: windowName,
|
|
411
|
+
cwd: worktreePath,
|
|
412
|
+
command: cmd,
|
|
413
|
+
});
|
|
414
|
+
if (created.ok) {
|
|
413
415
|
dispatch({
|
|
414
416
|
type: "SET_ACTION_MESSAGE",
|
|
415
417
|
message: resumeCmd
|
|
416
418
|
? `Resumed session in new window: ${windowName}`
|
|
417
|
-
: `Launched ${mode} in
|
|
419
|
+
: `Launched ${mode} in ${mux.kind} window: ${windowName}`,
|
|
418
420
|
});
|
|
419
421
|
}
|
|
420
|
-
|
|
421
|
-
dispatch({
|
|
422
|
+
else {
|
|
423
|
+
dispatch({
|
|
424
|
+
type: "SET_ACTION_MESSAGE",
|
|
425
|
+
message: `Failed to create ${mux.kind} window${created.message ? `: ${created.message}` : ""}`,
|
|
426
|
+
});
|
|
422
427
|
}
|
|
423
428
|
}
|
|
424
429
|
// Delayed refresh to pick up session ID created by `st worktree work`
|
|
425
430
|
setTimeout(() => refresh(), 3000);
|
|
426
431
|
}, [refresh]);
|
|
427
|
-
const launchAfterCreation = useCallback((mode, worktreePath, ticketId, contextFile) => {
|
|
428
|
-
|
|
432
|
+
const launchAfterCreation = useCallback(async (mode, worktreePath, ticketId, contextFile) => {
|
|
433
|
+
const mux = getMultiplexer();
|
|
434
|
+
if (mux.isActive()) {
|
|
429
435
|
const windowName = ticketId;
|
|
430
436
|
const contextArg = contextFile ? ` --context-file "${contextFile}"` : "";
|
|
431
437
|
const workCmd = mode === "plan"
|
|
432
438
|
? `st worktree work --plan${contextArg}`
|
|
433
439
|
: `st worktree work${contextArg}`;
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
440
|
+
const created = await mux.createWindow({
|
|
441
|
+
name: windowName,
|
|
442
|
+
cwd: worktreePath,
|
|
443
|
+
command: workCmd,
|
|
444
|
+
});
|
|
445
|
+
if (created.ok) {
|
|
438
446
|
dispatch({
|
|
439
447
|
type: "SET_ACTION_MESSAGE",
|
|
440
448
|
message: `Created worktree + launched ${mode} in: ${windowName}`,
|
|
441
449
|
});
|
|
442
450
|
}
|
|
443
|
-
|
|
444
|
-
dispatch({
|
|
451
|
+
else {
|
|
452
|
+
dispatch({
|
|
453
|
+
type: "SET_ACTION_MESSAGE",
|
|
454
|
+
message: `Worktree created, but ${mux.kind} failed${created.message ? `: ${created.message}` : ""}`,
|
|
455
|
+
});
|
|
445
456
|
}
|
|
446
457
|
setTimeout(() => refresh(), 3000);
|
|
447
458
|
}
|
|
@@ -587,8 +598,8 @@ export default function Dashboard() {
|
|
|
587
598
|
const contextFile = writeContextFile(customContext);
|
|
588
599
|
if (di.worktree) {
|
|
589
600
|
// Worktree exists — launch work
|
|
590
|
-
if (
|
|
591
|
-
launchWorkInTmux(di, mode, di.worktree.path, contextFile);
|
|
601
|
+
if (getMultiplexer().isActive()) {
|
|
602
|
+
void launchWorkInTmux(di, mode, di.worktree.path, contextFile);
|
|
592
603
|
}
|
|
593
604
|
else {
|
|
594
605
|
leaveAltScreen();
|
|
@@ -890,6 +901,9 @@ export default function Dashboard() {
|
|
|
890
901
|
}
|
|
891
902
|
// PR create overlay
|
|
892
903
|
if (state.overlay === "pr-create") {
|
|
904
|
+
// Review phase — MultilineTextArea owns the keyboard
|
|
905
|
+
if (state.prCreatePhase === "review")
|
|
906
|
+
return;
|
|
893
907
|
if (key.escape) {
|
|
894
908
|
dispatch({ type: "PR_CREATE_CANCEL" });
|
|
895
909
|
return;
|
|
@@ -904,21 +918,17 @@ export default function Dashboard() {
|
|
|
904
918
|
return;
|
|
905
919
|
}
|
|
906
920
|
}
|
|
907
|
-
if (state.prCreatePhase === "
|
|
921
|
+
if (state.prCreatePhase === "confirm") {
|
|
908
922
|
if (input === "y" || key.return) {
|
|
909
923
|
confirmPrCreate();
|
|
910
924
|
return;
|
|
911
925
|
}
|
|
912
|
-
if (input === "
|
|
913
|
-
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
if (key.shift && key.downArrow) {
|
|
917
|
-
dispatch({ type: "SCROLL_DETAIL", offset: state.detailScrollOffset + 3 });
|
|
926
|
+
if (input === "e") {
|
|
927
|
+
dispatch({ type: "PR_CREATE_EDIT" });
|
|
918
928
|
return;
|
|
919
929
|
}
|
|
920
|
-
if (
|
|
921
|
-
|
|
930
|
+
if (input === "w") {
|
|
931
|
+
openPrInWeb();
|
|
922
932
|
return;
|
|
923
933
|
}
|
|
924
934
|
}
|
|
@@ -1001,9 +1011,29 @@ export default function Dashboard() {
|
|
|
1001
1011
|
}
|
|
1002
1012
|
return;
|
|
1003
1013
|
}
|
|
1004
|
-
// Context-input overlay
|
|
1005
|
-
//
|
|
1014
|
+
// Context-input overlay.
|
|
1015
|
+
// Editing phase: MultilineTextArea owns useInput (outer is disabled
|
|
1016
|
+
// via isActive below).
|
|
1017
|
+
// Review phase: outer handles y/n/e/ESC.
|
|
1006
1018
|
if (state.overlay === "context-input") {
|
|
1019
|
+
if (state.contextInputPhase === "review") {
|
|
1020
|
+
if (input === "y" || key.return) {
|
|
1021
|
+
const mode = state.contextInputMode;
|
|
1022
|
+
const ctx = state.contextInputValue;
|
|
1023
|
+
dispatch({ type: "CONTEXT_INPUT_DONE" });
|
|
1024
|
+
if (mode)
|
|
1025
|
+
doWork(mode, ctx);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
if (input === "n" || input === "e") {
|
|
1029
|
+
dispatch({ type: "CONTEXT_INPUT_EDIT" });
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
if (key.escape) {
|
|
1033
|
+
dispatch({ type: "CONTEXT_INPUT_DONE" });
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1007
1037
|
return;
|
|
1008
1038
|
}
|
|
1009
1039
|
// Confirm delete overlay
|
|
@@ -1203,7 +1233,7 @@ export default function Dashboard() {
|
|
|
1203
1233
|
dispatch({ type: "SET_ACTION_MESSAGE", message: `Opened in ${editor}` });
|
|
1204
1234
|
return;
|
|
1205
1235
|
}
|
|
1206
|
-
// AI Review in
|
|
1236
|
+
// AI Review in multiplexer
|
|
1207
1237
|
if (input === "r") {
|
|
1208
1238
|
if (!ri.worktree) {
|
|
1209
1239
|
dispatch({
|
|
@@ -1212,21 +1242,23 @@ export default function Dashboard() {
|
|
|
1212
1242
|
});
|
|
1213
1243
|
return;
|
|
1214
1244
|
}
|
|
1215
|
-
|
|
1245
|
+
const mux = getMultiplexer();
|
|
1246
|
+
if (mux.isActive()) {
|
|
1216
1247
|
const windowName = `review-${extractTicketId(ri.branch ?? "") ?? ri.pr.number}`;
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1248
|
+
const cwd = ri.worktree.path;
|
|
1249
|
+
void (async () => {
|
|
1250
|
+
const created = await mux.createWindow({
|
|
1251
|
+
name: windowName,
|
|
1252
|
+
cwd,
|
|
1253
|
+
command: "st pr review",
|
|
1220
1254
|
});
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1255
|
+
dispatch({
|
|
1256
|
+
type: "SET_ACTION_MESSAGE",
|
|
1257
|
+
message: created.ok
|
|
1258
|
+
? `Launched AI review in ${mux.kind}`
|
|
1259
|
+
: `Failed to launch review${created.message ? `: ${created.message}` : ""}`,
|
|
1224
1260
|
});
|
|
1225
|
-
|
|
1226
|
-
}
|
|
1227
|
-
catch {
|
|
1228
|
-
dispatch({ type: "SET_ACTION_MESSAGE", message: "Failed to launch review" });
|
|
1229
|
-
}
|
|
1261
|
+
})();
|
|
1230
1262
|
}
|
|
1231
1263
|
else {
|
|
1232
1264
|
leaveAltScreen();
|
|
@@ -1308,30 +1340,30 @@ export default function Dashboard() {
|
|
|
1308
1340
|
dispatch({ type: "SET_ACTION_MESSAGE", message: "No worktree to switch to" });
|
|
1309
1341
|
return;
|
|
1310
1342
|
}
|
|
1311
|
-
|
|
1343
|
+
const mux = getMultiplexer();
|
|
1344
|
+
if (mux.isActive()) {
|
|
1312
1345
|
const windowName = di.issue.identifier;
|
|
1313
1346
|
const sessionId = di.worktree.sessionId;
|
|
1314
1347
|
const bin = resolveAgentBinary();
|
|
1315
1348
|
const resumeCmd = sessionId && bin ? `${bin} --resume ${sessionId}` : null;
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1349
|
+
const worktreePath = di.worktree.path;
|
|
1350
|
+
void (async () => {
|
|
1351
|
+
const selected = await mux.selectWindow(windowName);
|
|
1352
|
+
if (selected.ok)
|
|
1353
|
+
return;
|
|
1354
|
+
const cmd = resumeCmd ?? "st worktree work";
|
|
1355
|
+
const created = await mux.createWindow({
|
|
1356
|
+
name: windowName,
|
|
1357
|
+
cwd: worktreePath,
|
|
1358
|
+
command: cmd,
|
|
1359
|
+
});
|
|
1360
|
+
if (!created.ok) {
|
|
1361
|
+
dispatch({
|
|
1362
|
+
type: "SET_ACTION_MESSAGE",
|
|
1363
|
+
message: `Failed to switch ${mux.kind} window${created.message ? `: ${created.message}` : ""}`,
|
|
1329
1364
|
});
|
|
1330
1365
|
}
|
|
1331
|
-
|
|
1332
|
-
dispatch({ type: "SET_ACTION_MESSAGE", message: "Failed to switch tmux window" });
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1366
|
+
})();
|
|
1335
1367
|
}
|
|
1336
1368
|
else {
|
|
1337
1369
|
leaveAltScreen();
|
|
@@ -1386,18 +1418,23 @@ export default function Dashboard() {
|
|
|
1386
1418
|
dispatch({ type: "SET_ACTION_MESSAGE", message: "No PR to review" });
|
|
1387
1419
|
return;
|
|
1388
1420
|
}
|
|
1389
|
-
|
|
1421
|
+
const mux = getMultiplexer();
|
|
1422
|
+
if (mux.isActive()) {
|
|
1390
1423
|
const windowName = `review-${di.issue.identifier}`;
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1424
|
+
const cwd = di.worktree.path;
|
|
1425
|
+
void (async () => {
|
|
1426
|
+
const created = await mux.createWindow({
|
|
1427
|
+
name: windowName,
|
|
1428
|
+
cwd,
|
|
1429
|
+
command: "st pr review",
|
|
1394
1430
|
});
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1431
|
+
dispatch({
|
|
1432
|
+
type: "SET_ACTION_MESSAGE",
|
|
1433
|
+
message: created.ok
|
|
1434
|
+
? `Launched review in ${mux.kind}`
|
|
1435
|
+
: `Failed to launch review${created.message ? `: ${created.message}` : ""}`,
|
|
1436
|
+
});
|
|
1437
|
+
})();
|
|
1401
1438
|
}
|
|
1402
1439
|
else {
|
|
1403
1440
|
leaveAltScreen();
|
|
@@ -1445,18 +1482,23 @@ export default function Dashboard() {
|
|
|
1445
1482
|
dispatch({ type: "SET_ACTION_MESSAGE", message: "No PR to fix" });
|
|
1446
1483
|
return;
|
|
1447
1484
|
}
|
|
1448
|
-
|
|
1485
|
+
const mux = getMultiplexer();
|
|
1486
|
+
if (mux.isActive()) {
|
|
1449
1487
|
const windowName = `fix-${di.issue.identifier}`;
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1488
|
+
const cwd = di.worktree.path;
|
|
1489
|
+
void (async () => {
|
|
1490
|
+
const created = await mux.createWindow({
|
|
1491
|
+
name: windowName,
|
|
1492
|
+
cwd,
|
|
1493
|
+
command: "st pr fix",
|
|
1453
1494
|
});
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1495
|
+
dispatch({
|
|
1496
|
+
type: "SET_ACTION_MESSAGE",
|
|
1497
|
+
message: created.ok
|
|
1498
|
+
? `Launched PR fix in ${mux.kind}`
|
|
1499
|
+
: `Failed to launch PR fix${created.message ? `: ${created.message}` : ""}`,
|
|
1500
|
+
});
|
|
1501
|
+
})();
|
|
1460
1502
|
}
|
|
1461
1503
|
else {
|
|
1462
1504
|
leaveAltScreen();
|
|
@@ -1475,7 +1517,8 @@ export default function Dashboard() {
|
|
|
1475
1517
|
return;
|
|
1476
1518
|
}
|
|
1477
1519
|
}, {
|
|
1478
|
-
isActive: state.overlay !== "context-input" &&
|
|
1520
|
+
isActive: (state.overlay !== "context-input" || state.contextInputPhase === "review") &&
|
|
1521
|
+
(state.overlay !== "pr-create" || state.prCreatePhase !== "review") &&
|
|
1479
1522
|
(state.overlay !== "commit" || state.commitPhase !== "awaiting-message"),
|
|
1480
1523
|
});
|
|
1481
1524
|
// ── Render ─────────────────────────────────────────────────────────
|
|
@@ -1487,13 +1530,10 @@ export default function Dashboard() {
|
|
|
1487
1530
|
}
|
|
1488
1531
|
const selectedIssue = state.flatIssues[state.selectedIndex] ?? null;
|
|
1489
1532
|
const selectedReview = state.flatReviews[state.reviewSelectedIndex] ?? null;
|
|
1490
|
-
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Dashboard" }), _jsxs(Text, { dimColor: true, children: [" ", "v", version] }), CLAUDE_VERSION ? (_jsxs(Text, { dimColor: true, children: [" ", "claude ", CLAUDE_VERSION] })) : null, _jsx(Text, { dimColor: true, children: state.refreshing ? " refreshing..." : "" }), state.actionMessage && (_jsxs(Text, { color: "yellow", children: [" ", state.actionMessage] }))] }), _jsxs(Box, { children: [_jsxs(Text, { bold: state.activeTab === "issues", color: state.activeTab === "issues" ? "cyan" : undefined, dimColor: state.activeTab !== "issues", children: [state.activeTab === "issues" ? "\u25b8 " : " ", "1 Issues (", state.flatIssues.length, ")"] }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: state.activeTab === "reviews", color: state.activeTab === "reviews" ? "cyan" : undefined, dimColor: state.activeTab !== "reviews", children: [state.activeTab === "reviews" ? "\u25b8 " : " ", "2 Reviews (", state.flatReviews.length, ")"] })] }), 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, 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: () => {
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
if (mode)
|
|
1495
|
-
doWork(mode, ctx);
|
|
1496
|
-
}, 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: "Enter" }), " ", "newline", " ", _jsx(Text, { color: "cyan", bold: true, children: "ESC" }), " ", "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) => {
|
|
1533
|
+
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Dashboard" }), _jsxs(Text, { dimColor: true, children: [" ", "v", version] }), CLAUDE_VERSION ? (_jsxs(Text, { dimColor: true, children: [" ", "claude ", CLAUDE_VERSION] })) : null, _jsx(Text, { dimColor: true, children: state.refreshing ? " refreshing..." : "" }), state.actionMessage && (_jsxs(Text, { color: "yellow", children: [" ", state.actionMessage] }))] }), _jsxs(Box, { children: [_jsxs(Text, { bold: state.activeTab === "issues", color: state.activeTab === "issues" ? "cyan" : undefined, dimColor: state.activeTab !== "issues", children: [state.activeTab === "issues" ? "\u25b8 " : " ", "1 Issues (", state.flatIssues.length, ")"] }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: state.activeTab === "reviews", color: state.activeTab === "reviews" ? "cyan" : undefined, dimColor: state.activeTab !== "reviews", children: [state.activeTab === "reviews" ? "\u25b8 " : " ", "2 Reviews (", state.flatReviews.length, ")"] })] }), 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: " " }), state.contextInputPhase === "editing" ? (_jsxs(_Fragment, { children: [_jsx(MultilineTextArea, { value: state.contextInputValue, onChange: (v) => dispatch({ type: "CONTEXT_INPUT_CHANGE", value: v }), onSubmit: () => dispatch({ type: "CONTEXT_INPUT_REVIEW" }), 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" }), " send · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+O" }), " editor · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+C" }), " cancel"] })] })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, minHeight: 6, children: [(state.contextInputValue || "(no extra context)")
|
|
1534
|
+
.split("\n")
|
|
1535
|
+
.slice(0, 12)
|
|
1536
|
+
.map((line, i) => (_jsx(Text, { children: line || " " }, i))), state.contextInputValue.split("\n").length > 12 && (_jsxs(Text, { dimColor: true, children: ["\u2026+", state.contextInputValue.split("\n").length - 12, " more lines"] }))] }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Anything else to add?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " / ", _jsx(Text, { color: "green", bold: true, children: "Enter" }), " launch ", _jsx(Text, { color: "yellow", bold: true, children: "n" }), " / ", _jsx(Text, { color: "yellow", bold: true, children: "e" }), " keep editing ", _jsx(Text, { color: "red", bold: true, children: "ESC" }), " 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) => {
|
|
1497
1537
|
const selected = idx === state.baseSelectIndex;
|
|
1498
1538
|
const defaultBranch = getDefaultBranch();
|
|
1499
1539
|
const label = branch === defaultBranch ? `${branch} (default)` : branch;
|
|
@@ -1501,5 +1541,5 @@ export default function Dashboard() {
|
|
|
1501
1541
|
}), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to select, ESC to cancel" })] }) })) : state.overlay === "confirm-delete" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Remove worktree?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: selectedIssue?.worktree?.branch ?? "" }), selectedIssue?.worktree?.dirty && (_jsx(Text, { color: "yellow", children: "Warning: worktree has uncommitted changes" })), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " Confirm"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "n" }), " Cancel"] })] }) })) : state.overlay === "confirm-setup" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Run setup script?" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: ".santree/init.sh" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " Run setup"] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", bold: true, children: "n" }), " Skip"] })] }) })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: leftWidth, children: state.activeTab === "reviews" ? (_jsx(ReviewList, { flatReviews: state.flatReviews, selectedIndex: state.reviewSelectedIndex, scrollOffset: state.reviewListScrollOffset, height: contentHeight, width: leftWidth })) : state.flatIssues.length === 0 ? (_jsx(Box, { width: leftWidth, height: contentHeight, justifyContent: "center", alignItems: "center", children: _jsx(Text, { color: "yellow", children: "No active issues" }) })) : (_jsx(IssueList, { groups: state.groups, flatIssues: state.flatIssues, selectedIndex: state.selectedIndex, scrollOffset: state.listScrollOffset, height: contentHeight, width: leftWidth, creatingForTicket: state.creatingForTicket, deletingForTicket: state.deletingForTicket })) }), _jsx(Box, { flexDirection: "column", width: 3, children: Array.from({ length: contentHeight }).map((_, i) => (_jsx(Text, { dimColor: true, children: " │ " }, i))) }), _jsx(Box, { width: rightWidth, children: state.activeTab === "reviews" && state.creatingForTicket ? (_jsxs(Box, { flexDirection: "column", width: rightWidth, height: contentHeight, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Setting up worktree for ", state.creatingForTicket, "..."] }), state.creationLogs
|
|
1502
1542
|
.split("\n")
|
|
1503
1543
|
.slice(-(contentHeight - 1))
|
|
1504
|
-
.map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl, body: state.prCreateBody, title: state.prCreateTitle,
|
|
1544
|
+
.map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl, body: state.prCreateBody, title: state.prCreateTitle, dispatch: dispatch })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] }))] }));
|
|
1505
1545
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -11,6 +11,7 @@ const require = createRequire(import.meta.url);
|
|
|
11
11
|
const { version } = require("../../package.json");
|
|
12
12
|
import { findMainRepoRoot, getSantreeDir, getInitScriptPath } from "../lib/git.js";
|
|
13
13
|
import { getAuthStatus, getValidTokens } from "../lib/linear.js";
|
|
14
|
+
import { getMultiplexer } from "../lib/multiplexer/index.js";
|
|
14
15
|
const execAsync = promisify(exec);
|
|
15
16
|
export const description = "Check system requirements and integrations";
|
|
16
17
|
/**
|
|
@@ -55,6 +56,70 @@ async function checkTool(name, description, required, versionCommand, hint) {
|
|
|
55
56
|
path,
|
|
56
57
|
};
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Reports the active multiplexer (tmux/cmux/none) and verifies the underlying
|
|
61
|
+
* binary is reachable. Surfaces a hint when the configured multiplexer can't run.
|
|
62
|
+
*/
|
|
63
|
+
async function checkMultiplexer() {
|
|
64
|
+
const mux = getMultiplexer();
|
|
65
|
+
const explicit = process.env["SANTREE_MULTIPLEXER"]?.toLowerCase();
|
|
66
|
+
const description = `Multiplexer (active: ${mux.kind}${explicit ? `, SANTREE_MULTIPLEXER=${explicit}` : ""})`;
|
|
67
|
+
if (mux.kind === "none") {
|
|
68
|
+
return {
|
|
69
|
+
name: "multiplexer",
|
|
70
|
+
description,
|
|
71
|
+
required: false,
|
|
72
|
+
installed: false,
|
|
73
|
+
hint: "No multiplexer active. Set SANTREE_MULTIPLEXER=tmux (or cmux) and run inside one. Install: brew install tmux",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (mux.kind === "tmux") {
|
|
77
|
+
const path = await getPath("tmux");
|
|
78
|
+
if (!path) {
|
|
79
|
+
return {
|
|
80
|
+
name: "tmux",
|
|
81
|
+
description,
|
|
82
|
+
required: false,
|
|
83
|
+
installed: false,
|
|
84
|
+
hint: "Install: brew install tmux",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const version = await tryExec("tmux -V");
|
|
88
|
+
return {
|
|
89
|
+
name: "tmux",
|
|
90
|
+
description,
|
|
91
|
+
required: false,
|
|
92
|
+
installed: true,
|
|
93
|
+
version: version || "unknown",
|
|
94
|
+
path,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// cmux
|
|
98
|
+
const path = await getPath("cmux");
|
|
99
|
+
if (!path) {
|
|
100
|
+
return {
|
|
101
|
+
name: "cmux",
|
|
102
|
+
description,
|
|
103
|
+
required: false,
|
|
104
|
+
installed: false,
|
|
105
|
+
hint: "Install cmux.app from https://cmux.com or set SANTREE_MULTIPLEXER=tmux. cmux is macOS-only.",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const version = await tryExec("cmux --version 2>/dev/null");
|
|
109
|
+
const ping = await tryExec("cmux ping 2>/dev/null");
|
|
110
|
+
const hint = !ping
|
|
111
|
+
? "cmux app not reachable — open cmux.app or set SANTREE_MULTIPLEXER=tmux. NOTE: cmux #1472 — programmatic workspaces may have dead PTYs (https://github.com/manaflow-ai/cmux/issues/1472)."
|
|
112
|
+
: "NOTE: cmux #1472 — programmatic workspaces may have dead PTYs (https://github.com/manaflow-ai/cmux/issues/1472).";
|
|
113
|
+
return {
|
|
114
|
+
name: "cmux",
|
|
115
|
+
description,
|
|
116
|
+
required: false,
|
|
117
|
+
installed: !!ping,
|
|
118
|
+
version: version || "unknown",
|
|
119
|
+
path,
|
|
120
|
+
hint,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
58
123
|
/**
|
|
59
124
|
* Checks GitHub CLI auth status.
|
|
60
125
|
* Uses `gh api user` which works across all gh versions.
|
|
@@ -399,7 +464,7 @@ export default function Doctor() {
|
|
|
399
464
|
const results = await Promise.all([
|
|
400
465
|
checkTool("git", "Version control", true, "git --version | head -1", "Install: brew install git"),
|
|
401
466
|
checkGhAuth(),
|
|
402
|
-
|
|
467
|
+
checkMultiplexer(),
|
|
403
468
|
checkTool("claude", "Claude Code CLI", true, "claude --version 2>/dev/null | head -1", "Install: npm install -g @anthropic-ai/claude-code"),
|
|
404
469
|
]);
|
|
405
470
|
// Check for either code or cursor (only need one)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
export declare const description = "Open $EDITOR on a temp file, then print the path on stdout (compose with --context-file).";
|
|
3
|
+
export declare const options: z.ZodObject<{
|
|
4
|
+
initial: z.ZodOptional<z.ZodString>;
|
|
5
|
+
from: z.ZodOptional<z.ZodString>;
|
|
6
|
+
ext: z.ZodDefault<z.ZodString>;
|
|
7
|
+
editor: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
type Props = {
|
|
10
|
+
options: z.infer<typeof options>;
|
|
11
|
+
};
|
|
12
|
+
export default function TextEditor({ options: opts }: Props): null;
|
|
13
|
+
export {};
|