santree 0.6.2 → 0.6.3

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.
@@ -18,7 +18,10 @@ import { shellEscape } from "../lib/multiplexer/types.js";
18
18
  import SquirrelLoader from "../lib/squirrel-loader.js";
19
19
  import { getPRTemplate } from "../lib/github.js";
20
20
  import { renderPrompt, renderDiff, renderTicket } from "../lib/prompts.js";
21
- import { getIssueTracker } from "../lib/trackers/index.js";
21
+ import { getIssueTracker, isRepoTrackerConfigured, setRepoTracker } from "../lib/trackers/index.js";
22
+ import { setRepoLinearOrg } from "../lib/trackers/linear/index.js";
23
+ import { readLinearAuthStore } from "../lib/trackers/auth-store.js";
24
+ import { getAuthenticatedUser, getCurrentRepoNwo } from "../lib/trackers/github/auth.js";
22
25
  import { openUrl } from "../lib/open-url.js";
23
26
  import { parseUnifiedDiff } from "../lib/diff-parse.js";
24
27
  import * as os from "os";
@@ -264,7 +267,6 @@ function ensureAltScreen() {
264
267
  if (altScreenEntered)
265
268
  return;
266
269
  altScreenEntered = true;
267
- getMultiplexer().renameWindow("", "santree");
268
270
  process.stdout.write("\x1b[?1049h"); // Enter alternate screen buffer
269
271
  process.stdout.write("\x1b[?25l"); // Hide cursor
270
272
  }
@@ -387,6 +389,14 @@ export default function Dashboard() {
387
389
  return;
388
390
  }
389
391
  repoRootRef.current = repoRoot;
392
+ // No tracker configured → show the selection flow instead of letting
393
+ // getIssueTracker silently fall back to GitHub and then fail auth on
394
+ // the dead-end error screen. Genuine auth/network failures of a
395
+ // *configured* tracker still hit the catch → error screen below.
396
+ if (!isRepoTrackerConfigured(repoRoot)) {
397
+ dispatch({ type: "TRACKER_SELECT_OPEN" });
398
+ return;
399
+ }
390
400
  try {
391
401
  // Re-detect terminal theme alongside data fetch so light↔dark
392
402
  // switches propagate within one refresh cycle (≤30s). Skip the
@@ -528,18 +538,22 @@ export default function Dashboard() {
528
538
  }
529
539
  return;
530
540
  }
531
- if (inLeftPane) {
532
- // Scroll left pane (issue list)
533
- const maxIdx = s.flatIssues.length - 1;
534
- if (maxIdx < 0)
535
- return;
536
- const next = Math.max(0, Math.min(s.selectedIndex + delta, maxIdx));
537
- dispatch({ type: "SELECT", index: next });
538
- }
539
- else {
540
- // Scroll right pane (detail)
541
- const next = Math.max(0, s.detailScrollOffset + delta);
542
- dispatch({ type: "SCROLL_DETAIL", offset: next });
541
+ {
542
+ const isTreesTab = s.activeTab === "trees";
543
+ const flat = isTreesTab ? s.flatTrees : s.flatIssues;
544
+ const idx = isTreesTab ? s.treeSelectedIndex : s.selectedIndex;
545
+ const detailOff = isTreesTab ? s.treeDetailScrollOffset : s.detailScrollOffset;
546
+ if (inLeftPane) {
547
+ const maxIdx = flat.length - 1;
548
+ if (maxIdx < 0)
549
+ return;
550
+ const next = Math.max(0, Math.min(idx + delta, maxIdx));
551
+ dispatch({ type: isTreesTab ? "TREE_SELECT" : "SELECT", index: next });
552
+ }
553
+ else {
554
+ const next = Math.max(0, detailOff + delta);
555
+ dispatch({ type: isTreesTab ? "TREE_SCROLL_DETAIL" : "SCROLL_DETAIL", offset: next });
556
+ }
543
557
  }
544
558
  return;
545
559
  }
@@ -611,12 +625,18 @@ export default function Dashboard() {
611
625
  }
612
626
  return;
613
627
  }
614
- if (s.flatIssues.length === 0)
615
- return;
616
- const listRow = s.listScrollOffset + contentRow;
617
- const flatIdx = getFlatIndexForListRow(s.groups, s.flatIssues, listRow);
618
- if (flatIdx !== null && flatIdx >= 0 && flatIdx < s.flatIssues.length) {
619
- dispatch({ type: "SELECT", index: flatIdx });
628
+ {
629
+ const isTreesTab = s.activeTab === "trees";
630
+ const flat = isTreesTab ? s.flatTrees : s.flatIssues;
631
+ const grps = isTreesTab ? s.treeGroups : s.groups;
632
+ const scrollOff = isTreesTab ? s.treeListScrollOffset : s.listScrollOffset;
633
+ if (flat.length === 0)
634
+ return;
635
+ const listRow = scrollOff + contentRow;
636
+ const flatIdx = getFlatIndexForListRow(grps, flat, listRow);
637
+ if (flatIdx !== null && flatIdx >= 0 && flatIdx < flat.length) {
638
+ dispatch({ type: isTreesTab ? "TREE_SELECT" : "SELECT", index: flatIdx });
639
+ }
620
640
  }
621
641
  };
