fix: infinite remount loop — use stable key for session components
When replaceSession changed the session ID from pending-XXX to a real UUID, Vue's :key="session.id" treated it as a new element, destroyed and recreated TerminalInstance, which called connectToHost again, got another UUID, replaced again — infinite loop. Added a stable `key` field to sessions that never changes after creation, used as the Vue :key instead of the mutable `id`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4ccf138744
commit
aa457b54d4
@ -41,7 +41,7 @@ function handleRdpClipboard(sessionId: string, text: string) {
|
|||||||
<div class="flex-1 overflow-hidden relative">
|
<div class="flex-1 overflow-hidden relative">
|
||||||
<div
|
<div
|
||||||
v-for="session in sessions.sessions"
|
v-for="session in sessions.sessions"
|
||||||
:key="session.id"
|
:key="session.key"
|
||||||
v-show="session.id === sessions.activeSessionId"
|
v-show="session.id === sessions.activeSessionId"
|
||||||
class="absolute inset-0 flex"
|
class="absolute inset-0 flex"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ const sessions = useSessionStore()
|
|||||||
<div class="flex h-8 bg-gray-950 border-b border-gray-800 overflow-x-auto shrink-0">
|
<div class="flex h-8 bg-gray-950 border-b border-gray-800 overflow-x-auto shrink-0">
|
||||||
<SessionTab
|
<SessionTab
|
||||||
v-for="session in sessions.sessions"
|
v-for="session in sessions.sessions"
|
||||||
:key="session.id"
|
:key="session.key"
|
||||||
:session="session"
|
:session="session"
|
||||||
:is-active="session.id === sessions.activeSessionId"
|
:is-active="session.id === sessions.activeSessionId"
|
||||||
@activate="sessions.setActive(session.id)"
|
@activate="sessions.setActive(session.id)"
|
||||||
|
|||||||
@ -182,6 +182,7 @@ export function useRdp() {
|
|||||||
// Wire tunnel callbacks
|
// Wire tunnel callbacks
|
||||||
tunnel.onConnected = (_resolvedHostId: number, resolvedHostName: string) => {
|
tunnel.onConnected = (_resolvedHostId: number, resolvedHostName: string) => {
|
||||||
sessions.addSession({
|
sessions.addSession({
|
||||||
|
key: sessionId,
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
hostId,
|
hostId,
|
||||||
hostName: resolvedHostName || hostName,
|
hostName: resolvedHostName || hostName,
|
||||||
|
|||||||
@ -68,7 +68,7 @@ export function useTerminal() {
|
|||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case 'connected':
|
case 'connected':
|
||||||
// Replace the pending placeholder with the real backend session
|
// Replace the pending placeholder with the real backend session
|
||||||
sessions.replaceSession(pendingSessionId, { id: msg.sessionId, hostId, hostName, protocol, color, active: true })
|
sessions.replaceSession(pendingSessionId, { key: pendingSessionId, id: msg.sessionId, hostId, hostName, protocol, color, active: true })
|
||||||
// Send initial terminal size
|
// Send initial terminal size
|
||||||
ws!.send(JSON.stringify({ type: 'resize', sessionId: msg.sessionId, cols: term.cols, rows: term.rows }))
|
ws!.send(JSON.stringify({ type: 'resize', sessionId: msg.sessionId, cols: term.cols, rows: term.rows }))
|
||||||
break
|
break
|
||||||
|
|||||||
@ -90,6 +90,7 @@ function connectHost(host: any) {
|
|||||||
}
|
}
|
||||||
const pendingId = `pending-${Date.now()}`
|
const pendingId = `pending-${Date.now()}`
|
||||||
sessions.addSession({
|
sessions.addSession({
|
||||||
|
key: pendingId,
|
||||||
id: pendingId,
|
id: pendingId,
|
||||||
hostId: host.id,
|
hostId: host.id,
|
||||||
hostName: host.name,
|
hostName: host.name,
|
||||||
@ -107,6 +108,7 @@ function handleQuickConnect(params: { hostname: string; port: number; username:
|
|||||||
: params.hostname
|
: params.hostname
|
||||||
|
|
||||||
sessions.addSession({
|
sessions.addSession({
|
||||||
|
key: sessionId,
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
hostId: null,
|
hostId: null,
|
||||||
hostName: displayName,
|
hostName: displayName,
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
interface Session {
|
interface Session {
|
||||||
id: string // uuid from backend
|
key: string // stable Vue key — never changes after creation
|
||||||
|
id: string // uuid from backend (starts as pending-XXX, replaced with real UUID)
|
||||||
hostId: number
|
hostId: number
|
||||||
hostName: string
|
hostName: string
|
||||||
protocol: 'ssh' | 'rdp'
|
protocol: 'ssh' | 'rdp'
|
||||||
@ -32,6 +33,8 @@ export const useSessionStore = defineStore('sessions', {
|
|||||||
replaceSession(oldId: string, newSession: Session) {
|
replaceSession(oldId: string, newSession: Session) {
|
||||||
const idx = this.sessions.findIndex(s => s.id === oldId)
|
const idx = this.sessions.findIndex(s => s.id === oldId)
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
|
// Preserve the stable key so Vue doesn't remount the component
|
||||||
|
newSession.key = this.sessions[idx].key
|
||||||
this.sessions[idx] = newSession
|
this.sessions[idx] = newSession
|
||||||
} else {
|
} else {
|
||||||
this.sessions.push(newSession)
|
this.sessions.push(newSession)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user