Skip to content

Commit 6e9e027

Browse files
authored
fix: trim retained desktop terminal buffers (#16583)
1 parent f9a3d12 commit 6e9e027

3 files changed

Lines changed: 65 additions & 8 deletions

File tree

packages/app/e2e/terminal/terminal-tabs.spec.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,14 @@ async function store(page: Page, key: string) {
4444
}, key)
4545
}
4646

47-
test("terminal tab buffers persist across tab switches", async ({ page, withProject }) => {
47+
test("inactive terminal tab buffers persist across tab switches", async ({ page, withProject }) => {
4848
await withProject(async ({ directory, gotoSession }) => {
4949
const key = workspacePersistKey(directory, "terminal")
5050
const one = `E2E_TERM_ONE_${Date.now()}`
5151
const two = `E2E_TERM_TWO_${Date.now()}`
5252
const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]')
53+
const first = tabs.filter({ hasText: /Terminal 1/ }).first()
54+
const second = tabs.filter({ hasText: /Terminal 2/ }).first()
5355

5456
await gotoSession()
5557
await open(page)
@@ -61,22 +63,39 @@ test("terminal tab buffers persist across tab switches", async ({ page, withProj
6163

6264
await run(page, `echo ${two}`)
6365

64-
await tabs
65-
.filter({ hasText: /Terminal 1/ })
66-
.first()
67-
.click()
66+
await first.click()
67+
await expect(first).toHaveAttribute("aria-selected", "true")
68+
await expect
69+
.poll(
70+
async () => {
71+
const state = await store(page, key)
72+
const first = state?.all.find((item) => item.titleNumber === 1)?.buffer ?? ""
73+
const second = state?.all.find((item) => item.titleNumber === 2)?.buffer ?? ""
74+
return {
75+
first: first.includes(one),
76+
second: second.includes(two),
77+
}
78+
},
79+
{ timeout: 30_000 },
80+
)
81+
.toEqual({ first: false, second: true })
6882

83+
await second.click()
84+
await expect(second).toHaveAttribute("aria-selected", "true")
6985
await expect
7086
.poll(
7187
async () => {
7288
const state = await store(page, key)
7389
const first = state?.all.find((item) => item.titleNumber === 1)?.buffer ?? ""
7490
const second = state?.all.find((item) => item.titleNumber === 2)?.buffer ?? ""
75-
return first.includes(one) && second.includes(two)
91+
return {
92+
first: first.includes(one),
93+
second: second.includes(two),
94+
}
7695
},
7796
{ timeout: 30_000 },
7897
)
79-
.toBe(true)
98+
.toEqual({ first: true, second: false })
8099
})
81100
})
82101

packages/app/src/context/terminal.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createStore, produce } from "solid-js/store"
22
import { createSimpleContext } from "@opencode-ai/ui/context"
3-
import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js"
3+
import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "solid-js"
44
import { useParams } from "@solidjs/router"
55
import { useSDK } from "./sdk"
66
import type { Platform } from "./platform"
@@ -38,6 +38,16 @@ type TerminalCacheEntry = {
3838

3939
const caches = new Set<Map<string, TerminalCacheEntry>>()
4040

41+
const trimTerminal = (pty: LocalPTY) => {
42+
if (!pty.buffer && pty.cursor === undefined && pty.scrollY === undefined) return pty
43+
return {
44+
...pty,
45+
buffer: undefined,
46+
cursor: undefined,
47+
scrollY: undefined,
48+
}
49+
}
50+
4151
export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform) {
4252
const key = getWorkspaceTerminalCacheKey(dir)
4353
for (const cache of caches) {
@@ -188,6 +198,18 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
188198
console.error("Failed to update terminal", error)
189199
})
190200
},
201+
trim(id: string) {
202+
const index = store.all.findIndex((x) => x.id === id)
203+
if (index === -1) return
204+
setStore("all", index, (pty) => trimTerminal(pty))
205+
},
206+
trimAll() {
207+
setStore("all", (all) => {
208+
const next = all.map(trimTerminal)
209+
if (next.every((pty, index) => pty === all[index])) return all
210+
return next
211+
})
212+
},
191213
async clone(id: string) {
192214
const index = store.all.findIndex((x) => x.id === id)
193215
const pty = store.all[index]
@@ -322,12 +344,27 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
322344

323345
const workspace = createMemo(() => loadWorkspace(params.dir!, params.id))
324346

347+
createEffect(
348+
on(
349+
() => ({ dir: params.dir, id: params.id }),
350+
(next, prev) => {
351+
if (!prev?.dir) return
352+
if (next.dir === prev.dir && next.id === prev.id) return
353+
if (next.dir === prev.dir && next.id) return
354+
loadWorkspace(prev.dir, prev.id).trimAll()
355+
},
356+
{ defer: true },
357+
),
358+
)
359+
325360
return {
326361
ready: () => workspace().ready(),
327362
all: () => workspace().all(),
328363
active: () => workspace().active(),
329364
new: () => workspace().new(),
330365
update: (pty: Partial<LocalPTY> & { id: string }) => workspace().update(pty),
366+
trim: (id: string) => workspace().trim(id),
367+
trimAll: () => workspace().trimAll(),
331368
clone: (id: string) => workspace().clone(id),
332369
open: (id: string) => workspace().open(id),
333370
close: (id: string) => workspace().close(id),

packages/app/src/pages/session/terminal-panel.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ export function TerminalPanel() {
250250
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
251251
<Terminal
252252
pty={pty()}
253+
onConnect={() => terminal.trim(id)}
253254
onCleanup={terminal.update}
254255
onConnectError={() => terminal.clone(id)}
255256
/>

0 commit comments

Comments
 (0)