diff options
| author | Adam <[email protected]> | 2026-03-09 15:28:29 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-03-09 15:28:35 -0500 |
| commit | 794532928f76c197181bc80944e8be1f6e5eda9a (patch) | |
| tree | 302836999faccde5788038071ad8920476399155 /packages/app/src/context/terminal.tsx | |
| parent | 7b773c65ecc5b9e4796aa320723645d6e7431474 (diff) | |
| download | opencode-794532928f76c197181bc80944e8be1f6e5eda9a.tar.gz opencode-794532928f76c197181bc80944e8be1f6e5eda9a.zip | |
fix(app): terminal state corruption
Diffstat (limited to 'packages/app/src/context/terminal.tsx')
| -rw-r--r-- | packages/app/src/context/terminal.tsx | 98 |
1 files changed, 69 insertions, 29 deletions
diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index 4467495b7..a2807375f 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -20,6 +20,71 @@ export type LocalPTY = { const WORKSPACE_KEY = "__workspace__" const MAX_TERMINAL_SESSIONS = 20 +function record(value: unknown): value is Record<string, unknown> { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function text(value: unknown) { + return typeof value === "string" ? value : undefined +} + +function num(value: unknown) { + return typeof value === "number" && Number.isFinite(value) ? value : undefined +} + +function numberFromTitle(title: string) { + const match = title.match(/^Terminal (\d+)$/) + if (!match) return + const value = Number(match[1]) + if (!Number.isFinite(value) || value <= 0) return + return value +} + +function pty(value: unknown): LocalPTY | undefined { + if (!record(value)) return + + const id = text(value.id) + if (!id) return + + const title = text(value.title) ?? "" + const number = num(value.titleNumber) + const rows = num(value.rows) + const cols = num(value.cols) + const buffer = text(value.buffer) + const scrollY = num(value.scrollY) + const cursor = num(value.cursor) + + return { + id, + title, + titleNumber: number && number > 0 ? number : (numberFromTitle(title) ?? 0), + ...(rows !== undefined ? { rows } : {}), + ...(cols !== undefined ? { cols } : {}), + ...(buffer !== undefined ? { buffer } : {}), + ...(scrollY !== undefined ? { scrollY } : {}), + ...(cursor !== undefined ? { cursor } : {}), + } +} + +export function migrateTerminalState(value: unknown) { + if (!record(value)) return value + + const seen = new Set<string>() + const all = (Array.isArray(value.all) ? value.all : []).flatMap((item) => { + const next = pty(item) + if (!next || seen.has(next.id)) return [] + seen.add(next.id) + return [next] + }) + + const active = text(value.active) + + return { + active: active && seen.has(active) ? active : all[0]?.id, + all, + } +} + export function getWorkspaceTerminalCacheKey(dir: string) { return `${dir}:${WORKSPACE_KEY}` } @@ -71,16 +136,11 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, legacySessionID?: string) { const legacy = getLegacyTerminalStorageKeys(dir, legacySessionID) - const numberFromTitle = (title: string) => { - const match = title.match(/^Terminal (\d+)$/) - if (!match) return - const value = Number(match[1]) - if (!Number.isFinite(value) || value <= 0) return - return value - } - const [store, setStore, _, ready] = persisted( - Persist.workspace(dir, "terminal", legacy), + { + ...Persist.workspace(dir, "terminal", legacy), + migrate: migrateTerminalState, + }, createStore<{ active?: string all: LocalPTY[] @@ -128,26 +188,6 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str }) onCleanup(unsub) - const meta = { migrated: false } - - createEffect(() => { - if (!ready()) return - if (meta.migrated) return - meta.migrated = true - - setStore("all", (all) => { - const next = all.map((pty) => { - const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined - if (direct !== undefined) return pty - const parsed = numberFromTitle(pty.title) - if (parsed === undefined) return pty - return { ...pty, titleNumber: parsed } - }) - if (next.every((pty, index) => pty === all[index])) return all - return next - }) - }) - return { ready, all: createMemo(() => store.all), |
