summaryrefslogtreecommitdiffhomepage
path: root/packages/app
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-13 05:50:12 -0600
committerAdam <[email protected]>2026-02-13 05:52:42 -0600
commit1c71604e0a2a34786daa99b7002c2f567671051a (patch)
treec1605b6a2a0d52bbe5936ad71d382a9d360cf18b /packages/app
parente242fe19e48f6aa70e5c3f7d54f34d688181edb2 (diff)
downloadopencode-1c71604e0a2a34786daa99b7002c2f567671051a.tar.gz
opencode-1c71604e0a2a34786daa99b7002c2f567671051a.zip
fix(app): terminal resize
Diffstat (limited to 'packages/app')
-rw-r--r--packages/app/src/components/terminal.tsx138
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}