santree 0.7.5 → 0.7.7

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.
Files changed (109) hide show
  1. package/README.md +2 -3
  2. package/dist/commands/config.d.ts +12 -0
  3. package/dist/commands/config.js +444 -0
  4. package/dist/commands/dashboard.js +165 -61
  5. package/dist/commands/helpers/statusline.js +0 -30
  6. package/dist/commands/issue/index.d.ts +1 -1
  7. package/dist/commands/issue/index.js +1 -1
  8. package/dist/commands/pr/context.d.ts +7 -0
  9. package/dist/commands/pr/context.js +36 -0
  10. package/dist/commands/pr/fix.d.ts +10 -2
  11. package/dist/commands/pr/fix.js +61 -12
  12. package/dist/commands/worktree/create.js +6 -1
  13. package/dist/commands/worktree/open.js +3 -2
  14. package/dist/lib/ai.d.ts +11 -0
  15. package/dist/lib/ai.js +71 -2
  16. package/dist/lib/claude-config.d.ts +18 -4
  17. package/dist/lib/claude-config.js +58 -23
  18. package/dist/lib/config/TrackerPicker.d.ts +10 -0
  19. package/dist/lib/config/TrackerPicker.js +113 -0
  20. package/dist/lib/config/diagnostics.d.ts +37 -0
  21. package/dist/lib/config/diagnostics.js +190 -0
  22. package/dist/lib/config-store.d.ts +23 -0
  23. package/dist/lib/config-store.js +68 -0
  24. package/dist/lib/dashboard/DetailPanel.d.ts +4 -2
  25. package/dist/lib/dashboard/DetailPanel.js +44 -37
  26. package/dist/lib/dashboard/IssueList.d.ts +5 -1
  27. package/dist/lib/dashboard/IssueList.js +24 -5
  28. package/dist/lib/dashboard/MultilineTextArea.js +1 -1
  29. package/dist/lib/dashboard/Overlays.js +3 -4
  30. package/dist/lib/dashboard/ReviewDetailPanel.js +11 -2
  31. package/dist/lib/dashboard/ReviewList.js +6 -3
  32. package/dist/lib/dashboard/data.js +7 -34
  33. package/dist/lib/dashboard/external-editor.d.ts +1 -1
  34. package/dist/lib/dashboard/external-editor.js +3 -2
  35. package/dist/lib/dashboard/types.d.ts +13 -2
  36. package/dist/lib/dashboard/types.js +33 -2
  37. package/dist/lib/exec.d.ts +13 -0
  38. package/dist/lib/exec.js +11 -1
  39. package/dist/lib/fix-loop.d.ts +45 -0
  40. package/dist/lib/fix-loop.js +83 -0
  41. package/dist/lib/git.d.ts +3 -29
  42. package/dist/lib/git.js +6 -62
  43. package/dist/lib/github.d.ts +70 -0
  44. package/dist/lib/github.js +128 -0
  45. package/dist/lib/multiplexer/cmux.js +143 -17
  46. package/dist/lib/multiplexer/none.js +3 -3
  47. package/dist/lib/multiplexer/tmux.js +5 -29
  48. package/dist/lib/multiplexer/types.d.ts +34 -1
  49. package/dist/lib/open-url.d.ts +3 -2
  50. package/dist/lib/open-url.js +5 -4
  51. package/dist/lib/prompts.d.ts +35 -1
  52. package/dist/lib/prompts.js +7 -0
  53. package/dist/lib/setup/apply.d.ts +2 -2
  54. package/dist/lib/setup/apply.js +2 -2
  55. package/dist/lib/setup/gitignore.d.ts +7 -0
  56. package/dist/lib/setup/gitignore.js +39 -1
  57. package/dist/lib/setup/steps.d.ts +6 -2
  58. package/dist/lib/setup/steps.js +65 -60
  59. package/dist/lib/setup/tools.d.ts +1 -1
  60. package/dist/lib/setup/tools.js +1 -1
  61. package/dist/lib/trackers/github/index.js +2 -2
  62. package/dist/lib/trackers/local/store.js +1 -1
  63. package/package.json +1 -1
  64. package/prompts/fix-context.njk +79 -0
  65. package/prompts/fix-loop.njk +18 -0
  66. package/prompts/pr.njk +1 -1
  67. package/dist/commands/doctor.d.ts +0 -2
  68. package/dist/commands/doctor.js +0 -545
  69. package/dist/commands/github/auth.d.ts +0 -2
  70. package/dist/commands/github/auth.js +0 -56
  71. package/dist/commands/github/index.d.ts +0 -1
  72. package/dist/commands/github/index.js +0 -1
  73. package/dist/commands/helpers/english-tutor/index.d.ts +0 -1
  74. package/dist/commands/helpers/english-tutor/index.js +0 -1
  75. package/dist/commands/helpers/english-tutor/install.d.ts +0 -8
  76. package/dist/commands/helpers/english-tutor/install.js +0 -24
  77. package/dist/commands/helpers/english-tutor/prompt.d.ts +0 -2
  78. package/dist/commands/helpers/english-tutor/prompt.js +0 -16
  79. package/dist/commands/helpers/english-tutor/session-start.d.ts +0 -2
  80. package/dist/commands/helpers/english-tutor/session-start.js +0 -34
  81. package/dist/commands/helpers/english-tutor/uninstall.d.ts +0 -2
  82. package/dist/commands/helpers/english-tutor/uninstall.js +0 -15
  83. package/dist/commands/helpers/session-signal/end.d.ts +0 -2
  84. package/dist/commands/helpers/session-signal/end.js +0 -13
  85. package/dist/commands/helpers/session-signal/index.d.ts +0 -1
  86. package/dist/commands/helpers/session-signal/index.js +0 -1
  87. package/dist/commands/helpers/session-signal/install.d.ts +0 -8
  88. package/dist/commands/helpers/session-signal/install.js +0 -24
  89. package/dist/commands/helpers/session-signal/notification.d.ts +0 -2
  90. package/dist/commands/helpers/session-signal/notification.js +0 -13
  91. package/dist/commands/helpers/session-signal/prompt.d.ts +0 -2
  92. package/dist/commands/helpers/session-signal/prompt.js +0 -13
  93. package/dist/commands/helpers/session-signal/stop.d.ts +0 -2
  94. package/dist/commands/helpers/session-signal/stop.js +0 -13
  95. package/dist/commands/issue/setup.d.ts +0 -2
  96. package/dist/commands/issue/setup.js +0 -108
  97. package/dist/commands/issue/switch.d.ts +0 -12
  98. package/dist/commands/issue/switch.js +0 -38
  99. package/dist/commands/linear/switch.d.ts +0 -2
  100. package/dist/commands/linear/switch.js +0 -75
  101. package/dist/commands/setup.d.ts +0 -11
  102. package/dist/commands/setup.js +0 -194
  103. package/dist/lib/english-tutor.d.ts +0 -13
  104. package/dist/lib/english-tutor.js +0 -125
  105. package/dist/lib/session-signal.d.ts +0 -16
  106. package/dist/lib/session-signal.js +0 -104
  107. package/dist/lib/setup/shell-config.d.ts +0 -38
  108. package/dist/lib/setup/shell-config.js +0 -131
  109. package/prompts/english-tutor-prompt.njk +0 -15
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
  <p align="center">
