santree 0.4.0 → 0.5.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.
@@ -12,6 +12,8 @@ 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
14
  import { getMultiplexer } from "../lib/multiplexer/index.js";
15
+ import { resolveClaudeBinary } from "../lib/ai.js";
16
+ import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, SANTREE_PACKAGE, getLatestVersionFor, isUpdateAvailable, detectPackageManager, getInstallCommandFor, } from "../lib/version.js";
15
17
  const execAsync = promisify(exec);
16
18
  export const description = "Check system requirements and integrations";
17
19
  /**
@@ -107,9 +109,12 @@ async function checkMultiplexer() {
107
109
  }
108
110
  const version = await tryExec("cmux --version 2>/dev/null");
109
111
  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).";
112
+ // Note: cmux #1472 (programmatic workspaces with dead PTYs) is a real
113
+ // limitation but only surfaces when a specific dashboard flow tries to
114
+ // auto-execute a command in a freshly-created workspace. Showing it on
115
+ // every doctor run made cmux look broken when it isn't — the limitation
116
+ // is documented in CLAUDE.md and the README. We only flag a hint here
117
+ // when cmux is actually unreachable.
113
118
  return {
114
119
  name: "cmux",
115
120
  description,
@@ -117,7 +122,40 @@ async function checkMultiplexer() {
117
122
  installed: !!ping,
118
123
  version: version || "unknown",
119
124
  path,
120
- hint,
125
+ hint: !ping
126
+ ? "cmux app not reachable — open cmux.app or set SANTREE_MULTIPLEXER=tmux."
127
+ : undefined,
128
+ };
129
+ }
130
+ /**
131
+ * Checks the Claude CLI, preferring cmux's bundled binary when running inside
132
+ * cmux. The standard `checkTool` uses `which claude` which can't locate the
133
+ * cmux shim at /Applications/cmux.app/Contents/Resources/bin/claude — that
134
+ * binary isn't on PATH. See manaflow-ai/cmux#2048.
135
+ */
136
+ async function checkClaude() {
137
+ const resolved = resolveClaudeBinary();
138
+ const inCmux = getMultiplexer().kind === "cmux";
139
+ const description = inCmux ? "Claude Code CLI (cmux-bundled)" : "Claude Code CLI";
140
+ if (!resolved) {
141
+ return {
142
+ name: "claude",
143
+ description,
144
+ required: true,
145
+ installed: false,
146
+ hint: inCmux
147
+ ? "Open cmux.app to install its bundled Claude, or install standalone: npm install -g @anthropic-ai/claude-code"
148
+ : "Install: npm install -g @anthropic-ai/claude-code",
149
+ };
150
+ }
151
+ const version = await tryExec(`"${resolved}" --version 2>/dev/null | head -1`);
152
+ return {
153
+ name: "claude",
154
+ description,
155
+ required: true,
156
+ installed: true,
157
+ version: version || "unknown",
158
+ path: resolved,
121
159
  };
122
160
  }
