santree 0.7.3 → 0.7.4

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.
@@ -10,7 +10,8 @@ const require = createRequire(import.meta.url);
10
10
  const { version } = require("../../package.json");
11
11
  import { findMainRepoRoot, createWorktree, getDefaultBranch, getBaseBranch, hasInitScript, getInitScriptPath, removeWorktree, getDiffTool, getWorktreeStatus, stageFile, unstageFile, stageAll, unstageAll, discardFile, } from "../lib/git.js";
12
12
  import { run, spawnAsync } from "../lib/exec.js";
13
- import { resolveAgentBinary, fillCommitMessage, askTicketQuestion } from "../lib/ai.js";
13
+ import { resolveAgentBinary, resolveClaudeBinary, fillCommitMessage, askTicketQuestion, } from "../lib/ai.js";
14
+ import { readTriageInvestigateConfig, isTriageInvestigateConfigured, buildInvestigatePrompt, buildInvestigateCommand, } from "../lib/triage-config.js";
14
15
  import { getInstalledClaudeVersion } from "../lib/version.js";
15
16
  import { extractTicketId, getStagedDiffContent } from "../lib/git.js";
16
17
  import { getMultiplexer } from "../lib/multiplexer/index.js";
@@ -325,6 +326,11 @@ export default function Dashboard() {
325
326
  // Triage tab appears at all. Recomputed on every data refresh — feature
326
327
  // detection via `tracker.supportsTriage`, never a kind check.
327
328
  const [supportsTriage, setSupportsTriage] = useState(false);
329
+ // Whether `.santree/metadata.json` configures an "investigate triage ticket"
330
+ // skill/prompt (`_triage.skill_name` / `_triage.prompt`). Drives whether the
331
+ // Triage `[i]` action renders enabled (cyan) or greyed. Read fresh on every
332
+ // data refresh so manual edits to metadata.json show up within a cycle.
333
+ const [triageInvestigateConfigured, setTriageInvestigateConfigured] = useState(false);
328
334
  const refreshTimerRef = useRef(null);
329
335
  const repoRootRef = useRef(null);
330
336
  const stateRef = useRef(state);
@@ -440,6 +446,7 @@ export default function Dashboard() {
440
446
  setHasWorkspaceFile(hasWs);
441
447
  const tracker = getIssueTracker(repoRoot);
442
448
  setSupportsTriage(tracker.supportsTriage === true);
449
+ setTriageInvestigateConfigured(isTriageInvestigateConfigured(readTriageInvestigateConfig(repoRoot)));
443
450
  dispatch({ type: "SET_DATA", ...data });
444
451
  dispatch({ type: "SET_REVIEWS_DATA", flatReviews: reviewData.flatReviews });
445
452
  // Triage on-call rotations (Linear). Best-effort, non-blocking — never
@@ -2658,6 +2665,43 @@ export default function Dashboard() {
2658
2665
  dispatch({ type: "TRIAGE_ASK_OPEN", ticketId: di.issue.identifier });
2659
2666
  return;
2660
2667
  }
2668
+ // Investigate — hand the ticket to a user-configured skill/prompt in a
2669
+ // new multiplexer window. Config lives in `.santree/metadata.json`
2670
+ // (`_triage.skill_name` or `_triage.prompt`); read fresh so manual
2671
+ // edits take effect without waiting for a refresh.
2672
+ if (input === "i") {
2673
+ const root = repoRootRef.current;
2674
+ if (!root)
2675
+ return;
2676
+ const prompt = buildInvestigatePrompt(readTriageInvestigateConfig(root), di.issue.identifier);
2677
+ if (!prompt) {
2678
+ dispatch({
2679
+ type: "SET_ACTION_MESSAGE",
2680
+ message: "Set _triage.skill_name (or _triage.prompt) in .santree/metadata.json to enable investigate",
2681
+ });
2682
+ return;
2683
+ }
2684
+ const mux = getMultiplexer();
2685
+ if (!mux.isActive()) {
2686
+ dispatch({
2687
+ type: "SET_ACTION_MESSAGE",
2688
+ message: "Investigate needs a multiplexer (tmux or cmux)",
2689
+ });
2690
+ return;
2691
+ }
2692
+ const command = buildInvestigateCommand(resolveClaudeBinary() ?? "claude", prompt);
2693
+ const windowName = `investigate-${di.issue.identifier}`;
2694
+ void (async () => {
2695
+ const created = await mux.createWindow({ name: windowName, cwd: root, command });
2696
+ dispatch({
2697
+ type: "SET_ACTION_MESSAGE",
2698
+ message: created.ok
2699
+ ? "Launched investigation in new window"
2700
+ : `Failed to launch investigation${created.message ? `: ${created.message}` : ""}`,
2701
+ });
2702
+ })();
2703
+ return;
2704
+ }
2661
2705
  if (input === "o") {
2662
2706
  if (!di.issue.url) {
2663
2707
  dispatch({ type: "SET_ACTION_MESSAGE", message: "No issue URL available" });
@@ -3108,14 +3152,14 @@ export default function Dashboard() {
3108
3152
  ? state.treeDetailScrollOffset
3109
3153
  : state.activeTab === "triage"
3110
3154
  ? state.triageDetailScrollOffset
3111
- : state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs, deleteStatus: selectedDeleteStatus, triage: state.activeTab === "triage", comments: triageComments, onCall: onCall })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : state.overlay === "triage-schedule" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " scroll" }), _jsx(Text, { dimColor: true, children: " · " }), _jsx(Text, { color: "cyan", bold: true, children: "q" }), _jsx(Text, { dimColor: true, children: " close" })] }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay, trackerName: activeTracker.displayName, canMutate: activeTracker.canMutate === true }) })] })) })] })] }));
3155
+ : state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs, deleteStatus: selectedDeleteStatus, triage: state.activeTab === "triage", comments: triageComments, onCall: onCall })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : state.overlay === "triage-schedule" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " scroll" }), _jsx(Text, { dimColor: true, children: " · " }), _jsx(Text, { color: "cyan", bold: true, children: "q" }), _jsx(Text, { dimColor: true, children: " close" })] }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay, trackerName: activeTracker.displayName, canMutate: activeTracker.canMutate === true, triageInvestigateConfigured: triageInvestigateConfigured }) })] })) })] })] }));
3112
3156
  }
