feat: fix 6 frontend issues (F-1, F-5, F-6, F-7, F-10, F-11)
F-1 (Theme Application): Theme selection now applies to all active xterm.js
terminals at runtime via session store reactive propagation. TerminalView
watches sessionStore.activeTheme and calls terminal.options.theme = {...}.
F-5 (Tab Badges): isRootUser() now checks session.username, connection
options JSON, and connection tags — no longer hardcoded to false.
F-6 (Keyboard Shortcuts): Added Ctrl+W (close tab), Ctrl+Tab / Ctrl+Shift+Tab
(next/prev tab), Ctrl+1–9 (tab by index), Ctrl+B (toggle sidebar). Input
field guard prevents shortcuts from firing while typing.
F-7 (Status Bar Dimensions): StatusBar now reads live cols×rows from
sessionStore.activeDimensions. TerminalView hooks onResize to call
sessionStore.setTerminalDimensions(). Falls back to "120×40" until first resize.
F-10 (Multiple Sessions): Removed the "already connected" early-return guard.
Multiple SSH/RDP sessions to the same host are now allowed. Disambiguated tab
names auto-generated: "Asgard", "Asgard (2)", "Asgard (3)", etc.
F-11 (First-Run MobaConf): onMounted checks connectionStore.connections.length
after loadAll(). If empty, shows a dialog offering to import from MobaXTerm.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9d19147568
commit
a6db3ddfa4
@ -53,7 +53,10 @@
|
|||||||
Theme: {{ activeThemeName }}
|
Theme: {{ activeThemeName }}
|
||||||
</button>
|
</button>
|
||||||
<span>UTF-8</span>
|
<span>UTF-8</span>
|
||||||
<span>120×40</span>
|
<span v-if="sessionStore.activeDimensions">
|
||||||
|
{{ sessionStore.activeDimensions.cols }}×{{ sessionStore.activeDimensions.rows }}
|
||||||
|
</span>
|
||||||
|
<span v-else>120×40</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -85,11 +85,24 @@ function getSessionTags(session: Session): string[] {
|
|||||||
|
|
||||||
/** Check if the connection for this session uses the root user. */
|
/** Check if the connection for this session uses the root user. */
|
||||||
function isRootUser(session: Session): boolean {
|
function isRootUser(session: Session): boolean {
|
||||||
|
// Check username stored on the session object (set during connect)
|
||||||
|
if (session.username === "root") return true;
|
||||||
|
|
||||||
|
// Fall back to checking the connection's options JSON for a stored username
|
||||||
const conn = connectionStore.connections.find((c) => c.id === session.connectionId);
|
const conn = connectionStore.connections.find((c) => c.id === session.connectionId);
|
||||||
if (!conn) return false;
|
if (!conn) return false;
|
||||||
// TODO: Get actual username from the credential or session
|
|
||||||
// For now, check mock data — root user detection will come from the session/credential store
|
if (conn.options) {
|
||||||
return false;
|
try {
|
||||||
|
const opts = JSON.parse(conn.options);
|
||||||
|
if (opts?.username === "root") return true;
|
||||||
|
} catch {
|
||||||
|
// ignore malformed options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check if "root" appears in the connection tags
|
||||||
|
return conn.tags?.includes("root") ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return Tailwind classes for environment tag badges. */
|
/** Return Tailwind classes for environment tag badges. */
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch } from "vue";
|
import { ref, onMounted, watch } from "vue";
|
||||||
import { useTerminal } from "@/composables/useTerminal";
|
import { useTerminal } from "@/composables/useTerminal";
|
||||||
|
import { useSessionStore } from "@/stores/session.store";
|
||||||
import "@/assets/css/terminal.css";
|
import "@/assets/css/terminal.css";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -16,6 +17,7 @@ const props = defineProps<{
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const sessionStore = useSessionStore();
|
||||||
const containerRef = ref<HTMLElement | null>(null);
|
const containerRef = ref<HTMLElement | null>(null);
|
||||||
const { terminal, mount, fit } = useTerminal(props.sessionId);
|
const { terminal, mount, fit } = useTerminal(props.sessionId);
|
||||||
|
|
||||||
@ -23,6 +25,16 @@ onMounted(() => {
|
|||||||
if (containerRef.value) {
|
if (containerRef.value) {
|
||||||
mount(containerRef.value);
|
mount(containerRef.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply the current theme immediately if one is already active
|
||||||
|
if (sessionStore.activeTheme) {
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track terminal dimensions in the session store
|
||||||
|
terminal.onResize(({ cols, rows }) => {
|
||||||
|
sessionStore.setTerminalDimensions(props.sessionId, cols, rows);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-fit and focus terminal when this tab becomes active
|
// Re-fit and focus terminal when this tab becomes active
|
||||||
@ -39,6 +51,38 @@ watch(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Apply the session store's active theme to this terminal instance. */
|
||||||
|
function applyTheme(): void {
|
||||||
|
const theme = sessionStore.activeTheme;
|
||||||
|
if (!theme) return;
|
||||||
|
terminal.options.theme = {
|
||||||
|
background: theme.background,
|
||||||
|
foreground: theme.foreground,
|
||||||
|
cursor: theme.cursor,
|
||||||
|
black: theme.black,
|
||||||
|
red: theme.red,
|
||||||
|
green: theme.green,
|
||||||
|
yellow: theme.yellow,
|
||||||
|
blue: theme.blue,
|
||||||
|
magenta: theme.magenta,
|
||||||
|
cyan: theme.cyan,
|
||||||
|
white: theme.white,
|
||||||
|
brightBlack: theme.brightBlack,
|
||||||
|
brightRed: theme.brightRed,
|
||||||
|
brightGreen: theme.brightGreen,
|
||||||
|
brightYellow: theme.brightYellow,
|
||||||
|
brightBlue: theme.brightBlue,
|
||||||
|
brightMagenta: theme.brightMagenta,
|
||||||
|
brightCyan: theme.brightCyan,
|
||||||
|
brightWhite: theme.brightWhite,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for theme changes in the session store and apply to this terminal
|
||||||
|
watch(() => sessionStore.activeTheme, (newTheme) => {
|
||||||
|
if (newTheme) applyTheme();
|
||||||
|
});
|
||||||
|
|
||||||
function handleFocus(): void {
|
function handleFocus(): void {
|
||||||
terminal.focus();
|
terminal.focus();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -105,6 +105,7 @@
|
|||||||
<div class="flex flex-1 min-h-0">
|
<div class="flex flex-1 min-h-0">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div
|
<div
|
||||||
|
v-if="sidebarVisible"
|
||||||
class="flex flex-col bg-[var(--wraith-bg-secondary)] border-r border-[var(--wraith-border)] shrink-0"
|
class="flex flex-col bg-[var(--wraith-bg-secondary)] border-r border-[var(--wraith-border)] shrink-0"
|
||||||
:style="{ width: sidebarWidth + 'px' }"
|
:style="{ width: sidebarWidth + 'px' }"
|
||||||
>
|
>
|
||||||
@ -187,6 +188,36 @@
|
|||||||
|
|
||||||
<!-- Connection Edit Dialog (for File menu / Command Palette new connection) -->
|
<!-- Connection Edit Dialog (for File menu / Command Palette new connection) -->
|
||||||
<ConnectionEditDialog ref="connectionEditDialog" />
|
<ConnectionEditDialog ref="connectionEditDialog" />
|
||||||
|
|
||||||
|
<!-- First-run: MobaXTerm import prompt -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="showMobaPrompt"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div class="absolute inset-0 bg-black/50" @click="showMobaPrompt = false" />
|
||||||
|
<div class="relative w-full max-w-sm bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl p-6 space-y-4">
|
||||||
|
<h3 class="text-sm font-semibold text-[var(--wraith-text-primary)]">No connections found</h3>
|
||||||
|
<p class="text-xs text-[var(--wraith-text-secondary)]">
|
||||||
|
It looks like this is your first time running Wraith. Would you like to import connections from MobaXTerm?
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 text-xs rounded bg-[var(--wraith-bg-tertiary)] text-[var(--wraith-text-secondary)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||||
|
@click="showMobaPrompt = false"
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 text-xs rounded bg-[#1f6feb] text-white hover:bg-[#388bfd] transition-colors cursor-pointer"
|
||||||
|
@click="() => { showMobaPrompt = false; importDialog?.open(); }"
|
||||||
|
>
|
||||||
|
Import from MobaXTerm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -224,9 +255,13 @@ const sessionStore = useSessionStore();
|
|||||||
// copilotStore removed
|
// copilotStore removed
|
||||||
|
|
||||||
const sidebarWidth = ref(240);
|
const sidebarWidth = ref(240);
|
||||||
|
const sidebarVisible = ref(true);
|
||||||
const sidebarTab = ref<SidebarTab>("connections");
|
const sidebarTab = ref<SidebarTab>("connections");
|
||||||
const quickConnectInput = ref("");
|
const quickConnectInput = ref("");
|
||||||
|
|
||||||
|
/** Whether to show the MobaXTerm import prompt (first run, no connections). */
|
||||||
|
const showMobaPrompt = ref(false);
|
||||||
|
|
||||||
// Auto-switch to SFTP tab when an SSH session becomes active
|
// Auto-switch to SFTP tab when an SSH session becomes active
|
||||||
watch(() => sessionStore.activeSession, (session) => {
|
watch(() => sessionStore.activeSession, (session) => {
|
||||||
if (session && session.protocol === "ssh") {
|
if (session && session.protocol === "ssh") {
|
||||||
@ -294,6 +329,8 @@ async function handleOpenFile(entry: FileEntry): Promise<void> {
|
|||||||
/** Handle theme selection from the ThemePicker. */
|
/** Handle theme selection from the ThemePicker. */
|
||||||
function handleThemeSelect(theme: ThemeDefinition): void {
|
function handleThemeSelect(theme: ThemeDefinition): void {
|
||||||
statusBar.value?.setThemeName(theme.name);
|
statusBar.value?.setThemeName(theme.name);
|
||||||
|
// Propagate theme to all active terminal instances via the session store
|
||||||
|
sessionStore.setTheme(theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -375,10 +412,70 @@ async function handleQuickConnect(): Promise<void> {
|
|||||||
|
|
||||||
/** Global keyboard shortcut handler. */
|
/** Global keyboard shortcut handler. */
|
||||||
function handleKeydown(event: KeyboardEvent): void {
|
function handleKeydown(event: KeyboardEvent): void {
|
||||||
// Ctrl+K or Cmd+K — open command palette
|
// Skip shortcuts when the user is typing in an input, textarea, or select
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
|
const target = event.target as HTMLElement;
|
||||||
|
const isInputFocused = target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
|
||||||
|
|
||||||
|
const ctrl = event.ctrlKey || event.metaKey;
|
||||||
|
|
||||||
|
// Ctrl+K — open command palette (fires even in inputs to match VS Code behavior)
|
||||||
|
if (ctrl && event.key === "k") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
commandPalette.value?.toggle();
|
commandPalette.value?.toggle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All remaining shortcuts skip when typing in input fields
|
||||||
|
if (isInputFocused) return;
|
||||||
|
|
||||||
|
// Ctrl+W — close active tab
|
||||||
|
if (ctrl && event.key === "w") {
|
||||||
|
event.preventDefault();
|
||||||
|
const active = sessionStore.activeSession;
|
||||||
|
if (active) {
|
||||||
|
sessionStore.closeSession(active.id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+Tab — next tab
|
||||||
|
if (ctrl && event.key === "Tab" && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
const sessions = sessionStore.sessions;
|
||||||
|
if (sessions.length < 2) return;
|
||||||
|
const idx = sessions.findIndex((s) => s.id === sessionStore.activeSessionId);
|
||||||
|
const next = sessions[(idx + 1) % sessions.length];
|
||||||
|
sessionStore.activateSession(next.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+Shift+Tab — previous tab
|
||||||
|
if (ctrl && event.key === "Tab" && event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
const sessions = sessionStore.sessions;
|
||||||
|
if (sessions.length < 2) return;
|
||||||
|
const idx = sessions.findIndex((s) => s.id === sessionStore.activeSessionId);
|
||||||
|
const prev = sessions[(idx - 1 + sessions.length) % sessions.length];
|
||||||
|
sessionStore.activateSession(prev.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+1 through Ctrl+9 — switch to tab by index
|
||||||
|
if (ctrl && event.key >= "1" && event.key <= "9") {
|
||||||
|
const tabIndex = parseInt(event.key, 10) - 1;
|
||||||
|
const sessions = sessionStore.sessions;
|
||||||
|
if (tabIndex < sessions.length) {
|
||||||
|
event.preventDefault();
|
||||||
|
sessionStore.activateSession(sessions[tabIndex].id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+B — toggle sidebar
|
||||||
|
if (ctrl && event.key === "b") {
|
||||||
|
event.preventDefault();
|
||||||
|
sidebarVisible.value = !sidebarVisible.value;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,6 +483,11 @@ onMounted(async () => {
|
|||||||
document.addEventListener("keydown", handleKeydown);
|
document.addEventListener("keydown", handleKeydown);
|
||||||
// Load connections and groups from the Go backend after vault unlock
|
// Load connections and groups from the Go backend after vault unlock
|
||||||
await connectionStore.loadAll();
|
await connectionStore.loadAll();
|
||||||
|
|
||||||
|
// First-run: if no connections found, offer to import from MobaXTerm
|
||||||
|
if (connectionStore.connections.length === 0) {
|
||||||
|
showMobaPrompt.value = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { defineStore } from "pinia";
|
|||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { Call } from "@wailsio/runtime";
|
import { Call } from "@wailsio/runtime";
|
||||||
import { useConnectionStore } from "@/stores/connection.store";
|
import { useConnectionStore } from "@/stores/connection.store";
|
||||||
|
import type { ThemeDefinition } from "@/components/common/ThemePicker.vue";
|
||||||
|
|
||||||
const APP = "github.com/vstockwell/wraith/internal/app.WraithApp";
|
const APP = "github.com/vstockwell/wraith/internal/app.WraithApp";
|
||||||
|
|
||||||
@ -11,6 +12,12 @@ export interface Session {
|
|||||||
name: string;
|
name: string;
|
||||||
protocol: "ssh" | "rdp";
|
protocol: "ssh" | "rdp";
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerminalDimensions {
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSessionStore = defineStore("session", () => {
|
export const useSessionStore = defineStore("session", () => {
|
||||||
@ -19,6 +26,12 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
const connecting = ref(false);
|
const connecting = ref(false);
|
||||||
const lastError = ref<string | null>(null);
|
const lastError = ref<string | null>(null);
|
||||||
|
|
||||||
|
/** Active terminal theme — applied to all terminal instances. */
|
||||||
|
const activeTheme = ref<ThemeDefinition | null>(null);
|
||||||
|
|
||||||
|
/** Per-session terminal dimensions (cols x rows). */
|
||||||
|
const terminalDimensions = ref<Record<string, TerminalDimensions>>({});
|
||||||
|
|
||||||
const activeSession = computed(() =>
|
const activeSession = computed(() =>
|
||||||
sessions.value.find((s) => s.id === activeSessionId.value) ?? null,
|
sessions.value.find((s) => s.id === activeSessionId.value) ?? null,
|
||||||
);
|
);
|
||||||
@ -54,26 +67,32 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Count how many sessions already exist for this connection (for tab name disambiguation). */
|
||||||
|
function sessionCountForConnection(connId: number): number {
|
||||||
|
return sessions.value.filter((s) => s.connectionId === connId).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generate a disambiguated tab name like "Asgard", "Asgard (2)", "Asgard (3)". */
|
||||||
|
function disambiguatedName(baseName: string, connId: number): string {
|
||||||
|
const count = sessionCountForConnection(connId);
|
||||||
|
return count === 0 ? baseName : `${baseName} (${count + 1})`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to a server by connection ID.
|
* Connect to a server by connection ID.
|
||||||
* Calls the real Go backend to establish an SSH or RDP session.
|
* 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> {
|
async function connect(connectionId: number): Promise<void> {
|
||||||
const connectionStore = useConnectionStore();
|
const connectionStore = useConnectionStore();
|
||||||
const conn = connectionStore.connections.find((c) => c.id === connectionId);
|
const conn = connectionStore.connections.find((c) => c.id === connectionId);
|
||||||
if (!conn) return;
|
if (!conn) return;
|
||||||
|
|
||||||
// Check if there's already an active session for this connection
|
|
||||||
const existing = sessions.value.find((s) => s.connectionId === connectionId);
|
|
||||||
if (existing) {
|
|
||||||
activeSessionId.value = existing.id;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
connecting.value = true;
|
connecting.value = true;
|
||||||
try {
|
try {
|
||||||
if (conn.protocol === "ssh") {
|
if (conn.protocol === "ssh") {
|
||||||
let sessionId: string;
|
let sessionId: string;
|
||||||
|
let resolvedUsername: string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try with stored credentials first
|
// Try with stored credentials first
|
||||||
@ -93,6 +112,7 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
const password = prompt(`Password for ${username}@${conn.hostname}:`);
|
const password = prompt(`Password for ${username}@${conn.hostname}:`);
|
||||||
if (password === null) throw new Error("Connection cancelled");
|
if (password === null) throw new Error("Connection cancelled");
|
||||||
|
|
||||||
|
resolvedUsername = username;
|
||||||
sessionId = await Call.ByName(
|
sessionId = await Call.ByName(
|
||||||
`${APP}.ConnectSSHWithPassword`,
|
`${APP}.ConnectSSHWithPassword`,
|
||||||
connectionId,
|
connectionId,
|
||||||
@ -106,12 +126,23 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to get username from connection options if not already resolved
|
||||||
|
if (!resolvedUsername && conn.options) {
|
||||||
|
try {
|
||||||
|
const opts = JSON.parse(conn.options);
|
||||||
|
if (opts?.username) resolvedUsername = opts.username;
|
||||||
|
} catch {
|
||||||
|
// ignore malformed options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sessions.value.push({
|
sessions.value.push({
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
connectionId,
|
connectionId,
|
||||||
name: conn.name,
|
name: disambiguatedName(conn.name, connectionId),
|
||||||
protocol: "ssh",
|
protocol: "ssh",
|
||||||
active: true,
|
active: true,
|
||||||
|
username: resolvedUsername,
|
||||||
});
|
});
|
||||||
activeSessionId.value = sessionId;
|
activeSessionId.value = sessionId;
|
||||||
} else if (conn.protocol === "rdp") {
|
} else if (conn.protocol === "rdp") {
|
||||||
@ -126,7 +157,7 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
sessions.value.push({
|
sessions.value.push({
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
connectionId,
|
connectionId,
|
||||||
name: conn.name,
|
name: disambiguatedName(conn.name, connectionId),
|
||||||
protocol: "rdp",
|
protocol: "rdp",
|
||||||
active: true,
|
active: true,
|
||||||
});
|
});
|
||||||
@ -143,6 +174,22 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Apply a theme to all active terminal instances. */
|
||||||
|
function setTheme(theme: ThemeDefinition): void {
|
||||||
|
activeTheme.value = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update the recorded dimensions for a terminal session. */
|
||||||
|
function setTerminalDimensions(sessionId: string, cols: number, rows: number): void {
|
||||||
|
terminalDimensions.value[sessionId] = { cols, rows };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the dimensions for the active session, or null if not tracked yet. */
|
||||||
|
const activeDimensions = computed<TerminalDimensions | null>(() => {
|
||||||
|
if (!activeSessionId.value) return null;
|
||||||
|
return terminalDimensions.value[activeSessionId.value] ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessions,
|
sessions,
|
||||||
activeSessionId,
|
activeSessionId,
|
||||||
@ -150,8 +197,13 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
sessionCount,
|
sessionCount,
|
||||||
connecting,
|
connecting,
|
||||||
lastError,
|
lastError,
|
||||||
|
activeTheme,
|
||||||
|
terminalDimensions,
|
||||||
|
activeDimensions,
|
||||||
activateSession,
|
activateSession,
|
||||||
closeSession,
|
closeSession,
|
||||||
connect,
|
connect,
|
||||||
|
setTheme,
|
||||||
|
setTerminalDimensions,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user