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, {
|
|
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
|
+
}
|