@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 +3 -3
- package/dist/app.js +19 -10
- package/dist/components/ActionPanel.js +1 -1
- package/dist/components/HelpWindow.js +1 -1
- package/dist/core/runtime-state.js +2 -10
- package/dist/core/tui-interaction.js +3 -3
- package/package.json +1 -1
- package/dist/repro.d.ts +0 -1
- package/dist/repro.js +0 -13
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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: '
|
|
37
|
-
|
|
38
|
-
|
|
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
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 {};
|