@@ -52,6 +52,7 @@ import { selectTerminalEventEntries, useTerminalStateStore } from "../terminalSt
5252const MIN_DRAWER_HEIGHT = 180 ;
5353const MAX_DRAWER_HEIGHT_RATIO = 0.75 ;
5454const MULTI_CLICK_SELECTION_ACTION_DELAY_MS = 260 ;
55+ const MAX_APPLIED_TERMINAL_EVENT_KEYS = 500 ;
5556
5657function maxDrawerHeight ( ) : number {
5758 if ( typeof window === "undefined" ) return DEFAULT_THREAD_TERMINAL_HEIGHT ;
@@ -89,6 +90,45 @@ export function selectPendingTerminalEventEntries(
8990 return entries . filter ( ( entry ) => entry . id > lastAppliedTerminalEventId ) ;
9091}
9192
93+ function terminalEventDedupeKey ( event : TerminalEvent ) : string {
94+ switch ( event . type ) {
95+ case "output" :
96+ return [ event . threadId , event . terminalId , event . createdAt , event . type , event . data ] . join ( "\0" ) ;
97+ case "started" :
98+ case "restarted" :
99+ return [
100+ event . threadId ,
101+ event . terminalId ,
102+ event . createdAt ,
103+ event . type ,
104+ event . snapshot . updatedAt ,
105+ ] . join ( "\0" ) ;
106+ case "activity" :
107+ return [
108+ event . threadId ,
109+ event . terminalId ,
110+ event . createdAt ,
111+ event . type ,
112+ String ( event . hasRunningSubprocess ) ,
113+ ] . join ( "\0" ) ;
114+ case "error" :
115+ return [ event . threadId , event . terminalId , event . createdAt , event . type , event . message ] . join (
116+ "\0" ,
117+ ) ;
118+ case "exited" :
119+ return [
120+ event . threadId ,
121+ event . terminalId ,
122+ event . createdAt ,
123+ event . type ,
124+ String ( event . exitCode ) ,
125+ String ( event . exitSignal ) ,
126+ ] . join ( "\0" ) ;
127+ case "cleared" :
128+ return [ event . threadId , event . terminalId , event . createdAt , event . type ] . join ( "\0" ) ;
129+ }
130+ }
131+
92132function normalizeComputedColor ( value : string | null | undefined , fallback : string ) : string {
93133 const normalizedValue = value ?. trim ( ) . toLowerCase ( ) ;
94134 if (
@@ -292,6 +332,7 @@ export function TerminalViewport({
292332 const selectionActionTimerRef = useRef < number | null > ( null ) ;
293333 const keybindingsRef = useRef ( keybindings ) ;
294334 const lastAppliedTerminalEventIdRef = useRef ( 0 ) ;
335+ const appliedTerminalEventKeysRef = useRef < Set < string > > ( new Set ( ) ) ;
295336 const terminalHydratedRef = useRef ( false ) ;
296337 const handleSessionExited = useEffectEvent ( ( ) => {
297338 onSessionExited ( ) ;
@@ -405,6 +446,40 @@ export function TerminalViewport({
405446 }
406447 } ;
407448
449+ const terminalOpenInput = ( ) => {
450+ const activeTerminal = terminalRef . current ;
451+ const activeFitAddon = fitAddonRef . current ;
452+ if ( ! activeTerminal || ! activeFitAddon ) {
453+ return null ;
454+ }
455+ activeFitAddon . fit ( ) ;
456+ return {
457+ threadId,
458+ terminalId,
459+ cwd,
460+ ...( worktreePath !== undefined ? { worktreePath } : { } ) ,
461+ cols : activeTerminal . cols ,
462+ rows : activeTerminal . rows ,
463+ ...( runtimeEnv ? { env : runtimeEnv } : { } ) ,
464+ } ;
465+ } ;
466+
467+ const markTerminalEventApplied = ( event : TerminalEvent ) => {
468+ const key = terminalEventDedupeKey ( event ) ;
469+ const keys = appliedTerminalEventKeysRef . current ;
470+ if ( keys . has ( key ) ) {
471+ return false ;
472+ }
473+ keys . add ( key ) ;
474+ if ( keys . size > MAX_APPLIED_TERMINAL_EVENT_KEYS ) {
475+ const oldestKey = keys . values ( ) . next ( ) . value ;
476+ if ( typeof oldestKey === "string" ) {
477+ keys . delete ( oldestKey ) ;
478+ }
479+ }
480+ return true ;
481+ } ;
482+
408483 const sendTerminalInput = async ( data : string , fallbackError : string ) => {
409484 const activeTerminal = terminalRef . current ;
410485 if ( ! activeTerminal ) return ;
@@ -514,14 +589,7 @@ export function TerminalViewport({
514589 } ) ;
515590
516591 const inputDisposable = terminal . onData ( ( data ) => {
517- void api . terminal
518- . write ( { threadId, terminalId, data } )
519- . catch ( ( err ) =>
520- writeSystemMessage (
521- terminal ,
522- err instanceof Error ? err . message : "Terminal write failed" ,
523- ) ,
524- ) ;
592+ void sendTerminalInput ( data , "Terminal write failed" ) ;
525593 } ) ;
526594
527595 const selectionDisposable = terminal . onSelectionChange ( ( ) => {
@@ -572,6 +640,9 @@ export function TerminalViewport({
572640 if ( ! activeTerminal ) {
573641 return ;
574642 }
643+ if ( ! markTerminalEventApplied ( event ) ) {
644+ return ;
645+ }
575646
576647 if ( event . type === "activity" ) {
577648 return ;
@@ -663,22 +734,22 @@ export function TerminalViewport({
663734
664735 applyPendingTerminalEvents ( nextEntries ) ;
665736 } ) ;
737+ const unsubscribeLiveTerminalEvents = api . terminal . onEvent ( ( event ) => {
738+ if ( event . threadId !== threadId || event . terminalId !== terminalId ) {
739+ return ;
740+ }
741+ applyTerminalEvent ( event ) ;
742+ } ) ;
666743
667744 const openTerminal = async ( ) => {
668745 try {
669746 const activeTerminal = terminalRef . current ;
670747 const activeFitAddon = fitAddonRef . current ;
671748 if ( ! activeTerminal || ! activeFitAddon ) return ;
672749 activeFitAddon . fit ( ) ;
673- const snapshot = await api . terminal . open ( {
674- threadId,
675- terminalId,
676- cwd,
677- ...( worktreePath !== undefined ? { worktreePath } : { } ) ,
678- cols : activeTerminal . cols ,
679- rows : activeTerminal . rows ,
680- ...( runtimeEnv ? { env : runtimeEnv } : { } ) ,
681- } ) ;
750+ const input = terminalOpenInput ( ) ;
751+ if ( ! input ) return ;
752+ const snapshot = await api . terminal . open ( input ) ;
682753 if ( disposed ) return ;
683754 writeTerminalSnapshot ( activeTerminal , snapshot ) ;
684755 const bufferedEntries = selectTerminalEventEntries (
@@ -734,7 +805,9 @@ export function TerminalViewport({
734805 disposed = true ;
735806 terminalHydratedRef . current = false ;
736807 lastAppliedTerminalEventIdRef . current = 0 ;
808+ appliedTerminalEventKeysRef . current . clear ( ) ;
737809 unsubscribeTerminalEvents ( ) ;
810+ unsubscribeLiveTerminalEvents ( ) ;
738811 window . clearTimeout ( fitTimer ) ;
739812 inputDisposable . dispose ( ) ;
740813 selectionDisposable . dispose ( ) ;
0 commit comments