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