From 77cb02a8460a2bc9891707c81b7b78cd7b2ddb9d Mon Sep 17 00:00:00 2001 From: zhravan Date: Sun, 7 Dec 2025 12:29:01 +0530 Subject: [PATCH 1/2] fix: unsynced line buffer on backspace or on keyboard type --- api/api/versions.json | 2 +- api/internal/features/terminal/init.go | 8 ++--- view/app/terminal/terminal.tsx | 12 ++++++- view/app/terminal/utils/useTerminal.ts | 49 ++++++++++++++++++++++---- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/api/api/versions.json b/api/api/versions.json index 8216f0154..41c5a0f80 100644 --- a/api/api/versions.json +++ b/api/api/versions.json @@ -3,7 +3,7 @@ { "version": "v1", "status": "active", - "release_date": "2025-12-06T19:40:52.670434+05:30", + "release_date": "2025-12-07T06:51:02.836994+05:30", "end_of_life": "0001-01-01T00:00:00Z", "changes": [ "Initial API version" diff --git a/api/internal/features/terminal/init.go b/api/internal/features/terminal/init.go index 534956cf5..b3883e541 100644 --- a/api/internal/features/terminal/init.go +++ b/api/internal/features/terminal/init.go @@ -161,12 +161,8 @@ func (t *Terminal) readOutput(r io.Reader) { return } - func() { - t.wsLock.Lock() - defer t.wsLock.Unlock() - t.outputBuf = append(t.outputBuf, buf[:n]...) - }() - + // Send output immediately to frontend + // Don't buffer - this prevents duplicate sends msg := TerminalMessage{ TerminalId: t.TerminalId, Type: "stdout", diff --git a/view/app/terminal/terminal.tsx b/view/app/terminal/terminal.tsx index 986815877..6cd44edbf 100644 --- a/view/app/terminal/terminal.tsx +++ b/view/app/terminal/terminal.tsx @@ -63,8 +63,11 @@ const TerminalSession: React.FC<{ useEffect(() => { if (isTerminalOpen && isActive && isContainerReady) { initializeTerminal(); + } else { + // Cleanup: destroy terminal when it's closed or becomes inactive + destroyTerminal(); } - }, [isTerminalOpen, isActive, isContainerReady, initializeTerminal]); + }, [isTerminalOpen, isActive, isContainerReady, initializeTerminal, destroyTerminal]); useEffect(() => { if (fitAddonRef) { @@ -72,6 +75,13 @@ const TerminalSession: React.FC<{ } }, [fitAddonRef, setFitAddonRef]); + // Cleanup on unmount + useEffect(() => { + return () => { + destroyTerminal(); + }; + }, [destroyTerminal]); + return (
(null); const resizeTimeoutRef = useRef(undefined); + const terminalInstanceRef = useRef(null); const destroyTerminal = useCallback(() => { - if (terminalInstance) { - terminalInstance.dispose(); + const instance = terminalInstanceRef.current; + if (instance) { + instance.dispose(); + terminalInstanceRef.current = null; setTerminalInstance(null); } + // Clear the terminal container to remove any stale input + if (terminalRef.current) { + terminalRef.current.innerHTML = ''; + } if (resizeTimeoutRef.current) { clearTimeout(resizeTimeoutRef.current); } - }, [terminalInstance, terminalId]); + }, []); useEffect(() => { if (isStopped && terminalInstance) { @@ -70,6 +77,8 @@ export const useTerminal = ( if (parsedMessage.type === OutputType.EXIT) { destroyTerminal(); } else if (parsedMessage.data) { + // Write output from backend - this includes echoed input + // Using write ensures proper synchronization with terminal state terminalInstance.write(parsedMessage.data); } } @@ -78,6 +87,19 @@ export const useTerminal = ( } }, [message, terminalInstance, destroyTerminal, terminalId]); + // Cleanup effect: destroy terminal when isTerminalOpen becomes false or component unmounts + useEffect(() => { + if (!isTerminalOpen && terminalInstanceRef.current) { + destroyTerminal(); + } + return () => { + // Cleanup on unmount + if (terminalInstanceRef.current) { + destroyTerminal(); + } + }; + }, [isTerminalOpen, destroyTerminal]); + const initializeTerminal = useCallback(async () => { if (!terminalRef.current || terminalInstance || !isReady) return; @@ -118,7 +140,9 @@ export const useTerminal = ( scrollback: 1000, tabStopWidth: 8, macOptionIsMeta: true, - macOptionClickForcesSelection: true + macOptionClickForcesSelection: true, + // Ensure proper input handling - backend echo will handle all display + // This prevents sync issues between local display and backend echo }); const fitAddon = new FitAddon(); @@ -158,15 +182,20 @@ export const useTerminal = ( if (allowInput) { term.attachCustomKeyEventHandler((event: KeyboardEvent) => { const key = event.key.toLowerCase(); + + // Handle Ctrl+J or Cmd+J (toggle terminal shortcut) if (key === 'j' && (event.ctrlKey || event.metaKey)) { return false; - } else if (key === 'c' && (event.ctrlKey || event.metaKey) && !event.shiftKey) { + } + + // Handle Ctrl+C or Cmd+C for copy (when there's a selection) + if (key === 'c' && (event.ctrlKey || event.metaKey) && !event.shiftKey) { if (event.type === 'keydown') { try { const selection = term.getSelection(); if (selection) { navigator.clipboard.writeText(selection).then(() => { - term.clearSelection(); // Clear selection after successful copy + term.clearSelection(); }); return false; } @@ -174,10 +203,15 @@ export const useTerminal = ( console.error('Error in Ctrl+C handler:', error); } } - return false; + // If no selection, let it pass through as Ctrl+C signal } + + // Allow xterm to process all other keys normally return true; }); + + // onData is called when xterm processes input + // Send all input to backend - backend echo will handle display term.onData((data) => { sendJsonMessage({ action: 'terminal', @@ -198,6 +232,7 @@ export const useTerminal = ( }); } + terminalInstanceRef.current = term; setTerminalInstance(term); } catch (error) { console.error('Error initializing terminal:', error); From d51faf71c78c6f925976e7eb70c484fee285c4b4 Mon Sep 17 00:00:00 2001 From: zhravan Date: Sun, 7 Dec 2025 12:35:33 +0530 Subject: [PATCH 2/2] fix: unsynced line buffer on backspace or on keyboard type --- api/api/versions.json | 2 +- api/internal/features/terminal/init.go | 2 -- view/app/terminal/utils/useTerminal.ts | 13 +++++-------- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/api/api/versions.json b/api/api/versions.json index 41c5a0f80..14dd9206f 100644 --- a/api/api/versions.json +++ b/api/api/versions.json @@ -3,7 +3,7 @@ { "version": "v1", "status": "active", - "release_date": "2025-12-07T06:51:02.836994+05:30", + "release_date": "2025-12-07T12:32:26.918875+05:30", "end_of_life": "0001-01-01T00:00:00Z", "changes": [ "Initial API version" diff --git a/api/internal/features/terminal/init.go b/api/internal/features/terminal/init.go index b3883e541..57d63b851 100644 --- a/api/internal/features/terminal/init.go +++ b/api/internal/features/terminal/init.go @@ -161,8 +161,6 @@ func (t *Terminal) readOutput(r io.Reader) { return } - // Send output immediately to frontend - // Don't buffer - this prevents duplicate sends msg := TerminalMessage{ TerminalId: t.TerminalId, Type: "stdout", diff --git a/view/app/terminal/utils/useTerminal.ts b/view/app/terminal/utils/useTerminal.ts index 30cabefed..2e03dca26 100644 --- a/view/app/terminal/utils/useTerminal.ts +++ b/view/app/terminal/utils/useTerminal.ts @@ -140,9 +140,7 @@ export const useTerminal = ( scrollback: 1000, tabStopWidth: 8, macOptionIsMeta: true, - macOptionClickForcesSelection: true, - // Ensure proper input handling - backend echo will handle all display - // This prevents sync issues between local display and backend echo + macOptionClickForcesSelection: true }); const fitAddon = new FitAddon(); @@ -182,12 +180,12 @@ export const useTerminal = ( if (allowInput) { term.attachCustomKeyEventHandler((event: KeyboardEvent) => { const key = event.key.toLowerCase(); - + // Handle Ctrl+J or Cmd+J (toggle terminal shortcut) if (key === 'j' && (event.ctrlKey || event.metaKey)) { return false; } - + // Handle Ctrl+C or Cmd+C for copy (when there's a selection) if (key === 'c' && (event.ctrlKey || event.metaKey) && !event.shiftKey) { if (event.type === 'keydown') { @@ -203,13 +201,12 @@ export const useTerminal = ( console.error('Error in Ctrl+C handler:', error); } } - // If no selection, let it pass through as Ctrl+C signal } - + // Allow xterm to process all other keys normally return true; }); - + // onData is called when xterm processes input // Send all input to backend - backend echo will handle display term.onData((data) => {