diff --git a/frontend/src/components/sftp/FileTree.vue b/frontend/src/components/sftp/FileTree.vue new file mode 100644 index 0000000..72bad14 --- /dev/null +++ b/frontend/src/components/sftp/FileTree.vue @@ -0,0 +1,165 @@ + + + + + + + + + + + {{ currentPath }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Loading... + + + + + Empty directory + + + + + + + + + + + + + + + {{ entry.name }} + + + + {{ humanizeSize(entry.size) }} + + + + + {{ entry.modTime }} + + + + + + + + + + Follow terminal folder + + + + + + diff --git a/frontend/src/components/sftp/TransferProgress.vue b/frontend/src/components/sftp/TransferProgress.vue new file mode 100644 index 0000000..988c4b3 --- /dev/null +++ b/frontend/src/components/sftp/TransferProgress.vue @@ -0,0 +1,72 @@ + + + + + Transfers ({{ transfers.length }}) + + + + + + + + + + + {{ transfer.fileName }} + + + {{ transfer.speed }} + + + + + + + {{ transfer.percentage }}% - {{ transfer.direction === 'upload' ? 'Uploading' : 'Downloading' }} + + + + + + + No active transfers + + + + + diff --git a/frontend/src/components/sidebar/SidebarToggle.vue b/frontend/src/components/sidebar/SidebarToggle.vue index fc07ab9..99635af 100644 --- a/frontend/src/components/sidebar/SidebarToggle.vue +++ b/frontend/src/components/sidebar/SidebarToggle.vue @@ -5,11 +5,11 @@ :key="tab.id" class="flex-1 py-2 text-xs font-medium text-center transition-colors cursor-pointer" :class=" - activeTab === tab.id + modelValue === tab.id ? 'text-[var(--wraith-accent-blue)] border-b-2 border-[var(--wraith-accent-blue)]' : 'text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-secondary)]' " - @click="activeTab = tab.id" + @click="emit('update:modelValue', tab.id)" > {{ tab.label }} @@ -17,12 +17,18 @@ diff --git a/frontend/src/composables/useSftp.ts b/frontend/src/composables/useSftp.ts new file mode 100644 index 0000000..16df99f --- /dev/null +++ b/frontend/src/composables/useSftp.ts @@ -0,0 +1,100 @@ +import { ref, type Ref } from "vue"; + +export interface FileEntry { + name: string; + path: string; + size: number; + isDir: boolean; + permissions: string; + modTime: string; +} + +export interface UseSftpReturn { + currentPath: Ref; + entries: Ref; + isLoading: Ref; + followTerminal: Ref; + navigateTo: (path: string) => Promise; + goUp: () => Promise; + refresh: () => Promise; +} + +/** Mock directory listings used until Wails SFTP bindings are connected. */ +const mockDirectories: Record = { + "/home/user": [ + { name: "docs", path: "/home/user/docs", size: 0, isDir: true, permissions: "drwxr-xr-x", modTime: "2026-03-17" }, + { name: "projects", path: "/home/user/projects", size: 0, isDir: true, permissions: "drwxr-xr-x", modTime: "2026-03-16" }, + { name: ".ssh", path: "/home/user/.ssh", size: 0, isDir: true, permissions: "drwx------", modTime: "2026-03-10" }, + { name: ".bashrc", path: "/home/user/.bashrc", size: 3771, isDir: false, permissions: "-rw-r--r--", modTime: "2026-03-15" }, + { name: "deploy.sh", path: "/home/user/deploy.sh", size: 1024, isDir: false, permissions: "-rwxr-xr-x", modTime: "2026-03-16" }, + { name: ".profile", path: "/home/user/.profile", size: 807, isDir: false, permissions: "-rw-r--r--", modTime: "2026-03-10" }, + ], + "/home/user/docs": [ + { name: "readme.md", path: "/home/user/docs/readme.md", size: 2048, isDir: false, permissions: "-rw-r--r--", modTime: "2026-03-17" }, + { name: "notes.txt", path: "/home/user/docs/notes.txt", size: 512, isDir: false, permissions: "-rw-r--r--", modTime: "2026-03-14" }, + ], + "/home/user/projects": [ + { name: "app", path: "/home/user/projects/app", size: 0, isDir: true, permissions: "drwxr-xr-x", modTime: "2026-03-16" }, + { name: "Makefile", path: "/home/user/projects/Makefile", size: 256, isDir: false, permissions: "-rw-r--r--", modTime: "2026-03-16" }, + ], + "/home/user/.ssh": [ + { name: "authorized_keys", path: "/home/user/.ssh/authorized_keys", size: 743, isDir: false, permissions: "-rw-------", modTime: "2026-03-10" }, + { name: "config", path: "/home/user/.ssh/config", size: 128, isDir: false, permissions: "-rw-------", modTime: "2026-03-10" }, + ], +}; + +/** + * Composable that manages SFTP file browsing state. + * + * Uses mock data until Wails SFTPService bindings are connected. + */ +export function useSftp(_sessionId: string): UseSftpReturn { + const currentPath = ref("/home/user"); + const entries = ref([]); + const isLoading = ref(false); + const followTerminal = ref(false); + + async function listDirectory(path: string): Promise { + // TODO: Replace with Wails binding call — SFTPService.List(sessionId, path) + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 150)); + return mockDirectories[path] ?? []; + } + + async function navigateTo(path: string): Promise { + isLoading.value = true; + try { + currentPath.value = path; + entries.value = await listDirectory(path); + } finally { + isLoading.value = false; + } + } + + async function goUp(): Promise { + const parts = currentPath.value.split("/").filter(Boolean); + if (parts.length <= 1) { + await navigateTo("/"); + return; + } + parts.pop(); + await navigateTo("/" + parts.join("/")); + } + + async function refresh(): Promise { + await navigateTo(currentPath.value); + } + + // Load initial directory + navigateTo(currentPath.value); + + return { + currentPath, + entries, + isLoading, + followTerminal, + navigateTo, + goUp, + refresh, + }; +} diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue index 237db2f..f89b94a 100644 --- a/frontend/src/layouts/MainLayout.vue +++ b/frontend/src/layouts/MainLayout.vue @@ -33,10 +33,10 @@ class="flex flex-col bg-[var(--wraith-bg-secondary)] border-r border-[var(--wraith-border)] shrink-0" :style="{ width: sidebarWidth + 'px' }" > - + - - + + - + - + + + + + + + + + Connect to an SSH session to browse files + + + + + + @@ -56,6 +74,15 @@ + + + @@ -73,13 +100,42 @@ import { useConnectionStore } from "@/stores/connection.store"; import { useSessionStore } from "@/stores/session.store"; import SidebarToggle from "@/components/sidebar/SidebarToggle.vue"; import ConnectionTree from "@/components/sidebar/ConnectionTree.vue"; +import FileTree from "@/components/sftp/FileTree.vue"; +import TransferProgress from "@/components/sftp/TransferProgress.vue"; import TabBar from "@/components/session/TabBar.vue"; import SessionContainer from "@/components/session/SessionContainer.vue"; import StatusBar from "@/components/common/StatusBar.vue"; +import EditorWindow from "@/components/editor/EditorWindow.vue"; +import type { SidebarTab } from "@/components/sidebar/SidebarToggle.vue"; +import type { FileEntry } from "@/composables/useSftp"; const appStore = useAppStore(); const connectionStore = useConnectionStore(); const sessionStore = useSessionStore(); const sidebarWidth = ref(240); +const sidebarTab = ref("connections"); + +/** Currently open file in the editor panel (null = no file open). */ +const editorFile = ref<{ content: string; path: string; sessionId: string } | null>(null); + +/** Handle file open from SFTP sidebar — loads mock content for now. */ +function handleOpenFile(entry: FileEntry): void { + if (!sessionStore.activeSession) return; + + // TODO: Replace with Wails binding call — SFTPService.ReadFile(sessionId, entry.path) + // Mock file content for development + const mockContent = `# ${entry.name}\n\n` + + `# File: ${entry.path}\n` + + `# Size: ${entry.size} bytes\n` + + `# Permissions: ${entry.permissions}\n` + + `# Modified: ${entry.modTime}\n\n` + + `# TODO: Content will be loaded from SFTPService.ReadFile()\n`; + + editorFile.value = { + content: mockContent, + path: entry.path, + sessionId: sessionStore.activeSession.id, + }; +}
+ Connect to an SSH session to browse files +