This commit is contained in:
Timothy Jaeryang Baek
2026-06-22 17:02:14 +02:00
parent 7e8153e889
commit a0c2ec3d2c
2 changed files with 85 additions and 16 deletions
+18 -2
View File
@@ -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<string | null> => {
): Promise<TerminalCwd | null> => {
const url = `${baseUrl.replace(/\/$/, '')}/files/cwd`;
const headers: Record<string, string> = { 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 (
+67 -14
View File
@@ -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);
}