From 8415c98970429ccd393768a07ef58db64da88595 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Tue, 17 Mar 2026 07:04:40 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20SFTP=20sidebar=20=E2=80=94=20file=20tre?= =?UTF-8?q?e=20with=20mock=20data=20and=20transfer=20progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add useSftp composable with mock directory listings, path navigation, and refresh. FileTree component shows path bar, toolbar (upload, download, new folder, refresh, delete icons), file entries with icons, humanized sizes, and dates. TransferProgress component shows expandable transfer list with progress bars. SidebarToggle now uses v-model to emit tab changes. MainLayout switches between ConnectionTree and FileTree based on sidebar tab, and includes TransferProgress panel. File double-click emits openFile event for the editor integration. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/sftp/FileTree.vue | 165 ++++++++++++++++++ .../src/components/sftp/TransferProgress.vue | 72 ++++++++ .../src/components/sidebar/SidebarToggle.vue | 20 ++- frontend/src/composables/useSftp.ts | 100 +++++++++++ frontend/src/layouts/MainLayout.vue | 66 ++++++- 5 files changed, 411 insertions(+), 12 deletions(-) create mode 100644 frontend/src/components/sftp/FileTree.vue create mode 100644 frontend/src/components/sftp/TransferProgress.vue create mode 100644 frontend/src/composables/useSftp.ts 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 @@ + + + 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 @@ + + + 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' }" > - + - -
+ +
- +
- + + + + +
+ + +
@@ -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, + }; +}