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/host.vue b/frontend/src/components/terminal/host.vue
index 6c3c19afc..735f67ee5 100644
--- a/frontend/src/components/terminal/host.vue
+++ b/frontend/src/components/terminal/host.vue
@@ -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)"
/>
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() : '');
}