622
642
  if (process.stdin.isTTY) {
@@ -663,6 +683,21 @@ export default function Dashboard() {
663
683
  dispatch({ type: "SCROLL_LIST", offset });
664
684
  }
665
685
  }, [state.selectedIndex, state.groups, contentHeight, state.listScrollOffset]);
686
+ // ── Trees list scroll tracking ───────────────────────────────────
687
+ useEffect(() => {
688
+ const rowIdx = getRowIndexForFlatIndex(state.treeGroups, state.flatTrees, state.treeSelectedIndex);
689
+ const maxVisible = contentHeight - LIST_FOOTER_HEIGHT;
690
+ let offset = state.treeListScrollOffset;
691
+ if (rowIdx < offset) {
692
+ offset = Math.max(0, rowIdx - 1);
693
+ }
694
+ else if (rowIdx >= offset + maxVisible) {
695
+ offset = rowIdx - maxVisible + 2;
696
+ }
697
+ if (offset !== state.treeListScrollOffset) {
698
+ dispatch({ type: "TREE_SCROLL_LIST", offset });
699
+ }
700
+ }, [state.treeSelectedIndex, state.treeGroups, contentHeight, state.treeListScrollOffset]);
666
701
  // ── Review list scroll tracking ──────────────────────────────────
667
702
  useEffect(() => {
668
703
  // Row index = 1 (column header) + flatIndex
@@ -686,6 +721,7 @@ export default function Dashboard() {
686
721
  // Disable tracking while any text-input overlay is mounted; restore on exit.
687
722
  useEffect(() => {
688
723
  const needsMouseOff = state.overlay === "context-input" ||
724
+ (state.overlay === "issue-form" && state.issueFormPhase !== "saving") ||
689
725
  (state.overlay === "pr-create" && state.prCreatePhase === "review") ||
690
726
  (state.overlay === "commit" && state.commitPhase === "awaiting-message");
691
727
  if (!needsMouseOff)
@@ -694,7 +730,7 @@ export default function Dashboard() {
694
730
  return () => {
695
731
  process.stdout.write("\x1b[?1002h\x1b[?1006h");
696
732
  };
697
- }, [state.overlay, state.prCreatePhase, state.commitPhase]);
733
+ }, [state.overlay, state.issueFormPhase, state.prCreatePhase, state.commitPhase]);
698
734
  // ── Diff overlay: load file list when opened (gh pr diff path) ────
699
735
  // Reviews-tab PRs without a local worktree shell out to `gh pr diff <n>`,
700
736
  // parse the unified blob into per-file records, and stash the per-file
@@ -982,7 +1018,10 @@ export default function Dashboard() {
982
1018
  }
983
1019
  }, []);