3113
3157
  /**
3114
3158
  * Renders the per-issue action key hints (Resume / Editor / View diff / …)
3115
3159
  * lifted out of the detail panels so they sit on the same row as the global
3116
3160
  * command bar. Empty when nothing is selected.
3117
3161
  */
3118
- function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, trackerName, canMutate, }) {
3162
+ function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, trackerName, canMutate, triageInvestigateConfigured, }) {
3119
3163
  // During the diff overlay, none of the per-issue actions apply (View diff
3120
3164
  // is what got us here, Commit/PR/etc. need the detail panel context). Keep
3121
3165
  // the row blank so the diff-specific CommandBar reads cleanly.
@@ -3126,7 +3170,11 @@ function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, trackerN
3126
3170
  ? buildReviewActions(selectedReview)
3127
3171
  : []
3128
3172
  : selectedIssue
3129
- ? buildIssueActions(selectedIssue, trackerName, { tab: activeTab, canMutate })
3173
+ ? buildIssueActions(selectedIssue, trackerName, {
3174
+ tab: activeTab,
3175
+ canMutate,
3176
+ triageInvestigateConfigured,
3177
+ })
3130
3178
  : [];
3131
3179
  if (items.length === 0)
3132
3180
  return _jsx(Text, { children: " " });
@@ -37,6 +37,7 @@ export type IssueActionItem = {
37
37
  export declare function buildIssueActions(di: DashboardIssue, trackerName: string, opts?: {
38
38
  tab?: DashboardTab;
39
39
  canMutate?: boolean;
40
+ triageInvestigateConfigured?: boolean;
40
41
  }): IssueActionItem[];
41
42
  export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, deleteStatus, triage, comments, onCall, }: Props): import("react/jsx-runtime").JSX.Element;
42
43
  export {};
