@ohzw/worktree-command-tui 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ It keeps one active runtime session per namespace, lets you switch worktrees wit
6
6
  ## Features
7
7
 
8
8
  - Discover worktrees from the current repository even when launched from a subdirectory
9
- - Start or switch the active worktree session with `Enter`
9
+ - Start, switch, or restart the active worktree session with `Enter`
10
10
  - Stop the active session and clean up recorded orphan processes with `s`
11
11
  - Run an optional per-worktree setup command with `i`
12
12
  - Open the selected worktree in your editor with `e`
@@ -65,7 +65,7 @@ If config is missing, the CLI exits with a message telling you to run `wctui ini
65
65
  Primary shortcuts in the footer:
66
66
 
67
67
  - `↑↓` / `j` `k` — move selection
68
- - `Enter` — start or switch to selected worktree
68
+ - `Enter` — start, switch to, or restart the selected worktree
69
69
  - `i` — run `setupCommand` when configured
70
70
  - `e` — open the selected worktree in the configured editor when `editorCommand` is configured
71
71
  - `o` — open the selected worktree's pull request when GitHub metadata is available
@@ -89,7 +89,7 @@ Additional shortcuts from the help window:
89
89
 
90
90
  `wctui` executes the argv commands stored in `.worktree-command-tui.jsonc` when you press the matching keys. Treat repository config as trusted code:
91
91
 
92
- - `Enter` starts `command` in the selected worktree.
92
+ - `Enter` starts `command` in the selected worktree; pressing it on the active worktree restarts that session.
93
93
  - `i` runs `setupCommand`; package-manager install commands may run dependency lifecycle scripts.
94
94
  - `e` runs `editorCommand` with the selected worktree path appended.
95
95
 
package/dist/app.js CHANGED
@@ -64,7 +64,7 @@ function getLogPaneHeight(_rootHeight) {
64
64
  return 9;
65
65
  }
66
66
  const ACTIVE_TAG = 'active';
67
- const ALREADY_ACTIVE_MESSAGE = 'already active';
67
+ const REPEATED_ENTER_DEBOUNCE_MS = 750;
68
68
  function syncActiveTags(rows, activePath) {
69
69
  let changed = false;
70
70
  const nextRows = rows.map(row => {
@@ -82,13 +82,7 @@ function syncActiveTags(rows, activePath) {
82
82
  return changed ? nextRows : rows;
83
83
  }
84
84
  function shouldRefreshLogs(model) {
85
- if (model.activePath === null) {
86
- return false;
87
- }
88
- if (model.status.kind === 'running' || model.status.kind === 'error') {
89
- return true;
90
- }
91
- return model.status.message === ALREADY_ACTIVE_MESSAGE;
85
+ return model.activePath !== null && (model.status.kind === 'running' || model.status.kind === 'error');
92
86
  }
93
87
  function getStatusAfterLogRefresh(current, refresh) {
94
88
  if (current.status.kind !== 'running') {
@@ -124,6 +118,7 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
124
118
  const logRefreshInFlightRef = useRef(false);
125
119
  const actionGenerationRef = useRef(0);
126
120
  const previousStatusRef = useRef(initialModel.status.kind);
121
+ const lastStartRequestRef = useRef({ path: null, atMs: 0 });
127
122
  const alertTimeoutRef = useRef(null);
128
123
  useEffect(() => {
129
124
  setSelectedPath(currentPath => getNextSelectedPath(model.rows, currentPath));
@@ -134,7 +129,8 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
134
129
  useEffect(() => {
135
130
  const becameRunning = previousStatusRef.current === 'starting' && model.status.kind === 'running';
136
131
  if (becameRunning) {
137
- setCompletedAlert(model.activeBranch ? `Switched to ${model.activeBranch}` : 'Worktree switch complete.');
132
+ const switchedAlert = model.activeBranch ? `Switched to ${model.activeBranch}` : 'Worktree switch complete.';
133
+ setCompletedAlert(model.status.message.startsWith('restarted ') && model.activeBranch ? `Restarted ${model.activeBranch}` : switchedAlert);
138
134
  if (alertTimeoutRef.current !== null) {
139
135
  clearTimeout(alertTimeoutRef.current);
140
136
  }
@@ -143,7 +139,7 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
143
139
  }, 2500);
144
140
  }
145
141
  previousStatusRef.current = model.status.kind;
146
- }, [model.status.kind, model.activeBranch]);
142
+ }, [model.status.kind, model.status.message, model.activeBranch]);
147
143
  useEffect(() => {
148
144
  return () => {
149
145
  if (alertTimeoutRef.current !== null) {
@@ -256,6 +252,16 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
256
252
  userActionInFlightRef.current = false;
257
253
  }
258
254
  }
255
+ function shouldAcceptStartRequest(path) {
256
+ const nowMs = Date.now();
257
+ const last = lastStartRequestRef.current;
258
+ if (last.path === path && nowMs - last.atMs < REPEATED_ENTER_DEBOUNCE_MS) {
259
+ return false;
260
+ }
261
+ last.path = path;
262
+ last.atMs = nowMs;
263
+ return true;
264
+ }
259
265
  useInput((input, key) => {
260
266
  if (isHelpOverlayOpen) {
261
267
  if (key.escape || input === '\u001B' || input === 'q' || input === '?') {
@@ -372,6 +378,9 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
372
378
  clearTransientAlert();
373
379
  return;
374
380
  }
381
+ if (!shouldAcceptStartRequest(decision.path)) {
382
+ return;
383
+ }
375
384
  setModel(current => ({ ...current, status: decision.status }));
376
385
  clearTransientAlert();
377
386
  void apply(() => actions.start(decision.path));
@@ -99,7 +99,7 @@ function getActionMessage(selectedRow, activePath) {
99
99
  return 'Cannot start this worktree.';
100
100
  }
101
101
  if (action.kind === 'active') {
102
- return 'Already active. Press s to stop the current session.';
102
+ return 'Already active. Press Enter to restart, or s to stop.';
103
103
  }
104
104
  return 'Press Enter to start here and switch the active session.';
105
105
  }
@@ -8,7 +8,7 @@ function buildHelpLines(setupAvailable, editorAvailable) {
8
8
  { section: 'Scroll', binding: 'PageUp/PageDn', description: 'selection page' },
9
9
  { section: 'Logs', binding: 'L', description: 'full-screen logs' },
10
10
  { section: 'Logs', binding: '[/]', description: 'scroll log' },
11
- { section: 'Actions', binding: 'Enter', description: 'start/switch worktree' },
11
+ { section: 'Actions', binding: 'Enter', description: 'start/switch/restart worktree' },
12
12
  ];
13
13
  if (setupAvailable) {
14
14
  lines.push({ section: 'Actions', binding: 'i', description: 'setup selected worktree' });
@@ -76,19 +76,11 @@ export function createRuntimeStateActions({ config, paths, adapter }) {
76
76
  };
77
77
  const start = async (worktreePath) => {
78
78
  const current = await adapter.readActive();
79
- if (current?.worktreePath === worktreePath) {
80
- const model = await adapter.refresh();
81
- return {
82
- ...model,
83
- activePath: current.worktreePath,
84
- activeBranch: current.branch,
85
- status: { kind: 'idle', message: 'already active' },
86
- };
87
- }
88
79
  const invalidReason = await adapter.getInvalidReason(worktreePath);
89
80
  if (invalidReason) {
90
81
  throw new Error(invalidReason);
91
82
  }
83
+ const isRestart = current?.worktreePath === worktreePath;
92
84
  if (current) {
93
85
  await adapter.stopSession(current);
94
86
  await adapter.clearSession();
@@ -115,7 +107,7 @@ export function createRuntimeStateActions({ config, paths, adapter }) {
115
107
  ...model,
116
108
  activePath: worktreePath,
117
109
  activeBranch: branch,
118
- status: { kind: 'running', message: `started ${branch}` },
110
+ status: { kind: 'running', message: `${isRestart ? 'restarted' : 'started'} ${branch}` },
119
111
  };
120
112
  };
121
113
  const openEditor = async (worktreePath) => refreshWithStatus(() => adapter.openEditor(worktreePath), true);
@@ -33,9 +33,9 @@ export function decideEnterInteraction(selected, activePath) {
33
33
  }
34
34
  if (selected.path === activePath) {
35
35
  return {
36
- kind: 'set-status',
37
- status: { kind: 'idle', message: 'already active' },
38
- suppressesBackgroundRefreshes: true,
36
+ kind: 'start',
37
+ path: selected.path,
38
+ status: { kind: 'starting', message: `Restarting ${selected.branch}...` },
39
39
  };
40
40
  }
41
41
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohzw/worktree-command-tui",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "A TUI for managing git worktrees",
5
5
  "private": false,
6
6
  "type": "module",
package/dist/repro.d.ts DELETED
@@ -1 +0,0 @@
1
- export {};
package/dist/repro.js DELETED
@@ -1,13 +0,0 @@
1
- const model = {
2
- repoName: "reclaim-the-forest",
3
- namespace: "rojo-serve",
4
- rows: Array.from({ length: 10 }, (_, index) => ({
5
- path: `/repo/.worktree/feat-${index}`,
6
- shortPath: `.worktree/feat-${index}`,
7
- branch: `feat/${index}`,
8
- tags: (index === 0 ? [active] : []),
9
- pullRequest: index === 0 ? { kind: found, number: 2125, title: Selection }
10
- :
11
- }))
12
- };
13
- export {};