18
18
  Pick an issue, work on it with Claude in an isolated worktree, ship a PR — without leaving your terminal.<br/>
19
- Pluggable issue trackers (Linear, GitHub Issues), pluggable multiplexers (tmux, cmux), and AI in the loop.
19
+ Pluggable issue trackers (Linear, GitHub Issues), pluggable multiplexers (cmux, tmux), and AI in the loop.
20
20
  </p>
21
21
 
22
22
  <p align="center">
@@ -31,8 +31,7 @@
31
31
 
32
32
  ```bash
33
33
  npm install -g santree
34
- eval "$(santree helpers shell-init zsh)" # or bash
35
- santree doctor
34
+ santree config # inspect & set up tools, Claude Code integration, and this repo
36
35
  ```
37
36
 
38
37
  Full setup: [Installation](https://santree.toscanini.me/installation.html).
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ export declare const description = "Inspect and configure santree \u2014 tools, Claude Code integration, and this repo";
3
+ export declare const options: z.ZodObject<{
4
+ check: z.ZodDefault<z.ZodBoolean>;
5
+ yes: z.ZodDefault<z.ZodBoolean>;
6
+ dryRun: z.ZodDefault<z.ZodBoolean>;
7
+ }, z.core.$strip>;
8
+ type Props = {
9
+ options: z.infer<typeof options>;
10
+ };
11
+ export default function Config({ options: opts }: Props): import("react/jsx-runtime").JSX.Element;
12
+ export {};
@@ -0,0 +1,444 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import Spinner from "ink-spinner";
5
+ import { z } from "zod";
6
+ import { buildContext, buildSteps, } from "../lib/setup/steps.js";
7
+ import { loadDiagnostics } from "../lib/config/diagnostics.js";
8
+ import { getConfiguredEditor, getConfiguredDiffTool } from "../lib/config-store.js";
9
+ import { pruneSessionSignalHooks } from "../lib/claude-config.js";
10
+ import TrackerPicker from "../lib/config/TrackerPicker.js";
11
+ import { findMainRepoRoot } from "../lib/git.js";
12
+ import { getActiveTrackerKind, isRepoTrackerConfigured } from "../lib/trackers/index.js";
13
+ import { getRepoLinearOrg } from "../lib/trackers/linear/index.js";
14
+ export const description = "Inspect and configure santree — tools, Claude Code integration, and this repo";
15
+ export const options = z.object({
16
+ check: z
17
+ .boolean()
18
+ .default(false)
19
+ .describe("Print a read-only status report and exit (non-interactive)"),
20
+ yes: z.boolean().default(false).describe("Apply all recommended changes non-interactively"),
21
+ dryRun: z.boolean().default(false).describe("Preview changes without writing anything"),
22
+ });
23
+ // Presentation scope — overrides the catalog's coarse global/repo so install
24
+ // tools land under "System" next to the read-only version rows.
25
+ const SYSTEM_STEP_IDS = new Set(["claude", "gh", "tmux"]);
26
+ const REPO_STEP_IDS = new Set(["tracker", "gitignore", "scaffold"]);
27
+ function presScope(id) {
28
+ if (SYSTEM_STEP_IDS.has(id))
29
+ return "system";
30
+ if (REPO_STEP_IDS.has(id))
31
+ return "repo";
32
+ return "global";
33
+ }
34
+ const ROW_ORDER = [
35
+ // System
36
+ "santree",
37
+ "node",
38
+ "git",
39
+ "multiplexer",
40
+ "claude",
41
+ "gh",
42
+ "tmux",
43
+ "workspace-editor",
44
+ // Global
45
+ "editor",
46
+ "diff-tool",
47
+ "statusline",
48
+ "remote-control",
49
+ // This repo
50
+ "tracker",
51
+ "gitignore",
52
+ "scaffold",
53
+ ];
54
+ const SCOPE_TITLES = {
55
+ system: "System",
56
+ global: "Global (editor & Claude Code)",
57
+ repo: "This repo",
58
+ };
59
+ /** Current resolved value for the value-bearing select rows (env override → config file). */
60
+ function currentConfigValue(id) {
61
+ if (id === "editor")
62
+ return getConfiguredEditor();
63
+ if (id === "diff-tool")
64
+ return getConfiguredDiffTool();
65
+ return undefined;
66
+ }
67
+ function infoToRow(info) {
68
+ return {
69
+ id: info.id,
70
+ kind: "info",
71
+ title: info.title,
72
+ detail: info.description,
73
+ scope: info.scope,
74
+ ok: info.ok,
75
+ required: info.required,
76
+ recommended: false,
77
+ lines: info.lines,
78
+ hint: info.hint,
79
+ value: info.lines[0] ? compact(info.lines[0]) : undefined,
80
+ };
81
+ }
82
+ function stepToRow(s, detail) {
83
+ const ok = s.detect === "ok";
84
+ let kind;
85
+ if (s.id === "tracker")
86
+ kind = "tracker";
87
+ else if (s.unapply)
88
+ kind = "toggle";
89
+ else if (s.options && s.options.length)
90
+ kind = "select";
91
+ else
92
+ kind = ok ? "info" : "action";
93
+ const row = {
94
+ id: s.id,
95
+ kind,
96
+ title: s.title,
97
+ detail: s.detail,
98
+ scope: presScope(s.id),
99
+ ok,
100
+ required: s.id === "gh" || s.id === "claude",
101
+ recommended: s.recommended,
102
+ on: s.unapply ? ok : undefined,
103
+ lines: detail?.lines ?? [],
104
+ hint: detail?.hint,
105
+ step: s,
106
+ };
107
+ if (kind === "select")
108
+ row.value = currentConfigValue(s.id) || (ok ? "set" : "not set");
109
+ if (kind === "tracker")
110
+ row.value = ok ? compact(detail?.lines?.[0] || "") : "not configured";
111
+ if (kind === "info" && detail?.lines?.[0])
112
+ row.value = compact(detail.lines[0]);
113
+ return row;
114
+ }
115
+ /** "Label: value" → "value"; otherwise the line unchanged. */
116
+ function compact(line) {
117
+ const idx = line.indexOf(": ");
118
+ return idx >= 0 ? line.slice(idx + 2) : line;
119
+ }
120
+ function buildRows(ctx, diag) {
121
+ const rows = [];
122
+ const steps = buildSteps(ctx).filter((s) => s.detect !== "unavailable");
123
+ for (const s of steps) {
124
+ if (s.id === "tmux" && diag.muxActive)
125
+ continue; // shown as the multiplexer info row
126
+ rows.push(stepToRow(s, diag.stepDetail.get(s.id)));
127
+ }
128
+ for (const info of diag.infoRows)
129
+ rows.push(infoToRow(info));
130
+ rows.sort((a, b) => {
131
+ const ia = ROW_ORDER.indexOf(a.id);
132
+ const ib = ROW_ORDER.indexOf(b.id);
133
+ return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib);
134
+ });
135
+ return rows;
136
+ }
137
+ function rowIcon(row) {
138
+ const on = row.kind === "toggle" ? !!row.on : row.ok;
139
+ if (on)
140
+ return { char: "✓", color: "green" };
141
+ return row.required ? { char: "✗", color: "red" } : { char: "○", color: "yellow" };
142
+ }
143
+ function rowSummary(row) {
144
+ switch (row.kind) {
145
+ case "toggle":
146
+ return row.on ? "on" : "off";
147
+ case "select":
148
+ return row.value || "";
149
+ case "action":
150
+ return row.required ? "missing" : "not installed";
151
+ case "tracker":
152
+ return row.value || "";
153
+ case "info":
154
+ return row.ok ? row.value || "✓" : "—";
155
+ }
156
+ }
157
+ /** Truncate to a visible width with an ellipsis. Inputs are plain (no ANSI). */
158
+ function clamp(s, w) {
159
+ if (w <= 0)
160
+ return "";
161
+ return s.length <= w ? s : s.slice(0, w - 1) + "…";
162
+ }
163
+ /** Per-row action hint shown in the footer when that row is focused. */
164
+ function footerVerb(row) {
165
+ switch (row?.kind) {
166
+ case "toggle":
167
+ return "Space toggle";
168
+ case "select":
169
+ return "Enter change";
170
+ case "action":
171
+ return "Enter run";
172
+ case "tracker":
173
+ return "Enter choose";
174
+ default:
175
+ return "read-only";
176
+ }
177
+ }
178
+ const PANEL_WIDTH = Math.max(40, Math.min((process.stdout.columns || 80) - 2, 96));
179
+ export default function Config({ options: opts }) {
180
+ const dryRun = opts.dryRun;
181
+ const ctxRef = useRef(null);
182
+ const diagRef = useRef(null);
183
+ const [mode, setMode] = useState("loading");
184
+ const [rows, setRows] = useState([]);
185
+ const [cursor, setCursor] = useState(0);
186
+ const [submenu, setSubmenu] = useState(null);
187
+ const [busy, setBusy] = useState(null);
188
+ const [toast, setToast] = useState(null);
189
+ // repo context for the tracker picker
190
+ const [repoRoot, setRepoRoot] = useState(null);
191
+ const [activeKind, setActiveKind] = useState(null);
192
+ const [activeOrg, setActiveOrg] = useState(null);
193
+ // Every row is focusable so you can inspect read-only rows too; Space/Enter
194
+ // just no-op on `info` rows. This keeps the cursor's travel predictable and
195
+ // lets the detail panel show full info for whatever's highlighted.
196
+ const selectable = rows;
197
+ const current = selectable[cursor];
198
+ // ── Initial load ────────────────────────────────────────────────────────────
199
+ useEffect(() => {
200
+ (async () => {
201
+ await new Promise((r) => setTimeout(r, 80));
202
+ const ctx = buildContext(dryRun);
203
+ ctxRef.current = ctx;
204
+ const diag = await loadDiagnostics();
205
+ diagRef.current = diag;
206
+ const built = buildRows(ctx, diag);
207
+ const root = findMainRepoRoot();
208
+ setRepoRoot(root);
209
+ if (root && isRepoTrackerConfigured(root)) {
210
+ const kind = getActiveTrackerKind(root);
211
+ setActiveKind(kind);
212
+ if (kind === "linear")
213
+ setActiveOrg(getRepoLinearOrg(root));
214
+ }
215
+ if (opts.check) {
216
+ setRows(built);
217
+ setMode("list"); // rendered read-only; effect below exits
218
+ return;
219
+ }
220
+ // Self-heal: the session-state feature was removed, so strip any
221
+ // leftover session-signal hooks that would otherwise fire a missing
222
+ // command on every Claude event. Skipped in --check (read-only).
223
+ const prunedHooks = dryRun ? 0 : pruneSessionSignalHooks();
224
+ if (opts.yes) {
225
+ await applyRecommended(ctx, built);
226
+ return;
227
+ }
228
+ setRows(built);
229
+ if (prunedHooks > 0) {
230
+ setToast({
231
+ ok: true,
232
+ message: `Removed ${prunedHooks} stale session-signal hook${prunedHooks === 1 ? "" : "s"} from Claude Code settings`,
233
+ });
234
+ }
235
+ setMode("list");
236
+ })();
237
+ }, []);
238
+ // ── --check / --yes exit ─────────────────────────────────────────────────────
239
+ useEffect(() => {
240
+ if (opts.check && mode === "list" && rows.length > 0) {
241
+ const t = setTimeout(() => {
242
+ const requiredMissing = rows.some((r) => r.required && !r.ok);
243
+ process.exit(requiredMissing ? 1 : 0);
244
+ }, 80);
245
+ return () => clearTimeout(t);
246
+ }
247
+ }, [mode, rows, opts.check]);
248
+ async function applyRecommended(ctx, built) {
249
+ // Non-interactive: apply recommended, reversible/file-kind steps only.
250
+ // Spawn installs and the tracker picker need a human, so we skip them.
251
+ const results = [];
252
+ for (const row of built) {
253
+ const s = row.step;
254
+ if (!s)
255
+ continue;
256
+ if (s.id === "tracker")
257
+ continue;
258
+ if (s.kind === "spawn")
259
+ continue;
260
+ if (s.detect !== "actionable" || !s.recommended)
261
+ continue;
262
+ const choice = s.options?.[0]?.value;
263
+ try {
264
+ results.push({ title: s.title, result: await s.apply(choice) });
265
+ }
266
+ catch (e) {
267
+ results.push({
268
+ title: s.title,
269
+ result: { ok: false, message: e instanceof Error ? e.message : String(e) },
270
+ });
271
+ }
272
+ }
273
+ setRows(results.map((r) => ({
274
+ id: r.title,
275
+ kind: "info",
276
+ title: r.title,
277
+ detail: "",
278
+ scope: "global",
279
+ ok: r.result.ok,
280
+ required: false,
281
+ recommended: false,
282
+ lines: [r.result.message],
283
+ })));
284
+ setMode("list");
285
+ setTimeout(() => process.exit(0), 100);
286
+ }
287
+ function rebuildRows() {
288
+ const ctx = ctxRef.current;
289
+ const diag = diagRef.current;
290
+ setRows(buildRows(ctx, diag));
291
+ }
292
+ async function applyStep(step, action, choice) {
293
+ setBusy(`${action === "unapply" ? "Disabling" : "Applying"} ${step.title}…`);
294
+ setToast(null);
295
+ await new Promise((r) => setTimeout(r, 50));
296
+ let result;
297
+ try {
298
+ const fn = action === "unapply" ? step.unapply : step.apply;
299
+ result = await fn(choice);
300
+ }
301
+ catch (e) {
302
+ result = { ok: false, message: e instanceof Error ? e.message : String(e) };
303
+ }
304
+ // Spawn steps may have installed a tool — refresh version diagnostics.
305
+ if (step.kind === "spawn" || choice?.startsWith("install:")) {
306
+ diagRef.current = await loadDiagnostics();
307
+ }
308
+ rebuildRows();
309
+ setToast({ ok: result.ok, message: result.message });
310
+ setBusy(null);
311
+ }
312
+ function activate(row) {
313
+ if (row.kind === "tracker") {
314
+ setMode("tracker");
315
+ return;
316
+ }
317
+ if (row.kind === "toggle") {
318
+ toggleRow(row);
319
+ return;
320
+ }
321
+ if (row.kind === "select") {
322
+ const opts2 = row.step?.options ?? [];
323
+ if (opts2.length <= 1) {
324
+ void applyStep(row.step, "apply", opts2[0]?.value);
325
+ }
326
+ else {
327
+ setSubmenu({ row, cursor: 0 });
328
+ setMode("submenu");
329
+ }
330
+ return;
331
+ }
332
+ if (row.kind === "action") {
333
+ void applyStep(row.step, "apply", row.step?.options?.[0]?.value);
334
+ }
335
+ }
336
+ function toggleRow(row) {
337
+ const step = row.step;
338
+ if (row.on) {
339
+ void applyStep(step, "unapply", undefined);
340
+ }
341
+ else if (step.options && step.options.length > 1) {
342
+ // Turning on a step that needs a choice (e.g. gitignore target).
343
+ setSubmenu({ row, cursor: 0 });
344
+ setMode("submenu");
345
+ }
346
+ else {
347
+ void applyStep(step, "apply", step.options?.[0]?.value);
348
+ }
349
+ }
350
+ // ── List input ────────────────────────────────────────────────────────────────
351
+ useInput((_input, key) => {
352
+ if (key.escape || key.leftArrow)
353
+ process.exit(0);
354
+ else if (key.upArrow) {
355
+ setToast(null);
356
+ setCursor((c) => Math.max(0, c - 1));
357
+ }
358
+ else if (key.downArrow) {
359
+ setToast(null);
360
+ setCursor((c) => Math.min(selectable.length - 1, c + 1));
361
+ }
362
+ else if (_input === " ") {
363
+ if (current?.kind === "toggle")
364
+ toggleRow(current);
365
+ }
366
+ else if (key.return || key.rightArrow) {
367
+ if (current)
368
+ activate(current);
369
+ }
370
+ }, { isActive: mode === "list" && !busy && !opts.check && !opts.yes });
371
+ // ── Submenu input ───────────────────────────────────────────────────────────
372
+ useInput((_input, key) => {
373
+ if (!submenu)
374
+ return;
375
+ const opts2 = submenu.row.step.options;
376
+ if (key.escape || key.leftArrow) {
377
+ setSubmenu(null);
378
+ setMode("list");
379
+ }
380
+ else if (key.upArrow) {
381
+ setSubmenu((s) => (s ? { ...s, cursor: Math.max(0, s.cursor - 1) } : s));
382
+ }
383
+ else if (key.downArrow) {
384
+ setSubmenu((s) => (s ? { ...s, cursor: Math.min(opts2.length - 1, s.cursor + 1) } : s));
385
+ }
386
+ else if (key.return) {
387
+ const choice = opts2[submenu.cursor].value;
388
+ const step = submenu.row.step;
389
+ setSubmenu(null);
390
+ setMode("list");
391
+ void applyStep(step, "apply", choice);
392
+ }
393
+ }, { isActive: mode === "submenu" && !busy });
394
+ // ── Render ──────────────────────────────────────────────────────────────────
395
+ const title = (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "santree config" }), dryRun && _jsx(Text, { color: "yellow", children: " (dry run \u2014 nothing will be written)" }), opts.check && _jsx(Text, { dimColor: true, children: " \u2014 status report" })] }));
396
+ if (mode === "loading") {
397
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Inspecting your setup\u2026" })] })] }));
398
+ }
399
+ if (opts.yes) {
400
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, rows.length === 0 ? (_jsx(Text, { color: "green", children: "Nothing to apply \u2014 your setup already matches." })) : (rows.map((r) => (_jsxs(Text, { color: r.ok ? "green" : "red", children: [r.ok ? "✓" : "✗", " ", r.title, " ", _jsxs(Text, { dimColor: true, children: ["\u2014 ", r.lines[0]] })] }, r.id))))] }));
401
+ }
402
+ if (mode === "tracker") {
403
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, _jsx(TrackerPicker, { repoRoot: repoRoot, activeKind: activeKind, activeOrg: activeOrg, onApplied: (message) => {
404
+ const root = repoRoot;
405
+ setActiveKind(getActiveTrackerKind(root));
406
+ setActiveOrg(getActiveTrackerKind(root) === "linear" ? getRepoLinearOrg(root) : null);
407
+ (async () => {
408
+ diagRef.current = await loadDiagnostics();
409
+ rebuildRows();
410
+ })();
411
+ setToast({ ok: true, message });
412
+ setMode("list");
413
+ }, onCancel: () => setMode("list") })] }));
414
+ }
415
+ if (mode === "submenu" && submenu) {
416
+ const step = submenu.row.step;
417
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, _jsx(Text, { bold: true, children: step.title }), _jsx(Text, { dimColor: true, children: step.optionPrompt || "Pick one:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: step.options.map((o, i) => (_jsxs(Text, { children: [_jsx(Text, { color: i === submenu.cursor ? "cyan" : undefined, bold: i === submenu.cursor, children: i === submenu.cursor ? "> " : " " }), o.label] }, o.value))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 select \u00B7 Enter confirm \u00B7 \u2190 / Esc back" }) })] }));
418
+ }
419
+ // ── --check: flat, read-only report (no cursor, every row expanded) ─────────
420
+ if (opts.check) {
421
+ let lastScope = null;
422
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, rows.map((row) => {
423
+ const head = row.scope !== lastScope;
424
+ lastScope = row.scope;
425
+ const icon = rowIcon(row);
426
+ return (_jsxs(Box, { flexDirection: "column", children: [head && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, underline: true, children: SCOPE_TITLES[row.scope] }) })), _jsxs(Text, { children: [_jsx(Text, { color: icon.color, children: icon.char }), " ", _jsx(Text, { bold: true, children: row.title }), _jsxs(Text, { dimColor: true, children: [" ", rowSummary(row)] })] }), _jsxs(Box, { marginLeft: 4, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: row.detail }), row.lines.map((l, i) => (_jsx(Text, { dimColor: true, children: l }, i))), row.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", row.hint] })] })] }, row.id));
427
+ })] }));
428
+ }
429
+ // ── Interactive panel ───────────────────────────────────────────────────────
430
+ // A stable, single-line-per-row list (it never reflows on cursor moves) with
431
+ // the focused row's detail rendered in a fixed-height panel below. Highlight
432
+ // is a colored caret + accent label — no inline expand/collapse.
433
+ const labelW = Math.min(22, rows.reduce((m, r) => Math.max(m, r.title.length), 0));
434
+ const statusLine = current ? current.lines.join(" · ") : "";
435
+ let lastScope = null;
436
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, rows.map((row) => {
437
+ const head = row.scope !== lastScope;
438
+ lastScope = row.scope;
439
+ const focused = row === current;
440
+ const icon = rowIcon(row);
441
+ const isInfo = row.kind === "info";
442
+ return (_jsxs(Box, { flexDirection: "column", children: [head && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: "cyan", children: SCOPE_TITLES[row.scope] }) })), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: focused ? "❯ " : " " }), _jsx(Text, { color: icon.color, children: icon.char }), " ", _jsx(Text, { bold: !isInfo, color: focused ? "cyan" : undefined, children: row.title.padEnd(labelW) }), _jsxs(Text, { dimColor: true, children: [" ", rowSummary(row)] })] })] }, row.id));
443
+ }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "─".repeat(PANEL_WIDTH) }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: current ? current.title : "" }), _jsx(Text, { dimColor: true, children: current ? " " + clamp(current.detail, PANEL_WIDTH - current.title.length - 2) : "" })] }), _jsx(Text, { dimColor: true, children: statusLine ? clamp(statusLine, PANEL_WIDTH) : " " }), current?.hint ? (_jsx(Text, { color: "yellow", children: clamp("↳ " + current.hint, PANEL_WIDTH) })) : (_jsx(Text, { children: " " }))] }), _jsx(Box, { marginTop: 1, children: busy ? (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", busy] })) : toast ? (_jsxs(Text, { color: toast.ok ? "green" : "red", children: [toast.ok ? "✓" : "✗", " ", toast.message] })) : (_jsxs(Text, { dimColor: true, children: ["\u2191/\u2193 move \u00B7 ", footerVerb(current), " \u00B7 \u2190/Esc quit"] })) })] }));
444
+ }