Browse Source

fix(app): terminal resize

Adam 2 weeks ago
parent
commit
7222fc0ba0

+ 34 - 10
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,19 +291,29 @@ export const Terminal = (props: TerminalProps) => {
 
       focusTerminal()
 
-      fit.fit()
+      const startResize = () => {
+        fit.observeResize()
+        handleResize = () => fit.fit()
+        window.addEventListener("resize", handleResize)
+        cleanups.push(() => window.removeEventListener("resize", handleResize))
+      }
 
-      if (local.pty.buffer) {
-        t.write(local.pty.buffer, () => {
-          if (local.pty.scrollY) t.scrollToLine(local.pty.scrollY)
+      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()
       }
 
-      fit.observeResize()
-      handleResize = () => fit.fit()
-      window.addEventListener("resize", handleResize)
-      cleanups.push(() => window.removeEventListener("resize", handleResize))
-
       const onResize = t.onResize(async (size) => {
         if (socket.readyState === WebSocket.OPEN) {
           await sdk.client.pty

+ 42 - 11
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<Map<string, TerminalCacheEntry>>()
+
+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<typeof useSDK>, dir: string, legacySessionID?: string) {
   const legacy = getLegacyTerminalStorageKeys(dir, legacySessionID)
 
@@ -56,7 +78,7 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, 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<typeof useSDK>, 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<typeof useSDK>, 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<typeof useSDK>, 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<LocalPTY> & { id: string }) {
@@ -143,8 +171,8 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, 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<typeof useSDK>, 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<typeof useSDK>, 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<string, TerminalCacheEntry>()
 
+    caches.add(cache)
+    onCleanup(() => caches.delete(cache))
+
     const disposeAll = () => {
       for (const entry of cache.values()) {
         entry.dispose()

+ 8 - 1
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)