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:
1 parent
7977c264be
commit
7467433a61
12 files changed
+1593
-3
No files matched your search
@@ -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') {
|
||||
|
||||
Reference in new issue
Block a user