@ohzw/worktree-command-tui 0.1.1 → 0.1.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.
- package/README.md +5 -0
- package/dist/app.d.ts +1 -1
- package/dist/app.js +235 -113
- package/dist/components/ActionPanel.d.ts +4 -2
- package/dist/components/ActionPanel.js +87 -135
- package/dist/components/ContextBar.d.ts +4 -1
- package/dist/components/ContextBar.js +27 -3
- package/dist/components/HelpWindow.d.ts +7 -0
- package/dist/components/HelpWindow.js +29 -0
- package/dist/components/LogPanel.d.ts +10 -3
- package/dist/components/LogPanel.js +239 -33
- package/dist/components/WorktreeList.js +20 -40
- package/dist/core/command-runner.d.ts +11 -0
- package/dist/core/command-runner.js +44 -0
- package/dist/core/config-lifecycle.d.ts +25 -0
- package/dist/core/config-lifecycle.js +143 -0
- package/dist/core/config.d.ts +2 -3
- package/dist/core/config.js +0 -48
- package/dist/core/git-metadata.d.ts +25 -0
- package/dist/core/git-metadata.js +84 -0
- package/dist/core/git-worktrees.d.ts +2 -1
- package/dist/core/git-worktrees.js +30 -11
- package/dist/core/github-metadata.d.ts +14 -0
- package/dist/core/github-metadata.js +137 -0
- package/dist/core/init.d.ts +3 -2
- package/dist/core/init.js +9 -57
- package/dist/core/log-reader.d.ts +7 -0
- package/dist/core/log-reader.js +43 -0
- package/dist/core/runtime-state.d.ts +42 -0
- package/dist/core/runtime-state.js +125 -0
- package/dist/core/runtime.d.ts +19 -39
- package/dist/core/runtime.js +112 -216
- package/dist/core/tui-interaction.d.ts +31 -0
- package/dist/core/tui-interaction.js +59 -0
- package/dist/core/worktree-projection.d.ts +76 -0
- package/dist/core/worktree-projection.js +124 -0
- package/dist/terminal/viewport.d.ts +15 -0
- package/dist/terminal/viewport.js +49 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,6 +65,8 @@ A minimal example of the generated config:
|
|
|
65
65
|
"namespace": "worktree-command-tui",
|
|
66
66
|
// Command executed in each selected worktree
|
|
67
67
|
"command": ["npm", "run", "start"],
|
|
68
|
+
// Optional setup command run manually in the selected worktree
|
|
69
|
+
"setupCommand": ["npm", "install"],
|
|
68
70
|
// Port used for cleanup/monitoring
|
|
69
71
|
"port": 3000,
|
|
70
72
|
// Required files that must exist in a worktree
|
|
@@ -74,6 +76,9 @@ A minimal example of the generated config:
|
|
|
74
76
|
}
|
|
75
77
|
```
|
|
76
78
|
|
|
79
|
+
When `setupCommand` is configured, press `i` in the TUI to run it for the selected worktree.
|
|
80
|
+
It is never run automatically when switching worktrees.
|
|
81
|
+
|
|
77
82
|
## Development
|
|
78
83
|
|
|
79
84
|
```bash
|
package/dist/app.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare function getMouseWheelDelta(input: string): number;
|
|
|
14
14
|
export declare function getShellDimensions(columns: number, rows: number): ShellDimensions;
|
|
15
15
|
export declare function shouldUseCompactLayout(_columns: number, _rows: number, _worktreeCount?: number): boolean;
|
|
16
16
|
export declare function shouldUseMinimalLayout(columns: number, rows: number): boolean;
|
|
17
|
-
export declare function shouldStackPanes(columns: number, rows: number,
|
|
17
|
+
export declare function shouldStackPanes(columns: number, rows: number, _worktreeCount?: number): boolean;
|
|
18
18
|
export declare function App({ initialModel, actions, windowSizeOverride, }: {
|
|
19
19
|
initialModel: AppModel;
|
|
20
20
|
actions: AppActions;
|
package/dist/app.js
CHANGED
|
@@ -5,9 +5,11 @@ import { Box, Text, useApp, useInput, useStdin, useStdout, useWindowSize } from
|
|
|
5
5
|
import { ActionPanel } from './components/ActionPanel.js';
|
|
6
6
|
import { ContextBar } from './components/ContextBar.js';
|
|
7
7
|
import { Header } from './components/Header.js';
|
|
8
|
+
import { HelpWindow } from './components/HelpWindow.js';
|
|
8
9
|
import { FloatingLogWindow } from './components/FloatingLogWindow.js';
|
|
9
10
|
import { LogPanel, buildLogLines } from './components/LogPanel.js';
|
|
10
11
|
import { WorktreeList } from './components/WorktreeList.js';
|
|
12
|
+
import { clampSelectionIndex, decideEnterInteraction, decideSetupInteraction, getNextSelectedPath, getSelectedIndex } from './core/tui-interaction.js';
|
|
11
13
|
const ENABLE_MOUSE_TRACKING = '\u001B[?1000h\u001B[?1006h';
|
|
12
14
|
const DISABLE_MOUSE_TRACKING = '\u001B[?1000l\u001B[?1006l';
|
|
13
15
|
function parseMouseWheelEvents(input) {
|
|
@@ -31,19 +33,10 @@ function parseMouseWheelEvents(input) {
|
|
|
31
33
|
export function getMouseWheelDelta(input) {
|
|
32
34
|
return parseMouseWheelEvents(input).reduce((sum, event) => sum + event.delta, 0);
|
|
33
35
|
}
|
|
34
|
-
function getNextSelectedPath(rows, currentPath) {
|
|
35
|
-
if (rows.length === 0) {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
if (currentPath && rows.some(row => row.path === currentPath)) {
|
|
39
|
-
return currentPath;
|
|
40
|
-
}
|
|
41
|
-
return rows[0].path;
|
|
42
|
-
}
|
|
43
36
|
export function getShellDimensions(columns, rows) {
|
|
44
37
|
const rootWidth = Math.max(columns, 1);
|
|
45
38
|
const rootHeight = Math.max(rows, 1);
|
|
46
|
-
const bodyWidth =
|
|
39
|
+
const bodyWidth = rootWidth;
|
|
47
40
|
const maxListWidth = Math.max(1, bodyWidth - 21);
|
|
48
41
|
const desiredListWidth = Math.max(1, Math.floor((bodyWidth - 1) * 0.34));
|
|
49
42
|
const listWidth = Math.min(42, desiredListWidth, maxListWidth);
|
|
@@ -56,15 +49,61 @@ export function shouldUseCompactLayout(_columns, _rows, _worktreeCount = 0) {
|
|
|
56
49
|
export function shouldUseMinimalLayout(columns, rows) {
|
|
57
50
|
return columns < 20 || rows < 10;
|
|
58
51
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
const STACKED_LAYOUT_FRAME_ROWS = 9;
|
|
53
|
+
const HEADER_HEIGHT = 5;
|
|
54
|
+
const CONTEXT_BAR_HEIGHT = 4;
|
|
55
|
+
const PANE_GAP_WIDTH = 1;
|
|
56
|
+
const MIN_STACKED_PANE_HEIGHT = 9;
|
|
57
|
+
export function shouldStackPanes(columns, rows, _worktreeCount = 0) {
|
|
58
|
+
// Header + context bar consume the fixed chrome; each stacked pane still keeps ~6 visible content lines at the minimum height.
|
|
59
|
+
return columns < 96 && rows >= STACKED_LAYOUT_FRAME_ROWS + (MIN_STACKED_PANE_HEIGHT * 2);
|
|
63
60
|
}
|
|
64
61
|
function getLogPaneHeight(_rootHeight) {
|
|
65
|
-
//
|
|
62
|
+
// Each bordered log pane keeps ~6 visible content lines at 9 rows.
|
|
66
63
|
return 9;
|
|
67
64
|
}
|
|
65
|
+
const ACTIVE_TAG = 'active';
|
|
66
|
+
const ALREADY_ACTIVE_MESSAGE = 'already active';
|
|
67
|
+
function syncActiveTags(rows, activePath) {
|
|
68
|
+
let changed = false;
|
|
69
|
+
const nextRows = rows.map(row => {
|
|
70
|
+
const isActive = row.path === activePath;
|
|
71
|
+
const hasActiveTag = row.tags.includes(ACTIVE_TAG);
|
|
72
|
+
if (isActive === hasActiveTag) {
|
|
73
|
+
return row;
|
|
74
|
+
}
|
|
75
|
+
changed = true;
|
|
76
|
+
return {
|
|
77
|
+
...row,
|
|
78
|
+
tags: isActive ? [...row.tags, ACTIVE_TAG] : row.tags.filter(tag => tag !== ACTIVE_TAG),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
return changed ? nextRows : rows;
|
|
82
|
+
}
|
|
83
|
+
function shouldRefreshLogs(model) {
|
|
84
|
+
if (model.activePath === null) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (model.status.kind === 'running' || model.status.kind === 'error') {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return model.status.message === ALREADY_ACTIVE_MESSAGE;
|
|
91
|
+
}
|
|
92
|
+
function getStatusAfterLogRefresh(current, refresh) {
|
|
93
|
+
if (current.status.kind !== 'running') {
|
|
94
|
+
return current.status;
|
|
95
|
+
}
|
|
96
|
+
if (current.activePath === refresh.activePath && current.activeBranch === refresh.activeBranch) {
|
|
97
|
+
return current.status;
|
|
98
|
+
}
|
|
99
|
+
if (refresh.activePath === null) {
|
|
100
|
+
return { kind: 'idle', message: 'session ended' };
|
|
101
|
+
}
|
|
102
|
+
if (refresh.activeBranch) {
|
|
103
|
+
return { kind: 'running', message: `Active: ${refresh.activeBranch}` };
|
|
104
|
+
}
|
|
105
|
+
return { kind: 'running', message: 'running' };
|
|
106
|
+
}
|
|
68
107
|
export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
69
108
|
const { exit } = useApp();
|
|
70
109
|
const { stdin } = useStdin();
|
|
@@ -77,11 +116,12 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
77
116
|
const [worktreeScrollOffset, setWorktreeScrollOffset] = useState(0);
|
|
78
117
|
const [logScrollOffset, setLogScrollOffset] = useState(0);
|
|
79
118
|
const [isLogOverlayOpen, setIsLogOverlayOpen] = useState(false);
|
|
119
|
+
const [isHelpOverlayOpen, setIsHelpOverlayOpen] = useState(false);
|
|
120
|
+
const [pendingDelete, setPendingDelete] = useState(null);
|
|
80
121
|
const [completedAlert, setCompletedAlert] = useState(null);
|
|
81
122
|
const userActionInFlightRef = useRef(false);
|
|
82
|
-
const backgroundRefreshInFlightRef = useRef(false);
|
|
83
|
-
const actionGenerationRef = useRef(0);
|
|
84
123
|
const logRefreshInFlightRef = useRef(false);
|
|
124
|
+
const actionGenerationRef = useRef(0);
|
|
85
125
|
const previousStatusRef = useRef(initialModel.status.kind);
|
|
86
126
|
const alertTimeoutRef = useRef(null);
|
|
87
127
|
useEffect(() => {
|
|
@@ -110,31 +150,40 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
110
150
|
}
|
|
111
151
|
};
|
|
112
152
|
}, []);
|
|
153
|
+
const confirmationOpen = pendingDelete !== null;
|
|
154
|
+
const visibleStatus = confirmationOpen
|
|
155
|
+
? { kind: 'idle', message: `Delete ${pendingDelete.branch}? d/y confirm, Esc/n/q cancel` }
|
|
156
|
+
: model.status;
|
|
113
157
|
useEffect(() => {
|
|
114
|
-
|
|
115
|
-
if (alertTimeoutRef.current !== null) {
|
|
116
|
-
clearTimeout(alertTimeoutRef.current);
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
}, []);
|
|
120
|
-
useEffect(() => {
|
|
121
|
-
if (model.status.kind !== 'running') {
|
|
158
|
+
if (!shouldRefreshLogs(model)) {
|
|
122
159
|
return;
|
|
123
160
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
void apply(() => actions.refresh(), { blocksInput: false });
|
|
129
|
-
}, 1500);
|
|
161
|
+
// Only logs and active-session liveness need near-real-time updates.
|
|
162
|
+
// Full worktree metadata includes GitHub PR lookups and is refreshed by
|
|
163
|
+
// explicit user actions instead of a tight polling loop.
|
|
130
164
|
const logRefreshInterval = setInterval(() => {
|
|
131
|
-
if (userActionInFlightRef.current ||
|
|
165
|
+
if (userActionInFlightRef.current || logRefreshInFlightRef.current) {
|
|
132
166
|
return;
|
|
133
167
|
}
|
|
168
|
+
const generation = actionGenerationRef.current;
|
|
134
169
|
logRefreshInFlightRef.current = true;
|
|
135
170
|
void actions.refreshLogs()
|
|
136
|
-
.then(
|
|
137
|
-
|
|
171
|
+
.then(refresh => {
|
|
172
|
+
if (generation !== actionGenerationRef.current || userActionInFlightRef.current) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
setModel(current => {
|
|
176
|
+
const activeChanged = current.activePath !== refresh.activePath || current.activeBranch !== refresh.activeBranch;
|
|
177
|
+
const status = getStatusAfterLogRefresh(current, refresh);
|
|
178
|
+
return {
|
|
179
|
+
...current,
|
|
180
|
+
logs: refresh.logs,
|
|
181
|
+
activePath: refresh.activePath,
|
|
182
|
+
activeBranch: refresh.activeBranch,
|
|
183
|
+
status,
|
|
184
|
+
rows: activeChanged ? syncActiveTags(current.rows, refresh.activePath) : current.rows,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
138
187
|
})
|
|
139
188
|
.catch(() => { })
|
|
140
189
|
.finally(() => {
|
|
@@ -142,17 +191,15 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
142
191
|
});
|
|
143
192
|
}, 400);
|
|
144
193
|
return () => {
|
|
145
|
-
clearInterval(fullRefreshInterval);
|
|
146
194
|
clearInterval(logRefreshInterval);
|
|
147
195
|
};
|
|
148
|
-
}, [actions, model.status.kind]);
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}, [model.rows, selectedPath]);
|
|
196
|
+
}, [actions, model.activePath, model.status.kind, model.status.message]);
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (confirmationOpen && !model.rows.some(row => row.path === pendingDelete.path)) {
|
|
199
|
+
setPendingDelete(null);
|
|
200
|
+
}
|
|
201
|
+
}, [confirmationOpen, model.rows, pendingDelete]);
|
|
202
|
+
const selectedIndex = useMemo(() => getSelectedIndex(model.rows, selectedPath), [model.rows, selectedPath]);
|
|
156
203
|
const selected = model.rows[selectedIndex];
|
|
157
204
|
const { rootWidth, rootHeight, bodyWidth, listWidth, actionWidth } = getShellDimensions(columns, rows);
|
|
158
205
|
const minimalLayout = shouldUseMinimalLayout(rootWidth, rootHeight);
|
|
@@ -161,10 +208,11 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
161
208
|
const compactDetailPane = !stackedLayout && rootHeight <= 30 && model.rows.length > 1;
|
|
162
209
|
const showLogPanel = !stackedLayout && rootHeight >= 34;
|
|
163
210
|
const logPaneHeight = showLogPanel ? getLogPaneHeight(rootHeight) : 0;
|
|
211
|
+
const stackedPaneHeight = Math.max(3, Math.floor((rootHeight - STACKED_LAYOUT_FRAME_ROWS) / 2));
|
|
164
212
|
const paneHeight = stackedLayout
|
|
165
|
-
?
|
|
166
|
-
: Math.max(3, rootHeight -
|
|
167
|
-
const selectionScrollPageSize = Math.max(1, Math.floor(
|
|
213
|
+
? stackedPaneHeight
|
|
214
|
+
: Math.max(3, rootHeight - STACKED_LAYOUT_FRAME_ROWS - logPaneHeight);
|
|
215
|
+
const selectionScrollPageSize = Math.max(1, Math.floor(paneHeight / 2));
|
|
168
216
|
const logLineCount = useMemo(() => buildLogLines(model.logs).length, [model.logs]);
|
|
169
217
|
const logViewportHeight = isLogOverlayOpen
|
|
170
218
|
? Math.max(1, rootHeight - 3)
|
|
@@ -172,10 +220,11 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
172
220
|
const maxLogScrollOffset = Math.max(0, logLineCount - logViewportHeight);
|
|
173
221
|
const logScrollPageSize = Math.max(1, Math.floor((logViewportHeight || rootHeight) / 2));
|
|
174
222
|
function moveSelection(nextIndex) {
|
|
175
|
-
|
|
223
|
+
const clampedIndex = clampSelectionIndex(nextIndex, model.rows.length);
|
|
224
|
+
if (clampedIndex === null) {
|
|
176
225
|
return;
|
|
177
226
|
}
|
|
178
|
-
setSelectedPath(model.rows[
|
|
227
|
+
setSelectedPath(model.rows[clampedIndex].path);
|
|
179
228
|
}
|
|
180
229
|
function clearTransientAlert() {
|
|
181
230
|
if (alertTimeoutRef.current !== null) {
|
|
@@ -184,47 +233,40 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
184
233
|
}
|
|
185
234
|
setCompletedAlert(null);
|
|
186
235
|
}
|
|
187
|
-
function
|
|
236
|
+
function invalidateStaleLogRefreshes() {
|
|
188
237
|
actionGenerationRef.current += 1;
|
|
189
238
|
}
|
|
190
|
-
async function apply(action
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (blocksInput) {
|
|
194
|
-
actionGenerationRef.current = generation;
|
|
195
|
-
userActionInFlightRef.current = true;
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
backgroundRefreshInFlightRef.current = true;
|
|
199
|
-
}
|
|
239
|
+
async function apply(action) {
|
|
240
|
+
invalidateStaleLogRefreshes();
|
|
241
|
+
userActionInFlightRef.current = true;
|
|
200
242
|
try {
|
|
201
|
-
|
|
202
|
-
if (blocksInput || (generation === actionGenerationRef.current && !userActionInFlightRef.current)) {
|
|
203
|
-
setModel(next);
|
|
204
|
-
}
|
|
243
|
+
setModel(await action());
|
|
205
244
|
}
|
|
206
245
|
catch (error) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}));
|
|
215
|
-
}
|
|
246
|
+
setModel(current => ({
|
|
247
|
+
...current,
|
|
248
|
+
status: {
|
|
249
|
+
kind: 'error',
|
|
250
|
+
message: error instanceof Error ? error.message : String(error),
|
|
251
|
+
},
|
|
252
|
+
}));
|
|
216
253
|
}
|
|
217
254
|
finally {
|
|
218
|
-
|
|
219
|
-
userActionInFlightRef.current = false;
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
backgroundRefreshInFlightRef.current = false;
|
|
223
|
-
}
|
|
255
|
+
userActionInFlightRef.current = false;
|
|
224
256
|
}
|
|
225
257
|
}
|
|
226
258
|
useInput((input, key) => {
|
|
259
|
+
if (isHelpOverlayOpen) {
|
|
260
|
+
if (key.escape || input === '\u001B' || input === 'q' || input === '?') {
|
|
261
|
+
setIsHelpOverlayOpen(false);
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
227
265
|
if (isLogOverlayOpen) {
|
|
266
|
+
if (input === '?') {
|
|
267
|
+
setIsHelpOverlayOpen(true);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
228
270
|
if (key.escape || input === 'q' || input === 'L') {
|
|
229
271
|
setIsLogOverlayOpen(false);
|
|
230
272
|
return;
|
|
@@ -255,6 +297,20 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
255
297
|
}
|
|
256
298
|
return;
|
|
257
299
|
}
|
|
300
|
+
if (confirmationOpen) {
|
|
301
|
+
if (key.escape || input === '\u001B' || input === 'q' || input === 'n') {
|
|
302
|
+
setPendingDelete(null);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (input === 'd' || input === 'y') {
|
|
306
|
+
const { path: worktreePath } = pendingDelete;
|
|
307
|
+
setPendingDelete(null);
|
|
308
|
+
clearTransientAlert();
|
|
309
|
+
void apply(() => actions.deleteWorktree(worktreePath));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
258
314
|
if (key.escape || input === 'q') {
|
|
259
315
|
exit();
|
|
260
316
|
return;
|
|
@@ -275,6 +331,10 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
275
331
|
moveSelection(model.rows.length - 1);
|
|
276
332
|
return;
|
|
277
333
|
}
|
|
334
|
+
if (input === '?') {
|
|
335
|
+
setIsHelpOverlayOpen(true);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
278
338
|
if (input === 'L') {
|
|
279
339
|
setIsLogOverlayOpen(true);
|
|
280
340
|
return;
|
|
@@ -298,22 +358,22 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
298
358
|
if (userActionInFlightRef.current) {
|
|
299
359
|
return;
|
|
300
360
|
}
|
|
301
|
-
if (key.return
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
setModel(current => ({ ...current, status: { kind: 'error', message: selected.invalidReason } }));
|
|
305
|
-
clearTransientAlert();
|
|
361
|
+
if (key.return) {
|
|
362
|
+
const decision = decideEnterInteraction(selected, model.activePath);
|
|
363
|
+
if (decision.kind === 'ignore') {
|
|
306
364
|
return;
|
|
307
365
|
}
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
|
|
366
|
+
if (decision.kind === 'set-status') {
|
|
367
|
+
if (decision.suppressesBackgroundRefreshes) {
|
|
368
|
+
invalidateStaleLogRefreshes();
|
|
369
|
+
}
|
|
370
|
+
setModel(current => ({ ...current, status: decision.status }));
|
|
311
371
|
clearTransientAlert();
|
|
312
372
|
return;
|
|
313
373
|
}
|
|
314
|
-
setModel(current => ({ ...current, status:
|
|
374
|
+
setModel(current => ({ ...current, status: decision.status }));
|
|
315
375
|
clearTransientAlert();
|
|
316
|
-
void apply(() => actions.start(
|
|
376
|
+
void apply(() => actions.start(decision.path));
|
|
317
377
|
return;
|
|
318
378
|
}
|
|
319
379
|
if (input === 's') {
|
|
@@ -322,6 +382,31 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
322
382
|
void apply(() => actions.stop());
|
|
323
383
|
return;
|
|
324
384
|
}
|
|
385
|
+
if (input === 'i') {
|
|
386
|
+
const decision = decideSetupInteraction(selected, model.setupAvailable);
|
|
387
|
+
if (decision.kind === 'ignore') {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
setModel(current => ({ ...current, status: decision.status }));
|
|
391
|
+
clearTransientAlert();
|
|
392
|
+
void apply(() => actions.setup(decision.path));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (input === 'e' && selected && model.editorAvailable) {
|
|
396
|
+
clearTransientAlert();
|
|
397
|
+
void apply(() => actions.openEditor(selected.path));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (input === 'o' && selected) {
|
|
401
|
+
clearTransientAlert();
|
|
402
|
+
void apply(() => actions.openPullRequest(selected.path));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (input === 'd' && selected) {
|
|
406
|
+
clearTransientAlert();
|
|
407
|
+
setPendingDelete({ path: selected.path, branch: selected.branch });
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
325
410
|
if (input === 'r') {
|
|
326
411
|
clearTransientAlert();
|
|
327
412
|
void apply(() => actions.refresh());
|
|
@@ -329,15 +414,22 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
329
414
|
});
|
|
330
415
|
const listPaneViewportHeight = paneHeight === undefined ? undefined : Math.max(1, paneHeight - 3);
|
|
331
416
|
const mouseWheelLineStep = 3;
|
|
332
|
-
const paneAreaLeft =
|
|
417
|
+
const paneAreaLeft = 1;
|
|
333
418
|
const worktreePaneRight = !stackedLayout ? paneAreaLeft + listWidth - 1 : undefined;
|
|
334
|
-
const selectionPaneLeft = !stackedLayout && worktreePaneRight !== undefined ? worktreePaneRight +
|
|
335
|
-
const bodyPaneTop = !stackedLayout && paneHeight !== undefined ?
|
|
419
|
+
const selectionPaneLeft = !stackedLayout && worktreePaneRight !== undefined ? worktreePaneRight + PANE_GAP_WIDTH + 1 : undefined;
|
|
420
|
+
const bodyPaneTop = !stackedLayout && paneHeight !== undefined ? HEADER_HEIGHT + 1 : undefined;
|
|
336
421
|
const bodyPaneBottom = !stackedLayout && bodyPaneTop !== undefined && paneHeight !== undefined ? bodyPaneTop + paneHeight - 1 : undefined;
|
|
337
|
-
const
|
|
338
|
-
const
|
|
422
|
+
const stackedWorktreePaneTop = stackedLayout && paneHeight !== undefined ? HEADER_HEIGHT + 1 : undefined;
|
|
423
|
+
const stackedWorktreePaneBottom = stackedLayout && stackedWorktreePaneTop !== undefined && paneHeight !== undefined ? stackedWorktreePaneTop + paneHeight - 1 : undefined;
|
|
424
|
+
const stackedSelectionPaneTop = stackedLayout && stackedWorktreePaneBottom !== undefined ? stackedWorktreePaneBottom + 1 : undefined;
|
|
425
|
+
const stackedSelectionPaneBottom = stackedLayout && stackedSelectionPaneTop !== undefined && paneHeight !== undefined ? stackedSelectionPaneTop + paneHeight - 1 : undefined;
|
|
426
|
+
const logPaneTop = showLogPanel ? rootHeight - CONTEXT_BAR_HEIGHT - logPaneHeight + 1 : undefined;
|
|
427
|
+
const logPaneBottom = showLogPanel ? rootHeight - CONTEXT_BAR_HEIGHT : undefined;
|
|
339
428
|
useEffect(() => {
|
|
340
429
|
const onData = (data) => {
|
|
430
|
+
if (isHelpOverlayOpen) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
341
433
|
const events = parseMouseWheelEvents(typeof data === 'string' ? data : data.toString('utf8'));
|
|
342
434
|
for (const event of events) {
|
|
343
435
|
if (isLogOverlayOpen) {
|
|
@@ -355,32 +447,55 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
355
447
|
setLogScrollOffset(current => Math.max(0, Math.min(maxLogScrollOffset, current - event.delta * mouseWheelLineStep)));
|
|
356
448
|
continue;
|
|
357
449
|
}
|
|
450
|
+
const scrollWorktrees = () => {
|
|
451
|
+
setWorktreeScrollOffset(current => {
|
|
452
|
+
if (listPaneViewportHeight === undefined) {
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
const max = Math.max(0, model.rows.length - listPaneViewportHeight);
|
|
456
|
+
return Math.max(0, Math.min(max, current + event.delta * mouseWheelLineStep));
|
|
457
|
+
});
|
|
458
|
+
};
|
|
459
|
+
if (stackedLayout) {
|
|
460
|
+
const isStackedWorktreeEvent = stackedWorktreePaneTop !== undefined
|
|
461
|
+
&& stackedWorktreePaneBottom !== undefined
|
|
462
|
+
&& event.y !== undefined
|
|
463
|
+
&& event.y >= stackedWorktreePaneTop
|
|
464
|
+
&& event.y <= stackedWorktreePaneBottom;
|
|
465
|
+
if (isStackedWorktreeEvent) {
|
|
466
|
+
scrollWorktrees();
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const isStackedSelectionEvent = stackedSelectionPaneTop !== undefined
|
|
470
|
+
&& stackedSelectionPaneBottom !== undefined
|
|
471
|
+
&& event.y !== undefined
|
|
472
|
+
&& event.y >= stackedSelectionPaneTop
|
|
473
|
+
&& event.y <= stackedSelectionPaneBottom;
|
|
474
|
+
if (isStackedSelectionEvent) {
|
|
475
|
+
setSelectionScrollOffset(current => Math.max(0, current + event.delta * mouseWheelLineStep));
|
|
476
|
+
}
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
358
479
|
const isBodyPaneEvent = bodyPaneTop !== undefined
|
|
359
480
|
&& bodyPaneBottom !== undefined
|
|
360
481
|
&& event.y !== undefined
|
|
361
482
|
&& event.y >= bodyPaneTop
|
|
362
483
|
&& event.y <= bodyPaneBottom;
|
|
363
|
-
|
|
364
|
-
|
|
484
|
+
if (!isBodyPaneEvent) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const isSelectionPaneEvent = selectionPaneLeft !== undefined
|
|
365
488
|
&& event.x !== undefined
|
|
366
489
|
&& event.x >= selectionPaneLeft;
|
|
367
490
|
if (isSelectionPaneEvent) {
|
|
368
491
|
setSelectionScrollOffset(current => Math.max(0, current + event.delta * mouseWheelLineStep));
|
|
369
492
|
continue;
|
|
370
493
|
}
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return 0;
|
|
377
|
-
}
|
|
378
|
-
const max = Math.max(0, model.rows.length - listPaneViewportHeight);
|
|
379
|
-
return Math.max(0, Math.min(max, current + event.delta * mouseWheelLineStep));
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
else {
|
|
383
|
-
setSelectionScrollOffset(current => Math.max(0, current + event.delta * mouseWheelLineStep));
|
|
494
|
+
const isWorktreePaneEvent = event.x === undefined
|
|
495
|
+
|| worktreePaneRight === undefined
|
|
496
|
+
|| event.x <= worktreePaneRight;
|
|
497
|
+
if (isWorktreePaneEvent) {
|
|
498
|
+
scrollWorktrees();
|
|
384
499
|
}
|
|
385
500
|
}
|
|
386
501
|
};
|
|
@@ -394,7 +509,7 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
394
509
|
stdout.write(DISABLE_MOUSE_TRACKING);
|
|
395
510
|
}
|
|
396
511
|
};
|
|
397
|
-
}, [stdin, stdout, listWidth, stackedLayout, listPaneViewportHeight, mouseWheelLineStep, model.rows.length, showLogPanel, logPaneTop, logPaneBottom, maxLogScrollOffset, worktreePaneRight, selectionPaneLeft, bodyPaneTop, bodyPaneBottom, isLogOverlayOpen]);
|
|
512
|
+
}, [stdin, stdout, listWidth, stackedLayout, listPaneViewportHeight, mouseWheelLineStep, model.rows.length, showLogPanel, logPaneTop, logPaneBottom, maxLogScrollOffset, worktreePaneRight, selectionPaneLeft, bodyPaneTop, bodyPaneBottom, stackedWorktreePaneTop, stackedWorktreePaneBottom, stackedSelectionPaneTop, stackedSelectionPaneBottom, isLogOverlayOpen, isHelpOverlayOpen]);
|
|
398
513
|
useEffect(() => {
|
|
399
514
|
if (listPaneViewportHeight === undefined) {
|
|
400
515
|
setWorktreeScrollOffset(0);
|
|
@@ -418,16 +533,23 @@ export function App({ initialModel, actions, windowSizeOverride, }) {
|
|
|
418
533
|
}
|
|
419
534
|
setLogScrollOffset(current => Math.min(current, maxLogScrollOffset));
|
|
420
535
|
}, [showLogPanel, isLogOverlayOpen, maxLogScrollOffset]);
|
|
536
|
+
if (isHelpOverlayOpen) {
|
|
537
|
+
return (_jsx(HelpWindow, { setupAvailable: model.setupAvailable, editorAvailable: model.editorAvailable, width: Math.max(1, rootWidth - 1), height: rootHeight }));
|
|
538
|
+
}
|
|
421
539
|
if (isLogOverlayOpen) {
|
|
422
540
|
return (_jsx(FloatingLogWindow, { logs: model.logs, width: Math.max(1, rootWidth - 1), height: rootHeight, scrollOffset: logScrollOffset }));
|
|
423
541
|
}
|
|
424
542
|
if (minimalLayout) {
|
|
425
|
-
return (_jsxs(Box, { width: rootWidth, height: rootHeight, flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "green", wrap: "truncate-end", children: ["A:", model.activeBranch ?? '-'] }), rootHeight >= 2 ? _jsxs(Text, { wrap: "truncate-end", children: ["S:", selected?.branch ?? '-'] }) : null, rootHeight >= 3 ?
|
|
543
|
+
return (_jsxs(Box, { width: rootWidth, height: rootHeight, flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "green", wrap: "truncate-end", children: ["A:", model.activeBranch ?? '-'] }), rootHeight >= 2 ? _jsxs(Text, { wrap: "truncate-end", children: ["S:", selected?.branch ?? '-'] }) : null, rootHeight >= 3 ? _jsx(Text, { wrap: "truncate-end", children: confirmationOpen ? `D:${visibleStatus.message}` : `T:${model.status.kind}` }) : null, rootHeight >= 4 ? (confirmationOpen
|
|
544
|
+
? _jsx(Text, { dimColor: true, wrap: "truncate-end", children: "d/y confirm \u00B7 Esc/n/q cancel" })
|
|
545
|
+
: _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u2191\u2193jk\u21B5", model.setupAvailable ? 'i' : '', model.editorAvailable ? 'e' : '', "odLq"] })) : null] }));
|
|
426
546
|
}
|
|
427
547
|
if (compactLayout) {
|
|
428
|
-
return (_jsxs(Box, { width: rootWidth, height: rootHeight,
|
|
548
|
+
return (_jsxs(Box, { width: rootWidth, height: rootHeight, flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "green", wrap: "truncate-end", children: ["Active: ", model.activeBranch ?? '-'] }), _jsxs(Text, { wrap: "truncate-end", children: ["Selected: ", selected?.branch ?? '-'] }), completedAlert
|
|
429
549
|
? _jsxs(Text, { color: "green", wrap: "truncate-end", children: ["\u2714 ", completedAlert] })
|
|
430
|
-
: model.status.kind === 'starting' || model.status.kind === 'stopping' ? (_jsx(Spinner, { label: `Status: ${model.status.kind} — ${model.status.message}` })) : (_jsxs(Text, { wrap: "truncate-end", children: ["Status: ",
|
|
550
|
+
: model.status.kind === 'setting-up' || model.status.kind === 'starting' || model.status.kind === 'stopping' ? (_jsx(Spinner, { label: `Status: ${model.status.kind} — ${model.status.message}` })) : (_jsxs(Text, { wrap: "truncate-end", children: ["Status: ", visibleStatus.kind, " \u2014 ", visibleStatus.message] })), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: confirmationOpen
|
|
551
|
+
? 'Keys: d/y confirm | Esc/n/q cancel'
|
|
552
|
+
: `Keys: ↑↓/jk g/G ↵${model.setupAvailable ? ' i' : ''}${model.editorAvailable ? ' e' : ''} o d L s r q · Resize terminal for split view` })] }));
|
|
431
553
|
}
|
|
432
|
-
return (_jsxs(Box, { width: rootWidth, height: rootHeight,
|
|
554
|
+
return (_jsxs(Box, { width: rootWidth, height: rootHeight, flexDirection: "column", children: [_jsx(Header, { repoName: model.repoName, namespace: model.namespace, activeBranch: model.activeBranch }), _jsxs(Box, { flexDirection: stackedLayout ? 'column' : 'row', flexGrow: stackedLayout ? 0 : 1, flexShrink: 1, children: [_jsx(WorktreeList, { rows: model.rows, selectedIndex: selectedIndex, width: stackedLayout ? bodyWidth : listWidth, height: paneHeight, stacked: stackedLayout, scrollOffset: worktreeScrollOffset }), _jsx(ActionPanel, { selectedRow: selected, activePath: model.activePath, setupAvailable: model.setupAvailable, stacked: stackedLayout, width: stackedLayout ? bodyWidth : actionWidth, height: paneHeight, compactDetails: compactDetailPane, scrollOffset: selectionScrollOffset })] }), showLogPanel ? _jsx(LogPanel, { logs: model.logs, width: bodyWidth, height: logPaneHeight, scrollOffset: logScrollOffset }) : null, _jsx(ContextBar, { status: visibleStatus, setupAvailable: model.setupAvailable, editorAvailable: model.editorAvailable, confirmationOpen: confirmationOpen }), completedAlert ? (_jsx(Box, { position: "absolute", top: 1, right: 2, children: _jsx(Alert, { variant: "success", children: completedAlert }) })) : null] }));
|
|
433
555
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { AppRow } from '../core/runtime.js';
|
|
2
|
-
|
|
2
|
+
import { type ProjectionSeverity } from '../core/worktree-projection.js';
|
|
3
|
+
export declare function getActionVariant(selectedRow: AppRow, activePath: string | null): ProjectionSeverity;
|
|
3
4
|
export declare function getPullRequestColor(selectedRow: AppRow): 'green' | 'yellow' | 'red' | undefined;
|
|
4
|
-
export declare function ActionPanel({ selectedRow, activePath, stacked, width, height, compactDetails, scrollOffset, }: {
|
|
5
|
+
export declare function ActionPanel({ selectedRow, activePath, setupAvailable, stacked, width, height, compactDetails, scrollOffset, }: {
|
|
5
6
|
selectedRow: AppRow | undefined;
|
|
6
7
|
activePath: string | null;
|
|
8
|
+
setupAvailable: boolean;
|
|
7
9
|
stacked: boolean;
|
|
8
10
|
width?: number;
|
|
9
11
|
height?: number;
|