feat(M6): broadcast input + main-process ZMODEM + quick input panel + shortcuts
- broadcast: session target picker popover, fan-out write path, <2 targets auto-disable, per-tab target dots - zmodem: main-process engine over ssh sessions (rz send / sz receive), progress reuses transfer panel, offer 120s / stall 90s watchdogs, e2e test with real zmodem.js peer (both directions) - quick input panel: bottom-right overlay (command line + auto-\r + library + recent), broadcast-aware send - shortcuts: Ctrl+=/-/0 font size (persisted), globalShowHide accelerator via globalShortcut, Ctrl+PgUp/PgDn terminal cycling - contracts: ZMODEM_OFFER/RESPOND/DONE channels, SystemSettings.globalShowHide, TransferKind zmodem-upload/download - review fixes: xterm-helper-textarea font hotkey pass-through, zmodem error-vs-cancelled progress state, offer timer clear, receive stream destroy
This commit is contained in:
1 parent
0953ac4f41
commit
0663e5affa
30 files changed
+9028
-37
No files matched your search
+36
-9
@@ -8,6 +8,7 @@ import { connectSsh, type SshSessionHandle } from './ssh'
|
||||
import type { HostKeyCheckResult } from './knownHosts'
|
||||
import { configureSysinfo, registerSysinfoClient, stopPolling } from './sysinfo'
|
||||
import { registerSftpClientProvider, closeSftp } from './sftp'
|
||||
import { attachZmodem, detachZmodem, feedZmodem, isZmodemActive } from './zmodem'
|
||||
|
||||
// Re-export so the session-layer loopback bundle (tests/*-e2e.mjs) can drive the
|
||||
// M3 polling engine without importing src/main/sysinfo.ts separately.
|
||||
@@ -203,21 +204,43 @@ export async function openSession(opts: SessionOpenOptions): Promise<{ id: strin
|
||||
})
|
||||
sessions.set(handle.id, { kind: 'ssh', ssh: handle })
|
||||
|
||||
handle.stream.on('data', (data: Buffer) => {
|
||||
try {
|
||||
const text = data.toString('utf8')
|
||||
appendReplay(handle.id, text)
|
||||
safeLog(handle.id, text)
|
||||
deps.broadcast(Ipc.PTY_DATA, { id: handle.id, data: text })
|
||||
} catch {
|
||||
// never crash the event loop
|
||||
// ZMODEM (M6): attach the in-process engine to the raw ssh stream. Only ssh
|
||||
// sessions are supported -- node-pty/Windows ConPTY hands out UTF-8 strings
|
||||
// only and would corrupt binary frames. The sentry inspects every data chunk:
|
||||
// non-ZMODEM traffic is routed back through toTerminal below; once a ZMODEM
|
||||
// header is spotted the transfer suppresses the terminal data plane (and
|
||||
// writePty drops user keystrokes while isZmodemActive is true).
|
||||
attachZmodem(handle.id, {
|
||||
broadcast: (channel, ...args) => deps.broadcast(channel, ...args),
|
||||
toTerminal: (id, text) => {
|
||||
appendReplay(id, text)
|
||||
safeLog(id, text)
|
||||
deps.broadcast(Ipc.PTY_DATA, { id, data: text })
|
||||
},
|
||||
writeStream: (id, data) => {
|
||||
const s = sessions.get(id)
|
||||
if (s?.kind === 'ssh') {
|
||||
try {
|
||||
s.ssh.stream.write(data)
|
||||
} catch {
|
||||
// stream may already be dead
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
handle.stream.on('data', (data: Buffer) => {
|
||||
// Route through the ZMODEM sentry. It forwards non-ZMODEM bytes to
|
||||
// deps.toTerminal and absorbs the transfer untouched; while a transfer is
|
||||
// active the engine suppresses the normal data plane entirely.
|
||||
feedZmodem(handle.id, data)
|
||||
})
|
||||
|
||||
handle.stream.on('close', () => {
|
||||
try {
|
||||
stopPolling(handle.id)
|
||||
closeSftp(handle.id)
|
||||
detachZmodem(handle.id)
|
||||
safeStopLog(handle.id)
|
||||
sessions.delete(handle.id)
|
||||
deps.broadcast(Ipc.PTY_EXIT, { id: handle.id, exitCode: 0 })
|
||||
@@ -230,6 +253,9 @@ export async function openSession(opts: SessionOpenOptions): Promise<{ id: strin
|
||||
}
|
||||
|
||||
export function writePty(id: string, data: string): void {
|
||||
// While a ZMODEM transfer is active the stream carries binary frames; user
|
||||
// keystrokes must be dropped (they would corrupt the transfer).
|
||||
if (isZmodemActive(id)) return
|
||||
const session = sessions.get(id)
|
||||
if (!session) return
|
||||
try {
|
||||
@@ -255,7 +281,7 @@ export function resizePty(id: string, cols: number, rows: number): void {
|
||||
export function killPty(id: string): void {
|
||||
stopPolling(id)
|
||||
closeSftp(id)
|
||||
closeSftp(id)
|
||||
detachZmodem(id)
|
||||
safeStopLog(id)
|
||||
replayBuffers.delete(id)
|
||||
const session = sessions.get(id)
|
||||
@@ -285,6 +311,7 @@ export function killAllPtys(): void {
|
||||
for (const id of sessions.keys()) {
|
||||
stopPolling(id)
|
||||
closeSftp(id)
|
||||
detachZmodem(id)
|
||||
safeStopLog(id)
|
||||
const session = sessions.get(id)
|
||||
if (!session) continue
|
||||
|
||||
Reference in new issue
Block a user