summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components/terminal.tsx
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-12 09:49:14 -0600
committerGitHub <[email protected]>2026-02-12 09:49:14 -0600
commitff4414bb152acfddb5c0eb073c38bedc1df4ae14 (patch)
tree78381c67d21ef6f089647f6b19e7aa2976840dbc /packages/app/src/components/terminal.tsx
parent56ad2db02055955f926fda0e4a89055b22ead6f9 (diff)
downloadopencode-ff4414bb152acfddb5c0eb073c38bedc1df4ae14.tar.gz
opencode-ff4414bb152acfddb5c0eb073c38bedc1df4ae14.zip
chore: refactor packages/app files (#13236)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Frank <[email protected]>
Diffstat (limited to 'packages/app/src/components/terminal.tsx')
-rw-r--r--packages/app/src/components/terminal.tsx192
1 files changed, 104 insertions, 88 deletions
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx
index 09c04db40..f6bb0b48a 100644
--- a/packages/app/src/components/terminal.tsx
+++ b/packages/app/src/components/terminal.tsx
@@ -56,6 +56,91 @@ const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
},
}
+const debugTerminal = (...values: unknown[]) => {
+ if (!import.meta.env.DEV) return
+ console.debug("[terminal]", ...values)
+}
+
+const useTerminalUiBindings = (input: {
+ container: HTMLDivElement
+ term: Term
+ cleanups: VoidFunction[]
+ handlePointerDown: () => void
+ handleLinkClick: (event: MouseEvent) => void
+}) => {
+ const handleCopy = (event: ClipboardEvent) => {
+ const selection = input.term.getSelection()
+ if (!selection) return
+
+ const clipboard = event.clipboardData
+ if (!clipboard) return
+
+ event.preventDefault()
+ clipboard.setData("text/plain", selection)
+ }
+
+ const handlePaste = (event: ClipboardEvent) => {
+ const clipboard = event.clipboardData
+ const text = clipboard?.getData("text/plain") ?? clipboard?.getData("text") ?? ""
+ if (!text) return
+
+ event.preventDefault()
+ event.stopPropagation()
+ input.term.paste(text)
+ }
+
+ const handleTextareaFocus = () => {
+ input.term.options.cursorBlink = true
+ }
+ const handleTextareaBlur = () => {
+ input.term.options.cursorBlink = false
+ }
+
+ input.container.addEventListener("copy", handleCopy, true)
+ input.cleanups.push(() => input.container.removeEventListener("copy", handleCopy, true))
+
+ input.container.addEventListener("paste", handlePaste, true)
+ input.cleanups.push(() => input.container.removeEventListener("paste", handlePaste, true))
+
+ input.container.addEventListener("pointerdown", input.handlePointerDown)
+ input.cleanups.push(() => input.container.removeEventListener("pointerdown", input.handlePointerDown))
+
+ input.container.addEventListener("click", input.handleLinkClick, { capture: true })
+ input.cleanups.push(() => input.container.removeEventListener("click", input.handleLinkClick, { capture: true }))
+
+ input.term.textarea?.addEventListener("focus", handleTextareaFocus)
+ input.term.textarea?.addEventListener("blur", handleTextareaBlur)
+ input.cleanups.push(() => input.term.textarea?.removeEventListener("focus", handleTextareaFocus))
+ input.cleanups.push(() => input.term.textarea?.removeEventListener("blur", handleTextareaBlur))
+}
+
+const persistTerminal = (input: {
+ term: Term | undefined
+ addon: SerializeAddon | undefined
+ cursor: number
+ pty: LocalPTY
+ onCleanup?: (pty: LocalPTY) => void
+}) => {
+ if (!input.addon || !input.onCleanup || !input.term) return
+ const buffer = (() => {
+ try {
+ return input.addon.serialize()
+ } catch {
+ debugTerminal("failed to serialize terminal buffer")
+ return ""
+ }
+ })()
+
+ input.onCleanup({
+ ...input.pty,
+ buffer,
+ cursor: input.cursor,
+ rows: input.term.rows,
+ cols: input.term.cols,
+ scrollY: input.term.getViewportY(),
+ })
+}
+
export const Terminal = (props: TerminalProps) => {
const platform = usePlatform()
const sdk = useSDK()
@@ -70,8 +155,6 @@ export const Terminal = (props: TerminalProps) => {
let serializeAddon: SerializeAddon
let fitAddon: FitAddon
let handleResize: () => void
- let handleTextareaFocus: () => void
- let handleTextareaBlur: () => void
let disposed = false
const cleanups: VoidFunction[] = []
const start =
@@ -84,12 +167,23 @@ export const Terminal = (props: TerminalProps) => {
for (const fn of fns) {
try {
fn()
- } catch {
- // ignore
+ } catch (err) {
+ debugTerminal("cleanup failed", err)
}
}
}
+ const pushSize = (cols: number, rows: number) => {
+ return sdk.client.pty
+ .update({
+ ptyID: local.pty.id,
+ size: { cols, rows },
+ })
+ .catch((err) => {
+ debugTerminal("failed to sync terminal size", err)
+ })
+ }
+
const getTerminalColors = (): TerminalColors => {
const mode = theme.mode() === "dark" ? "dark" : "light"
const fallback = DEFAULT_TERMINAL_COLORS[mode]
@@ -219,27 +313,6 @@ export const Terminal = (props: TerminalProps) => {
ghostty = g
term = t
- const handleCopy = (event: ClipboardEvent) => {
- const selection = t.getSelection()
- if (!selection) return
-
- const clipboard = event.clipboardData
- if (!clipboard) return
-
- event.preventDefault()
- clipboard.setData("text/plain", selection)
- }
-
- const handlePaste = (event: ClipboardEvent) => {
- const clipboard = event.clipboardData
- const text = clipboard?.getData("text/plain") ?? clipboard?.getData("text") ?? ""
- if (!text) return
-
- event.preventDefault()
- event.stopPropagation()
- t.paste(text)
- }
-
t.attachCustomKeyEventHandler((event) => {
const key = event.key.toLowerCase()
@@ -255,12 +328,6 @@ export const Terminal = (props: TerminalProps) => {
return matchKeybind(keybinds, event)
})
- container.addEventListener("copy", handleCopy, true)
- cleanups.push(() => container.removeEventListener("copy", handleCopy, true))
-
- container.addEventListener("paste", handlePaste, true)
- cleanups.push(() => container.removeEventListener("paste", handlePaste, true))
-
const fit = new mod.FitAddon()
const serializer = new SerializeAddon()
cleanups.push(() => disposeIfDisposable(fit))
@@ -270,24 +337,7 @@ export const Terminal = (props: TerminalProps) => {
serializeAddon = serializer
t.open(container)
-
- container.addEventListener("pointerdown", handlePointerDown)
- cleanups.push(() => container.removeEventListener("pointerdown", handlePointerDown))
-
- container.addEventListener("click", handleLinkClick, { capture: true })
- cleanups.push(() => container.removeEventListener("click", handleLinkClick, { capture: true }))
-
- handleTextareaFocus = () => {
- t.options.cursorBlink = true
- }
- handleTextareaBlur = () => {
- t.options.cursorBlink = false
- }
-
- t.textarea?.addEventListener("focus", handleTextareaFocus)
- t.textarea?.addEventListener("blur", handleTextareaBlur)
- cleanups.push(() => t.textarea?.removeEventListener("focus", handleTextareaFocus))
- cleanups.push(() => t.textarea?.removeEventListener("blur", handleTextareaBlur))
+ useTerminalUiBindings({ container, term: t, cleanups, handlePointerDown, handleLinkClick })
focusTerminal()
@@ -316,15 +366,7 @@ export const Terminal = (props: TerminalProps) => {
const onResize = t.onResize(async (size) => {
if (socket.readyState === WebSocket.OPEN) {
- await sdk.client.pty
- .update({
- ptyID: local.pty.id,
- size: {
- cols: size.cols,
- rows: size.rows,
- },
- })
- .catch(() => {})
+ await pushSize(size.cols, size.rows)
}
})
cleanups.push(() => disposeIfDisposable(onResize))
@@ -346,15 +388,7 @@ export const Terminal = (props: TerminalProps) => {
const handleOpen = () => {
local.onConnect?.()
- sdk.client.pty
- .update({
- ptyID: local.pty.id,
- size: {
- cols: t.cols,
- rows: t.rows,
- },
- })
- .catch(() => {})
+ void pushSize(t.cols, t.rows)
}
socket.addEventListener("open", handleOpen)
cleanups.push(() => socket.removeEventListener("open", handleOpen))
@@ -374,8 +408,8 @@ export const Terminal = (props: TerminalProps) => {
if (typeof next === "number" && Number.isSafeInteger(next) && next >= 0) {
cursor = next
}
- } catch {
- // ignore
+ } catch (err) {
+ debugTerminal("invalid websocket control frame", err)
}
return
}
@@ -425,25 +459,7 @@ export const Terminal = (props: TerminalProps) => {
onCleanup(() => {
disposed = true
- const t = term
- if (serializeAddon && props.onCleanup && t) {
- const buffer = (() => {
- try {
- return serializeAddon.serialize()
- } catch {
- return ""
- }
- })()
- props.onCleanup({
- ...local.pty,
- buffer,
- cursor,
- rows: t.rows,
- cols: t.cols,
- scrollY: t.getViewportY(),
- })
- }
-
+ persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup })
cleanup()
})