From 04099ae2a26953aa8fffef3d8a1def590ebf33e4 Mon Sep 17 00:00:00 2001 From: HynoR Date: Mon, 19 Oct 2026 17:54:05 +0800 Subject: [PATCH] feat(terminal): add session grace expiry tests and improve detach logic (#14127) Co-authored-by: Cursor --- agent/utils/terminal/session.go | 12 +++++++++++- frontend/src/components/terminal/dock/index.vue | 1 + frontend/src/components/terminal/host.vue | 1 + frontend/src/components/terminal/index.vue | 12 +++++++++++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/agent/utils/terminal/session.go b/agent/utils/terminal/session.go index 4cc1d7924..42b82e377 100644 --- a/agent/utils/terminal/session.go +++ b/agent/utils/terminal/session.go @@ -314,7 +314,17 @@ func (s *Session) detach(a *attachment, clean, revalidate bool, cursor uint64) { if !s.Persistent || revalidate { timeout = revalidateGrace } - s.grace = time.AfterFunc(timeout, s.Close) + // Stop cannot cancel a fired timer; a racing Attach clears s.grace. + var grace *time.Timer + grace = time.AfterFunc(timeout, func() { + s.mu.Lock() + expired := s.grace == grace + s.mu.Unlock() + if expired { + s.Close() + } + }) + s.grace = grace } s.mu.Unlock() if clean { diff --git a/frontend/src/components/terminal/dock/index.vue b/frontend/src/components/terminal/dock/index.vue index 7ae8b724c..772b3ca94 100644 --- a/frontend/src/components/terminal/dock/index.vue +++ b/frontend/src/components/terminal/dock/index.vue @@ -18,6 +18,7 @@ size="w-70" :show-close="false" :modal="false" + modal-penetrable @opened="claim" > diff --git a/frontend/src/components/terminal/index.vue b/frontend/src/components/terminal/index.vue index d485be755..1375d53df 100644 --- a/frontend/src/components/terminal/index.vue +++ b/frontend/src/components/terminal/index.vue @@ -31,7 +31,7 @@ const { currentNode } = useGlobalStore(); // session: agent side session id known (fresh or reattached) // expired: the agent no longer has the session; a reconnect must open a new one -const emit = defineEmits(['session', 'expired']); +const emit = defineEmits(['session', 'expired', 'reconnect']); // Close codes of the agent's session protocol (agent/utils/terminal/session.go). const CLOSE_SESSION_NOT_FOUND = 4404; @@ -56,6 +56,7 @@ let closing = false; let reconnecting = false; let reconnectNoticeShown = false; let revalidating = false; +let expired = false; let reconnectStartedAt = 0; let reconnectDelay = 1000; let reconnectTimer: ReturnType | null = null; @@ -279,6 +280,7 @@ function resizeTerminal() { if (terminalElement.value.clientWidth <= 0 || terminalElement.value.clientHeight <= 0) { return; } + if (terminalElement.value.closest('.terminal-host')) return; fitAddon.fit(); if (isWsOpen()) { @@ -484,6 +486,7 @@ const closeRealTerminal = (ev: CloseEvent) => { '31', ev.code === 1000 ? 'The connection has been disconnected.' : i18n.global.t('terminal.sessionExpired'), ); + expired = true; emit('expired'); return; case CLOSE_ATTACHED_ELSEWHERE: @@ -602,6 +605,13 @@ function sendMsg(data: string, line: string = '') { function onTermData(data: string) { if (!data) return; + if (expired) { + if (isEnterInputData(data)) { + expired = false; + emit('reconnect'); + } + return; + } if (aiNotice.value.loading) return; sendMsg(data, isEnterInputData(data) ? getCurrentTerminalLine() : ''); }