diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index 0c950871a335..ec8b082d4a9e 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -56,6 +56,7 @@ import { ServerConnection, ServerProvider, serverName, useServer } from "@/conte
import { SettingsProvider, useSettings } from "@/context/settings"
import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
import { SDKProvider, useSDK } from "@/context/sdk"
+import { TerminalProvider, TerminalRegistryProvider } from "@/context/terminal"
import { WslServersProvider } from "@/wsl/context"
import DirectoryLayout, { DirectoryDataProvider } from "@/pages/directory-layout"
import LegacyLayout from "@/pages/layout"
@@ -378,15 +379,17 @@ function NewAppLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) {
)
}
-// The draft page only renders the prompt composer, so it drops TerminalProvider.
-// FileProvider and CommentsProvider stay because PromptInput uses file search and comment context.
+// Drafts share their workspace with the terminal, files, and prompt context. A new chat can
+// therefore open the same bottom terminal before its first prompt is submitted.
function DraftProviders(props: ParentProps) {
return (
-
-
- {props.children}
-
-
+
+
+
+ {props.children}
+
+
+
)
}
@@ -413,11 +416,13 @@ export function AppBaseProviders(
}}
>
-
-
- {props.children}
-
-
+
+
+
+ {props.children}
+
+
+
diff --git a/packages/app/src/context/terminal.test.ts b/packages/app/src/context/terminal.test.ts
index 25ef35dd4138..d97e3e31b361 100644
--- a/packages/app/src/context/terminal.test.ts
+++ b/packages/app/src/context/terminal.test.ts
@@ -4,6 +4,7 @@ import { ServerScope } from "@/utils/server-scope"
let getWorkspaceTerminalCacheKey: typeof import("./terminal").getWorkspaceTerminalCacheKey
let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => string[]
let migrateTerminalState: (value: unknown) => unknown
+let createWorkspaceTerminalRegistry: typeof import("./terminal").createWorkspaceTerminalRegistry
beforeAll(async () => {
mock.module("@solidjs/router", () => ({
@@ -22,6 +23,7 @@ beforeAll(async () => {
getWorkspaceTerminalCacheKey = mod.getWorkspaceTerminalCacheKey
getLegacyTerminalStorageKeys = mod.getLegacyTerminalStorageKeys
migrateTerminalState = mod.migrateTerminalState
+ createWorkspaceTerminalRegistry = mod.createWorkspaceTerminalRegistry
})
describe("getWorkspaceTerminalCacheKey", () => {
@@ -89,3 +91,39 @@ describe("migrateTerminalState", () => {
})
})
})
+
+describe("createWorkspaceTerminalRegistry", () => {
+ test("reuses one terminal session across chats in the same workspace", () => {
+ const registry = createWorkspaceTerminalRegistry<{ id: string }>()
+ const disposed: string[] = []
+ const first = registry.get("server\u0000repo-a", () => ({
+ value: { id: "terminal-a" },
+ dispose: () => disposed.push("terminal-a"),
+ }))
+ const second = registry.get("server\u0000repo-a", () => ({
+ value: { id: "replacement" },
+ dispose: () => disposed.push("replacement"),
+ }))
+
+ expect(second).toBe(first)
+ expect(disposed).toEqual([])
+ registry.dispose()
+ expect(disposed).toEqual(["terminal-a"])
+ })
+
+ test("keeps workspaces separate and evicts the least recently used entry", () => {
+ const registry = createWorkspaceTerminalRegistry<{ id: string }>(2)
+ const disposed: string[] = []
+ const create = (id: string) => ({ value: { id }, dispose: () => disposed.push(id) })
+
+ expect(registry.get("server\u0000repo-a", () => create("a")).id).toBe("a")
+ expect(registry.get("server\u0000repo-b", () => create("b")).id).toBe("b")
+ expect(registry.get("server\u0000repo-a", () => create("replacement")).id).toBe("a")
+ expect(registry.get("server\u0000repo-c", () => create("c")).id).toBe("c")
+
+ expect(registry.peek("server\u0000repo-a")?.id).toBe("a")
+ expect(registry.peek("server\u0000repo-b")).toBeUndefined()
+ expect(registry.peek("server\u0000repo-c")?.id).toBe("c")
+ expect(disposed).toEqual(["b"])
+ })
+})
diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx
index df575b71f7ce..571e5e256f42 100644
--- a/packages/app/src/context/terminal.tsx
+++ b/packages/app/src/context/terminal.tsx
@@ -96,12 +96,41 @@ export function getLegacyTerminalStorageKeys(dir: string, legacySessionID?: stri
type TerminalSession = ReturnType
-type TerminalCacheEntry = {
- value: TerminalSession
- dispose: VoidFunction
+export function createWorkspaceTerminalRegistry(limit = MAX_TERMINAL_SESSIONS) {
+ const cache = new Map()
+
+ return {
+ get(key: string, create: () => { value: T; dispose: VoidFunction }) {
+ const existing = cache.get(key)
+ if (existing) {
+ cache.delete(key)
+ cache.set(key, existing)
+ return existing.value
+ }
+
+ const entry = create()
+ cache.set(key, entry)
+ while (cache.size > limit) {
+ const first = cache.keys().next().value
+ if (!first) break
+ cache.get(first)?.dispose()
+ cache.delete(first)
+ }
+ return entry.value
+ },
+ peek(key: string) {
+ return cache.get(key)?.value
+ },
+ dispose() {
+ for (const entry of cache.values()) entry.dispose()
+ cache.clear()
+ },
+ }
}
-const caches = new Set