@ogpoyraz/wtx 0.8.0 → 0.8.2

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 +305 -82
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -169,7 +169,7 @@ Config lives at `~/.config/wtx/config.json`.
169
169
  | `agents.<name>.command` | – | Shell command template for `--agent`; `{wt}`, `{branch}`, `{repo}` expanded |
170
170
  | `repos.<name>.main_branch` | `"auto"` | Auto-detects via `git symbolic-ref` |
171
171
  | `repos.<name>.fetch_main_on_create` | `true` | Fetch before creating so branches start fresh |
172
- | `repos.<name>.sync_files` | `[]` | Files copied from main checkout on create and sync |
172
+ | `repos.<name>.sync_files` | `[]` | Files or folders copied from main checkout on create and sync — directories are copied recursively (`build/`) |
173
173
  | `repos.<name>.post_create` / `post_sync` | `[]` | Hook commands; failures fail the command with a rerun hint |
174
174
  | `repos.<name>.install_script` | `null` | Command run inside a worktree (or the main checkout via `wtx deps --install`) for dependency installs (`{wt}`, `{branch}`, `{main}` expanded); when unset, the detected package manager performs a real install |
175
175
  | `repos.<name>.deps.manager` | `"auto"` | Force a manager: `npm` `bun` `pnpm` `yarn` `go` `python` `cargo` |
