|
@@ -74,7 +74,9 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
let handleTextareaBlur: () => void
|
|
let handleTextareaBlur: () => void
|
|
|
let disposed = false
|
|
let disposed = false
|
|
|
const cleanups: VoidFunction[] = []
|
|
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 = () => {
|
|
const cleanup = () => {
|
|
|
if (!cleanups.length) return
|
|
if (!cleanups.length) return
|
|
@@ -164,13 +166,16 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
|
|
|
|
|
const once = { value: false }
|
|
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:"
|
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
|
|
if (window.__OPENCODE__?.serverPassword) {
|
|
if (window.__OPENCODE__?.serverPassword) {
|
|
|
url.username = "opencode"
|
|
url.username = "opencode"
|
|
|
url.password = window.__OPENCODE__?.serverPassword
|
|
url.password = window.__OPENCODE__?.serverPassword
|
|
|
}
|
|
}
|
|
|
const socket = new WebSocket(url)
|
|
const socket = new WebSocket(url)
|
|
|
|
|
+ socket.binaryType = "arraybuffer"
|
|
|
cleanups.push(() => {
|
|
cleanups.push(() => {
|
|
|
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close()
|
|
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close()
|
|
|
})
|
|
})
|
|
@@ -289,26 +294,6 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
handleResize = () => fit.fit()
|
|
handleResize = () => fit.fit()
|
|
|
window.addEventListener("resize", handleResize)
|
|
window.addEventListener("resize", handleResize)
|
|
|
cleanups.push(() => window.removeEventListener("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) => {
|
|
const onResize = t.onResize(async (size) => {
|
|
|
if (socket.readyState === WebSocket.OPEN) {
|
|
if (socket.readyState === WebSocket.OPEN) {
|
|
@@ -325,7 +310,6 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
})
|
|
})
|
|
|
cleanups.push(() => disposeIfDisposable(onResize))
|
|
cleanups.push(() => disposeIfDisposable(onResize))
|
|
|
const onData = t.onData((data) => {
|
|
const onData = t.onData((data) => {
|
|
|
- if (data) stopSync()
|
|
|
|
|
if (socket.readyState === WebSocket.OPEN) {
|
|
if (socket.readyState === WebSocket.OPEN) {
|
|
|
socket.send(data)
|
|
socket.send(data)
|
|
|
}
|
|
}
|
|
@@ -343,7 +327,6 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
|
|
|
|
|
const handleOpen = () => {
|
|
const handleOpen = () => {
|
|
|
local.onConnect?.()
|
|
local.onConnect?.()
|
|
|
- if (sync) syncUntil = Date.now() + windowMs
|
|
|
|
|
sdk.client.pty
|
|
sdk.client.pty
|
|
|
.update({
|
|
.update({
|
|
|
ptyID: local.pty.id,
|
|
ptyID: local.pty.id,
|
|
@@ -357,31 +340,31 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
socket.addEventListener("open", handleOpen)
|
|
socket.addEventListener("open", handleOpen)
|
|
|
cleanups.push(() => socket.removeEventListener("open", handleOpen))
|
|
cleanups.push(() => socket.removeEventListener("open", handleOpen))
|
|
|
|
|
|
|
|
|
|
+ const decoder = new TextDecoder()
|
|
|
|
|
+
|
|
|
const handleMessage = (event: MessageEvent) => {
|
|
const handleMessage = (event: MessageEvent) => {
|
|
|
if (disposed) return
|
|
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)
|
|
socket.addEventListener("message", handleMessage)
|
|
|
cleanups.push(() => socket.removeEventListener("message", handleMessage))
|
|
cleanups.push(() => socket.removeEventListener("message", handleMessage))
|
|
@@ -435,7 +418,7 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
props.onCleanup({
|
|
props.onCleanup({
|
|
|
...local.pty,
|
|
...local.pty,
|
|
|
buffer,
|
|
buffer,
|
|
|
- tail,
|
|
|
|
|
|
|
+ cursor,
|
|
|
rows: t.rows,
|
|
rows: t.rows,
|
|
|
cols: t.cols,
|
|
cols: t.cols,
|
|
|
scrollY: t.getViewportY(),
|
|
scrollY: t.getViewportY(),
|