santree 0.7.5 → 0.7.6
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 +2 -3
- package/dist/commands/config.d.ts +12 -0
- package/dist/commands/config.js +444 -0
- package/dist/commands/dashboard.js +165 -61
- package/dist/commands/helpers/statusline.js +0 -30
- package/dist/commands/issue/index.d.ts +1 -1
- package/dist/commands/issue/index.js +1 -1
- package/dist/commands/pr/context.d.ts +7 -0
- package/dist/commands/pr/context.js +36 -0
- package/dist/commands/pr/fix.d.ts +11 -2
- package/dist/commands/pr/fix.js +64 -12
- package/dist/commands/worktree/create.js +6 -1
- package/dist/commands/worktree/open.js +3 -2
- package/dist/lib/ai.d.ts +11 -0
- package/dist/lib/ai.js +71 -2
- package/dist/lib/claude-config.d.ts +18 -4
- package/dist/lib/claude-config.js +58 -23
- package/dist/lib/config/TrackerPicker.d.ts +10 -0
- package/dist/lib/config/TrackerPicker.js +113 -0
- package/dist/lib/config/diagnostics.d.ts +37 -0
- package/dist/lib/config/diagnostics.js +190 -0
- package/dist/lib/config-store.d.ts +23 -0
- package/dist/lib/config-store.js +68 -0
- package/dist/lib/dashboard/DetailPanel.d.ts +4 -2
- package/dist/lib/dashboard/DetailPanel.js +44 -37
- package/dist/lib/dashboard/IssueList.d.ts +5 -1
- package/dist/lib/dashboard/IssueList.js +24 -5
- package/dist/lib/dashboard/MultilineTextArea.js +1 -1
- package/dist/lib/dashboard/Overlays.js +3 -4
- package/dist/lib/dashboard/ReviewDetailPanel.js +11 -2
- package/dist/lib/dashboard/ReviewList.js +6 -3
- package/dist/lib/dashboard/data.js +7 -34
- package/dist/lib/dashboard/external-editor.d.ts +1 -1
- package/dist/lib/dashboard/external-editor.js +3 -2
- package/dist/lib/dashboard/types.d.ts +13 -2
- package/dist/lib/dashboard/types.js +33 -2
- package/dist/lib/exec.d.ts +13 -0
- package/dist/lib/exec.js +11 -1
- package/dist/lib/fix-loop.d.ts +45 -0
- package/dist/lib/fix-loop.js +83 -0
- package/dist/lib/git.d.ts +3 -29
- package/dist/lib/git.js +6 -62
- package/dist/lib/github.d.ts +70 -0
- package/dist/lib/github.js +128 -0
- package/dist/lib/multiplexer/cmux.js +143 -17
- package/dist/lib/multiplexer/none.js +3 -3
- package/dist/lib/multiplexer/tmux.js +5 -29
- package/dist/lib/multiplexer/types.d.ts +34 -1
- package/dist/lib/open-url.d.ts +3 -2
- package/dist/lib/open-url.js +5 -4
- package/dist/lib/prompts.d.ts +35 -1
- package/dist/lib/prompts.js +7 -0
- package/dist/lib/setup/apply.d.ts +2 -2
- package/dist/lib/setup/apply.js +2 -2
- package/dist/lib/setup/gitignore.d.ts +7 -0
- package/dist/lib/setup/gitignore.js +39 -1
- package/dist/lib/setup/steps.d.ts +6 -2
- package/dist/lib/setup/steps.js +65 -60
- package/dist/lib/setup/tools.d.ts +1 -1
- package/dist/lib/setup/tools.js +1 -1
- package/dist/lib/trackers/github/index.js +2 -2
- package/dist/lib/trackers/local/store.js +1 -1
- package/package.json +1 -1
- package/prompts/fix-context.njk +79 -0
- package/prompts/fix-loop.njk +18 -0
- package/prompts/pr.njk +1 -1
- package/dist/commands/doctor.d.ts +0 -2
- package/dist/commands/doctor.js +0 -545
- package/dist/commands/github/auth.d.ts +0 -2
- package/dist/commands/github/auth.js +0 -56
- package/dist/commands/github/index.d.ts +0 -1
- package/dist/commands/github/index.js +0 -1
- package/dist/commands/helpers/english-tutor/index.d.ts +0 -1
- package/dist/commands/helpers/english-tutor/index.js +0 -1
- package/dist/commands/helpers/english-tutor/install.d.ts +0 -8
- package/dist/commands/helpers/english-tutor/install.js +0 -24
- package/dist/commands/helpers/english-tutor/prompt.d.ts +0 -2
- package/dist/commands/helpers/english-tutor/prompt.js +0 -16
- package/dist/commands/helpers/english-tutor/session-start.d.ts +0 -2
- package/dist/commands/helpers/english-tutor/session-start.js +0 -34
- package/dist/commands/helpers/english-tutor/uninstall.d.ts +0 -2
- package/dist/commands/helpers/english-tutor/uninstall.js +0 -15
- package/dist/commands/helpers/session-signal/end.d.ts +0 -2
- package/dist/commands/helpers/session-signal/end.js +0 -13
- package/dist/commands/helpers/session-signal/index.d.ts +0 -1
- package/dist/commands/helpers/session-signal/index.js +0 -1
- package/dist/commands/helpers/session-signal/install.d.ts +0 -8
- package/dist/commands/helpers/session-signal/install.js +0 -24
- package/dist/commands/helpers/session-signal/notification.d.ts +0 -2
- package/dist/commands/helpers/session-signal/notification.js +0 -13
- package/dist/commands/helpers/session-signal/prompt.d.ts +0 -2
- package/dist/commands/helpers/session-signal/prompt.js +0 -13
- package/dist/commands/helpers/session-signal/stop.d.ts +0 -2
- package/dist/commands/helpers/session-signal/stop.js +0 -13
- package/dist/commands/issue/setup.d.ts +0 -2
- package/dist/commands/issue/setup.js +0 -108
- package/dist/commands/issue/switch.d.ts +0 -12
- package/dist/commands/issue/switch.js +0 -38
- package/dist/commands/linear/switch.d.ts +0 -2
- package/dist/commands/linear/switch.js +0 -75
- package/dist/commands/setup.d.ts +0 -11
- package/dist/commands/setup.js +0 -194
- package/dist/lib/english-tutor.d.ts +0 -13
- package/dist/lib/english-tutor.js +0 -125
- package/dist/lib/session-signal.d.ts +0 -16
- package/dist/lib/session-signal.js +0 -104
- package/dist/lib/setup/shell-config.d.ts +0 -38
- package/dist/lib/setup/shell-config.js +0 -131
- package/prompts/english-tutor-prompt.njk +0 -15
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { run } from "../exec.js";
|
|
2
|
+
import { which } from "../setup/tools.js";
|
|
3
|
+
import { getMultiplexer } from "../multiplexer/index.js";
|
|
4
|
+
import { resolveClaudeBinary } from "../ai.js";
|
|
5
|
+
import { findMainRepoRoot } from "../git.js";
|
|
6
|
+
import { getIssueTracker } from "../trackers/index.js";
|
|
7
|
+
import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, SANTREE_PACKAGE, getLatestVersionFor, isUpdateAvailable, detectPackageManager, getInstallCommandFor, } from "../version.js";
|
|
8
|
+
function tryRun(cmd) {
|
|
9
|
+
return run(`${cmd} 2>/dev/null`);
|
|
10
|
+
}
|
|
11
|
+
function checkGit() {
|
|
12
|
+
const p = which("git");
|
|
13
|
+
if (!p) {
|
|
14
|
+
return {
|
|
15
|
+
id: "git",
|
|
16
|
+
title: "git",
|
|
17
|
+
description: "Version control",
|
|
18
|
+
scope: "system",
|
|
19
|
+
required: true,
|
|
20
|
+
ok: false,
|
|
21
|
+
lines: [],
|
|
22
|
+
hint: "Install: brew install git",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const version = tryRun("git --version | head -1");
|
|
26
|
+
return {
|
|
27
|
+
id: "git",
|
|
28
|
+
title: "git",
|
|
29
|
+
description: "Version control",
|
|
30
|
+
scope: "system",
|
|
31
|
+
required: true,
|
|
32
|
+
ok: true,
|
|
33
|
+
lines: [`Version: ${version || "unknown"}`, `Path: ${p}`],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function checkNode() {
|
|
37
|
+
const version = process.versions.node;
|
|
38
|
+
const major = Number(version.split(".")[0] || "0");
|
|
39
|
+
const ok = major >= 20;
|
|
40
|
+
return {
|
|
41
|
+
id: "node",
|
|
42
|
+
title: "node",
|
|
43
|
+
description: "JavaScript runtime (>= 20 required)",
|
|
44
|
+
scope: "system",
|
|
45
|
+
required: true,
|
|
46
|
+
ok,
|
|
47
|
+
lines: [`Version: v${version}`, `Path: ${process.execPath}`],
|
|
48
|
+
hint: ok ? undefined : "Santree needs Node.js >= 20. Upgrade: https://nodejs.org",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function checkSantreeSelf() {
|
|
52
|
+
const latest = await getLatestVersionFor(SANTREE_PACKAGE);
|
|
53
|
+
const updatable = !!latest && isUpdateAvailable(CURRENT_VERSION, latest);
|
|
54
|
+
const lines = [`Version: ${CURRENT_VERSION}`];
|
|
55
|
+
if (latest)
|
|
56
|
+
lines.push(`Latest: ${latest}${updatable ? " ⬆ update available" : ""}`);
|
|
57
|
+
return {
|
|
58
|
+
id: "santree",
|
|
59
|
+
title: "santree",
|
|
60
|
+
description: "Santree CLI (this app)",
|
|
61
|
+
scope: "system",
|
|
62
|
+
required: true,
|
|
63
|
+
ok: true,
|
|
64
|
+
lines,
|
|
65
|
+
hint: updatable ? "Run: santree update" : undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function checkMultiplexer() {
|
|
69
|
+
const mux = getMultiplexer();
|
|
70
|
+
if (mux.kind === "tmux") {
|
|
71
|
+
const version = tryRun("tmux -V");
|
|
72
|
+
return {
|
|
73
|
+
id: "multiplexer",
|
|
74
|
+
title: "tmux",
|
|
75
|
+
description: "Active multiplexer — new-window flows enabled",
|
|
76
|
+
scope: "system",
|
|
77
|
+
required: false,
|
|
78
|
+
ok: true,
|
|
79
|
+
lines: [`Version: ${version || "unknown"}`, `Path: ${which("tmux") || "unknown"}`],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// cmux
|
|
83
|
+
const version = tryRun("cmux --version");
|
|
84
|
+
const ping = tryRun("cmux ping");
|
|
85
|
+
return {
|
|
86
|
+
id: "multiplexer",
|
|
87
|
+
title: "cmux",
|
|
88
|
+
description: "Active multiplexer — new-window flows enabled",
|
|
89
|
+
scope: "system",
|
|
90
|
+
required: false,
|
|
91
|
+
ok: !!ping,
|
|
92
|
+
lines: [`Version: ${version || "unknown"}`, `Path: ${which("cmux") || "unknown"}`],
|
|
93
|
+
hint: ping ? undefined : "cmux app not reachable — open cmux.app.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function checkWorkspaceEditor() {
|
|
97
|
+
const code = which("code");
|
|
98
|
+
const cursor = which("cursor");
|
|
99
|
+
const found = code ? "code" : cursor ? "cursor" : null;
|
|
100
|
+
if (found) {
|
|
101
|
+
const version = tryRun(`${found} --version | head -1`);
|
|
102
|
+
return {
|
|
103
|
+
id: "workspace-editor",
|
|
104
|
+
title: "Workspace editor",
|
|
105
|
+
description: "code/cursor — powers the dashboard's `E workspace` shortcut",
|
|
106
|
+
scope: "system",
|
|
107
|
+
required: false,
|
|
108
|
+
ok: true,
|
|
109
|
+
lines: [`Editor: ${found}`, `Version: ${version || "unknown"}`],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
id: "workspace-editor",
|
|
114
|
+
title: "Workspace editor",
|
|
115
|
+
description: "code/cursor — powers the dashboard's `E workspace` shortcut",
|
|
116
|
+
scope: "system",
|
|
117
|
+
required: false,
|
|
118
|
+
ok: false,
|
|
119
|
+
lines: [],
|
|
120
|
+
hint: "Optional — only the `.code-workspace` shortcut needs code/cursor. Everything else uses your configured editor.",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** Version + auth detail for the gh and claude step rows. */
|
|
124
|
+
async function loadStepDetail() {
|
|
125
|
+
const detail = new Map();
|
|
126
|
+
const pm = detectPackageManager();
|
|
127
|
+
// claude — version + latest + cmux-aware update hint
|
|
128
|
+
const resolved = resolveClaudeBinary();
|
|
129
|
+
if (resolved) {
|
|
130
|
+
const version = tryRun(`"${resolved}" --version | head -1`);
|
|
131
|
+
const latest = await getLatestVersionFor(CLAUDE_CODE_PACKAGE);
|
|
132
|
+
const lines = [`Version: ${version || "unknown"}`, `Path: ${resolved}`];
|
|
133
|
+
let hint;
|
|
134
|
+
if (latest && version && isUpdateAvailable(version, latest)) {
|
|
135
|
+
lines.push(`Latest: ${latest} ⬆ update available`);
|
|
136
|
+
if (resolved.includes("/cmux.app/")) {
|
|
137
|
+
hint = "Bundled with cmux — update cmux.app to get the latest Claude.";
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
hint = `Run: ${getInstallCommandFor(pm, `${CLAUDE_CODE_PACKAGE}@latest`).display}`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
detail.set("claude", { lines, hint });
|
|
144
|
+
}
|
|
145
|
+
// gh — version + authenticated login
|
|
146
|
+
if (which("gh")) {
|
|
147
|
+
const version = tryRun("gh --version | head -1");
|
|
148
|
+
const login = tryRun("gh api user --jq .login");
|
|
149
|
+
const lines = [`Version: ${version || "unknown"}`];
|
|
150
|
+
if (login)
|
|
151
|
+
lines.push(`Auth: authenticated as ${login}`);
|
|
152
|
+
detail.set("gh", { lines, hint: login ? undefined : "Run: gh auth login" });
|
|
153
|
+
}
|
|
154
|
+
return detail;
|
|
155
|
+
}
|
|
156
|
+
/** Active issue tracker auth detail for the tracker row. */
|
|
157
|
+
async function loadTrackerDetail() {
|
|
158
|
+
const repoRoot = findMainRepoRoot();
|
|
159
|
+
if (!repoRoot)
|
|
160
|
+
return null;
|
|
161
|
+
try {
|
|
162
|
+
const tracker = getIssueTracker(repoRoot);
|
|
163
|
+
const status = await tracker.getAuthStatus(repoRoot);
|
|
164
|
+
const lines = [`Tracker: ${tracker.displayName}`];
|
|
165
|
+
if (status.accountLabel)
|
|
166
|
+
lines.push(`Account: ${status.accountLabel}`);
|
|
167
|
+
if (status.repoLinked !== undefined)
|
|
168
|
+
lines.push(`Repo linked: ${status.repoLinked ? "yes" : "no"}`);
|
|
169
|
+
return { lines, hint: status.authenticated ? undefined : status.hint };
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Run every read-only check. Network calls (npm registry, gh, tracker) run in parallel. */
|
|
176
|
+
export async function loadDiagnostics() {
|
|
177
|
+
const muxActive = getMultiplexer().kind !== "none";
|
|
178
|
+
const [santree, stepDetail, trackerDetail] = await Promise.all([
|
|
179
|
+
checkSantreeSelf(),
|
|
180
|
+
loadStepDetail(),
|
|
181
|
+
loadTrackerDetail(),
|
|
182
|
+
]);
|
|
183
|
+
if (trackerDetail)
|
|
184
|
+
stepDetail.set("tracker", trackerDetail);
|
|
185
|
+
const infoRows = [santree, checkNode(), checkGit()];
|
|
186
|
+
if (muxActive)
|
|
187
|
+
infoRows.push(checkMultiplexer());
|
|
188
|
+
infoRows.push(checkWorkspaceEditor());
|
|
189
|
+
return { infoRows, stepDetail, muxActive };
|
|
190
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface SantreeConfig {
|
|
2
|
+
/** Editor launched by the dashboard's [e] action and Ctrl+O in text areas. */
|
|
3
|
+
editor?: string;
|
|
4
|
+
/** Unified-diff pager for `worktree diff` and the dashboard [v] overlay. */
|
|
5
|
+
diffTool?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function configStorePath(): string;
|
|
8
|
+
export declare function readConfigStore(): SantreeConfig;
|
|
9
|
+
export declare function setConfigValue<K extends keyof SantreeConfig>(key: K, value: SantreeConfig[K]): void;
|
|
10
|
+
export declare function unsetConfigValue(key: keyof SantreeConfig): void;
|
|
11
|
+
/**
|
|
12
|
+
* Resolved editor command: `SANTREE_EDITOR` env override → config file → none.
|
|
13
|
+
* Call sites keep their own final fallback (e.g. `"code"` for GUI-open flows,
|
|
14
|
+
* the `VISUAL`/`EDITOR`/`vim` chain for in-terminal editing).
|
|
15
|
+
*/
|
|
16
|
+
export declare function getConfiguredEditor(): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Resolved diff pager: `SANTREE_DIFF_TOOL` env override → config file → none.
|
|
19
|
+
* Restricted to a safe shell-token character set since it ends up in spawn()
|
|
20
|
+
* arguments — even though we never use shell:true, a tight surface defends
|
|
21
|
+
* against accidental misconfigurations.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getConfiguredDiffTool(): string | undefined;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
/**
|
|
5
|
+
* Santree's global config file — `$XDG_CONFIG_HOME/santree/config.json`
|
|
6
|
+
* (defaults to `~/.config/santree/config.json`), sitting next to `auth.json`.
|
|
7
|
+
*
|
|
8
|
+
* This is where preferences santree owns live (editor, diff tool) instead of
|
|
9
|
+
* exporting them into the user's shell rc. Writing here takes effect on the
|
|
10
|
+
* next santree run with no shell restart. The matching `SANTREE_EDITOR` /
|
|
11
|
+
* `SANTREE_DIFF_TOOL` env vars still work as a one-off override that wins over
|
|
12
|
+
* the file, so existing shell exports and CI overrides keep behaving.
|
|
13
|
+
*/
|
|
14
|
+
const CONFIG_DIR = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
15
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "santree", "config.json");
|
|
16
|
+
export function configStorePath() {
|
|
17
|
+
return CONFIG_PATH;
|
|
18
|
+
}
|
|
19
|
+
export function readConfigStore() {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
22
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function writeConfigStore(cfg) {
|
|
29
|
+
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
30
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
|
|
31
|
+
}
|
|
32
|
+
export function setConfigValue(key, value) {
|
|
33
|
+
const cfg = readConfigStore();
|
|
34
|
+
cfg[key] = value;
|
|
35
|
+
writeConfigStore(cfg);
|
|
36
|
+
}
|
|
37
|
+
export function unsetConfigValue(key) {
|
|
38
|
+
const cfg = readConfigStore();
|
|
39
|
+
if (!(key in cfg))
|
|
40
|
+
return;
|
|
41
|
+
delete cfg[key];
|
|
42
|
+
writeConfigStore(cfg);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolved editor command: `SANTREE_EDITOR` env override → config file → none.
|
|
46
|
+
* Call sites keep their own final fallback (e.g. `"code"` for GUI-open flows,
|
|
47
|
+
* the `VISUAL`/`EDITOR`/`vim` chain for in-terminal editing).
|
|
48
|
+
*/
|
|
49
|
+
export function getConfiguredEditor() {
|
|
50
|
+
const env = process.env.SANTREE_EDITOR;
|
|
51
|
+
if (env && env.trim())
|
|
52
|
+
return env.trim();
|
|
53
|
+
const stored = readConfigStore().editor;
|
|
54
|
+
return stored && stored.trim() ? stored.trim() : undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolved diff pager: `SANTREE_DIFF_TOOL` env override → config file → none.
|
|
58
|
+
* Restricted to a safe shell-token character set since it ends up in spawn()
|
|
59
|
+
* arguments — even though we never use shell:true, a tight surface defends
|
|
60
|
+
* against accidental misconfigurations.
|
|
61
|
+
*/
|
|
62
|
+
export function getConfiguredDiffTool() {
|
|
63
|
+
const raw = process.env.SANTREE_DIFF_TOOL ?? readConfigStore().diffTool;
|
|
64
|
+
if (!raw || !raw.trim())
|
|
65
|
+
return undefined;
|
|
66
|
+
const tool = raw.trim();
|
|
67
|
+
return /^[a-zA-Z0-9_\-/.+]+$/.test(tool) ? tool : undefined;
|
|
68
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Comment, DashboardIssue, DashboardTab, DeleteStatus } from "./types.js";
|
|
1
|
+
import type { Comment, DashboardIssue, DashboardTab, DeleteStatus, FixLoopRuntime } from "./types.js";
|
|
2
2
|
interface Props {
|
|
3
3
|
issue: DashboardIssue | null;
|
|
4
4
|
scrollOffset: number;
|
|
@@ -11,6 +11,8 @@ interface Props {
|
|
|
11
11
|
/** True while a just-created PR is shown optimistically, before a refresh
|
|
12
12
|
* confirms it (renders a "syncing…" hint next to the PR). */
|
|
13
13
|
prSyncing?: boolean;
|
|
14
|
+
/** Active/recent auto-fix loop for the selected ticket, shown as a ⟳ line. */
|
|
15
|
+
fixLoop?: FixLoopRuntime;
|
|
14
16
|
/** Triage mode: hide worktree/PR/checks sections (they never apply to an
|
|
15
17
|
* inbox issue) and show the discussion instead. */
|
|
16
18
|
triage?: boolean;
|
|
@@ -42,5 +44,5 @@ export declare function buildIssueActions(di: DashboardIssue, trackerName: strin
|
|
|
42
44
|
canMutate?: boolean;
|
|
43
45
|
triageInvestigateConfigured?: boolean;
|
|
44
46
|
}): IssueActionItem[];
|
|
45
|
-
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, deleteStatus, prSyncing, triage, comments, onCall, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
47
|
+
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, deleteStatus, prSyncing, fixLoop, triage, comments, onCall, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
46
48
|
export {};
|
|
@@ -134,7 +134,7 @@ export function buildIssueActions(di, trackerName, opts) {
|
|
|
134
134
|
items.push({ key: "c", label: "Create PR", color: "cyan" });
|
|
135
135
|
}
|
|
136
136
|
if (pr) {
|
|
137
|
-
items.push({ key: "f", label: "Fix
|
|
137
|
+
items.push({ key: "f", label: "Fix loop", color: "cyan" });
|
|
138
138
|
items.push({ key: "r", label: "Review", color: "cyan" });
|
|
139
139
|
}
|
|
140
140
|
if (issue.url) {
|
|
@@ -158,7 +158,7 @@ function sectionHeader(icon, label, iconColor = "cyan") {
|
|
|
158
158
|
],
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
|
-
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, deleteStatus, prSyncing = false, triage = false, comments, onCall, }) {
|
|
161
|
+
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, deleteStatus, prSyncing = false, fixLoop, triage = false, comments, onCall, }) {
|
|
162
162
|
// Show deletion progress when the selected worktree is being removed.
|
|
163
163
|
if (issue && deleteStatus) {
|
|
164
164
|
const logLines = deleteStatus.logs.split("\n");
|
|
@@ -417,39 +417,8 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
417
417
|
lines.push({ text: ` +${gs.files.length - maxFiles} more`, dim: true });
|
|
418
418
|
}
|
|
419
419
|
}
|
|
420
|
-
//
|
|
421
|
-
if (worktree.
|
|
422
|
-
const msg = worktree.sessionMessage
|
|
423
|
-
? `NEEDS INPUT: ${worktree.sessionMessage}`
|
|
424
|
-
: "NEEDS INPUT";
|
|
425
|
-
lines.push({
|
|
426
|
-
text: "",
|
|
427
|
-
segments: [
|
|
428
|
-
{ text: " ◆ ", color: "red" },
|
|
429
|
-
{ text: msg, color: "red", bold: true },
|
|
430
|
-
],
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
else if (worktree.sessionState === "active") {
|
|
434
|
-
lines.push({
|
|
435
|
-
text: "",
|
|
436
|
-
segments: [
|
|
437
|
-
{ text: " ◆ ", color: "green" },
|
|
438
|
-
{ text: "session active", color: "green" },
|
|
439
|
-
],
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
else if (worktree.sessionState === "idle") {
|
|
443
|
-
lines.push({
|
|
444
|
-
text: "",
|
|
445
|
-
segments: [
|
|
446
|
-
{ text: " ◆ ", color: "yellow" },
|
|
447
|
-
{ text: "session idle", color: "yellow" },
|
|
448
|
-
{ text: " (waiting for prompt)", dim: true },
|
|
449
|
-
],
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
else if (worktree.sessionId) {
|
|
420
|
+
// Resumable Claude session (the stored session id), or none.
|
|
421
|
+
if (worktree.sessionId) {
|
|
453
422
|
lines.push({
|
|
454
423
|
text: "",
|
|
455
424
|
segments: [
|
|
@@ -577,18 +546,51 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
577
546
|
lines.push(sectionHeader("◉", "Pull Request"));
|
|
578
547
|
lines.push({ text: " no PR yet", dim: true });
|
|
579
548
|
}
|
|
549
|
+
// ── Fix loop ──────────────────────────────────────────────────────
|
|
550
|
+
if (!isMain && fixLoop) {
|
|
551
|
+
const { phase } = fixLoop;
|
|
552
|
+
const color = phase === "running"
|
|
553
|
+
? "cyan"
|
|
554
|
+
: phase === "stalled"
|
|
555
|
+
? "yellow"
|
|
556
|
+
: phase === "stopped-clean"
|
|
557
|
+
? "green"
|
|
558
|
+
: "red";
|
|
559
|
+
const label = phase === "running"
|
|
560
|
+
? `fix loop · ${fixLoop.status}`
|
|
561
|
+
: phase === "stalled"
|
|
562
|
+
? "fix loop · stalled (no recent activity)"
|
|
563
|
+
: phase === "stopped-clean"
|
|
564
|
+
? "fix loop · stopped — all clear"
|
|
565
|
+
: "fix loop · stopped — needs a human";
|
|
566
|
+
const ageMin = Math.max(0, Math.round(fixLoop.ageMs / 60000));
|
|
567
|
+
lines.push(ruleLine);
|
|
568
|
+
lines.push({
|
|
569
|
+
text: "",
|
|
570
|
+
segments: [
|
|
571
|
+
{ text: "⟳ ", color, bold: true },
|
|
572
|
+
{ text: label, color },
|
|
573
|
+
{ text: ` updated ${ageMin}m ago`, dim: true },
|
|
574
|
+
],
|
|
575
|
+
});
|
|
576
|
+
}
|
|
580
577
|
// ── Checks ────────────────────────────────────────────────────────
|
|
581
578
|
if (!isMain && checks && checks.length > 0) {
|
|
579
|
+
// `gh` buckets: pass | fail | pending | skipping | cancel. Skipped/cancelled
|
|
580
|
+
// checks don't run under the current conditions and don't block the PR —
|
|
581
|
+
// GitHub counts them as "skipped" while still reporting "all checks passed".
|
|
582
|
+
// Treat them as a non-blocking aside, never as pending.
|
|
582
583
|
const passing = checks.filter((c) => c.bucket === "pass");
|
|
583
584
|
const failing = checks.filter((c) => c.bucket === "fail");
|
|
584
|
-
const pending = checks.filter((c) => c.bucket
|
|
585
|
+
const pending = checks.filter((c) => c.bucket === "pending");
|
|
586
|
+
const skipped = checks.filter((c) => c.bucket === "skipping" || c.bucket === "cancel");
|
|
585
587
|
const headerColor = failing.length > 0 ? "red" : pending.length > 0 ? "yellow" : "green";
|
|
586
588
|
lines.push(ruleLine);
|
|
587
589
|
const headerSegs = [
|
|
588
590
|
{ text: "✓ ", color: "cyan", bold: true },
|
|
589
591
|
{ text: "Checks", bold: true },
|
|
590
592
|
{ text: " " },
|
|
591
|
-
{ text: `${passing.length}
|
|
593
|
+
{ text: `${passing.length} passed`, color: headerColor },
|
|
592
594
|
];
|
|
593
595
|
if (failing.length > 0) {
|
|
594
596
|
headerSegs.push({ text: " · ", dim: true });
|
|
@@ -598,8 +600,13 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
598
600
|
headerSegs.push({ text: " · ", dim: true });
|
|
599
601
|
headerSegs.push({ text: `${pending.length} pending`, color: "yellow" });
|
|
600
602
|
}
|
|
603
|
+
if (skipped.length > 0) {
|
|
604
|
+
headerSegs.push({ text: " · ", dim: true });
|
|
605
|
+
headerSegs.push({ text: `${skipped.length} skipped`, dim: true });
|
|
606
|
+
}
|
|
601
607
|
lines.push({ text: "", segments: headerSegs });
|
|
602
608
|
// Order: failing first (most important), then pending, then passing.
|
|
609
|
+
// Skipped checks aren't listed individually — just summarized above.
|
|
603
610
|
for (const check of failing) {
|
|
604
611
|
const desc = check.description ? ` — ${check.description}` : "";
|
|
605
612
|
lines.push({ text: ` ✗ ${check.name}${desc}`, color: "red" });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FixLoopRuntime } from "../fix-loop.js";
|
|
1
2
|
import type { ProjectGroup, DashboardIssue } from "./types.js";
|
|
2
3
|
interface Props {
|
|
3
4
|
groups: ProjectGroup[];
|
|
@@ -16,6 +17,9 @@ interface Props {
|
|
|
16
17
|
/** Ticket ids whose worktree is currently being removed — shown with a
|
|
17
18
|
* distinct WT-column glyph so concurrent deletions are visible in the list. */
|
|
18
19
|
deletingIds?: Set<string>;
|
|
20
|
+
/** Active/recent auto-fix loops by ticket id — a running/stalled loop shows a
|
|
21
|
+
* ⟳ in place of the CI glyph (the loop is the CI story while it runs). */
|
|
22
|
+
fixLoops?: Record<string, FixLoopRuntime>;
|
|
19
23
|
}
|
|
20
24
|
export type ListRow = {
|
|
21
25
|
kind: "spacer";
|
|
@@ -36,5 +40,5 @@ export type ListRow = {
|
|
|
36
40
|
depth: number;
|
|
37
41
|
};
|
|
38
42
|
export declare function buildIssueListRows(groups: ProjectGroup[], flatIssues: DashboardIssue[]): ListRow[];
|
|
39
|
-
export default function IssueList({ groups, flatIssues, selectedIndex, scrollOffset, height, width, selectionBg, variant, deletingIds, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
43
|
+
export default function IssueList({ groups, flatIssues, selectedIndex, scrollOffset, height, width, selectionBg, variant, deletingIds, fixLoops, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
40
44
|
export {};
|
|
@@ -45,11 +45,30 @@ function workIndicator(wt) {
|
|
|
45
45
|
function ciIndicator(checks) {
|
|
46
46
|
if (!checks || checks.length === 0)
|
|
47
47
|
return { glyph: "·", color: "gray" };
|
|
48
|
+
// `gh` buckets: pass | fail | pending | skipping | cancel. Only `fail` blocks
|
|
49
|
+
// (red) and only `pending` is genuinely still running (yellow). `skipping` /
|
|
50
|
+
// `cancel` don't run under the current conditions and don't block — GitHub
|
|
51
|
+
// reports them as "skipped" and still shows "all checks passed", so they must
|
|
52
|
+
// not turn the glyph yellow.
|
|
48
53
|
if (checks.some((c) => c.bucket === "fail"))
|
|
49
54
|
return { glyph: "✗", color: "red" };
|
|
50
|
-
if (checks.
|
|
51
|
-
return { glyph: "
|
|
52
|
-
return { glyph: "
|
|
55
|
+
if (checks.some((c) => c.bucket === "pending"))
|
|
56
|
+
return { glyph: "●", color: "yellow" };
|
|
57
|
+
return { glyph: "✓", color: "green" };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* While a fix loop is actively running (or stalled), it owns the CI column — a ⟳
|
|
61
|
+
* conveys "being auto-fixed" better than the underlying check state. Stopped loops
|
|
62
|
+
* fall back to the real CI glyph (CI green ≈ stopped clean, CI red ≈ stopped stuck).
|
|
63
|
+
*/
|
|
64
|
+
function fixLoopOverride(loop) {
|
|
65
|
+
if (!loop)
|
|
66
|
+
return null;
|
|
67
|
+
if (loop.phase === "running")
|
|
68
|
+
return { glyph: "⟳", color: "cyan" };
|
|
69
|
+
if (loop.phase === "stalled")
|
|
70
|
+
return { glyph: "⟳", color: "yellow" };
|
|
71
|
+
return null;
|
|
53
72
|
}
|
|
54
73
|
export function buildIssueListRows(groups, flatIssues) {
|
|
55
74
|
const rows = [];
|
|
@@ -109,7 +128,7 @@ function readinessGlyph(di) {
|
|
|
109
128
|
return { glyph: "·", color: "gray" };
|
|
110
129
|
}
|
|
111
130
|
}
|
|
112
|
-
export default function IssueList({ groups, flatIssues, selectedIndex, scrollOffset, height, width, selectionBg = "#1e3a5f", variant = "default", deletingIds, }) {
|
|
131
|
+
export default function IssueList({ groups, flatIssues, selectedIndex, scrollOffset, height, width, selectionBg = "#1e3a5f", variant = "default", deletingIds, fixLoops, }) {
|
|
113
132
|
const isTriage = variant === "triage";
|
|
114
133
|
const isIssues = variant === "issues";
|
|
115
134
|
const rightFixed = isTriage ? SLA_COL_WIDTH : isIssues ? READY_COL_WIDTH : RIGHT_FIXED;
|
|
@@ -146,7 +165,7 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
|
|
|
146
165
|
const prio = snoozed ? { glyph: " ", color: "gray" } : priorityMarker(di.issue.priority);
|
|
147
166
|
const isDeleting = deletingIds?.has(di.issue.identifier) ?? false;
|
|
148
167
|
const work = isDeleting ? { glyph: "⌫", color: "yellow" } : workIndicator(di.worktree);
|
|
149
|
-
const ci = ciIndicator(di.checks);
|
|
168
|
+
const ci = fixLoopOverride(fixLoops?.[di.issue.identifier]) ?? ciIndicator(di.checks);
|
|
150
169
|
const nestPrefix = depth > 0 ? " ".repeat(depth - 1) + "└ " : "";
|
|
151
170
|
const adjustedTitleWidth = Math.max(titleMaxWidth - nestPrefix.length, 5);
|
|
152
171
|
const title = di.issue.title.length > adjustedTitleWidth
|
|
@@ -162,7 +162,7 @@ export function MultilineTextArea({ value, onChange, onSubmit, onCancel, placeho
|
|
|
162
162
|
onSubmit();
|
|
163
163
|
return;
|
|
164
164
|
}
|
|
165
|
-
// Ctrl+O: escalate to
|
|
165
|
+
// Ctrl+O: escalate to the configured editor / $VISUAL / $EDITOR. On save+close
|
|
166
166
|
// the buffer is replaced and control returns to the textbox so the
|
|
167
167
|
// user can keep editing or submit with Ctrl+D.
|
|
168
168
|
if (key.ctrl && input === "o") {
|
|
@@ -68,6 +68,8 @@ const LEGEND = [
|
|
|
68
68
|
{ glyph: "✗", color: "red", meaning: "CI column: a check is failing" },
|
|
69
69
|
{ glyph: "●", color: "yellow", meaning: "CI column: checks pending / running" },
|
|
70
70
|
{ glyph: "·", color: "gray", meaning: "CI column: no PR or no checks" },
|
|
71
|
+
{ glyph: "⟳", color: "cyan", meaning: "CI column: auto-fix loop running (every 5 min)" },
|
|
72
|
+
{ glyph: "⟳", color: "yellow", meaning: "CI column: fix loop stalled (no recent activity)" },
|
|
71
73
|
{ glyph: "◷", color: "red", meaning: "SLA column (Triage): breached / under a day" },
|
|
72
74
|
{ glyph: "◷", color: "yellow", meaning: "SLA column (Triage): under two days" },
|
|
73
75
|
{ glyph: "●", color: "gray", meaning: "Triage row greyed: snoozed (parked, sunk to bottom)" },
|
|
@@ -86,10 +88,7 @@ const LEGEND = [
|
|
|
86
88
|
{ glyph: "✓ clean", color: "green", meaning: "Working tree clean" },
|
|
87
89
|
{ glyph: "↑ N", color: "cyan", meaning: "N commits ahead of base" },
|
|
88
90
|
{ glyph: "↓ N behind", color: "yellow", meaning: "Main repo: N commits to pull from origin" },
|
|
89
|
-
{ glyph: "
|
|
90
|
-
{ glyph: "◆", color: "green", meaning: "Session active (Claude is working)" },
|
|
91
|
-
{ glyph: "◆", color: "yellow", meaning: "Session idle (waiting for prompt)" },
|
|
92
|
-
{ glyph: "◇", color: "cyan", meaning: "Session id stored, no live signal" },
|
|
91
|
+
{ glyph: "◇", color: "cyan", meaning: "Resumable Claude session id stored" },
|
|
93
92
|
{ glyph: "◇", color: "gray", meaning: "No session" },
|
|
94
93
|
],
|
|
95
94
|
},
|
|
@@ -223,16 +223,20 @@ export default function ReviewDetailPanel({ item, scrollOffset, height, width })
|
|
|
223
223
|
}
|
|
224
224
|
// ── Checks ────────────────────────────────────────────────────────
|
|
225
225
|
if (checks && checks.length > 0) {
|
|
226
|
+
// `skipping`/`cancel` checks don't run under the current conditions and
|
|
227
|
+
// don't block — GitHub counts them as "skipped" while still reporting all
|
|
228
|
+
// checks passed. Treat them as a non-blocking aside, never as pending.
|
|
226
229
|
const passing = checks.filter((c) => c.bucket === "pass");
|
|
227
230
|
const failing = checks.filter((c) => c.bucket === "fail");
|
|
228
|
-
const pending = checks.filter((c) => c.bucket
|
|
231
|
+
const pending = checks.filter((c) => c.bucket === "pending");
|
|
232
|
+
const skipped = checks.filter((c) => c.bucket === "skipping" || c.bucket === "cancel");
|
|
229
233
|
const headerColor = failing.length > 0 ? "red" : pending.length > 0 ? "yellow" : "green";
|
|
230
234
|
lines.push(ruleLine);
|
|
231
235
|
const headerSegs = [
|
|
232
236
|
{ text: "✓ ", color: "cyan", bold: true },
|
|
233
237
|
{ text: "Checks", bold: true },
|
|
234
238
|
{ text: " " },
|
|
235
|
-
{ text: `${passing.length}
|
|
239
|
+
{ text: `${passing.length} passed`, color: headerColor },
|
|
236
240
|
];
|
|
237
241
|
if (failing.length > 0) {
|
|
238
242
|
headerSegs.push({ text: " · ", dim: true });
|
|
@@ -242,8 +246,13 @@ export default function ReviewDetailPanel({ item, scrollOffset, height, width })
|
|
|
242
246
|
headerSegs.push({ text: " · ", dim: true });
|
|
243
247
|
headerSegs.push({ text: `${pending.length} pending`, color: "yellow" });
|
|
244
248
|
}
|
|
249
|
+
if (skipped.length > 0) {
|
|
250
|
+
headerSegs.push({ text: " · ", dim: true });
|
|
251
|
+
headerSegs.push({ text: `${skipped.length} skipped`, dim: true });
|
|
252
|
+
}
|
|
245
253
|
lines.push({ text: "", segments: headerSegs });
|
|
246
254
|
// Order: failing first (most important), then pending, then passing.
|
|
255
|
+
// Skipped checks aren't listed individually — just summarized above.
|
|
247
256
|
for (const check of failing) {
|
|
248
257
|
const desc = check.description ? ` — ${check.description}` : "";
|
|
249
258
|
lines.push({ text: ` ✗ ${check.name}${desc}`, color: "red" });
|
|
@@ -3,11 +3,14 @@ import { Box, Text } from "ink";
|
|
|
3
3
|
function checksIndicator(checks) {
|
|
4
4
|
if (!checks || checks.length === 0)
|
|
5
5
|
return { text: "-", color: "gray" };
|
|
6
|
+
// Only `fail` blocks (red) and only `pending` is genuinely running (yellow);
|
|
7
|
+
// `skipping`/`cancel` don't run under the current conditions and don't block,
|
|
8
|
+
// so they must not turn the glyph yellow (matches GitHub's "all checks passed").
|
|
6
9
|
if (checks.some((c) => c.bucket === "fail"))
|
|
7
10
|
return { text: "✗", color: "red" };
|
|
8
|
-
if (checks.
|
|
9
|
-
return { text: "
|
|
10
|
-
return { text: "
|
|
11
|
+
if (checks.some((c) => c.bucket === "pending"))
|
|
12
|
+
return { text: "●", color: "yellow" };
|
|
13
|
+
return { text: "✓", color: "green" };
|
|
11
14
|
}
|
|
12
15
|
const HEADER_ROWS = 1;
|
|
13
16
|
export function getReviewListRowCount(flatReviews) {
|