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 个终端。
This commit is contained in:
1 parent
8eacd34338
commit
beeb32a76b
17 files changed
+917
-37
No files matched your search
@@ -0,0 +1,63 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user