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:
Bill committed 2026-09-14 11:29:56 +08:00
1 parent 8eacd34338
commit beeb32a76b
17 files changed
+917 -37

No files matched your search

+29 -2
View File
@@ -9,6 +9,9 @@ import type { HostKeyCheckResult } from './knownHosts'
import { configureSysinfo, registerSysinfoClient, stopPolling } from './sysinfo'
import { registerSftpClientProvider, closeSftp } from './sftp'
import { attachZmodem, detachZmodem, feedZmodem, isZmodemActive } from './zmodem'
import { pickAdapter } from './shellIntegration'
import { loadSettings } from './settingsStore'
import { statSync } from 'fs'
// Re-export so the session-layer loopback bundle (tests/*-e2e.mjs) can drive the
// M3 polling engine without importing src/main/sysinfo.ts separately.
@@ -124,6 +127,15 @@ export function configureSessionRuntime(deps: SessionRuntimeDeps): void {
})
}
function existingDir(candidate: string | undefined): string | null {
if (!candidate || !candidate.trim()) return null
try {
return statSync(candidate).isDirectory() ? candidate : null
} catch {
return null
}
}
function defaultShell(): string {
switch (process.platform) {
case 'win32':
@@ -138,7 +150,10 @@ function defaultShell(): string {
export function createPty(opts: PtyCreateOptions = {}): PtyCreateResult {
const id = randomUUID()
const shell = opts.shell ?? defaultShell()
const cwd = opts.cwd ?? homedir()
// A remembered directory can be gone by the next launch (renamed, unmounted,
// another machine's path after syncing settings) — fall back to home rather
// than failing the spawn.
const cwd = existingDir(opts.cwd) ?? homedir()
// Advertise color capability + terminal identity so TUIs (e.g. kimi CLI)
// use their full-color theme instead of the degraded white/amber fallback.
const env = {
@@ -156,7 +171,19 @@ export function createPty(opts: PtyCreateOptions = {}): PtyCreateResult {
delete env.NO_COLOR
if (env.FORCE_COLOR === '0') delete env.FORCE_COLOR
const pty = spawn(shell, [], { name: 'xterm-256color', cols: 80, rows: 24, cwd, env })
// Shell integration (opt-in): let the shell announce its own cwd over OSC 7 so
// the remembered path is exact instead of inferred from typed `cd` commands.
// Every platform/shell difference lives in the adapter table, not here.
const args = [...(opts.shellArgs ?? [])]
if (opts.shellArgs === undefined && loadSettings().system.shellIntegration) {
const adapter = pickAdapter(shell)
if (adapter) {
args.push(...adapter.args(shell))
Object.assign(env, adapter.env?.() ?? {})
}
}
const pty = spawn(shell, args, { name: 'xterm-256color', cols: 80, rows: 24, cwd, env })
sessions.set(id, { kind: 'local', pty })
replayBuffers.set(id, '')