santree 0.2.0 → 0.2.1

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.
@@ -4,8 +4,11 @@ import { Text, Box, useInput, useStdout, useApp } from "ink";
4
4
  import Spinner from "ink-spinner";
5
5
  import { exec, execSync, spawn } from "child_process";
6
6
  import { promisify } from "util";
7
+ import { createRequire } from "module";
7
8
  import * as fs from "fs";
8
9
  import * as path from "path";
10
+ const require = createRequire(import.meta.url);
11
+ const { version } = require("../../package.json");
9
12
  import { findMainRepoRoot, createWorktree, getDefaultBranch, getBaseBranch, hasInitScript, getInitScriptPath, removeWorktree, } from "../lib/git.js";
10
13
  import { spawnAsync } from "../lib/exec.js";
11
14
  import { resolveAgentBinary } from "../lib/ai.js";
@@ -32,12 +35,15 @@ function getRowIndexForFlatIndex(groups, flatIndex) {
32
35
  let row = 1; // skip column header row
33
36
  let issuesSeen = 0;
34
37
  for (const g of groups) {
35
- row++; // group header
36
- for (let i = 0; i < g.issues.length; i++) {
37
- if (issuesSeen === flatIndex)
38
- return row;
39
- row++;
40
- issuesSeen++;
38
+ row++; // project header
39
+ for (const sg of g.statusGroups) {
40
+ row++; // status header
41
+ for (let i = 0; i < sg.issues.length; i++) {
42
+ if (issuesSeen === flatIndex)
43
+ return row;
44
+ row++;
45
+ issuesSeen++;
46
+ }
41
47
  }
42
48
  }
43
49
  return 0;
@@ -49,13 +55,18 @@ function getFlatIndexForListRow(groups, listRow) {
49
55
  let issuesSeen = 0;
50
56
  for (const g of groups) {
51
57
  if (row === listRow)
52
- return null; // group header row
58
+ return null; // project header row
53
59
  row++;
54
- for (let i = 0; i < g.issues.length; i++) {
60
+ for (const sg of g.statusGroups) {
55
61
  if (row === listRow)
56
- return issuesSeen;
62
+ return null; // status header row
57
63
  row++;
58
- issuesSeen++;
64
+ for (let i = 0; i < sg.issues.length; i++) {
65
+ if (row === listRow)
66
+ return issuesSeen;
67
+ row++;
68
+ issuesSeen++;
69
+ }
59
70
  }
60
71
  }
61
72
  return null;
@@ -287,33 +298,30 @@ export default function Dashboard() {
287
298
  try {
288
299
  // Switch to existing window if it exists
289
300
  execSync(`tmux select-window -t "${windowName}"`, { stdio: "ignore" });
290
- if (resumeCmd) {
291
- execSync(`tmux send-keys -t "${windowName}" "${resumeCmd}" Enter`, { stdio: "ignore" });
292
- dispatch({ type: "SET_ACTION_MESSAGE", message: `Resumed session in: ${windowName}` });
293
- }
294
- else {
295
- execSync(`tmux send-keys -t "${windowName}" "${workCmd}" Enter`, { stdio: "ignore" });
296
- dispatch({ type: "SET_ACTION_MESSAGE", message: `Launched ${mode} in: ${windowName}` });
297
- }
301
+ const cmd = resumeCmd ?? workCmd;
302
+ execSync(`tmux send-keys -t "${windowName}" "${cmd}" Enter`, { stdio: "ignore" });
303
+ dispatch({
304
+ type: "SET_ACTION_MESSAGE",
305
+ message: resumeCmd
306
+ ? `Resumed session in: ${windowName}`
307
+ : `Launched ${mode} in: ${windowName}`,
308
+ });
298
309
  }
299
310
  catch {
300
311
  // Window doesn't exist — create it
301
312
  try {
302
313
  execSync(`tmux new-window -n "${windowName}" -c "${worktreePath}"`, { stdio: "ignore" });
303
- if (resumeCmd) {
304
- execSync(`tmux send-keys -t "${windowName}" "${resumeCmd}" Enter`, { stdio: "ignore" });
305
- dispatch({
306
- type: "SET_ACTION_MESSAGE",
307
- message: `Resumed session in new window: ${windowName}`,
308
- });
309
- }
310
- else {
311
- execSync(`tmux send-keys -t "${windowName}" "${workCmd}" Enter`, { stdio: "ignore" });
312
- dispatch({
313
- type: "SET_ACTION_MESSAGE",
314
- message: `Launched ${mode} in tmux window: ${windowName}`,
315
- });
316
- }
314
+ // Small delay so the new shell can start reading input before we send keys,
315
+ // otherwise buffered keystrokes from the dashboard pane can leak in.
316
+ execSync("sleep 0.1", { stdio: "ignore" });
317
+ const cmd = resumeCmd ?? workCmd;
318
+ execSync(`tmux send-keys -t "${windowName}" "${cmd}" Enter`, { stdio: "ignore" });
319
+ dispatch({
320
+ type: "SET_ACTION_MESSAGE",
321
+ message: resumeCmd
322
+ ? `Resumed session in new window: ${windowName}`
323
+ : `Launched ${mode} in tmux window: ${windowName}`,
324
+ });
317
325
  }
318
326
  catch {
319
327
  dispatch({ type: "SET_ACTION_MESSAGE", message: "Failed to create tmux window" });
@@ -322,131 +330,148 @@ export default function Dashboard() {
322
330
  // Delayed refresh to pick up session ID created by `st worktree work`
323
331
  setTimeout(() => refresh(), 3000);
324
332
  }, [refresh]);
325
- const doWork = useCallback(async (mode) => {
326
- const di = state.flatIssues[state.selectedIndex];
333
+ const launchAfterCreation = useCallback((mode, worktreePath, ticketId) => {
334
+ if (isInTmux()) {
335
+ const windowName = ticketId;
336
+ const workCmd = mode === "plan" ? "st worktree work --plan" : "st worktree work";
337
+ try {
338
+ execSync(`tmux new-window -n "${windowName}" -c "${worktreePath}"`, { stdio: "ignore" });
339
+ execSync("sleep 0.1", { stdio: "ignore" });
340
+ execSync(`tmux send-keys -t "${windowName}" "${workCmd}" Enter`, { stdio: "ignore" });
341
+ dispatch({
342
+ type: "SET_ACTION_MESSAGE",
343
+ message: `Created worktree + launched ${mode} in: ${windowName}`,
344
+ });
345
+ }
346
+ catch {
347
+ dispatch({ type: "SET_ACTION_MESSAGE", message: "Worktree created, but tmux failed" });
348
+ }
349
+ setTimeout(() => refresh(), 3000);
350
+ }
351
+ else {
352
+ leaveAltScreen();
353
+ console.log(`SANTREE_CD:${worktreePath}`);
354
+ console.log(`SANTREE_WORK:${mode}`);
355
+ exit();
356
+ }
357
+ }, [exit, refresh]);
358
+ const createAndLaunch = useCallback(async (mode, runSetup) => {
359
+ const di = stateRef.current.flatIssues[stateRef.current.selectedIndex];
327
360
  if (!di)
328
361
  return;
329
362
  const repoRoot = repoRootRef.current;
330
363
  if (!repoRoot)
331
364
  return;
332
- dispatch({ type: "SET_OVERLAY", overlay: null });
333
- if (di.worktree) {
334
- // Worktree exists — launch work
335
- if (isInTmux()) {
336
- launchWorkInTmux(di, mode, di.worktree.path);
337
- }
338
- else {
339
- leaveAltScreen();
340
- console.log(`SANTREE_CD:${di.worktree.path}`);
341
- console.log(`SANTREE_WORK:${mode}`);
342
- exit();
343
- }
365
+ // Guard against concurrent creation
366
+ if (stateRef.current.creatingForTicket)
367
+ return;
368
+ const ticketId = di.issue.identifier;
369
+ dispatch({ type: "CREATION_START", ticketId });
370
+ const slug = slugify(di.issue.title);
371
+ const branchName = `feature/${ticketId}-${slug}`;
372
+ const base = getDefaultBranch();
373
+ // 1. Pull latest (async to avoid blocking the event loop)
374
+ dispatch({ type: "CREATION_LOG", logs: `Fetching origin...\n` });
375
+ try {
376
+ await execAsync("git fetch origin", { cwd: repoRoot });
377
+ dispatch({ type: "CREATION_LOG", logs: `Checking out ${base}...\n` });
378
+ await execAsync(`git checkout ${base}`, { cwd: repoRoot });
379
+ dispatch({ type: "CREATION_LOG", logs: `Pulling ${base}...\n` });
380
+ await execAsync(`git pull origin ${base}`, { cwd: repoRoot });
381
+ dispatch({ type: "CREATION_LOG", logs: `Pulled latest ${base}\n` });
344
382
  }
345
- else {
346
- // No worktree full creation flow (pull + create + init script)
347
- // Guard against concurrent creation
348
- if (stateRef.current.creatingForTicket)
349
- return;
350
- const ticketId = di.issue.identifier;
351
- dispatch({ type: "CREATION_START", ticketId });
352
- const slug = slugify(di.issue.title);
353
- const branchName = `feature/${ticketId}-${slug}`;
354
- const base = getDefaultBranch();
355
- // 1. Pull latest (async to avoid blocking the event loop)
356
- dispatch({ type: "CREATION_LOG", logs: `Fetching origin...\n` });
383
+ catch (e) {
384
+ const msg = e instanceof Error ? e.message : "Failed to pull latest";
385
+ dispatch({ type: "CREATION_LOG", logs: `Warning: ${msg}\n` });
386
+ }
387
+ // 2. Create worktree
388
+ dispatch({ type: "CREATION_LOG", logs: `Creating worktree ${branchName}...\n` });
389
+ const result = await createWorktree(branchName, base, repoRoot);
390
+ if (!result.success || !result.path) {
391
+ dispatch({ type: "CREATION_ERROR", error: result.error ?? "Unknown error" });
392
+ dispatch({
393
+ type: "SET_ACTION_MESSAGE",
394
+ message: `Failed: ${result.error ?? "Unknown error"}`,
395
+ });
396
+ return;
397
+ }
398
+ dispatch({ type: "CREATION_LOG", logs: `Worktree created at ${result.path}\n` });
399
+ // 3. Run init script if requested
400
+ if (runSetup) {
401
+ const initScript = getInitScriptPath(repoRoot);
402
+ let canExecute = true;
357
403
  try {
358
- await execAsync("git fetch origin", { cwd: repoRoot });
359
- dispatch({ type: "CREATION_LOG", logs: `Checking out ${base}...\n` });
360
- await execAsync(`git checkout ${base}`, { cwd: repoRoot });
361
- dispatch({ type: "CREATION_LOG", logs: `Pulling ${base}...\n` });
362
- await execAsync(`git pull origin ${base}`, { cwd: repoRoot });
363
- dispatch({ type: "CREATION_LOG", logs: `Pulled latest ${base}\n` });
364
- }
365
- catch (e) {
366
- const msg = e instanceof Error ? e.message : "Failed to pull latest";
367
- dispatch({ type: "CREATION_LOG", logs: `Warning: ${msg}\n` });
368
- }
369
- // 2. Create worktree
370
- dispatch({ type: "CREATION_LOG", logs: `Creating worktree ${branchName}...\n` });
371
- const result = await createWorktree(branchName, base, repoRoot);
372
- if (!result.success || !result.path) {
373
- dispatch({ type: "CREATION_ERROR", error: result.error ?? "Unknown error" });
404
+ fs.accessSync(initScript, fs.constants.X_OK);
405
+ }
406
+ catch {
374
407
  dispatch({
375
- type: "SET_ACTION_MESSAGE",
376
- message: `Failed: ${result.error ?? "Unknown error"}`,
408
+ type: "CREATION_LOG",
409
+ logs: "Warning: init.sh exists but is not executable, skipping\n",
377
410
  });
378
- return;
379
- }
380
- dispatch({ type: "CREATION_LOG", logs: `Worktree created at ${result.path}\n` });
381
- // 3. Run init script if it exists
382
- if (hasInitScript(repoRoot)) {
383
- const initScript = getInitScriptPath(repoRoot);
384
- let canExecute = true;
385
- try {
386
- fs.accessSync(initScript, fs.constants.X_OK);
387
- }
388
- catch {
411
+ canExecute = false;
412
+ }
413
+ if (canExecute) {
414
+ dispatch({ type: "CREATION_LOG", logs: "Running init.sh...\n" });
415
+ let lastLen = 0;
416
+ const initResult = await spawnAsync(initScript, [], {
417
+ cwd: result.path,
418
+ env: {
419
+ ...process.env,
420
+ SANTREE_WORKTREE_PATH: result.path,
421
+ SANTREE_REPO_ROOT: repoRoot,
422
+ },
423
+ onOutput: (output) => {
424
+ const delta = output.slice(lastLen);
425
+ if (delta)
426
+ dispatch({ type: "CREATION_LOG", logs: delta });
427
+ lastLen = output.length;
428
+ },
429
+ });
430
+ if (initResult.code !== 0) {
389
431
  dispatch({
390
432
  type: "CREATION_LOG",
391
- logs: "Warning: init.sh exists but is not executable, skipping\n",
433
+ logs: `\nInit script exited with code ${initResult.code}\n`,
392
434
  });
393
- canExecute = false;
394
435
  }
395
- if (canExecute) {
396
- dispatch({ type: "CREATION_LOG", logs: "Running init.sh...\n" });
397
- let lastLen = 0;
398
- const initResult = await spawnAsync(initScript, [], {
399
- cwd: result.path,
400
- env: {
401
- ...process.env,
402
- SANTREE_WORKTREE_PATH: result.path,
403
- SANTREE_REPO_ROOT: repoRoot,
404
- },
405
- onOutput: (output) => {
406
- const delta = output.slice(lastLen);
407
- if (delta)
408
- dispatch({ type: "CREATION_LOG", logs: delta });
409
- lastLen = output.length;
410
- },
411
- });
412
- if (initResult.code !== 0) {
413
- dispatch({
414
- type: "CREATION_LOG",
415
- logs: `\nInit script exited with code ${initResult.code}\n`,
416
- });
417
- }
418
- else {
419
- dispatch({ type: "CREATION_LOG", logs: "\nSetup complete!\n" });
420
- }
436
+ else {
437
+ dispatch({ type: "CREATION_LOG", logs: "\nSetup complete!\n" });
421
438
  }
422
439
  }
423
- // 4. Done — launch work
424
- dispatch({ type: "CREATION_DONE" });
440
+ }
441
+ // 4. Done — launch work
442
+ dispatch({ type: "CREATION_DONE" });
443
+ launchAfterCreation(mode, result.path, ticketId);
444
+ }, [launchAfterCreation]);
445
+ const doWork = useCallback((mode) => {
446
+ const di = state.flatIssues[state.selectedIndex];
447
+ if (!di)
448
+ return;
449
+ const repoRoot = repoRootRef.current;
450
+ if (!repoRoot)
451
+ return;
452
+ dispatch({ type: "SET_OVERLAY", overlay: null });
453
+ if (di.worktree) {
454
+ // Worktree exists — launch work
425
455
  if (isInTmux()) {
426
- const windowName = ticketId;
427
- const workCmd = mode === "plan" ? "st worktree work --plan" : "st worktree work";
428
- try {
429
- execSync(`tmux new-window -n "${windowName}" -c "${result.path}"`, { stdio: "ignore" });
430
- execSync(`tmux send-keys -t "${windowName}" "${workCmd}" Enter`, { stdio: "ignore" });
431
- dispatch({
432
- type: "SET_ACTION_MESSAGE",
433
- message: `Created worktree + launched ${mode} in: ${windowName}`,
434
- });
435
- }
436
- catch {
437
- dispatch({ type: "SET_ACTION_MESSAGE", message: "Worktree created, but tmux failed" });
438
- }
439
- // Refresh to pick up new worktree + session
440
- setTimeout(() => refresh(), 3000);
456
+ launchWorkInTmux(di, mode, di.worktree.path);
441
457
  }
442
458
  else {
443
459
  leaveAltScreen();
444
- console.log(`SANTREE_CD:${result.path}`);
460
+ console.log(`SANTREE_CD:${di.worktree.path}`);
445
461
  console.log(`SANTREE_WORK:${mode}`);
446
462
  exit();
447
463
  }
448
464
  }
449
- }, [state.flatIssues, state.selectedIndex, exit, refresh, launchWorkInTmux]);
465
+ else {
466
+ // No worktree — ask about setup if init script exists
467
+ if (hasInitScript(repoRoot)) {
468
+ dispatch({ type: "SETUP_CONFIRM_SHOW", mode });
469
+ return;
470
+ }
471
+ // No init script — create directly
472
+ createAndLaunch(mode, false);
473
+ }
474
+ }, [state.flatIssues, state.selectedIndex, exit, launchWorkInTmux, createAndLaunch]);
450
475
  // ── Commit flow ──────────────────────────────────────────────────
451
476
  const handleStageAll = useCallback(async () => {
452
477
  const wtPath = stateRef.current.commitWorktreePath;
@@ -616,6 +641,25 @@ export default function Dashboard() {
616
641
  }
617
642
  return;
618
643
  }
644
+ // Confirm setup overlay
645
+ if (state.overlay === "confirm-setup") {
646
+ const mode = state.setupMode;
647
+ if (input === "y" && mode) {
648
+ dispatch({ type: "SETUP_CONFIRM_DONE" });
649
+ createAndLaunch(mode, true);
650
+ return;
651
+ }
652
+ if (input === "n" && mode) {
653
+ dispatch({ type: "SETUP_CONFIRM_DONE" });
654
+ createAndLaunch(mode, false);
655
+ return;
656
+ }
657
+ if (key.escape) {
658
+ dispatch({ type: "SETUP_CONFIRM_DONE" });
659
+ return;
660
+ }
661
+ return;
662
+ }
619
663
  // Mode select overlay
620
664
  if (state.overlay === "mode-select") {
621
665
  if (input === "p" || input === "1") {
@@ -732,6 +776,7 @@ export default function Dashboard() {
732
776
  execSync(`tmux new-window -n "${windowName}" -c "${di.worktree.path}"`, {
733
777
  stdio: "ignore",
734
778
  });
779
+ execSync("sleep 0.1", { stdio: "ignore" });
735
780
  const cmd = resumeCmd ?? "st worktree work";
736
781
  execSync(`tmux send-keys -t "${windowName}" "${cmd}" Enter`, {
737
782
  stdio: "ignore",
@@ -896,5 +941,5 @@ export default function Dashboard() {
896
941
  return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsxs(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "No active issues assigned to you" }), _jsx(Text, { dimColor: true, children: "Press R to refresh or q to quit" })] }) }));
897
942
  }
898
943
  const selectedIssue = state.flatIssues[state.selectedIndex] ?? null;
899
- return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Dashboard" }), _jsxs(Text, { dimColor: true, children: [" ", "(", state.flatIssues.length, " issues)", state.refreshing ? " refreshing..." : ""] }), state.actionMessage && (_jsxs(Text, { color: "yellow", children: [" ", state.actionMessage] }))] }), state.overlay === "mode-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Select mode:" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "p" }), " Plan"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "i" }), " Implement"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }) })) : state.overlay === "confirm-delete" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Remove worktree?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: selectedIssue?.worktree?.branch ?? "" }), selectedIssue?.worktree?.dirty && (_jsx(Text, { color: "yellow", children: "Warning: worktree has uncommitted changes" })), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " Confirm"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "n" }), " Cancel"] })] }) })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: leftWidth, children: _jsx(IssueList, { groups: state.groups, flatIssues: state.flatIssues, selectedIndex: state.selectedIndex, scrollOffset: state.listScrollOffset, height: contentHeight, width: leftWidth, creatingForTicket: state.creatingForTicket, deletingForTicket: state.deletingForTicket }) }), _jsx(Box, { flexDirection: "column", width: 3, children: Array.from({ length: contentHeight }).map((_, i) => (_jsx(Text, { dimColor: true, children: " │ " }, i))) }), _jsx(Box, { width: rightWidth, children: state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] }))] }));
944
+ return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Dashboard" }), _jsxs(Text, { dimColor: true, children: [" v", version] }), _jsxs(Text, { dimColor: true, children: [" ", "(", state.flatIssues.length, " issues)", state.refreshing ? " refreshing..." : ""] }), state.actionMessage && (_jsxs(Text, { color: "yellow", children: [" ", state.actionMessage] }))] }), state.overlay === "mode-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Select mode:" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "p" }), " Plan"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "i" }), " Implement"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }) })) : state.overlay === "confirm-delete" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Remove worktree?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: selectedIssue?.worktree?.branch ?? "" }), selectedIssue?.worktree?.dirty && (_jsx(Text, { color: "yellow", children: "Warning: worktree has uncommitted changes" })), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " Confirm"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "n" }), " Cancel"] })] }) })) : state.overlay === "confirm-setup" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Run setup script?" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: ".santree/init.sh" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " Run setup"] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", bold: true, children: "n" }), " Skip"] })] }) })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: leftWidth, children: _jsx(IssueList, { groups: state.groups, flatIssues: state.flatIssues, selectedIndex: state.selectedIndex, scrollOffset: state.listScrollOffset, height: contentHeight, width: leftWidth, creatingForTicket: state.creatingForTicket, deletingForTicket: state.deletingForTicket }) }), _jsx(Box, { flexDirection: "column", width: 3, children: Array.from({ length: contentHeight }).map((_, i) => (_jsx(Text, { dimColor: true, children: " │ " }, i))) }), _jsx(Box, { width: rightWidth, children: state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] }))] }));
900
945
  }
@@ -4,8 +4,11 @@ import Spinner from "ink-spinner";
4
4
  import { useEffect, useState } from "react";
5
5
  import { exec, execSync } from "child_process";
6
6
  import { promisify } from "util";
7
+ import { createRequire } from "module";
7
8
  import * as fs from "fs";
8
9
  import * as path from "path";
10
+ const require = createRequire(import.meta.url);
11
+ const { version } = require("../../package.json");
9
12
  import { findMainRepoRoot, getSantreeDir, getInitScriptPath } from "../lib/git.js";
10
13
  import { getAuthStatus, getValidTokens } from "../lib/linear.js";
11
14
  const execAsync = promisify(exec);
@@ -336,5 +339,5 @@ export default function Doctor() {
336
339
  const optionalMissing = tools.filter((t) => !t.required && !t.installed);
337
340
  const linearOk = linear?.authenticated && linear?.tokenValid && linear?.repoLinked;
338
341
  const allRequired = requiredMissing.length === 0 && linearOk && shellStatus?.configured;
339
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Santree Doctor" }) }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "CLI Tools" }) }), tools.map((tool) => (_jsx(ToolRow, { tool: tool }, tool.name))), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Integrations" }) }), linear && _jsx(LinearRow, { linear: linear }), shellStatus && _jsx(ShellRow, { configured: shellStatus.configured, shell: shellStatus.shell }), santreeSetup && _jsx(SantreeSetupRow, { status: santreeSetup }), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Aesthetics" }) }), statusline && _jsx(StatuslineRow, { status: statusline }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: allRequired ? "green" : "yellow", paddingX: 2, children: allRequired ? (_jsx(Text, { color: "green", children: "All requirements satisfied! Santree is ready to use." })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: [requiredMissing.length + (linearOk ? 0 : 1) + (shellStatus?.configured ? 0 : 1), " ", "required item(s) need attention"] }), optionalMissing.length > 0 && (_jsxs(Text, { dimColor: true, children: [optionalMissing.length, " optional item(s) not installed"] }))] })) })] }));
342
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Doctor" }), _jsxs(Text, { dimColor: true, children: [" v", version] })] }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "CLI Tools" }) }), tools.map((tool) => (_jsx(ToolRow, { tool: tool }, tool.name))), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Integrations" }) }), linear && _jsx(LinearRow, { linear: linear }), shellStatus && _jsx(ShellRow, { configured: shellStatus.configured, shell: shellStatus.shell }), santreeSetup && _jsx(SantreeSetupRow, { status: santreeSetup }), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Aesthetics" }) }), statusline && _jsx(StatuslineRow, { status: statusline }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: allRequired ? "green" : "yellow", paddingX: 2, children: allRequired ? (_jsx(Text, { color: "green", children: "All requirements satisfied! Santree is ready to use." })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: [requiredMissing.length + (linearOk ? 0 : 1) + (shellStatus?.configured ? 0 : 1), " ", "required item(s) need attention"] }), optionalMissing.length > 0 && (_jsxs(Text, { dimColor: true, children: [optionalMissing.length, " optional item(s) not installed"] }))] })) })] }));
340
343
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
3
  import { Text, Box } from "ink";
