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.
- package/dist/commands/dashboard.js +180 -135
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/pr/fix.js +2 -2
- package/dist/commands/pr/review.js +2 -2
- package/dist/commands/worktree/work.js +2 -2
- package/dist/lib/dashboard/IssueList.js +19 -5
- package/dist/lib/dashboard/data.js +34 -6
- package/dist/lib/dashboard/types.d.ts +13 -2
- package/dist/lib/dashboard/types.js +13 -0
- package/dist/lib/linear.js +1 -1
- package/package.json +1 -1
|
@@ -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++; //
|
|
36
|
-
for (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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; //
|
|
58
|
+
return null; // project header row
|
|
53
59
|
row++;
|
|
54
|
-
for (
|
|
60
|
+
for (const sg of g.statusGroups) {
|
|
55
61
|
if (row === listRow)
|
|
56
|
-
return
|
|
62
|
+
return null; // status header row
|
|
57
63
|
row++;
|
|
58
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
|
326
|
-
|
|
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
|
-
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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: "
|
|
376
|
-
|
|
408
|
+
type: "CREATION_LOG",
|
|
409
|
+
logs: "Warning: init.sh exists but is not executable, skipping\n",
|
|
377
410
|
});
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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:
|
|
433
|
+
logs: `\nInit script exited with code ${initResult.code}\n`,
|
|
392
434
|
});
|
|
393
|
-
canExecute = false;
|
|
394
435
|
}
|
|
395
|
-
|
|
396
|
-
dispatch({ type: "CREATION_LOG", logs: "
|
|
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
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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:${
|
|
460
|
+
console.log(`SANTREE_CD:${di.worktree.path}`);
|
|
445
461
|
console.log(`SANTREE_WORK:${mode}`);
|
|
446
462
|
exit();
|
|
447
463
|
}
|
|
448
464
|
}
|
|
449
|
-
|
|
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
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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: [
|
|
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
|
}
|
package/dist/commands/pr/fix.js
CHANGED
|
@@ -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: [
|
|
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: [
|
|
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: [
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/lib/linear.js
CHANGED
|
@@ -395,7 +395,7 @@ const ASSIGNED_ISSUES_QUERY = `
|
|
|
395
395
|
query AssignedIssues {
|
|
396
396
|
viewer {
|
|
397
397
|
assignedIssues(
|
|
398
|
-
filter: { state: { type: { nin: ["completed", "
|
|
398
|
+
filter: { state: { type: { nin: ["completed", "canceled"] } } }
|
|
399
399
|
orderBy: updatedAt
|
|
400
400
|
first: 100
|
|
401
401
|
) {
|