From 0e6daaabe019c93cb4cef8ab654ace56525ea376 Mon Sep 17 00:00:00 2001 From: pptx704 Date: Fri, 10 Apr 2026 19:10:20 +0600 Subject: [PATCH] Fix file browser: use ~ as default path, support tilde expansion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Default to ~ instead of hardcoded /home/user — envd resolves it to the actual home dir of the configured user - Pass ~ and ~/... paths through to envd for server-side expansion - Resolve actual absolute path from response entries for breadcrumbs - Fall back to / if home dir is empty or doesn't exist - Fix leftover label prop on admin templates CopyButton --- frontend/src/lib/components/FilesTab.svelte | 244 ++++++++++++------ .../src/routes/admin/templates/+page.svelte | 2 +- 2 files changed, 159 insertions(+), 87 deletions(-) diff --git a/frontend/src/lib/components/FilesTab.svelte b/frontend/src/lib/components/FilesTab.svelte index ea5bcc9..7fcb98e 100644 --- a/frontend/src/lib/components/FilesTab.svelte +++ b/frontend/src/lib/components/FilesTab.svelte @@ -17,7 +17,7 @@ let { sandboxId, isRunning }: Props = $props(); // Directory navigation state - let currentPath = $state('/'); + let currentPath = $state('~'); let entries = $state([]); let dirLoading = $state(false); let dirError = $state(null); @@ -29,8 +29,9 @@ let fileError = $state(null); // Path input - let pathInput = $state('/'); + let pathInput = $state('~'); let pathInputFocused = $state(false); + let pathInputEl = $state(undefined); // Sorted entries: directories first, then files, alphabetical within each group const sortedEntries = $derived( @@ -51,6 +52,10 @@ return crumbs; }); + // Count of dirs vs files for the footer + const dirCount = $derived(entries.filter((e) => e.type === 'directory').length); + const fileCount = $derived(entries.filter((e) => e.type !== 'directory').length); + async function navigateTo(path: string) { currentPath = normalizePath(path); pathInput = currentPath; @@ -61,6 +66,11 @@ } function normalizePath(p: string): string { + // Let envd handle ~ expansion — pass through as-is + if (p === '~' || p.startsWith('~/')) { + return p; + } + if (!p.startsWith('/')) { // Relative path — resolve against current directory p = currentPath.replace(/\/$/, '') + '/' + p; @@ -75,6 +85,13 @@ return '/' + resolved.join('/'); } + /** Derive the parent directory from an entry's absolute path. */ + function parentFromEntry(entryPath: string): string { + const lastSlash = entryPath.lastIndexOf('/'); + if (lastSlash <= 0) return '/'; + return entryPath.slice(0, lastSlash); + } + async function loadDir() { if (!isRunning) return; dirLoading = true; @@ -82,6 +99,11 @@ const result = await listDir(sandboxId, currentPath); if (result.ok) { entries = result.data.entries ?? []; + // Resolve actual path when envd expanded ~ or a relative path + if (!currentPath.startsWith('/') && entries.length > 0) { + currentPath = parentFromEntry(entries[0].path); + pathInput = currentPath; + } } else { dirError = result.error; entries = []; @@ -146,10 +168,7 @@ e.preventDefault(); const target = pathInput.trim(); if (!target) return; - // If ends with / or has no extension, treat as directory navigation - // Otherwise, attempt to open as a file const resolved = normalizePath(target); - // Try to navigate — if it fails we'll show an error navigateOrOpenFile(resolved); } @@ -157,9 +176,20 @@ // First try as directory const dirResult = await listDir(sandboxId, path); if (dirResult.ok) { - currentPath = path; - pathInput = path; - entries = dirResult.data.entries ?? []; + // Resolve actual path from entries (handles ~ expansion by envd) + const resolvedEntries = dirResult.data.entries ?? []; + let resolvedPath = path; + if (resolvedEntries.length > 0) { + // Derive parent dir from first entry's absolute path + const firstPath = resolvedEntries[0].path; + const lastSlash = firstPath.lastIndexOf('/'); + if (lastSlash >= 0) { + resolvedPath = lastSlash === 0 ? '/' : firstPath.slice(0, lastSlash); + } + } + currentPath = resolvedPath; + pathInput = resolvedPath; + entries = resolvedEntries; selectedFile = null; fileContent = null; fileError = null; @@ -183,7 +213,6 @@ if (found && found.type !== 'directory') { await selectFile(found); } else { - // Not found in parent either — show error dirError = `Not found: ${path}`; } } else { @@ -204,20 +233,23 @@ return 'file'; } - function fmtModified(ts: number): string { - if (!ts) return '—'; - return new Date(ts * 1000).toLocaleString([], { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); + // File extension for subtle coloring + function fileExt(name: string): string { + const dot = name.lastIndexOf('.'); + return dot > 0 ? name.slice(dot + 1).toLowerCase() : ''; } - // Load initial directory on mount + // Load initial directory on mount, falling back to / if home can't be resolved $effect(() => { if (isRunning) { - loadDir(); + loadDir().then(() => { + // If ~ couldn't be resolved (empty dir or error), fall back to / + if (!currentPath.startsWith('/')) { + currentPath = '/'; + pathInput = '/'; + if (dirError) loadDir(); + } + }); } }); @@ -230,7 +262,11 @@ background-color: var(--color-bg-3); } .file-row.active { - background-color: rgba(94, 140, 88, 0.08); + background-color: var(--color-accent-glow); + border-left: 2px solid var(--color-accent); + } + .file-row:not(.active) { + border-left: 2px solid transparent; } .preview-code { @@ -238,23 +274,21 @@ -moz-tab-size: 4; } - /* Thin scrollbar for file tree and preview */ - .thin-scroll::-webkit-scrollbar { width: 6px; height: 6px; } - .thin-scroll::-webkit-scrollbar-track { background: transparent; } - .thin-scroll::-webkit-scrollbar-thumb { - background: var(--color-bg-5); - border-radius: 3px; + /* Staggered row entrance */ + @keyframes rowSlideIn { + from { opacity: 0; transform: translateX(-4px); } + to { opacity: 1; transform: translateX(0); } } - .thin-scroll::-webkit-scrollbar-thumb:hover { - background: var(--color-text-muted); + .row-enter { + animation: rowSlideIn 0.15s ease both; } - @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } + /* Line highlight on hover */ + .code-line:hover .line-content { + background-color: var(--color-bg-3); } - .fade-in { - animation: fadeIn 0.2s ease both; + .code-line:hover .line-num { + color: var(--color-text-tertiary); } @@ -264,50 +298,52 @@ - File browser is only available for running capsules. + File browser requires a running capsule. {:else}
-
+
- - - - - - - + ? 'border-[var(--color-accent)]/50 bg-[var(--color-bg-0)]' + : 'border-[var(--color-border)] bg-[var(--color-bg-1)]'}"> + + (pathInputFocused = true)} onblur={() => (pathInputFocused = false)} onkeydown={handleKeydown} - placeholder="/path/to/file" + placeholder="Enter path..." spellcheck="false" autocomplete="off" class="flex-1 bg-transparent font-mono text-meta text-[var(--color-text-primary)] outline-none placeholder:text-[var(--color-text-muted)]" />
-
+
{#each breadcrumbs() as crumb, i} {#if i > 0} @@ -316,16 +352,25 @@ {/if} {/each}
-
+
{#if dirLoading}
@@ -345,11 +390,13 @@
{:else if entries.length === 0} -
- - - - Empty directory +
+
+ + + +
+ Nothing here yet
{:else} @@ -365,11 +412,12 @@ {/if} - {#each sortedEntries as entry (entry.path)} + {#each sortedEntries as entry, idx (entry.path)}
- {#if !dirLoading && !dirError} -
- - {entries.length} item{entries.length !== 1 ? 's' : ''} - + {#if !dirLoading && !dirError && entries.length > 0} +
+ {#if dirCount > 0} + + {dirCount} dir{dirCount !== 1 ? 's' : ''} + + {/if} + {#if fileCount > 0} + + {fileCount} file{fileCount !== 1 ? 's' : ''} + + {/if}
{/if}
@@ -435,24 +490,41 @@
- - - - - Select a file to preview +
+ + + + +
+
+ No file selected + Choose a file from the tree, or enter a path directly +
{:else} -
+
- - - - + {#if isBinaryFile(selectedFile.name) || isFileTooLarge(selectedFile.size)} + + + + + {:else} + + + + + {/if} {selectedFile.path} + {#if fileExt(selectedFile.name)} + + {fileExt(selectedFile.name)} + + {/if}
-
+
{formatFileSize(selectedFile.size)}
{:else if fileContent !== null} -
-
{#each fileContent.split('\n') as line, i}
{i + 1}{line}
{/each}
+
+
{#each fileContent.split('\n') as line, i}
{i + 1}{line || ' '}
{/each}
{/if}
diff --git a/frontend/src/routes/admin/templates/+page.svelte b/frontend/src/routes/admin/templates/+page.svelte index a15bf96..0a4703c 100644 --- a/frontend/src/routes/admin/templates/+page.svelte +++ b/frontend/src/routes/admin/templates/+page.svelte @@ -419,7 +419,7 @@
{tmpl.name} - +