面板 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 个终端。
64 lines
2.3 KiB
TypeScript
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)
|
|
}
|