123
161
  /**
@@ -411,7 +449,7 @@ function StatusIcon({ ok, required }) {
411
449
  return required ? _jsx(Text, { color: "red", children: "\u2717" }) : _jsx(Text, { color: "yellow", children: "\u25CB" });
412
450
  }
413
451
  function ToolRow({ tool }) {
414
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: tool.installed && !tool.hint, required: tool.required }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: tool.name }), _jsxs(Text, { dimColor: true, children: [" - ", tool.description] }), !tool.required && _jsx(Text, { dimColor: true, children: " (optional)" })] }), tool.installed ? (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Version: ", tool.version] }), _jsxs(Text, { dimColor: true, children: ["Path: ", tool.path] }), tool.authStatus && _jsxs(Text, { dimColor: true, children: ["Auth: ", tool.authStatus] }), tool.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] })] })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] }) }))] }));
452
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: tool.installed && !tool.hint, required: tool.required }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: tool.name }), _jsxs(Text, { dimColor: true, children: [" - ", tool.description] }), !tool.required && _jsx(Text, { dimColor: true, children: " (optional)" })] }), tool.installed ? (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Version: ", tool.version] }), tool.latestVersion && tool.version && (_jsxs(Text, { color: isUpdateAvailable(tool.version, tool.latestVersion) ? "yellow" : undefined, dimColor: !isUpdateAvailable(tool.version, tool.latestVersion), children: ["Latest: ", tool.latestVersion, isUpdateAvailable(tool.version, tool.latestVersion) ? " ⬆ update available" : ""] })), tool.path && _jsxs(Text, { dimColor: true, children: ["Path: ", tool.path] }), tool.authStatus && _jsxs(Text, { dimColor: true, children: ["Auth: ", tool.authStatus] }), tool.updateHint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.updateHint] }), tool.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] })] })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] }) }))] }));
415
453
  }
416
454
  function LinearRow({ linear }) {
417
455
  const isOk = linear.authenticated && linear.tokenValid && linear.repoLinked;
@@ -461,30 +499,78 @@ export default function Doctor() {
461
499
  const [loading, setLoading] = useState(true);
462
500
  useEffect(() => {
463
501
  async function runChecks() {
464
- const results = await Promise.all([
465
- checkTool("git", "Version control", true, "git --version | head -1", "Install: brew install git"),
466
- checkGhAuth(),
467
- checkMultiplexer(),
468
- checkTool("claude", "Claude Code CLI", true, "claude --version 2>/dev/null | head -1", "Install: npm install -g @anthropic-ai/claude-code"),
502
+ const pm = detectPackageManager();
503
+ const [results, latestSantree, latestClaude] = await Promise.all([
504
+ Promise.all([
505
+ checkTool("git", "Version control", true, "git --version | head -1", "Install: brew install git"),
506
+ checkGhAuth(),
507
+ checkMultiplexer(),
508
+ checkClaude(),
509
+ ]),
510
+ getLatestVersionFor(SANTREE_PACKAGE),
511
+ getLatestVersionFor(CLAUDE_CODE_PACKAGE),
469
512
  ]);
470
- // Check for either code or cursor (only need one)
513
+ // Synthetic row for santree itself surfaces update status.
514
+ const santreeRow = {
515
+ name: "santree",
516
+ description: "Santree CLI (this app)",
517
+ required: true,
518
+ installed: true,
519
+ version: CURRENT_VERSION,
520
+ latestVersion: latestSantree ?? undefined,
521
+ updateHint: latestSantree && isUpdateAvailable(CURRENT_VERSION, latestSantree)
522
+ ? "Run: santree update"
523
+ : undefined,
524
+ };
525
+ results.unshift(santreeRow);
526
+ // Augment the claude row with latest-version info from npm registry.
527
+ // When the resolved binary is the cmux-bundled one, npm install can't
528
+ // update it — the bundled binary is shipped inside cmux.app. Show a
529
+ // cmux-aware hint instead of the generic npm command.
530
+ const claudeRow = results.find((r) => r.name === "claude");
531
+ if (claudeRow && claudeRow.installed && latestClaude) {
532
+ claudeRow.latestVersion = latestClaude;
533
+ if (claudeRow.version && isUpdateAvailable(claudeRow.version, latestClaude)) {
534
+ const isCmuxBundled = !!claudeRow.path?.includes("/cmux.app/");
535
+ if (isCmuxBundled) {
536
+ claudeRow.updateHint = "Bundled with cmux — update cmux.app to get the latest Claude.";
537
+ }
538
+ else {
539
+ const cmd = getInstallCommandFor(pm, `${CLAUDE_CODE_PACKAGE}@latest`);
540
+ claudeRow.updateHint = `Run: ${cmd.display}`;
541
+ }
542
+ }
543
+ }
544
+ // Optional: a syntax-highlighted diff pager — used by `st worktree diff`
545
+ // and the dashboard `v` overlay when SANTREE_DIFF_TOOL is set. Any
546
+ // pager works (delta, diff-so-fancy, …); without one set, git's
547
+ // default pager runs. Delta is the most popular choice so we check
548
+ // for it as a convenience, but it is never a hard dependency.
549
+ const deltaCheck = await checkTool("delta", "Recommended diff pager — any pager works", false, "delta --version | head -1", "Optional — git's default pager works too. Set SANTREE_DIFF_TOOL or `git config core.pager <tool>`. To install delta: brew install git-delta");
550
+ results.push(deltaCheck);
551
+ // Optional: a `.code-workspace`-aware editor (VSCode or Cursor).
552
+ // Santree itself works with any editor via $SANTREE_EDITOR — this
553
+ // check exists only because the dashboard's `E workspace` shortcut
554
+ // needs an editor that understands `.code-workspace` files. Missing
555
+ // here just means the shortcut is hidden; everything else still works.
556
+ const workspaceEditorDesc = "Workspace editor (`E workspace` shortcut)";
471
557
  const [codeCheck, cursorCheck] = await Promise.all([
472
- checkTool("code", "VSCode editor", false, "code --version | head -1", ""),
473
- checkTool("cursor", "Cursor editor", false, "cursor --version | head -1", ""),
558
+ checkTool("code", workspaceEditorDesc, false, "code --version | head -1", ""),
559
+ checkTool("cursor", workspaceEditorDesc, false, "cursor --version | head -1", ""),
474
560
  ]);
475
561
  if (codeCheck.installed) {
476
- results.push({ ...codeCheck, description: "Editor (VSCode)" });
562
+ results.push(codeCheck);
477
563
  }
478
564
  else if (cursorCheck.installed) {
479
- results.push({ ...cursorCheck, description: "Editor (Cursor)" });
565
+ results.push(cursorCheck);
480
566
  }
481
567
  else {
482
568
  results.push({
483
569
  name: "code/cursor",
484
- description: "Editor (VSCode or Cursor)",
570
+ description: workspaceEditorDesc,
485
571
  required: false,
486
572
  installed: false,
487
- hint: "Install VSCode (https://code.visualstudio.com) or Cursor (https://cursor.sh)",
573
+ hint: "Optional santree works with any $SANTREE_EDITOR. Only needed for the dashboard's `.code-workspace` shortcut.",
488
574
  });
489
575
  }
490
576
  const linearResult = await checkLinearAuth();
@@ -132,9 +132,17 @@ function getGitChanges(cwd) {
132
132
  untracked: countLines(git(cwd, "ls-files --others --exclude-standard")),
133
133
  };
134
134
  }
135
- // Build a progress bar for context usage
135
+ // Build a progress bar for context usage.
136
+ //
137
+ // We deliberately inflate the displayed percentage by 20% (clamped to 100) so
138
+ // the bar fills up faster than the model's actual context window. The point is
139
+ // to nudge toward more-frequent /compact: the color thresholds (60%/80%) trip
140
+ // earlier, so the yellow/red warnings show up while there's still real headroom
141
+ // left to compact gracefully instead of after the model has already started
142
+ // dropping content.
143
+ const CONTEXT_DISPLAY_MULTIPLIER = 1.2;
136
144
  function formatContextUsage(usedPercentage) {
137
- const used = Math.round(usedPercentage);
145
+ const used = Math.min(100, Math.round(usedPercentage * CONTEXT_DISPLAY_MULTIPLIER));
138
146
  const color = used >= 80 ? c.red : used >= 60 ? c.yellow : c.green;
139
147
  const width = 20;
140
148
  const filled = Math.round((used * width) / 100);
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ export declare const description = "Update santree to the latest version on npm";
3
+ export declare const options: z.ZodObject<{
4
+ force: z.ZodOptional<z.ZodBoolean>;
5
+ pm: z.ZodOptional<z.ZodEnum<{
6
+ npm: "npm";
7
+ pnpm: "pnpm";
8
+ yarn: "yarn";
9
+ }>>;
10
+ }, z.core.$strip>;
11
+ type Props = {
12
+ options: z.infer<typeof options>;
13
+ };
14
+ export default function Update({ options }: Props): import("react/jsx-runtime").JSX.Element;
15
+ export {};
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Text, Box, useApp } from "ink";
4
+ import Spinner from "ink-spinner";
5
+ import { z } from "zod";
6
+ import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, getLatestVersion, getLatestVersionFor, getInstalledClaudeVersion, isUpdateAvailable, detectPackageManager, getInstallCommand, getInstallCommandFor, } from "../lib/version.js";
7
+ import { spawnAsync } from "../lib/exec.js";
8
+ export const description = "Update santree to the latest version on npm";
9
+ export const options = z.object({
10
+ force: z.boolean().optional().describe("Reinstall even if already on the latest version"),
11
+ pm: z
12
+ .enum(["npm", "pnpm", "yarn"])
13
+ .optional()
14
+ .describe("Override package manager auto-detection"),
15
+ });
16
+ const TAIL_LINES = 8;
17
+ function tail(text, n) {
18
+ return text
19
+ .split("\n")
20
+ .filter((l) => l.length > 0)
21
+ .slice(-n);
22
+ }
23
+ export default function Update({ options }) {
24
+ const { exit } = useApp();
25
+ const [status, setStatus] = useState("checking");
26
+ const [latest, setLatest] = useState(null);
27
+ const [pm, setPm] = useState("npm");
28
+ const [installCmd, setInstallCmd] = useState("");
29
+ const [output, setOutput] = useState("");
30
+ const [error, setError] = useState(null);
31
+ const [claude, setClaude] = useState(null);
32
+ useEffect(() => {
33
+ (async () => {
34
+ await new Promise((r) => setTimeout(r, 80));
35
+ // Check santree + claude versions in parallel.
36
+ const [latestVersion, latestClaude] = await Promise.all([
37
+ getLatestVersion({ force: true }),
38
+ getLatestVersionFor(CLAUDE_CODE_PACKAGE, { force: true }),
39
+ ]);
40
+ setLatest(latestVersion);
41
+ setClaude({ installed: getInstalledClaudeVersion(), latest: latestClaude });
42
+ const detectedPm = options.pm ?? detectPackageManager();
43
+ setPm(detectedPm);
44
+ const cmd = getInstallCommand(detectedPm);
45
+ setInstallCmd(cmd.display);
46
+ if (!latestVersion) {
47
+ setStatus("error");
48
+ setError("Could not reach the npm registry. Check your connection.");
49
+ setTimeout(() => exit(), 100);
50
+ return;
51
+ }
52
+ if (!options.force && !isUpdateAvailable(CURRENT_VERSION, latestVersion)) {
53
+ setStatus("up-to-date");
54
+ setTimeout(() => exit(), 100);
55
+ return;
56
+ }
57
+ setStatus("installing");
58
+ const result = await spawnAsync(cmd.cmd, cmd.args, {
59
+ onOutput: (data) => setOutput(data),
60
+ });
61
+ if (result.code === 0) {
62
+ setStatus("done");
63
+ }
64
+ else {
65
+ setStatus("error");
66
+ setError(`${cmd.display} exited with code ${result.code}`);
67
+ }
68
+ setTimeout(() => exit(), 100);
69
+ })();
70
+ }, []);
71
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Santree Update" }) }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "current:" }), _jsxs(Text, { children: ["v", CURRENT_VERSION] })] }), latest && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "latest: " }), _jsxs(Text, { color: isUpdateAvailable(CURRENT_VERSION, latest) ? "yellow" : "green", children: ["v", latest] })] })), installCmd && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "via: " }), _jsxs(Text, { children: [installCmd, " ", _jsxs(Text, { dimColor: true, children: ["(detected: ", pm, ")"] })] })] }))] }), _jsxs(Box, { marginTop: 1, children: [status === "checking" && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: "Checking npm registry..." })] })), status === "installing" && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: ["Running ", installCmd, "..."] })] })), status === "up-to-date" && (_jsx(Text, { color: "green", bold: true, children: "\u2713 Already on the latest version" })), status === "done" && latest && (_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Updated to v", latest] })), status === "error" && error && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] }), (status === "installing" || status === "error") && output && (_jsx(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: tail(output, TAIL_LINES).map((line, i) => (_jsx(Text, { dimColor: true, children: line }, i))) })), claude && claude.installed && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: claude.latest && isUpdateAvailable(claude.installed, claude.latest) ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "yellow", children: ["\u2B06 Claude Code ", claude.installed, " \u2192 ", claude.latest, " available"] }), _jsxs(Text, { dimColor: true, children: ["Run: ", getInstallCommandFor(pm, `${CLAUDE_CODE_PACKAGE}@latest`).display] })] })) : claude.latest ? (_jsxs(Text, { dimColor: true, children: ["\u2713 Claude Code ", claude.installed, " is up to date"] })) : null }))] }));
72
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ export declare const description = "View worktree diff against its base branch (uses delta if installed)";
3
+ export declare const options: z.ZodObject<{
4
+ staged: z.ZodOptional<z.ZodBoolean>;
5
+ unstaged: z.ZodOptional<z.ZodBoolean>;
6
+ commits: z.ZodOptional<z.ZodBoolean>;
7
+ base: z.ZodOptional<z.ZodString>;
8
+ }, z.core.$strip>;
9
+ type Props = {
10
+ options: z.infer<typeof options>;
11
+ };
12
+ export default function Diff({ options: opts }: Props): import("react/jsx-runtime").JSX.Element | null;
13
+ export {};
@@ -0,0 +1,76 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Text, Box, useApp } from "ink";
4
+ import { z } from "zod";
5
+ import { spawn } from "child_process";
6
+ import { findRepoRoot, getCurrentBranch, getBaseBranch, getDiffTool } from "../../lib/git.js";
7
+ import { run } from "../../lib/exec.js";
8
+ export const description = "View worktree diff against its base branch (uses delta if installed)";
9
+ export const options = z.object({
10
+ staged: z.boolean().optional().describe("Show only staged changes"),
11
+ unstaged: z.boolean().optional().describe("Show only unstaged changes (working tree vs index)"),
12
+ commits: z.boolean().optional().describe("Show only committed changes (base...HEAD)"),
13
+ base: z.string().optional().describe("Override base branch"),
14
+ });
15
+ export default function Diff({ options: opts }) {
16
+ const [status, setStatus] = useState({ state: "running" });
17
+ const { exit } = useApp();
18
+ useEffect(() => {
19
+ const repoRoot = findRepoRoot();
20
+ if (!repoRoot) {
21
+ setStatus({ state: "error", message: "Not inside a git repository" });
22
+ return;
23
+ }
24
+ const branch = getCurrentBranch();
25
+ if (!branch) {
26
+ setStatus({ state: "error", message: "Could not determine current branch" });
27
+ return;
28
+ }
29
+ const baseBranch = opts.base ?? getBaseBranch(branch);
30
+ // Use merge-base (not base tip) so upstream-only changes are excluded —
31
+ // matches GitHub PR diff semantics. Falls back to baseBranch if merge-base
32
+ // can't be resolved (e.g. unrelated histories).
33
+ const mergeBase = run(`git -C "${repoRoot}" merge-base "${baseBranch}" HEAD`) ?? baseBranch;
34
+ // Resolve diff range based on flags. Defaults to merge-base..working-tree
35
+ // (everything on this branch including uncommitted work, branch-only).
36
+ // Honor SANTREE_DIFF_TOOL by overriding core.pager just for this invocation
37
+ // — `-c` config takes precedence over the user's global git config.
38
+ const tool = getDiffTool();
39
+ const args = ["-C", repoRoot];
40
+ if (tool) {
41
+ args.push("-c", `core.pager=${tool}`);
42
+ }
43
+ args.push("diff");
44
+ if (opts.staged) {
45
+ args.push("--staged");
46
+ }
47
+ else if (opts.unstaged) {
48
+ // working tree vs index — no extra arg
49
+ }
50
+ else if (opts.commits) {
51
+ args.push(`${mergeBase}..HEAD`);
52
+ }
53
+ else {
54
+ args.push(mergeBase);
55
+ }
56
+ const child = spawn("git", args, { stdio: "inherit" });
57
+ child.on("error", (err) => {
58
+ setStatus({ state: "error", message: err.message });
59
+ exit();
60
+ });
61
+ child.on("close", (code) => {
62
+ setStatus({ state: "done", exitCode: code ?? 0 });
63
+ exit();
64
+ });
65
+ return () => {
66
+ if (!child.killed)
67
+ child.kill();
68
+ };
69
+ }, []);
70
+ if (status.state === "error") {
71
+ return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["\u2717 ", status.message] }) }));
72
+ }
73
+ // While running: render nothing so git/delta own the terminal.
74
+ // On done: render nothing — git's output already filled the screen.
75
+ return null;
76
+ }
package/dist/lib/ai.d.ts CHANGED
@@ -37,8 +37,18 @@ export declare function fetchAndRenderPR(branch: string): Promise<string | null>
37
37
  */
