diff options
| author | Luke Parker <[email protected]> | 2026-03-12 17:35:26 +1000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-12 17:35:26 +1000 |
| commit | 328c6de80d51704c09bdd962df2ddf5b9d7c82ea (patch) | |
| tree | bf6426b3afeb824feaae73aa22cfc20e6e0d438d /packages/app/src | |
| parent | c9c0318e0e5c2fcd80fc1c32a1ccfe360f182f90 (diff) | |
| download | opencode-328c6de80d51704c09bdd962df2ddf5b9d7c82ea.tar.gz opencode-328c6de80d51704c09bdd962df2ddf5b9d7c82ea.zip | |
Fix terminal e2e flakiness with a real terminal driver (#17144)
Diffstat (limited to 'packages/app/src')
| -rw-r--r-- | packages/app/src/components/terminal.tsx | 19 | ||||
| -rw-r--r-- | packages/app/src/testing/terminal.ts | 64 |
2 files changed, 78 insertions, 5 deletions
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index fda66917f..840903293 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -10,6 +10,7 @@ import { useSDK } from "@/context/sdk" import { useServer } from "@/context/server" import { monoFontFamily, useSettings } from "@/context/settings" import type { LocalPTY } from "@/context/terminal" +import { terminalAttr, terminalProbe } from "@/testing/terminal" import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters" import { terminalWriter } from "@/utils/terminal-writer" @@ -160,6 +161,7 @@ export const Terminal = (props: TerminalProps) => { let container!: HTMLDivElement const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"]) const id = local.pty.id + const probe = terminalProbe(id) const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : "" const restoreSize = restore && @@ -326,6 +328,9 @@ export const Terminal = (props: TerminalProps) => { } onMount(() => { + probe.init() + cleanups.push(() => probe.drop()) + const run = async () => { const loaded = await loadGhostty() if (disposed) return @@ -353,7 +358,13 @@ export const Terminal = (props: TerminalProps) => { } ghostty = g term = t - output = terminalWriter((data, done) => t.write(data, done)) + output = terminalWriter((data, done) => + t.write(data, () => { + probe.render(data) + probe.settle() + done?.() + }), + ) t.attachCustomKeyEventHandler((event) => { const key = event.key.toLowerCase() @@ -441,10 +452,6 @@ export const Terminal = (props: TerminalProps) => { startResize() } - // t.onScroll((ydisp) => { - // console.log("Scroll position:", ydisp) - // }) - const once = { value: false } let closing = false @@ -460,6 +467,7 @@ export const Terminal = (props: TerminalProps) => { ws = socket const handleOpen = () => { + probe.connect() local.onConnect?.() scheduleSize(t.cols, t.rows) } @@ -560,6 +568,7 @@ export const Terminal = (props: TerminalProps) => { <div ref={container} data-component="terminal" + {...{ [terminalAttr]: id }} data-prevent-autofocus tabIndex={-1} style={{ "background-color": terminalColors().background }} diff --git a/packages/app/src/testing/terminal.ts b/packages/app/src/testing/terminal.ts new file mode 100644 index 000000000..aa1974404 --- /dev/null +++ b/packages/app/src/testing/terminal.ts @@ -0,0 +1,64 @@ +export const terminalAttr = "data-pty-id" + +export type TerminalProbeState = { + connected: boolean + rendered: string + settled: number +} + +export type E2EWindow = Window & { + __opencode_e2e?: { + terminal?: { + enabled?: boolean + terminals?: Record<string, TerminalProbeState> + } + } +} + +const seed = (): TerminalProbeState => ({ + connected: false, + rendered: "", + settled: 0, +}) + +const root = () => { + if (typeof window === "undefined") return + const state = (window as E2EWindow).__opencode_e2e?.terminal + if (!state?.enabled) return + state.terminals ??= {} + return state.terminals +} + +export const terminalProbe = (id: string) => { + const set = (next: Partial<TerminalProbeState>) => { + const terms = root() + if (!terms) return + terms[id] = { ...(terms[id] ?? seed()), ...next } + } + + return { + init() { + set(seed()) + }, + connect() { + set({ connected: true }) + }, + render(data: string) { + const terms = root() + if (!terms) return + const prev = terms[id] ?? seed() + terms[id] = { ...prev, rendered: prev.rendered + data } + }, + settle() { + const terms = root() + if (!terms) return + const prev = terms[id] ?? seed() + terms[id] = { ...prev, settled: prev.settled + 1 } + }, + drop() { + const terms = root() + if (!terms) return + delete terms[id] + }, + } +} |