4
4
  import Spinner from "ink-spinner";
5
- import { resolveAIContext, renderAIPrompt, launchAgent, cleanupImages, fetchAndRenderPR, fetchAndRenderDiff, } from "../../lib/ai.js";
5
+ import { resolveAIContext, renderAIPrompt, launchAgent, resolveAgentBinary, cleanupImages, fetchAndRenderPR, fetchAndRenderDiff, } from "../../lib/ai.js";
6
6
  export const description = "Fix PR review comments";
7
7
  export default function Fix() {
8
8
  const [status, setStatus] = useState("loading");
@@ -54,5 +54,5 @@ export default function Fix() {
54
54
  }
55
55
  init();
56
56
  }, []);
57
- return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Fix PR" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "magenta", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "magenta", color: "white", bold: true, children: " fix PR " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude (through Happy)..." }), _jsxs(Text, { dimColor: true, children: [" happy ", `"<fix-pr prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
57
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Fix PR" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "magenta", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "magenta", color: "white", bold: true, children: " fix PR " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Launching ", resolveAgentBinary() === "happy" ? "Claude (through Happy)" : "Claude", "..."] }), _jsxs(Text, { dimColor: true, children: [" ", resolveAgentBinary(), " ", `"<fix-pr prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
58
58
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
3
  import { Text, Box } from "ink";
4
4
  import Spinner from "ink-spinner";
5
- import { resolveAIContext, renderAIPrompt, launchAgent, cleanupImages, fetchAndRenderDiff, } from "../../lib/ai.js";
5
+ import { resolveAIContext, renderAIPrompt, launchAgent, resolveAgentBinary, cleanupImages, fetchAndRenderDiff, } from "../../lib/ai.js";
6
6
  export const description = "Review changes against ticket requirements";
7
7
  export default function Review() {
8
8
  const [status, setStatus] = useState("loading");
@@ -47,5 +47,5 @@ export default function Review() {
47
47
  }
48
48
  init();
49
49
  }, []);
50
- return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Review" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "yellow", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "yellow", color: "white", bold: true, children: " review " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket, diff, and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude (through Happy)..." }), _jsxs(Text, { dimColor: true, children: [" happy ", `"<review prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
50
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Review" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "yellow", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "yellow", color: "white", bold: true, children: " review " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket, diff, and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Launching ", resolveAgentBinary() === "happy" ? "Claude (through Happy)" : "Claude", "..."] }), _jsxs(Text, { dimColor: true, children: [" ", resolveAgentBinary(), " ", `"<review prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
51
51
  }
@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
3
3
  import { Text, Box } from "ink";
4
4
  import Spinner from "ink-spinner";
5
5
  import { z } from "zod";
6
- import { resolveAIContext, renderAIPrompt, launchAgent, cleanupImages, } from "../../lib/ai.js";
6
+ import { resolveAIContext, renderAIPrompt, launchAgent, resolveAgentBinary, cleanupImages, } from "../../lib/ai.js";
7
7
  import { randomUUID } from "crypto";
8
8
  import { getSessionId, setSessionId } from "../../lib/git.js";
9
9
  export const description = "Launch Claude to work on current ticket";
@@ -78,5 +78,5 @@ export default function Work({ options }) {
78
78
  setError(err instanceof Error ? err.message : "Failed to launch agent");
79
79
  }
80
80
  }, [status, aiContext, mode]);
