From 7222fc0ba0e46b3ee787be608c71738aa14fe480 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 11 Feb 2026 08:19:42 -0600 Subject: [PATCH] fix(app): terminal resize --- packages/app/src/components/terminal.tsx | 46 +++++++++++++++----- packages/app/src/context/terminal.tsx | 53 +++++++++++++++++++----- packages/app/src/pages/layout.tsx | 9 +++- 3 files changed, 85 insertions(+), 23 deletions(-) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 2527c74ec..09c04db40 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -91,7 +91,7 @@ export const Terminal = (props: TerminalProps) => { } const getTerminalColors = (): TerminalColors => { - const mode = theme.mode() + const mode = theme.mode() === "dark" ? "dark" : "light" const fallback = DEFAULT_TERMINAL_COLORS[mode] const currentTheme = theme.themes()[theme.themeId()] if (!currentTheme) return fallback @@ -186,9 +186,23 @@ export const Terminal = (props: TerminalProps) => { } ws = socket + const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : "" + const restoreSize = + restore && + typeof local.pty.cols === "number" && + Number.isSafeInteger(local.pty.cols) && + local.pty.cols > 0 && + typeof local.pty.rows === "number" && + Number.isSafeInteger(local.pty.rows) && + local.pty.rows > 0 + ? { cols: local.pty.cols, rows: local.pty.rows } + : undefined + const t = new mod.Terminal({ cursorBlink: true, cursorStyle: "bar", + cols: restoreSize?.cols, + rows: restoreSize?.rows, fontSize: 14, fontFamily: monoFontFamily(settings.appearance.font()), allowTransparency: false, @@ -277,18 +291,28 @@ export const Terminal = (props: TerminalProps) => { focusTerminal() - fit.fit() - - if (local.pty.buffer) { - t.write(local.pty.buffer, () => { - if (local.pty.scrollY) t.scrollToLine(local.pty.scrollY) - }) + const startResize = () => { + fit.observeResize() + handleResize = () => fit.fit() + window.addEventListener("resize", handleResize) + cleanups.push(() => window.removeEventListener("resize", handleResize)) } - fit.observeResize() - handleResize = () => fit.fit() - window.addEventListener("resize", handleResize) - cleanups.push(() => window.removeEventListener("resize", handleResize)) + if (restore && restoreSize) { + t.write(restore, () => { + fit.fit() + if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) + startResize() + }) + } else { + fit.fit() + if (restore) { + t.write(restore, () => { + if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) + }) + } + startResize() + } const onResize = t.onResize(async (size) => { if (socket.readyState === WebSocket.OPEN) { diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index f0f184f8b..f6e36319b 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -3,7 +3,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context" import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js" import { useParams } from "@solidjs/router" import { useSDK } from "./sdk" -import { Persist, persisted } from "@/utils/persist" +import { Persist, persisted, removePersisted } from "@/utils/persist" export type LocalPTY = { id: string @@ -35,6 +35,28 @@ type TerminalCacheEntry = { dispose: VoidFunction } +const caches = new Set>() + +export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[]) { + const key = getWorkspaceTerminalCacheKey(dir) + for (const cache of caches) { + const entry = cache.get(key) + entry?.value.clear() + } + + removePersisted(Persist.workspace(dir, "terminal")) + + const legacy = new Set(getLegacyTerminalStorageKeys(dir)) + for (const id of sessionIDs ?? []) { + for (const key of getLegacyTerminalStorageKeys(dir, id)) { + legacy.add(key) + } + } + for (const key of legacy) { + removePersisted({ key }) + } +} + function createWorkspaceTerminalSession(sdk: ReturnType, dir: string, legacySessionID?: string) { const legacy = getLegacyTerminalStorageKeys(dir, legacySessionID) @@ -56,7 +78,7 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str }), ) - const unsub = sdk.event.on("pty.exited", (event) => { + const unsub = sdk.event.on("pty.exited", (event: { properties: { id: string } }) => { const id = event.properties.id if (!store.all.some((x) => x.id === id)) return batch(() => { @@ -96,6 +118,12 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str ready, all: createMemo(() => Object.values(store.all)), active: createMemo(() => store.active), + clear() { + batch(() => { + setStore("active", undefined) + setStore("all", []) + }) + }, new() { const existingTitleNumbers = new Set( store.all.flatMap((pty) => { @@ -114,7 +142,7 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str sdk.client.pty .create({ title: `Terminal ${nextNumber}` }) - .then((pty) => { + .then((pty: { data?: { id?: string; title?: string } }) => { const id = pty.data?.id if (!id) return const newTerminal = { @@ -128,8 +156,8 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str }) setStore("active", id) }) - .catch((e) => { - console.error("Failed to create terminal", e) + .catch((error: unknown) => { + console.error("Failed to create terminal", error) }) }, update(pty: Partial & { id: string }) { @@ -143,8 +171,8 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str title: pty.title, size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined, }) - .catch((e) => { - console.error("Failed to update terminal", e) + .catch((error: unknown) => { + console.error("Failed to update terminal", error) }) }, async clone(id: string) { @@ -155,8 +183,8 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str .create({ title: pty.title, }) - .catch((e) => { - console.error("Failed to clone terminal", e) + .catch((error: unknown) => { + console.error("Failed to clone terminal", error) return undefined }) if (!clone?.data) return @@ -200,8 +228,8 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str setStore("all", filtered) }) - await sdk.client.pty.remove({ ptyID: id }).catch((e) => { - console.error("Failed to close terminal", e) + await sdk.client.pty.remove({ ptyID: id }).catch((error: unknown) => { + console.error("Failed to close terminal", error) }) }, move(id: string, to: number) { @@ -225,6 +253,9 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont const params = useParams() const cache = new Map() + caches.add(cache) + onCleanup(() => caches.delete(cache)) + const disposeAll = () => { for (const entry of cache.values()) { entry.dispose() diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index a18b7ef23..e8435c3f9 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -34,6 +34,7 @@ import type { DragEvent } from "@thisbeyond/solid-dnd" import { useProviders } from "@/hooks/use-providers" import { showToast, Toast, toaster } from "@opencode-ai/ui/toast" import { useGlobalSDK } from "@/context/global-sdk" +import { clearWorkspaceTerminals } from "@/context/terminal" import { useNotification } from "@/context/notification" import { usePermission } from "@/context/permission" import { Binary } from "@opencode-ai/util/binary" @@ -1221,11 +1222,17 @@ export default function Layout(props: ParentProps) { }) const dismiss = () => toaster.dismiss(progress) - const sessions = await globalSDK.client.session + const sessions: Session[] = await globalSDK.client.session .list({ directory }) .then((x) => x.data ?? []) .catch(() => []) + clearWorkspaceTerminals( + directory, + sessions.map((s) => s.id), + ) + await globalSDK.client.instance.dispose({ directory }).catch(() => undefined) + const result = await globalSDK.client.worktree .reset({ directory: root, worktreeResetInput: { directory } }) .then((x) => x.data)