feat: connection status indicators + draggable tab reordering
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 2m52s
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:
parent
087b00c886
commit
02b3ee053d
@ -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(() => {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user