feat(M5): command history/library + completion + session logs

- main/commands.ts: JSON-backed history (500 cap, dedupe), command library
  CRUD, per-session plain-text output logs (start/stop/index/open dir)
- pty data path feeds the session logger via injected hooks
- TerminalView: input line capture -> history recording, prefix completion
  popup (history + library, Tab/Arrows/Esc), session log toggle buttons
- Sidebar: 命令 section — history (click to run, clear) and library
  (add/delete, click to run into active session)
- tests: commands-store self-test ALL PASSED; full suite green
This commit is contained in:
954801926@qq.com committed 2026-09-06 17:28:15 +08:00
1 parent 7977c264be
commit 7467433a61
12 files changed
+1593 -3

No files matched your search

+38
View File
@@ -13,6 +13,38 @@ import { registerSftpClientProvider, closeSftp } from './sftp'
// M3 polling engine without importing src/main/sysinfo.ts separately.
export { startPolling, stopPolling } from './sysinfo'
/**
* M5 command-store / log-service hooks (injected once by ipc.ts). Mirrors the
* sysinfo injection pattern so pty.ts routes PTY data into the session log
* without importing the store directly. `log` runs on every onData chunk (the
* logger itself decides whether a session is actively logging), `stop` runs on
* session close / kill to finalize the log file.
*/
type DataLogger = (id: string, data: string) => void
let dataLogger: DataLogger | undefined
let logStopper: (id: string) => void | undefined
export function registerLogHooks(log: DataLogger, stop: (id: string) => void): void {
dataLogger = log
logStopper = stop
}
function safeLog(id: string, data: string): void {
try {
dataLogger?.(id, data)
} catch {
// never crash the event loop
}
}
function safeStopLog(id: string): void {
try {
logStopper?.(id)
} catch {
// never crash the event loop
}
}
/**
* Session routing table. A session is either a local pty or an established ssh
* shell; the generic PTY_* (data plane) channels address both. PTY_DATA /
@@ -115,6 +147,7 @@ export function createPty(opts: PtyCreateOptions = {}): PtyCreateResult {
pty.onData((data) => {
try {
appendReplay(id, data)
safeLog(id, data)
broadcast(Ipc.PTY_DATA, { id, data })
} catch {
// never crash the event loop
@@ -124,6 +157,7 @@ export function createPty(opts: PtyCreateOptions = {}): PtyCreateResult {
pty.onExit(({ exitCode }) => {
try {
sessions.delete(id)
safeStopLog(id)
broadcast(Ipc.PTY_EXIT, { id, exitCode })
} catch {
// never crash the event loop
@@ -173,6 +207,7 @@ export async function openSession(opts: SessionOpenOptions): Promise<{ id: strin
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
@@ -183,6 +218,7 @@ export async function openSession(opts: SessionOpenOptions): Promise<{ id: strin
try {
stopPolling(handle.id)
closeSftp(handle.id)
safeStopLog(handle.id)
sessions.delete(handle.id)
deps.broadcast(Ipc.PTY_EXIT, { id: handle.id, exitCode: 0 })
} catch {
@@ -220,6 +256,7 @@ export function killPty(id: string): void {
stopPolling(id)
closeSftp(id)
closeSftp(id)
safeStopLog(id)
replayBuffers.delete(id)
const session = sessions.get(id)
if (!session) return
@@ -248,6 +285,7 @@ export function killAllPtys(): void {
for (const id of sessions.keys()) {
stopPolling(id)
closeSftp(id)
safeStopLog(id)
const session = sessions.get(id)
if (!session) continue
if (session.kind === 'ssh') {