81
- return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Work" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : getModeColor(mode), paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: getModeColor(mode), color: "white", bold: true, children: ` ${getModeLabel(mode)} ` })] })] }), _jsxs(Box, { marginTop: 1, children: [status === "loading" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] })), status === "fetching" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Fetching ticket from Linear..." })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude (through Happy)..." }), _jsxs(Text, { dimColor: true, children: [" ", "happy", mode === "plan" ? " --permission-mode plan" : "", " ", `"<${getModeLabel(mode)} prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
81
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Work" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : getModeColor(mode), paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: getModeColor(mode), color: "white", bold: true, children: ` ${getModeLabel(mode)} ` })] })] }), _jsxs(Box, { marginTop: 1, children: [status === "loading" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] })), status === "fetching" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Fetching ticket from Linear..." })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Launching ", resolveAgentBinary() === "happy" ? "Claude (through Happy)" : "Claude", "..."] }), _jsxs(Text, { dimColor: true, children: [" ", resolveAgentBinary(), mode === "plan" ? " --permission-mode plan" : "", " ", `"<${getModeLabel(mode)} prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
82
82
  }
@@ -1,6 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- function stateColor(type) {
3
+ function stateColor(type, name) {
4
+ const n = name?.toLowerCase();
5
+ if (n === "blocked")
6
+ return "red";
7
+ if (n === "in review")
8
+ return "green";
9
+ if (n === "in progress")
10
+ return "yellow";
4
11
  switch (type) {
5
12
  case "started":
6
13
  return "green";
@@ -64,9 +71,13 @@ function buildRows(groups, flatIssues) {
64
71
  const indexMap = new Map();
65
72
  flatIssues.forEach((di, i) => indexMap.set(di.issue.identifier, i));
66
73
  for (const group of groups) {
67
- rows.push({ kind: "header", name: group.name, count: group.issues.length });
68
- for (const di of group.issues) {
69
- rows.push({ kind: "issue", issue: di, flatIndex: indexMap.get(di.issue.identifier) ?? -1 });
74
+ const totalIssues = group.statusGroups.reduce((sum, sg) => sum + sg.issues.length, 0);
75
+ rows.push({ kind: "header", name: group.name, count: totalIssues });
76
+ for (const sg of group.statusGroups) {
77
+ rows.push({ kind: "status-header", name: sg.name, type: sg.type, count: sg.issues.length });
78
+ for (const di of sg.issues) {
79
+ rows.push({ kind: "issue", issue: di, flatIndex: indexMap.get(di.issue.identifier) ?? -1 });
80
+ }
70
81
  }
71
82
  }
72
83
  return rows;
@@ -92,10 +103,13 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
92
103
  if (row.kind === "header") {
93
104
  return (_jsx(Box, { children: _jsxs(Text, { dimColor: true, bold: true, children: ["── ", row.name, " (", row.count, ")", " ──"] }) }, `h-${i}`));
94
105
  }
106
+ if (row.kind === "status-header") {
107
+ return (_jsx(Box, { children: _jsxs(Text, { color: stateColor(row.type, row.name), dimColor: true, children: [" ", row.name, " (", row.count, ")"] }) }, `sh-${i}`));
108
+ }
95
109
  const { issue, flatIndex } = row;
96
110
  const selected = flatIndex === selectedIndex;
97
111
  const di = issue;
98
- const sc = stateColor(di.issue.state.type);
112
+ const sc = stateColor(di.issue.state.type, di.issue.state.name);
99
113
  const isCreating = di.issue.identifier === creatingForTicket;
100
114
  const isDeleting = di.issue.identifier === deletingForTicket;
101
115
  const sess = sessionIndicator(di.worktree, isCreating, isDeleting);
@@ -66,10 +66,38 @@ export async function loadDashboardData(repoRoot) {
66
66
  list.push(di);
67
67
  groupMap.set(key, list);
68
68
  }
69
- const groups = [...groupMap.entries()].map(([name, issues]) => ({
70
- name,
71
- id: issues[0]?.issue.projectId ?? null,
72
- issues,
73
- }));
74
- return { groups, flatIssues: groups.flatMap((g) => g.issues) };
69
+ // Status type priority: started > unstarted > backlog > triage
70
+ const statusTypePriority = {
71
+ started: 0,
72
+ unstarted: 1,
73
+ backlog: 2,
74
+ triage: 3,
75
+ };
76
+ const groups = [...groupMap.entries()].map(([name, issues]) => {
77
+ // Sub-group by status
78
+ const statusMap = new Map();
79
+ for (const di of issues) {
80
+ const statusName = di.issue.state.name;
81
+ const existing = statusMap.get(statusName);
82
+ if (existing) {
83
+ existing.issues.push(di);
84
+ }
85
+ else {
86
+ statusMap.set(statusName, {
87
+ name: statusName,
88
+ type: di.issue.state.type,
89
+ issues: [di],
90
+ });
91
+ }
92
+ }
93
+ // Sort status groups by type priority
94
+ const statusGroups = [...statusMap.values()].sort((a, b) => (statusTypePriority[a.type] ?? 99) - (statusTypePriority[b.type] ?? 99));
95
+ return {
96
+ name,
97
+ id: issues[0]?.issue.projectId ?? null,
98
+ statusGroups,
99
+ };
100
+ });
101
+ const flatIssues = groups.flatMap((g) => g.statusGroups.flatMap((sg) => sg.issues));
102
+ return { groups, flatIssues };
75
103
  }
@@ -29,12 +29,17 @@ export interface DashboardIssue {
29
29
  checks: PRCheck[] | null;
30
30
  reviews: PRReview[] | null;
31
31
  }
32
+ export interface StatusGroup {
33
+ name: string;
34
+ type: string;
35
+ issues: DashboardIssue[];
36
+ }
32
37
  export interface ProjectGroup {
33
38
  name: string;
34
39
  id: string | null;
35
- issues: DashboardIssue[];
40
+ statusGroups: StatusGroup[];
36
41
  }
37
- export type ActionOverlay = "mode-select" | "confirm-delete" | "commit" | "pr-create" | null;
42
+ export type ActionOverlay = "mode-select" | "confirm-delete" | "confirm-setup" | "commit" | "pr-create" | null;
38
43
  export type CommitPhase = "idle" | "confirm-stage" | "awaiting-message" | "committing" | "pushing" | "done" | "error";
39
44
  export type PrCreatePhase = "idle" | "choose-mode" | "pushing" | "creating" | "done" | "error";
40
45
  export interface DashboardState {
@@ -65,6 +70,7 @@ export interface DashboardState {
65
70
  prCreateBranch: string | null;
66
71
  prCreateError: string | null;
67
72
  prCreateUrl: string | null;
73
+ setupMode: "plan" | "implement" | null;
68
74
  }
69
75
  export type DashboardAction = {
70
76
  type: "SET_DATA";
@@ -145,6 +151,11 @@ export type DashboardAction = {
145
151
  url: string;
146
152
  } | {
147
153
  type: "PR_CREATE_CANCEL";
154
+ } | {
155
+ type: "SETUP_CONFIRM_SHOW";
156
+ mode: "plan" | "implement";
157
+ } | {
158
+ type: "SETUP_CONFIRM_DONE";
148
159
  };
149
160
  export declare const initialState: DashboardState;
150
161
  export declare function reducer(state: DashboardState, action: DashboardAction): DashboardState;
@@ -27,6 +27,7 @@ export const initialState = {
27
27
  prCreateBranch: null,
28
28
  prCreateError: null,
29
29
  prCreateUrl: null,
30
+ setupMode: null,
30
31
  };
31
32
  export function reducer(state, action) {
32
33
  switch (action.type) {
@@ -145,6 +146,18 @@ export function reducer(state, action) {
145
146
  prCreateError: null,
146
147
  prCreateUrl: null,
147
148
  };
149
+ case "SETUP_CONFIRM_SHOW":
150
+ return {
151
+ ...state,
152
+ overlay: "confirm-setup",
153
+ setupMode: action.mode,
154
+ };
155
+ case "SETUP_CONFIRM_DONE":
156
+ return {
157
+ ...state,
158
+ overlay: null,
159
+ setupMode: null,
160
+ };
148
161
  default:
149
162
  return state;
150
163
  }
@@ -395,7 +395,7 @@ const ASSIGNED_ISSUES_QUERY = `
395
395
  query AssignedIssues {
396
396
  viewer {
397
397
  assignedIssues(
398
- filter: { state: { type: { nin: ["completed", "cancelled"] } } }
398
+ filter: { state: { type: { nin: ["completed", "canceled"] } } }
399
399
  orderBy: updatedAt
400
400
  first: 100
401
401
  ) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "santree",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Git worktree manager",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Toscanini",