38
38
  export declare function fetchAndRenderDiff(branch: string): Promise<string>;
39
39
  /**
40
- * Check if claude CLI is available on PATH.
41
- * Returns "claude" or null if not installed.
40
+ * Resolve the path to the Claude CLI binary, preferring cmux's bundled copy
41
+ * when running inside cmux. Falls back to PATH lookup, then to Anthropic's
42
+ * standard installer location (`~/.claude/local/claude`). Returns null if
43
+ * none of those resolve.
44
+ *
45
+ * Used by every santree code path that needs to invoke or report the Claude
46
+ * binary — version display, doctor checks, and interactive launches.
47
+ */
48
+ export declare function resolveClaudeBinary(): string | null;
49
+ /**
50
+ * @deprecated Use `resolveClaudeBinary()` directly. Kept as an alias because
51
+ * existing call sites pass the return value straight to spawn args.
42
52
  */
43
53
  export declare function resolveAgentBinary(): string | null;
44
54
  /**
package/dist/lib/ai.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { execSync, spawn, spawnSync } from "child_process";
2
- import { writeFileSync } from "fs";
2
+ import { existsSync, writeFileSync } from "fs";
3
3
  import { join } from "path";
4
- import { tmpdir } from "os";
4
+ import { homedir, tmpdir } from "os";
5
+ import { getMultiplexer } from "./multiplexer/index.js";
5
6
  import { getCurrentBranch, extractTicketId, findRepoRoot, findMainRepoRoot, getBaseBranch, } from "./git.js";
6
7
  import { renderPrompt, renderTicket, renderDiff, renderPR } from "./prompts.js";
7
8
  import { getTicketContent, cleanupImages } from "./linear.js";
@@ -105,17 +106,47 @@ export async function fetchAndRenderDiff(branch) {
105
106
  });
106
107
  }
107
108
  /**
108
- * Check if claude CLI is available on PATH.
109
- * Returns "claude" or null if not installed.
109
+ * cmux ships its own Claude CLI shim wired to the active cmux workspace. When
110
+ * we run inside cmux, the system `claude` (if any) talks to a different
111
+ * session — confusing for the user. See manaflow-ai/cmux#2048.
110
112
  */
