feat(M3): SSH productization — Terminal/SSH split + Wave-style server monitoring

- Sidebar split into 本地终端 / SSH 服务器 sections + recent sessions quick reconnect
- SSH tabs carry server badge (icon + green edge); local tabs unchanged
- SSH session panels mount a collapsible live monitor panel (CPU/mem/disk/net/load/uptime)
- src/main/sysinfo.ts polls remote via ssh2 exec (/proc + df), 3s interval,
  error-tolerant (3 consecutive failures auto-stop), auto-stops on session close
- tests: sysinfo-e2e (loopback exec with canned /proc output) — ALL PASSED
This commit is contained in:
954801926@qq.com committed 2026-09-06 13:45:52 +08:00
1 parent f97edbb016
commit 06fa4ca023
19 files changed
+1632 -30

No files matched your search

+19 -1
View File
@@ -6,6 +6,11 @@ import type { SessionOpenOptions, HostKeyPromptEvent, SshConnection } from '../s
import { broadcast } from './broadcast'
import { connectSsh, type SshSessionHandle } from './ssh'
import type { HostKeyCheckResult } from './knownHosts'
import { configureSysinfo, registerSysinfoClient, stopPolling } from './sysinfo'
// Re-export so the session-layer loopback bundle (tests/*-e2e.mjs) can drive the
// M3 polling engine without importing src/main/sysinfo.ts separately.
export { startPolling, stopPolling } from './sysinfo'
/**
* Session routing table. A session is either a local pty or an established ssh
@@ -64,6 +69,14 @@ let runtimeDeps: SessionRuntimeDeps | undefined
/** Wire the real stores + renderer prompt. Called once during app startup. */
export function configureSessionRuntime(deps: SessionRuntimeDeps): void {
runtimeDeps = deps
configureSysinfo({
broadcast: (channel, ...args) => deps.broadcast(channel, ...args)
})
registerSysinfoClient((id) => {
const session = sessions.get(id)
if (session?.kind === 'ssh') return session.ssh.client
return undefined
})
}
function defaultShell(): string {
@@ -156,6 +169,7 @@ export async function openSession(opts: SessionOpenOptions): Promise<{ id: strin
handle.stream.on('close', () => {
try {
stopPolling(handle.id)
sessions.delete(handle.id)
deps.broadcast(Ipc.PTY_EXIT, { id: handle.id, exitCode: 0 })
} catch {
@@ -190,6 +204,7 @@ export function resizePty(id: string, cols: number, rows: number): void {
}
export function killPty(id: string): void {
stopPolling(id)
replayBuffers.delete(id)
const session = sessions.get(id)
if (!session) return
@@ -215,7 +230,10 @@ export function killPty(id: string): void {
}
export function killAllPtys(): void {
for (const session of sessions.values()) {
for (const id of sessions.keys()) {
stopPolling(id)
const session = sessions.get(id)
if (!session) continue
if (session.kind === 'ssh') {
try {
session.ssh.stream.close()