diff options
| author | Adam <[email protected]> | 2026-01-05 00:20:18 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-05 13:21:32 -0600 |
| commit | cb8533ef5b24b0ec9a249e8a966b05e012d11a2d (patch) | |
| tree | b39a0662f2325ab291b995e0ca70a2d451348b57 /packages/app/src | |
| parent | cdbb009ab04f702c78b11d2d8338cb5ee3b685da (diff) | |
| download | opencode-cb8533ef5b24b0ec9a249e8a966b05e012d11a2d.tar.gz opencode-cb8533ef5b24b0ec9a249e8a966b05e012d11a2d.zip | |
fix(app): terminal long-running processes
Diffstat (limited to 'packages/app/src')
| -rw-r--r-- | packages/app/src/components/terminal.tsx | 97 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 4 |
2 files changed, 57 insertions, 44 deletions
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 4fe6f0f0d..a298e3f76 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -36,12 +36,14 @@ export const Terminal = (props: TerminalProps) => { const theme = useTheme() let container!: HTMLDivElement const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"]) - let ws: WebSocket - let term: Term + let ws: WebSocket | undefined + let term: Term | undefined let ghostty: Ghostty let serializeAddon: SerializeAddon let fitAddon: FitAddon let handleResize: () => void + let reconnect: number | undefined + let disposed = false const getTerminalColors = (): TerminalColors => { const mode = theme.mode() @@ -76,22 +78,24 @@ export const Terminal = (props: TerminalProps) => { if (!term || !term.hasSelection()) return false const selection = term.getSelection() if (!selection) return false + if (document.body) { + const textarea = document.createElement("textarea") + textarea.value = selection + textarea.setAttribute("readonly", "") + textarea.style.position = "fixed" + textarea.style.opacity = "0" + document.body.appendChild(textarea) + textarea.select() + const copied = document.execCommand("copy") + document.body.removeChild(textarea) + if (copied) return true + } const clipboard = navigator.clipboard if (clipboard?.writeText) { clipboard.writeText(selection).catch(() => {}) return true } - if (!document.body) return false - const textarea = document.createElement("textarea") - textarea.value = selection - textarea.setAttribute("readonly", "") - textarea.style.position = "fixed" - textarea.style.opacity = "0" - document.body.appendChild(textarea) - textarea.select() - const copied = document.execCommand("copy") - document.body.removeChild(textarea) - return copied + return false } const handlePointerDown = () => { const activeElement = document.activeElement @@ -104,8 +108,12 @@ export const Terminal = (props: TerminalProps) => { onMount(async () => { ghostty = await Ghostty.load() - ws = new WebSocket(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`) - term = new Term({ + const socket = new WebSocket( + sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`, + ) + ws = socket + + const t = new Term({ cursorBlink: true, fontSize: 14, fontFamily: "IBM Plex Mono, monospace", @@ -114,7 +122,9 @@ export const Terminal = (props: TerminalProps) => { scrollback: 10_000, ghostty, }) - term.attachCustomKeyEventHandler((event) => { + term = t + + t.attachCustomKeyEventHandler((event) => { const key = event.key.toLowerCase() if (key === "c") { const macCopy = event.metaKey && !event.ctrlKey && !event.altKey @@ -134,21 +144,21 @@ export const Terminal = (props: TerminalProps) => { fitAddon = new FitAddon() serializeAddon = new SerializeAddon() - term.loadAddon(serializeAddon) - term.loadAddon(fitAddon) + t.loadAddon(serializeAddon) + t.loadAddon(fitAddon) - term.open(container) + t.open(container) container.addEventListener("pointerdown", handlePointerDown) focusTerminal() if (local.pty.buffer) { if (local.pty.rows && local.pty.cols) { - term.resize(local.pty.cols, local.pty.rows) + t.resize(local.pty.cols, local.pty.rows) } - term.reset() - term.write(local.pty.buffer, () => { + t.reset() + t.write(local.pty.buffer, () => { if (local.pty.scrollY) { - term.scrollToLine(local.pty.scrollY) + t.scrollToLine(local.pty.scrollY) } fitAddon.fit() }) @@ -157,8 +167,8 @@ export const Terminal = (props: TerminalProps) => { fitAddon.observeResize() handleResize = () => fitAddon.fit() window.addEventListener("resize", handleResize) - term.onResize(async (size) => { - if (ws && ws.readyState === WebSocket.OPEN) { + t.onResize(async (size) => { + if (socket.readyState === WebSocket.OPEN) { await sdk.client.pty .update({ ptyID: local.pty.id, @@ -170,39 +180,39 @@ export const Terminal = (props: TerminalProps) => { .catch(() => {}) } }) - term.onData((data) => { - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(data) + t.onData((data) => { + if (socket.readyState === WebSocket.OPEN) { + socket.send(data) } }) - term.onKey((key) => { + t.onKey((key) => { if (key.key == "Enter") { props.onSubmit?.() } }) - // term.onScroll((ydisp) => { + // t.onScroll((ydisp) => { // console.log("Scroll position:", ydisp) // }) - ws.addEventListener("open", () => { + socket.addEventListener("open", () => { console.log("WebSocket connected") sdk.client.pty .update({ ptyID: local.pty.id, size: { - cols: term.cols, - rows: term.rows, + cols: t.cols, + rows: t.rows, }, }) .catch(() => {}) }) - ws.addEventListener("message", (event) => { - term.write(event.data) + socket.addEventListener("message", (event) => { + t.write(event.data) }) - ws.addEventListener("error", (error) => { + socket.addEventListener("error", (error) => { console.error("WebSocket error:", error) props.onConnectError?.(error) }) - ws.addEventListener("close", () => { + socket.addEventListener("close", () => { console.log("WebSocket disconnected") }) }) @@ -212,18 +222,21 @@ export const Terminal = (props: TerminalProps) => { window.removeEventListener("resize", handleResize) } container.removeEventListener("pointerdown", handlePointerDown) - if (serializeAddon && props.onCleanup) { + + const t = term + if (serializeAddon && props.onCleanup && t) { const buffer = serializeAddon.serialize() props.onCleanup({ ...local.pty, buffer, - rows: term.rows, - cols: term.cols, - scrollY: term.getViewportY(), + rows: t.rows, + cols: t.cols, + scrollY: t.getViewportY(), }) } + ws?.close() - term?.dispose() + t?.dispose() }) return ( diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 3c23ac61e..b2da4bf03 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -863,9 +863,9 @@ export default function Page() { data-message-id={message.id} classList={{ "min-w-0 w-full max-w-full": true, - "last:min-h-[calc(100vh-13.5rem)] md:last:min-h-[calc(100vh-14.5rem)]": + "last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-32px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-32px)]": platform.platform !== "desktop", - "last:min-h-[calc(100vh-15rem)] md:last:min-h-[calc(100vh-16rem)]": + "last:min-h-[calc(100vh-7rem-var(--prompt-height,8rem)-32px)] md:last:min-h-[calc(100vh-6rem-var(--prompt-height,10rem)-32px)]": platform.platform === "desktop", }} > |
