diff options
| author | Adam <[email protected]> | 2026-02-10 10:15:19 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-10 10:15:19 -0600 |
| commit | 3929f0b5bd5535fc601b8ce9092929af1adb6629 (patch) | |
| tree | 62aaa849e84ab62b6b960b8378661dfd10dd681e /packages/app | |
| parent | 6f5dfe125aaa82514318ea39d0ae443da37612f6 (diff) | |
| download | opencode-3929f0b5bd5535fc601b8ce9092929af1adb6629.tar.gz opencode-3929f0b5bd5535fc601b8ce9092929af1adb6629.zip | |
fix(app): terminal replay (#12991)
Diffstat (limited to 'packages/app')
| -rw-r--r-- | packages/app/src/components/terminal.tsx | 75 | ||||
| -rw-r--r-- | packages/app/src/context/terminal.tsx | 2 |
2 files changed, 30 insertions, 47 deletions
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 3baafe511..97491d0d3 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -74,7 +74,9 @@ export const Terminal = (props: TerminalProps) => { let handleTextareaBlur: () => void let disposed = false const cleanups: VoidFunction[] = [] - let tail = local.pty.tail ?? "" + const start = + typeof local.pty.cursor === "number" && Number.isSafeInteger(local.pty.cursor) ? local.pty.cursor : undefined + let cursor = start ?? 0 const cleanup = () => { if (!cleanups.length) return @@ -164,13 +166,16 @@ export const Terminal = (props: TerminalProps) => { const once = { value: false } - const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`) + 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() }) @@ -289,26 +294,6 @@ export const Terminal = (props: TerminalProps) => { handleResize = () => fit.fit() window.addEventListener("resize", handleResize) cleanups.push(() => window.removeEventListener("resize", handleResize)) - const limit = 16_384 - const min = 32 - const windowMs = 750 - const seed = tail.length > limit ? tail.slice(-limit) : tail - let sync = seed.length >= min - let syncUntil = 0 - const stopSync = () => { - sync = false - syncUntil = 0 - } - - const overlap = (data: string) => { - if (!seed) return 0 - const max = Math.min(seed.length, data.length) - if (max < min) return 0 - for (let i = max; i >= min; i--) { - if (seed.slice(-i) === data.slice(0, i)) return i - } - return 0 - } const onResize = t.onResize(async (size) => { if (socket.readyState === WebSocket.OPEN) { @@ -325,7 +310,6 @@ export const Terminal = (props: TerminalProps) => { }) cleanups.push(() => disposeIfDisposable(onResize)) const onData = t.onData((data) => { - if (data) stopSync() if (socket.readyState === WebSocket.OPEN) { socket.send(data) } @@ -343,7 +327,6 @@ export const Terminal = (props: TerminalProps) => { const handleOpen = () => { local.onConnect?.() - if (sync) syncUntil = Date.now() + windowMs sdk.client.pty .update({ ptyID: local.pty.id, @@ -357,31 +340,31 @@ export const Terminal = (props: TerminalProps) => { socket.addEventListener("open", handleOpen) cleanups.push(() => socket.removeEventListener("open", handleOpen)) + const decoder = new TextDecoder() + const handleMessage = (event: MessageEvent) => { if (disposed) return - const data = typeof event.data === "string" ? event.data : "" - if (!data) return - - const next = (() => { - if (!sync) return data - if (syncUntil && Date.now() > syncUntil) { - stopSync() - return data - } - const n = overlap(data) - if (!n) { - stopSync() - return data + 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)) + try { + const meta = JSON.parse(json) as { cursor?: unknown } + const next = meta?.cursor + if (typeof next === "number" && Number.isSafeInteger(next) && next >= 0) { + cursor = next + } + } catch { + // ignore } - const trimmed = data.slice(n) - if (trimmed) stopSync() - return trimmed - })() - - if (!next) return + return + } - t.write(next) - tail = next.length >= limit ? next.slice(-limit) : (tail + next).slice(-limit) + const data = typeof event.data === "string" ? event.data : "" + if (!data) return + t.write(data) + cursor += data.length } socket.addEventListener("message", handleMessage) cleanups.push(() => socket.removeEventListener("message", handleMessage)) @@ -435,7 +418,7 @@ export const Terminal = (props: TerminalProps) => { props.onCleanup({ ...local.pty, buffer, - tail, + cursor, rows: t.rows, cols: t.cols, scrollY: t.getViewportY(), diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index 76e8cf0f7..f0f184f8b 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -13,7 +13,7 @@ export type LocalPTY = { cols?: number buffer?: string scrollY?: number - tail?: string + cursor?: number } const WORKSPACE_KEY = "__workspace__" |
