zerogterm 0.4.0-alpha.1 → 0.5.0-alpha.1
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 +50 -10
- package/dist/main/main/main.js +11 -2
- package/dist/main/main/preload.cjs +1 -1
- package/dist/main/main/session-service.js +61 -10
- package/dist/renderer/assets/index-BJsoL2QL.js +17 -0
- package/dist/renderer/assets/index-BPfYUrvm.css +1 -0
- package/dist/renderer/index.html +2 -2
- package/package.json +1 -1
- package/dist/renderer/assets/index-DSL4JtQn.css +0 -1
- package/dist/renderer/assets/index-x_BAzbUO.js +0 -17
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ ZeroG Terminal is an alpha project, but it is already useful as a multi-session
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- Multi-pane workspaces with stack, vertical split, horizontal split, and four-pane grid layouts.
|
|
10
|
+
- Draggable dividers between panes and beside the sidebar, so a split does not have to be an even one. Sizes are remembered between launches.
|
|
10
11
|
- Maximize a focused pane and cycle between sessions without losing the other panes.
|
|
11
12
|
- Local sessions powered by Bash, PowerShell, WSL, and other supported shell backends; persistent sessions use `screen` where available, with a process-only fallback when it is unavailable.
|
|
12
13
|
- SSH sessions for hosts, `user@host`, and `user@host:port` targets.
|
|
@@ -18,6 +19,7 @@ ZeroG Terminal is an alpha project, but it is already useful as a multi-session
|
|
|
18
19
|
- Session overview, collapsible sidebar, keyboard shortcuts, and light/dark themes.
|
|
19
20
|
- xterm.js terminal rendering with scrollback preservation while changing layouts.
|
|
20
21
|
- Local voice input, either with Whisper ONNX inside the app through Transformers.js or through a transcription server on this machine; transcribed text is typed into the selected terminal without automatic execution.
|
|
22
|
+
- A per-pane proceed button that sends a configurable phrase — `OK, proceed` by default — for waving an agent on without typing the same reply again.
|
|
21
23
|
- A settings panel for appearance, terminal behaviour, session defaults, and speech recognition, including a built-in recognition test.
|
|
22
24
|
- AI command suggestion and approval UI, keeping command execution explicit.
|
|
23
25
|
- Sandboxed Electron renderer, context isolation, disabled Node integration, and a narrow typed preload API.
|
|
@@ -29,7 +31,7 @@ See the project walkthrough on [YouTube](https://youtu.be/4aJZCxLHD14).
|
|
|
29
31
|
|
|
30
32
|
## Release status
|
|
31
33
|
|
|
32
|
-
ZeroG Terminal is currently a public alpha. The current release is `0.
|
|
34
|
+
ZeroG Terminal is currently a public alpha. The current release is `0.5.0-alpha.1`; the version history is tracked in [versions.txt](versions.txt).
|
|
33
35
|
|
|
34
36
|
The npm package contains the built Electron application and project documentation. It is intended for early adopters and testing rather than production use.
|
|
35
37
|
|
|
@@ -51,6 +53,17 @@ sequence — this is how TUI tools such as CLI coding agents, tmux and Neovim pu
|
|
|
51
53
|
text on the clipboard, including over SSH. Reading the clipboard through OSC 52
|
|
52
54
|
is refused, so a program on a remote host cannot see what you last copied.
|
|
53
55
|
|
|
56
|
+
## Resizing panes and the sidebar
|
|
57
|
+
|
|
58
|
+
Drag the line between two panes, or the sidebar's right edge, to change how the
|
|
59
|
+
space is shared. Sizes are remembered between launches and clamped so that no
|
|
60
|
+
pane can be dragged down to nothing.
|
|
61
|
+
|
|
62
|
+
The dividers take keyboard focus as well: the arrow keys nudge one two percent at
|
|
63
|
+
a time, and Enter or a double-click puts it back in the middle. One divider
|
|
64
|
+
position is shared by every layout, so a split you set up in the vertical split
|
|
65
|
+
is the same split you get in the four-pane grid.
|
|
66
|
+
|
|
54
67
|
## Settings
|
|
55
68
|
|
|
56
69
|
Settings open from the gear at the bottom of the left rail, the avatar in the
|
|
@@ -64,9 +77,23 @@ between launches; each page can be reset on its own.
|
|
|
64
77
|
layout to start in, and whether the sidebar starts collapsed.
|
|
65
78
|
- **AI & voice** — whether AI suggestions need approval before running, and
|
|
66
79
|
whether a transcript is typed straight into the pane or shown for review
|
|
67
|
-
first. Neither option presses Enter for you.
|
|
80
|
+
first. Neither option presses Enter for you. Also the phrase the pane's
|
|
81
|
+
proceed button sends, described below.
|
|
68
82
|
- **Speech recognition** — engine, model and tuning, described below.
|
|
69
83
|
|
|
84
|
+
### Proceed button
|
|
85
|
+
|
|
86
|
+
Each pane's title bar carries a tick beside the microphone. Clicking it sends
|
|
87
|
+
`OK, proceed` and presses Enter — for the common case of an AI coding agent
|
|
88
|
+
pausing to ask whether it should carry on. The phrase is editable under
|
|
89
|
+
**Settings ▸ AI & voice**, so an agent that responds better to different wording
|
|
90
|
+
can have it.
|
|
91
|
+
|
|
92
|
+
This is the one control that presses Enter for you; voice transcripts and AI
|
|
93
|
+
suggestions deliberately do not. It sends to the pane it belongs to, so a pane
|
|
94
|
+
sitting at a shell prompt rather than in an agent will simply try to run the
|
|
95
|
+
phrase as a command.
|
|
96
|
+
|
|
70
97
|
### Speech recognition
|
|
71
98
|
|
|
72
99
|
Two engines are available.
|
|
@@ -119,28 +146,41 @@ npm run build
|
|
|
119
146
|
npm start
|
|
120
147
|
```
|
|
121
148
|
|
|
122
|
-
|
|
149
|
+
Node.js is the only prerequisite every host needs. The scripts above run on Linux
|
|
150
|
+
and Windows; npm runs them through `cmd.exe` on Windows, so no POSIX shell is
|
|
151
|
+
required whichever shell you start them from.
|
|
152
|
+
|
|
153
|
+
`node-pty` provides terminal I/O on every platform. It ships prebuilt binaries
|
|
154
|
+
for Windows and macOS, so a C/C++ toolchain is a Linux requirement rather than a
|
|
155
|
+
general one — there, `npm install` compiles it.
|
|
123
156
|
|
|
124
|
-
|
|
157
|
+
### Linux
|
|
125
158
|
|
|
126
159
|
```bash
|
|
127
160
|
sudo dnf install screen make gcc-c++ python3
|
|
128
161
|
```
|
|
129
162
|
|
|
130
|
-
|
|
163
|
+
`make`, `gcc-c++` and `python3` build `node-pty`. `screen` is optional but worth
|
|
164
|
+
having: with it, local sessions are persistent and rediscovered after relaunch.
|
|
165
|
+
Without it, ZeroG falls back to a direct PTY on the chosen shell and labels the
|
|
166
|
+
session process-only; that shell is lost when the application exits.
|
|
131
167
|
|
|
132
|
-
|
|
168
|
+
### Windows
|
|
133
169
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
170
|
+
No tooling beyond Node.js. The new-terminal dialog offers the shells it finds on
|
|
171
|
+
PATH — Windows PowerShell, PowerShell 7, Command Prompt, WSL (with a distribution
|
|
172
|
+
picker), and Git Bash where Git for Windows is installed.
|
|
173
|
+
|
|
174
|
+
`screen` does not exist on Windows, so local sessions are always process-only and
|
|
175
|
+
do not survive app exit. Remote SSH sessions are unaffected by the local shell
|
|
176
|
+
backend, and a remote host with `screen` still gives persistent sessions there.
|
|
137
177
|
|
|
138
178
|
## Verification
|
|
139
179
|
|
|
140
180
|
The current main branch has the following local verification coverage:
|
|
141
181
|
|
|
142
182
|
- `npm run typecheck`: passes.
|
|
143
|
-
- `npm test`: passes (
|
|
183
|
+
- `npm test`: passes (167 tests covering session service behaviour and PTY sizing, shell discovery, SSH inventory and argument validation, remote-screen parsing and prompt readiness, session history, the session dialog, settings, terminal clipboard and OSC 52 handling, dialog dismissal, and the speech and voice helpers; one further test needs a real `screen` and is opt-in through `ZEROG_LIVE_SCREEN=1`).
|
|
144
184
|
- `npm run build`: passes and writes `dist/main` plus `dist/renderer`.
|
|
145
185
|
- `npm audit --omit=dev`: production dependency auditing is part of the project quality checks.
|
|
146
186
|
|
package/dist/main/main/main.js
CHANGED
|
@@ -10,6 +10,15 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
|
10
10
|
const history = new SessionHistoryStore({ filePath: defaultHistoryPath(app.getPath('userData')) });
|
|
11
11
|
const service = new ScreenService({ onEvent: (event, session, available) => { void history.record(event, session, available); } });
|
|
12
12
|
let win;
|
|
13
|
+
/** A pane's measured size, as it arrives from the renderer. */
|
|
14
|
+
function parsePtySize(value) {
|
|
15
|
+
if (!value || typeof value !== 'object')
|
|
16
|
+
return undefined;
|
|
17
|
+
const { cols, rows } = value;
|
|
18
|
+
if (!Number.isInteger(cols) || !Number.isInteger(rows))
|
|
19
|
+
return undefined;
|
|
20
|
+
return { cols: cols, rows: rows };
|
|
21
|
+
}
|
|
13
22
|
// GPU is unstable under Toolbox/Wayland on this host; allow override.
|
|
14
23
|
if (process.env.ZEROG_ENABLE_GPU !== '1') {
|
|
15
24
|
app.disableHardwareAcceleration();
|
|
@@ -108,10 +117,10 @@ ipcMain.handle('screens:attachRemote', (_event, input, screenName) => {
|
|
|
108
117
|
throw new Error('screenName is required');
|
|
109
118
|
return buildRemoteScreenAttachArgs(connection, screenName);
|
|
110
119
|
});
|
|
111
|
-
ipcMain.handle('sessions:attach', (_event, id) => {
|
|
120
|
+
ipcMain.handle('sessions:attach', (_event, id, size) => {
|
|
112
121
|
if (typeof id !== 'string' || !id)
|
|
113
122
|
throw new Error('attachSession requires a session id');
|
|
114
|
-
return service.attach(id, (data) => win?.webContents.send('terminal:data', id, data), (message) => win?.webContents.send('terminal:status', id, message));
|
|
123
|
+
return service.attach(id, (data) => win?.webContents.send('terminal:data', id, data), (message) => win?.webContents.send('terminal:status', id, message), parsePtySize(size));
|
|
115
124
|
});
|
|
116
125
|
ipcMain.handle('sessions:close', (_event, id) => {
|
|
117
126
|
if (typeof id !== 'string' || !id)
|
|
@@ -11,7 +11,7 @@ const api = {
|
|
|
11
11
|
listKnownConnections: () => ipcRenderer.invoke('connections:listKnown'),
|
|
12
12
|
discoverRemoteScreens: (connection) => ipcRenderer.invoke('screens:discoverRemote', connection),
|
|
13
13
|
buildRemoteScreenAttach: (connection, screenName) => ipcRenderer.invoke('screens:attachRemote', connection, screenName),
|
|
14
|
-
attachSession: (id) => ipcRenderer.invoke('sessions:attach', id),
|
|
14
|
+
attachSession: (id, size) => ipcRenderer.invoke('sessions:attach', id, size),
|
|
15
15
|
closeSession: (id) => ipcRenderer.invoke('sessions:close', id),
|
|
16
16
|
write: (sessionId, data) => ipcRenderer.send('terminal:write', sessionId, data),
|
|
17
17
|
resize: (sessionId, cols, rows) => ipcRenderer.send('terminal:resize', sessionId, cols, rows),
|
|
@@ -14,6 +14,26 @@ const NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,48}$/;
|
|
|
14
14
|
* -chosen config file (hence ProxyCommand) without any shell involvement.
|
|
15
15
|
*/
|
|
16
16
|
const SSH_TARGET = /^(?:([A-Za-z0-9][A-Za-z0-9._-]*)@)?([A-Za-z0-9][A-Za-z0-9.-]*)(?::(\d{1,5}))?$/;
|
|
17
|
+
/**
|
|
18
|
+
* Size a pty starts at when the pane could not be measured — a placeholder,
|
|
19
|
+
* not a preference. Panes pass their real size through attach().
|
|
20
|
+
*/
|
|
21
|
+
const FALLBACK_SIZE = { cols: 120, rows: 32 };
|
|
22
|
+
/**
|
|
23
|
+
* A size a pty can actually be given. Sizes cross the IPC boundary from the
|
|
24
|
+
* renderer, and a zero or fractional one is rejected by ConPTY (and makes
|
|
25
|
+
* every full-screen program wrap in the wrong place on Unix).
|
|
26
|
+
*/
|
|
27
|
+
function usableSize(size) {
|
|
28
|
+
if (!size)
|
|
29
|
+
return undefined;
|
|
30
|
+
const { cols, rows } = size;
|
|
31
|
+
if (!Number.isInteger(cols) || !Number.isInteger(rows))
|
|
32
|
+
return undefined;
|
|
33
|
+
if (cols < 2 || rows < 2 || cols > 2000 || rows > 2000)
|
|
34
|
+
return undefined;
|
|
35
|
+
return { cols, rows };
|
|
36
|
+
}
|
|
17
37
|
export function validateSessionName(name) {
|
|
18
38
|
const value = name.trim();
|
|
19
39
|
if (!NAME.test(value)) {
|
|
@@ -176,10 +196,23 @@ export class ScreenService {
|
|
|
176
196
|
this.onEvent?.('created', session, true);
|
|
177
197
|
return session;
|
|
178
198
|
}
|
|
179
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Attach a pane to a session, starting its pty if it does not have one.
|
|
201
|
+
*
|
|
202
|
+
* `size` is the pane's own measurement. A pty spawned at some stock size and
|
|
203
|
+
* resized a moment later shows its first frame at the wrong width, and a
|
|
204
|
+
* full-screen program redrawing over that frame leaves pieces of it behind —
|
|
205
|
+
* so the shell is started at the size it will actually be displayed at.
|
|
206
|
+
*/
|
|
207
|
+
attach(id, onData, onExit, size) {
|
|
180
208
|
const existing = this.getSession(id);
|
|
181
|
-
if (this.ptys.has(id))
|
|
209
|
+
if (this.ptys.has(id)) {
|
|
210
|
+
// Re-attaching pane may be a different size than the one that started it.
|
|
211
|
+
const measured = usableSize(size);
|
|
212
|
+
if (measured)
|
|
213
|
+
this.resize(id, measured.cols, measured.rows);
|
|
182
214
|
return { ...existing, status: 'connected' };
|
|
215
|
+
}
|
|
183
216
|
if (id.startsWith('local:')) {
|
|
184
217
|
const fallback = this.fallbackLocalSessions.get(id);
|
|
185
218
|
if (fallback) {
|
|
@@ -188,14 +221,14 @@ export class ScreenService {
|
|
|
188
221
|
const shell = isLocalShellBackend(fallback.backend)
|
|
189
222
|
? resolveShellBackend(fallback.backend, fallback.wslDistribution)
|
|
190
223
|
: defaultShellBackend();
|
|
191
|
-
this.spawnCommand(id, shell.executable, shell.args, onData, onExit, fallback.cwd);
|
|
224
|
+
this.spawnCommand(id, shell.executable, shell.args, onData, onExit, fallback.cwd, size);
|
|
192
225
|
this.onEvent?.('attached', fallback, true);
|
|
193
226
|
return { ...fallback };
|
|
194
227
|
}
|
|
195
228
|
// Session ids cross the IPC boundary from the renderer, so re-validate
|
|
196
229
|
// rather than trusting that createLocal produced this one.
|
|
197
230
|
const name = validateSessionName(id.slice('local:'.length));
|
|
198
|
-
this.spawnCommand(id, 'screen', ['-x', name], onData, onExit);
|
|
231
|
+
this.spawnCommand(id, 'screen', ['-x', name], onData, onExit, homedir(), size);
|
|
199
232
|
this.onEvent?.('attached', existing, true);
|
|
200
233
|
return { ...existing, status: 'connected', persistence: 'screen' };
|
|
201
234
|
}
|
|
@@ -207,7 +240,7 @@ export class ScreenService {
|
|
|
207
240
|
session.status = 'connected';
|
|
208
241
|
session.lastSeen = new Date().toISOString();
|
|
209
242
|
try {
|
|
210
|
-
this.spawnCommand(id, 'ssh', args, onData, onExit);
|
|
243
|
+
this.spawnCommand(id, 'ssh', args, onData, onExit, homedir(), size);
|
|
211
244
|
}
|
|
212
245
|
catch (error) {
|
|
213
246
|
session.status = 'error';
|
|
@@ -239,11 +272,12 @@ export class ScreenService {
|
|
|
239
272
|
throw new Error(`Unknown session: ${id}`);
|
|
240
273
|
return session;
|
|
241
274
|
}
|
|
242
|
-
spawnCommand(sessionId, file, args, onData, onExit, cwd = homedir()) {
|
|
275
|
+
spawnCommand(sessionId, file, args, onData, onExit, cwd = homedir(), size) {
|
|
276
|
+
const { cols, rows } = usableSize(size) ?? FALLBACK_SIZE;
|
|
243
277
|
const proc = this.spawnPty(file, args, {
|
|
244
278
|
name: 'xterm-256color',
|
|
245
|
-
cols
|
|
246
|
-
rows
|
|
279
|
+
cols,
|
|
280
|
+
rows,
|
|
247
281
|
cwd,
|
|
248
282
|
env: process.env
|
|
249
283
|
});
|
|
@@ -259,7 +293,7 @@ export class ScreenService {
|
|
|
259
293
|
}
|
|
260
294
|
}
|
|
261
295
|
};
|
|
262
|
-
this.ptys.set(sessionId, { handle, kind: sessionId.startsWith('ssh:') ? 'ssh' : 'local' });
|
|
296
|
+
this.ptys.set(sessionId, { handle, kind: sessionId.startsWith('ssh:') ? 'ssh' : 'local', cols, rows });
|
|
263
297
|
proc.onData(onData);
|
|
264
298
|
proc.onExit(() => {
|
|
265
299
|
this.ptys.delete(sessionId);
|
|
@@ -281,8 +315,25 @@ export class ScreenService {
|
|
|
281
315
|
write(sessionId, data) {
|
|
282
316
|
this.ptys.get(sessionId)?.handle.write(data);
|
|
283
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Tell a session's pty how big its pane is now.
|
|
320
|
+
*
|
|
321
|
+
* A resize is not free and not invisible: it makes ConPTY reflow and re-emit
|
|
322
|
+
* its screen, and every full-screen program redraw on SIGWINCH. Panes refit
|
|
323
|
+
* on layout, font and status changes, which mostly produce the size the pty
|
|
324
|
+
* already has, so a repeat of the current size is dropped rather than paid
|
|
325
|
+
* for in a torn redraw.
|
|
326
|
+
*/
|
|
284
327
|
resize(sessionId, cols, rows) {
|
|
285
|
-
this.ptys.get(sessionId)
|
|
328
|
+
const pty = this.ptys.get(sessionId);
|
|
329
|
+
if (!pty)
|
|
330
|
+
return;
|
|
331
|
+
const size = usableSize({ cols, rows });
|
|
332
|
+
if (!size || (size.cols === pty.cols && size.rows === pty.rows))
|
|
333
|
+
return;
|
|
334
|
+
pty.cols = size.cols;
|
|
335
|
+
pty.rows = size.rows;
|
|
336
|
+
pty.handle.resize(size.cols, size.rows);
|
|
286
337
|
}
|
|
287
338
|
detach(sessionId) {
|
|
288
339
|
this.ptys.get(sessionId)?.handle.kill();
|