From a0c2ec3d2cf8d696ede479211330eec2da360d39 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 22 Jun 2026 17:02:14 +0200 Subject: [PATCH] refac --- src/lib/apis/terminal/index.ts | 20 ++++++- src/lib/components/chat/FileNav.svelte | 81 +++++++++++++++++++++----- 2 files changed, 85 insertions(+), 16 deletions(-) diff --git a/src/lib/apis/terminal/index.ts b/src/lib/apis/terminal/index.ts index 69ee2c5a0a..da491043ef 100644 --- a/src/lib/apis/terminal/index.ts +++ b/src/lib/apis/terminal/index.ts @@ -15,6 +15,17 @@ export type TerminalFeatures = { terminal?: boolean; }; +export type TerminalFileRoot = { + path: string; + label: string; +}; + +export type TerminalCwd = { + cwd: string | null; + home?: string; + root?: TerminalFileRoot; +}; + import { WEBUI_API_BASE_URL } from '$lib/constants'; export type TerminalServer = { @@ -49,14 +60,19 @@ export const getCwd = async ( baseUrl: string, apiKey: string, sessionId?: string -): Promise => { +): Promise => { const url = `${baseUrl.replace(/\/$/, '')}/files/cwd`; const headers: Record = { Authorization: `Bearer ${apiKey}` }; if (sessionId) headers['X-Session-Id'] = sessionId; const res = await fetch(url, { headers }).catch(() => null); if (!res || !res.ok) return null; const json = await res.json().catch(() => null); - return json?.cwd ?? null; + if (!json) return null; + return { + cwd: json?.cwd ?? null, + home: json?.home, + root: json?.root + }; }; export const listFiles = async ( diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index c0043a2409..61a40318fa 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -25,7 +25,9 @@ deleteEntry, moveEntry, setCwd, - type FileEntry + type FileEntry, + type TerminalFileRoot, + type TerminalCwd } from '$lib/apis/terminal'; import { isCodeFile } from '$lib/utils/codeHighlight'; import Folder from '../icons/Folder.svelte'; @@ -88,6 +90,7 @@ // ── Directory state ────────────────────────────────────────────────── let currentPath = savedPath; + let fileRoot: TerminalFileRoot | null = null; let entries: FileEntry[] = []; let loading = false; let error: string | null = null; @@ -280,11 +283,8 @@ terminalEnabled = config?.features?.terminal !== false; } - const rawCwd = await getCwd(terminal.url, terminal.key, chatId ?? undefined); - const cwd = rawCwd ? normalizePath(rawCwd) : null; - const dir = cwd ? (cwd.endsWith('/') ? cwd : cwd + '/') : '/'; - savedPath = dir; - loadDir(dir); + savedPath = applyCwd(await getCwd(terminal.url, terminal.key, chatId ?? undefined)); + loadDir(savedPath); })(); } } @@ -305,7 +305,52 @@ /** Normalize Windows backslashes to forward slashes. */ const normalizePath = (p: string) => p.replace(/\\/g, '/'); + const asDirectoryPath = (path: string) => { + const normalized = normalizePath(path || '/'); + return normalized.endsWith('/') ? normalized : `${normalized}/`; + }; + + const setFileRoot = (root?: TerminalFileRoot) => { + fileRoot = root?.path + ? { + path: asDirectoryPath(root.path), + label: root.label || 'Home' + } + : null; + }; + + const isInsideFileRoot = (path: string) => { + if (!fileRoot) return true; + const directory = asDirectoryPath(path); + return directory === fileRoot.path || directory.startsWith(fileRoot.path); + }; + + const clampToFileRoot = (path: string) => { + if (!fileRoot) return asDirectoryPath(path); + return isInsideFileRoot(path) ? asDirectoryPath(path) : fileRoot.path; + }; + + const applyCwd = (cwd: TerminalCwd | null) => { + setFileRoot(cwd?.root); + const path = cwd?.cwd ? asDirectoryPath(cwd.cwd) : (fileRoot?.path ?? '/'); + return clampToFileRoot(path); + }; + const buildBreadcrumbs = (path: string) => { + if (fileRoot) { + const directory = clampToFileRoot(path); + const relative = directory === fileRoot.path ? '' : directory.slice(fileRoot.path.length); + const parts = relative.split('/').filter(Boolean); + return parts.reduce( + (acc, part) => { + const prev = acc[acc.length - 1]; + acc.push({ label: part, path: `${prev.path}${part}/` }); + return acc; + }, + [{ label: fileRoot.label, path: fileRoot.path }] + ); + } + const parts = path.split('/').filter(Boolean); const isDrive = /^[A-Za-z]:$/.test(parts[0] ?? ''); const root = isDrive ? { label: parts[0], path: `${parts[0]}/` } : { label: '/', path: '/' }; @@ -348,6 +393,7 @@ const loadDir = async (path: string) => { const terminal = selectedTerminal; if (!terminal) return; + const directory = clampToFileRoot(path); loading = true; error = null; @@ -355,15 +401,15 @@ previewPort = null; clearFilePreview(); clearSelection(); - currentPath = path; - savedPath = path; - pushNavHistory(path); + currentPath = directory; + savedPath = directory; + pushNavHistory(directory); - const result = await listFiles(terminal.url, terminal.key, path, chatId ?? undefined); + const result = await listFiles(terminal.url, terminal.key, directory, chatId ?? undefined); loading = false; // Set working directory on the terminal server (fire-and-forget) - setCwd(terminal.url, terminal.key, path, chatId ?? undefined); + setCwd(terminal.url, terminal.key, directory, chatId ?? undefined); if (result === null) { error = @@ -788,6 +834,10 @@ handledDisplayFile = true; showFileNavPath.set(null); filePath = normalizePath(filePath); + if (!isInsideFileRoot(filePath)) { + await loadDir(fileRoot?.path ?? '/'); + return; + } const lastSlash = filePath.lastIndexOf('/'); const dir = lastSlash > 0 ? filePath.substring(0, lastSlash + 1) : '/'; @@ -810,6 +860,10 @@ if (!filePath || !selectedTerminal) return; showFileNavDir.set(null); filePath = normalizePath(filePath); + if (!isInsideFileRoot(filePath)) { + await loadDir(fileRoot?.path ?? '/'); + return; + } const lastSlash = filePath.lastIndexOf('/'); const dir = lastSlash > 0 ? filePath.substring(0, lastSlash + 1) : '/'; @@ -835,10 +889,9 @@ if (chatId || savedPath === '/') { // Fetch session-specific cwd from the server (or global default for new chats) - const rawCwd = await getCwd(terminal.url, terminal.key, chatId ?? undefined); - const cwd = rawCwd ? normalizePath(rawCwd) : null; - if (cwd) savedPath = cwd.endsWith('/') ? cwd : cwd + '/'; + savedPath = applyCwd(await getCwd(terminal.url, terminal.key, chatId ?? undefined)); } + savedPath = clampToFileRoot(savedPath); loadDir(savedPath); }