diff options
| author | Adam <[email protected]> | 2026-02-13 05:50:12 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-02-13 05:52:42 -0600 |
| commit | 1c71604e0a2a34786daa99b7002c2f567671051a (patch) | |
| tree | c1605b6a2a0d52bbe5936ad71d382a9d360cf18b /packages/app | |
| parent | e242fe19e48f6aa70e5c3f7d54f34d688181edb2 (diff) | |
| download | opencode-1c71604e0a2a34786daa99b7002c2f567671051a.tar.gz opencode-1c71604e0a2a34786daa99b7002c2f567671051a.zip | |
fix(app): terminal resize
Diffstat (limited to 'packages/app')
| -rw-r--r-- | packages/app/src/components/terminal.tsx | 138 |
1 files changed, 98 insertions, 40 deletions
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index ccf7012d2..14413dfda 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -156,6 +156,10 @@ export const Terminal = (props: TerminalProps) => { let serializeAddon: SerializeAddon let fitAddon: FitAddon let handleResize: () => void + let fitFrame: number | undefined + let sizeTimer: ReturnType<typeof setTimeout> | undefined + let pendingSize: { cols: number; rows: number } | undefined + let lastSize: { cols: number; rows: number } | undefined let disposed = false const cleanups: VoidFunction[] = [] const start = @@ -209,6 +213,43 @@ export const Terminal = (props: TerminalProps) => { const [terminalColors, setTerminalColors] = createSignal<TerminalColors>(getTerminalColors()) + const scheduleFit = () => { + if (disposed) return + if (!fitAddon) return + if (fitFrame !== undefined) return + + fitFrame = requestAnimationFrame(() => { + fitFrame = undefined + if (disposed) return + fitAddon.fit() + }) + } + + const scheduleSize = (cols: number, rows: number) => { + if (disposed) return + if (lastSize?.cols === cols && lastSize?.rows === rows) return + + pendingSize = { cols, rows } + + if (!lastSize) { + lastSize = pendingSize + void pushSize(cols, rows) + return + } + + if (sizeTimer !== undefined) return + sizeTimer = setTimeout(() => { + sizeTimer = undefined + const next = pendingSize + if (!next) return + pendingSize = undefined + if (disposed) return + if (lastSize?.cols === next.cols && lastSize?.rows === next.rows) return + lastSize = next + void pushSize(next.cols, next.rows) + }, 100) + } + createEffect(() => { const colors = getTerminalColors() setTerminalColors(colors) @@ -220,6 +261,16 @@ export const Terminal = (props: TerminalProps) => { const font = monoFontFamily(settings.appearance.font()) if (!term) return setOptionIfSupported(term, "fontFamily", font) + scheduleFit() + }) + + let zoom = platform.webviewZoom?.() + createEffect(() => { + const next = platform.webviewZoom?.() + if (next === undefined) return + if (next === zoom) return + zoom = next + scheduleFit() }) const focusTerminal = () => { @@ -263,25 +314,6 @@ export const Terminal = (props: TerminalProps) => { const once = { value: 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:" - if (window.__OPENCODE__?.serverPassword) { - url.username = "opencode" - url.password = window.__OPENCODE__?.serverPassword - } - const socket = new WebSocket(url) - socket.binaryType = "arraybuffer" - cleanups.push(() => { - if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close() - }) - if (disposed) { - cleanup() - return - } - ws = socket - const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : "" const restoreSize = restore && @@ -344,9 +376,28 @@ export const Terminal = (props: TerminalProps) => { focusTerminal() + if (typeof document !== "undefined" && document.fonts) { + document.fonts.ready.then(scheduleFit) + } + + const onResize = t.onResize((size) => { + scheduleSize(size.cols, size.rows) + }) + cleanups.push(() => disposeIfDisposable(onResize)) + const onData = t.onData((data) => { + if (ws?.readyState === WebSocket.OPEN) ws.send(data) + }) + cleanups.push(() => disposeIfDisposable(onData)) + const onKey = t.onKey((key) => { + if (key.key == "Enter") { + props.onSubmit?.() + } + }) + cleanups.push(() => disposeIfDisposable(onKey)) + const startResize = () => { fit.observeResize() - handleResize = () => fit.fit() + handleResize = scheduleFit window.addEventListener("resize", handleResize) cleanups.push(() => window.removeEventListener("resize", handleResize)) } @@ -354,11 +405,13 @@ export const Terminal = (props: TerminalProps) => { 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() }) } else { fit.fit() + scheduleSize(t.cols, t.rows) if (restore) { t.write(restore, () => { if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) @@ -367,35 +420,38 @@ export const Terminal = (props: TerminalProps) => { startResize() } - const onResize = t.onResize(async (size) => { - if (socket.readyState === WebSocket.OPEN) { - await pushSize(size.cols, size.rows) - } - }) - cleanups.push(() => disposeIfDisposable(onResize)) - const onData = t.onData((data) => { - if (socket.readyState === WebSocket.OPEN) { - socket.send(data) - } - }) - cleanups.push(() => disposeIfDisposable(onData)) - const onKey = t.onKey((key) => { - if (key.key == "Enter") { - props.onSubmit?.() - } - }) - cleanups.push(() => disposeIfDisposable(onKey)) // t.onScroll((ydisp) => { // console.log("Scroll position:", ydisp) // }) + 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:" + if (window.__OPENCODE__?.serverPassword) { + url.username = "opencode" + url.password = window.__OPENCODE__?.serverPassword + } + 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?.() - void pushSize(t.cols, t.rows) + 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) => { @@ -462,6 +518,8 @@ export const Terminal = (props: TerminalProps) => { onCleanup(() => { disposed = true + if (fitFrame !== undefined) cancelAnimationFrame(fitFrame) + if (sizeTimer !== undefined) clearTimeout(sizeTimer) output?.flush() persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup }) cleanup() @@ -477,7 +535,7 @@ export const Terminal = (props: TerminalProps) => { classList={{ ...(local.classList ?? {}), "select-text": true, - "size-full px-6 py-3 font-mono": true, + "size-full px-6 py-3 font-mono relative overflow-hidden": true, [local.class ?? ""]: !!local.class, }} {...others} |
