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

+46
View File
@@ -9,6 +9,8 @@ import type {
SessionOpenOptions,
SshConnectionInput
} from '../shared/connections'
import type { SysinfoMeta, SysinfoSample } from '../shared/sysinfo'
import type { TransferProgressEvent } from '../shared/sftp'
const api: AppApi = {
appInfo: () => ipcRenderer.invoke(Ipc.APP_INFO),
@@ -31,6 +33,50 @@ const api: AppApi = {
return () => ipcRenderer.removeListener(Ipc.PTY_EXIT, listener)
},
sysinfoStart: (id: string) => ipcRenderer.send(Ipc.SYSINFO_START, id),
sysinfoStop: (id: string) => ipcRenderer.send(Ipc.SYSINFO_STOP, id),
onSysinfoMeta: (cb: (e: { id: string; meta: SysinfoMeta }) => void) => {
const listener = (_: unknown, e: { id: string; meta: SysinfoMeta }): void => cb(e)
ipcRenderer.on(Ipc.SYSINFO_META, listener)
return () => ipcRenderer.removeListener(Ipc.SYSINFO_META, listener)
},
onSysinfoSample: (cb: (e: { id: string; sample: SysinfoSample }) => void) => {
const listener = (_: unknown, e: { id: string; sample: SysinfoSample }): void => cb(e)
ipcRenderer.on(Ipc.SYSINFO_SAMPLE, listener)
return () => ipcRenderer.removeListener(Ipc.SYSINFO_SAMPLE, listener)
},
listRemote: (sessionId: string, dir: string) => ipcRenderer.invoke(Ipc.SFTP_LIST, sessionId, dir),
mkdirRemote: (sessionId: string, dir: string, name: string) =>
ipcRenderer.invoke(Ipc.SFTP_MKDIR, sessionId, dir, name),
renameRemote: (sessionId: string, from: string, to: string) =>
ipcRenderer.invoke(Ipc.SFTP_RENAME, sessionId, from, to),
deleteRemote: (sessionId: string, paths: string[]) =>
ipcRenderer.invoke(Ipc.SFTP_DELETE, sessionId, paths),
uploadRemote: (sessionId: string, localPaths: string[], remoteDir: string) =>
ipcRenderer.invoke(Ipc.SFTP_UPLOAD, sessionId, localPaths, remoteDir),
downloadRemote: (sessionId: string, remotePaths: string[], localDir: string) =>
ipcRenderer.invoke(Ipc.SFTP_DOWNLOAD, sessionId, remotePaths, localDir),
cancelTransfer: (transferId: string) => ipcRenderer.send(Ipc.TRANSFER_CANCEL, transferId),
onTransferProgress: (cb: (e: TransferProgressEvent) => void) => {
const listener = (_: unknown, e: TransferProgressEvent): void => cb(e)
ipcRenderer.on(Ipc.TRANSFER_PROGRESS, listener)
return () => ipcRenderer.removeListener(Ipc.TRANSFER_PROGRESS, listener)
},
pickFiles: () => ipcRenderer.invoke(Ipc.DIALOG_PICK_FILES),
pickDirectory: () => ipcRenderer.invoke(Ipc.DIALOG_PICK_DIR),
recordCommand: (cmd: string) => ipcRenderer.invoke(Ipc.COMMANDS_RECORD, cmd),
listHistory: () => ipcRenderer.invoke(Ipc.COMMANDS_HISTORY_LIST),
clearHistory: () => ipcRenderer.invoke(Ipc.COMMANDS_HISTORY_CLEAR),
listLibrary: () => ipcRenderer.invoke(Ipc.COMMANDS_LIBRARY_LIST),
saveLibraryItem: (item) => ipcRenderer.invoke(Ipc.COMMANDS_LIBRARY_SAVE, item),
deleteLibraryItem: (id: string) => ipcRenderer.invoke(Ipc.COMMANDS_LIBRARY_DELETE, id),
logStart: (sessionId: string) => ipcRenderer.invoke(Ipc.LOG_START, sessionId),
logStop: (sessionId: string) => ipcRenderer.invoke(Ipc.LOG_STOP, sessionId),
listSessionLogs: () => ipcRenderer.invoke(Ipc.LOG_LIST),
openLogsDir: () => ipcRenderer.send(Ipc.LOG_OPEN_DIR),
listConnections: () => ipcRenderer.invoke(Ipc.CONNECTIONS_LIST),
saveConnection: (input: SshConnectionInput) => ipcRenderer.invoke(Ipc.CONNECTIONS_SAVE, input),
deleteConnection: (id: string) => ipcRenderer.invoke(Ipc.CONNECTIONS_DELETE, id),