@@ -156,6 +156,10 @@ export const Terminal = (props: TerminalProps) => {
156156 let serializeAddon : SerializeAddon
157157 let fitAddon : FitAddon
158158 let handleResize : ( ) => void
159+ let fitFrame : number | undefined
160+ let sizeTimer : ReturnType < typeof setTimeout > | undefined
161+ let pendingSize : { cols : number ; rows : number } | undefined
162+ let lastSize : { cols : number ; rows : number } | undefined
159163 let disposed = false
160164 const cleanups : VoidFunction [ ] = [ ]
161165 const start =
@@ -209,6 +213,43 @@ export const Terminal = (props: TerminalProps) => {
209213
210214 const [ terminalColors , setTerminalColors ] = createSignal < TerminalColors > ( getTerminalColors ( ) )
211215
216+ const scheduleFit = ( ) => {
217+ if ( disposed ) return
218+ if ( ! fitAddon ) return
219+ if ( fitFrame !== undefined ) return
220+
221+ fitFrame = requestAnimationFrame ( ( ) => {
222+ fitFrame = undefined
223+ if ( disposed ) return
224+ fitAddon . fit ( )
225+ } )
226+ }
227+
228+ const scheduleSize = ( cols : number , rows : number ) => {
229+ if ( disposed ) return
230+ if ( lastSize ?. cols === cols && lastSize ?. rows === rows ) return
231+
232+ pendingSize = { cols, rows }
233+
234+ if ( ! lastSize ) {
235+ lastSize = pendingSize
236+ void pushSize ( cols , rows )
237+ return
238+ }
239+
240+ if ( sizeTimer !== undefined ) return
241+ sizeTimer = setTimeout ( ( ) => {
242+ sizeTimer = undefined
243+ const next = pendingSize
244+ if ( ! next ) return
245+ pendingSize = undefined
246+ if ( disposed ) return
247+ if ( lastSize ?. cols === next . cols && lastSize ?. rows === next . rows ) return
248+ lastSize = next
249+ void pushSize ( next . cols , next . rows )
250+ } , 100 )
251+ }
252+
212253 createEffect ( ( ) => {
213254 const colors = getTerminalColors ( )
214255 setTerminalColors ( colors )
@@ -220,6 +261,16 @@ export const Terminal = (props: TerminalProps) => {
220261 const font = monoFontFamily ( settings . appearance . font ( ) )
221262 if ( ! term ) return
222263 setOptionIfSupported ( term , "fontFamily" , font )
264+ scheduleFit ( )
265+ } )
266+
267+ let zoom = platform . webviewZoom ?.( )
268+ createEffect ( ( ) => {
269+ const next = platform . webviewZoom ?.( )
270+ if ( next === undefined ) return
271+ if ( next === zoom ) return
272+ zoom = next
273+ scheduleFit ( )
223274 } )
224275
225276 const focusTerminal = ( ) => {
@@ -263,25 +314,6 @@ export const Terminal = (props: TerminalProps) => {
263314
264315 const once = { value : false }
265316
266- const url = new URL ( sdk . url + `/pty/${ local . pty . id } /connect` )
267- url . searchParams . set ( "directory" , sdk . directory )
268- url . searchParams . set ( "cursor" , String ( start !== undefined ? start : local . pty . buffer ? - 1 : 0 ) )
269- url . protocol = url . protocol === "https:" ? "wss:" : "ws:"
270- if ( window . __OPENCODE__ ?. serverPassword ) {
271- url . username = "opencode"
272- url . password = window . __OPENCODE__ ?. serverPassword
273- }
274- const socket = new WebSocket ( url )
275- socket . binaryType = "arraybuffer"
276- cleanups . push ( ( ) => {
277- if ( socket . readyState !== WebSocket . CLOSED && socket . readyState !== WebSocket . CLOSING ) socket . close ( )
278- } )
279- if ( disposed ) {
280- cleanup ( )
281- return
282- }
283- ws = socket
284-
285317 const restore = typeof local . pty . buffer === "string" ? local . pty . buffer : ""
286318 const restoreSize =
287319 restore &&
@@ -344,21 +376,42 @@ export const Terminal = (props: TerminalProps) => {
344376
345377 focusTerminal ( )
346378
379+ if ( typeof document !== "undefined" && document . fonts ) {
380+ document . fonts . ready . then ( scheduleFit )
381+ }
382+
383+ const onResize = t . onResize ( ( size ) => {
384+ scheduleSize ( size . cols , size . rows )
385+ } )
386+ cleanups . push ( ( ) => disposeIfDisposable ( onResize ) )
387+ const onData = t . onData ( ( data ) => {
388+ if ( ws ?. readyState === WebSocket . OPEN ) ws . send ( data )
389+ } )
390+ cleanups . push ( ( ) => disposeIfDisposable ( onData ) )
391+ const onKey = t . onKey ( ( key ) => {
392+ if ( key . key == "Enter" ) {
393+ props . onSubmit ?.( )
394+ }
395+ } )
396+ cleanups . push ( ( ) => disposeIfDisposable ( onKey ) )
397+
347398 const startResize = ( ) => {
348399 fit . observeResize ( )
349- handleResize = ( ) => fit . fit ( )
400+ handleResize = scheduleFit
350401 window . addEventListener ( "resize" , handleResize )
351402 cleanups . push ( ( ) => window . removeEventListener ( "resize" , handleResize ) )
352403 }
353404
354405 if ( restore && restoreSize ) {
355406 t . write ( restore , ( ) => {
356407 fit . fit ( )
408+ scheduleSize ( t . cols , t . rows )
357409 if ( typeof local . pty . scrollY === "number" ) t . scrollToLine ( local . pty . scrollY )
358410 startResize ( )
359411 } )
360412 } else {
361413 fit . fit ( )
414+ scheduleSize ( t . cols , t . rows )
362415 if ( restore ) {
363416 t . write ( restore , ( ) => {
364417 if ( typeof local . pty . scrollY === "number" ) t . scrollToLine ( local . pty . scrollY )
@@ -367,35 +420,38 @@ export const Terminal = (props: TerminalProps) => {
367420 startResize ( )
368421 }
369422
370- const onResize = t . onResize ( async ( size ) => {
371- if ( socket . readyState === WebSocket . OPEN ) {
372- await pushSize ( size . cols , size . rows )
373- }
374- } )
375- cleanups . push ( ( ) => disposeIfDisposable ( onResize ) )
376- const onData = t . onData ( ( data ) => {
377- if ( socket . readyState === WebSocket . OPEN ) {
378- socket . send ( data )
379- }
380- } )
381- cleanups . push ( ( ) => disposeIfDisposable ( onData ) )
382- const onKey = t . onKey ( ( key ) => {
383- if ( key . key == "Enter" ) {
384- props . onSubmit ?.( )
385- }
386- } )
387- cleanups . push ( ( ) => disposeIfDisposable ( onKey ) )
388423 // t.onScroll((ydisp) => {
389424 // console.log("Scroll position:", ydisp)
390425 // })
391426
427+ const url = new URL ( sdk . url + `/pty/${ local . pty . id } /connect` )
428+ url . searchParams . set ( "directory" , sdk . directory )
429+ url . searchParams . set ( "cursor" , String ( start !== undefined ? start : local . pty . buffer ? - 1 : 0 ) )
430+ url . protocol = url . protocol === "https:" ? "wss:" : "ws:"
431+ if ( window . __OPENCODE__ ?. serverPassword ) {
432+ url . username = "opencode"
433+ url . password = window . __OPENCODE__ ?. serverPassword
434+ }
435+ const socket = new WebSocket ( url )
436+ socket . binaryType = "arraybuffer"
437+ ws = socket
438+ cleanups . push ( ( ) => {
439+ if ( socket . readyState !== WebSocket . CLOSED && socket . readyState !== WebSocket . CLOSING ) socket . close ( )
440+ } )
441+ if ( disposed ) {
442+ cleanup ( )
443+ return
444+ }
445+
392446 const handleOpen = ( ) => {
393447 local . onConnect ?.( )
394- void pushSize ( t . cols , t . rows )
448+ scheduleSize ( t . cols , t . rows )
395449 }
396450 socket . addEventListener ( "open" , handleOpen )
397451 cleanups . push ( ( ) => socket . removeEventListener ( "open" , handleOpen ) )
398452
453+ if ( socket . readyState === WebSocket . OPEN ) handleOpen ( )
454+
399455 const decoder = new TextDecoder ( )
400456
401457 const handleMessage = ( event : MessageEvent ) => {
@@ -462,6 +518,8 @@ export const Terminal = (props: TerminalProps) => {
462518
463519 onCleanup ( ( ) => {
464520 disposed = true
521+ if ( fitFrame !== undefined ) cancelAnimationFrame ( fitFrame )
522+ if ( sizeTimer !== undefined ) clearTimeout ( sizeTimer )
465523 output ?. flush ( )
466524 persistTerminal ( { term, addon : serializeAddon , cursor, pty : local . pty , onCleanup : props . onCleanup } )
467525 cleanup ( )
@@ -477,7 +535,7 @@ export const Terminal = (props: TerminalProps) => {
477535 classList = { {
478536 ...( local . classList ?? { } ) ,
479537 "select-text" : true ,
480- "size-full px-6 py-3 font-mono" : true ,
538+ "size-full px-6 py-3 font-mono relative overflow-hidden " : true ,
481539 [ local . class ?? "" ] : ! ! local . class ,
482540 } }
483541 { ...others }
0 commit comments