984
1020
  const createAndLaunch = useCallback(async (mode, runSetup, base, contextFile) => {
985
- const di = stateRef.current.flatIssues[stateRef.current.selectedIndex];
1021
+ const sref = stateRef.current;
1022
+ const di = sref.activeTab === "trees"
1023
+ ? sref.flatTrees[sref.treeSelectedIndex]
1024
+ : sref.flatIssues[sref.selectedIndex];
986
1025
  if (!di)
987
1026
  return;
988
1027
  const repoRoot = repoRootRef.current;
@@ -1091,7 +1130,9 @@ export default function Dashboard() {
1091
1130
  createAndLaunch(mode, false, base, contextFile);
1092
1131
  }, [createAndLaunch]);
1093
1132
  const doWork = useCallback((mode, customContext) => {
1094
- const di = state.flatIssues[state.selectedIndex];
1133
+ const di = state.activeTab === "trees"
1134
+ ? state.flatTrees[state.treeSelectedIndex]
1135
+ : state.flatIssues[state.selectedIndex];
1095
1136
  if (!di)
1096
1137
  return;
1097
1138
  const repoRoot = repoRootRef.current;
@@ -1118,7 +1159,9 @@ export default function Dashboard() {
1118
1159
  pendingContextFileRef.current = contextFile ?? null;
1119
1160
  const defaultBranch = getDefaultBranch();
1120
1161
  const baseOptions = [defaultBranch];
1121
- for (const fi of state.flatIssues) {
1162
+ // Worktree branches live on the Trees tab now; scan there for
1163
+ // candidate base branches (stacked work).
1164
+ for (const fi of [...state.flatTrees, ...state.flatIssues]) {
1122
1165
  if (fi.worktree && !baseOptions.includes(fi.worktree.branch)) {
1123
1166
  baseOptions.push(fi.worktree.branch);
1124
1167
  }
@@ -1136,11 +1179,153 @@ export default function Dashboard() {
1136
1179
  }, [
1137
1180
  state.flatIssues,
1138
1181
  state.selectedIndex,
1182
+ state.flatTrees,
1183
+ state.treeSelectedIndex,
1184
+ state.activeTab,
1139
1185
  exit,
1140
1186
  launchWorkInTmux,
1141
1187
  proceedAfterBaseSelect,
1142
1188
  writeContextFile,
1143
1189
  ]);
1190
+ // ── Tracker selection ────────────────────────────────────────────
1191
+ // Mirrors `santree issue setup`. Local needs no account; Linear picks an
1192
+ // authenticated workspace (sub-list when >1); GitHub verifies `gh`.
1193
+ const chooseTracker = useCallback(async (kind) => {
1194
+ const root = repoRootRef.current ?? findMainRepoRoot();
1195
+ if (!root) {
1196
+ dispatch({ type: "SET_ERROR", error: "Not inside a git repository" });
1197
+ return;
1198
+ }
1199
+ repoRootRef.current = root;
1200
+ if (kind === "local") {
1201
+ setRepoTracker(root, "local");
1202
+ dispatch({ type: "TRACKER_SELECT_CLOSE" });
1203
+ refresh();
1204
+ return;
1205
+ }
1206
+ if (kind === "linear") {
1207
+ const store = readLinearAuthStore();
1208
+ const orgs = Object.entries(store).map(([slug, tokens]) => ({
1209
+ slug,
1210
+ name: tokens.org_name,
1211
+ }));
1212
+ if (orgs.length === 0) {
1213
+ dispatch({
1214
+ type: "TRACKER_SELECT_MESSAGE",
1215
+ message: "No authenticated Linear workspaces. Run: santree linear auth",
1216
+ });
1217
+ return;
1218
+ }
1219
+ if (orgs.length === 1) {
1220
+ setRepoLinearOrg(root, orgs[0].slug);
1221
+ setRepoTracker(root, "linear");
1222
+ dispatch({ type: "TRACKER_SELECT_CLOSE" });
1223
+ refresh();
1224
+ return;
1225
+ }
1226
+ dispatch({ type: "TRACKER_SELECT_PHASE", phase: "linear-org", orgs });
1227
+ return;
1228
+ }
1229
+ // github
1230
+ const user = await getAuthenticatedUser();
1231
+ if (!user) {
1232
+ dispatch({
1233
+ type: "TRACKER_SELECT_MESSAGE",
1234
+ message: "GitHub CLI not authenticated. Run: santree github auth",
1235
+ });
1236
+ return;
1237
+ }
1238
+ setRepoTracker(root, "github");
1239
+ await getCurrentRepoNwo(root);
1240
+ dispatch({ type: "TRACKER_SELECT_CLOSE" });
1241
+ refresh();
1242
+ }, [refresh]);
1243
+ const chooseLinearOrg = useCallback((slug) => {
1244
+ const root = repoRootRef.current ?? findMainRepoRoot();
1245
+ if (!root)
1246
+ return;
1247
+ setRepoLinearOrg(root, slug);
1248
+ setRepoTracker(root, "linear");
1249
+ dispatch({ type: "TRACKER_SELECT_CLOSE" });
1250
+ refresh();
1251
+ }, [refresh]);
1252
+ // ── Issue CRUD (built-in tracker only) ───────────────────────────
1253
+ const submitIssueForm = useCallback(async () => {
1254
+ const s = stateRef.current;
1255
+ const root = repoRootRef.current;
1256
+ if (!root)
1257
+ return;
1258
+ const tracker = getIssueTracker(root);
1259
+ const title = s.issueFormTitle.split("\n")[0]?.trim() ?? "";
1260
+ if (!title) {
1261
+ dispatch({ type: "ISSUE_FORM_ERROR", error: "Title is required" });
1262
+ return;
1263
+ }
1264
+ const description = s.issueFormDescription;
1265
+ dispatch({ type: "ISSUE_FORM_PHASE", phase: "saving" });
1266
+ try {
1267
+ if (s.issueFormMode === "edit" && s.issueFormId && tracker.updateIssue) {
1268
+ const res = await tracker.updateIssue(s.issueFormId, { title, description }, root);
1269
+ if (!res.ok) {
1270
+ dispatch({ type: "ISSUE_FORM_ERROR", error: res.message ?? "Update failed" });
1271
+ return;
1272
+ }
1273
+ }
1274
+ else if (tracker.createIssue) {
1275
+ const res = await tracker.createIssue({ title, description }, root);
1276
+ if (!res.ok) {
1277
+ dispatch({ type: "ISSUE_FORM_ERROR", error: res.message ?? "Create failed" });
1278
+ return;
1279
+ }
1280
+ }
1281
+ else {
1282
+ dispatch({ type: "ISSUE_FORM_ERROR", error: "Tracker does not support editing" });
1283
+ return;
1284
+ }
1285
+ dispatch({ type: "ISSUE_FORM_CLOSE" });
1286
+ dispatch({
1287
+ type: "SET_ACTION_MESSAGE",
1288
+ message: s.issueFormMode === "edit" ? "Issue updated" : "Issue created",
1289
+ });
1290
+ refresh();
1291
+ }
1292
+ catch (e) {
1293
+ dispatch({
1294
+ type: "ISSUE_FORM_ERROR",
1295
+ error: e instanceof Error ? e.message : "Failed to save issue",
1296
+ });
1297
+ }
1298
+ }, [refresh]);
1299
+ const deleteSelectedIssue = useCallback(async () => {
1300
+ const s = stateRef.current;
1301
+ const root = repoRootRef.current;
1302
+ if (!root)
1303
+ return;
1304
+ const di = s.flatIssues[s.selectedIndex];
1305
+ if (!di)
1306
+ return;
1307
+ const tracker = getIssueTracker(root);
1308
+ dispatch({ type: "ISSUE_DELETE_CLOSE" });
1309
+ if (!tracker.deleteIssue)
1310
+ return;
1311
+ try {
1312
+ const res = await tracker.deleteIssue(di.issue.identifier, root);
1313
+ dispatch({
1314
+ type: "SET_ACTION_MESSAGE",
1315
+ message: res.ok
1316
+ ? `Deleted ${di.issue.identifier}`
1317
+ : `Failed: ${res.message ?? "delete failed"}`,
1318
+ });
1319
+ if (res.ok)
1320
+ refresh();
1321
+ }
1322
+ catch (e) {
1323
+ dispatch({
1324
+ type: "SET_ACTION_MESSAGE",
1325
+ message: e instanceof Error ? e.message : "Delete failed",
1326
+ });
1327
+ }
1328
+ }, [refresh]);
1144
1329
  // ── Commit flow ──────────────────────────────────────────────────
1145
1330
  const handleStageAll = useCallback(async () => {
1146
1331
  const wtPath = stateRef.current.commitWorktreePath;
@@ -1782,7 +1967,8 @@ export default function Dashboard() {
1782
1967
  if (state.overlay === "confirm-delete") {
1783
1968
  if (input === "y") {
1784
1969
  dispatch({ type: "SET_OVERLAY", overlay: null });
1785
- const di = state.flatIssues[state.selectedIndex];
1970
+ // Worktree removal is a Trees-tab action.
1971
+ const di = state.flatTrees[state.treeSelectedIndex];
1786
1972
  if (di?.worktree) {
1787
1973
  const repoRoot = repoRootRef.current;
1788
1974
  if (repoRoot) {
@@ -1814,12 +2000,86 @@ export default function Dashboard() {
1814
2000
  }
1815
2001
  return;
1816
2002
  }
2003
+ // Tracker-selection overlay (shown when no tracker is configured,
2004
+ // or reopened with `t`). Not a text input — outer useInput drives it.
2005
+ if (state.overlay === "tracker-select") {
2006
+ if (state.trackerSelectPhase === "linear-org") {
2007
+ const orgs = state.trackerSelectOrgs;
2008
+ if (input === "j" || key.downArrow) {
2009
+ dispatch({
2010
+ type: "TRACKER_SELECT_MOVE",
2011
+ index: Math.min(state.trackerSelectIndex + 1, orgs.length - 1),
2012
+ });
2013
+ return;
2014
+ }
2015
+ if (input === "k" || key.upArrow) {
2016
+ dispatch({
2017
+ type: "TRACKER_SELECT_MOVE",
2018
+ index: Math.max(state.trackerSelectIndex - 1, 0),
2019
+ });
2020
+ return;
2021
+ }
2022
+ if (key.return) {
2023
+ const org = orgs[state.trackerSelectIndex];
2024
+ if (org)
2025
+ chooseLinearOrg(org.slug);
2026
+ return;
2027
+ }
2028
+ if (key.escape) {
2029
+ dispatch({ type: "TRACKER_SELECT_PHASE", phase: "root" });
2030
+ return;
2031
+ }
2032
+ return;
2033
+ }
2034
+ const TRACKER_KINDS = ["local", "linear", "github"];
2035
+ if (input === "j" || key.downArrow) {
2036
+ dispatch({
2037
+ type: "TRACKER_SELECT_MOVE",
2038
+ index: Math.min(state.trackerSelectIndex + 1, TRACKER_KINDS.length - 1),
2039
+ });
2040
+ return;
2041
+ }
2042
+ if (input === "k" || key.upArrow) {
2043
+ dispatch({
2044
+ type: "TRACKER_SELECT_MOVE",
2045
+ index: Math.max(state.trackerSelectIndex - 1, 0),
2046
+ });
2047
+ return;
2048
+ }
2049
+ if (key.return) {
2050
+ void chooseTracker(TRACKER_KINDS[state.trackerSelectIndex] ?? "local");
2051
+ return;
2052
+ }
2053
+ if (input === "q" || key.escape) {
2054
+ // Can't proceed without a tracker — leave the dashboard.
2055
+ exit();
2056
+ return;
2057
+ }
2058
+ return;
2059
+ }
2060
+ // Confirm-delete-issue overlay (built-in tracker only)
2061
+ if (state.overlay === "confirm-delete-issue") {
2062
+ if (input === "y") {
2063
+ void deleteSelectedIssue();
2064
+ return;
2065
+ }
2066
+ if (input === "n" || key.escape || input === "q") {
2067
+ dispatch({ type: "ISSUE_DELETE_CLOSE" });
2068
+ return;
2069
+ }
2070
+ return;
2071
+ }
2072
+ // Issue create/edit form. Title/description phases are owned by
2073
+ // MultilineTextArea (outer useInput disabled via isActive); only
2074
+ // the "saving" phase reaches here — swallow all keys.
2075
+ if (state.overlay === "issue-form") {
2076
+ return;
2077
+ }
1817
2078
  // Tab switching (only when no overlay is active)
2079
+ const TAB_ORDER = ["issues", "trees", "reviews"];
1818
2080
  if (key.tab) {
1819
- dispatch({
1820
- type: "SET_TAB",
1821
- tab: state.activeTab === "issues" ? "reviews" : "issues",
1822
- });
2081
+ const idx = TAB_ORDER.indexOf(state.activeTab);
2082
+ dispatch({ type: "SET_TAB", tab: TAB_ORDER[(idx + 1) % TAB_ORDER.length] });
1823
2083
  return;
1824
2084
  }
1825
2085
  if (input === "1") {
@@ -1827,9 +2087,18 @@ export default function Dashboard() {
1827
2087
  return;
1828
2088
  }
1829
2089
  if (input === "2") {
2090
+ dispatch({ type: "SET_TAB", tab: "trees" });
2091
+ return;
2092
+ }
2093
+ if (input === "3") {
1830
2094
  dispatch({ type: "SET_TAB", tab: "reviews" });
1831
2095
  return;
1832
2096
  }
2097
+ // Reopen tracker selection from any tab.
2098
+ if (input === "t") {
2099
+ dispatch({ type: "TRACKER_SELECT_OPEN" });
2100
+ return;
2101
+ }
1833
2102
  // Quit
1834
2103
  if (input === "q") {
1835
2104
  exit();
@@ -2077,33 +2346,109 @@ export default function Dashboard() {
2077
2346
  }
2078
2347
  return; // prevent fallthrough to issues actions
2079
2348
  }
2080
- const maxIndex = state.flatIssues.length - 1;
2349
+ // Issues tab = backlog/planning; Trees tab = worktrees in
2350
+ // progress. Both reuse the same list/detail UI and worktree action
2351
+ // handlers below — only the backing data + selection slice differ.
2352
+ const isTrees = state.activeTab === "trees";
2353
+ const viewFlat = isTrees ? state.flatTrees : state.flatIssues;
2354
+ const viewIndex = isTrees ? state.treeSelectedIndex : state.selectedIndex;
2355
+ const viewDetailScroll = isTrees ? state.treeDetailScrollOffset : state.detailScrollOffset;
2356
+ const selectAction = isTrees ? "TREE_SELECT" : "SELECT";
2357
+ const scrollDetailAction = isTrees
2358
+ ? "TREE_SCROLL_DETAIL"
2359
+ : "SCROLL_DETAIL";
2360
+ const maxIndex = viewFlat.length - 1;
2081
2361
  // Navigation
2082
2362
  if (input === "j" || (key.downArrow && !key.shift)) {
2083
- const next = Math.min(state.selectedIndex + 1, maxIndex);
2084
- dispatch({ type: "SELECT", index: next });
2363
+ dispatch({ type: selectAction, index: Math.min(viewIndex + 1, maxIndex) });
2085
2364
  return;
2086
2365
  }
2087
2366
  if (input === "k" || (key.upArrow && !key.shift)) {
2088
- const prev = Math.max(state.selectedIndex - 1, 0);
2089
- dispatch({ type: "SELECT", index: prev });
2367
+ dispatch({ type: selectAction, index: Math.max(viewIndex - 1, 0) });
2090
2368
  return;
2091
2369
  }
2092
2370
  // Detail scroll
2093
2371
  if (key.shift && key.downArrow) {
2094
- dispatch({ type: "SCROLL_DETAIL", offset: state.detailScrollOffset + 3 });
2372
+ dispatch({ type: scrollDetailAction, offset: viewDetailScroll + 3 });
2095
2373
  return;
2096
2374
  }
2097
2375
  if (key.shift && key.upArrow) {
2098
- dispatch({
2099
- type: "SCROLL_DETAIL",
2100
- offset: Math.max(0, state.detailScrollOffset - 3),
2101
- });
2376
+ dispatch({ type: scrollDetailAction, offset: Math.max(0, viewDetailScroll - 3) });
2102
2377
  return;
2103
2378
  }
2104
- const di = state.flatIssues[state.selectedIndex];
2379
+ const di = viewFlat[viewIndex];
2105
2380
  if (!di)
2106
2381
  return;
2382
+ // ── Issues tab: backlog actions only (no worktree ops) ──────
2383
+ if (!isTrees) {
2384
+ const tracker = getIssueTracker(repoRootRef.current);
2385
+ const canMutate = tracker.canMutate === true;
2386
+ if (input === "w") {
2387
+ if (di.worktree?.sessionId) {
2388
+ dispatch({
2389
+ type: "SET_ACTION_MESSAGE",
2390
+ message: "Session active — switch to the Trees tab to resume.",
2391
+ });
2392
+ return;
2393
+ }
2394
+ dispatch({ type: "SET_OVERLAY", overlay: "mode-select" });
2395
+ return;
2396
+ }
2397
+ if (input === "n") {
2398
+ if (!canMutate) {
2399
+ dispatch({
2400
+ type: "SET_ACTION_MESSAGE",
2401
+ message: `${tracker.displayName} issues can't be created from santree`,
2402
+ });
2403
+ return;
2404
+ }
2405
+ dispatch({
2406
+ type: "ISSUE_FORM_OPEN",
2407
+ mode: "create",
2408
+ id: null,
2409
+ title: "",
2410
+ description: "",
2411
+ });
2412
+ return;
2413
+ }
2414
+ if (input === "e") {
2415
+ if (!canMutate) {
2416
+ dispatch({
2417
+ type: "SET_ACTION_MESSAGE",
2418
+ message: `${tracker.displayName} issues can't be edited from santree`,
2419
+ });
2420
+ return;
2421
+ }
2422
+ dispatch({
2423
+ type: "ISSUE_FORM_OPEN",
2424
+ mode: "edit",
2425
+ id: di.issue.identifier,
2426
+ title: di.issue.title,
2427
+ description: di.issue.description ?? "",
2428
+ });
2429
+ return;
2430
+ }
2431
+ if (input === "d") {
2432
+ if (!canMutate) {
2433
+ dispatch({ type: "SET_ACTION_MESSAGE", message: "Nothing to delete here" });
2434
+ return;
2435
+ }
2436
+ dispatch({ type: "ISSUE_DELETE_OPEN" });
2437
+ return;
2438
+ }
2439
+ if (input === "o") {
2440
+ if (!di.issue.url) {
2441
+ dispatch({ type: "SET_ACTION_MESSAGE", message: "No issue URL available" });
2442
+ return;
2443
+ }
2444
+ if (openUrl(di.issue.url)) {
2445
+ dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened in browser" });
2446
+ }
2447
+ return;
2448
+ }
2449
+ return; // backlog has no worktree/PR actions
2450
+ }
2451
+ // ── Trees tab: worktree-in-progress actions ─────────────────
2107
2452
  // Work
2108
2453
  if (input === "w") {
2109
2454
  if (di.worktree?.sessionId) {
@@ -2323,6 +2668,10 @@ export default function Dashboard() {
2323
2668
  }
2324
2669
  }, {
2325
2670
  isActive: state.overlay !== "context-input" &&
2671
+ // Issue form title/description are owned by MultilineTextArea;
2672
+ // only the "saving" phase needs the outer handler (a no-op
2673
+ // swallow), so disabling it for the whole overlay is fine.
2674
+ state.overlay !== "issue-form" &&
2326
2675
  (state.overlay !== "pr-create" || state.prCreatePhase !== "review") &&
2327
2676
  (state.overlay !== "commit" || state.commitPhase !== "awaiting-message"),
2328
2677
  });
@@ -2333,9 +2682,29 @@ export default function Dashboard() {
2333
2682
  if (state.error) {
2334
2683
  return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsxs(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, flexDirection: "column", children: [_jsxs(Text, { color: "red", bold: true, children: ["Error: ", state.error] }), _jsx(Text, { dimColor: true, children: "Press R to retry or q to quit" })] }) }));
2335
2684
  }
2336
- const selectedIssue = state.flatIssues[state.selectedIndex] ?? null;
2685
+ // The active issue/tree row drives the detail pane and action row.
2686
+ const selectedIssue = (state.activeTab === "trees"
2687
+ ? state.flatTrees[state.treeSelectedIndex]
2688
+ : state.flatIssues[state.selectedIndex]) ?? null;
2337
2689
  const selectedReview = state.flatReviews[state.reviewSelectedIndex] ?? null;
2338
- return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "santree" }), _jsxs(Text, { dimColor: true, children: [" ", "v", version] }), updateAvailable && latestVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ v", latestVersion, " available — `santree update`"] })) : null, CLAUDE_VERSION ? (_jsxs(Text, { dimColor: true, children: [" · claude ", CLAUDE_VERSION] })) : null, claudeUpdateAvailable && latestClaudeVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ ", latestClaudeVersion] })) : null, state.refreshing ? _jsx(Text, { dimColor: true, children: " · refreshing…" }) : null, state.actionMessage ? (_jsxs(Text, { color: "yellow", children: [" · ", state.actionMessage] })) : null] }), _jsxs(Box, { paddingX: 1, children: [_jsx(Tab, { active: state.activeTab === "issues", label: `1 Issues (${state.flatIssues.length})`, mode: theme.mode }), _jsx(Text, { children: " " }), _jsx(Tab, { active: state.activeTab === "reviews", label: `2 Reviews (${state.flatReviews.length})`, mode: theme.mode })] }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", flexDirection: "column", children: [state.overlay === "help" ? (_jsx(HelpOverlay, { width: innerWidth, height: contentHeight })) : 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 === "context-input" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", paddingX: 2, width: Math.min(columns - 8, 100), children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Extra context for ", state.contextInputMode] }), _jsx(Text, { dimColor: true, children: "Optional \u2014 appended to the prompt before launching Claude" }), _jsx(Text, { children: " " }), _jsx(MultilineTextArea, { value: state.contextInputValue, onChange: (v) => dispatch({ type: "CONTEXT_INPUT_CHANGE", value: v }), onSubmit: () => {
2690
+ const activeTracker = getIssueTracker(repoRootRef.current);
2691
+ return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "santree" }), _jsxs(Text, { dimColor: true, children: [" ", "v", version] }), updateAvailable && latestVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ v", latestVersion, " available — `santree update`"] })) : null, CLAUDE_VERSION ? (_jsxs(Text, { dimColor: true, children: [" · claude ", CLAUDE_VERSION] })) : null, claudeUpdateAvailable && latestClaudeVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ ", latestClaudeVersion] })) : null, state.refreshing ? _jsx(Text, { dimColor: true, children: " · refreshing…" }) : null, state.actionMessage ? (_jsxs(Text, { color: "yellow", children: [" · ", state.actionMessage] })) : null] }), _jsxs(Box, { paddingX: 1, children: [_jsx(Tab, { active: state.activeTab === "issues", label: `1 Issues (${state.flatIssues.length})`, mode: theme.mode }), _jsx(Text, { children: " " }), _jsx(Tab, { active: state.activeTab === "trees", label: `2 Trees (${state.flatTrees.length})`, mode: theme.mode }), _jsx(Text, { children: " " }), _jsx(Tab, { active: state.activeTab === "reviews", label: `3 Reviews (${state.flatReviews.length})`, mode: theme.mode })] }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", flexDirection: "column", children: [state.overlay === "help" ? (_jsx(HelpOverlay, { width: innerWidth, height: contentHeight })) : state.overlay === "tracker-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: state.trackerSelectPhase === "linear-org" ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Select a Linear workspace:" }), _jsx(Text, { children: " " }), state.trackerSelectOrgs.map((org, i) => {
2692
+ const sel = i === state.trackerSelectIndex;
2693
+ return (_jsxs(Text, { color: sel ? "cyan" : undefined, bold: sel, children: [sel ? "> " : " ", org.name, " (", org.slug, ")"] }, org.slug));
2694
+ }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to link, ESC to go back" })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Select an issue tracker for this repo:" }), _jsx(Text, { children: " " }), [
2695
+ { label: "Local", hint: "built-in, file-based — no account needed" },
2696
+ { label: "Linear", hint: "OAuth workspace" },
2697
+ { label: "GitHub", hint: "GitHub Issues via gh CLI" },
2698
+ ].map((t, i) => {
2699
+ const sel = i === state.trackerSelectIndex;
2700
+ return (_jsxs(Text, { children: [_jsxs(Text, { color: sel ? "cyan" : undefined, bold: sel, children: [sel ? "> " : " ", t.label] }), _jsxs(Text, { dimColor: true, children: [" \u2014 ", t.hint] })] }, t.label));
2701
+ }), _jsx(Text, { children: " " }), state.trackerSelectMessage ? (_jsx(Text, { color: "yellow", children: state.trackerSelectMessage })) : null, _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to select, q to quit" })] })) }) })) : state.overlay === "confirm-delete-issue" ? (_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: "Delete issue?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [selectedIssue?.issue.identifier, " ", selectedIssue?.issue.title] }), _jsx(Text, { dimColor: true, children: "This removes the issue file from .santree/issues/" }), _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 === "issue-form" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", paddingX: 2, width: Math.min(columns - 8, 100), children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.issueFormMode === "edit" ? `Edit ${state.issueFormId}` : "New issue", " · ", state.issueFormPhase === "title"
2702
+ ? "title"
2703
+ : state.issueFormPhase === "description"
2704
+ ? "description"
2705
+ : "saving…"] }), state.issueFormError ? (_jsx(Text, { color: "red", children: state.issueFormError })) : (_jsx(Text, { dimColor: true, children: state.issueFormPhase === "title"
2706
+ ? "First line is the title"
2707
+ : "Markdown body — Ctrl+D to save" })), _jsx(Text, { children: " " }), state.issueFormPhase === "saving" ? (_jsx(Text, { color: "cyan", children: "Saving\u2026" })) : state.issueFormPhase === "title" ? (_jsx(MultilineTextArea, { value: state.issueFormTitle, onChange: (v) => dispatch({ type: "ISSUE_FORM_TITLE", title: v }), onSubmit: () => dispatch({ type: "ISSUE_FORM_PHASE", phase: "description" }), onCancel: () => dispatch({ type: "ISSUE_FORM_CLOSE" }), width: Math.min(columns - 8, 100), height: 3, placeholder: "Issue title\u2026" })) : (_jsx(MultilineTextArea, { value: state.issueFormDescription, onChange: (v) => dispatch({ type: "ISSUE_FORM_DESC", description: v }), onSubmit: () => void submitIssueForm(), onCancel: () => dispatch({ type: "ISSUE_FORM_CLOSE" }), width: Math.min(columns - 8, 100), height: 10, placeholder: "Description (optional)\u2026" })), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "Ctrl+D" }), state.issueFormPhase === "title" ? " next · " : " save · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+G" }), " cancel"] })] }) })) : 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 === "context-input" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", paddingX: 2, width: Math.min(columns - 8, 100), children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Extra context for ", state.contextInputMode] }), _jsx(Text, { dimColor: true, children: "Optional \u2014 appended to the prompt before launching Claude" }), _jsx(Text, { children: " " }), _jsx(MultilineTextArea, { value: state.contextInputValue, onChange: (v) => dispatch({ type: "CONTEXT_INPUT_CHANGE", value: v }), onSubmit: () => {
2339
2708
  const mode = state.contextInputMode;
2340
2709
  const ctx = state.contextInputValue;
2341
2710
  dispatch({ type: "CONTEXT_INPUT_DONE" });
@@ -2346,17 +2715,22 @@ export default function Dashboard() {
2346
2715
  const defaultBranch = getDefaultBranch();
2347
2716
  const label = branch === defaultBranch ? `${branch} (default)` : branch;
2348
2717
  return (_jsx(Text, { children: _jsxs(Text, { color: selected ? "cyan" : undefined, bold: selected, children: [selected ? "> " : " ", label] }) }, branch));
2349
- }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to select, 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 === "diff" ? (_jsx(DiffOverlay, { width: innerWidth, height: contentHeight, ticketId: state.diffTicketId ?? "", baseBranch: state.diffBaseBranch ?? "", files: state.diffFiles, fileIndex: state.diffFileIndex, fileScrollOffset: state.diffFileScrollOffset, content: state.diffContent, contentScrollOffset: state.diffContentScrollOffset, loadingFiles: state.diffLoadingFiles, loadingContent: state.diffLoadingContent, error: state.diffError, selectionBg: theme.selectionBg, leftWidthOverride: diffLeftWidth ?? undefined, pendingDiscard: state.diffPendingDiscard })) : 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: state.activeTab === "reviews" ? (_jsx(ReviewList, { flatReviews: state.flatReviews, selectedIndex: state.reviewSelectedIndex, scrollOffset: state.reviewListScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) : state.flatIssues.length === 0 ? (_jsx(Box, { width: leftWidth, height: contentHeight, justifyContent: "center", alignItems: "center", children: _jsx(Text, { color: "yellow", children: "No active issues" }) })) : (_jsx(IssueList, { groups: state.groups, flatIssues: state.flatIssues, selectedIndex: state.selectedIndex, scrollOffset: state.listScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) }), _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.activeTab === "reviews" && state.creatingForTicket ? (_jsxs(Box, { flexDirection: "column", width: rightWidth, height: contentHeight, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Setting up worktree for ", state.creatingForTicket, "..."] }), state.creationLogs
2718
+ }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to select, 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 === "diff" ? (_jsx(DiffOverlay, { width: innerWidth, height: contentHeight, ticketId: state.diffTicketId ?? "", baseBranch: state.diffBaseBranch ?? "", files: state.diffFiles, fileIndex: state.diffFileIndex, fileScrollOffset: state.diffFileScrollOffset, content: state.diffContent, contentScrollOffset: state.diffContentScrollOffset, loadingFiles: state.diffLoadingFiles, loadingContent: state.diffLoadingContent, error: state.diffError, selectionBg: theme.selectionBg, leftWidthOverride: diffLeftWidth ?? undefined, pendingDiscard: state.diffPendingDiscard })) : 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: state.activeTab === "reviews" ? (_jsx(ReviewList, { flatReviews: state.flatReviews, selectedIndex: state.reviewSelectedIndex, scrollOffset: state.reviewListScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) : (state.activeTab === "trees" ? state.flatTrees : state.flatIssues).length ===
2719
+ 0 ? (_jsxs(Box, { width: leftWidth, height: contentHeight, justifyContent: "center", alignItems: "center", flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: state.activeTab === "trees" ? "No worktrees yet" : "No active issues" }), state.activeTab === "issues" && activeTracker.canMutate ? (_jsx(Text, { dimColor: true, children: "Press n to create one" })) : null] })) : (_jsx(IssueList, { groups: state.activeTab === "trees" ? state.treeGroups : state.groups, flatIssues: state.activeTab === "trees" ? state.flatTrees : state.flatIssues, selectedIndex: state.activeTab === "trees" ? state.treeSelectedIndex : state.selectedIndex, scrollOffset: state.activeTab === "trees"
2720
+ ? state.treeListScrollOffset
2721
+ : state.listScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) }), _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.activeTab === "reviews" && state.creatingForTicket ? (_jsxs(Box, { flexDirection: "column", width: rightWidth, height: contentHeight, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Setting up worktree for ", state.creatingForTicket, "..."] }), state.creationLogs
2350
2722
  .split("\n")
2351
2723
  .slice(-(contentHeight - 1))
2352
- .map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : 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, body: state.prCreateBody, title: state.prCreateTitle, dispatch: dispatch })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay, trackerName: getIssueTracker(repoRootRef.current).displayName }) })] })) })] })] }));
2724
+ .map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : 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, body: state.prCreateBody, title: state.prCreateTitle, dispatch: dispatch })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.activeTab === "trees"
2725
+ ? state.treeDetailScrollOffset
2726
+ : state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay, trackerName: activeTracker.displayName, canMutate: activeTracker.canMutate === true }) })] })) })] })] }));
2353
2727
  }
2354
2728
  /**
2355
2729
  * Renders the per-issue action key hints (Resume / Editor / View diff / …)
2356
2730
  * lifted out of the detail panels so they sit on the same row as the global
2357
2731
  * command bar. Empty when nothing is selected.
2358
2732
  */
2359
- function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, trackerName, }) {
2733
+ function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, trackerName, canMutate, }) {
2360
2734
  // During the diff overlay, none of the per-issue actions apply (View diff
2361
2735
  // is what got us here, Commit/PR/etc. need the detail panel context). Keep
2362
2736
  // the row blank so the diff-specific CommandBar reads cleanly.
@@ -2367,7 +2741,7 @@ function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, trackerN
2367
2741
  ? buildReviewActions(selectedReview)
2368
2742
  : []
2369
2743
  : selectedIssue
2370
- ? buildIssueActions(selectedIssue, trackerName)
2744
+ ? buildIssueActions(selectedIssue, trackerName, { tab: activeTab, canMutate })
2371
2745
  : [];
2372
2746
  if (items.length === 0)
2373
2747
  return _jsx(Text, { children: " " });