diff --git a/packages/app/src/context/terminal.test.ts b/packages/app/src/context/terminal.test.ts index 6e07e0312412..b44ed27c37a2 100644 --- a/packages/app/src/context/terminal.test.ts +++ b/packages/app/src/context/terminal.test.ts @@ -1,6 +1,7 @@ import { beforeAll, describe, expect, mock, test } from "bun:test" -let getWorkspaceTerminalCacheKey: (dir: string) => string +let getTerminalServerKey: (local: boolean, key: string) => string +let getWorkspaceTerminalCacheKey: (scope: string, dir: string) => string let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => string[] let migrateTerminalState: (value: unknown) => unknown @@ -16,14 +17,25 @@ beforeAll(async () => { }), })) const mod = await import("./terminal") + getTerminalServerKey = mod.getTerminalServerKey getWorkspaceTerminalCacheKey = mod.getWorkspaceTerminalCacheKey getLegacyTerminalStorageKeys = mod.getLegacyTerminalStorageKeys migrateTerminalState = mod.migrateTerminalState }) +describe("getTerminalServerKey", () => { + test("uses local scope for local servers", () => { + expect(getTerminalServerKey(true, "sidecar")).toBe("local") + }) + + test("uses server key for remote servers", () => { + expect(getTerminalServerKey(false, "ssh:box")).toBe("ssh:box") + }) +}) + describe("getWorkspaceTerminalCacheKey", () => { - test("uses workspace-only directory cache key", () => { - expect(getWorkspaceTerminalCacheKey("/repo")).toBe("/repo:__workspace__") + test("uses server-scoped directory cache key", () => { + expect(getWorkspaceTerminalCacheKey("local", "/repo")).toBe("local:/repo:__workspace__") }) }) diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index e65c16788461..66e4a6522a16 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -3,6 +3,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context" import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "solid-js" import { useParams } from "@solidjs/router" import { useSDK } from "./sdk" +import { useServer } from "./server" import type { Platform } from "./platform" import { defaultTitle, titleNumber } from "./terminal-title" import { Persist, persisted, removePersisted } from "@/utils/persist" @@ -82,8 +83,16 @@ export function migrateTerminalState(value: unknown) { } } -export function getWorkspaceTerminalCacheKey(dir: string) { - return `${dir}:${WORKSPACE_KEY}` +export function getTerminalServerKey(local: boolean, key: string) { + return local ? "local" : key +} + +export function getWorkspaceTerminalCacheKey(serverKey: string, dir: string) { + return `${serverKey}:${dir}:${WORKSPACE_KEY}` +} + +function getWorkspaceTerminalPersistKey(serverKey: string) { + return `terminal:${serverKey}` } export function getLegacyTerminalStorageKeys(dir: string, legacySessionID?: string) { @@ -110,14 +119,14 @@ const trimTerminal = (pty: LocalPTY) => { } } -export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform) { - const key = getWorkspaceTerminalCacheKey(dir) +export function clearWorkspaceTerminals(serverKey: string, dir: string, sessionIDs?: string[], platform?: Platform) { + const key = getWorkspaceTerminalCacheKey(serverKey, dir) for (const cache of caches) { const entry = cache.get(key) entry?.value.clear() } - removePersisted(Persist.workspace(dir, "terminal"), platform) + removePersisted(Persist.workspace(dir, getWorkspaceTerminalPersistKey(serverKey)), platform) const legacy = new Set(getLegacyTerminalStorageKeys(dir)) for (const id of sessionIDs ?? []) { @@ -130,12 +139,17 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat } } -function createWorkspaceTerminalSession(sdk: ReturnType, dir: string, legacySessionID?: string) { +function createWorkspaceTerminalSession( + sdk: ReturnType, + serverKey: string, + dir: string, + legacySessionID?: string, +) { const legacy = getLegacyTerminalStorageKeys(dir, legacySessionID) const [store, setStore, _, ready] = persisted( { - ...Persist.workspace(dir, "terminal", legacy), + ...Persist.workspace(dir, getWorkspaceTerminalPersistKey(serverKey), legacy), migrate: migrateTerminalState, }, createStore<{ @@ -334,6 +348,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont gate: false, init: () => { const sdk = useSDK() + const server = useServer() const params = useParams() const cache = new Map() @@ -359,9 +374,9 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont } } - const loadWorkspace = (dir: string, legacySessionID?: string) => { + const loadWorkspace = (serverKey: string, dir: string, legacySessionID?: string) => { // Terminals are workspace-scoped so tabs persist while switching sessions in the same directory. - const key = getWorkspaceTerminalCacheKey(dir) + const key = getWorkspaceTerminalCacheKey(serverKey, dir) const existing = cache.get(key) if (existing) { cache.delete(key) @@ -370,7 +385,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont } const entry = createRoot((dispose) => ({ - value: createWorkspaceTerminalSession(sdk, dir, legacySessionID), + value: createWorkspaceTerminalSession(sdk, serverKey, dir, legacySessionID), dispose, })) @@ -379,16 +394,18 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont return entry.value } - const workspace = createMemo(() => loadWorkspace(params.dir!, params.id)) + const serverKey = createMemo(() => getTerminalServerKey(!!server.isLocal(), server.key)) + + const workspace = createMemo(() => loadWorkspace(serverKey(), params.dir!, params.id)) createEffect( on( - () => ({ dir: params.dir, id: params.id }), + () => ({ serverKey: serverKey(), dir: params.dir, id: params.id }), (next, prev) => { - if (!prev?.dir) return - if (next.dir === prev.dir && next.id === prev.id) return - if (next.dir === prev.dir && next.id) return - loadWorkspace(prev.dir, prev.id).trimAll() + if (!prev?.dir || !prev.serverKey) return + if (next.serverKey === prev.serverKey && next.dir === prev.dir && next.id === prev.id) return + if (next.serverKey === prev.serverKey && next.dir === prev.dir && next.id) return + loadWorkspace(prev.serverKey, prev.dir, prev.id).trimAll() }, { defer: true }, ), diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index ab2687dcab93..96761073a5c5 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -33,7 +33,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 { clearWorkspaceTerminals, getTerminalServerKey } from "@/context/terminal" import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache" import { clearSessionPrefetchInflight, @@ -1504,6 +1504,7 @@ export default function Layout(props: ParentProps) { .catch(() => []) clearWorkspaceTerminals( + getTerminalServerKey(!!server.isLocal(), server.key), directory, sessions.map((s) => s.id), platform,