diff --git a/internal/api/terminal.go b/internal/api/terminal.go index 31d2e87..236f794 100644 --- a/internal/api/terminal.go +++ b/internal/api/terminal.go @@ -146,13 +146,6 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) { return } log.Printf("terminal: pty started successfully") - defer func() { - ptmx.Close() - if cmd.Process != nil { - cmd.Process.Kill() - cmd.Wait() - } - }() var once sync.Once cleanup := func() { @@ -164,6 +157,7 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) { } }) } + defer cleanup() go func() { buf := make([]byte, 4096) @@ -230,12 +224,11 @@ func (s *Server) handleTerminalSessions(w http.ResponseWriter, r *http.Request) return } var body struct { - Name string `json:"name"` - Host string `json:"host"` - Port int `json:"port"` - User string `json:"user"` - Password string `json:"password"` - KeyPath string `json:"key_path"` + Name string `json:"name"` + Host string `json:"host"` + Port int `json:"port"` + User string `json:"user"` + KeyPath string `json:"key_path"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, err.Error(), http.StatusBadRequest) diff --git a/web/src/components/Shell.jsx b/web/src/components/Shell.jsx index c03fbfd..ce4da13 100644 --- a/web/src/components/Shell.jsx +++ b/web/src/components/Shell.jsx @@ -149,7 +149,7 @@ function createTerminal(container, settings = {}) { return { term, fitAddon } } -function connectWebSocket(term, fitAddon, initPayload) { +function connectWebSocket(term, fitAddon, initPayload, onStateChange) { const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const ws = new WebSocket(`${proto}//${window.location.host}/api/ws/terminal`) @@ -159,6 +159,7 @@ function connectWebSocket(term, fitAddon, initPayload) { if (dims) { ws.send(JSON.stringify({ type: 'resize', rows: dims.rows, cols: dims.cols })) } + if (onStateChange) onStateChange(true) }) ws.addEventListener('message', (event) => { @@ -176,10 +177,12 @@ function connectWebSocket(term, fitAddon, initPayload) { ws.addEventListener('close', () => { term.write('\r\n\x1b[33m— Connection closed —\x1b[0m\r\n') + if (onStateChange) onStateChange(false) }) ws.addEventListener('error', () => { term.write('\r\n\x1b[31m— Connection error —\x1b[0m\r\n') + if (onStateChange) onStateChange(false) }) term.onData((data) => { @@ -335,7 +338,11 @@ export default function Shell({ api }) { } } - const ws = connectWebSocket(term, fitAddon, initPayload) + const onWsState = (connected) => { + if (!connected) saveBuffer() + setTabs(prev => prev.map(t => t.id === tabId ? { ...t, connected } : t)) + } + const ws = connectWebSocket(term, fitAddon, initPayload, onWsState) // Restore saved terminal buffer after first output settles const restoreBuffer = () => { @@ -365,13 +372,10 @@ export default function Shell({ api }) { const bufferSaveInterval = setInterval(saveBuffer, 5000) - // Detect clear command to wipe saved buffer - // We read the current line from the terminal buffer on Enter - // instead of trying to reconstruct from keystrokes (unreliable with history, ANSI, etc.) const clearBufferOnClear = () => { try { const buf = term.buffer.active - const lineY = buf.baseY + buf.cursorY + const lineY = buf.viewportY + buf.cursorY const line = buf.getLine(lineY) if (line) { const text = line.translateToString(true).trim().toLowerCase() @@ -384,28 +388,12 @@ export default function Shell({ api }) { } catch {} } - // Hook into onData to detect Enter for clear detection - // The connectWebSocket already registered its own onData for WS forwarding, - // this one is purely for clear detection term.onData((data) => { if (data === '\r') { clearBufferOnClear() } }) - ws.onopen = () => { - setTabs(prev => prev.map(t => t.id === tabId ? { ...t, connected: true } : t)) - } - - ws.onclose = () => { - saveBuffer() - setTabs(prev => prev.map(t => t.id === tabId ? { ...t, connected: false } : t)) - } - - ws.onerror = () => { - setTabs(prev => prev.map(t => t.id === tabId ? { ...t, connected: false } : t)) - } - const onResize = () => { const el = document.getElementById(`terminal-${tabId}`) if (el && el.offsetParent !== null) { @@ -585,14 +573,15 @@ export default function Shell({ api }) { } } - const sendToTerminal = useCallback((code) => { - const entry = tabsRef.current[activeTab] + const sendToTerminal = useCallback((code, tabId) => { + const targetId = tabId || activeTab + const entry = tabsRef.current[targetId] if (!entry) { - console.warn('sendToTerminal: no terminal initialized for tab', activeTab) + console.warn('sendToTerminal: no terminal initialized for tab', targetId) return } if (!entry.ws || entry.ws.readyState !== WebSocket.OPEN) { - console.warn('sendToTerminal: WebSocket not ready for tab', activeTab) + console.warn('sendToTerminal: WebSocket not ready for tab', targetId) return } entry.ws.send(JSON.stringify({ type: 'input', data: code + '\r' })) @@ -841,7 +830,7 @@ export default function Shell({ api }) {