diff options
| author | Adam <[email protected]> | 2026-02-19 06:35:14 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-19 06:35:14 -0600 |
| commit | d07f09925fae3dd0eac245b1817ace5eee19f0aa (patch) | |
| tree | 78a834c2c851be37bb6d25846fc029f2fbbaff6e /packages/app | |
| parent | c7b35342ddca083b2a2b9668778b4cccb6b5f602 (diff) | |
| download | opencode-d07f09925fae3dd0eac245b1817ace5eee19f0aa.tar.gz opencode-d07f09925fae3dd0eac245b1817ace5eee19f0aa.zip | |
fix(app): terminal rework (#14217)
Diffstat (limited to 'packages/app')
| -rw-r--r-- | packages/app/src/components/terminal.tsx | 58 | ||||
| -rw-r--r-- | packages/app/src/pages/session/terminal-panel.tsx | 60 |
2 files changed, 80 insertions, 38 deletions
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 085a79613..bd7ab2447 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -320,8 +320,6 @@ export const Terminal = (props: TerminalProps) => { const mod = loaded.mod const g = loaded.ghostty - const once = { value: false } - const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : "" const restoreSize = restore && @@ -416,20 +414,28 @@ export const Terminal = (props: TerminalProps) => { cleanups.push(() => window.removeEventListener("resize", handleResize)) } - if (restore && restoreSize) { - t.write(restore, () => { - fit.fit() - scheduleSize(t.cols, t.rows) - if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) - startResize() + const write = (data: string) => + new Promise<void>((resolve) => { + if (!output) { + resolve() + return + } + output.push(data) + output.flush(resolve) }) + + if (restore && restoreSize) { + await write(restore) + fit.fit() + scheduleSize(t.cols, t.rows) + if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) + startResize() } else { fit.fit() scheduleSize(t.cols, t.rows) if (restore) { - t.write(restore, () => { - if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) - }) + await write(restore) + if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) } startResize() } @@ -438,38 +444,32 @@ export const Terminal = (props: TerminalProps) => { // console.log("Scroll position:", ydisp) // }) + const once = { value: false } + let closing = false + const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`) url.searchParams.set("directory", sdk.directory) url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0)) url.protocol = url.protocol === "https:" ? "wss:" : "ws:" url.username = server.current?.http.username ?? "" url.password = server.current?.http.password ?? "" + const socket = new WebSocket(url) socket.binaryType = "arraybuffer" ws = socket - cleanups.push(() => { - if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close() - }) - if (disposed) { - cleanup() - return - } const handleOpen = () => { local.onConnect?.() scheduleSize(t.cols, t.rows) } socket.addEventListener("open", handleOpen) - cleanups.push(() => socket.removeEventListener("open", handleOpen)) - if (socket.readyState === WebSocket.OPEN) handleOpen() const decoder = new TextDecoder() - const handleMessage = (event: MessageEvent) => { if (disposed) return + if (closing) return if (event.data instanceof ArrayBuffer) { - // WebSocket control frame: 0x00 + UTF-8 JSON (currently { cursor }). const bytes = new Uint8Array(event.data) if (bytes[0] !== 0) return const json = decoder.decode(bytes.subarray(1)) @@ -491,20 +491,20 @@ export const Terminal = (props: TerminalProps) => { cursor += data.length } socket.addEventListener("message", handleMessage) - cleanups.push(() => socket.removeEventListener("message", handleMessage)) const handleError = (error: Event) => { if (disposed) return + if (closing) return if (once.value) return once.value = true console.error("WebSocket error:", error) local.onConnectError?.(error) } socket.addEventListener("error", handleError) - cleanups.push(() => socket.removeEventListener("error", handleError)) const handleClose = (event: CloseEvent) => { if (disposed) return + if (closing) return // Normal closure (code 1000) means PTY process exited - server event handles cleanup // For other codes (network issues, server restart), trigger error handler if (event.code !== 1000) { @@ -514,7 +514,15 @@ export const Terminal = (props: TerminalProps) => { } } socket.addEventListener("close", handleClose) - cleanups.push(() => socket.removeEventListener("close", handleClose)) + + cleanups.push(() => { + closing = true + socket.removeEventListener("open", handleOpen) + socket.removeEventListener("message", handleMessage) + socket.removeEventListener("error", handleError) + socket.removeEventListener("close", handleClose) + if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close(1000) + }) } void run().catch((err) => { diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index 33421c386..73f61ab05 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -38,9 +38,34 @@ export function TerminalPanel() { const [store, setStore] = createStore({ autoCreated: false, + everOpened: false, activeDraggable: undefined as string | undefined, }) + const rendered = createMemo(() => isDesktop() && (opened() || store.everOpened)) + + createEffect( + on(open, (isOpen, prev) => { + if (isOpen) { + if (!store.everOpened) setStore("everOpened", true) + const activeId = terminal.active() + if (!activeId) return + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur() + } + setTimeout(() => focusTerminalById(activeId), 0) + return + } + + if (!prev) return + const panel = document.getElementById("terminal-panel") + const activeElement = document.activeElement + if (!panel || !(activeElement instanceof HTMLElement)) return + if (!panel.contains(activeElement)) return + activeElement.blur() + }), + ) + createEffect(() => { if (!opened()) { setStore("autoCreated", false) @@ -67,7 +92,7 @@ export function TerminalPanel() { on( () => terminal.active(), (activeId) => { - if (!activeId || !opened()) return + if (!activeId || !open()) return if (document.activeElement instanceof HTMLElement) { document.activeElement.blur() } @@ -133,23 +158,32 @@ export function TerminalPanel() { } return ( - <Show when={open()}> + <Show when={rendered()}> <div id="terminal-panel" role="region" aria-label={language.t("terminal.title")} - class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base" - style={{ height: `${height()}px` }} + classList={{ + "relative w-full flex flex-col shrink-0 overflow-hidden": true, + "border-t border-border-weak-base": open(), + "pointer-events-none": !open(), + }} + style={{ + height: `${height()}px`, + display: open() ? "flex" : "none", + }} > - <ResizeHandle - direction="vertical" - size={height()} - min={100} - max={typeof window === "undefined" ? 1000 : window.innerHeight * 0.6} - collapseThreshold={50} - onResize={layout.terminal.resize} - onCollapse={close} - /> + <Show when={open()}> + <ResizeHandle + direction="vertical" + size={height()} + min={100} + max={typeof window === "undefined" ? 1000 : window.innerHeight * 0.6} + collapseThreshold={50} + onResize={layout.terminal.resize} + onCollapse={close} + /> + </Show> <Show when={terminal.ready()} fallback={ |
