Files
OpenTerminal/src/main/store.ts
T
Bill beeb32a76b fix(session): keep panel ids distinct from session ids so restore rebinds panes
面板 id 曾直接复用 pty 会话 id(`addPanel({ id: sessionId })`),而
`updateParameters` 只能换会话、不能改面板 id。于是恢复时面板 id 指向一个
已不存在的会话:分屏少一个、剩下的 pane 背后没有 pty,界面看起来是空白,
也无法输入。

- 新增 `panelId()`,面板 id 与会话 id 彻底分离,并用于所有建面板处
  (新建终端、SSH 连接、分屏复制)
- 新增 `breakIdCoincidence()`:旧快照仍带 `panelId === sessionId`,
  在 `fromJSON` 之前重写 grid/panels/views/activeView 中的 id,
  返回 old→new 映射
- `rebindSessionPanels` 用该映射回查快照,恢复每个 pane 自己的目录
  (否则新 id 查不到记录,cwd 会静默退回 home)

验证(重启恢复 E2E,legacy 快照 + 2 分屏):
  2/2 pane 挂载、2 个独立存活会话、输入可回环、
  cwd 分别回到 D:/AIGC/OpenTerminal 与 C:/Windows、回写快照无 id 冲突;
  restoreSession=false 时正确忽略快照只建 1 个终端。
2026-09-14 11:29:56 +08:00

64 lines
2.3 KiB
TypeScript

import { mkdirSync, readFileSync, renameSync, writeFileSync, unlinkSync } from 'fs'
import { dirname, join } from 'path'
/**
* Small shared persistence helper for the app's JSON stores (settings,
* connections, known hosts, commands, layouts, the session snapshot).
*
* Why not a database: these are a handful of KB written by a single process at
* human speed, and every native DB binding (better-sqlite3 et al) would add a
* per-platform rebuild + packaging burden to a cross-platform app. What the
* stores actually needed was the *reliability* a DB implies — that is what this
* gives: an atomic write (write to a sibling temp file, then rename) so a crash
* or a full disk can never leave a half-written file behind, and a capped size
* so a runaway value cannot fill the disk either.
*
* If a dataset ever outgrows this (command history in the tens of thousands, or
* full-text search over session logs), swap the implementation here — callers
* only ever see `readJson`/`writeJson`.
*/
/** Refuse to load a file larger than this (guards against a runaway store). */
const MAX_BYTES = 8 * 1024 * 1024
const tmpPath = (file: string): string => `${file}.tmp`
export function readJson<T = unknown>(file: string): T | null {
try {
const raw = readFileSync(file, 'utf8')
if (raw.length > MAX_BYTES) return null
return JSON.parse(raw) as T
} catch {
// Missing, unreadable or corrupt: callers fall back to their defaults.
// A corrupt file is left in place (not deleted) so it can be inspected.
return null
}
}
export function writeJson(file: string, value: unknown): void {
mkdirSync(dirname(file), { recursive: true })
const tmp = tmpPath(file)
try {
writeFileSync(tmp, JSON.stringify(value, null, 2), 'utf8')
renameSync(tmp, file)
} catch (err) {
// Clean up the temp file so a failed write does not leave litter behind.
try {
unlinkSync(tmp)
} catch {
/* nothing to clean */
}
throw err
}
}
/** Convenience for stores whose file lives under a directory. */
export function jsonFileIn(dir: string, name: string): string {
return join(dir, name)
}
/** Minimal shape guard used by the stores before trusting a parsed value. */
export function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}