summaryrefslogtreecommitdiffhomepage
path: root/packages/app
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-10 10:15:19 -0600
committerGitHub <[email protected]>2026-02-10 10:15:19 -0600
commit3929f0b5bd5535fc601b8ce9092929af1adb6629 (patch)
tree62aaa849e84ab62b6b960b8378661dfd10dd681e /packages/app
parent6f5dfe125aaa82514318ea39d0ae443da37612f6 (diff)
downloadopencode-3929f0b5bd5535fc601b8ce9092929af1adb6629.tar.gz
opencode-3929f0b5bd5535fc601b8ce9092929af1adb6629.zip
fix(app): terminal replay (#12991)
Diffstat (limited to 'packages/app')
-rw-r--r--packages/app/src/components/terminal.tsx75
-rw-r--r--packages/app/src/context/terminal.tsx2
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__"