@ogpoyraz/wtx 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.mjs +446 -235
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -280,7 +280,7 @@ Lookups degrade gracefully: if `gh` is missing or unauthenticated you get a warn
280
280
  | `c` | Edit configuration |
281
281
  | `?` / `q` | Help / quit |
282
282
 
283
- Actions show inline progress`creating worktree` next to the repo header, `deleting` / `rebasing` / `syncing` next to the branch while the dashboard is busy. Input is locked until the operation (and its refresh) finishes; on failure a log modal opens with the captured output. Destructive ones confirm first.
283
+ Actions run in the background navigation never locks. Progress shows inline: `fetching` next to the repo header, `deleting` / `rebasing` / `syncing` next to the branch (dimmed while busy), and a new worktree appears immediately as a `(creating)` row. One operation runs per repo at a time; conflicting actions are rejected with a toast until it finishes. On failure a log modal opens with the captured output; press any key to dismiss. Destructive ones confirm first.
284
284
 
285
285
  ---
286
286
 
package/dist/cli.mjs CHANGED
@@ -24910,7 +24910,7 @@ var init_deps = __esm(() => {
24910
24910
  });
24911
24911
 
24912
24912
  // src/tui/data.ts
24913
- async function fetchWorktreeData(opts) {
24913
+ async function fetchWorktreeData(opts, scope) {
24914
24914
  const warnings = [];
24915
24915
  let config2;
24916
24916
  try {
@@ -24924,6 +24924,10 @@ async function fetchWorktreeData(opts) {
24924
24924
  } catch (err) {
24925
24925
  throw new Error(`Failed to resolve repos: ${err.message}`);
24926
24926
  }
24927
+ if (scope) {
24928
+ const scopeSet = new Set(scope);
24929
+ repos = repos.filter((r) => scopeSet.has(r.name));
24930
+ }
24927
24931
  const semaphore = new Semaphore(4);
24928
24932
  const allRows = [];
24929
24933
  const processRepo = async (repo) => {
@@ -24941,7 +24945,7 @@ async function fetchWorktreeData(opts) {
24941
24945
  prCache.set(`${repo.name}/${br}`, pr);
24942
24946
  }
24943
24947
  } catch (err) {
24944
- warnings.push(`PR lookup failed for ${repo.name}: ${err.message}`);
24948
+ warnings.push({ repoName: repo.name, message: `PR lookup failed for ${repo.name}: ${err.message}` });
24945
24949
  for (const br of branches) {
24946
24950
  const cached2 = prCache.get(`${repo.name}/${br}`);
24947
24951
  if (cached2) {
@@ -25043,7 +25047,7 @@ async function fetchWorktreeData(opts) {
25043
25047
  allRows.push(row);
25044
25048
  }
25045
25049
  } catch (err) {
25046
- warnings.push(`Failed to process repo ${repo.name}: ${err.message}`);
25050
+ warnings.push({ repoName: repo.name, message: `Failed to process repo ${repo.name}: ${err.message}` });
25047
25051
  } finally {
25048
25052
  semaphore.release();
25049
25053
  }
@@ -25066,20 +25070,131 @@ var init_data = __esm(() => {
25066
25070
  prCache = new Map;
25067
25071
  });
25068
25072
 
25073
+ // src/tui/utils.ts
25074
+ function matchesFilter(entry, term) {
25075
+ if (!term)
25076
+ return true;
25077
+ const lower = term.toLowerCase();
25078
+ if (entry.branch.toLowerCase().includes(lower))
25079
+ return true;
25080
+ if (entry.repoName.toLowerCase().includes(lower))
25081
+ return true;
25082
+ if (entry.prNumber?.toString().includes(lower))
25083
+ return true;
25084
+ if (entry.owner?.toLowerCase().includes(lower))
25085
+ return true;
25086
+ if (entry.prState?.toLowerCase().includes(lower))
25087
+ return true;
25088
+ if (entry.prUrl?.toLowerCase().includes(lower))
25089
+ return true;
25090
+ return false;
25091
+ }
25092
+ function toggleSelection(current, path33) {
25093
+ const next = new Set(current);
25094
+ if (next.has(path33)) {
25095
+ next.delete(path33);
25096
+ } else {
25097
+ next.add(path33);
25098
+ }
25099
+ return next;
25100
+ }
25101
+ function computeScrollWindow(selectedIndex, currentStart, visibleRows, totalRows) {
25102
+ if (totalRows === 0)
25103
+ return { start: 0, end: 0 };
25104
+ let start = currentStart;
25105
+ const maxStart = Math.max(0, totalRows - visibleRows);
25106
+ if (start > maxStart) {
25107
+ start = maxStart;
25108
+ }
25109
+ if (selectedIndex < start) {
25110
+ start = selectedIndex;
25111
+ } else if (selectedIndex >= start + visibleRows) {
25112
+ start = selectedIndex - visibleRows + 1;
25113
+ }
25114
+ const end = Math.min(start + visibleRows, totalRows);
25115
+ return { start, end };
25116
+ }
25117
+ function rowSort(a2, b) {
25118
+ if (a2.isMainCheckout && !b.isMainCheckout)
25119
+ return -1;
25120
+ if (!a2.isMainCheckout && b.isMainCheckout)
25121
+ return 1;
25122
+ return a2.branch.localeCompare(b.branch);
25123
+ }
25124
+ function mergeBlocks(prev, next, scope) {
25125
+ if (!scope)
25126
+ return next;
25127
+ const kept = prev.filter((b) => !scope.has(b.repoName));
25128
+ return [...kept, ...next].sort((a2, b) => a2.repoName.localeCompare(b.repoName));
25129
+ }
25130
+ function mergeWarnings(prev, next, scope) {
25131
+ if (!scope)
25132
+ return next;
25133
+ const kept = prev.filter((w) => !scope.has(w.repoName));
25134
+ return [...kept, ...next];
25135
+ }
25136
+ function makePlaceholderRow(repoName, branch) {
25137
+ return {
25138
+ repoName,
25139
+ branch,
25140
+ path: `pending-create:${repoName}:${branch}`,
25141
+ commitShort: "",
25142
+ isMainCheckout: false,
25143
+ isLocked: false,
25144
+ isPrunable: false,
25145
+ isBare: false,
25146
+ dirtyFiles: [],
25147
+ ahead: null,
25148
+ behind: null,
25149
+ prNumber: null,
25150
+ prState: null,
25151
+ prChecks: null,
25152
+ prUrl: null,
25153
+ owner: null,
25154
+ rebaseStatus: null,
25155
+ depsStrategy: "none",
25156
+ isPendingCreate: true
25157
+ };
25158
+ }
25159
+ function withCreatePlaceholders(blocks, creating) {
25160
+ if (creating.length === 0)
25161
+ return blocks;
25162
+ const byRepo = new Map;
25163
+ for (const c4 of creating) {
25164
+ const arr = byRepo.get(c4.repoName);
25165
+ if (arr) {
25166
+ arr.push(c4.branch);
25167
+ } else {
25168
+ byRepo.set(c4.repoName, [c4.branch]);
25169
+ }
25170
+ }
25171
+ return blocks.map((block) => {
25172
+ const branches = byRepo.get(block.repoName);
25173
+ if (!branches)
25174
+ return block;
25175
+ const placeholders = branches.map((br) => makePlaceholderRow(block.repoName, br));
25176
+ return { ...block, rows: [...block.rows, ...placeholders].sort(rowSort) };
25177
+ });
25178
+ }
25179
+
25069
25180
  // src/tui/hooks/useWorktrees.ts
25070
- import { useState, useEffect, useCallback } from "react";
25181
+ import { useState, useEffect, useCallback, useRef } from "react";
25071
25182
  function useWorktrees(opts) {
25072
25183
  const [blocks, setBlocks] = useState([]);
25073
25184
  const [loading, setLoading] = useState(true);
25185
+ const [refreshing, setRefreshing] = useState(false);
25074
25186
  const [error52, setError] = useState(null);
25075
25187
  const [warnings, setWarnings] = useState([]);
25076
25188
  const [lastRefreshed, setLastRefreshed] = useState("");
25077
- const refresh = useCallback(async () => {
25078
- setLoading(true);
25189
+ const seqRef = useRef(0);
25190
+ const refresh = useCallback(async (scope) => {
25191
+ const seq = ++seqRef.current;
25192
+ setRefreshing(true);
25079
25193
  setError(null);
25080
- setWarnings([]);
25081
25194
  try {
25082
- const data = await fetchWorktreeData(opts);
25195
+ const data = await fetchWorktreeData(opts, scope);
25196
+ if (seq !== seqRef.current)
25197
+ return;
25083
25198
  const byRepo = new Map;
25084
25199
  for (const row of data.rows) {
25085
25200
  let arr = byRepo.get(row.repoName);
@@ -25091,29 +25206,27 @@ function useWorktrees(opts) {
25091
25206
  }
25092
25207
  const newBlocks = [];
25093
25208
  for (const [repoName, rows] of byRepo.entries()) {
25094
- rows.sort((a2, b) => {
25095
- if (a2.isMainCheckout && !b.isMainCheckout)
25096
- return -1;
25097
- if (!a2.isMainCheckout && b.isMainCheckout)
25098
- return 1;
25099
- return a2.branch.localeCompare(b.branch);
25100
- });
25209
+ rows.sort(rowSort);
25101
25210
  newBlocks.push({ repoName, rows });
25102
25211
  }
25103
- newBlocks.sort((a2, b) => a2.repoName.localeCompare(b.repoName));
25104
- setBlocks(newBlocks);
25105
- setWarnings(data.warnings);
25212
+ const scopeSet = scope ? new Set(scope) : undefined;
25213
+ setBlocks((prev) => mergeBlocks(prev, newBlocks, scopeSet));
25214
+ setWarnings((prev) => mergeWarnings(prev, data.warnings, scopeSet));
25106
25215
  setLastRefreshed(new Date().toLocaleTimeString());
25107
25216
  } catch (err) {
25108
- setError(err.message);
25217
+ if (seq === seqRef.current)
25218
+ setError(err.message);
25109
25219
  } finally {
25110
- setLoading(false);
25220
+ if (seq === seqRef.current) {
25221
+ setLoading(false);
25222
+ setRefreshing(false);
25223
+ }
25111
25224
  }
25112
25225
  }, [opts]);
25113
25226
  useEffect(() => {
25114
25227
  refresh();
25115
25228
  }, [refresh]);
25116
- return { blocks, loading, error: error52, warnings, lastRefreshed, refresh };
25229
+ return { blocks, loading, refreshing, error: error52, warnings, lastRefreshed, refresh };
25117
25230
  }
25118
25231
  var init_useWorktrees = __esm(() => {
25119
25232
  init_data();
@@ -25151,7 +25264,7 @@ var init_theme = __esm(() => {
25151
25264
  });
25152
25265
 
25153
25266
  // src/tui/components/WorktreeTable.tsx
25154
- import { useEffect as useEffect2, useRef } from "react";
25267
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
25155
25268
  import { jsx, jsxs } from "@opentui/react/jsx-runtime";
25156
25269
  function statusBadge(row) {
25157
25270
  if (row.isMainCheckout)
@@ -25166,9 +25279,10 @@ function statusBadge(row) {
25166
25279
  return { text: `dirty (${row.dirtyFiles.length})`, fg: tokens.warning };
25167
25280
  return { text: "clean", fg: tokens.dim };
25168
25281
  }
25169
- function WorktreeItem({ row, isSelected, isMultiSelected, busy, id }) {
25170
- const badge = busy ? { text: `${busy.frame} ${busy.verb}…`, fg: tokens.accent } : statusBadge(row);
25171
- const primary = isSelected ? tokens.bright : tokens.fg;
25282
+ function WorktreeItem({ row, isSelected, isMultiSelected, indicator, frame, id }) {
25283
+ 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);
25284
+ const disabled = indicator !== undefined || row.isPendingCreate === true;
25285
+ const primary = isSelected ? tokens.bright : disabled ? tokens.dim : tokens.fg;
25172
25286
  const divergence = row.ahead !== null && row.behind !== null && (row.ahead > 0 || row.behind > 0) ? ` · ↑${row.ahead} ↓${row.behind}` : "";
25173
25287
  const prSegment = row.prNumber ? [
25174
25288
  `· #${row.prNumber}`,
@@ -25224,8 +25338,8 @@ function WorktreeItem({ row, isSelected, isMultiSelected, busy, id }) {
25224
25338
  ]
25225
25339
  });
25226
25340
  }
25227
- function WorktreeTable({ blocks, selectedIndex, selection = new Set, busy }) {
25228
- const scrollRef = useRef(null);
25341
+ function WorktreeTable({ blocks, selectedIndex, selection = new Set, frame, repoVerbs, rowVerbs }) {
25342
+ const scrollRef = useRef2(null);
25229
25343
  useEffect2(() => {
25230
25344
  if (scrollRef.current?.scrollChildIntoView) {
25231
25345
  scrollRef.current.scrollChildIntoView("selected-row");
@@ -25245,42 +25359,48 @@ function WorktreeTable({ blocks, selectedIndex, selection = new Set, busy }) {
25245
25359
  children: blocks.length === 0 ? /* @__PURE__ */ jsx("text", {
25246
25360
  fg: tokens.dim,
25247
25361
  children: "No repositories configured."
25248
- }) : blocks.map((block) => /* @__PURE__ */ jsxs("box", {
25249
- flexDirection: "column",
25250
- style: { marginBottom: 1 },
25251
- children: [
25252
- /* @__PURE__ */ jsx("box", {
25253
- style: { marginTop: flatIndex === 0 ? 0 : 1 },
25254
- children: /* @__PURE__ */ jsxs("text", {
25255
- children: [
25256
- /* @__PURE__ */ jsx("span", {
25257
- fg: tokens.bright,
25258
- children: block.repoName
25259
- }),
25260
- /* @__PURE__ */ jsx("span", {
25261
- fg: tokens.dim,
25262
- children: ` · ${block.rows.length}`
25263
- }),
25264
- busy && busy.repoNames.includes(block.repoName) && !busy.rowPath && /* @__PURE__ */ jsx("span", {
25265
- fg: tokens.accent,
25266
- children: ` ${busy.frame} ${busy.verb}…`
25267
- })
25268
- ]
25362
+ }) : blocks.map((block) => {
25363
+ const repoIndicator = repoVerbs.get(block.repoName);
25364
+ return /* @__PURE__ */ jsxs("box", {
25365
+ flexDirection: "column",
25366
+ style: { marginBottom: 1 },
25367
+ children: [
25368
+ /* @__PURE__ */ jsx("box", {
25369
+ style: { marginTop: flatIndex === 0 ? 0 : 1 },
25370
+ children: /* @__PURE__ */ jsxs("text", {
25371
+ children: [
25372
+ /* @__PURE__ */ jsx("span", {
25373
+ fg: tokens.bright,
25374
+ children: block.repoName
25375
+ }),
25376
+ /* @__PURE__ */ jsx("span", {
25377
+ fg: tokens.dim,
25378
+ children: ` · ${block.rows.filter((r) => !r.isPendingCreate).length}`
25379
+ }),
25380
+ repoIndicator && /* @__PURE__ */ jsx("span", {
25381
+ fg: repoIndicator.running ? tokens.accent : tokens.dim,
25382
+ children: repoIndicator.running ? ` ${frame} ${repoIndicator.verb}…` : ` ◌ ${repoIndicator.verb}`
25383
+ })
25384
+ ]
25385
+ })
25386
+ }),
25387
+ block.rows.map((row) => {
25388
+ const navigable = !row.isPendingCreate;
25389
+ const isSelected = navigable && flatIndex === selectedIndex;
25390
+ if (navigable)
25391
+ flatIndex++;
25392
+ return /* @__PURE__ */ jsx(WorktreeItem, {
25393
+ row,
25394
+ isSelected,
25395
+ isMultiSelected: selection.has(row.path),
25396
+ indicator: rowVerbs.get(row.path),
25397
+ frame,
25398
+ id: isSelected ? "selected-row" : undefined
25399
+ }, row.path);
25269
25400
  })
25270
- }),
25271
- block.rows.map((row) => {
25272
- const isSelected = flatIndex === selectedIndex;
25273
- flatIndex++;
25274
- return /* @__PURE__ */ jsx(WorktreeItem, {
25275
- row,
25276
- isSelected,
25277
- isMultiSelected: selection.has(row.path),
25278
- busy: busy?.rowPath === row.path ? busy : undefined,
25279
- id: isSelected ? "selected-row" : undefined
25280
- }, row.path);
25281
- })
25282
- ]
25283
- }, block.repoName))
25401
+ ]
25402
+ }, block.repoName);
25403
+ })
25284
25404
  });
25285
25405
  }
25286
25406
  var SECONDARY_INDENT = " ";
@@ -25949,7 +26069,7 @@ async function runWtxAction(args, onLine) {
25949
26069
 
25950
26070
  // src/tui/components/ActionLogModal.tsx
25951
26071
  import { jsx as jsx7, jsxs as jsxs6 } from "@opentui/react/jsx-runtime";
25952
- function ActionLogModal({ title, lines, done, exitCode }) {
26072
+ function ActionLogModal({ title, lines, done, exitCode, remaining }) {
25953
26073
  return /* @__PURE__ */ jsxs6(Overlay, {
25954
26074
  title,
25955
26075
  borderColor: tokens.border,
@@ -25989,6 +26109,10 @@ function ActionLogModal({ title, lines, done, exitCode }) {
25989
26109
  exitCode
25990
26110
  ]
25991
26111
  }),
26112
+ remaining !== undefined && remaining > 0 && /* @__PURE__ */ jsx7("span", {
26113
+ fg: tokens.warning,
26114
+ children: ` · ${remaining} more failure${remaining > 1 ? "s" : ""}`
26115
+ }),
25992
26116
  /* @__PURE__ */ jsx7("span", {
25993
26117
  fg: tokens.dim,
25994
26118
  children: " - press any key to close"
@@ -26200,51 +26324,6 @@ var init_InputModal = __esm(() => {
26200
26324
  init_theme();
26201
26325
  });
26202
26326
 
26203
- // src/tui/utils.ts
26204
- function matchesFilter(entry, term) {
26205
- if (!term)
26206
- return true;
26207
- const lower = term.toLowerCase();
26208
- if (entry.branch.toLowerCase().includes(lower))
26209
- return true;
26210
- if (entry.repoName.toLowerCase().includes(lower))
26211
- return true;
26212
- if (entry.prNumber?.toString().includes(lower))
26213
- return true;
26214
- if (entry.owner?.toLowerCase().includes(lower))
26215
- return true;
26216
- if (entry.prState?.toLowerCase().includes(lower))
26217
- return true;
26218
- if (entry.prUrl?.toLowerCase().includes(lower))
26219
- return true;
26220
- return false;
26221
- }
26222
- function toggleSelection(current, path34) {
26223
- const next = new Set(current);
26224
- if (next.has(path34)) {
26225
- next.delete(path34);
26226
- } else {
26227
- next.add(path34);
26228
- }
26229
- return next;
26230
- }
26231
- function computeScrollWindow(selectedIndex, currentStart, visibleRows, totalRows) {
26232
- if (totalRows === 0)
26233
- return { start: 0, end: 0 };
26234
- let start = currentStart;
26235
- const maxStart = Math.max(0, totalRows - visibleRows);
26236
- if (start > maxStart) {
26237
- start = maxStart;
26238
- }
26239
- if (selectedIndex < start) {
26240
- start = selectedIndex;
26241
- } else if (selectedIndex >= start + visibleRows) {
26242
- start = selectedIndex - visibleRows + 1;
26243
- }
26244
- const end = Math.min(start + visibleRows, totalRows);
26245
- return { start, end };
26246
- }
26247
-
26248
26327
  // src/tui/components/ConfigOverlay.tsx
26249
26328
  import { useState as useState4, useEffect as useEffect4 } from "react";
26250
26329
  import { useKeyboard } from "@opentui/react";
@@ -26634,16 +26713,19 @@ var init_useSpinnerFrame = __esm(() => {
26634
26713
  });
26635
26714
 
26636
26715
  // src/tui/components/App.tsx
26637
- import { useState as useState6, useEffect as useEffect6 } from "react";
26716
+ import { useState as useState6, useEffect as useEffect6, useMemo, useRef as useRef3, useCallback as useCallback2 } from "react";
26638
26717
  import { useKeyboard as useKeyboard2, useRenderer } from "@opentui/react";
26639
26718
  import { jsx as jsx11, jsxs as jsxs10 } from "@opentui/react/jsx-runtime";
26640
26719
  function App({ opts }) {
26641
26720
  const renderer = useRenderer();
26642
- const { blocks, loading, error: error52, warnings, lastRefreshed, refresh } = useWorktrees(opts);
26721
+ const { blocks, loading, refreshing, error: error52, warnings, lastRefreshed, refresh } = useWorktrees(opts);
26643
26722
  const [selectedIndex, setSelectedIndex] = useState6(0);
26644
26723
  const [modal, setModal] = useState6({ type: "none" });
26645
26724
  const [actionMessage, setActionMessage] = useState6();
26646
- const [actionRun, setActionRun] = useState6(null);
26725
+ const messageTimer = useRef3(null);
26726
+ const [ops, setOps] = useState6([]);
26727
+ const [failedLogs, setFailedLogs] = useState6([]);
26728
+ const nextOpId = useRef3(1);
26647
26729
  const [createModal, setCreateModal] = useState6(false);
26648
26730
  const [createError, setCreateError] = useState6();
26649
26731
  const [configOpen, setConfigOpen] = useState6(false);
@@ -26655,11 +26737,19 @@ function App({ opts }) {
26655
26737
  setModal({ type: "error", message: error52 });
26656
26738
  }
26657
26739
  }, [error52]);
26658
- const filteredBlocks = blocks.map((b) => ({
26659
- ...b,
26660
- rows: b.rows.filter((r) => matchesFilter(r, filterText))
26661
- })).filter((b) => b.rows.length > 0);
26662
- const flatRows = filteredBlocks.flatMap((b) => b.rows);
26740
+ const flash = useCallback2((message, ms = 3000) => {
26741
+ if (messageTimer.current)
26742
+ clearTimeout(messageTimer.current);
26743
+ setActionMessage(message);
26744
+ messageTimer.current = setTimeout(() => setActionMessage(undefined), ms);
26745
+ }, []);
26746
+ const busyRepos = useMemo(() => new Set(ops.flatMap((o2) => o2.repoNames)), [ops]);
26747
+ const busyRowPaths = useMemo(() => new Set(ops.map((o2) => o2.rowPath).filter((p) => p !== undefined)), [ops]);
26748
+ const anyRunning = ops.some((o2) => o2.status === "running");
26749
+ const spinnerFrame = useSpinnerFrame(anyRunning || loading || refreshing);
26750
+ const baseFiltered = useMemo(() => blocks.map((b) => ({ ...b, rows: b.rows.filter((r) => matchesFilter(r, filterText)) })).filter((b) => b.rows.length > 0), [blocks, filterText]);
26751
+ const displayBlocks = useMemo(() => withCreatePlaceholders(baseFiltered, ops.filter((o2) => o2.branch !== undefined).map((o2) => ({ repoName: o2.repoNames[0], branch: o2.branch }))), [baseFiltered, ops]);
26752
+ const flatRows = useMemo(() => displayBlocks.flatMap((b) => b.rows.filter((r) => !r.isPendingCreate)), [displayBlocks]);
26663
26753
  const totalRows = blocks.flatMap((b) => b.rows).length;
26664
26754
  const maxIndex = Math.max(0, flatRows.length - 1);
26665
26755
  useEffect6(() => {
@@ -26668,50 +26758,118 @@ function App({ opts }) {
26668
26758
  }
26669
26759
  }, [maxIndex, selectedIndex]);
26670
26760
  const selectedRow = flatRows[selectedIndex] ?? null;
26671
- const busy = actionRun !== null && !actionRun.done ? { kind: actionRun.kind, repoNames: actionRun.repoNames, rowPath: actionRun.rowPath } : null;
26672
- const spinnerFrame = useSpinnerFrame(busy !== null || loading);
26761
+ const { repoVerbs, rowVerbs } = useMemo(() => {
26762
+ const rv = new Map;
26763
+ const nv = new Map;
26764
+ for (const o2 of ops) {
26765
+ const indicator = { verb: VERBS[o2.kind], running: o2.status === "running" };
26766
+ if (o2.rowPath) {
26767
+ nv.set(o2.rowPath, indicator);
26768
+ } else {
26769
+ for (const rn of o2.repoNames)
26770
+ rv.set(rn, indicator);
26771
+ }
26772
+ }
26773
+ return { repoVerbs: rv, rowVerbs: nv };
26774
+ }, [ops]);
26775
+ const runningOps = ops.filter((o2) => o2.status === "running");
26776
+ const latestOp = runningOps[runningOps.length - 1];
26673
26777
  const getSelectedRows = () => {
26674
26778
  if (selection.size > 0) {
26675
26779
  return blocks.flatMap((b) => b.rows).filter((r) => selection.has(r.path));
26676
26780
  }
26677
26781
  return selectedRow ? [selectedRow] : [];
26678
26782
  };
26679
- const runSequentialActions = async (kind, targets, actionArgs) => {
26680
- for (const row of targets) {
26681
- await startAction(kind, `${capitalize(VERBS[kind])} ${row.branch}`, row.branch, [row.repoName], row.path, actionArgs(row));
26783
+ const findConflict = (targets) => {
26784
+ for (const t of targets) {
26785
+ if (busyRowPaths.has(t.path))
26786
+ return `${t.branch} is busy`;
26787
+ }
26788
+ for (const t of targets) {
26789
+ if (busyRepos.has(t.repoName))
26790
+ return `${t.repoName} is busy`;
26682
26791
  }
26683
- setSelection(new Set);
26792
+ return null;
26684
26793
  };
26685
- const startAction = async (kind, title, label, repoNames, rowPath, args) => {
26686
- return new Promise((resolve2) => {
26687
- setActionRun({ kind, repoNames, rowPath, title, label, lines: [], done: false, exitCode: null });
26688
- runWtxAction(args, (text, type) => {
26689
- setActionRun((prev) => {
26690
- if (!prev)
26691
- return prev;
26692
- return {
26693
- ...prev,
26694
- lines: [...prev.lines, { text, type }]
26695
- };
26696
- });
26697
- }).then((result) => {
26698
- if (result.exitCode === 0) {
26699
- setActionRun(null);
26700
- } else {
26701
- setActionRun((prev) => {
26702
- if (!prev)
26703
- return prev;
26704
- return {
26705
- ...prev,
26706
- done: true,
26707
- exitCode: result.exitCode
26708
- };
26709
- });
26710
- }
26711
- refresh();
26712
- resolve2();
26794
+ const executeOp = async (op, args, refreshScope) => {
26795
+ setOps((prev) => prev.map((o2) => o2.id === op.id ? { ...o2, status: "running" } : o2));
26796
+ const collected = [];
26797
+ let exitCode;
26798
+ try {
26799
+ const result = await runWtxAction(args, (text, type) => {
26800
+ collected.push({ text, type });
26801
+ setOps((prev) => prev.map((o2) => o2.id === op.id ? { ...o2, lines: [...o2.lines, { text, type }] } : o2));
26713
26802
  });
26803
+ exitCode = result.exitCode;
26804
+ } catch (err) {
26805
+ exitCode = 1;
26806
+ const msg = err instanceof Error ? err.message : String(err);
26807
+ collected.push({ text: `Failed to run wtx ${args.join(" ")}: ${msg}`, type: "err" });
26808
+ }
26809
+ if (exitCode === 0) {
26810
+ if (refreshScope)
26811
+ await refresh(refreshScope);
26812
+ setOps((prev) => prev.filter((o2) => o2.id !== op.id));
26813
+ } else {
26814
+ setOps((prev) => prev.filter((o2) => o2.id !== op.id));
26815
+ setFailedLogs((prev) => [...prev, { title: op.title, lines: collected, exitCode }]);
26816
+ }
26817
+ };
26818
+ const startBatchActions = (kind, targets, argsFor) => {
26819
+ const created = targets.map((row) => {
26820
+ const op = {
26821
+ id: nextOpId.current++,
26822
+ kind,
26823
+ repoNames: [row.repoName],
26824
+ rowPath: row.path,
26825
+ label: row.branch,
26826
+ title: `${capitalize(VERBS[kind])} ${row.branch}`,
26827
+ status: "queued",
26828
+ lines: []
26829
+ };
26830
+ return { op, row };
26714
26831
  });
26832
+ setOps((prev) => [...prev, ...created.map((c4) => c4.op)]);
26833
+ (async () => {
26834
+ for (const { op, row } of created) {
26835
+ await executeOp(op, argsFor(row), [row.repoName]);
26836
+ }
26837
+ setSelection(new Set);
26838
+ })();
26839
+ };
26840
+ const startFetch = (targets) => {
26841
+ const repoNames = [...new Set(targets.map((t) => t.repoName))];
26842
+ const conflict = repoNames.find((rn) => busyRepos.has(rn));
26843
+ if (conflict) {
26844
+ flash(`${conflict} is busy`);
26845
+ return;
26846
+ }
26847
+ const label = repoNames.length === 1 ? repoNames[0] : `${repoNames.length} repos`;
26848
+ const op = {
26849
+ id: nextOpId.current++,
26850
+ kind: "fetch",
26851
+ repoNames,
26852
+ label,
26853
+ title: `Fetch ${label}`,
26854
+ status: "queued",
26855
+ lines: []
26856
+ };
26857
+ setOps((prev) => [...prev, op]);
26858
+ executeOp(op, ["fetch", "--repo", repoNames.join(",")], repoNames).then(() => setSelection(new Set));
26859
+ };
26860
+ const startCreate = (branch, repoName) => {
26861
+ const op = {
26862
+ id: nextOpId.current++,
26863
+ kind: "create",
26864
+ repoNames: [repoName],
26865
+ branch,
26866
+ label: `${branch} in ${repoName}`,
26867
+ title: `Create ${branch}`,
26868
+ status: "queued",
26869
+ lines: []
26870
+ };
26871
+ setOps((prev) => [...prev, op]);
26872
+ executeOp(op, ["create", branch, "--repo", repoName], [repoName]);
26715
26873
  };
26716
26874
  useKeyboard2((key) => {
26717
26875
  if (isFiltering) {
@@ -26724,11 +26882,8 @@ function App({ opts }) {
26724
26882
  }
26725
26883
  return;
26726
26884
  }
26727
- if (loading || busy) {
26728
- return;
26729
- }
26730
- if (actionRun) {
26731
- setActionRun(null);
26885
+ if (failedLogs.length > 0) {
26886
+ setFailedLogs((prev) => prev.slice(1));
26732
26887
  return;
26733
26888
  }
26734
26889
  if (configOpen)
@@ -26755,16 +26910,16 @@ function App({ opts }) {
26755
26910
  const action = modal.type;
26756
26911
  setModal({ type: "none" });
26757
26912
  if (action === "confirm_remove") {
26758
- runSequentialActions("remove", rows, (r) => {
26913
+ startBatchActions("remove", rows, (r) => {
26759
26914
  const args = ["remove", r.branch, "--repo", r.repoName, "--yes"];
26760
26915
  if (r.dirtyFiles.length > 0)
26761
26916
  args.push("--force");
26762
26917
  return args;
26763
26918
  });
26764
26919
  } else if (action === "confirm_rebase") {
26765
- runSequentialActions("rebase", rows, (r) => ["rebase", r.branch, "--repo", r.repoName]);
26920
+ startBatchActions("rebase", rows, (r) => ["rebase", r.branch, "--repo", r.repoName]);
26766
26921
  } else {
26767
- runSequentialActions("sync", rows, (r) => ["sync", r.branch, "--repo", r.repoName]);
26922
+ startBatchActions("sync", rows, (r) => ["sync", r.branch, "--repo", r.repoName]);
26768
26923
  }
26769
26924
  return;
26770
26925
  }
@@ -26792,113 +26947,160 @@ function App({ opts }) {
26792
26947
  }
26793
26948
  return;
26794
26949
  }
26795
- if (key.name === "a") {
26796
- (async () => {
26797
- const targets = getSelectedRows();
26798
- if (targets.length === 0)
26799
- return;
26800
- const target = targets[0];
26801
- if (!target)
26802
- return;
26803
- const startedAt = Date.now();
26804
- try {
26805
- const config2 = await loadConfig();
26806
- const cmdTemplate = resolveAgentCommand("claude", config2.agents) ?? "claude";
26807
- const result = await spawnAgentInWorktree(cmdTemplate, target.path, { repoName: target.repoName, branch: target.branch });
26808
- appendHistory({
26809
- ts: new Date().toISOString(),
26810
- source: "terminal",
26811
- command: "agent",
26812
- args: ["agent", target.branch, "--repo", target.repoName],
26813
- durationMs: Date.now() - startedAt,
26814
- exit: 0
26815
- });
26816
- setActionMessage(`Agent spawned (${result.mode}${result.session ? ` session ${result.session}` : ""})`);
26817
- setTimeout(() => setActionMessage(undefined), 5000);
26818
- } catch (e) {
26819
- appendHistory({
26820
- ts: new Date().toISOString(),
26821
- source: "terminal",
26822
- command: "agent",
26823
- args: ["agent", target.branch, "--repo", target.repoName],
26824
- durationMs: Date.now() - startedAt,
26825
- exit: 1
26826
- });
26827
- setActionMessage(`Agent failed: ${e.message}`);
26828
- }
26829
- })();
26950
+ if (key.name === "down" || key.name === "j") {
26951
+ setSelectedIndex((prev) => Math.min(prev + 1, maxIndex));
26830
26952
  return;
26831
26953
  }
26832
- if (key.name === "f") {
26833
- const targets = getSelectedRows();
26834
- if (targets.length === 0)
26835
- return;
26836
- const repoNames = [...new Set(targets.map((t) => t.repoName))];
26837
- const label = repoNames.length === 1 ? repoNames[0] : `${repoNames.length} repos`;
26838
- startAction("fetch", `Fetch ${label}`, label, repoNames, undefined, ["fetch", "--repo", repoNames.join(",")]).then(() => setSelection(new Set));
26839
- return;
26840
- } else if (key.name === "down" || key.name === "j") {
26841
- setSelectedIndex((prev) => Math.min(prev + 1, maxIndex));
26842
- } else if (key.name === "up" || key.name === "k") {
26954
+ if (key.name === "up" || key.name === "k") {
26843
26955
  setSelectedIndex((prev) => Math.max(prev - 1, 0));
26844
- } else if (key.name === "r") {
26956
+ return;
26957
+ }
26958
+ if (key.name === "r" && !key.shift) {
26845
26959
  refresh();
26846
- } else if (key.name === "c") {
26960
+ return;
26961
+ }
26962
+ if (key.name === "c" && !key.ctrl) {
26847
26963
  setConfigOpen(true);
26848
- } else if (key.name === "?" || key.name === "H" || key.name === "h" && key.shift) {
26964
+ return;
26965
+ }
26966
+ if (key.name === "?" || key.name === "H" || key.name === "h" && key.shift) {
26849
26967
  if (key.name === "?") {
26850
26968
  setModal({ type: "help" });
26851
26969
  } else {
26852
26970
  setModal({ type: "history" });
26853
26971
  }
26854
- } else if (key.name === "n") {
26972
+ return;
26973
+ }
26974
+ if (key.name === "n") {
26855
26975
  if (!selectedRow)
26856
26976
  return;
26857
26977
  setCreateModal(true);
26858
26978
  setCreateError(undefined);
26859
- } else if (key.name === "o") {
26979
+ return;
26980
+ }
26981
+ if (key.name === "f") {
26982
+ const targets = getSelectedRows();
26983
+ if (targets.length > 0)
26984
+ startFetch(targets);
26985
+ return;
26986
+ }
26987
+ if (key.name === "o") {
26860
26988
  const targets = getSelectedRows();
26861
26989
  if (targets.length === 0)
26862
26990
  return;
26863
26991
  if (targets.length > 1) {
26864
- setActionMessage("Cannot open multiple worktrees");
26865
- setTimeout(() => setActionMessage(undefined), 3000);
26992
+ flash("Cannot open multiple worktrees");
26866
26993
  return;
26867
26994
  }
26868
26995
  const target = targets[0];
26869
26996
  if (!target)
26870
26997
  return;
26871
- startAction("open", `Open ${target.branch}`, target.branch, [target.repoName], target.path, ["open", target.branch, "--repo", target.repoName]);
26872
- } else if (key.name === "d" || key.name === "D" && key.shift) {
26998
+ const conflict = findConflict(targets);
26999
+ if (conflict) {
27000
+ flash(conflict);
27001
+ return;
27002
+ }
27003
+ const op = {
27004
+ id: nextOpId.current++,
27005
+ kind: "open",
27006
+ repoNames: [target.repoName],
27007
+ rowPath: target.path,
27008
+ label: target.branch,
27009
+ title: `Open ${target.branch}`,
27010
+ status: "queued",
27011
+ lines: []
27012
+ };
27013
+ setOps((prev) => [...prev, op]);
27014
+ executeOp(op, ["open", target.branch, "--repo", target.repoName], null);
27015
+ return;
27016
+ }
27017
+ if (key.name === "d" || key.name === "D" && key.shift) {
26873
27018
  const targets = getSelectedRows();
26874
27019
  if (targets.length === 0)
26875
27020
  return;
26876
27021
  if (targets.some((r) => r.isMainCheckout)) {
26877
- setActionMessage("Cannot remove main checkout");
26878
- setTimeout(() => setActionMessage(undefined), 3000);
27022
+ flash("Cannot remove main checkout");
27023
+ return;
27024
+ }
27025
+ const conflict = findConflict(targets);
27026
+ if (conflict) {
27027
+ flash(conflict);
26879
27028
  return;
26880
27029
  }
26881
27030
  setModal({ type: "confirm_remove", rows: targets });
26882
- } else if (key.name === "b" || key.name === "R" || key.name === "r" && key.shift) {
27031
+ return;
27032
+ }
27033
+ if (key.name === "b" || key.name === "R" || key.name === "r" && key.shift) {
26883
27034
  const targets = getSelectedRows();
26884
27035
  if (targets.length === 0)
26885
27036
  return;
26886
27037
  if (targets.some((r) => r.isMainCheckout)) {
26887
- setActionMessage("Cannot rebase main checkout");
26888
- setTimeout(() => setActionMessage(undefined), 3000);
27038
+ flash("Cannot rebase main checkout");
27039
+ return;
27040
+ }
27041
+ const conflict = findConflict(targets);
27042
+ if (conflict) {
27043
+ flash(conflict);
26889
27044
  return;
26890
27045
  }
26891
27046
  setModal({ type: "confirm_rebase", rows: targets });
26892
- } else if (key.name === "s") {
27047
+ return;
27048
+ }
27049
+ if (key.name === "s") {
26893
27050
  const targets = getSelectedRows();
26894
27051
  if (targets.length === 0)
26895
27052
  return;
26896
27053
  if (targets.some((r) => r.isMainCheckout)) {
26897
- setActionMessage("Cannot sync main checkout");
26898
- setTimeout(() => setActionMessage(undefined), 3000);
27054
+ flash("Cannot sync main checkout");
27055
+ return;
27056
+ }
27057
+ const conflict = findConflict(targets);
27058
+ if (conflict) {
27059
+ flash(conflict);
26899
27060
  return;
26900
27061
  }
26901
27062
  setModal({ type: "confirm_sync", rows: targets });
27063
+ return;
27064
+ }
27065
+ if (key.name === "a") {
27066
+ const targets = getSelectedRows();
27067
+ if (targets.length === 0)
27068
+ return;
27069
+ const target = targets[0];
27070
+ if (!target)
27071
+ return;
27072
+ const conflict = findConflict([target]);
27073
+ if (conflict) {
27074
+ flash(conflict);
27075
+ return;
27076
+ }
27077
+ (async () => {
27078
+ const startedAt = Date.now();
27079
+ try {
27080
+ const config2 = await loadConfig();
27081
+ const cmdTemplate = resolveAgentCommand("claude", config2.agents) ?? "claude";
27082
+ const result = await spawnAgentInWorktree(cmdTemplate, target.path, { repoName: target.repoName, branch: target.branch });
27083
+ appendHistory({
27084
+ ts: new Date().toISOString(),
27085
+ source: "terminal",
27086
+ command: "agent",
27087
+ args: ["agent", target.branch, "--repo", target.repoName],
27088
+ durationMs: Date.now() - startedAt,
27089
+ exit: 0
27090
+ });
27091
+ flash(`Agent spawned (${result.mode}${result.session ? ` session ${result.session}` : ""})`, 5000);
27092
+ } catch (e) {
27093
+ appendHistory({
27094
+ ts: new Date().toISOString(),
27095
+ source: "terminal",
27096
+ command: "agent",
27097
+ args: ["agent", target.branch, "--repo", target.repoName],
27098
+ durationMs: Date.now() - startedAt,
27099
+ exit: 1
27100
+ });
27101
+ flash(`Agent failed: ${e.message}`, 5000);
27102
+ }
27103
+ })();
26902
27104
  }
26903
27105
  });
26904
27106
  return /* @__PURE__ */ jsxs10("box", {
@@ -26912,10 +27114,12 @@ function App({ opts }) {
26912
27114
  flexGrow: 1,
26913
27115
  children: [
26914
27116
  /* @__PURE__ */ jsx11(WorktreeTable, {
26915
- blocks: filteredBlocks,
27117
+ blocks: displayBlocks,
26916
27118
  selectedIndex,
26917
27119
  selection,
26918
- busy: busy ? { repoNames: busy.repoNames, rowPath: busy.rowPath, verb: VERBS[busy.kind], frame: spinnerFrame } : undefined
27120
+ frame: spinnerFrame,
27121
+ repoVerbs,
27122
+ rowVerbs
26919
27123
  }),
26920
27124
  /* @__PURE__ */ jsx11(DetailPane, {
26921
27125
  selectedRow
@@ -26939,11 +27143,11 @@ function App({ opts }) {
26939
27143
  ]
26940
27144
  }),
26941
27145
  /* @__PURE__ */ jsx11(Footer, {
26942
- loading,
27146
+ loading: loading || refreshing,
26943
27147
  lastRefreshed,
26944
27148
  errorCount: warnings.length,
26945
27149
  message: actionMessage,
26946
- busyText: actionRun && !actionRun.done ? `${capitalize(VERBS[actionRun.kind])} ${actionRun.label}…` : undefined,
27150
+ busyText: latestOp ? `${capitalize(VERBS[latestOp.kind])} ${latestOp.label}…${runningOps.length > 1 ? ` (+${runningOps.length - 1})` : ""}` : undefined,
26947
27151
  spinnerFrame,
26948
27152
  filter: filterText ? { term: filterText, matches: flatRows.length, total: totalRows } : undefined
26949
27153
  }),
@@ -26979,7 +27183,7 @@ function App({ opts }) {
26979
27183
  message: `Are you sure you want to sync ${modal.rows.length === 1 ? modal.rows[0]?.branch : modal.rows.length + " worktrees"}?`
26980
27184
  }),
26981
27185
  createModal && /* @__PURE__ */ jsx11(InputModal, {
26982
- title: "New worktree branch",
27186
+ title: `New worktree branch in ${selectedRow?.repoName ?? ""}`,
26983
27187
  placeholder: "Branch name (empty to cancel)",
26984
27188
  errorMessage: createError,
26985
27189
  onSubmit: (value) => {
@@ -26992,10 +27196,16 @@ function App({ opts }) {
26992
27196
  setCreateError("Invalid branch name");
26993
27197
  return;
26994
27198
  }
26995
- setCreateModal(false);
26996
- if (selectedRow) {
26997
- startAction("create", `Create ${branch}`, `${branch} in ${selectedRow.repoName}`, [selectedRow.repoName], undefined, ["create", branch, "--repo", selectedRow.repoName]);
27199
+ const repoName = selectedRow?.repoName;
27200
+ if (!repoName)
27201
+ return;
27202
+ if (busyRepos.has(repoName)) {
27203
+ setCreateError(`${repoName} is busy`);
27204
+ return;
26998
27205
  }
27206
+ setCreateModal(false);
27207
+ setCreateError(undefined);
27208
+ startCreate(branch, repoName);
26999
27209
  }
27000
27210
  }),
27001
27211
  configOpen && /* @__PURE__ */ jsx11(ConfigOverlay, {
@@ -27003,11 +27213,12 @@ function App({ opts }) {
27003
27213
  onSaved: () => refresh(),
27004
27214
  onError: (msg) => setModal({ type: "error", message: msg })
27005
27215
  }),
27006
- actionRun?.done && /* @__PURE__ */ jsx11(ActionLogModal, {
27007
- title: actionRun.title,
27008
- lines: actionRun.lines,
27009
- done: actionRun.done,
27010
- exitCode: actionRun.exitCode
27216
+ failedLogs.length > 0 && /* @__PURE__ */ jsx11(ActionLogModal, {
27217
+ title: failedLogs[0].title,
27218
+ lines: failedLogs[0].lines,
27219
+ done: true,
27220
+ exitCode: failedLogs[0].exitCode,
27221
+ remaining: failedLogs.length - 1
27011
27222
  })
27012
27223
  ]
27013
27224
  });
@@ -31194,7 +31405,7 @@ function registerTerminalCommand(program2) {
31194
31405
  `);
31195
31406
  process.stderr.write(` To use the dashboard, run it via Bun:
31196
31407
  `);
31197
- process.stderr.write(` bunx wtx terminal
31408
+ process.stderr.write(` bunx --bun wtx terminal
31198
31409
 
31199
31410
  `);
31200
31411
  process.stderr.write(" Note: `wtx ls` provides a fast list view and works everywhere.\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogpoyraz/wtx",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Multi-repo git worktree manager",
5
5
  "type": "module",
6
6
  "bin": {