@@ -74,7 +74,9 @@ export const Terminal = (props: TerminalProps) => {
7474 let handleTextareaBlur : ( ) => void
7575 let disposed = false
7676 const cleanups : VoidFunction [ ] = [ ]
77- let tail = local . pty . tail ?? ""
77+ const start =
78+ typeof local . pty . cursor === "number" && Number . isSafeInteger ( local . pty . cursor ) ? local . pty . cursor : undefined
79+ let cursor = start ?? 0
7880
7981 const cleanup = ( ) => {
8082 if ( ! cleanups . length ) return
@@ -164,13 +166,16 @@ export const Terminal = (props: TerminalProps) => {
164166
165167 const once = { value : false }
166168
167- const url = new URL ( sdk . url + `/pty/${ local . pty . id } /connect?directory=${ encodeURIComponent ( sdk . directory ) } ` )
169+ const url = new URL ( sdk . url + `/pty/${ local . pty . id } /connect` )
170+ url . searchParams . set ( "directory" , sdk . directory )
171+ url . searchParams . set ( "cursor" , String ( start !== undefined ? start : local . pty . buffer ? - 1 : 0 ) )
168172 url . protocol = url . protocol === "https:" ? "wss:" : "ws:"
169173 if ( window . __OPENCODE__ ?. serverPassword ) {
170174 url . username = "opencode"
171175 url . password = window . __OPENCODE__ ?. serverPassword
172176 }
173177 const socket = new WebSocket ( url )
178+ socket . binaryType = "arraybuffer"
174179 cleanups . push ( ( ) => {
175180 if ( socket . readyState !== WebSocket . CLOSED && socket . readyState !== WebSocket . CLOSING ) socket . close ( )
176181 } )
@@ -289,26 +294,6 @@ export const Terminal = (props: TerminalProps) => {
289294 handleResize = ( ) => fit . fit ( )
290295 window . addEventListener ( "resize" , handleResize )
291296 cleanups . push ( ( ) => window . removeEventListener ( "resize" , handleResize ) )
292- const limit = 16_384
293- const min = 32
294- const windowMs = 750
295- const seed = tail . length > limit ? tail . slice ( - limit ) : tail
296- let sync = seed . length >= min
297- let syncUntil = 0
298- const stopSync = ( ) => {
299- sync = false
300- syncUntil = 0
301- }
302-
303- const overlap = ( data : string ) => {
304- if ( ! seed ) return 0
305- const max = Math . min ( seed . length , data . length )
306- if ( max < min ) return 0
307- for ( let i = max ; i >= min ; i -- ) {
308- if ( seed . slice ( - i ) === data . slice ( 0 , i ) ) return i
309- }
310- return 0
311- }
312297
313298 const onResize = t . onResize ( async ( size ) => {
314299 if ( socket . readyState === WebSocket . OPEN ) {
@@ -325,7 +310,6 @@ export const Terminal = (props: TerminalProps) => {
325310 } )
326311 cleanups . push ( ( ) => disposeIfDisposable ( onResize ) )
327312 const onData = t . onData ( ( data ) => {
328- if ( data ) stopSync ( )
329313 if ( socket . readyState === WebSocket . OPEN ) {
330314 socket . send ( data )
331315 }
@@ -343,7 +327,6 @@ export const Terminal = (props: TerminalProps) => {
343327
344328 const handleOpen = ( ) => {
345329 local . onConnect ?.( )
346- if ( sync ) syncUntil = Date . now ( ) + windowMs
347330 sdk . client . pty
348331 . update ( {
349332 ptyID : local . pty . id ,
@@ -357,31 +340,31 @@ export const Terminal = (props: TerminalProps) => {
357340 socket . addEventListener ( "open" , handleOpen )
358341 cleanups . push ( ( ) => socket . removeEventListener ( "open" , handleOpen ) )
359342
343+ const decoder = new TextDecoder ( )
344+
360345 const handleMessage = ( event : MessageEvent ) => {
361346 if ( disposed ) return
362- const data = typeof event . data === "string" ? event . data : ""
363- if ( ! data ) return
364-
365- const next = ( ( ) => {
366- if ( ! sync ) return data
367- if ( syncUntil && Date . now ( ) > syncUntil ) {
368- stopSync ( )
369- return data
370- }
371- const n = overlap ( data )
372- if ( ! n ) {
373- stopSync ( )
374- return data
347+ if ( event . data instanceof ArrayBuffer ) {
348+ // WebSocket control frame: 0x00 + UTF-8 JSON (currently { cursor }).
349+ const bytes = new Uint8Array ( event . data )
350+ if ( bytes [ 0 ] !== 0 ) return
351+ const json = decoder . decode ( bytes . subarray ( 1 ) )
352+ try {
353+ const meta = JSON . parse ( json ) as { cursor ?: unknown }
354+ const next = meta ?. cursor
355+ if ( typeof next === "number" && Number . isSafeInteger ( next ) && next >= 0 ) {
356+ cursor = next
357+ }
358+ } catch {
359+ // ignore
375360 }
376- const trimmed = data . slice ( n )
377- if ( trimmed ) stopSync ( )
378- return trimmed
379- } ) ( )
380-
381- if ( ! next ) return
361+ return
362+ }
382363
383- t . write ( next )
384- tail = next . length >= limit ? next . slice ( - limit ) : ( tail + next ) . slice ( - limit )
364+ const data = typeof event . data === "string" ? event . data : ""
365+ if ( ! data ) return
366+ t . write ( data )
367+ cursor += data . length
385368 }
386369 socket . addEventListener ( "message" , handleMessage )
387370 cleanups . push ( ( ) => socket . removeEventListener ( "message" , handleMessage ) )
@@ -435,7 +418,7 @@ export const Terminal = (props: TerminalProps) => {
435418 props . onCleanup ( {
436419 ...local . pty ,
437420 buffer,
438- tail ,
421+ cursor ,
439422 rows : t . rows ,
440423 cols : t . cols ,
441424 scrollY : t . getViewportY ( ) ,
0 commit comments