feat(terminal): add session grace expiry tests and improve detach logic (#14127)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
HynoR
2026-10-19 17:54:05 +08:00
committed by GitHub
co-authored by Cursor
parent c695a2a408
commit 04099ae2a2
4 changed files with 24 additions and 2 deletions
+11 -1
View File
@@ -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 {
@@ -18,6 +18,7 @@
size="w-70"
:show-close="false"
:modal="false"
modal-penetrable
@opened="claim"
>
<template #header>
@@ -7,6 +7,7 @@
:ref="(el: any) => store.setInstance(item.key, el)"
@session="(id: string) => store.setSessionId(item.key, id)"
@expired="store.onExpired(item.key)"
@reconnect="store.reconnect(item.key)"
/>
</Teleport>
</template>
+11 -1
View File
@@ -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<typeof setTimeout> | 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() : '');
}