fix(terminal): refactor WS cleanup, improve clear detection, fix sendToTerminal
All checks were successful
Beta Release / beta (push) Successful in 49s
All checks were successful
Beta Release / beta (push) Successful in 49s
- Move defer cleanup after async goroutine setup to prevent premature closure - Remove unused Password field from terminal sessions struct - Fix line calculation in clear detection using viewportY instead of baseY - Add onStateChange callback to connectWebSocket for connection state - Add tabId parameter to sendToTerminal for targeted tab control - Simplify ShellAIMessage to use specific tab for command sending 💘 Generated with Crush Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 }) {
|
||||
</div>
|
||||
<div className="ai-panel-messages" ref={aiMessagesRef}>
|
||||
{aiMessages.map((msg, i) => (
|
||||
<ShellAIMessage key={i} msg={msg} sendToTerminal={sendToTerminal} />
|
||||
<ShellAIMessage key={i} msg={msg} sendToTerminal={sendToTerminal} terminalTabId={activeTab} />
|
||||
))}
|
||||
{aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>}
|
||||
</div>
|
||||
@@ -933,7 +922,7 @@ export default function Shell({ api }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ShellAIMessage({ msg, sendToTerminal }) {
|
||||
function ShellAIMessage({ msg, sendToTerminal, terminalTabId }) {
|
||||
const role = msg.role === 'user' ? 'user' : msg.role === 'system' ? 'system' : 'assistant'
|
||||
const content = msg.content || ''
|
||||
|
||||
@@ -959,7 +948,7 @@ function ShellAIMessage({ msg, sendToTerminal }) {
|
||||
<button onClick={() => navigator.clipboard.writeText(part.content)} title="Copier">
|
||||
<Copy size={12} /> Copier
|
||||
</button>
|
||||
<button onClick={() => sendToTerminal(part.content)} title="Envoyer au terminal">
|
||||
<button onClick={() => sendToTerminal(part.content, terminalTabId)} title="Envoyer au terminal">
|
||||
<Send size={12} /> Terminal
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user