fix: resolve 8 Vue 3 lifecycle and component issues
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 4m5s
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 4m5s
VUE-1: store workspace save interval ID, clear in onUnmounted VUE-2: extract beforeunload handler to named function, remove in onUnmounted VUE-3: move useTerminal() to <script setup> top level in DetachedSession VUE-4: call useTerminal() before nextTick await in CopilotPanel launch() VUE-5: remove duplicate ResizeObserver from LocalTerminalView (useTerminal already creates one) VUE-6: store terminal.onResize() IDisposable, dispose in onBeforeUnmount VUE-7: extract connectSsh(), connectRdp(), resolveCredentials() from 220-line connect() VUE-8: check session protocol before ssh_resize vs pty_resize in TerminalView Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1b7b1a0051
commit
ff9fc798c3
@ -203,10 +203,12 @@ async function launch(): Promise<void> {
|
||||
});
|
||||
connected.value = true;
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (containerRef.value) {
|
||||
// Instantiate terminal synchronously (before any further awaits) now that
|
||||
// sessionId is known. Cleanup is owned by this component's onBeforeUnmount.
|
||||
terminalInstance = useTerminal(sessionId, "pty");
|
||||
|
||||
nextTick(() => {
|
||||
if (containerRef.value && terminalInstance) {
|
||||
terminalInstance.mount(containerRef.value);
|
||||
|
||||
// Fit after mount to get real dimensions, then resize the PTY
|
||||
@ -222,6 +224,7 @@ async function launch(): Promise<void> {
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for shell exit
|
||||
closeUnlisten = await listen(`pty:close:${sessionId}`, () => {
|
||||
|
||||
@ -26,36 +26,34 @@ const sessionName = ref("Detached Session");
|
||||
const protocol = ref("ssh");
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
|
||||
let terminalInstance: ReturnType<typeof useTerminal> | null = null;
|
||||
|
||||
onMounted(async () => {
|
||||
// Parse session info from URL hash
|
||||
// Parse session info from URL hash synchronously so backend type is known at setup time
|
||||
const hash = window.location.hash;
|
||||
const params = new URLSearchParams(hash.split("?")[1] || "");
|
||||
sessionId.value = params.get("sessionId") || "";
|
||||
const _initialSessionId = params.get("sessionId") || "";
|
||||
const _initialProtocol = params.get("protocol") || "ssh";
|
||||
const _backend = (_initialProtocol === "local" ? "pty" : "ssh") as 'ssh' | 'pty';
|
||||
|
||||
const terminalInstance = useTerminal(_initialSessionId, _backend);
|
||||
|
||||
onMounted(async () => {
|
||||
sessionId.value = _initialSessionId;
|
||||
sessionName.value = decodeURIComponent(params.get("name") || "Detached Session");
|
||||
protocol.value = params.get("protocol") || "ssh";
|
||||
protocol.value = _initialProtocol;
|
||||
|
||||
if (!sessionId.value || !containerRef.value) return;
|
||||
|
||||
// Determine backend type
|
||||
const backend = protocol.value === "local" ? "pty" : "ssh";
|
||||
terminalInstance = useTerminal(sessionId.value, backend as 'ssh' | 'pty');
|
||||
terminalInstance.mount(containerRef.value);
|
||||
|
||||
setTimeout(() => {
|
||||
if (terminalInstance) {
|
||||
terminalInstance.fit();
|
||||
terminalInstance.terminal.focus();
|
||||
|
||||
// Resize the backend
|
||||
const resizeCmd = backend === "ssh" ? "ssh_resize" : "pty_resize";
|
||||
const resizeCmd = _backend === "ssh" ? "ssh_resize" : "pty_resize";
|
||||
invoke(resizeCmd, {
|
||||
sessionId: sessionId.value,
|
||||
cols: terminalInstance.terminal.cols,
|
||||
rows: terminalInstance.terminal.rows,
|
||||
}).catch(() => {});
|
||||
}
|
||||
}, 50);
|
||||
|
||||
// On window close, emit event so main window reattaches the tab
|
||||
@ -72,9 +70,6 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (terminalInstance) {
|
||||
terminalInstance.destroy();
|
||||
terminalInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -21,19 +21,10 @@ const props = defineProps<{
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
const { terminal, mount, fit, destroy } = useTerminal(props.sessionId, "pty");
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
if (containerRef.value) {
|
||||
mount(containerRef.value);
|
||||
|
||||
// Watch container size changes and refit terminal
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (props.isActive) {
|
||||
fit();
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(containerRef.value);
|
||||
}
|
||||
setTimeout(() => {
|
||||
fit();
|
||||
@ -66,10 +57,6 @@ watch(
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -59,11 +59,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onMounted, watch } from "vue";
|
||||
import { ref, nextTick, onMounted, onBeforeUnmount, watch } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useTerminal } from "@/composables/useTerminal";
|
||||
import { useSessionStore } from "@/stores/session.store";
|
||||
import MonitorBar from "@/components/terminal/MonitorBar.vue";
|
||||
import type { IDisposable } from "@xterm/xterm";
|
||||
import "@/assets/css/terminal.css";
|
||||
|
||||
const props = defineProps<{
|
||||
@ -74,6 +75,7 @@ const props = defineProps<{
|
||||
const sessionStore = useSessionStore();
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
const { terminal, searchAddon, mount, fit } = useTerminal(props.sessionId);
|
||||
let resizeDisposable: IDisposable | null = null;
|
||||
|
||||
// --- Search state ---
|
||||
const searchVisible = ref(false);
|
||||
@ -139,7 +141,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
// Track terminal dimensions in the session store
|
||||
terminal.onResize(({ cols, rows }) => {
|
||||
resizeDisposable = terminal.onResize(({ cols, rows }) => {
|
||||
sessionStore.setTerminalDimensions(props.sessionId, cols, rows);
|
||||
});
|
||||
|
||||
@ -162,7 +164,9 @@ watch(
|
||||
fit();
|
||||
terminal.focus();
|
||||
// Also notify the backend of the correct size
|
||||
invoke("ssh_resize", {
|
||||
const session = sessionStore.sessions.find(s => s.id === props.sessionId);
|
||||
const resizeCmd = session?.protocol === "local" ? "pty_resize" : "ssh_resize";
|
||||
invoke(resizeCmd, {
|
||||
sessionId: props.sessionId,
|
||||
cols: terminal.cols,
|
||||
rows: terminal.rows,
|
||||
@ -205,6 +209,13 @@ watch(() => sessionStore.activeTheme, (newTheme) => {
|
||||
if (newTheme) applyTheme();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (resizeDisposable) {
|
||||
resizeDisposable.dispose();
|
||||
resizeDisposable = null;
|
||||
}
|
||||
});
|
||||
|
||||
function handleFocus(): void {
|
||||
terminal.focus();
|
||||
}
|
||||
|
||||
@ -494,15 +494,19 @@ function handleKeydown(event: KeyboardEvent): void {
|
||||
if (ctrl && event.key === "f") { const active = sessionStore.activeSession; if (active?.protocol === "ssh") { event.preventDefault(); sessionContainer.value?.openActiveSearch(); } return; }
|
||||
}
|
||||
|
||||
let workspaceSaveInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function handleBeforeUnload(e: BeforeUnloadEvent): void {
|
||||
if (sessionStore.sessions.length > 0) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
document.addEventListener("keydown", handleKeydown);
|
||||
|
||||
// Confirm before closing if sessions are active (synchronous — won't hang)
|
||||
window.addEventListener("beforeunload", (e) => {
|
||||
if (sessionStore.sessions.length > 0) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
|
||||
await connectionStore.loadAll();
|
||||
|
||||
@ -520,7 +524,7 @@ onMounted(async () => {
|
||||
|
||||
// Auto-save workspace every 30 seconds instead of on close
|
||||
// (onCloseRequested was hanging the window close on Windows)
|
||||
setInterval(() => {
|
||||
workspaceSaveInterval = setInterval(() => {
|
||||
const tabs = sessionStore.sessions
|
||||
.filter(s => s.protocol === "ssh" || s.protocol === "rdp")
|
||||
.map((s, i) => ({ connectionId: s.connectionId, protocol: s.protocol, position: i }));
|
||||
@ -543,5 +547,10 @@ onMounted(async () => {
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("keydown", handleKeydown);
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
if (workspaceSaveInterval !== null) {
|
||||
clearInterval(workspaceSaveInterval);
|
||||
workspaceSaveInterval = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -115,38 +115,31 @@ export const useSessionStore = defineStore("session", () => {
|
||||
return count === 0 ? baseName : `${baseName} (${count + 1})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a server by connection ID.
|
||||
* Multiple sessions to the same host are allowed (MobaXTerm-style).
|
||||
* Each gets its own tab with a disambiguated name like "Asgard (2)".
|
||||
*
|
||||
* For Tauri: we must resolve the connection details ourselves and pass
|
||||
* hostname/port/username/password directly to connect_ssh, because the
|
||||
* Rust side has no knowledge of connection IDs — the vault owns credentials.
|
||||
*/
|
||||
async function connect(connectionId: number): Promise<void> {
|
||||
const connectionStore = useConnectionStore();
|
||||
const conn = connectionStore.connections.find((c) => c.id === connectionId);
|
||||
if (!conn) return;
|
||||
type CredentialRow = { id: number; name: string; username: string | null; domain?: string | null; credentialType: string; sshKeyId: number | null };
|
||||
|
||||
connecting.value = true;
|
||||
async function resolveCredentials(credentialId: number): Promise<CredentialRow | null> {
|
||||
try {
|
||||
if (conn.protocol === "ssh") {
|
||||
const allCreds = await invoke<CredentialRow[]>("list_credentials");
|
||||
return allCreds.find((c) => c.id === credentialId) ?? null;
|
||||
} catch (credErr) {
|
||||
console.warn("Failed to resolve credential:", credErr);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function connectSsh(
|
||||
conn: { id: number; name: string; hostname: string; port: number; credentialId?: number | null },
|
||||
connectionId: number,
|
||||
): Promise<void> {
|
||||
let sessionId: string;
|
||||
let resolvedUsername = "";
|
||||
let resolvedPassword = "";
|
||||
|
||||
// If connection has a linked credential, decrypt it from the vault
|
||||
if (conn.credentialId) {
|
||||
try {
|
||||
const allCreds = await invoke<{ id: number; name: string; username: string | null; credentialType: string; sshKeyId: number | null }[]>("list_credentials");
|
||||
const cred = allCreds.find((c) => c.id === conn.credentialId);
|
||||
|
||||
const cred = await resolveCredentials(conn.credentialId);
|
||||
if (cred) {
|
||||
resolvedUsername = cred.username ?? "";
|
||||
|
||||
if (cred.credentialType === "ssh_key" && cred.sshKeyId) {
|
||||
// SSH key auth — decrypt key from vault
|
||||
const [privateKey, passphrase] = await invoke<[string, string]>("decrypt_ssh_key", { sshKeyId: cred.sshKeyId });
|
||||
sessionId = await invoke<string>("connect_ssh_with_key", {
|
||||
hostname: conn.hostname,
|
||||
@ -157,7 +150,6 @@ export const useSessionStore = defineStore("session", () => {
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
});
|
||||
|
||||
sessions.value.push({
|
||||
id: sessionId,
|
||||
connectionId,
|
||||
@ -170,22 +162,15 @@ export const useSessionStore = defineStore("session", () => {
|
||||
});
|
||||
setupStatusListeners(sessionId);
|
||||
activeSessionId.value = sessionId;
|
||||
return; // early return — key auth handled
|
||||
return;
|
||||
} else {
|
||||
// Password auth — decrypt password from vault
|
||||
resolvedPassword = await invoke<string>("decrypt_password", { credentialId: cred.id });
|
||||
}
|
||||
}
|
||||
} catch (credErr) {
|
||||
console.warn("Failed to resolve credential, will prompt:", credErr);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!resolvedUsername) {
|
||||
// No credential linked — prompt immediately
|
||||
throw new Error("NO_CREDENTIALS");
|
||||
}
|
||||
if (!resolvedUsername) throw new Error("NO_CREDENTIALS");
|
||||
sessionId = await invoke<string>("connect_ssh", {
|
||||
hostname: conn.hostname,
|
||||
port: conn.port,
|
||||
@ -195,20 +180,13 @@ export const useSessionStore = defineStore("session", () => {
|
||||
rows: 40,
|
||||
});
|
||||
} catch (sshErr: unknown) {
|
||||
const errMsg = sshErr instanceof Error
|
||||
? sshErr.message
|
||||
: typeof sshErr === "string"
|
||||
? sshErr
|
||||
: String(sshErr);
|
||||
|
||||
// If no credentials or auth failed, prompt for username/password
|
||||
const errMsg = sshErr instanceof Error ? sshErr.message : typeof sshErr === "string" ? sshErr : String(sshErr);
|
||||
const errLower = errMsg.toLowerCase();
|
||||
if (errLower.includes("no_credentials") || errLower.includes("unable to authenticate") || errLower.includes("authentication") || errLower.includes("rejected")) {
|
||||
const username = window.prompt(`Username for ${conn.hostname}:`, resolvedUsername || "root");
|
||||
if (!username) throw new Error("Connection cancelled");
|
||||
const password = window.prompt(`Password for ${username}@${conn.hostname}:`);
|
||||
if (password === null) throw new Error("Connection cancelled");
|
||||
|
||||
resolvedUsername = username;
|
||||
sessionId = await invoke<string>("connect_ssh", {
|
||||
hostname: conn.hostname,
|
||||
@ -235,27 +213,25 @@ export const useSessionStore = defineStore("session", () => {
|
||||
});
|
||||
setupStatusListeners(sessionId);
|
||||
activeSessionId.value = sessionId;
|
||||
} else if (conn.protocol === "rdp") {
|
||||
}
|
||||
|
||||
async function connectRdp(
|
||||
conn: { id: number; name: string; hostname: string; port: number; credentialId?: number | null; options?: string },
|
||||
connectionId: number,
|
||||
): Promise<void> {
|
||||
let username = "";
|
||||
let password = "";
|
||||
let domain = "";
|
||||
|
||||
// Try vault credentials first (same as SSH path)
|
||||
if (conn.credentialId) {
|
||||
try {
|
||||
const allCreds = await invoke<{ id: number; name: string; username: string | null; domain: string | null; credentialType: string; sshKeyId: number | null }[]>("list_credentials");
|
||||
const cred = allCreds.find((c) => c.id === conn.credentialId);
|
||||
const cred = await resolveCredentials(conn.credentialId);
|
||||
if (cred && cred.credentialType === "password") {
|
||||
username = cred.username ?? "";
|
||||
domain = cred.domain ?? "";
|
||||
password = await invoke<string>("decrypt_password", { credentialId: cred.id });
|
||||
}
|
||||
} catch (credErr) {
|
||||
console.warn("Failed to resolve RDP credential from vault:", credErr);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to connection options JSON if vault didn't provide creds
|
||||
if (!username && conn.options) {
|
||||
try {
|
||||
const opts = JSON.parse(conn.options);
|
||||
@ -270,53 +246,19 @@ export const useSessionStore = defineStore("session", () => {
|
||||
let sessionId: string;
|
||||
try {
|
||||
sessionId = await invoke<string>("connect_rdp", {
|
||||
config: {
|
||||
hostname: conn.hostname,
|
||||
port: conn.port,
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
config: { hostname: conn.hostname, port: conn.port, username, password, domain, width: 1920, height: 1080 },
|
||||
});
|
||||
} catch (rdpErr: unknown) {
|
||||
const errMsg =
|
||||
rdpErr instanceof Error
|
||||
? rdpErr.message
|
||||
: typeof rdpErr === "string"
|
||||
? rdpErr
|
||||
: String(rdpErr);
|
||||
|
||||
// If credentials are missing or rejected, prompt the operator
|
||||
if (
|
||||
errMsg.includes("NO_CREDENTIALS") ||
|
||||
errMsg.includes("authentication") ||
|
||||
errMsg.includes("logon failure")
|
||||
) {
|
||||
const promptedUsername = prompt(
|
||||
`Username for ${conn.hostname}:`,
|
||||
"Administrator",
|
||||
);
|
||||
const errMsg = rdpErr instanceof Error ? rdpErr.message : typeof rdpErr === "string" ? rdpErr : String(rdpErr);
|
||||
if (errMsg.includes("NO_CREDENTIALS") || errMsg.includes("authentication") || errMsg.includes("logon failure")) {
|
||||
const promptedUsername = prompt(`Username for ${conn.hostname}:`, "Administrator");
|
||||
if (!promptedUsername) throw new Error("Connection cancelled");
|
||||
const promptedPassword = prompt(
|
||||
`Password for ${promptedUsername}@${conn.hostname}:`,
|
||||
);
|
||||
const promptedPassword = prompt(`Password for ${promptedUsername}@${conn.hostname}:`);
|
||||
if (promptedPassword === null) throw new Error("Connection cancelled");
|
||||
const promptedDomain = prompt(`Domain (leave blank if none):`, "") ?? "";
|
||||
|
||||
username = promptedUsername;
|
||||
|
||||
sessionId = await invoke<string>("connect_rdp", {
|
||||
config: {
|
||||
hostname: conn.hostname,
|
||||
port: conn.port,
|
||||
username: promptedUsername,
|
||||
password: promptedPassword,
|
||||
domain: promptedDomain,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
config: { hostname: conn.hostname, port: conn.port, username: promptedUsername, password: promptedPassword, domain: promptedDomain, width: 1920, height: 1080 },
|
||||
});
|
||||
} else {
|
||||
throw rdpErr;
|
||||
@ -335,11 +277,28 @@ export const useSessionStore = defineStore("session", () => {
|
||||
});
|
||||
activeSessionId.value = sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a server by connection ID.
|
||||
* Multiple sessions to the same host are allowed (MobaXTerm-style).
|
||||
* Each gets its own tab with a disambiguated name like "Asgard (2)".
|
||||
*/
|
||||
async function connect(connectionId: number): Promise<void> {
|
||||
const connectionStore = useConnectionStore();
|
||||
const conn = connectionStore.connections.find((c) => c.id === connectionId);
|
||||
if (!conn) return;
|
||||
|
||||
connecting.value = true;
|
||||
try {
|
||||
if (conn.protocol === "ssh") {
|
||||
await connectSsh(conn, connectionId);
|
||||
} else if (conn.protocol === "rdp") {
|
||||
await connectRdp(conn, connectionId);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
||||
console.error("Connection failed:", msg);
|
||||
lastError.value = msg;
|
||||
// Show error as native alert so it's visible without DevTools
|
||||
alert(`Connection failed: ${msg}`);
|
||||
} finally {
|
||||
connecting.value = false;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user