111
- export function resolveAgentBinary() {
113
+ const CMUX_CLAUDE_PATH = "/Applications/cmux.app/Contents/Resources/bin/claude";
114
+ /**
115
+ * Resolve the path to the Claude CLI binary, preferring cmux's bundled copy
116
+ * when running inside cmux. Falls back to PATH lookup, then to Anthropic's
117
+ * standard installer location (`~/.claude/local/claude`). Returns null if
118
+ * none of those resolve.
119
+ *
120
+ * Used by every santree code path that needs to invoke or report the Claude
121
+ * binary — version display, doctor checks, and interactive launches.
122
+ */
123
+ export function resolveClaudeBinary() {
124
+ // Inside cmux, the bundled binary is the only one wired to the active
125
+ // workspace. Always prefer it when present.
126
+ if (getMultiplexer().kind === "cmux" && existsSync(CMUX_CLAUDE_PATH)) {
127
+ return CMUX_CLAUDE_PATH;
128
+ }
129
+ // PATH lookup
112
130
  try {
113
131
  execSync("which claude", { stdio: "ignore" });
114
132
  return "claude";
115
133
  }
116
134
  catch {
117
- return null;
135
+ // fall through
118
136
  }
137
+ // Anthropic installer location — Ink renders may not inherit the user's
138
+ // shell PATH, so check this explicitly.
139
+ const localClaude = join(homedir(), ".claude", "local", "claude");
140
+ if (existsSync(localClaude))
141
+ return localClaude;
142
+ return null;
143
+ }
144
+ /**
145
+ * @deprecated Use `resolveClaudeBinary()` directly. Kept as an alias because
146
+ * existing call sites pass the return value straight to spawn args.
147
+ */
148
+ export function resolveAgentBinary() {
149
+ return resolveClaudeBinary();
119
150
  }
120
151
  // Conservative limit: 200KB leaves room for env vars within macOS 256KB ARG_MAX
121
152
  const ARG_MAX_SAFE = 200 * 1024;
@@ -7,5 +7,14 @@ interface Props {
7
7
  creatingForTicket: string | null;
8
8
  creationLogs: string;
9
9
  }
10
+ export type IssueActionItem = {
11
+ key: string;
12
+ label: string;
13
+ color: string;
14
+ };
15
+ /** Returns the context-sensitive action key list for the selected issue.
16
+ * Lifted out of the panel so the dashboard can render it on the same row as
17
+ * the global command bar (so left- and right-pane key hints align). */
18
+ export declare function buildIssueActions(di: DashboardIssue): IssueActionItem[];
10
19
  export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, }: Props): import("react/jsx-runtime").JSX.Element;
11
20
  export {};