gitspace 0.2.0-rc.14 → 0.2.0-rc.15
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/package.json +5 -5
- package/src/app.web.tsx +11 -0
- package/src/components/SessionTerminal.tui.tsx +34 -4
- package/src/components/SessionTerminal.web.tsx +62 -3
- package/src/components/session-terminal-page-navigation.ts +48 -0
- package/src/tui/__tests__/session-terminal-page-navigation.test.ts +94 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitspace",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.15",
|
|
4
4
|
"description": "CLI for managing GitHub workspaces with git worktrees and secure remote terminal access",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gssh": "./bin/gssh"
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"relay": "bun src/relay/index.ts"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@gitspace/darwin-arm64": "0.2.0-rc.
|
|
21
|
-
"@gitspace/darwin-x64": "0.2.0-rc.
|
|
22
|
-
"@gitspace/linux-x64": "0.2.0-rc.
|
|
23
|
-
"@gitspace/linux-arm64": "0.2.0-rc.
|
|
20
|
+
"@gitspace/darwin-arm64": "0.2.0-rc.15",
|
|
21
|
+
"@gitspace/darwin-x64": "0.2.0-rc.15",
|
|
22
|
+
"@gitspace/linux-x64": "0.2.0-rc.15",
|
|
23
|
+
"@gitspace/linux-arm64": "0.2.0-rc.15"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"cli",
|
package/src/app.web.tsx
CHANGED
|
@@ -47,6 +47,9 @@ import {
|
|
|
47
47
|
|
|
48
48
|
type View = "machines" | "terminal";
|
|
49
49
|
|
|
50
|
+
const PAGE_UP = '\x1b[5~';
|
|
51
|
+
const PAGE_DOWN = '\x1b[6~';
|
|
52
|
+
|
|
50
53
|
export default function App() {
|
|
51
54
|
const [view, setView] = useState<View>("machines");
|
|
52
55
|
const [selectedMachine, setSelectedMachine] = useState<MachineInfo | null>(null);
|
|
@@ -762,6 +765,14 @@ export default function App() {
|
|
|
762
765
|
if (view === "terminal" && terminal.status === "established" && terminal.mode === "attached") {
|
|
763
766
|
// Handler for sending data from mobile controls (already processed)
|
|
764
767
|
const handleSendData = (data: string) => {
|
|
768
|
+
if (data === PAGE_UP && terminalRef.current?.pageUp()) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (data === PAGE_DOWN && terminalRef.current?.pageDown()) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
765
776
|
terminal.send(new TextEncoder().encode(data));
|
|
766
777
|
};
|
|
767
778
|
|
|
@@ -12,6 +12,10 @@ import { findUtf8Boundary } from '../utils/utf8.js';
|
|
|
12
12
|
import { BracketedPasteModeTracker, wrapPaste } from './terminal-bracketed-paste.tui.js';
|
|
13
13
|
import { toast } from '@opentui-ui/toast';
|
|
14
14
|
import { copyToClipboard } from '../utils/clipboard.js';
|
|
15
|
+
import {
|
|
16
|
+
getPageNavigationEscapeSequence,
|
|
17
|
+
shouldConsumePageNavigationInScrollbox,
|
|
18
|
+
} from './session-terminal-page-navigation.js';
|
|
15
19
|
|
|
16
20
|
extend({ 'ghostty-terminal': GhosttyTerminalRenderable });
|
|
17
21
|
|
|
@@ -256,13 +260,35 @@ export function SessionTerminal({
|
|
|
256
260
|
}
|
|
257
261
|
|
|
258
262
|
if (key.name === 'pageup') {
|
|
259
|
-
scrollBoxRef.current
|
|
260
|
-
|
|
263
|
+
const scrollBox = scrollBoxRef.current;
|
|
264
|
+
if (
|
|
265
|
+
scrollBox &&
|
|
266
|
+
shouldConsumePageNavigationInScrollbox({
|
|
267
|
+
direction: 'up',
|
|
268
|
+
scrollTop: scrollBox.scrollTop,
|
|
269
|
+
scrollHeight: scrollBox.scrollHeight,
|
|
270
|
+
viewportHeight: scrollBox.viewport.height,
|
|
271
|
+
})
|
|
272
|
+
) {
|
|
273
|
+
scrollBox.scrollBy(-1, 'viewport');
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
261
276
|
}
|
|
262
277
|
|
|
263
278
|
if (key.name === 'pagedown') {
|
|
264
|
-
scrollBoxRef.current
|
|
265
|
-
|
|
279
|
+
const scrollBox = scrollBoxRef.current;
|
|
280
|
+
if (
|
|
281
|
+
scrollBox &&
|
|
282
|
+
shouldConsumePageNavigationInScrollbox({
|
|
283
|
+
direction: 'down',
|
|
284
|
+
scrollTop: scrollBox.scrollTop,
|
|
285
|
+
scrollHeight: scrollBox.scrollHeight,
|
|
286
|
+
viewportHeight: scrollBox.viewport.height,
|
|
287
|
+
})
|
|
288
|
+
) {
|
|
289
|
+
scrollBox.scrollBy(1, 'viewport');
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
266
292
|
}
|
|
267
293
|
|
|
268
294
|
if (key.name === 'escape' && key.ctrl) {
|
|
@@ -298,6 +324,10 @@ export function SessionTerminal({
|
|
|
298
324
|
} else if (code.length === 1) {
|
|
299
325
|
data = `\x1b[1;${modifier}${code}`;
|
|
300
326
|
}
|
|
327
|
+
} else if (key.name === 'pageup') {
|
|
328
|
+
data = getPageNavigationEscapeSequence('up');
|
|
329
|
+
} else if (key.name === 'pagedown') {
|
|
330
|
+
data = getPageNavigationEscapeSequence('down');
|
|
301
331
|
} else if (key.sequence) {
|
|
302
332
|
data = key.sequence;
|
|
303
333
|
} else if (key.raw) {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { useEffect, useRef, useImperativeHandle, forwardRef } from "react";
|
|
1
|
+
import { useEffect, useRef, useImperativeHandle, forwardRef, useCallback } from "react";
|
|
2
2
|
import { init, Terminal as GhosttyTerminal, FitAddon } from "ghostty-web";
|
|
3
|
+
import {
|
|
4
|
+
canConsumePageNavigationInViewport,
|
|
5
|
+
type PageDirection,
|
|
6
|
+
} from './session-terminal-page-navigation.js';
|
|
3
7
|
|
|
4
8
|
interface Props {
|
|
5
9
|
onData: (data: Uint8Array) => void;
|
|
@@ -20,6 +24,19 @@ export interface SessionTerminalHandle {
|
|
|
20
24
|
sendData: (data: string) => void;
|
|
21
25
|
isFocused: () => boolean;
|
|
22
26
|
getSize: () => { cols: number; rows: number } | null;
|
|
27
|
+
pageUp: () => boolean;
|
|
28
|
+
pageDown: () => boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TerminalViewportLike {
|
|
32
|
+
viewportY?: number;
|
|
33
|
+
rows?: number;
|
|
34
|
+
scrollLines: (lines: number) => void;
|
|
35
|
+
buffer?: {
|
|
36
|
+
active?: {
|
|
37
|
+
baseY?: number;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
23
40
|
}
|
|
24
41
|
|
|
25
42
|
// Touch scrolling constants (agentboard-inspired accumulated delta pattern)
|
|
@@ -73,6 +90,30 @@ export const SessionTerminal = forwardRef<SessionTerminalHandle, Props>(function
|
|
|
73
90
|
allowTouchScrollRef.current = allowTouchScroll;
|
|
74
91
|
}, [allowTouchScroll]);
|
|
75
92
|
|
|
93
|
+
const tryConsumePageNavigation = useCallback((direction: PageDirection): boolean => {
|
|
94
|
+
const terminal = terminalRef.current as unknown as TerminalViewportLike | null;
|
|
95
|
+
if (!terminal) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const viewportY = terminal.viewportY ?? 0;
|
|
100
|
+
const baseY = terminal.buffer?.active?.baseY ?? 0;
|
|
101
|
+
const canConsume = canConsumePageNavigationInViewport({
|
|
102
|
+
direction,
|
|
103
|
+
viewportY,
|
|
104
|
+
baseY,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!canConsume) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const linesPerPage = Math.max(1, (terminal.rows ?? 1) - 1);
|
|
112
|
+
terminal.scrollLines(direction === 'up' ? -linesPerPage : linesPerPage);
|
|
113
|
+
onActivityRef.current?.();
|
|
114
|
+
return true;
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
76
117
|
// Expose methods via ref for external control (e.g., from TerminalControls)
|
|
77
118
|
useImperativeHandle(
|
|
78
119
|
ref,
|
|
@@ -102,8 +143,10 @@ export const SessionTerminal = forwardRef<SessionTerminalHandle, Props>(function
|
|
|
102
143
|
}
|
|
103
144
|
return { cols: term.cols, rows: term.rows };
|
|
104
145
|
},
|
|
146
|
+
pageUp: () => tryConsumePageNavigation('up'),
|
|
147
|
+
pageDown: () => tryConsumePageNavigation('down'),
|
|
105
148
|
}),
|
|
106
|
-
[]
|
|
149
|
+
[tryConsumePageNavigation]
|
|
107
150
|
);
|
|
108
151
|
|
|
109
152
|
useEffect(() => {
|
|
@@ -164,6 +207,22 @@ export const SessionTerminal = forwardRef<SessionTerminalHandle, Props>(function
|
|
|
164
207
|
event.preventDefault();
|
|
165
208
|
event.stopPropagation();
|
|
166
209
|
onDataRef.current(new TextEncoder().encode('\x1b[Z'));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (event.key === 'PageUp') {
|
|
214
|
+
if (tryConsumePageNavigation('up')) {
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
event.stopPropagation();
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (event.key === 'PageDown') {
|
|
222
|
+
if (tryConsumePageNavigation('down')) {
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
event.stopPropagation();
|
|
225
|
+
}
|
|
167
226
|
}
|
|
168
227
|
};
|
|
169
228
|
container.addEventListener('keydown', handleKeyDown, true);
|
|
@@ -301,7 +360,7 @@ export const SessionTerminal = forwardRef<SessionTerminalHandle, Props>(function
|
|
|
301
360
|
setWriteCallback(null);
|
|
302
361
|
}
|
|
303
362
|
};
|
|
304
|
-
}, [setWriteCallback]);
|
|
363
|
+
}, [setWriteCallback, tryConsumePageNavigation]);
|
|
305
364
|
|
|
306
365
|
return (
|
|
307
366
|
<div
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type PageDirection = 'up' | 'down';
|
|
2
|
+
|
|
3
|
+
interface ScrollboxPageNavigationState {
|
|
4
|
+
direction: PageDirection;
|
|
5
|
+
scrollTop: number;
|
|
6
|
+
scrollHeight: number;
|
|
7
|
+
viewportHeight: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ViewportPageNavigationState {
|
|
11
|
+
direction: PageDirection;
|
|
12
|
+
viewportY: number;
|
|
13
|
+
baseY: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function shouldConsumePageNavigationInScrollbox({
|
|
17
|
+
direction,
|
|
18
|
+
scrollTop,
|
|
19
|
+
scrollHeight,
|
|
20
|
+
viewportHeight,
|
|
21
|
+
}: ScrollboxPageNavigationState): boolean {
|
|
22
|
+
const maxScrollTop = Math.max(0, scrollHeight - viewportHeight);
|
|
23
|
+
if (maxScrollTop <= 0) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (direction === 'up') {
|
|
28
|
+
return scrollTop > 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return scrollTop < maxScrollTop;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function canConsumePageNavigationInViewport({
|
|
35
|
+
direction,
|
|
36
|
+
viewportY,
|
|
37
|
+
baseY,
|
|
38
|
+
}: ViewportPageNavigationState): boolean {
|
|
39
|
+
if (direction === 'up') {
|
|
40
|
+
return viewportY < baseY;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return viewportY > 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getPageNavigationEscapeSequence(direction: PageDirection): string {
|
|
47
|
+
return direction === 'up' ? '\x1b[5~' : '\x1b[6~';
|
|
48
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
canConsumePageNavigationInViewport,
|
|
4
|
+
getPageNavigationEscapeSequence,
|
|
5
|
+
shouldConsumePageNavigationInScrollbox,
|
|
6
|
+
} from '../../components/session-terminal-page-navigation.js'
|
|
7
|
+
|
|
8
|
+
describe('session terminal page navigation helpers', () => {
|
|
9
|
+
it('consumes scrollbox page navigation only when scroll can move', () => {
|
|
10
|
+
expect(
|
|
11
|
+
shouldConsumePageNavigationInScrollbox({
|
|
12
|
+
direction: 'up',
|
|
13
|
+
scrollTop: 10,
|
|
14
|
+
scrollHeight: 100,
|
|
15
|
+
viewportHeight: 20,
|
|
16
|
+
})
|
|
17
|
+
).toBe(true)
|
|
18
|
+
|
|
19
|
+
expect(
|
|
20
|
+
shouldConsumePageNavigationInScrollbox({
|
|
21
|
+
direction: 'up',
|
|
22
|
+
scrollTop: 0,
|
|
23
|
+
scrollHeight: 100,
|
|
24
|
+
viewportHeight: 20,
|
|
25
|
+
})
|
|
26
|
+
).toBe(false)
|
|
27
|
+
|
|
28
|
+
expect(
|
|
29
|
+
shouldConsumePageNavigationInScrollbox({
|
|
30
|
+
direction: 'down',
|
|
31
|
+
scrollTop: 10,
|
|
32
|
+
scrollHeight: 100,
|
|
33
|
+
viewportHeight: 20,
|
|
34
|
+
})
|
|
35
|
+
).toBe(true)
|
|
36
|
+
|
|
37
|
+
expect(
|
|
38
|
+
shouldConsumePageNavigationInScrollbox({
|
|
39
|
+
direction: 'down',
|
|
40
|
+
scrollTop: 80,
|
|
41
|
+
scrollHeight: 100,
|
|
42
|
+
viewportHeight: 20,
|
|
43
|
+
})
|
|
44
|
+
).toBe(false)
|
|
45
|
+
|
|
46
|
+
expect(
|
|
47
|
+
shouldConsumePageNavigationInScrollbox({
|
|
48
|
+
direction: 'down',
|
|
49
|
+
scrollTop: 0,
|
|
50
|
+
scrollHeight: 20,
|
|
51
|
+
viewportHeight: 20,
|
|
52
|
+
})
|
|
53
|
+
).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('detects when viewport can consume page up/down', () => {
|
|
57
|
+
expect(
|
|
58
|
+
canConsumePageNavigationInViewport({
|
|
59
|
+
direction: 'up',
|
|
60
|
+
viewportY: 50,
|
|
61
|
+
baseY: 200,
|
|
62
|
+
})
|
|
63
|
+
).toBe(true)
|
|
64
|
+
|
|
65
|
+
expect(
|
|
66
|
+
canConsumePageNavigationInViewport({
|
|
67
|
+
direction: 'down',
|
|
68
|
+
viewportY: 1,
|
|
69
|
+
baseY: 200,
|
|
70
|
+
})
|
|
71
|
+
).toBe(true)
|
|
72
|
+
|
|
73
|
+
expect(
|
|
74
|
+
canConsumePageNavigationInViewport({
|
|
75
|
+
direction: 'down',
|
|
76
|
+
viewportY: 0,
|
|
77
|
+
baseY: 200,
|
|
78
|
+
})
|
|
79
|
+
).toBe(false)
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
canConsumePageNavigationInViewport({
|
|
83
|
+
direction: 'up',
|
|
84
|
+
viewportY: 200,
|
|
85
|
+
baseY: 200,
|
|
86
|
+
})
|
|
87
|
+
).toBe(false)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('returns standard escape sequences for page keys', () => {
|
|
91
|
+
expect(getPageNavigationEscapeSequence('up')).toBe('\x1b[5~')
|
|
92
|
+
expect(getPageNavigationEscapeSequence('down')).toBe('\x1b[6~')
|
|
93
|
+
})
|
|
94
|
+
})
|