@ogpoyraz/wtx 0.9.1 → 0.9.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.
Files changed (2) hide show
  1. package/dist/cli.mjs +652 -370
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -25068,7 +25068,7 @@ var init_owner = __esm(() => {
25068
25068
  });
25069
25069
 
25070
25070
  // src/lib/workspace.ts
25071
- import fs23 from "fs";
25071
+ import fs24 from "fs";
25072
25072
  import path29 from "path";
25073
25073
  function getWorkspaceRoot(config2) {
25074
25074
  return path29.resolve(expandTilde(config2.workspace_root ?? path29.join(config2.root, "wtx-workspaces")));
@@ -25119,12 +25119,12 @@ function assertNoPathNesting(workspacePath, members) {
25119
25119
  async function writeManifest(workspacePath, manifest) {
25120
25120
  const manifestPath = path29.join(workspacePath, MANIFEST_FILE);
25121
25121
  const tmpPath = path29.join(workspacePath, `${MANIFEST_FILE}.${process.pid}.${Date.now()}.tmp`);
25122
- await fs23.promises.writeFile(tmpPath, `${JSON.stringify(manifest, null, 2)}
25122
+ await fs24.promises.writeFile(tmpPath, `${JSON.stringify(manifest, null, 2)}
25123
25123
  `, "utf8");
25124
- await fs23.promises.rename(tmpPath, manifestPath);
25124
+ await fs24.promises.rename(tmpPath, manifestPath);
25125
25125
  }
25126
25126
  async function readManifest(workspacePath) {
25127
- const raw = await fs23.promises.readFile(path29.join(workspacePath, MANIFEST_FILE), "utf8");
25127
+ const raw = await fs24.promises.readFile(path29.join(workspacePath, MANIFEST_FILE), "utf8");
25128
25128
  const parsed = JSON.parse(raw);
25129
25129
  if (parsed.version !== 1) {
25130
25130
  throw new Error(`Unsupported workspace manifest version: ${parsed.version}`);
@@ -25139,7 +25139,7 @@ async function writeAgentsFile(workspacePath, linkNames) {
25139
25139
  ...linkNames.map((name) => `- ${name}`),
25140
25140
  ""
25141
25141
  ];
25142
- await fs23.promises.writeFile(path29.join(workspacePath, AGENTS_FILE), lines.join(`
25142
+ await fs24.promises.writeFile(path29.join(workspacePath, AGENTS_FILE), lines.join(`
25143
25143
  `), "utf8");
25144
25144
  }
25145
25145
  async function createMemberSymlinks(workspacePath, members) {
@@ -25149,13 +25149,13 @@ async function createMemberSymlinks(workspacePath, members) {
25149
25149
  const linkName = memberLinkName(member, taken);
25150
25150
  const linkPath = path29.join(workspacePath, linkName);
25151
25151
  const target = path29.resolve(member.path);
25152
- await fs23.promises.symlink(target, linkPath, process.platform === "win32" ? "junction" : "dir");
25152
+ await fs24.promises.symlink(target, linkPath, process.platform === "win32" ? "junction" : "dir");
25153
25153
  linkNames.push(linkName);
25154
25154
  }
25155
25155
  return linkNames;
25156
25156
  }
25157
25157
  async function workspaceLinkNames(workspacePath) {
25158
- const entries = await fs23.promises.readdir(workspacePath, { withFileTypes: true });
25158
+ const entries = await fs24.promises.readdir(workspacePath, { withFileTypes: true });
25159
25159
  return entries.filter((entry) => entry.name !== MANIFEST_FILE && entry.name !== AGENTS_FILE && entry.isSymbolicLink()).map((entry) => entry.name).sort();
25160
25160
  }
25161
25161
  async function rebuildAgentsFile(workspacePath) {
@@ -25165,7 +25165,7 @@ async function createWorkspace(opts) {
25165
25165
  validateWorkspaceName(opts.name);
25166
25166
  const workspacePath = workspacePathFor(opts.name, opts.config);
25167
25167
  assertNoPathNesting(workspacePath, opts.members);
25168
- await fs23.promises.mkdir(workspacePath, { recursive: true });
25168
+ await fs24.promises.mkdir(workspacePath, { recursive: true });
25169
25169
  const linkNames = await createMemberSymlinks(workspacePath, opts.members);
25170
25170
  await writeManifest(workspacePath, {
25171
25171
  version: 1,
@@ -25183,7 +25183,7 @@ async function addMember(opts) {
25183
25183
  const taken = new Set(await workspaceLinkNames(opts.workspacePath));
25184
25184
  const linkName = memberLinkName(opts.member, taken);
25185
25185
  const target = path29.resolve(opts.member.path);
25186
- await fs23.promises.symlink(target, path29.join(opts.workspacePath, linkName), process.platform === "win32" ? "junction" : "dir");
25186
+ await fs24.promises.symlink(target, path29.join(opts.workspacePath, linkName), process.platform === "win32" ? "junction" : "dir");
25187
25187
  manifest.members.push({ repo: opts.member.repo, branch: opts.member.branch });
25188
25188
  await writeManifest(opts.workspacePath, manifest);
25189
25189
  await rebuildAgentsFile(opts.workspacePath);
@@ -25197,7 +25197,7 @@ async function removeMember(opts) {
25197
25197
  const linkNames = await workspaceLinkNames(opts.workspacePath);
25198
25198
  for (const linkName of linkNames) {
25199
25199
  if (linkName === opts.repo || linkName === `${opts.repo}-${opts.branch.replace(/[\\/]+/g, "-")}`) {
25200
- await fs23.promises.unlink(path29.join(opts.workspacePath, linkName));
25200
+ await fs24.promises.unlink(path29.join(opts.workspacePath, linkName));
25201
25201
  break;
25202
25202
  }
25203
25203
  }
@@ -25206,7 +25206,7 @@ async function removeMember(opts) {
25206
25206
  }
25207
25207
  async function listWorkspaces(workspaceRoot) {
25208
25208
  try {
25209
- const entries = await fs23.promises.readdir(path29.resolve(expandTilde(workspaceRoot)), { withFileTypes: true });
25209
+ const entries = await fs24.promises.readdir(path29.resolve(expandTilde(workspaceRoot)), { withFileTypes: true });
25210
25210
  const workspaces = [];
25211
25211
  for (const entry of entries) {
25212
25212
  if (!entry.isDirectory())
@@ -25235,7 +25235,7 @@ async function verifyLink(linkPath, visited) {
25235
25235
  visited.add(resolvedLink);
25236
25236
  let stats;
25237
25237
  try {
25238
- stats = await fs23.promises.lstat(linkPath);
25238
+ stats = await fs24.promises.lstat(linkPath);
25239
25239
  } catch {
25240
25240
  return "broken";
25241
25241
  }
@@ -25243,13 +25243,13 @@ async function verifyLink(linkPath, visited) {
25243
25243
  return "ok";
25244
25244
  let rawTarget;
25245
25245
  try {
25246
- rawTarget = await fs23.promises.readlink(linkPath);
25246
+ rawTarget = await fs24.promises.readlink(linkPath);
25247
25247
  } catch {
25248
25248
  return "broken";
25249
25249
  }
25250
25250
  const target = path29.isAbsolute(rawTarget) ? rawTarget : path29.resolve(path29.dirname(linkPath), rawTarget);
25251
25251
  try {
25252
- const targetStats = await fs23.promises.lstat(target);
25252
+ const targetStats = await fs24.promises.lstat(target);
25253
25253
  if (!targetStats.isSymbolicLink())
25254
25254
  return "ok";
25255
25255
  } catch {
@@ -25260,7 +25260,7 @@ async function verifyLink(linkPath, visited) {
25260
25260
  async function verify(workspacePath) {
25261
25261
  const result = { ok: true, broken: [], cycles: [] };
25262
25262
  try {
25263
- const entries = await fs23.promises.readdir(workspacePath, { withFileTypes: true });
25263
+ const entries = await fs24.promises.readdir(workspacePath, { withFileTypes: true });
25264
25264
  for (const entry of entries) {
25265
25265
  if (entry.name === MANIFEST_FILE || entry.name === AGENTS_FILE || !entry.isSymbolicLink())
25266
25266
  continue;
@@ -25503,20 +25503,22 @@ async function fetchWorktreeData(opts, scope) {
25503
25503
  }
25504
25504
  let repos;
25505
25505
  try {
25506
- repos = resolveRepos(config2);
25506
+ if (scope && scope.length > 0) {
25507
+ repos = resolveRepos(config2, scope);
25508
+ } else {
25509
+ repos = resolveRepos(config2, Object.keys(config2.repos));
25510
+ }
25507
25511
  } catch (err) {
25508
25512
  throw new Error(`Failed to resolve repos: ${errorMessage3(err)}`);
25509
25513
  }
25510
- if (scope) {
25511
- const scopeSet = new Set(scope);
25512
- repos = repos.filter((r) => scopeSet.has(r.name));
25513
- }
25514
25514
  const semaphore = new Semaphore(4);
25515
25515
  const allRows = [];
25516
25516
  const processRepo = async (repo) => {
25517
25517
  await semaphore.acquire();
25518
+ let mainBranch = config2.default_main_branch;
25519
+ let hasMain = false;
25518
25520
  try {
25519
- const mainBranch = await resolveMainBranch(repo, config2);
25521
+ mainBranch = await resolveMainBranch(repo, config2);
25520
25522
  const wts = await getWorktreeList(repo.mainPath);
25521
25523
  let stackMetadata = { version: 1, branches: {} };
25522
25524
  try {
@@ -25531,7 +25533,9 @@ async function fetchWorktreeData(opts, scope) {
25531
25533
  if (wt.isBare)
25532
25534
  continue;
25533
25535
  let branch = wt.branch;
25534
- const isMainCheckout = wt.path === repo.mainPath;
25536
+ const isMainCheckout = safeResolve(wt.path) === safeResolve(repo.mainPath);
25537
+ if (isMainCheckout)
25538
+ hasMain = true;
25535
25539
  if (isMainCheckout && !branch) {
25536
25540
  branch = mainBranch;
25537
25541
  }
@@ -25562,6 +25566,7 @@ async function fetchWorktreeData(opts, scope) {
25562
25566
  };
25563
25567
  if (isMainCheckout) {
25564
25568
  allRows.push(row);
25569
+ prRows.push(row);
25565
25570
  continue;
25566
25571
  }
25567
25572
  await Promise.allSettled([
@@ -25622,8 +25627,56 @@ async function fetchWorktreeData(opts, scope) {
25622
25627
  if (forge && branches.length > 0) {
25623
25628
  prFetchJobs.push({ repo, forge, mainBranch, rows: prRows, branches, stackMetadata });
25624
25629
  }
25630
+ if (!hasMain) {
25631
+ allRows.push({
25632
+ repoName: repo.name,
25633
+ branch: mainBranch,
25634
+ path: repo.mainPath,
25635
+ commitShort: "",
25636
+ isMainCheckout: true,
25637
+ isLocked: false,
25638
+ isPrunable: false,
25639
+ isBare: false,
25640
+ dirtyFiles: [],
25641
+ ahead: null,
25642
+ behind: null,
25643
+ prNumber: null,
25644
+ prState: null,
25645
+ prChecks: null,
25646
+ prUrl: null,
25647
+ owner: null,
25648
+ rebaseStatus: null,
25649
+ depsStrategy: "none",
25650
+ base: undefined,
25651
+ baseChanged: false
25652
+ });
25653
+ }
25625
25654
  } catch (err) {
25626
25655
  warnings.push({ repoName: repo.name, message: `Failed to process repo ${repo.name}: ${errorMessage3(err)}` });
25656
+ if (!hasMain) {
25657
+ allRows.push({
25658
+ repoName: repo.name,
25659
+ branch: mainBranch,
25660
+ path: repo.mainPath,
25661
+ commitShort: "",
25662
+ isMainCheckout: true,
25663
+ isLocked: false,
25664
+ isPrunable: false,
25665
+ isBare: false,
25666
+ dirtyFiles: [],
25667
+ ahead: null,
25668
+ behind: null,
25669
+ prNumber: null,
25670
+ prState: null,
25671
+ prChecks: null,
25672
+ prUrl: null,
25673
+ owner: null,
25674
+ rebaseStatus: null,
25675
+ depsStrategy: "none",
25676
+ base: undefined,
25677
+ baseChanged: false
25678
+ });
25679
+ }
25627
25680
  } finally {
25628
25681
  semaphore.release();
25629
25682
  }
@@ -25650,6 +25703,7 @@ var init_data = __esm(() => {
25650
25703
  init_owner();
25651
25704
  init_types2();
25652
25705
  init_stack();
25706
+ init_path_safety();
25653
25707
  PR_CACHE_TTL_MS = 5 * 60 * 1000;
25654
25708
  prCache = new Map;
25655
25709
  });
@@ -25946,7 +26000,12 @@ var init_useWorktrees = __esm(() => {
25946
26000
  import { RGBA } from "@opentui/core";
25947
26001
  import { createContext, useContext } from "react";
25948
26002
  function useTheme() {
25949
- return useContext(ThemeContext);
26003
+ try {
26004
+ const ctx = useContext(ThemeContext);
26005
+ return ctx ?? tokens;
26006
+ } catch {
26007
+ return tokens;
26008
+ }
25950
26009
  }
25951
26010
  function truncateBranch(name, max = 42) {
25952
26011
  if (name.length <= max)
@@ -26101,18 +26160,18 @@ async function openInBrowser(url2) {
26101
26160
  // src/tui/components/WorktreeTable.tsx
26102
26161
  import { useEffect as useEffect2, useRef as useRef3 } from "react";
26103
26162
  import { jsx, jsxs } from "@opentui/react/jsx-runtime";
26104
- function statusBadge(row) {
26163
+ function statusBadge(row, theme) {
26105
26164
  if (row.isMainCheckout)
26106
- return { text: "[main]", fg: tokens.accent };
26165
+ return { text: "[main]", fg: theme.accent };
26107
26166
  if (row.isPrunable)
26108
- return { text: "missing", fg: tokens.error };
26167
+ return { text: "missing", fg: theme.error };
26109
26168
  if (row.isLocked)
26110
- return { text: "locked", fg: tokens.error };
26169
+ return { text: "locked", fg: theme.error };
26111
26170
  if (row.rebaseStatus)
26112
- return { text: "rebasing", fg: tokens.warning };
26171
+ return { text: "rebasing", fg: theme.warning };
26113
26172
  if (row.dirtyFiles.length > 0)
26114
- return { text: `dirty (${row.dirtyFiles.length})`, fg: tokens.warning };
26115
- return { text: "clean", fg: tokens.dim };
26173
+ return { text: `dirty (${row.dirtyFiles.length})`, fg: theme.warning };
26174
+ return { text: "clean", fg: theme.dim };
26116
26175
  }
26117
26176
  function WorktreeItem({
26118
26177
  row,
@@ -26125,6 +26184,7 @@ function WorktreeItem({
26125
26184
  onRowClick,
26126
26185
  onToggleSelect
26127
26186
  }) {
26187
+ const theme = useTheme();
26128
26188
  const prTap = useTapHandler(() => {
26129
26189
  if (row.prUrl)
26130
26190
  openInBrowser(row.prUrl);
@@ -26136,9 +26196,9 @@ function WorktreeItem({
26136
26196
  e.stopPropagation();
26137
26197
  onToggleSelect?.();
26138
26198
  });
26139
- const badge = indicator ? indicator.running ? { text: `${frame} ${indicator.verb}…`, fg: tokens.accent } : { text: `◌ ${indicator.verb}`, fg: tokens.dim } : row.isPendingCreate ? { text: `${frame} creating…`, fg: tokens.accent } : statusBadge(row);
26199
+ const badge = indicator ? indicator.running ? { text: `${frame} ${indicator.verb}…`, fg: theme.accent } : { text: `◌ ${indicator.verb}`, fg: theme.dim } : row.isPendingCreate ? { text: `${frame} creating…`, fg: theme.accent } : statusBadge(row, theme);
26140
26200
  const disabled = indicator !== undefined || row.isPendingCreate === true;
26141
- const primary = isSelected ? tokens.bright : disabled ? tokens.dim : tokens.fg;
26201
+ const primary = isSelected ? theme.bright : disabled ? theme.dim : theme.fg;
26142
26202
  const divergence = row.ahead !== null && row.behind !== null && (row.ahead > 0 || row.behind > 0) ? ` · ↑${row.ahead} ↓${row.behind}` : "";
26143
26203
  const ownerSegment = row.owner ? ` · by ${row.owner}` : "";
26144
26204
  const baseSegment = row.base ? ` · base ${row.base}` : "";
@@ -26156,7 +26216,7 @@ function WorktreeItem({
26156
26216
  return /* @__PURE__ */ jsxs("box", {
26157
26217
  id,
26158
26218
  flexDirection: "column",
26159
- backgroundColor: isSelected ? tokens.selectionBg : undefined,
26219
+ backgroundColor: isSelected ? theme.selectionBg : undefined,
26160
26220
  style: { paddingRight: 1 },
26161
26221
  ...isClickable && onRowClick ? rowTap : {},
26162
26222
  children: [
@@ -26173,18 +26233,18 @@ function WorktreeItem({
26173
26233
  width: 2,
26174
26234
  ...onToggleSelect ? gutterTap : {},
26175
26235
  children: /* @__PURE__ */ jsx("text", {
26176
- fg: tokens.accent,
26236
+ fg: theme.accent,
26177
26237
  children: isMultiSelected ? "✓ " : " "
26178
26238
  })
26179
26239
  }),
26180
26240
  /* @__PURE__ */ jsxs("text", {
26181
26241
  children: [
26182
26242
  /* @__PURE__ */ jsx("span", {
26183
- fg: tokens.dim,
26243
+ fg: theme.dim,
26184
26244
  children: hierarchyPrefix
26185
26245
  }),
26186
26246
  isWorkspaceMember && /* @__PURE__ */ jsx("span", {
26187
- fg: tokens.accent,
26247
+ fg: theme.accent,
26188
26248
  children: "⬡ "
26189
26249
  }),
26190
26250
  /* @__PURE__ */ jsx("span", {
@@ -26205,11 +26265,11 @@ function WorktreeItem({
26205
26265
  /* @__PURE__ */ jsxs("text", {
26206
26266
  children: [
26207
26267
  /* @__PURE__ */ jsx("span", {
26208
- fg: tokens.dim,
26268
+ fg: theme.dim,
26209
26269
  children: secondary
26210
26270
  }),
26211
26271
  row.prNumber !== null && secondary ? /* @__PURE__ */ jsx("span", {
26212
- fg: tokens.dim,
26272
+ fg: theme.dim,
26213
26273
  children: " · "
26214
26274
  }) : null
26215
26275
  ]
@@ -26219,26 +26279,26 @@ function WorktreeItem({
26219
26279
  children: /* @__PURE__ */ jsxs("text", {
26220
26280
  children: [
26221
26281
  /* @__PURE__ */ jsx("span", {
26222
- fg: tokens.accent,
26282
+ fg: theme.accent,
26223
26283
  children: `#${row.prNumber}`
26224
26284
  }),
26225
26285
  row.prState && /* @__PURE__ */ jsx("span", {
26226
- fg: tokens.dim,
26286
+ fg: theme.dim,
26227
26287
  children: ` ${row.prState}`
26228
26288
  }),
26229
26289
  row.prChecks && /* @__PURE__ */ jsx("span", {
26230
- fg: tokens.dim,
26290
+ fg: theme.dim,
26231
26291
  children: ` (${row.prChecks})`
26232
26292
  }),
26233
26293
  row.prUrl && /* @__PURE__ */ jsx("span", {
26234
- fg: tokens.dim,
26294
+ fg: theme.dim,
26235
26295
  children: " ↗"
26236
26296
  })
26237
26297
  ]
26238
26298
  })
26239
26299
  }),
26240
26300
  row.prNumber === null && row.prState === "FETCHING" && /* @__PURE__ */ jsxs("text", {
26241
- fg: tokens.dim,
26301
+ fg: theme.dim,
26242
26302
  children: [
26243
26303
  secondary ? " · " : "",
26244
26304
  "PR …"
@@ -26247,11 +26307,11 @@ function WorktreeItem({
26247
26307
  /* @__PURE__ */ jsxs("text", {
26248
26308
  children: [
26249
26309
  baseChangedSegment && /* @__PURE__ */ jsx("span", {
26250
- fg: tokens.warning,
26310
+ fg: theme.warning,
26251
26311
  children: baseChangedSegment
26252
26312
  }),
26253
26313
  rebaseSegment && /* @__PURE__ */ jsx("span", {
26254
- fg: tokens.error,
26314
+ fg: theme.error,
26255
26315
  children: rebaseSegment
26256
26316
  })
26257
26317
  ]
@@ -26262,6 +26322,7 @@ function WorktreeItem({
26262
26322
  });
26263
26323
  }
26264
26324
  function WorktreeTable({ blocks, selectedIndex, selection = new Set, frame, repoVerbs, rowVerbs, favorites = [], workspaceMemberSet = null, activeWorkspaceName = null, onRowClick, onToggleSelect }) {
26325
+ const theme = useTheme();
26265
26326
  const favoriteSet = new Set(favorites);
26266
26327
  const scrollRef = useRef3(null);
26267
26328
  useEffect2(() => {
@@ -26285,8 +26346,8 @@ function WorktreeTable({ blocks, selectedIndex, selection = new Set, frame, repo
26285
26346
  width: "100%",
26286
26347
  height: "100%",
26287
26348
  border: true,
26288
- borderColor: tokens.border,
26289
- focusedBorderColor: tokens.border,
26349
+ borderColor: theme.border,
26350
+ focusedBorderColor: theme.border,
26290
26351
  focusable: false,
26291
26352
  title: activeWorkspaceName ? `Repositories · workspace: ${activeWorkspaceName}` : "Repositories",
26292
26353
  paddingX: 1,
@@ -26295,7 +26356,7 @@ function WorktreeTable({ blocks, selectedIndex, selection = new Set, frame, repo
26295
26356
  e.stopPropagation();
26296
26357
  },
26297
26358
  children: blocks.length === 0 ? /* @__PURE__ */ jsx("text", {
26298
- fg: tokens.dim,
26359
+ fg: theme.dim,
26299
26360
  children: "No repositories configured."
26300
26361
  }) : blocks.map((block) => {
26301
26362
  const repoIndicator = repoVerbs.get(block.repoName);
@@ -26308,22 +26369,22 @@ function WorktreeTable({ blocks, selectedIndex, selection = new Set, frame, repo
26308
26369
  children: /* @__PURE__ */ jsxs("text", {
26309
26370
  children: [
26310
26371
  favoriteSet.has(block.repoName) && /* @__PURE__ */ jsx("span", {
26311
- fg: tokens.accent,
26372
+ fg: theme.accent,
26312
26373
  children: "★ "
26313
26374
  }),
26314
26375
  /* @__PURE__ */ jsx("span", {
26315
- fg: tokens.bright,
26376
+ fg: theme.bright,
26316
26377
  children: block.repoName
26317
26378
  }),
26318
26379
  block.rows.length > 0 && /* @__PURE__ */ jsx("span", {
26319
- fg: tokens.dim,
26380
+ fg: theme.dim,
26320
26381
  children: ` · ${block.rows.filter((r) => !r.isPendingCreate).length}`
26321
26382
  }),
26322
26383
  repoIndicator ? /* @__PURE__ */ jsx("span", {
26323
- fg: repoIndicator.running ? tokens.accent : tokens.dim,
26384
+ fg: repoIndicator.running ? theme.accent : theme.dim,
26324
26385
  children: repoIndicator.running ? ` ${frame} ${repoIndicator.verb}…` : ` ◌ ${repoIndicator.verb}`
26325
26386
  }) : block.rows.length === 0 ? /* @__PURE__ */ jsx("span", {
26326
- fg: tokens.accent,
26387
+ fg: theme.accent,
26327
26388
  children: ` ${frame} refreshing…`
26328
26389
  }) : null
26329
26390
  ]
@@ -26357,6 +26418,7 @@ function WorktreeTable({ blocks, selectedIndex, selection = new Set, frame, repo
26357
26418
  }
26358
26419
  var SECONDARY_INDENT = " ";
26359
26420
  var init_WorktreeTable = __esm(() => {
26421
+ init_theme();
26360
26422
  init_theme();
26361
26423
  init_use_tap();
26362
26424
  init_utils();
@@ -26372,6 +26434,7 @@ function TabItem({
26372
26434
  onSelect,
26373
26435
  onClose
26374
26436
  }) {
26437
+ const theme = useTheme();
26375
26438
  const tap = useTapHandler(() => onSelect(id));
26376
26439
  const closeTap = useTapHandler((e) => {
26377
26440
  e.stopPropagation();
@@ -26383,15 +26446,15 @@ function TabItem({
26383
26446
  ...tap,
26384
26447
  children: [
26385
26448
  /* @__PURE__ */ jsx2("text", {
26386
- fg: active ? tokens.accent : tokens.dim,
26449
+ fg: active ? theme.accent : theme.dim,
26387
26450
  attributes: active ? 1 : 0,
26388
26451
  children: label
26389
26452
  }),
26390
26453
  closable ? /* @__PURE__ */ jsx2("box", {
26391
26454
  ...closeTap,
26392
- style: { marginLeft: 1 },
26455
+ style: { marginLeft: 1, paddingLeft: 1, paddingRight: 1 },
26393
26456
  children: /* @__PURE__ */ jsx2("text", {
26394
- fg: active ? tokens.accent : tokens.dim,
26457
+ fg: theme.accent,
26395
26458
  children: "✕"
26396
26459
  })
26397
26460
  }) : null,
@@ -26426,6 +26489,7 @@ var init_TabBar = __esm(() => {
26426
26489
  import { useState as useState2, useEffect as useEffect3, useRef as useRef4, useCallback as useCallback3 } from "react";
26427
26490
  import { jsx as jsx3, jsxs as jsxs3 } from "@opentui/react/jsx-runtime";
26428
26491
  function TerminalView({ session, focused, onFocus, onSend, onResize, registerListener, unregisterListener }) {
26492
+ const theme = useTheme();
26429
26493
  const [draft, setDraft] = useState2("");
26430
26494
  const focusTap = useTapHandler(() => onFocus());
26431
26495
  const termRef = useRef4(null);
@@ -26568,7 +26632,7 @@ function TerminalView({ session, focused, onFocus, onSend, onResize, registerLis
26568
26632
  })
26569
26633
  }),
26570
26634
  session.exited !== null && /* @__PURE__ */ jsxs3("text", {
26571
- fg: tokens.warning,
26635
+ fg: theme.warning,
26572
26636
  children: [
26573
26637
  "— exited with code ",
26574
26638
  session.exited,
@@ -26576,7 +26640,7 @@ function TerminalView({ session, focused, onFocus, onSend, onResize, registerLis
26576
26640
  ]
26577
26641
  }),
26578
26642
  !focused && /* @__PURE__ */ jsxs3("text", {
26579
- fg: tokens.dim,
26643
+ fg: theme.dim,
26580
26644
  children: [
26581
26645
  "Click to focus · Ctrl+G or click table to unfocus · ",
26582
26646
  session.label,
@@ -26584,7 +26648,7 @@ function TerminalView({ session, focused, onFocus, onSend, onResize, registerLis
26584
26648
  ]
26585
26649
  }),
26586
26650
  focused && /* @__PURE__ */ jsx3("text", {
26587
- fg: tokens.accent,
26651
+ fg: theme.accent,
26588
26652
  children: "Focused PTY — all keys go to shell · Ctrl+G to unfocus · scroll wheel to view history"
26589
26653
  })
26590
26654
  ]
@@ -26602,12 +26666,12 @@ function TerminalView({ session, focused, onFocus, onSend, onResize, registerLis
26602
26666
  children: [
26603
26667
  spawnError && /* @__PURE__ */ jsx3("box", {
26604
26668
  border: true,
26605
- borderColor: tokens.error,
26669
+ borderColor: theme.error,
26606
26670
  paddingX: 1,
26607
26671
  paddingY: 0,
26608
26672
  marginBottom: 1,
26609
26673
  children: /* @__PURE__ */ jsx3("text", {
26610
- fg: tokens.error,
26674
+ fg: theme.error,
26611
26675
  children: spawnError
26612
26676
  })
26613
26677
  }),
@@ -26622,14 +26686,14 @@ function TerminalView({ session, focused, onFocus, onSend, onResize, registerLis
26622
26686
  onMouseScroll: handlePipeScroll,
26623
26687
  children: [
26624
26688
  lines.length === 0 ? /* @__PURE__ */ jsx3("text", {
26625
- fg: tokens.dim,
26689
+ fg: theme.dim,
26626
26690
  children: "No output yet — type a command below."
26627
26691
  }) : lines.slice(-500).map((line, idx) => /* @__PURE__ */ jsx3("text", {
26628
- fg: spawnError ? tokens.error : session.exited !== null ? tokens.dim : tokens.fg,
26692
+ fg: spawnError ? theme.error : session.exited !== null ? theme.dim : theme.fg,
26629
26693
  children: line || " "
26630
26694
  }, idx)),
26631
26695
  session.exited !== null && /* @__PURE__ */ jsxs3("text", {
26632
- fg: tokens.warning,
26696
+ fg: theme.warning,
26633
26697
  children: [
26634
26698
  "— exited with code ",
26635
26699
  session.exited,
@@ -26644,7 +26708,7 @@ function TerminalView({ session, focused, onFocus, onSend, onResize, registerLis
26644
26708
  width: "100%",
26645
26709
  children: [
26646
26710
  /* @__PURE__ */ jsx3("text", {
26647
- fg: tokens.dim,
26711
+ fg: theme.dim,
26648
26712
  children: "$"
26649
26713
  }),
26650
26714
  /* @__PURE__ */ jsx3("box", {
@@ -26666,11 +26730,11 @@ function TerminalView({ session, focused, onFocus, onSend, onResize, registerLis
26666
26730
  ]
26667
26731
  }),
26668
26732
  !focused && /* @__PURE__ */ jsx3("text", {
26669
- fg: tokens.dim,
26733
+ fg: theme.dim,
26670
26734
  children: "Click terminal or press 't' to focus · Ctrl+G or click table to unfocus"
26671
26735
  }),
26672
26736
  focused && /* @__PURE__ */ jsx3("text", {
26673
- fg: tokens.accent,
26737
+ fg: theme.accent,
26674
26738
  children: "Focused — typing goes to shell · Ctrl+G to unfocus · max 5 per worktree persist across switches"
26675
26739
  })
26676
26740
  ]
@@ -26694,12 +26758,16 @@ function TabPane({
26694
26758
  onClose,
26695
26759
  allSessionsFlat,
26696
26760
  terminalFocused,
26761
+ changesFocused,
26762
+ onChangesFocus,
26697
26763
  activeTabId,
26698
26764
  recentSessionIds,
26699
26765
  terminalSessions
26700
26766
  }) {
26701
- const borderColor = focused ? tokens.accent : tokens.border;
26767
+ const theme = useTheme();
26768
+ const borderColor = focused ? theme.accent : theme.border;
26702
26769
  const mountedSessions = allSessionsFlat?.filter((s) => activeTabId === s.id || recentSessionIds?.has(s.id));
26770
+ const nonSessionTabs = tabs.filter((t) => !t.closable);
26703
26771
  return /* @__PURE__ */ jsxs4("box", {
26704
26772
  flexDirection: "column",
26705
26773
  flexGrow: 1,
@@ -26724,13 +26792,28 @@ function TabPane({
26724
26792
  width: "100%",
26725
26793
  flexDirection: "column",
26726
26794
  children: [
26727
- tabs.map((t) => /* @__PURE__ */ jsx4("box", {
26728
- flexGrow: 1,
26729
- width: "100%",
26730
- flexDirection: "column",
26731
- visible: t.id === activeId,
26732
- children: t.render({ worktree: selectedRow, isActive: t.id === activeId, focused: focused && t.id === activeId })
26733
- }, t.id)),
26795
+ nonSessionTabs.map((t) => {
26796
+ const isChanges = t.id === "changes";
26797
+ const tabFocused = isChanges ? !!changesFocused : !terminalFocused && !changesFocused && t.id === activeId;
26798
+ const content = t.render({ worktree: selectedRow, isActive: t.id === activeId, focused: tabFocused });
26799
+ if (isChanges) {
26800
+ return /* @__PURE__ */ jsx4("box", {
26801
+ flexGrow: 1,
26802
+ width: "100%",
26803
+ flexDirection: "column",
26804
+ visible: t.id === activeId,
26805
+ onMouseDown: () => onChangesFocus?.(),
26806
+ children: content
26807
+ }, t.id);
26808
+ }
26809
+ return /* @__PURE__ */ jsx4("box", {
26810
+ flexGrow: 1,
26811
+ width: "100%",
26812
+ flexDirection: "column",
26813
+ visible: t.id === activeId,
26814
+ children: content
26815
+ }, t.id);
26816
+ }),
26734
26817
  mountedSessions?.map((s) => /* @__PURE__ */ jsx4("box", {
26735
26818
  flexGrow: 1,
26736
26819
  width: "100%",
@@ -26747,8 +26830,8 @@ function TabPane({
26747
26830
  unregisterListener: terminalSessions?.unregisterListener
26748
26831
  })
26749
26832
  }, s.id)),
26750
- tabs.length === 0 && (!mountedSessions || mountedSessions.length === 0) && /* @__PURE__ */ jsx4("text", {
26751
- fg: tokens.dim,
26833
+ nonSessionTabs.length === 0 && (!mountedSessions || mountedSessions.length === 0) && /* @__PURE__ */ jsx4("text", {
26834
+ fg: theme.dim,
26752
26835
  children: "No tab"
26753
26836
  })
26754
26837
  ]
@@ -26765,6 +26848,7 @@ var init_TabPane = __esm(() => {
26765
26848
  // src/tui/components/DetailsTab.tsx
26766
26849
  import { jsx as jsx5, jsxs as jsxs5, Fragment } from "@opentui/react/jsx-runtime";
26767
26850
  function DetailsContent({ selectedRow }) {
26851
+ const theme = useTheme();
26768
26852
  const prTap = useTapHandler(() => {
26769
26853
  if (selectedRow?.prUrl)
26770
26854
  openInBrowser(selectedRow.prUrl);
@@ -26777,7 +26861,7 @@ function DetailsContent({ selectedRow }) {
26777
26861
  justifyContent: "center",
26778
26862
  alignItems: "center",
26779
26863
  children: /* @__PURE__ */ jsx5("text", {
26780
- fg: tokens.dim,
26864
+ fg: theme.dim,
26781
26865
  children: "No worktree selected"
26782
26866
  })
26783
26867
  });
@@ -26814,11 +26898,11 @@ function DetailsContent({ selectedRow }) {
26814
26898
  /* @__PURE__ */ jsxs5("text", {
26815
26899
  children: [
26816
26900
  /* @__PURE__ */ jsx5("span", {
26817
- fg: tokens.dim,
26901
+ fg: theme.dim,
26818
26902
  children: "Repo:"
26819
26903
  }),
26820
26904
  /* @__PURE__ */ jsxs5("span", {
26821
- fg: tokens.fg,
26905
+ fg: theme.fg,
26822
26906
  children: [
26823
26907
  " ",
26824
26908
  repoName
@@ -26829,11 +26913,11 @@ function DetailsContent({ selectedRow }) {
26829
26913
  /* @__PURE__ */ jsxs5("text", {
26830
26914
  children: [
26831
26915
  /* @__PURE__ */ jsx5("span", {
26832
- fg: tokens.dim,
26916
+ fg: theme.dim,
26833
26917
  children: "Branch:"
26834
26918
  }),
26835
26919
  /* @__PURE__ */ jsxs5("span", {
26836
- fg: tokens.fg,
26920
+ fg: theme.fg,
26837
26921
  children: [
26838
26922
  " ",
26839
26923
  branch,
@@ -26846,11 +26930,11 @@ function DetailsContent({ selectedRow }) {
26846
26930
  /* @__PURE__ */ jsxs5("text", {
26847
26931
  children: [
26848
26932
  /* @__PURE__ */ jsx5("span", {
26849
- fg: tokens.dim,
26933
+ fg: theme.dim,
26850
26934
  children: "Path:"
26851
26935
  }),
26852
26936
  /* @__PURE__ */ jsxs5("span", {
26853
- fg: tokens.fg,
26937
+ fg: theme.fg,
26854
26938
  children: [
26855
26939
  " ",
26856
26940
  path37
@@ -26861,11 +26945,11 @@ function DetailsContent({ selectedRow }) {
26861
26945
  /* @__PURE__ */ jsxs5("text", {
26862
26946
  children: [
26863
26947
  /* @__PURE__ */ jsx5("span", {
26864
- fg: tokens.dim,
26948
+ fg: theme.dim,
26865
26949
  children: "Commit:"
26866
26950
  }),
26867
26951
  /* @__PURE__ */ jsxs5("span", {
26868
- fg: tokens.fg,
26952
+ fg: theme.fg,
26869
26953
  children: [
26870
26954
  " ",
26871
26955
  commitShort
@@ -26876,11 +26960,11 @@ function DetailsContent({ selectedRow }) {
26876
26960
  owner && /* @__PURE__ */ jsxs5("text", {
26877
26961
  children: [
26878
26962
  /* @__PURE__ */ jsx5("span", {
26879
- fg: tokens.dim,
26963
+ fg: theme.dim,
26880
26964
  children: "Owner:"
26881
26965
  }),
26882
26966
  /* @__PURE__ */ jsxs5("span", {
26883
- fg: tokens.fg,
26967
+ fg: theme.fg,
26884
26968
  children: [
26885
26969
  " ",
26886
26970
  owner
@@ -26891,11 +26975,11 @@ function DetailsContent({ selectedRow }) {
26891
26975
  /* @__PURE__ */ jsxs5("text", {
26892
26976
  children: [
26893
26977
  /* @__PURE__ */ jsx5("span", {
26894
- fg: tokens.dim,
26978
+ fg: theme.dim,
26895
26979
  children: "Status:"
26896
26980
  }),
26897
26981
  /* @__PURE__ */ jsxs5("span", {
26898
- fg: tokens.fg,
26982
+ fg: theme.fg,
26899
26983
  children: [
26900
26984
  " ",
26901
26985
  isLocked ? "LOCKED" : isPrunable ? "PRUNABLE" : "Active"
@@ -26906,11 +26990,11 @@ function DetailsContent({ selectedRow }) {
26906
26990
  base2 && /* @__PURE__ */ jsxs5("text", {
26907
26991
  children: [
26908
26992
  /* @__PURE__ */ jsx5("span", {
26909
- fg: tokens.dim,
26993
+ fg: theme.dim,
26910
26994
  children: "Base:"
26911
26995
  }),
26912
26996
  /* @__PURE__ */ jsxs5("span", {
26913
- fg: tokens.accent,
26997
+ fg: theme.accent,
26914
26998
  children: [
26915
26999
  " ",
26916
27000
  base2
@@ -26921,11 +27005,11 @@ function DetailsContent({ selectedRow }) {
26921
27005
  baseChanged && /* @__PURE__ */ jsxs5("text", {
26922
27006
  children: [
26923
27007
  /* @__PURE__ */ jsx5("span", {
26924
- fg: tokens.dim,
27008
+ fg: theme.dim,
26925
27009
  children: "Base state:"
26926
27010
  }),
26927
27011
  /* @__PURE__ */ jsx5("span", {
26928
- fg: tokens.warning,
27012
+ fg: theme.warning,
26929
27013
  children: " moved since recorded"
26930
27014
  })
26931
27015
  ]
@@ -26934,11 +27018,11 @@ function DetailsContent({ selectedRow }) {
26934
27018
  style: { marginTop: 1 },
26935
27019
  children: [
26936
27020
  /* @__PURE__ */ jsx5("span", {
26937
- fg: tokens.dim,
27021
+ fg: theme.dim,
26938
27022
  children: base2 ? "vs base:" : "vs main:"
26939
27023
  }),
26940
27024
  /* @__PURE__ */ jsxs5("span", {
26941
- fg: tokens.fg,
27025
+ fg: theme.fg,
26942
27026
  children: [
26943
27027
  " ",
26944
27028
  aheadBehindStr
@@ -26949,11 +27033,11 @@ function DetailsContent({ selectedRow }) {
26949
27033
  /* @__PURE__ */ jsxs5("text", {
26950
27034
  children: [
26951
27035
  /* @__PURE__ */ jsx5("span", {
26952
- fg: tokens.dim,
27036
+ fg: theme.dim,
26953
27037
  children: "Deps:"
26954
27038
  }),
26955
27039
  /* @__PURE__ */ jsxs5("span", {
26956
- fg: tokens.fg,
27040
+ fg: theme.fg,
26957
27041
  children: [
26958
27042
  " ",
26959
27043
  depsStrategy
@@ -26964,11 +27048,11 @@ function DetailsContent({ selectedRow }) {
26964
27048
  rebaseStatus && /* @__PURE__ */ jsxs5("text", {
26965
27049
  children: [
26966
27050
  /* @__PURE__ */ jsx5("span", {
26967
- fg: tokens.dim,
27051
+ fg: theme.dim,
26968
27052
  children: "Rebase:"
26969
27053
  }),
26970
27054
  /* @__PURE__ */ jsxs5("span", {
26971
- fg: tokens.error,
27055
+ fg: theme.error,
26972
27056
  children: [
26973
27057
  " ",
26974
27058
  rebaseStatus
@@ -26982,7 +27066,7 @@ function DetailsContent({ selectedRow }) {
26982
27066
  style: { marginTop: 1 },
26983
27067
  children: [
26984
27068
  /* @__PURE__ */ jsxs5("span", {
26985
- fg: tokens.accent,
27069
+ fg: theme.accent,
26986
27070
  children: [
26987
27071
  "PR #",
26988
27072
  prNumber,
@@ -26990,7 +27074,7 @@ function DetailsContent({ selectedRow }) {
26990
27074
  ]
26991
27075
  }),
26992
27076
  /* @__PURE__ */ jsxs5("span", {
26993
- fg: tokens.dim,
27077
+ fg: theme.dim,
26994
27078
  children: [
26995
27079
  " ",
26996
27080
  prState
@@ -27001,11 +27085,11 @@ function DetailsContent({ selectedRow }) {
27001
27085
  prChecks && /* @__PURE__ */ jsxs5("text", {
27002
27086
  children: [
27003
27087
  /* @__PURE__ */ jsx5("span", {
27004
- fg: tokens.dim,
27088
+ fg: theme.dim,
27005
27089
  children: "Checks:"
27006
27090
  }),
27007
27091
  /* @__PURE__ */ jsxs5("span", {
27008
- fg: tokens.fg,
27092
+ fg: theme.fg,
27009
27093
  children: [
27010
27094
  " ",
27011
27095
  prChecks
@@ -27019,15 +27103,15 @@ function DetailsContent({ selectedRow }) {
27019
27103
  selectable: false,
27020
27104
  children: [
27021
27105
  /* @__PURE__ */ jsx5("span", {
27022
- fg: tokens.dim,
27106
+ fg: theme.dim,
27023
27107
  children: "URL:"
27024
27108
  }),
27025
27109
  /* @__PURE__ */ jsx5("span", {
27026
- fg: tokens.accent,
27110
+ fg: theme.accent,
27027
27111
  children: ` ${prUrl}`
27028
27112
  }),
27029
27113
  /* @__PURE__ */ jsx5("span", {
27030
- fg: tokens.dim,
27114
+ fg: theme.dim,
27031
27115
  children: " ↗ click"
27032
27116
  })
27033
27117
  ]
@@ -27040,7 +27124,7 @@ function DetailsContent({ selectedRow }) {
27040
27124
  style: { marginTop: 1 },
27041
27125
  children: [
27042
27126
  /* @__PURE__ */ jsxs5("text", {
27043
- fg: tokens.warning,
27127
+ fg: theme.warning,
27044
27128
  children: [
27045
27129
  "Dirty Files (",
27046
27130
  dirtyFiles.length,
@@ -27048,14 +27132,14 @@ function DetailsContent({ selectedRow }) {
27048
27132
  ]
27049
27133
  }),
27050
27134
  dirtyFiles.slice(0, 20).map((file2, idx) => /* @__PURE__ */ jsxs5("text", {
27051
- fg: tokens.warning,
27135
+ fg: theme.warning,
27052
27136
  children: [
27053
27137
  " ",
27054
27138
  file2
27055
27139
  ]
27056
27140
  }, idx)),
27057
27141
  dirtyFiles.length > 20 && /* @__PURE__ */ jsxs5("text", {
27058
- fg: tokens.warning,
27142
+ fg: theme.warning,
27059
27143
  children: [
27060
27144
  " ...and ",
27061
27145
  dirtyFiles.length - 20,
@@ -27069,13 +27153,13 @@ function DetailsContent({ selectedRow }) {
27069
27153
  style: { marginTop: 1 },
27070
27154
  children: [
27071
27155
  /* @__PURE__ */ jsx5("text", {
27072
- fg: tokens.dim,
27156
+ fg: theme.dim,
27073
27157
  children: "Actions:"
27074
27158
  }),
27075
27159
  !isMainCheckout && /* @__PURE__ */ jsxs5(Fragment, {
27076
27160
  children: [
27077
27161
  /* @__PURE__ */ jsxs5("text", {
27078
- fg: tokens.dim,
27162
+ fg: theme.dim,
27079
27163
  children: [
27080
27164
  " i install deps (",
27081
27165
  depsStrategy,
@@ -27083,21 +27167,21 @@ function DetailsContent({ selectedRow }) {
27083
27167
  ]
27084
27168
  }),
27085
27169
  /* @__PURE__ */ jsx5("text", {
27086
- fg: tokens.dim,
27170
+ fg: theme.dim,
27087
27171
  children: " m rename worktree"
27088
27172
  })
27089
27173
  ]
27090
27174
  }),
27091
27175
  /* @__PURE__ */ jsx5("text", {
27092
- fg: tokens.dim,
27176
+ fg: theme.dim,
27093
27177
  children: " p pull branch"
27094
27178
  }),
27095
27179
  /* @__PURE__ */ jsx5("text", {
27096
- fg: tokens.dim,
27180
+ fg: theme.dim,
27097
27181
  children: " b rebase · s sync · o open in IDE · d remove"
27098
27182
  }),
27099
27183
  /* @__PURE__ */ jsx5("text", {
27100
- fg: tokens.dim,
27184
+ fg: theme.dim,
27101
27185
  children: " t new terminal session (max 5)"
27102
27186
  })
27103
27187
  ]
@@ -27111,6 +27195,8 @@ var init_DetailsTab = __esm(() => {
27111
27195
  });
27112
27196
 
27113
27197
  // src/lib/changes.ts
27198
+ import fs34 from "node:fs";
27199
+ import path37 from "node:path";
27114
27200
  function cacheKey(opts, filePath) {
27115
27201
  return [opts.repoPath, opts.branch, opts.scope, filePath ?? ""].join("\x00");
27116
27202
  }
@@ -27214,7 +27300,7 @@ async function getChangedFiles(opts) {
27214
27300
  return [];
27215
27301
  const key = cacheKey(opts);
27216
27302
  const cached2 = listCache.get(key);
27217
- if (!opts.invalidate && cached2?.head === head)
27303
+ if (!opts.invalidate && cached2?.head === head && opts.scope !== "worktree")
27218
27304
  return cached2.value;
27219
27305
  try {
27220
27306
  const baseArgs = await diffArgs(opts);
@@ -27231,6 +27317,29 @@ async function getChangedFiles(opts) {
27231
27317
  if (!await isSubmodule(opts.repoPath, file2.path))
27232
27318
  filtered.push(file2);
27233
27319
  }
27320
+ if (opts.scope === "worktree") {
27321
+ try {
27322
+ const untrackedRaw = await gitExec(["-C", opts.repoPath, "ls-files", "--others", "--exclude-standard", "-z"]);
27323
+ const untrackedPaths = untrackedRaw.split("\x00").filter(Boolean);
27324
+ for (const p of untrackedPaths) {
27325
+ if (filtered.some((f) => f.path === p))
27326
+ continue;
27327
+ if (await isSubmodule(opts.repoPath, p))
27328
+ continue;
27329
+ try {
27330
+ const full = path37.join(opts.repoPath, p);
27331
+ const buf = fs34.readFileSync(full);
27332
+ const binary = buf.includes(0);
27333
+ const str = binary ? "" : buf.toString("utf8");
27334
+ const added = binary ? 0 : str.split(`
27335
+ `).length;
27336
+ filtered.push({ path: p, status: "A", added, removed: 0, binary });
27337
+ } catch {
27338
+ filtered.push({ path: p, status: "A", added: 1, removed: 0, binary: false });
27339
+ }
27340
+ }
27341
+ } catch {}
27342
+ }
27234
27343
  listCache.set(key, { head, value: filtered });
27235
27344
  return filtered;
27236
27345
  } catch {
@@ -27266,7 +27375,26 @@ async function getFileDiff(opts) {
27266
27375
  const args = await diffArgs(opts, opts.filePath);
27267
27376
  if (!args)
27268
27377
  return empty;
27269
- const rawDiff = await gitExec(args);
27378
+ let rawDiff = await gitExec(args);
27379
+ if (!rawDiff && opts.scope === "worktree" && changedFile.status === "A") {
27380
+ try {
27381
+ const full = path37.join(opts.repoPath, opts.filePath);
27382
+ const buf = fs34.readFileSync(full);
27383
+ if (!buf.includes(0)) {
27384
+ const content = buf.toString("utf8");
27385
+ const lines = content.split(`
27386
+ `);
27387
+ const header = `diff --git a/${opts.filePath} b/${opts.filePath}
27388
+ new file mode 100644
27389
+ --- /dev/null
27390
+ +++ b/${opts.filePath}
27391
+ @@ -0,0 +1,${lines.length} @@
27392
+ `;
27393
+ rawDiff = header + lines.map((l) => `+${l}`).join(`
27394
+ `);
27395
+ }
27396
+ } catch {}
27397
+ }
27270
27398
  const { diff, truncated } = truncateDiff(rawDiff);
27271
27399
  const value = { ...empty, diff, truncated };
27272
27400
  diffCache.set(key, { head, value });
@@ -27932,7 +28060,9 @@ var init_Divider = __esm(() => {
27932
28060
 
27933
28061
  // src/tui/components/Overlay.tsx
27934
28062
  import { jsx as jsx9 } from "@opentui/react/jsx-runtime";
27935
- function Overlay({ title, borderColor = tokens.border, width = 64, children }) {
28063
+ function Overlay({ title, borderColor, width = 64, children }) {
28064
+ const theme = useTheme();
28065
+ const resolvedBorder = borderColor ?? theme.border;
27936
28066
  return /* @__PURE__ */ jsx9("box", {
27937
28067
  id: "overlay-scrim",
27938
28068
  position: "absolute",
@@ -27940,7 +28070,7 @@ function Overlay({ title, borderColor = tokens.border, width = 64, children }) {
27940
28070
  height: "100%",
27941
28071
  top: 0,
27942
28072
  left: 0,
27943
- backgroundColor: tokens.scrim,
28073
+ backgroundColor: theme.scrim,
27944
28074
  zIndex: 100,
27945
28075
  flexDirection: "row",
27946
28076
  justifyContent: "center",
@@ -27949,13 +28079,13 @@ function Overlay({ title, borderColor = tokens.border, width = 64, children }) {
27949
28079
  id: "overlay-panel",
27950
28080
  width,
27951
28081
  border: true,
27952
- borderColor,
28082
+ borderColor: resolvedBorder,
27953
28083
  title,
27954
- titleColor: borderColor,
28084
+ titleColor: resolvedBorder,
27955
28085
  paddingX: 2,
27956
28086
  paddingY: 1,
27957
28087
  flexDirection: "column",
27958
- backgroundColor: tokens.panelBg,
28088
+ backgroundColor: theme.panelBg,
27959
28089
  children
27960
28090
  })
27961
28091
  });
@@ -27967,85 +28097,132 @@ var init_Overlay = __esm(() => {
27967
28097
  // src/tui/components/HelpOverlay.tsx
27968
28098
  import { jsx as jsx10, jsxs as jsxs8 } from "@opentui/react/jsx-runtime";
27969
28099
  function HelpOverlay() {
28100
+ const theme = useTheme();
27970
28101
  return /* @__PURE__ */ jsxs8(Overlay, {
27971
28102
  title: "Help",
27972
- borderColor: tokens.border,
27973
- width: 84,
28103
+ borderColor: theme.border,
28104
+ width: 100,
27974
28105
  children: [
27975
28106
  /* @__PURE__ */ jsx10("box", {
27976
28107
  flexDirection: "column",
27977
- style: { maxHeight: 36 },
28108
+ style: { maxHeight: 38 },
27978
28109
  children: /* @__PURE__ */ jsx10("scrollbox", {
27979
28110
  flexGrow: 1,
27980
- children: HELP_ENTRIES.map(([key, desc]) => /* @__PURE__ */ jsxs8("box", {
27981
- flexDirection: "row",
27982
- gap: 1,
28111
+ children: HELP_GROUPS.map((group) => /* @__PURE__ */ jsxs8("box", {
28112
+ flexDirection: "column",
28113
+ style: { marginBottom: 1 },
27983
28114
  children: [
27984
28115
  /* @__PURE__ */ jsx10("box", {
27985
- width: 18,
27986
- flexShrink: 0,
27987
- justifyContent: "flex-end",
28116
+ style: { marginBottom: 1 },
27988
28117
  children: /* @__PURE__ */ jsx10("text", {
27989
- fg: tokens.accent,
27990
- children: key
28118
+ fg: theme.accent,
28119
+ attributes: 1,
28120
+ children: group.title
27991
28121
  })
27992
28122
  }),
27993
- /* @__PURE__ */ jsx10("text", {
27994
- fg: tokens.dim,
27995
- children: "-"
27996
- }),
27997
- /* @__PURE__ */ jsx10("box", {
27998
- flexGrow: 1,
27999
- children: /* @__PURE__ */ jsx10("text", {
28000
- fg: tokens.fg,
28001
- children: desc
28002
- })
28003
- })
28123
+ group.entries.map(([key, desc]) => /* @__PURE__ */ jsxs8("box", {
28124
+ flexDirection: "row",
28125
+ gap: 1,
28126
+ children: [
28127
+ /* @__PURE__ */ jsx10("box", {
28128
+ width: 22,
28129
+ flexShrink: 0,
28130
+ justifyContent: "flex-end",
28131
+ children: /* @__PURE__ */ jsx10("text", {
28132
+ fg: theme.accent,
28133
+ children: key
28134
+ })
28135
+ }),
28136
+ /* @__PURE__ */ jsx10("text", {
28137
+ fg: theme.dim,
28138
+ children: "-"
28139
+ }),
28140
+ /* @__PURE__ */ jsx10("box", {
28141
+ flexGrow: 1,
28142
+ children: /* @__PURE__ */ jsx10("text", {
28143
+ fg: theme.fg,
28144
+ children: desc
28145
+ })
28146
+ })
28147
+ ]
28148
+ }, key))
28004
28149
  ]
28005
- }, key))
28150
+ }, group.title))
28006
28151
  })
28007
28152
  }),
28008
28153
  /* @__PURE__ */ jsx10("text", {
28009
- style: { marginTop: 1, fg: tokens.dim },
28154
+ style: { marginTop: 1, fg: theme.dim },
28010
28155
  children: "Press any key to close"
28011
28156
  })
28012
28157
  ]
28013
28158
  });
28014
28159
  }
28015
- var HELP_ENTRIES;
28160
+ var HELP_GROUPS;
28016
28161
  var init_HelpOverlay = __esm(() => {
28017
28162
  init_Overlay();
28018
28163
  init_theme();
28019
- HELP_ENTRIES = [
28020
- ["↑/↓, k/j", "Navigate list"],
28021
- ["/", "Filter list"],
28022
- ["Space", "Multi-select"],
28023
- ["c", "Open configuration"],
28024
- ["n", "Create new worktree (pick dependency strategy)"],
28025
- ["f", "Fetch main for selected repo(s)"],
28026
- ["F (shift+f)", "Pin/unpin selected repo (favorites float to top)"],
28027
- ["W (shift+w)", "Scope list to a workspace's members (press again to clear)"],
28028
- ["p", "Pull latest changes for selected branch(es)"],
28029
- ["P (shift+p)", "Pull PR by link (force-override if exists)"],
28030
- ["b", "Rebase selected onto base (or main)"],
28031
- ["s", "Sync selected (env files + hooks)"],
28032
- ["s / Tab (Changes)", "Cycle changes scope: worktree staged → base"],
28033
- ["i", "Install dependencies in selection (worktrees and main)"],
28034
- ["m", "Rename selected worktree (branch + directory)"],
28035
- ["o", "Open selected in IDE"],
28036
- ["t", "New terminal session (max 5 per worktree, Details ↔ Session tabs)"],
28037
- ["T (shift+t)", "Cycle theme presets"],
28038
- ["click tab", "Switch Details Session (professional tabs with ▎ active)"],
28039
- ["Ctrl+G / click table", "Leave terminal focus focused terminal gets all keys"],
28040
- ["d", "Remove selected worktree(s)"],
28041
- ["e", "View data warnings (when count > 0)"],
28042
- ["r", "Refresh data"],
28043
- ["H", "Action history"],
28044
- ["mouse drag", "Select text — copied to clipboard automatically"],
28045
- ["ctrl+shift+c", "Copy selection again (terminals reserve cmd+c)"],
28046
- ["click PR #/URL", "Open pull request in browser"],
28047
- ["?", "Toggle help"],
28048
- ["q/esc", "Quit"]
28164
+ HELP_GROUPS = [
28165
+ {
28166
+ title: "Navigation & Filtering",
28167
+ entries: [
28168
+ ["↑/↓, k/j", "Navigate list"],
28169
+ ["/", "Filter list"],
28170
+ ["Space", "Multi-select"],
28171
+ ["q/esc", "Quit"]
28172
+ ]
28173
+ },
28174
+ {
28175
+ title: "Worktree Lifecycle",
28176
+ entries: [
28177
+ ["n", "Create new worktree (pick dependency strategy)"],
28178
+ ["d", "Remove selected worktree(s)"],
28179
+ ["m", "Rename selected worktree (branch + directory)"],
28180
+ ["o", "Open selected in IDE"],
28181
+ ["f", "Fetch main for selected repo(s)"],
28182
+ ["p", "Pull latest changes for selected branch(es)"],
28183
+ ["P (shift+p)", "Pull PR by link (force-override if exists)"],
28184
+ ["b", "Rebase selected onto base (or main)"],
28185
+ ["s", "Sync selected (env files + hooks)"],
28186
+ ["i", "Install dependencies in selection (worktrees and main)"]
28187
+ ]
28188
+ },
28189
+ {
28190
+ title: "Favorites & Workspaces",
28191
+ entries: [
28192
+ ["F (shift+f)", "Pin/unpin selected repo (favorites float to top)"],
28193
+ ["W (shift+w)", "Scope list to a workspace's members (press again to clear)"]
28194
+ ]
28195
+ },
28196
+ {
28197
+ title: "Terminal & Tabs",
28198
+ entries: [
28199
+ ["t", "New terminal session (max 5 per worktree)"],
28200
+ ["click tab", "Switch Details / Changes / Session tabs"],
28201
+ ["Ctrl+G / click table", "Leave terminal focus — focused terminal gets all keys"]
28202
+ ]
28203
+ },
28204
+ {
28205
+ title: "Changes Explorer (only for dirty worktrees)",
28206
+ entries: [
28207
+ ["click Changes", "Focus pane — j/k navigates files, s/Tab cycles scope"],
28208
+ ["s / Tab", "Cycle scope: worktree (dirty+untracked) → staged → base (vs base branch)"],
28209
+ ["j / k, ↑/↓", "Navigate files (when Changes focused, Ctrl+G/Esc to unfocus)"]
28210
+ ]
28211
+ },
28212
+ {
28213
+ title: "View & System",
28214
+ entries: [
28215
+ ["T (shift+t)", "Cycle theme presets"],
28216
+ ["c", "Open configuration"],
28217
+ ["e", "View data warnings (when count > 0)"],
28218
+ ["r", "Refresh data"],
28219
+ ["H", "Action history"],
28220
+ ["?", "Toggle help"],
28221
+ ["mouse drag", "Select text — copied to clipboard automatically"],
28222
+ ["ctrl+shift+c", "Copy selection again (terminals reserve cmd+c)"],
28223
+ ["click PR #/URL", "Open pull request in browser"]
28224
+ ]
28225
+ }
28049
28226
  ];
28050
28227
  });
28051
28228
 
@@ -28231,38 +28408,38 @@ var init_ActionLogModal = __esm(() => {
28231
28408
  });
28232
28409
 
28233
28410
  // src/lib/history.ts
28234
- import fs33 from "fs";
28235
- import path37 from "path";
28411
+ import fs35 from "fs";
28412
+ import path38 from "path";
28236
28413
  import os4 from "os";
28237
28414
  function getHistoryDir() {
28238
- const stateHome = process.env.XDG_STATE_HOME ?? path37.join(os4.homedir(), ".local", "state");
28239
- return path37.join(stateHome, "wtx");
28415
+ const stateHome = process.env.XDG_STATE_HOME ?? path38.join(os4.homedir(), ".local", "state");
28416
+ return path38.join(stateHome, "wtx");
28240
28417
  }
28241
28418
  function getHistoryPath() {
28242
- return path37.join(getHistoryDir(), "history.jsonl");
28419
+ return path38.join(getHistoryDir(), "history.jsonl");
28243
28420
  }
28244
28421
  function rotateHistory(maxBytes = HISTORY_MAX_BYTES, keepLines = HISTORY_ROTATE_KEEP_LINES) {
28245
28422
  const historyPath = getHistoryPath();
28246
28423
  let size = 0;
28247
28424
  try {
28248
- size = fs33.statSync(historyPath).size;
28425
+ size = fs35.statSync(historyPath).size;
28249
28426
  } catch {
28250
28427
  return;
28251
28428
  }
28252
28429
  if (size <= maxBytes)
28253
28430
  return;
28254
- const lines = fs33.readFileSync(historyPath, "utf-8").split(`
28431
+ const lines = fs35.readFileSync(historyPath, "utf-8").split(`
28255
28432
  `).filter(Boolean);
28256
28433
  const kept = lines.slice(-keepLines);
28257
28434
  const tmpPath = `${historyPath}.tmp.${process.pid}`;
28258
28435
  try {
28259
- fs33.writeFileSync(tmpPath, kept.length > 0 ? `${kept.join(`
28436
+ fs35.writeFileSync(tmpPath, kept.length > 0 ? `${kept.join(`
28260
28437
  `)}
28261
28438
  ` : "", "utf-8");
28262
- fs33.renameSync(tmpPath, historyPath);
28439
+ fs35.renameSync(tmpPath, historyPath);
28263
28440
  } catch (err) {
28264
- if (fs33.existsSync(tmpPath)) {
28265
- fs33.unlinkSync(tmpPath);
28441
+ if (fs35.existsSync(tmpPath)) {
28442
+ fs35.unlinkSync(tmpPath);
28266
28443
  }
28267
28444
  throw err;
28268
28445
  }
@@ -28270,18 +28447,18 @@ function rotateHistory(maxBytes = HISTORY_MAX_BYTES, keepLines = HISTORY_ROTATE_
28270
28447
  function appendHistory(entry) {
28271
28448
  try {
28272
28449
  const dir = getHistoryDir();
28273
- if (!fs33.existsSync(dir)) {
28274
- fs33.mkdirSync(dir, { recursive: true });
28450
+ if (!fs35.existsSync(dir)) {
28451
+ fs35.mkdirSync(dir, { recursive: true });
28275
28452
  }
28276
28453
  rotateHistory();
28277
- fs33.appendFileSync(getHistoryPath(), `${JSON.stringify(entry)}
28454
+ fs35.appendFileSync(getHistoryPath(), `${JSON.stringify(entry)}
28278
28455
  `, "utf-8");
28279
28456
  } catch {}
28280
28457
  }
28281
28458
  function readRecentHistory(limit = 50) {
28282
28459
  let content;
28283
28460
  try {
28284
- content = fs33.readFileSync(getHistoryPath(), "utf-8");
28461
+ content = fs35.readFileSync(getHistoryPath(), "utf-8");
28285
28462
  } catch {
28286
28463
  return [];
28287
28464
  }
@@ -28419,13 +28596,19 @@ function InputModal({ title, placeholder, initialValue, errorMessage: errorMessa
28419
28596
  borderColor: tokens.accent,
28420
28597
  children: /* @__PURE__ */ jsxs12("box", {
28421
28598
  flexDirection: "column",
28599
+ width: "100%",
28422
28600
  children: [
28423
- /* @__PURE__ */ jsx14("input", {
28424
- focused: true,
28425
- value,
28426
- placeholder,
28427
- onInput: (val) => setValue(val),
28428
- onSubmit: () => onSubmit(value)
28601
+ /* @__PURE__ */ jsx14("box", {
28602
+ flexGrow: 1,
28603
+ width: "100%",
28604
+ children: /* @__PURE__ */ jsx14("input", {
28605
+ focused: true,
28606
+ value,
28607
+ placeholder,
28608
+ onInput: (val) => setValue(val),
28609
+ onSubmit: () => onSubmit(value),
28610
+ width: "100%"
28611
+ })
28429
28612
  }),
28430
28613
  errorMessage4 ? /* @__PURE__ */ jsx14("text", {
28431
28614
  fg: tokens.error,
@@ -28992,8 +29175,8 @@ function hasDirectStdinWrite(proc) {
28992
29175
  function hasWritableStreamStdin(proc) {
28993
29176
  return !!proc && proc.stdin instanceof WritableStream;
28994
29177
  }
28995
- function worktreeKeyFor(repoName, branch, path38) {
28996
- return path38 || `${repoName}:${branch}`;
29178
+ function worktreeKeyFor(repoName, branch, path39) {
29179
+ return path39 || `${repoName}:${branch}`;
28997
29180
  }
28998
29181
  function nextTerminalSessionLabel(existingCount) {
28999
29182
  return `Session ${existingCount + 1}`;
@@ -29011,13 +29194,13 @@ function useTerminalSessions() {
29011
29194
  const [sessionsByKey, setSessionsByKey] = useState10(() => new Map);
29012
29195
  const procsRef = useRef7(new Map);
29013
29196
  const listenersRef = useRef7(new Map);
29014
- const getSessions = useCallback6((repoName, branch, path38) => {
29015
- const key = worktreeKeyFor(repoName, branch, path38);
29197
+ const getSessions = useCallback6((repoName, branch, path39) => {
29198
+ const key = worktreeKeyFor(repoName, branch, path39);
29016
29199
  return sessionsByKey.get(key) ?? [];
29017
29200
  }, [sessionsByKey]);
29018
- const getKey = useCallback6((repoName, branch, path38) => worktreeKeyFor(repoName, branch, path38), []);
29019
- const createSession = useCallback6((repoName, branch, path38, opts) => {
29020
- const key = worktreeKeyFor(repoName, branch, path38);
29201
+ const getKey = useCallback6((repoName, branch, path39) => worktreeKeyFor(repoName, branch, path39), []);
29202
+ const createSession = useCallback6((repoName, branch, path39, opts) => {
29203
+ const key = worktreeKeyFor(repoName, branch, path39);
29021
29204
  const existing = sessionsByKey.get(key) ?? [];
29022
29205
  if (existing.length >= MAX_TERMINAL_SESSIONS) {
29023
29206
  return { session: null, error: `Max ${MAX_TERMINAL_SESSIONS} sessions per worktree` };
@@ -29030,7 +29213,7 @@ function useTerminalSessions() {
29030
29213
  id,
29031
29214
  label,
29032
29215
  worktreeKey: key,
29033
- worktreePath: path38,
29216
+ worktreePath: path39,
29034
29217
  repoName,
29035
29218
  branch,
29036
29219
  lines: [],
@@ -29115,7 +29298,7 @@ function useTerminalSessions() {
29115
29298
  throw new Error("pty not available");
29116
29299
  }
29117
29300
  const proc = Bun.spawn([shell], {
29118
- cwd: path38 || process.cwd(),
29301
+ cwd: path39 || process.cwd(),
29119
29302
  env: { ...process.env, TERM: "xterm-256color", COLORTERM: "truecolor", WTX_SESSION: label },
29120
29303
  terminal: {
29121
29304
  cols,
@@ -29150,7 +29333,7 @@ function useTerminalSessions() {
29150
29333
  session.usePty = false;
29151
29334
  try {
29152
29335
  const proc = Bun.spawn([shell], {
29153
- cwd: path38 || process.cwd(),
29336
+ cwd: path39 || process.cwd(),
29154
29337
  stdin: "pipe",
29155
29338
  stdout: "pipe",
29156
29339
  stderr: "pipe",
@@ -29198,9 +29381,9 @@ function useTerminalSessions() {
29198
29381
  } catch {}
29199
29382
  }
29200
29383
  if (session.usePty) {
29201
- session.lines = [`${label} — ${path38} (${shell}) [pty]`, ""];
29384
+ session.lines = [`${label} — ${path39} (${shell}) [pty]`, ""];
29202
29385
  } else if (!session.spawnError && session.lines.length === 0) {
29203
- session.lines = [`${label} — ${path38} (${shell})`, ""];
29386
+ session.lines = [`${label} — ${path39} (${shell})`, ""];
29204
29387
  }
29205
29388
  setSessionsByKey((prev) => {
29206
29389
  const next = new Map(prev);
@@ -29210,8 +29393,8 @@ function useTerminalSessions() {
29210
29393
  });
29211
29394
  return { session };
29212
29395
  }, [sessionsByKey]);
29213
- const removeSession = useCallback6((repoName, branch, path38, id) => {
29214
- const key = worktreeKeyFor(repoName, branch, path38);
29396
+ const removeSession = useCallback6((repoName, branch, path39, id) => {
29397
+ const key = worktreeKeyFor(repoName, branch, path39);
29215
29398
  setSessionsByKey((prev) => {
29216
29399
  const next = new Map(prev);
29217
29400
  const list = next.get(key);
@@ -29240,8 +29423,8 @@ function useTerminalSessions() {
29240
29423
  return next;
29241
29424
  });
29242
29425
  }, []);
29243
- const sendInput = useCallback6((repoName, branch, path38, id, data) => {
29244
- const key = worktreeKeyFor(repoName, branch, path38);
29426
+ const sendInput = useCallback6((repoName, branch, path39, id, data) => {
29427
+ const key = worktreeKeyFor(repoName, branch, path39);
29245
29428
  const list = sessionsByKey.get(key) ?? [];
29246
29429
  const sess = list.find((s) => s.id === id);
29247
29430
  if (!sess?.proc)
@@ -29289,8 +29472,8 @@ function useTerminalSessions() {
29289
29472
  }
29290
29473
  }
29291
29474
  }, [sessionsByKey]);
29292
- const resizeSession = useCallback6((repoName, branch, path38, id, cols, rows) => {
29293
- const key = worktreeKeyFor(repoName, branch, path38);
29475
+ const resizeSession = useCallback6((repoName, branch, path39, id, cols, rows) => {
29476
+ const key = worktreeKeyFor(repoName, branch, path39);
29294
29477
  setSessionsByKey((prev) => {
29295
29478
  const list = prev.get(key);
29296
29479
  if (!list)
@@ -29682,6 +29865,7 @@ function App({ opts }) {
29682
29865
  const [activeTabByWorktree, setActiveTabByWorktree] = useState11(() => new Map);
29683
29866
  const [recentTerminalSessionIds, setRecentTerminalSessionIds] = useState11([]);
29684
29867
  const [terminalFocused, setTerminalFocused] = useState11(false);
29868
+ const [changesFocused, setChangesFocused] = useState11(false);
29685
29869
  const { width: termW, height: termH } = useTerminalDimensions();
29686
29870
  const totalWidth = termW || renderer.width || 80;
29687
29871
  const totalHeight = termH || renderer.terminalHeight || 24;
@@ -29799,7 +29983,17 @@ function App({ opts }) {
29799
29983
  const baseFiltered = useMemo3(() => blocks.map((b) => ({
29800
29984
  ...b,
29801
29985
  rows: b.rows.filter((r) => matchesFilter(r, filterText) && matchesWorkspace(r, workspaceMemberSet))
29802
- })).filter((b) => b.rows.length > 0 || workspaceMemberSet === null && pendingRepos.includes(b.repoName)), [blocks, filterText, pendingRepos, workspaceMemberSet]);
29986
+ })).filter((b) => {
29987
+ if (b.rows.length > 0)
29988
+ return true;
29989
+ if (workspaceMemberSet !== null)
29990
+ return false;
29991
+ if (pendingRepos.includes(b.repoName))
29992
+ return true;
29993
+ if (!filterText)
29994
+ return true;
29995
+ return matchesFilter({ repoName: b.repoName, branch: "" }, filterText);
29996
+ }), [blocks, filterText, pendingRepos, workspaceMemberSet]);
29803
29997
  const missingMemberWarnings = useMemo3(() => {
29804
29998
  if (!activeWorkspace)
29805
29999
  return [];
@@ -29818,7 +30012,7 @@ function App({ opts }) {
29818
30012
  const displayBlocks = useMemo3(() => sortBlocks([
29819
30013
  ...withCreatePlaceholders(baseFiltered, ops.filter((o2) => o2.branch !== undefined).map((o2) => ({ repoName: o2.repoNames[0], branch: o2.branch }))),
29820
30014
  ...pendingBlocks
29821
- ]), [baseFiltered, ops, pendingBlocks]);
30015
+ ], favorites), [baseFiltered, ops, pendingBlocks, favorites]);
29822
30016
  const flatRows = useMemo3(() => displayBlocks.flatMap((b) => b.rows.filter((r) => !r.isPendingCreate)), [displayBlocks]);
29823
30017
  const totalRows = blocks.flatMap((b) => b.rows).length;
29824
30018
  const maxIndex = Math.max(0, flatRows.length - 1);
@@ -29843,7 +30037,15 @@ function App({ opts }) {
29843
30037
  }
29844
30038
  }, [worktreeKey, activeTabByWorktree]);
29845
30039
  useEffect9(() => {
29846
- if (sessionsForSelected.length === 0 && activeTabId !== "details") {
30040
+ if (activeTabId === "changes" && selectedRow && (selectedRow.isMainCheckout || selectedRow.dirtyFiles.length === 0)) {
30041
+ setActiveTabByWorktree((prev) => {
30042
+ const next = new Map(prev);
30043
+ next.set(worktreeKey, "details");
30044
+ return next;
30045
+ });
30046
+ return;
30047
+ }
30048
+ if (sessionsForSelected.length === 0 && activeTabId !== "details" && activeTabId !== "changes") {
29847
30049
  setActiveTabByWorktree((prev) => {
29848
30050
  const next = new Map(prev);
29849
30051
  next.set(worktreeKey, "details");
@@ -29851,7 +30053,7 @@ function App({ opts }) {
29851
30053
  });
29852
30054
  setTerminalFocused(false);
29853
30055
  } else if (sessionsForSelected.length > 0) {
29854
- const exists = sessionsForSelected.some((s) => s.id === activeTabId) || activeTabId === "details";
30056
+ const exists = sessionsForSelected.some((s) => s.id === activeTabId) || activeTabId === "details" || selectedRow && !selectedRow.isMainCheckout && selectedRow.dirtyFiles.length > 0 && activeTabId === "changes";
29855
30057
  if (!exists) {
29856
30058
  setActiveTabByWorktree((prev) => {
29857
30059
  const next = new Map(prev);
@@ -29860,7 +30062,15 @@ function App({ opts }) {
29860
30062
  });
29861
30063
  }
29862
30064
  }
29863
- }, [sessionsForSelected, activeTabId, worktreeKey]);
30065
+ }, [sessionsForSelected, activeTabId, worktreeKey, selectedRow]);
30066
+ useEffect9(() => {
30067
+ setChangesFocused(false);
30068
+ setTerminalFocused(false);
30069
+ }, [worktreeKey]);
30070
+ useEffect9(() => {
30071
+ if (activeTabId !== "changes")
30072
+ setChangesFocused(false);
30073
+ }, [activeTabId]);
29864
30074
  usePaste((event) => {
29865
30075
  if (!terminalFocused)
29866
30076
  return;
@@ -29884,7 +30094,8 @@ function App({ opts }) {
29884
30094
  return next;
29885
30095
  });
29886
30096
  setTerminalFocused(isSession);
29887
- }, [activeTabId, renderer, worktreeKey, sessionsForSelected]);
30097
+ setChangesFocused(id === "changes" && selectedRow != null && !selectedRow.isMainCheckout && selectedRow.dirtyFiles.length > 0);
30098
+ }, [activeTabId, renderer, worktreeKey, sessionsForSelected, selectedRow]);
29888
30099
  const handleAddSession = useCallback7(() => {
29889
30100
  if (!selectedRow) {
29890
30101
  flash("No worktree selected");
@@ -29908,13 +30119,31 @@ function App({ opts }) {
29908
30119
  return next;
29909
30120
  });
29910
30121
  setTerminalFocused(true);
30122
+ setChangesFocused(false);
29911
30123
  flash(`Session ${session.label} created`, 2000);
29912
30124
  }, [selectedRow, terminalSessions, flash, totalWidth, splitRatio, totalHeight, activeTabId, sessionsForSelected]);
29913
30125
  const handleCloseSession = useCallback7((id) => {
29914
- if (!selectedRow)
29915
- return;
29916
- terminalSessions.removeSession(selectedRow.repoName, selectedRow.branch, selectedRow.path, id);
29917
- const key = terminalSessions.getKey(selectedRow.repoName, selectedRow.branch, selectedRow.path);
30126
+ let targetRepo;
30127
+ let targetBranch;
30128
+ let targetPath;
30129
+ for (const sessions of terminalSessions.sessionsByKey.values()) {
30130
+ const found = sessions.find((s) => s.id === id);
30131
+ if (found) {
30132
+ targetRepo = found.repoName;
30133
+ targetBranch = found.branch;
30134
+ targetPath = found.worktreePath;
30135
+ break;
30136
+ }
30137
+ }
30138
+ if (!targetRepo || !targetBranch || !targetPath) {
30139
+ if (!selectedRow)
30140
+ return;
30141
+ targetRepo = selectedRow.repoName;
30142
+ targetBranch = selectedRow.branch;
30143
+ targetPath = selectedRow.path;
30144
+ }
30145
+ terminalSessions.removeSession(targetRepo, targetBranch, targetPath, id);
30146
+ const key = terminalSessions.getKey(targetRepo, targetBranch, targetPath);
29918
30147
  setActiveTabByWorktree((prev) => {
29919
30148
  const next = new Map(prev);
29920
30149
  const current = next.get(key);
@@ -29950,8 +30179,10 @@ function App({ opts }) {
29950
30179
  render: ({ worktree }) => /* @__PURE__ */ jsx18(DetailsContent, {
29951
30180
  selectedRow: worktree
29952
30181
  })
29953
- },
29954
- {
30182
+ }
30183
+ ];
30184
+ if (selectedRow != null && !selectedRow.isMainCheckout && selectedRow.dirtyFiles.length > 0) {
30185
+ base2.push({
29955
30186
  id: "changes",
29956
30187
  label: "Changes",
29957
30188
  render: ({ worktree, isActive, focused }) => /* @__PURE__ */ jsx18(ChangesContent, {
@@ -29960,8 +30191,8 @@ function App({ opts }) {
29960
30191
  focused,
29961
30192
  worktreeKey
29962
30193
  })
29963
- }
29964
- ];
30194
+ });
30195
+ }
29965
30196
  for (const s of sessionsForSelected) {
29966
30197
  base2.push({
29967
30198
  id: s.id,
@@ -29975,7 +30206,7 @@ function App({ opts }) {
29975
30206
  });
29976
30207
  }
29977
30208
  return base2;
29978
- }, [sessionsForSelected, worktreeKey]);
30209
+ }, [selectedRow, sessionsForSelected, worktreeKey]);
29979
30210
  const { repoVerbs, rowVerbs } = useMemo3(() => {
29980
30211
  const rv = new Map;
29981
30212
  const nv = new Map;
@@ -30279,6 +30510,15 @@ function App({ opts }) {
30279
30510
  }
30280
30511
  }, [selectedRow, selection, combinedWarnings, busyRepos, busyRowPaths, doRefresh, flash, blocks, terminalSessions]);
30281
30512
  useKeyboard4((key) => {
30513
+ if (changesFocused) {
30514
+ if (key.ctrl && key.name === "g" || key.name === "escape") {
30515
+ setChangesFocused(false);
30516
+ return;
30517
+ }
30518
+ if (["j", "k", "down", "up", "s", "tab"].includes(key.name)) {
30519
+ return;
30520
+ }
30521
+ }
30282
30522
  if (terminalFocused) {
30283
30523
  if (key.ctrl && key.name === "g") {
30284
30524
  setTerminalFocused(false);
@@ -30720,6 +30960,7 @@ function App({ opts }) {
30720
30960
  useEffect9(() => () => terminalResizeSchedulerRef.current?.flush(), []);
30721
30961
  useEffect9(() => {
30722
30962
  setTerminalFocused(false);
30963
+ setChangesFocused(false);
30723
30964
  }, [selectedIndex]);
30724
30965
  return /* @__PURE__ */ jsx18(ThemeContext.Provider, {
30725
30966
  value: currentTheme,
@@ -30737,7 +30978,10 @@ function App({ opts }) {
30737
30978
  width: leftCols,
30738
30979
  height: "100%",
30739
30980
  flexDirection: "column",
30740
- onMouseDown: () => setTerminalFocused(false),
30981
+ onMouseDown: () => {
30982
+ setTerminalFocused(false);
30983
+ setChangesFocused(false);
30984
+ },
30741
30985
  children: /* @__PURE__ */ jsx18(WorktreeTable, {
30742
30986
  blocks: displayBlocks,
30743
30987
  selectedIndex,
@@ -30750,9 +30994,10 @@ function App({ opts }) {
30750
30994
  activeWorkspaceName: activeWorkspace?.name ?? null,
30751
30995
  onRowClick: (idx) => {
30752
30996
  setTerminalFocused(false);
30997
+ setChangesFocused(false);
30753
30998
  setSelectedIndex(idx);
30754
30999
  },
30755
- onToggleSelect: (path38) => setSelection((prev) => toggleSelection(prev, path38))
31000
+ onToggleSelect: (path39) => setSelection((prev) => toggleSelection(prev, path39))
30756
31001
  })
30757
31002
  }),
30758
31003
  /* @__PURE__ */ jsx18(Divider, {
@@ -30769,13 +31014,18 @@ function App({ opts }) {
30769
31014
  selectedRow,
30770
31015
  tabs: tabsForSelected,
30771
31016
  activeId: activeTabId,
30772
- focused: terminalFocused && tabsForSelected.some((t) => t.id === activeTabId && t.id !== "details"),
31017
+ focused: (terminalFocused || changesFocused) && activeTabId !== "details",
30773
31018
  canAdd: sessionsForSelected.length < MAX_TERMINAL_SESSIONS,
30774
31019
  onSelect: handleSelectTab,
30775
31020
  onAdd: handleAddSession,
30776
31021
  onClose: handleCloseSession,
30777
31022
  allSessionsFlat,
30778
31023
  terminalFocused,
31024
+ changesFocused,
31025
+ onChangesFocus: () => {
31026
+ setChangesFocused(true);
31027
+ setTerminalFocused(false);
31028
+ },
30779
31029
  activeTabId,
30780
31030
  recentSessionIds: mountedRecentSessionIds,
30781
31031
  terminalSessions
@@ -30788,15 +31038,20 @@ function App({ opts }) {
30788
31038
  paddingX: 1,
30789
31039
  border: true,
30790
31040
  borderColor: "magenta",
31041
+ width: "100%",
30791
31042
  children: [
30792
31043
  /* @__PURE__ */ jsx18("text", {
30793
31044
  children: "filter: "
30794
31045
  }),
30795
- /* @__PURE__ */ jsx18("input", {
30796
- focused: true,
30797
- value: filterText,
30798
- placeholder: "Type to filter...",
30799
- onInput: (v) => setFilterText(v)
31046
+ /* @__PURE__ */ jsx18("box", {
31047
+ flexGrow: 1,
31048
+ children: /* @__PURE__ */ jsx18("input", {
31049
+ focused: true,
31050
+ value: filterText,
31051
+ placeholder: "Type to filter...",
31052
+ onInput: (v) => setFilterText(v),
31053
+ width: "100%"
31054
+ })
30800
31055
  })
30801
31056
  ]
30802
31057
  }),
@@ -33882,8 +34137,66 @@ init_config();
33882
34137
  init_log();
33883
34138
  init_git();
33884
34139
  init_resolver();
33885
- import fs22 from "fs";
34140
+ import fs23 from "fs";
33886
34141
  import path28 from "path";
34142
+
34143
+ // src/lib/worktree-remove.ts
34144
+ init_git();
34145
+ import fs22 from "fs";
34146
+ function isSubmoduleWorktreeError(message) {
34147
+ return message.includes("submodule") || message.includes("containing submodules");
34148
+ }
34149
+ function isMissingWorktreePathError(message) {
34150
+ return message.includes("ENOENT") || message.includes("No such file") || message.includes("does not exist") || message.includes("not a git repository") || message.includes("cannot change to");
34151
+ }
34152
+ function isRecoverableWorktreeRemoveError(message) {
34153
+ return message.includes("not a working tree") || message.includes("already removed") || isSubmoduleWorktreeError(message) || isMissingWorktreePathError(message);
34154
+ }
34155
+ function removePathIfExists(wtPath, opts) {
34156
+ if (!opts.dryRun && fs22.existsSync(wtPath)) {
34157
+ fs22.rmSync(wtPath, { recursive: true, force: true });
34158
+ }
34159
+ }
34160
+ async function pruneWorktreeAdmin(mainPath, opts) {
34161
+ try {
34162
+ await gitExec(["-C", mainPath, "worktree", "prune"], opts);
34163
+ } catch {}
34164
+ }
34165
+ async function removeWorktreeSafely(params) {
34166
+ const { mainPath, wtPath, force, opts } = params;
34167
+ const args = ["-C", mainPath, "worktree", "remove", wtPath];
34168
+ if (force) {
34169
+ args.push("--force");
34170
+ }
34171
+ try {
34172
+ await gitExec(args, opts);
34173
+ return { usedForce: force, manualCleanup: false };
34174
+ } catch (err) {
34175
+ const msg = err instanceof Error ? err.message : String(err);
34176
+ if (isSubmoduleWorktreeError(msg) && !force) {
34177
+ try {
34178
+ await gitExec(["-C", mainPath, "worktree", "remove", "--force", wtPath], opts);
34179
+ return { usedForce: true, manualCleanup: false };
34180
+ } catch (forceErr) {
34181
+ const forceMsg = forceErr instanceof Error ? forceErr.message : String(forceErr);
34182
+ if (isRecoverableWorktreeRemoveError(forceMsg)) {
34183
+ await pruneWorktreeAdmin(mainPath, opts);
34184
+ removePathIfExists(wtPath, opts);
34185
+ return { usedForce: true, manualCleanup: true };
34186
+ }
34187
+ throw forceErr;
34188
+ }
34189
+ }
34190
+ if (isRecoverableWorktreeRemoveError(msg)) {
34191
+ await pruneWorktreeAdmin(mainPath, opts);
34192
+ removePathIfExists(wtPath, opts);
34193
+ return { usedForce: force, manualCleanup: true };
34194
+ }
34195
+ throw err;
34196
+ }
34197
+ }
34198
+
34199
+ // src/commands/pull.ts
33887
34200
  init_forge();
33888
34201
  init_stack();
33889
34202
  function registerPullCommand(program2) {
@@ -34000,7 +34313,7 @@ function registerPullCommand(program2) {
34000
34313
  const worktrees = await getWorktreeList(target.mainPath);
34001
34314
  const existingWorktree = worktrees.find((w) => w.path === wtPath || w.branch === branch);
34002
34315
  const localBranchFound = await localBranchExists(target.mainPath, branch, globalOpts);
34003
- const dirExists = Boolean(existingWorktree) || fs22.existsSync(wtPath);
34316
+ const dirExists = Boolean(existingWorktree) || fs23.existsSync(wtPath);
34004
34317
  if (localBranchFound || dirExists) {
34005
34318
  if (options.force) {
34006
34319
  stepWarning(`Branch '${branch}' already exists — --force: will override`, dirExists ? wtPath : "local branch exists");
@@ -34013,17 +34326,22 @@ function registerPullCommand(program2) {
34013
34326
  if (options.force && (localBranchFound || dirExists)) {
34014
34327
  if (existingWorktree) {
34015
34328
  try {
34016
- await gitExec(["-C", target.mainPath, "worktree", "remove", "--force", existingWorktree.path], { verbose: globalOpts.verbose, dryRun: globalOpts.dryRun });
34329
+ await removeWorktreeSafely({
34330
+ mainPath: target.mainPath,
34331
+ wtPath: existingWorktree.path,
34332
+ force: true,
34333
+ opts: { verbose: globalOpts.verbose, dryRun: globalOpts.dryRun }
34334
+ });
34017
34335
  } catch (err) {
34018
34336
  const message = err instanceof Error ? err.message : String(err);
34019
- if (!message.includes("not a working tree")) {
34337
+ if (!isRecoverableWorktreeRemoveError(message)) {
34020
34338
  stepWarning("Worktree removal failed", message.split(`
34021
34339
  `)[0] ?? message);
34022
34340
  }
34023
34341
  }
34024
34342
  }
34025
- if (!globalOpts.dryRun && fs22.existsSync(wtPath)) {
34026
- fs22.rmSync(wtPath, { recursive: true, force: true });
34343
+ if (!globalOpts.dryRun && fs23.existsSync(wtPath)) {
34344
+ fs23.rmSync(wtPath, { recursive: true, force: true });
34027
34345
  }
34028
34346
  if (localBranchFound) {
34029
34347
  try {
@@ -34067,7 +34385,7 @@ function registerPullCommand(program2) {
34067
34385
  }
34068
34386
  stepSuccess("Fetched");
34069
34387
  if (!globalOpts.dryRun) {
34070
- fs22.mkdirSync(path28.dirname(wtPath), { recursive: true });
34388
+ fs23.mkdirSync(path28.dirname(wtPath), { recursive: true });
34071
34389
  }
34072
34390
  try {
34073
34391
  const createBranchFlag = options.force ? "-B" : "-b";
@@ -34201,10 +34519,10 @@ function registerPullBranchCommand(program2) {
34201
34519
  init_config();
34202
34520
  init_log();
34203
34521
  init_git();
34522
+ import fs25 from "fs";
34523
+ import path30 from "path";
34204
34524
  init_resolver();
34205
34525
  init_path_safety();
34206
- import fs24 from "fs";
34207
- import path30 from "path";
34208
34526
 
34209
34527
  // src/lib/prompts.ts
34210
34528
  import * as readline2 from "readline";
@@ -34252,15 +34570,9 @@ init_workspace();
34252
34570
  function errorMessage2(err) {
34253
34571
  return err instanceof Error ? err.message : String(err);
34254
34572
  }
34255
- function isMissingWorktreePathError(message) {
34256
- return message.includes("ENOENT") || message.includes("No such file") || message.includes("does not exist") || message.includes("not a git repository") || message.includes("cannot change to");
34257
- }
34258
- function isRecoverableWorktreeRemoveError(message) {
34259
- return message.includes("not a working tree") || message.includes("already removed") || message.includes("submodule") || message.includes("containing submodules") || isMissingWorktreePathError(message);
34260
- }
34261
34573
  function removeExistingPath(wtPath, opts) {
34262
- if (!opts.dryRun && fs24.existsSync(wtPath)) {
34263
- fs24.rmSync(wtPath, { recursive: true, force: true });
34574
+ if (!opts.dryRun && fs25.existsSync(wtPath)) {
34575
+ fs25.rmSync(wtPath, { recursive: true, force: true });
34264
34576
  }
34265
34577
  }
34266
34578
  async function removeLocalBranchIfExists(repoPath, branch, opts) {
@@ -34349,7 +34661,7 @@ function registerRemoveCommand(program2) {
34349
34661
  const worktrees = await getWorktreeList(repo.mainPath);
34350
34662
  const target = findWorktreeForBranch(worktrees, branch, repo.mainPath, candidatePath);
34351
34663
  if (!target) {
34352
- if (options.force && (fs24.existsSync(candidatePath) || await localBranchExists(repo.mainPath, branch, globalOpts))) {
34664
+ if (options.force && (fs25.existsSync(candidatePath) || await localBranchExists(repo.mainPath, branch, globalOpts))) {
34353
34665
  if (!await confirmDeletionIfNeeded(repo, candidatePath, options, globalOpts)) {
34354
34666
  skipCount++;
34355
34667
  continue;
@@ -34391,7 +34703,7 @@ function registerRemoveCommand(program2) {
34391
34703
  }
34392
34704
  } catch (err) {
34393
34705
  const msg = errorMessage2(err);
34394
- if (isMissingWorktreePathError(msg) || !fs24.existsSync(wtPath)) {
34706
+ if (isMissingWorktreePathError(msg) || !fs25.existsSync(wtPath)) {
34395
34707
  stepWarning("Worktree path missing", "continuing with removal cleanup");
34396
34708
  } else {
34397
34709
  stepError("Failed to check for uncommitted changes", msg);
@@ -34400,7 +34712,7 @@ function registerRemoveCommand(program2) {
34400
34712
  }
34401
34713
  }
34402
34714
  }
34403
- if (options.force && !globalOpts.dryRun && !fs24.existsSync(wtPath)) {
34715
+ if (options.force && !globalOpts.dryRun && !fs25.existsSync(wtPath)) {
34404
34716
  if (!await confirmDeletionIfNeeded(repo, wtPath, options, globalOpts)) {
34405
34717
  skipCount++;
34406
34718
  continue;
@@ -34424,52 +34736,16 @@ function registerRemoveCommand(program2) {
34424
34736
  skipCount++;
34425
34737
  continue;
34426
34738
  }
34427
- const args = ["-C", repo.mainPath, "worktree", "remove", wtPath];
34428
- if (options.force) {
34429
- args.push("--force");
34430
- }
34431
- try {
34432
- await gitExec(args, globalOpts);
34433
- stepSuccess("Worktree removed", wtPath);
34434
- } catch (err) {
34435
- const msg = errorMessage2(err);
34436
- const isSubmodule = msg.includes("submodule") || msg.includes("containing submodules");
34437
- if (isSubmodule && !options.force && !args.includes("--force")) {
34438
- stepWarning("Worktree contains submodules — retrying with --force", msg.split(`
34439
- `)[0] ?? msg);
34440
- try {
34441
- await gitExec(["-C", repo.mainPath, "worktree", "remove", "--force", wtPath], globalOpts);
34442
- stepSuccess("Worktree removed", wtPath);
34443
- } catch (forceErr) {
34444
- const forceMsg = errorMessage2(forceErr);
34445
- if (isRecoverableWorktreeRemoveError(forceMsg)) {
34446
- stepWarning("Worktree already removed or invalid", forceMsg);
34447
- try {
34448
- await gitExec(["-C", repo.mainPath, "worktree", "prune"], globalOpts);
34449
- } catch (pruneErr) {
34450
- stepWarning("Worktree prune failed", errorMessage2(pruneErr));
34451
- }
34452
- removeExistingPath(wtPath, globalOpts);
34453
- await removeLocalBranchBestEffort(repo.mainPath, branch, globalOpts);
34454
- stepSuccess("Worktree removed", wtPath);
34455
- } else {
34456
- throw forceErr;
34457
- }
34458
- }
34459
- } else if (isRecoverableWorktreeRemoveError(msg)) {
34460
- stepWarning("Worktree already removed or invalid", msg);
34461
- try {
34462
- await gitExec(["-C", repo.mainPath, "worktree", "prune"], globalOpts);
34463
- } catch (pruneErr) {
34464
- stepWarning("Worktree prune failed", errorMessage2(pruneErr));
34465
- }
34466
- removeExistingPath(wtPath, globalOpts);
34467
- await removeLocalBranchBestEffort(repo.mainPath, branch, globalOpts);
34468
- stepSuccess("Worktree removed", wtPath);
34469
- } else {
34470
- throw err;
34471
- }
34739
+ const result = await removeWorktreeSafely({
34740
+ mainPath: repo.mainPath,
34741
+ wtPath,
34742
+ force: !!options.force,
34743
+ opts: globalOpts
34744
+ });
34745
+ if (result.manualCleanup) {
34746
+ await removeLocalBranchBestEffort(repo.mainPath, branch, globalOpts);
34472
34747
  }
34748
+ stepSuccess("Worktree removed", wtPath);
34473
34749
  await finishCleanup(repo, wtPath, branch, globalOpts);
34474
34750
  await unlinkRemovedWorktreeFromWorkspaces({ workspaceRoot, repo: repo.name, branch });
34475
34751
  if (children.length > 0) {
@@ -34496,9 +34772,9 @@ init_source();
34496
34772
  init_config();
34497
34773
  init_log();
34498
34774
  init_git();
34775
+ import path31 from "path";
34499
34776
  init_resolver();
34500
34777
  init_forge();
34501
- import path31 from "path";
34502
34778
 
34503
34779
  // src/lib/prune.ts
34504
34780
  function selectMergedCandidates(worktrees, mainPath, prMap) {
@@ -34635,16 +34911,17 @@ function registerPruneCommand(program2) {
34635
34911
  repoHeader(repo.name);
34636
34912
  currentRepo = repo.name;
34637
34913
  }
34638
- const args = ["-C", repo.mainPath, "worktree", "remove", candidate.path];
34639
- if (options.force) {
34640
- args.push("--force");
34641
- }
34642
34914
  try {
34643
- await gitExec(args, globalOpts);
34915
+ await removeWorktreeSafely({
34916
+ mainPath: repo.mainPath,
34917
+ wtPath: candidate.path,
34918
+ force: !!options.force,
34919
+ opts: globalOpts
34920
+ });
34644
34921
  stepSuccess("Worktree removed", label);
34645
34922
  } catch (err) {
34646
34923
  const msg = err instanceof Error ? err.message : String(err);
34647
- if (msg.includes("not a working tree") || msg.includes("already removed")) {
34924
+ if (isRecoverableWorktreeRemoveError(msg)) {
34648
34925
  stepWarning("Worktree already removed or invalid", msg);
34649
34926
  skippedCount++;
34650
34927
  continue;
@@ -34747,7 +35024,7 @@ init_source();
34747
35024
  init_stack();
34748
35025
  init_stack();
34749
35026
  import path32 from "path";
34750
- import fs25 from "fs";
35027
+ import fs26 from "fs";
34751
35028
  function buildLsJson(reposData) {
34752
35029
  const result = [];
34753
35030
  for (const repo of reposData) {
@@ -34857,7 +35134,7 @@ function registerLsCommand(program2) {
34857
35134
  statusStr = source_default.blue("[main checkout]");
34858
35135
  } else if (wt.isLocked) {
34859
35136
  statusStr = source_default.red("locked \uD83D\uDD12");
34860
- } else if (fs25.existsSync(wt.path)) {
35137
+ } else if (fs26.existsSync(wt.path)) {
34861
35138
  try {
34862
35139
  dirtyFiles = await getDirtyFiles(wt.path);
34863
35140
  if (dirtyFiles.length > 0) {
@@ -34983,7 +35260,7 @@ end
34983
35260
  init_config();
34984
35261
  init_log();
34985
35262
  init_git();
34986
- import fs26 from "fs";
35263
+ import fs27 from "fs";
34987
35264
  init_resolver();
34988
35265
  init_stack();
34989
35266
  function registerRebaseCommand(program2) {
@@ -34998,7 +35275,7 @@ function registerRebaseCommand(program2) {
34998
35275
  repoHeader(repo.name);
34999
35276
  const mainBranch = await resolveMainBranch(repo, config2);
35000
35277
  const wtPath = getWorktreePath(repo, branch);
35001
- if (!fs26.existsSync(wtPath)) {
35278
+ if (!fs27.existsSync(wtPath)) {
35002
35279
  stepError("No worktree found", `${wtPath} (skipped)`);
35003
35280
  failCount++;
35004
35281
  continue;
@@ -35124,7 +35401,7 @@ init_execa();
35124
35401
  init_config();
35125
35402
  init_log();
35126
35403
  init_resolver();
35127
- import fs27 from "fs";
35404
+ import fs28 from "fs";
35128
35405
  import path33 from "path";
35129
35406
  function registerSyncCommand(program2) {
35130
35407
  program2.command("sync <branch>").description("re-copy sync files + run post_sync").option("--repo <repos...>", "comma-separated list of repos to target").action(async (branch, _options, cmd) => {
@@ -35136,14 +35413,14 @@ function registerSyncCommand(program2) {
35136
35413
  for (const repo of repos) {
35137
35414
  repoHeader(repo.name);
35138
35415
  const wtPath = getWorktreePath(repo, branch);
35139
- if (!fs27.existsSync(wtPath)) {
35416
+ if (!fs28.existsSync(wtPath)) {
35140
35417
  stepWarning("No worktree found", `${wtPath} (skipped)`);
35141
35418
  continue;
35142
35419
  }
35143
35420
  try {
35144
35421
  if (repo.config.sync_files) {
35145
35422
  for (const file2 of repo.config.sync_files) {
35146
- if (fs27.existsSync(path33.join(repo.mainPath, file2))) {
35423
+ if (fs28.existsSync(path33.join(repo.mainPath, file2))) {
35147
35424
  if (!opts.dryRun) {
35148
35425
  syncEntry(repo.mainPath, wtPath, file2);
35149
35426
  }
@@ -35196,14 +35473,14 @@ function registerSyncCommand(program2) {
35196
35473
  }
35197
35474
  }
35198
35475
  const wtNodeModules = path33.join(wtPath, "node_modules");
35199
- if (fs27.existsSync(wtNodeModules) && fs27.lstatSync(wtNodeModules).isSymbolicLink()) {
35476
+ if (fs28.existsSync(wtNodeModules) && fs28.lstatSync(wtNodeModules).isSymbolicLink()) {
35200
35477
  const lockfiles = ["yarn.lock", "package-lock.json", "pnpm-lock.yaml", "bun.lockb", "bun.lock"];
35201
35478
  for (const lock of lockfiles) {
35202
35479
  const mainLock = path33.join(repo.mainPath, lock);
35203
35480
  const wtLock = path33.join(wtPath, lock);
35204
- if (fs27.existsSync(mainLock) && fs27.existsSync(wtLock)) {
35205
- const mainContent = fs27.readFileSync(mainLock);
35206
- const wtContent = fs27.readFileSync(wtLock);
35481
+ if (fs28.existsSync(mainLock) && fs28.existsSync(wtLock)) {
35482
+ const mainContent = fs28.readFileSync(mainLock);
35483
+ const wtContent = fs28.readFileSync(wtLock);
35207
35484
  if (!mainContent.equals(wtContent)) {
35208
35485
  stepWarning(`${lock} differs from main — node_modules is symlinked`);
35209
35486
  indented(`Run: wtx deps ${branch} --repo ${repo.name} --install`);
@@ -35232,7 +35509,7 @@ init_config();
35232
35509
  init_log();
35233
35510
  init_resolver();
35234
35511
  init_deps();
35235
- import fs28 from "fs";
35512
+ import fs29 from "fs";
35236
35513
  function registerDepsCommand(program2) {
35237
35514
  program2.command("deps [branch]").description("Manage node_modules strategy per worktree").option("-r, --repo <repos...>", "Target specific repo(s)").option("--install", "Switch to independent node_modules (run install)").option("--symlink", "Switch to legacy symlinked node_modules").option("--json", "Output machine-readable JSON state").action(async (branch, options) => {
35238
35515
  const globalOpts = program2.opts();
@@ -35269,7 +35546,7 @@ function registerDepsCommand(program2) {
35269
35546
  continue;
35270
35547
  }
35271
35548
  const wtPath = getWorktreePath(repo, branch);
35272
- if (!fs28.existsSync(wtPath)) {
35549
+ if (!fs29.existsSync(wtPath)) {
35273
35550
  if (options.json) {
35274
35551
  jsonResults[repo.name] = { error: "No worktree found", branch };
35275
35552
  } else {
@@ -35327,7 +35604,7 @@ function registerDepsCommand(program2) {
35327
35604
  init_config();
35328
35605
  init_log();
35329
35606
  init_resolver();
35330
- import fs29 from "fs";
35607
+ import fs30 from "fs";
35331
35608
  init_git();
35332
35609
  async function resolveMainCheckoutPath(repoCtx, branch) {
35333
35610
  try {
@@ -35352,7 +35629,7 @@ function registerOpenCommand(program2) {
35352
35629
  for (const repo of repos) {
35353
35630
  const wtPath = getWorktreePath(repo, branch);
35354
35631
  let targetPath = wtPath;
35355
- if (!fs29.existsSync(wtPath)) {
35632
+ if (!fs30.existsSync(wtPath)) {
35356
35633
  const mainCheckoutPath = await resolveMainCheckoutPath(repo, branch);
35357
35634
  if (!mainCheckoutPath) {
35358
35635
  continue;
@@ -35388,7 +35665,7 @@ import path35 from "path";
35388
35665
  init_git();
35389
35666
  init_resolver();
35390
35667
  init_path_safety();
35391
- import fs30 from "fs";
35668
+ import fs31 from "fs";
35392
35669
  import path34 from "path";
35393
35670
  init_workspace();
35394
35671
  async function getUpstream(wtPath) {
@@ -35431,7 +35708,7 @@ async function planRename(repo, oldBranch, newBranch, opts) {
35431
35708
  throw new Error(`Worktree '${oldBranch}' is locked — unlock it before renaming`);
35432
35709
  }
35433
35710
  const newPath = `${repo.wtRoot}/${newBranch}`;
35434
- if (fs30.existsSync(newPath)) {
35711
+ if (fs31.existsSync(newPath)) {
35435
35712
  throw new Error(`Target path already exists: ${newPath}`);
35436
35713
  }
35437
35714
  if (!opts.dryRun && await localBranchExists(repo.mainPath, newBranch, opts)) {
@@ -35464,7 +35741,7 @@ async function renameWorktree(params) {
35464
35741
  }
35465
35742
  await gitExec(["-C", planned.worktreePath, "branch", "-m", oldBranch, newBranch], opts);
35466
35743
  try {
35467
- fs30.mkdirSync(path34.dirname(planned.newPath), { recursive: true });
35744
+ fs31.mkdirSync(path34.dirname(planned.newPath), { recursive: true });
35468
35745
  await gitExec(["-C", repo.mainPath, "worktree", "move", planned.worktreePath, planned.newPath], opts);
35469
35746
  } catch (err) {
35470
35747
  const message = err instanceof Error ? err.message : String(err);
@@ -35483,10 +35760,10 @@ async function renameWorktree(params) {
35483
35760
  const resyncedFiles = [];
35484
35761
  const keptLocalSyncFiles = [];
35485
35762
  for (const entry of repo.config.sync_files ?? []) {
35486
- if (!fs30.existsSync(path34.join(repo.mainPath, entry)))
35763
+ if (!fs31.existsSync(path34.join(repo.mainPath, entry)))
35487
35764
  continue;
35488
35765
  const destPath = path34.join(planned.newPath, entry);
35489
- if (!fs30.existsSync(destPath)) {
35766
+ if (!fs31.existsSync(destPath)) {
35490
35767
  if (syncEntry(repo.mainPath, planned.newPath, entry)) {
35491
35768
  resyncedFiles.push(entry);
35492
35769
  }
@@ -35625,7 +35902,7 @@ init_resolver();
35625
35902
  init_deps();
35626
35903
  init_forge();
35627
35904
  init_types2();
35628
- import fs31 from "fs";
35905
+ import fs32 from "fs";
35629
35906
  init_owner();
35630
35907
  init_stack();
35631
35908
  init_source();
@@ -35675,7 +35952,7 @@ function registerStatusCommand(program2) {
35675
35952
  const jsonOutputs = [];
35676
35953
  for (const repo of repos) {
35677
35954
  const wtPath = getWorktreePath(repo, branch);
35678
- if (!fs31.existsSync(wtPath)) {
35955
+ if (!fs32.existsSync(wtPath)) {
35679
35956
  continue;
35680
35957
  }
35681
35958
  found++;
@@ -35975,7 +36252,7 @@ function registerPrsCommand(program2) {
35975
36252
 
35976
36253
  // src/commands/skill.ts
35977
36254
  init_log();
35978
- import fs32 from "fs";
36255
+ import fs33 from "fs";
35979
36256
  import path36 from "path";
35980
36257
  import { URL as URL2 } from "url";
35981
36258
  var __dirname2 = new URL2(".", import.meta.url).pathname;
@@ -36132,7 +36409,7 @@ function registerSkillCommand(program2) {
36132
36409
  }
36133
36410
  let projectRoot = path36.resolve(__dirname2, "..", "..");
36134
36411
  let skillPath = path36.join(projectRoot, "skills", `${platform2}.md`);
36135
- if (fs32.existsSync(skillPath)) {
36412
+ if (fs33.existsSync(skillPath)) {
36136
36413
  process.stdout.write(skillPath + `
36137
36414
  `);
36138
36415
  } else {
@@ -36145,8 +36422,8 @@ function registerSkillCommand(program2) {
36145
36422
 
36146
36423
  // src/commands/terminal.ts
36147
36424
  import { spawnSync as spawnSync3 } from "node:child_process";
36148
- import fs34 from "node:fs";
36149
- import path38 from "node:path";
36425
+ import fs36 from "node:fs";
36426
+ import path39 from "node:path";
36150
36427
  import { fileURLToPath as fileURLToPath4 } from "node:url";
36151
36428
  function findEntry() {
36152
36429
  const candidates = [];
@@ -36155,26 +36432,26 @@ function findEntry() {
36155
36432
  try {
36156
36433
  const thisPath = fileURLToPath4(import.meta.url);
36157
36434
  candidates.push(thisPath);
36158
- candidates.push(path38.resolve(path38.dirname(thisPath), "../../dist/cli.mjs"));
36159
- candidates.push(path38.resolve(path38.dirname(thisPath), "../cli.mjs"));
36435
+ candidates.push(path39.resolve(path39.dirname(thisPath), "../../dist/cli.mjs"));
36436
+ candidates.push(path39.resolve(path39.dirname(thisPath), "../cli.mjs"));
36160
36437
  } catch {}
36161
36438
  for (const p of candidates) {
36162
36439
  try {
36163
36440
  if (p.endsWith(".mjs") || p.endsWith(".js")) {
36164
- if (fs34.existsSync(p) && fs34.statSync(p).isFile())
36441
+ if (fs36.existsSync(p) && fs36.statSync(p).isFile())
36165
36442
  return p;
36166
- } else if (fs34.existsSync(p)) {
36167
- const stat = fs34.statSync(p);
36443
+ } else if (fs36.existsSync(p)) {
36444
+ const stat = fs36.statSync(p);
36168
36445
  if (stat.isFile()) {
36169
- const head = fs34.readFileSync(p, "utf8").slice(0, 1024);
36446
+ const head = fs36.readFileSync(p, "utf8").slice(0, 1024);
36170
36447
  if (head.includes("cli.mjs") || head.includes("#!/usr/bin/env node")) {
36171
36448
  if (p.endsWith("wtx") || p.endsWith("cli.mjs")) {
36172
- const dir = path38.dirname(fs34.realpathSync(p));
36173
- const cli = path38.join(dir, "cli.mjs");
36174
- if (fs34.existsSync(cli))
36449
+ const dir = path39.dirname(fs36.realpathSync(p));
36450
+ const cli = path39.join(dir, "cli.mjs");
36451
+ if (fs36.existsSync(cli))
36175
36452
  return cli;
36176
- const cli2 = path38.resolve(dir, "../dist/cli.mjs");
36177
- if (fs34.existsSync(cli2))
36453
+ const cli2 = path39.resolve(dir, "../dist/cli.mjs");
36454
+ if (fs36.existsSync(cli2))
36178
36455
  return cli2;
36179
36456
  if (head.includes("import") || head.includes("commander"))
36180
36457
  return p;
@@ -36186,9 +36463,9 @@ function findEntry() {
36186
36463
  }
36187
36464
  try {
36188
36465
  const thisPath = fileURLToPath4(import.meta.url);
36189
- const pkgRoot = path38.resolve(path38.dirname(thisPath), "../..");
36190
- const cli = path38.join(pkgRoot, "dist", "cli.mjs");
36191
- if (fs34.existsSync(cli))
36466
+ const pkgRoot = path39.resolve(path39.dirname(thisPath), "../..");
36467
+ const cli = path39.join(pkgRoot, "dist", "cli.mjs");
36468
+ if (fs36.existsSync(cli))
36192
36469
  return cli;
36193
36470
  } catch {}
36194
36471
  return null;
@@ -36292,7 +36569,7 @@ init_deps();
36292
36569
  import readline3 from "readline";
36293
36570
  init_path_safety();
36294
36571
  init_stack();
36295
- import fs35 from "fs";
36572
+ import fs37 from "fs";
36296
36573
  async function runMcpServer(opts = {}) {
36297
36574
  const input = opts.input ?? process.stdin;
36298
36575
  const output = opts.output ?? process.stdout;
@@ -36475,7 +36752,7 @@ async function handleToolCall(name, args, config2, _opts) {
36475
36752
  for (const wt of wts) {
36476
36753
  if (!wt.path.startsWith(repo.wtRoot))
36477
36754
  continue;
36478
- const dirtyCount = fs35.existsSync(wt.path) ? (await getDirtyFiles(wt.path)).length : 0;
36755
+ const dirtyCount = fs37.existsSync(wt.path) ? (await getDirtyFiles(wt.path)).length : 0;
36479
36756
  items.push({
36480
36757
  repo: repo.name,
36481
36758
  branch: wt.branch || null,
@@ -36503,7 +36780,7 @@ async function handleToolCall(name, args, config2, _opts) {
36503
36780
  }
36504
36781
  const repo = resolveRepos(config2, [args.repo])[0];
36505
36782
  const wtPath = getWorktreePath(repo, args.branch);
36506
- if (!fs35.existsSync(wtPath)) {
36783
+ if (!fs37.existsSync(wtPath)) {
36507
36784
  throw { isToolError: true, message: `Worktree not found for branch ${args.branch}` };
36508
36785
  }
36509
36786
  const mainBranch = await resolveMainBranch(repo, config2);
@@ -36622,13 +36899,18 @@ async function handleToolCall(name, args, config2, _opts) {
36622
36899
  if (children.length > 0 && args.force !== true) {
36623
36900
  throw { isToolError: true, message: `Branch has dependent worktrees: ${children.join(", ")}. Use force:true to override.` };
36624
36901
  }
36625
- if (!args.force && fs35.existsSync(wtPath)) {
36902
+ if (!args.force && fs37.existsSync(wtPath)) {
36626
36903
  const dirty = await getDirtyFiles(wtPath);
36627
36904
  if (dirty.length > 0) {
36628
36905
  throw { isToolError: true, message: `Worktree is dirty. Use force:true to remove it.` };
36629
36906
  }
36630
36907
  }
36631
- await gitExec(["-C", repo.mainPath, "worktree", "remove", args.force ? "--force" : "", wtPath].filter(Boolean));
36908
+ await removeWorktreeSafely({
36909
+ mainPath: repo.mainPath,
36910
+ wtPath,
36911
+ force: args.force === true,
36912
+ opts: { verbose: _opts.verbose === true, dryRun: false }
36913
+ });
36632
36914
  cleanupEmptyParents(repo.wtRoot, repo.mainPath, wtPath);
36633
36915
  await removeStackEntry(repo.mainPath, args.branch, { verbose: _opts.verbose === true, dryRun: false });
36634
36916
  return {
@@ -36641,7 +36923,7 @@ async function handleToolCall(name, args, config2, _opts) {
36641
36923
  }
36642
36924
  const repo = resolveRepos(config2, [args.repo])[0];
36643
36925
  const wtPath = getWorktreePath(repo, args.branch);
36644
- if (!fs35.existsSync(wtPath)) {
36926
+ if (!fs37.existsSync(wtPath)) {
36645
36927
  throw { isToolError: true, message: `Worktree not found for branch ${args.branch}` };
36646
36928
  }
36647
36929
  const mainBranch = await resolveMainBranch(repo, config2);
@@ -36863,8 +37145,8 @@ init_config();
36863
37145
  init_log();
36864
37146
  init_git();
36865
37147
  init_resolver();
36866
- import fs36 from "fs";
36867
- import path39 from "path";
37148
+ import fs38 from "fs";
37149
+ import path40 from "path";
36868
37150
  init_workspace();
36869
37151
  var DEPS_STRATEGIES2 = ["auto", "link", "symlink", "install", "off"];
36870
37152
  function errorMessage4(err) {
@@ -36874,7 +37156,7 @@ function workspaceRootFor(config2) {
36874
37156
  return getWorkspaceRoot(config2);
36875
37157
  }
36876
37158
  function workspacePathFor2(config2, name) {
36877
- return path39.join(workspaceRootFor(config2), name);
37159
+ return path40.join(workspaceRootFor(config2), name);
36878
37160
  }
36879
37161
  async function resolveWorktreePathForMember(config2, repoName, branch) {
36880
37162
  const repos = resolveRepos(config2, [repoName]);
@@ -36886,7 +37168,7 @@ async function resolveWorktreePathForMember(config2, repoName, branch) {
36886
37168
  if (match)
36887
37169
  return match.path;
36888
37170
  const candidate = getWorktreePath(repo, branch);
36889
- if (fs36.existsSync(candidate))
37171
+ if (fs38.existsSync(candidate))
36890
37172
  return candidate;
36891
37173
  return null;
36892
37174
  }
@@ -36944,7 +37226,7 @@ function registerCreateSub(workspace) {
36944
37226
  process.exit(1);
36945
37227
  }
36946
37228
  const wsPath = workspacePathFor2(config2, name);
36947
- if (fs36.existsSync(wsPath)) {
37229
+ if (fs38.existsSync(wsPath)) {
36948
37230
  stepError("Workspace already exists", wsPath);
36949
37231
  process.exit(1);
36950
37232
  }
@@ -37045,7 +37327,7 @@ function registerAddSub(workspace) {
37045
37327
  const globalOpts = workspace.parent.opts();
37046
37328
  const config2 = loadConfig();
37047
37329
  const wsPath = workspacePathFor2(config2, name);
37048
- if (!fs36.existsSync(wsPath)) {
37330
+ if (!fs38.existsSync(wsPath)) {
37049
37331
  stepError("Workspace not found", wsPath);
37050
37332
  process.exit(1);
37051
37333
  }
@@ -37081,7 +37363,7 @@ function registerRmSub(workspace) {
37081
37363
  const globalOpts = workspace.parent.opts();
37082
37364
  const config2 = loadConfig();
37083
37365
  const wsPath = workspacePathFor2(config2, name);
37084
- if (!fs36.existsSync(wsPath)) {
37366
+ if (!fs38.existsSync(wsPath)) {
37085
37367
  stepError("Workspace not found", wsPath);
37086
37368
  process.exit(1);
37087
37369
  }
@@ -37103,7 +37385,7 @@ function registerRemoveSub(workspace) {
37103
37385
  const globalOpts = workspace.parent.opts();
37104
37386
  const config2 = loadConfig();
37105
37387
  const wsPath = workspacePathFor2(config2, name);
37106
- if (!fs36.existsSync(wsPath)) {
37388
+ if (!fs38.existsSync(wsPath)) {
37107
37389
  stepError("Workspace not found", wsPath);
37108
37390
  process.exit(1);
37109
37391
  }
@@ -37117,7 +37399,7 @@ function registerRemoveSub(workspace) {
37117
37399
  return;
37118
37400
  }
37119
37401
  try {
37120
- fs36.rmSync(wsPath, { recursive: true, force: true });
37402
+ fs38.rmSync(wsPath, { recursive: true, force: true });
37121
37403
  stepSuccess("Workspace removed", wsPath);
37122
37404
  summary(`Done — workspace ${name} deleted (member worktrees preserved)`);
37123
37405
  } catch (err) {
@@ -37130,7 +37412,7 @@ function registerVerifySub(workspace) {
37130
37412
  workspace.command("verify <name>").description("Verify that every workspace member link resolves").action(async (name) => {
37131
37413
  const config2 = loadConfig();
37132
37414
  const wsPath = workspacePathFor2(config2, name);
37133
- if (!fs36.existsSync(wsPath)) {
37415
+ if (!fs38.existsSync(wsPath)) {
37134
37416
  stepError("Workspace not found", wsPath);
37135
37417
  process.exit(1);
37136
37418
  }
@@ -37165,7 +37447,7 @@ init_config();
37165
37447
  init_resolver();
37166
37448
  init_log();
37167
37449
  init_history();
37168
- import fs37 from "fs";
37450
+ import fs39 from "fs";
37169
37451
  var program2 = new Command;
37170
37452
  program2.name("wtx").description("Multi-repo git worktree manager").version(VERSION).option("-q, --quiet", "Suppress progress indicators", false).option("--verbose", "Show git commands as they run", false).option("--dry-run", "Show what would happen", false);
37171
37453
  var MUTATING_COMMANDS = new Set([
@@ -37275,7 +37557,7 @@ program2.command("_resolve-path <repo> <branch>", { hidden: true }).description(
37275
37557
  }
37276
37558
  const repo = repos[0];
37277
37559
  const wtPath = getWorktreePath(repo, branch);
37278
- if (!fs37.existsSync(wtPath)) {
37560
+ if (!fs39.existsSync(wtPath)) {
37279
37561
  process.stderr.write(`✗ No worktree at ${wtPath}
37280
37562
  `);
37281
37563
  process.exit(1);