@@ -85,6 +85,13 @@ export function buildIssueActions(di, trackerName, opts) {
85
85
  if (opts?.tab === "triage") {
86
86
  items.push({ key: "w", label: "Send to tree", color: "cyan" });
87
87
  items.push({ key: "a", label: "Ask", color: "cyan" });
88
+ // Greyed until `_triage.skill_name`/`_triage.prompt` is set in
89
+ // `.santree/metadata.json` — pressing it then just prints a hint.
90
+ items.push({
91
+ key: "i",
92
+ label: "Investigate",
93
+ color: opts.triageInvestigateConfigured ? "cyan" : "gray",
94
+ });
88
95
  items.push({ key: "s", label: "Schedule", color: "cyan" });
89
96
  if (issue.url) {
90
97
  items.push({ key: "o", label: trackerName, color: "gray" });
@@ -45,6 +45,11 @@ const LEGEND = [
45
45
  { glyph: "d", color: "red", meaning: "Delete issue (built-in) / remove worktree (Trees)" },
46
46
  { glyph: "w", color: "cyan", meaning: "Start work / send to tree (creates a worktree)" },
47
47
  { glyph: "a", color: "cyan", meaning: "Ask Claude about the issue + comments (Triage tab)" },
48
+ {
49
+ glyph: "i",
50
+ color: "cyan",
51
+ meaning: "Investigate ticket via your configured skill/prompt (Triage tab)",
52
+ },
48
53
  { glyph: "s", color: "cyan", meaning: "Triage on-call schedule (Triage tab)" },
49
54
  ],
50
55
  },
@@ -0,0 +1,21 @@
1
+ /** Per-repo "investigate triage ticket" config, read from
2
+ * `.santree/metadata.json` (`_triage.skill_name` / `_triage.prompt`).
3
+ * metadata.json is gitignored, so this is per-machine — each dev points it at
4
+ * their own skill or prompt without committing it. */
5
+ export interface TriageInvestigateConfig {
6
+ /** A Claude slash-command/skill name. Invoked as `/<skill_name> <ticketId>`. */
7
+ skillName: string | null;
8
+ /** A free-form prompt template; `{ticket_id}` is substituted with the ticket. */
9
+ prompt: string | null;
10
+ }
11
+ export declare function readTriageInvestigateConfig(repoRoot: string): TriageInvestigateConfig;
12
+ export declare function isTriageInvestigateConfigured(cfg: TriageInvestigateConfig): boolean;
13
+ /** Builds the prompt handed to Claude for investigating a ticket. Skill name
14
+ * wins when both are set: `/<skill> <ticketId>`. Otherwise the free-form
15
+ * template with every `{ticket_id}` replaced. Returns null when unconfigured. */
16
+ export declare function buildInvestigatePrompt(cfg: TriageInvestigateConfig, ticketId: string): string | null;
17
+ /** Builds the shell command line launched in the new multiplexer window:
18
+ * `<claudeBin> '<prompt>'`. The multiplexer escapes the whole line again for
19
+ * its send-keys step; this layer just needs the prompt to survive as a single
20
+ * argv entry so a slash command (`/skill TEAM-1`) reaches Claude intact. */
21
+ export declare function buildInvestigateCommand(claudeBin: string, prompt: string): string;
@@ -0,0 +1,32 @@
1
+ import { readAllMetadata } from "./metadata.js";
2
+ export function readTriageInvestigateConfig(repoRoot) {
3
+ const all = readAllMetadata(repoRoot);
4
+ const t = all["_triage"];
5
+ const skillName = typeof t?.skill_name === "string" && t.skill_name.trim() ? t.skill_name.trim() : null;
6
+ const prompt = typeof t?.prompt === "string" && t.prompt.trim() ? t.prompt.trim() : null;
7
+ return { skillName, prompt };
8
+ }
9
+ export function isTriageInvestigateConfigured(cfg) {
10
+ return cfg.skillName !== null || cfg.prompt !== null;
11
+ }
12
+ /** Builds the prompt handed to Claude for investigating a ticket. Skill name
13
+ * wins when both are set: `/<skill> <ticketId>`. Otherwise the free-form
14
+ * template with every `{ticket_id}` replaced. Returns null when unconfigured. */
15
+ export function buildInvestigatePrompt(cfg, ticketId) {
16
+ if (cfg.skillName)
17
+ return `/${cfg.skillName} ${ticketId}`;
18
+ if (cfg.prompt)
19
+ return cfg.prompt.replace(/\{ticket_id\}/g, ticketId);
20
+ return null;
21
+ }
22
+ /** POSIX single-quote a shell argument. */
23
+ function shellSingleQuote(s) {
24
+ return `'${s.replace(/'/g, `'\\''`)}'`;
25
+ }
26
+ /** Builds the shell command line launched in the new multiplexer window:
27
+ * `<claudeBin> '<prompt>'`. The multiplexer escapes the whole line again for
28
+ * its send-keys step; this layer just needs the prompt to survive as a single
29
+ * argv entry so a slash command (`/skill TEAM-1`) reaches Claude intact. */
30
+ export function buildInvestigateCommand(claudeBin, prompt) {
31
+ return `${claudeBin} ${shellSingleQuote(prompt)}`;
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "santree",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Git worktree manager",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Toscanini",