package/dist/cli.mjs CHANGED
@@ -15331,6 +15331,11 @@ function verbose(message, isVerbose) {
15331
15331
  function indented(message) {
15332
15332
  console.log(` ${message}`);
15333
15333
  }
15334
+ function terminalLink(url2, text = url2, stream = process.stdout) {
15335
+ if (!stream.isTTY)
15336
+ return text;
15337
+ return `\x1B]8;;${url2}\x1B\\${text}\x1B]8;;\x1B\\`;
15338
+ }
15334
15339
  var c, quietMode = false;
15335
15340
  var init_log = __esm(() => {
15336
15341
  init_source();
@@ -25410,6 +25415,28 @@ function sortRowsHierarchically(rows) {
25410
25415
  function sortBlocks(blocks) {
25411
25416
  return [...blocks].sort((a2, b) => a2.repoName.localeCompare(b.repoName));
25412
25417
  }
25418
+ function wrapText(text, width) {
25419
+ const lines = [];
25420
+ for (const paragraph of text.split(`
25421
+ `)) {
25422
+ let current = "";
25423
+ for (const word of paragraph.split(/\s+/).filter(Boolean)) {
25424
+ if (!current) {
25425
+ current = word;
25426
+ } else if (current.length + 1 + word.length <= width) {
25427
+ current += ` ${word}`;
25428
+ } else {
25429
+ lines.push(current);
25430
+ current = word;
25431
+ }
25432
+ }
25433
+ lines.push(current);
25434
+ }
25435
+ return lines;
25436
+ }
25437
+ function isTapWithoutDrag(down, up) {
25438
+ return Math.abs(up.x - down.x) <= 1 && Math.abs(up.y - down.y) <= 1;
25439
+ }
25413
25440
  function mergeBlocks(prev, next, scope) {
25414
25441
  if (!scope)
25415
25442
  return sortBlocks(next);
@@ -25568,9 +25595,91 @@ var init_theme = __esm(() => {
25568
25595
  };
25569
25596
  });
25570
25597
 
25598
+ // src/tui/hooks/use-tap.ts
25599
+ import { useRef as useRef2, useCallback as useCallback2 } from "react";
25600
+ function useTapHandler(onTap) {
25601
+ const down = useRef2(null);
25602
+ const onMouseDown = useCallback2((e) => {
25603
+ down.current = { x: e.x, y: e.y };
25604
+ }, []);
25605
+ const onMouseUp = useCallback2((e) => {
25606
+ const start = down.current;
25607
+ down.current = null;
25608
+ if (!start || !isTapWithoutDrag(start, e))
25609
+ return;
25610
+ onTap();
25611
+ }, [onTap]);
25612
+ return { onMouseDown, onMouseUp };
25613
+ }
25614
+ var init_use_tap = __esm(() => {
25615
+ init_utils();
25616
+ });
25617
+
25618
+ // src/tui/platform.ts
25619
+ function clipboardCandidatesFor(platform2, env2 = process.env) {
25620
+ if (platform2 === "darwin")
25621
+ return [{ cmd: "pbcopy", args: [] }];
25622
+ if (platform2 === "win32")
25623
+ return [{ cmd: "clip", args: [] }];
25624
+ const candidates = [];
25625
+ if (env2.WAYLAND_DISPLAY)
25626
+ candidates.push({ cmd: "wl-copy", args: [] });
25627
+ if (env2.DISPLAY) {
25628
+ candidates.push({ cmd: "xclip", args: ["-selection", "clipboard"] });
25629
+ candidates.push({ cmd: "xsel", args: ["--clipboard", "--input"] });
25630
+ }
25631
+ return candidates;
25632
+ }
25633
+ async function copyTextToClipboard(renderer, text) {
25634
+ if (!text)
25635
+ return false;
25636
+ const viaTerminal = renderer.copyToClipboardOSC52(text);
25637
+ let viaSystem = false;
25638
+ for (const candidate of clipboardCandidatesFor(process.platform)) {
25639
+ try {
25640
+ const proc = Bun.spawn([candidate.cmd, ...candidate.args], {
25641
+ stdin: "pipe",
25642
+ stdout: "ignore",
25643
+ stderr: "ignore"
25644
+ });
25645
+ proc.stdin.write(text);
25646
+ proc.stdin.end();
25647
+ if (await proc.exited === 0) {
25648
+ viaSystem = true;
25649
+ break;
25650
+ }
25651
+ } catch {}
25652
+ }
25653
+ return viaSystem || viaTerminal;
25654
+ }
25655
+ function browserCommandFor(platform2, url2) {
25656
+ if (!/^https?:\/\//.test(url2))
25657
+ return null;
25658
+ if (platform2 === "darwin")
25659
+ return { cmd: "open", args: [url2] };
25660
+ if (platform2 === "win32")
25661
+ return { cmd: "cmd", args: ["/c", "start", "", url2] };
25662
+ return { cmd: "xdg-open", args: [url2] };
25663
+ }
25664
+ async function openInBrowser(url2) {
25665
+ const command = browserCommandFor(process.platform, url2);
25666
+ if (!command)
25667
+ return false;
25668
+ try {
25669
+ const proc = Bun.spawn([command.cmd, ...command.args], {
25670
+ stdin: "ignore",
25671
+ stdout: "ignore",
25672
+ stderr: "ignore"
25673
+ });
25674
+ return await proc.exited === 0;
25675
+ } catch {
25676
+ return false;
25677
+ }
25678
+ }
25679
+
25571
25680
  // src/tui/components/WorktreeTable.tsx
25572
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
25573
- import { jsx, jsxs } from "@opentui/react/jsx-runtime";
25681
+ import { useEffect as useEffect2, useRef as useRef3 } from "react";
25682
+ import { jsx, jsxs, Fragment } from "@opentui/react/jsx-runtime";
25574
25683
  function statusBadge(row) {
25575
25684
  if (row.isMainCheckout)
25576
25685
  return { text: "[main]", fg: tokens.accent };
@@ -25585,15 +25694,14 @@ function statusBadge(row) {
25585
25694
  return { text: "clean", fg: tokens.dim };
25586
25695
  }
25587
25696
  function WorktreeItem({ row, isSelected, isMultiSelected, indicator, frame, id }) {
25697
+ const prTap = useTapHandler(() => {
25698
+ if (row.prUrl)
25699
+ openInBrowser(row.prUrl);
25700
+ });
25588
25701
  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);
25589
25702
  const disabled = indicator !== undefined || row.isPendingCreate === true;
25590
25703
  const primary = isSelected ? tokens.bright : disabled ? tokens.dim : tokens.fg;
25591
25704
  const divergence = row.ahead !== null && row.behind !== null && (row.ahead > 0 || row.behind > 0) ? ` · ↑${row.ahead} ↓${row.behind}` : "";
25592
- const prSegment = row.prNumber ? [
25593
- `· #${row.prNumber}`,
25594
- row.prState ?? "",
25595
- row.prChecks ? `(${row.prChecks})` : ""
25596
- ].filter(Boolean).join(" ") : "";
25597
25705
  const ownerSegment = row.owner ? ` · by ${row.owner}` : "";
25598
25706
  const baseSegment = row.base ? ` · base ${row.base}` : "";
25599
25707
  const rebaseSegment = !row.isMainCheckout && row.rebaseStatus && !row.isPrunable ? ` · ${row.rebaseStatus}` : "";
@@ -25603,7 +25711,6 @@ function WorktreeItem({ row, isSelected, isMultiSelected, indicator, frame, id }
25603
25711
  const secondary = [
25604
25712
  `${secondaryIndent}${row.commitShort}`,
25605
25713
  divergence,
25606
- prSegment,
25607
25714
  ownerSegment,
25608
25715
  baseSegment
25609
25716
  ].filter(Boolean).join(" ").trimEnd();
@@ -25638,11 +25745,36 @@ function WorktreeItem({ row, isSelected, isMultiSelected, indicator, frame, id }
25638
25745
  ]
25639
25746
  }),
25640
25747
  /* @__PURE__ */ jsxs("text", {
25748
+ ...row.prUrl ? prTap : {},
25641
25749
  children: [
25642
25750
  /* @__PURE__ */ jsx("span", {
25643
25751
  fg: tokens.dim,
25644
25752
  children: secondary
25645
25753
  }),
25754
+ row.prNumber !== null && /* @__PURE__ */ jsxs(Fragment, {
25755
+ children: [
25756
+ /* @__PURE__ */ jsx("span", {
25757
+ fg: tokens.dim,
25758
+ children: secondary ? " · " : ""
25759
+ }),
25760
+ /* @__PURE__ */ jsx("span", {
25761
+ fg: tokens.accent,
25762
+ children: `#${row.prNumber}`
25763
+ }),
25764
+ row.prState && /* @__PURE__ */ jsx("span", {
25765
+ fg: tokens.dim,
25766
+ children: ` ${row.prState}`
25767
+ }),
25768
+ row.prChecks && /* @__PURE__ */ jsx("span", {
25769
+ fg: tokens.dim,
25770
+ children: ` (${row.prChecks})`
25771
+ }),
25772
+ row.prUrl && /* @__PURE__ */ jsx("span", {
25773
+ fg: tokens.dim,
25774
+ children: " ↗"
25775
+ })
25776
+ ]
25777
+ }),
25646
25778
  baseChangedSegment && /* @__PURE__ */ jsx("span", {
25647
25779
  fg: tokens.warning,
25648
25780
  children: baseChangedSegment
@@ -25657,7 +25789,7 @@ function WorktreeItem({ row, isSelected, isMultiSelected, indicator, frame, id }
25657
25789
  });
25658
25790
  }
25659
25791
  function WorktreeTable({ blocks, selectedIndex, selection = new Set, frame, repoVerbs, rowVerbs }) {
25660
- const scrollRef = useRef2(null);
25792
+ const scrollRef = useRef3(null);
25661
25793
  useEffect2(() => {
25662
25794
  if (scrollRef.current?.scrollChildIntoView) {
25663
25795
  scrollRef.current.scrollChildIntoView("selected-row");
@@ -25727,11 +25859,16 @@ function WorktreeTable({ blocks, selectedIndex, selection = new Set, frame, repo
25727
25859
  var SECONDARY_INDENT = " ";
25728
25860
  var init_WorktreeTable = __esm(() => {
25729
25861
  init_theme();
25862
+ init_use_tap();
25730
25863
  });
25731
25864
 
25732
25865
  // src/tui/components/DetailPane.tsx
25733
- import { jsx as jsx2, jsxs as jsxs2, Fragment } from "@opentui/react/jsx-runtime";
25866
+ import { jsx as jsx2, jsxs as jsxs2, Fragment as Fragment2 } from "@opentui/react/jsx-runtime";
25734
25867
  function DetailPane({ selectedRow }) {
25868
+ const prTap = useTapHandler(() => {
25869
+ if (selectedRow?.prUrl)
25870
+ openInBrowser(selectedRow.prUrl);
25871
+ });
25735
25872
  if (!selectedRow) {
25736
25873
  return /* @__PURE__ */ jsx2("box", {
25737
25874
  id: "detail-pane",
@@ -25946,7 +26083,7 @@ function DetailPane({ selectedRow }) {
25946
26083
  })
25947
26084
  ]
25948
26085
  }),
25949
- prNumber !== null && /* @__PURE__ */ jsxs2(Fragment, {
26086
+ prNumber !== null && /* @__PURE__ */ jsxs2(Fragment2, {
25950
26087
  children: [
25951
26088
  /* @__PURE__ */ jsxs2("text", {
25952
26089
  style: { marginTop: 1 },
@@ -25983,20 +26120,25 @@ function DetailPane({ selectedRow }) {
25983
26120
  })
25984
26121
  ]
25985
26122
  }),
25986
- prUrl && /* @__PURE__ */ jsxs2("text", {
25987
- children: [
25988
- /* @__PURE__ */ jsx2("span", {
25989
- fg: tokens.dim,
25990
- children: "URL:"
25991
- }),
25992
- /* @__PURE__ */ jsxs2("span", {
25993
- fg: tokens.fg,
25994
- children: [
25995
- " ",
25996
- prUrl
25997
- ]
25998
- })
25999
- ]
26123
+ prUrl && /* @__PURE__ */ jsx2("box", {
26124
+ ...prTap,
26125
+ children: /* @__PURE__ */ jsxs2("text", {
26126
+ selectable: false,
26127
+ children: [
26128
+ /* @__PURE__ */ jsx2("span", {
26129
+ fg: tokens.dim,
26130
+ children: "URL:"
26131
+ }),
26132
+ /* @__PURE__ */ jsx2("span", {
26133
+ fg: tokens.accent,
26134
+ children: ` ${prUrl}`
26135
+ }),
26136
+ /* @__PURE__ */ jsx2("span", {
26137
+ fg: tokens.dim,
26138
+ children: " ↗ click"
26139
+ })
26140
+ ]
26141
+ })
26000
26142
  })
26001
26143
  ]
26002
26144
  }),
@@ -26037,7 +26179,7 @@ function DetailPane({ selectedRow }) {
26037
26179
  fg: tokens.dim,
26038
26180
  children: "Actions:"
26039
26181
  }),
26040
- !isMainCheckout && /* @__PURE__ */ jsxs2(Fragment, {
26182
+ !isMainCheckout && /* @__PURE__ */ jsxs2(Fragment2, {
26041
26183
  children: [
26042
26184
  /* @__PURE__ */ jsxs2("text", {
26043
26185
  fg: tokens.dim,
@@ -26068,6 +26210,7 @@ function DetailPane({ selectedRow }) {
26068
26210
  }
26069
26211
  var init_DetailPane = __esm(() => {
26070
26212
  init_theme();
26213
+ init_use_tap();
26071
26214
  });
26072
26215
 
26073
26216
  // src/tui/components/Footer.tsx
@@ -26114,11 +26257,15 @@ function Footer({ loading, lastRefreshed, errorCount, message, busyText, spinner
26114
26257
  ]
26115
26258
  }) : null,
26116
26259
  errorCount > 0 ? /* @__PURE__ */ jsxs3("text", {
26117
- fg: tokens.error,
26118
26260
  children: [
26119
- errorCount,
26120
- " error",
26121
- errorCount !== 1 ? "s" : ""
26261
+ /* @__PURE__ */ jsx3("span", {
26262
+ fg: tokens.error,
26263
+ children: `${errorCount} error${errorCount !== 1 ? "s" : ""}`
26264
+ }),
26265
+ /* @__PURE__ */ jsx3("span", {
26266
+ fg: tokens.dim,
26267
+ children: " · e view"
26268
+ })
26122
26269
  ]
26123
26270
  }) : null,
26124
26271
  /* @__PURE__ */ jsx3("text", {
@@ -26238,8 +26385,12 @@ var init_HelpOverlay = __esm(() => {
26238
26385
  ["o", "Open selected in IDE"],
26239
26386
  ["a", "Spawn agent in selected"],
26240
26387
  ["d", "Remove selected worktree(s)"],
26388
+ ["e", "View data warnings (when count > 0)"],
26241
26389
  ["r", "Refresh data"],
26242
26390
  ["H", "Action history"],
26391
+ ["mouse drag", "Select text — copied to clipboard automatically"],
26392
+ ["ctrl+shift+c", "Copy selection again (terminals reserve cmd+c)"],
26393
+ ["click PR #/URL", "Open pull request in browser"],
26243
26394
  ["?", "Toggle help"],
26244
26395
  ["q/esc", "Quit"]
26245
26396
  ];
@@ -26588,8 +26739,8 @@ var init_HistoryOverlay = __esm(() => {
26588
26739
  // src/tui/components/InputModal.tsx
26589
26740
  import { useState as useState3 } from "react";
26590
26741
  import { jsx as jsx9, jsxs as jsxs8 } from "@opentui/react/jsx-runtime";
26591
- function InputModal({ title, placeholder, errorMessage, onSubmit }) {
26592
- const [value, setValue] = useState3("");
26742
+ function InputModal({ title, placeholder, initialValue, errorMessage, onSubmit }) {
26743
+ const [value, setValue] = useState3(initialValue ?? "");
26593
26744
  return /* @__PURE__ */ jsx9(Overlay, {
26594
26745
  title,
26595
26746
  borderColor: tokens.accent,
@@ -26598,6 +26749,7 @@ function InputModal({ title, placeholder, errorMessage, onSubmit }) {
26598
26749
  children: [
26599
26750
  /* @__PURE__ */ jsx9("input", {
26600
26751
  focused: true,
26752
+ value: initialValue ?? "",
26601
26753
  placeholder,
26602
26754
  onInput: (val) => setValue(val),
26603
26755
  onSubmit: () => onSubmit(value)
@@ -26695,7 +26847,7 @@ var init_ChoiceModal = __esm(() => {
26695
26847
  // src/tui/components/ConfigOverlay.tsx
26696
26848
  import { useState as useState5, useEffect as useEffect4 } from "react";
26697
26849
  import { useKeyboard as useKeyboard2 } from "@opentui/react";
26698
- import { jsx as jsx11, jsxs as jsxs10, Fragment as Fragment2 } from "@opentui/react/jsx-runtime";
26850
+ import { jsx as jsx11, jsxs as jsxs10, Fragment as Fragment3 } from "@opentui/react/jsx-runtime";
26699
26851
  function ConfigOverlay({ onClose, onSaved, onError }) {
26700
26852
  const [config2, setConfig] = useState5(null);
26701
26853
  const [viewState, setViewState] = useState5({ type: "main" });
@@ -26958,7 +27110,7 @@ function ConfigOverlay({ onClose, onSaved, onError }) {
26958
27110
  hint = "Remove this repository from config";
26959
27111
  }
26960
27112
  }
26961
- return /* @__PURE__ */ jsxs10(Fragment2, {
27113
+ return /* @__PURE__ */ jsxs10(Fragment3, {
26962
27114
  children: [
26963
27115
  /* @__PURE__ */ jsx11(Overlay, {
26964
27116
  title: "Configuration",
@@ -27045,7 +27197,8 @@ function ConfigOverlay({ onClose, onSaved, onError }) {
27045
27197
  }),
27046
27198
  editState.type === "input" && /* @__PURE__ */ jsx11(InputModal, {
27047
27199
  title: editState.title,
27048
- placeholder: editState.currentValue || "Enter value...",
27200
+ initialValue: editState.currentValue,
27201
+ placeholder: "Enter value...",
27049
27202
  errorMessage: editState.error,
27050
27203
  onSubmit: handleInputSubmit
27051
27204
  }),
@@ -27065,6 +27218,47 @@ var init_ConfigOverlay = __esm(() => {
27065
27218
  init_ConfirmModal();
27066
27219
  });
27067
27220
 
27221
+ // src/tui/components/WarningsOverlay.tsx
27222
+ import { jsx as jsx12, jsxs as jsxs11 } from "@opentui/react/jsx-runtime";
27223
+ function WarningsOverlay({ warnings }) {
27224
+ return /* @__PURE__ */ jsxs11(Overlay, {
27225
+ title: `Warnings (${warnings.length})`,
27226
+ borderColor: tokens.warning,
27227
+ children: [
27228
+ /* @__PURE__ */ jsx12("box", {
27229
+ flexDirection: "column",
27230
+ style: { maxHeight: 30 },
27231
+ children: /* @__PURE__ */ jsx12("scrollbox", {
27232
+ flexGrow: 1,
27233
+ children: warnings.map((warning, i2) => /* @__PURE__ */ jsxs11("box", {
27234
+ flexDirection: "column",
27235
+ style: { marginBottom: 1 },
27236
+ children: [
27237
+ /* @__PURE__ */ jsx12("text", {
27238
+ fg: tokens.warning,
27239
+ children: `⚠ ${warning.repoName}`
27240
+ }),
27241
+ wrapText(warning.message, 56).map((line, j) => /* @__PURE__ */ jsx12("text", {
27242
+ fg: tokens.fg,
27243
+ children: ` ${line}`
27244
+ }, j))
27245
+ ]
27246
+ }, i2))
27247
+ })
27248
+ }),
27249
+ /* @__PURE__ */ jsx12("text", {
27250
+ style: { marginTop: 1, fg: tokens.dim },
27251
+ children: "Press any key to close"
27252
+ })
27253
+ ]
27254
+ });
27255
+ }
27256
+ var init_WarningsOverlay = __esm(() => {
27257
+ init_Overlay();
27258
+ init_theme();
27259
+ init_utils();
27260
+ });
27261
+
27068
27262
  // src/tui/hooks/useSpinnerFrame.ts
27069
27263
  import { useEffect as useEffect5, useState as useState6 } from "react";
27070
27264
  function useSpinnerFrame(active) {
@@ -27083,9 +27277,9 @@ var init_useSpinnerFrame = __esm(() => {
27083
27277
  });
27084
27278
 
27085
27279
  // src/tui/components/App.tsx
27086
- import { useState as useState7, useEffect as useEffect6, useMemo, useRef as useRef3, useCallback as useCallback2 } from "react";
27087
- import { useKeyboard as useKeyboard3, useRenderer } from "@opentui/react";
27088
- import { jsx as jsx12, jsxs as jsxs11 } from "@opentui/react/jsx-runtime";
27280
+ import { useState as useState7, useEffect as useEffect6, useMemo, useRef as useRef4, useCallback as useCallback3 } from "react";
27281
+ import { useKeyboard as useKeyboard3, useRenderer, useSelectionHandler } from "@opentui/react";
27282
+ import { jsx as jsx13, jsxs as jsxs12 } from "@opentui/react/jsx-runtime";
27089
27283
  function getWorktreePathFor(repoName, branch) {
27090
27284
  try {
27091
27285
  const config2 = loadConfig();
@@ -27101,10 +27295,10 @@ function App({ opts }) {
27101
27295
  const [selectedIndex, setSelectedIndex] = useState7(0);
27102
27296
  const [modal, setModal] = useState7({ type: "none" });
27103
27297
  const [actionMessage, setActionMessage] = useState7();
27104
- const messageTimer = useRef3(null);
27298
+ const messageTimer = useRef4(null);
27105
27299
  const [ops, setOps] = useState7([]);
27106
27300
  const [failedLogs, setFailedLogs] = useState7([]);
27107
- const nextOpId = useRef3(1);
27301
+ const nextOpId = useRef4(1);
27108
27302
  const [createModal, setCreateModal] = useState7(false);
27109
27303
  const [createError, setCreateError] = useState7();
27110
27304
  const [createBaseModal, setCreateBaseModal] = useState7(null);
@@ -27117,7 +27311,7 @@ function App({ opts }) {
27117
27311
  const [filterText, setFilterText] = useState7("");
27118
27312
  const [isFiltering, setIsFiltering] = useState7(false);
27119
27313
  const [selection, setSelection] = useState7(new Set);
27120
- const doRefresh = useCallback2(async (scope) => {
27314
+ const doRefresh = useCallback3(async (scope) => {
27121
27315
  const targets = scope ?? [
27122
27316
  ...new Set([...blocks.map((b) => b.repoName), ...pendingRepos])
27123
27317
  ];
@@ -27133,12 +27327,22 @@ function App({ opts }) {
27133
27327
  setModal({ type: "error", message: error52 });
27134
27328
  }
27135
27329
  }, [error52]);
27136
- const flash = useCallback2((message, ms = 3000) => {
27330
+ const flash = useCallback3((message, ms = 3000) => {
27137
27331
  if (messageTimer.current)
27138
27332
  clearTimeout(messageTimer.current);
27139
27333
  setActionMessage(message);
27140
27334
  messageTimer.current = setTimeout(() => setActionMessage(undefined), ms);
27141
27335
  }, []);
27336
+ const copySelectedText = useCallback3((warnOnEmpty) => {
27337
+ const text = renderer.getSelection()?.getSelectedText() ?? "";
27338
+ if (!text) {
27339
+ if (warnOnEmpty)
27340
+ flash("Nothing selected to copy");
27341
+ return;
27342
+ }
27343
+ copyTextToClipboard(renderer, text).then((ok) => flash(ok ? `Copied ${text.length} character${text.length !== 1 ? "s" : ""}` : "Copy failed"));
27344
+ }, [renderer, flash]);
27345
+ useSelectionHandler(() => copySelectedText(false));
27142
27346
  const busyRepos = useMemo(() => new Set(ops.flatMap((o2) => o2.repoNames)), [ops]);
27143
27347
  const busyRowPaths = useMemo(() => new Set(ops.map((o2) => o2.rowPath).filter((p) => p !== undefined)), [ops]);
27144
27348
  const anyRunning = ops.some((o2) => o2.status === "running");
@@ -27324,7 +27528,7 @@ function App({ opts }) {
27324
27528
  return;
27325
27529
  }
27326
27530
  if (modal.type !== "none") {
27327
- if (modal.type === "error" || modal.type === "help" || modal.type === "history") {
27531
+ if (modal.type === "error" || modal.type === "help" || modal.type === "history" || modal.type === "warnings") {
27328
27532
  setModal({ type: "none" });
27329
27533
  if (modal.type === "error" && error52) {
27330
27534
  renderer.destroy();
@@ -27372,6 +27576,10 @@ function App({ opts }) {
27372
27576
  return;
27373
27577
  }
27374
27578
  }
27579
+ if ((key.super || key.meta || key.ctrl && key.shift) && key.name === "c") {
27580
+ copySelectedText(true);
27581
+ return;
27582
+ }
27375
27583
  if (key.name === "q" || key.name === "escape" || key.name === "c" && key.ctrl) {
27376
27584
  if (key.name === "escape" && selection.size > 0) {
27377
27585
  setSelection(new Set);
@@ -27414,6 +27622,10 @@ function App({ opts }) {
27414
27622
  }
27415
27623
  return;
27416
27624
  }
27625
+ if (key.name === "e" && warnings.length > 0) {
27626
+ setModal({ type: "warnings" });
27627
+ return;
27628
+ }
27417
27629
  if (key.name === "n") {
27418
27630
  if (!selectedRow)
27419
27631
  return;
@@ -27591,17 +27803,17 @@ function App({ opts }) {
27591
27803
  })();
27592
27804
  }
27593
27805
  });
27594
- return /* @__PURE__ */ jsxs11("box", {
27806
+ return /* @__PURE__ */ jsxs12("box", {
27595
27807
  flexDirection: "column",
27596
27808
  width: "100%",
27597
27809
  height: "100%",
27598
27810
  children: [
27599
- /* @__PURE__ */ jsxs11("box", {
27811
+ /* @__PURE__ */ jsxs12("box", {
27600
27812
  flexDirection: "row",
27601
27813
  width: "100%",
27602
27814
  flexGrow: 1,
27603
27815
  children: [
27604
- /* @__PURE__ */ jsx12(WorktreeTable, {
27816
+ /* @__PURE__ */ jsx13(WorktreeTable, {
27605
27817
  blocks: displayBlocks,
27606
27818
  selectedIndex,
27607
27819
  selection,
@@ -27609,28 +27821,28 @@ function App({ opts }) {
27609
27821
  repoVerbs,
27610
27822
  rowVerbs
27611
27823
  }),
27612
- /* @__PURE__ */ jsx12(DetailPane, {
27824
+ /* @__PURE__ */ jsx13(DetailPane, {
27613
27825
  selectedRow
27614
27826
  })
27615
27827
  ]
27616
27828
  }),
27617
- isFiltering && /* @__PURE__ */ jsxs11("box", {
27829
+ isFiltering && /* @__PURE__ */ jsxs12("box", {
27618
27830
  flexDirection: "row",
27619
27831
  paddingX: 1,
27620
27832
  border: true,
27621
27833
  borderColor: "magenta",
27622
27834
  children: [
27623
- /* @__PURE__ */ jsx12("text", {
27835
+ /* @__PURE__ */ jsx13("text", {
27624
27836
  children: "filter: "
27625
27837
  }),
27626
- /* @__PURE__ */ jsx12("input", {
27838
+ /* @__PURE__ */ jsx13("input", {
27627
27839
  focused: true,
27628
27840
  placeholder: "Type to filter...",
27629
27841
  onInput: (v) => setFilterText(v)
27630
27842
  })
27631
27843
  ]
27632
27844
  }),
27633
- /* @__PURE__ */ jsx12(Footer, {
27845
+ /* @__PURE__ */ jsx13(Footer, {
27634
27846
  loading: loading || refreshing,
27635
27847
  lastRefreshed,
27636
27848
  errorCount: warnings.length,
@@ -27639,13 +27851,16 @@ function App({ opts }) {
27639
27851
  spinnerFrame,
27640
27852
  filter: filterText ? { term: filterText, matches: flatRows.length, total: totalRows } : undefined
27641
27853
  }),
27642
- modal.type === "help" && /* @__PURE__ */ jsx12(HelpOverlay, {}),
27643
- modal.type === "history" && /* @__PURE__ */ jsx12(HistoryOverlay, {}),
27644
- modal.type === "error" && /* @__PURE__ */ jsx12(ConfirmModal, {
27854
+ modal.type === "help" && /* @__PURE__ */ jsx13(HelpOverlay, {}),
27855
+ modal.type === "history" && /* @__PURE__ */ jsx13(HistoryOverlay, {}),
27856
+ modal.type === "warnings" && /* @__PURE__ */ jsx13(WarningsOverlay, {
27857
+ warnings
27858
+ }),
27859
+ modal.type === "error" && /* @__PURE__ */ jsx13(ConfirmModal, {
27645
27860
  title: "Error",
27646
27861
  message: modal.message
27647
27862
  }),
27648
- modal.type === "confirm_remove" && /* @__PURE__ */ jsx12(ConfirmModal, {
27863
+ modal.type === "confirm_remove" && /* @__PURE__ */ jsx13(ConfirmModal, {
27649
27864
  title: `Remove ${modal.rows.length} Worktree(s)`,
27650
27865
  message: (() => {
27651
27866
  const count2 = modal.rows.length;
@@ -27662,15 +27877,15 @@ function App({ opts }) {
27662
27877
  `);
27663
27878
  })()
27664
27879
  }),
27665
- modal.type === "confirm_rebase" && /* @__PURE__ */ jsx12(ConfirmModal, {
27880
+ modal.type === "confirm_rebase" && /* @__PURE__ */ jsx13(ConfirmModal, {
27666
27881
  title: `Rebase ${modal.rows.length} Worktree(s)`,
27667
27882
  message: `Are you sure you want to fetch and rebase ${modal.rows.length === 1 ? modal.rows[0]?.branch : modal.rows.length + " worktrees"}?`
27668
27883
  }),
27669
- modal.type === "confirm_sync" && /* @__PURE__ */ jsx12(ConfirmModal, {
27884
+ modal.type === "confirm_sync" && /* @__PURE__ */ jsx13(ConfirmModal, {
27670
27885
  title: `Sync ${modal.rows.length} Worktree(s)`,
27671
27886
  message: `Are you sure you want to sync ${modal.rows.length === 1 ? modal.rows[0]?.branch : modal.rows.length + " worktrees"}?`
27672
27887
  }),
27673
- createModal && /* @__PURE__ */ jsx12(InputModal, {
27888
+ createModal && /* @__PURE__ */ jsx13(InputModal, {
27674
27889
  title: `New worktree branch in ${selectedRow?.repoName ?? ""}`,
27675
27890
  placeholder: "Branch name (empty to cancel)",
27676
27891
  errorMessage: createError,
@@ -27697,7 +27912,7 @@ function App({ opts }) {
27697
27912
  setCreateBaseModal({ branch, repoName });
27698
27913
  }
27699
27914
  }),
27700
- createBaseModal && /* @__PURE__ */ jsx12(InputModal, {
27915
+ createBaseModal && /* @__PURE__ */ jsx13(InputModal, {
27701
27916
  title: `Base ref for ${createBaseModal.branch}`,
27702
27917
  placeholder: "origin/main (empty for default main)",
27703
27918
  errorMessage: createBaseError,
@@ -27713,7 +27928,7 @@ function App({ opts }) {
27713
27928
  setCreateDepsChoice({ branch, repoName, base: base2 || undefined });
27714
27929
  }
27715
27930
  }),
27716
- createDepsChoice && /* @__PURE__ */ jsx12(ChoiceModal, {
27931
+ createDepsChoice && /* @__PURE__ */ jsx13(ChoiceModal, {
27717
27932
  title: `Dependencies for ${createDepsChoice.branch}`,
27718
27933
  options: DEPS_CHOICES,
27719
27934
  onSubmit: (choice) => {
@@ -27723,9 +27938,10 @@ function App({ opts }) {
27723
27938
  },
27724
27939
  onCancel: () => setCreateDepsChoice(null)
27725
27940
  }),
27726
- renameModal && selectedRow && /* @__PURE__ */ jsx12(InputModal, {
27941
+ renameModal && selectedRow && /* @__PURE__ */ jsx13(InputModal, {
27727
27942
  title: `Rename branch ${selectedRow.branch}`,
27728
- placeholder: `New branch name (${selectedRow.branch})`,
27943
+ initialValue: selectedRow.branch,
27944
+ placeholder: "New branch name",
27729
27945
  errorMessage: renameError,
27730
27946
  onSubmit: (value) => {
27731
27947
  const target = selectedRow;
@@ -27747,7 +27963,7 @@ function App({ opts }) {
27747
27963
  setModal({ type: "confirm_rename", row: target, to });
27748
27964
  }
27749
27965
  }),
27750
- modal.type === "confirm_rename" && /* @__PURE__ */ jsx12(ConfirmModal, {
27966
+ modal.type === "confirm_rename" && /* @__PURE__ */ jsx13(ConfirmModal, {
27751
27967
  title: "Rename Worktree",
27752
27968
  message: [
27753
27969
  `${modal.row.branch} → ${modal.to}`,
@@ -27760,12 +27976,12 @@ function App({ opts }) {
27760
27976
  ].join(`
27761
27977
  `)
27762
27978
  }),
27763
- configOpen && /* @__PURE__ */ jsx12(ConfigOverlay, {
27979
+ configOpen && /* @__PURE__ */ jsx13(ConfigOverlay, {
27764
27980
  onClose: () => setConfigOpen(false),
27765
27981
  onSaved: () => void doRefresh(),
27766
27982
  onError: (msg) => setModal({ type: "error", message: msg })
27767
27983
  }),
27768
- failedLogs.length > 0 && /* @__PURE__ */ jsx12(ActionLogModal, {
27984
+ failedLogs.length > 0 && /* @__PURE__ */ jsx13(ActionLogModal, {
27769
27985
  title: failedLogs[0].title,
27770
27986
  lines: failedLogs[0].lines,
27771
27987
  done: true,
@@ -27789,6 +28005,7 @@ var init_App = __esm(() => {
27789
28005
  init_InputModal();
27790
28006
  init_ChoiceModal();
27791
28007
  init_ConfigOverlay();
28008
+ init_WarningsOverlay();
27792
28009
  init_utils();
27793
28010
  init_agents();
27794
28011
  init_config();
@@ -27819,14 +28036,14 @@ __export(exports_tui, {
27819
28036
  });
27820
28037
  import { createCliRenderer, TextTableRenderable } from "@opentui/core";
27821
28038
  import { createRoot, extend as extend2 } from "@opentui/react";
27822
- import { jsx as jsx13 } from "@opentui/react/jsx-runtime";
28039
+ import { jsx as jsx14 } from "@opentui/react/jsx-runtime";
27823
28040
  async function runTerminal(opts) {
27824
28041
  const renderer = await createCliRenderer({
27825
28042
  exitOnCtrlC: false
27826
28043
  });
27827
28044
  const root = createRoot(renderer);
27828
28045
  try {
27829
- root.render(/* @__PURE__ */ jsx13(App, {
28046
+ root.render(/* @__PURE__ */ jsx14(App, {
27830
28047
  opts
27831
28048
  }));
27832
28049
  } catch (err) {
@@ -30107,20 +30324,29 @@ async function getWorktreePort(repoName, branch, config2, currentWtPath) {
30107
30324
  }
30108
30325
 
30109
30326
  // src/lib/worktree-setup.ts
30327
+ function syncEntry(mainPath, wtPath, entry) {
30328
+ const src = path17.join(mainPath, entry);
30329
+ const dest = path17.join(wtPath, entry);
30330
+ if (!fs10.existsSync(src))
30331
+ return false;
30332
+ fs10.mkdirSync(path17.dirname(dest), { recursive: true });
30333
+ if (fs10.statSync(src).isDirectory()) {
30334
+ fs10.cpSync(src, dest, { recursive: true });
30335
+ } else {
30336
+ fs10.copyFileSync(src, dest);
30337
+ }
30338
+ return true;
30339
+ }
30110
30340
  async function runPostCreateSetup(params) {
30111
30341
  const { config: config2, repo, wtPath, branch, globalOpts } = params;
30112
30342
  let copiedFiles = [];
30113
30343
  let hooks = [];
30114
30344
  if (repo.config.sync_files && repo.config.sync_files.length > 0) {
30115
30345
  for (const file2 of repo.config.sync_files) {
30116
- const src = path17.join(repo.mainPath, file2);
30117
- const dest = path17.join(wtPath, file2);
30118
- if (fs10.existsSync(src)) {
30119
- if (!globalOpts.dryRun) {
30120
- fs10.mkdirSync(path17.dirname(dest), { recursive: true });
30121
- fs10.copyFileSync(src, dest);
30122
- copiedFiles.push(file2);
30123
- }
30346
+ if (!globalOpts.dryRun && syncEntry(repo.mainPath, wtPath, file2)) {
30347
+ copiedFiles.push(file2);
30348
+ }
30349
+ if (fs10.existsSync(path17.join(repo.mainPath, file2))) {
30124
30350
  stepSuccess(`Synced ${file2}`);
30125
30351
  } else {
30126
30352
  stepWarning(`Could not sync ${file2}`, "file not found in main checkout");
@@ -31233,7 +31459,7 @@ function registerLsCommand(program2) {
31233
31459
  const prInfo = prMap?.get(branch);
31234
31460
  if (prInfo) {
31235
31461
  const display = derivePrDisplay(prInfo);
31236
- prSegment = ` #${prInfo.number} ${renderDisplayState(display)} ${source_default.dim(prInfo.url)}`;
31462
+ prSegment = ` #${prInfo.number} ${renderDisplayState(display)} ${source_default.dim(terminalLink(prInfo.url))}`;
31237
31463
  }
31238
31464
  let ownerSuffix = "";
31239
31465
  const baseRef = wt.branch ? stackMetadata.branches[wt.branch]?.baseRef ?? prInfo?.baseRefName : prInfo?.baseRefName;
@@ -31501,12 +31727,9 @@ function registerSyncCommand(program2) {
31501
31727
  try {
31502
31728
  if (repo.config.sync_files) {
31503
31729
  for (const file2 of repo.config.sync_files) {
31504
- const src = path32.join(repo.mainPath, file2);
31505
- const dest = path32.join(wtPath, file2);
31506
- if (fs25.existsSync(src)) {
31730
+ if (fs25.existsSync(path32.join(repo.mainPath, file2))) {
31507
31731
  if (!opts.dryRun) {
31508
- fs25.mkdirSync(path32.dirname(dest), { recursive: true });
31509
- fs25.copyFileSync(src, dest);
31732
+ syncEntry(repo.mainPath, wtPath, file2);
31510
31733
  }
31511
31734
  stepSuccess(`Synced ${file2}`);
31512
31735
  }
@@ -32163,7 +32386,7 @@ function renderTable(rows) {
32163
32386
  const detailSuffix = details ? ` ${details}` : "";
32164
32387
  const baseSuffix = row.baseRef ? ` → ${row.baseRef}` : "";
32165
32388
  const authorTag = row.ownership && !row.ownership.mine && row.ownership.author ? ` ${source_default.dim(row.ownership.author)}` : "";
32166
- info(` #${row.prNumber} ${paddedBranch} ${renderDisplayState(row.prDisplay)}${baseSuffix}${detailSuffix}${authorTag} ${formatRelativeTime(row.updatedAt)} ${source_default.dim(row.url)}`);
32389
+ info(` #${row.prNumber} ${paddedBranch} ${renderDisplayState(row.prDisplay)}${baseSuffix}${detailSuffix}${authorTag} ${formatRelativeTime(row.updatedAt)} ${source_default.dim(terminalLink(row.url))}`);
32167
32390
  }
32168
32391
  }
32169
32392
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogpoyraz/wtx",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Multi-repo git worktree manager",
5
5
  "type": "module",
6
6
  "bin": {