feat: connection status indicators + draggable tab reordering
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 2m52s

Status indicators:
- Session.status field tracks connected/disconnected state
- Listens for ssh:close and ssh:exit backend events
- Tab dot turns red when connection drops (green=SSH, blue=RDP, red=dead)

Draggable tabs:
- HTML5 drag-and-drop on tab buttons
- Blue left-border indicator shows drop target
- moveSession() in store reorders the sessions array

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-03-24 21:30:20 -04:00
parent 087b00c886
commit 02b3ee053d
3 changed files with 70 additions and 5 deletions

View File

@ -36,12 +36,15 @@ const props = defineProps<{
username?: string;
/** Raw tags from the connection record. */
tags?: string[];
/** Connection status — drives the dot colour. */
status?: "connected" | "disconnected";
}>();
/** Green for SSH, blue for RDP. */
const protocolDotClass = computed(() =>
props.protocol === "ssh" ? "bg-[#3fb950]" : "bg-[#1f6feb]",
);
/** Green=connected SSH, blue=connected RDP, red=disconnected. */
const protocolDotClass = computed(() => {
if (props.status === "disconnected") return "bg-[#f85149]";
return props.protocol === "ssh" ? "bg-[#3fb950]" : "bg-[#1f6feb]";
});
/** True when the session is running as root or Administrator. */
const isRoot = computed(() => {

View File

@ -3,22 +3,30 @@
<!-- Tabs -->
<div class="flex items-center overflow-x-auto min-w-0">
<button
v-for="session in sessionStore.sessions"
v-for="(session, index) in sessionStore.sessions"
:key="session.id"
draggable="true"
class="group flex items-center gap-1.5 px-3 h-9 text-xs whitespace-nowrap border-r border-[var(--wraith-border)] transition-all duration-500 cursor-pointer shrink-0"
:class="[
session.id === sessionStore.activeSessionId
? 'bg-[var(--wraith-bg-primary)] text-[var(--wraith-text-primary)] border-b-2 border-b-[var(--wraith-accent-blue)]'
: 'text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-secondary)] hover:bg-[var(--wraith-bg-tertiary)]',
isRootUser(session) ? 'border-t-2 border-t-[#f8514966]' : '',
dragOverIndex === index ? 'border-l-2 border-l-[var(--wraith-accent-blue)]' : '',
]"
@click="sessionStore.activateSession(session.id)"
@dragstart="onDragStart(index, $event)"
@dragover.prevent="onDragOver(index)"
@dragleave="dragOverIndex = -1"
@drop.prevent="onDrop(index)"
@dragend="draggedIndex = -1; dragOverIndex = -1"
>
<!-- Badge: protocol dot + root dot + env pills -->
<TabBadge
:protocol="session.protocol"
:username="session.username"
:tags="getSessionTags(session)"
:status="session.status"
/>
<span>{{ session.name }}</span>
@ -44,6 +52,7 @@
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useSessionStore, type Session } from "@/stores/session.store";
import { useConnectionStore } from "@/stores/connection.store";
import TabBadge from "@/components/session/TabBadge.vue";
@ -51,6 +60,31 @@ import TabBadge from "@/components/session/TabBadge.vue";
const sessionStore = useSessionStore();
const connectionStore = useConnectionStore();
// Drag-and-drop tab reordering
const draggedIndex = ref(-1);
const dragOverIndex = ref(-1);
function onDragStart(index: number, event: DragEvent): void {
draggedIndex.value = index;
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "move";
}
}
function onDragOver(index: number): void {
if (draggedIndex.value !== -1 && draggedIndex.value !== index) {
dragOverIndex.value = index;
}
}
function onDrop(toIndex: number): void {
if (draggedIndex.value !== -1 && draggedIndex.value !== toIndex) {
sessionStore.moveSession(draggedIndex.value, toIndex);
}
draggedIndex.value = -1;
dragOverIndex.value = -1;
}
/** Get tags for a session's underlying connection. */
function getSessionTags(session: Session): string[] {
const conn = connectionStore.connections.find((c) => c.id === session.connectionId);

View File

@ -1,6 +1,7 @@
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useConnectionStore } from "@/stores/connection.store";
import type { ThemeDefinition } from "@/components/common/ThemePicker.vue";
@ -11,6 +12,7 @@ export interface Session {
protocol: "ssh" | "rdp";
active: boolean;
username?: string;
status: "connected" | "disconnected";
}
export interface TerminalDimensions {
@ -36,10 +38,30 @@ export const useSessionStore = defineStore("session", () => {
const sessionCount = computed(() => sessions.value.length);
// Listen for backend close/exit events to update session status
function setupStatusListeners(sessionId: string): void {
listen(`ssh:close:${sessionId}`, () => markDisconnected(sessionId));
listen(`ssh:exit:${sessionId}`, () => markDisconnected(sessionId));
}
function markDisconnected(sessionId: string): void {
const session = sessions.value.find((s) => s.id === sessionId);
if (session) session.status = "disconnected";
}
function activateSession(id: string): void {
activeSessionId.value = id;
}
/** Reorder sessions by moving a tab from one index to another. */
function moveSession(fromIndex: number, toIndex: number): void {
if (fromIndex === toIndex) return;
if (fromIndex < 0 || toIndex < 0) return;
if (fromIndex >= sessions.value.length || toIndex >= sessions.value.length) return;
const [moved] = sessions.value.splice(fromIndex, 1);
sessions.value.splice(toIndex, 0, moved);
}
async function closeSession(id: string): Promise<void> {
const idx = sessions.value.findIndex((s) => s.id === id);
if (idx === -1) return;
@ -130,7 +152,9 @@ export const useSessionStore = defineStore("session", () => {
protocol: "ssh",
active: true,
username: resolvedUsername,
status: "connected",
});
setupStatusListeners(sessionId);
activeSessionId.value = sessionId;
return; // early return — key auth handled
} else {
@ -192,7 +216,9 @@ export const useSessionStore = defineStore("session", () => {
protocol: "ssh",
active: true,
username: resolvedUsername,
status: "connected",
});
setupStatusListeners(sessionId);
activeSessionId.value = sessionId;
} else if (conn.protocol === "rdp") {
let username = "";
@ -274,6 +300,7 @@ export const useSessionStore = defineStore("session", () => {
protocol: "rdp",
active: true,
username,
status: "connected",
});
activeSessionId.value = sessionId;
}
@ -317,6 +344,7 @@ export const useSessionStore = defineStore("session", () => {
activateSession,
closeSession,
connect,
moveSession,
setTheme,
setTerminalDimensions,
};