diff --git a/app/src/components/ServerSettings/DataFolders.tsx b/app/src/components/ServerSettings/DataFolders.tsx
new file mode 100644
index 00000000..9338da09
--- /dev/null
+++ b/app/src/components/ServerSettings/DataFolders.tsx
@@ -0,0 +1,112 @@
+import { Folder01Icon, FolderOpenIcon } from '@hugeicons/core-free-icons';
+import { HugeiconsIcon } from '@hugeicons/react';
+import { Icon } from '@iconify/react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { useSystemFolders } from '@/lib/hooks/useSystemFolders';
+import { usePlatform } from '@/platform/PlatformContext';
+
+interface FolderRowProps {
+ label: string;
+ description: string;
+ path: string | undefined;
+ isLoading: boolean;
+ canOpen: boolean;
+ onOpen: () => void;
+}
+
+function FolderRow({ label, description, path, isLoading, canOpen, onOpen }: FolderRowProps) {
+ return (
+
+
+
+
{label}
+
{description}
+
+ {canOpen && path && (
+
+ )}
+
+
+
+ );
+}
+
+export function DataFolders() {
+ const { data: folders, isLoading, error } = useSystemFolders();
+ const platform = usePlatform();
+ const isTauri = platform.metadata.isTauri;
+
+ const handleOpenFolder = async (path: string | undefined) => {
+ if (!path) return;
+ const success = await platform.filesystem.openFolder(path);
+ if (!success && isTauri) {
+ console.error('Failed to open folder:', path);
+ }
+ };
+
+ return (
+
+
+
+
+ Data Folders
+
+
+ {isTauri
+ ? 'Click "Open" to view folders in your file explorer, or copy the paths below.'
+ : 'These are the server-side folder paths where your data is stored.'}
+
+
+
+ {error ? (
+
+
+ Failed to load folder paths: {error.message}
+
+ ) : (
+ <>
+ handleOpenFolder(folders?.data_dir)}
+ />
+ handleOpenFolder(folders?.models_dir)}
+ />
+ handleOpenFolder(folders?.providers_dir)}
+ />
+ >
+ )}
+
+
+ );
+}
diff --git a/app/src/components/ServerTab/ServerTab.tsx b/app/src/components/ServerTab/ServerTab.tsx
index 5a512027..d77c7800 100644
--- a/app/src/components/ServerTab/ServerTab.tsx
+++ b/app/src/components/ServerTab/ServerTab.tsx
@@ -1,4 +1,5 @@
import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm';
+import { DataFolders } from '@/components/ServerSettings/DataFolders';
import { ServerStatus } from '@/components/ServerSettings/ServerStatus';
import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus';
import { ProviderSettings } from '@/components/ServerSettings/ProviderSettings';
@@ -13,6 +14,7 @@ export function ServerTab() {
+
{platform.metadata.isTauri && }
Created by{' '}
diff --git a/app/src/lib/api/client.ts b/app/src/lib/api/client.ts
index b5da41a6..01f548d2 100644
--- a/app/src/lib/api/client.ts
+++ b/app/src/lib/api/client.ts
@@ -14,6 +14,7 @@ import type {
ModelStatusListResponse,
ModelDownloadRequest,
ActiveTasksResponse,
+ FolderPathsResponse,
StoryCreate,
StoryResponse,
StoryDetailResponse,
@@ -57,6 +58,11 @@ class ApiClient {
return this.request('/health');
}
+ // System
+ async getSystemFolders(): Promise {
+ return this.request('/system/folders');
+ }
+
// Profiles
async createProfile(data: VoiceProfileCreate): Promise {
return this.request('/profiles', {
diff --git a/app/src/lib/api/types.ts b/app/src/lib/api/types.ts
index dd76ca1e..717496ea 100644
--- a/app/src/lib/api/types.ts
+++ b/app/src/lib/api/types.ts
@@ -128,6 +128,12 @@ export interface ActiveTasksResponse {
generations: ActiveGenerationTask[];
}
+export interface FolderPathsResponse {
+ data_dir: string;
+ models_dir: string;
+ providers_dir: string;
+}
+
export interface StoryCreate {
name: string;
description?: string;
diff --git a/app/src/lib/hooks/useSystemFolders.ts b/app/src/lib/hooks/useSystemFolders.ts
new file mode 100644
index 00000000..47e92540
--- /dev/null
+++ b/app/src/lib/hooks/useSystemFolders.ts
@@ -0,0 +1,14 @@
+import { useQuery } from '@tanstack/react-query';
+import { apiClient } from '@/lib/api/client';
+import { useServerStore } from '@/stores/serverStore';
+
+export function useSystemFolders() {
+ const serverUrl = useServerStore((state) => state.serverUrl);
+
+ return useQuery({
+ queryKey: ['system', 'folders', serverUrl],
+ queryFn: () => apiClient.getSystemFolders(),
+ staleTime: 60000, // Cache for 1 minute - folder paths don't change often
+ retry: 1,
+ });
+}
diff --git a/app/src/platform/types.ts b/app/src/platform/types.ts
index 5ea4d609..6d724fd8 100644
--- a/app/src/platform/types.ts
+++ b/app/src/platform/types.ts
@@ -10,6 +10,13 @@ export interface FileFilter {
export interface PlatformFilesystem {
saveFile(filename: string, blob: Blob, filters?: FileFilter[]): Promise;
+ /**
+ * Open a folder in the native file explorer.
+ * On web, this is a no-op since browsers cannot open folders.
+ * @param path - The absolute path to the folder to open
+ * @returns true if the folder was opened, false if not supported
+ */
+ openFolder(path: string): Promise;
}
export interface UpdateStatus {
diff --git a/backend/main.py b/backend/main.py
index 306a0639..c74ae460 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -74,6 +74,19 @@ async def shutdown():
return {"message": "Shutting down..."}
+@app.get("/system/folders", response_model=models.FolderPathsResponse)
+async def get_system_folders():
+ """Get system folder paths for data, models, and providers."""
+ from huggingface_hub import constants as hf_constants
+ from .providers.installer import _get_providers_dir
+
+ return models.FolderPathsResponse(
+ data_dir=str(config.get_data_dir().absolute()),
+ models_dir=str(Path(hf_constants.HF_HUB_CACHE).absolute()),
+ providers_dir=str(_get_providers_dir().absolute()),
+ )
+
+
@app.get("/health", response_model=models.HealthResponse)
async def health():
"""Health check endpoint."""
diff --git a/backend/models.py b/backend/models.py
index 59e45405..0540644e 100644
--- a/backend/models.py
+++ b/backend/models.py
@@ -170,6 +170,13 @@ class ActiveTasksResponse(BaseModel):
generations: List[ActiveGenerationTask]
+class FolderPathsResponse(BaseModel):
+ """Response model for system folder paths."""
+ data_dir: str
+ models_dir: str
+ providers_dir: str
+
+
class AudioChannelCreate(BaseModel):
"""Request model for creating an audio channel."""
name: str = Field(..., min_length=1, max_length=100)
diff --git a/tauri/src-tauri/gen/Assets.car b/tauri/src-tauri/gen/Assets.car
index 0fba2b0e..fbe8f711 100644
Binary files a/tauri/src-tauri/gen/Assets.car and b/tauri/src-tauri/gen/Assets.car differ
diff --git a/tauri/src-tauri/tauri.conf.json b/tauri/src-tauri/tauri.conf.json
index 294d5049..202d22e0 100644
--- a/tauri/src-tauri/tauri.conf.json
+++ b/tauri/src-tauri/tauri.conf.json
@@ -56,7 +56,7 @@
},
"plugins": {
"shell": {
- "open": true
+ "open": "^((mailto:\\w+)|(tel:\\w+)|(https?://\\w+)|(/[^\\s]+)|([a-zA-Z]:\\\\[^\\s]*)).*"
},
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUxRENBQkRBQjdBNTM1OTIKUldTU05hVzMycXZjNGJGcUxmcVVocll2QjdSaTJNdlFxR2M3VDJsMnVvbDdyZGRPMmRlOW9aWTcK",
diff --git a/tauri/src/platform/filesystem.ts b/tauri/src/platform/filesystem.ts
index e37b4d18..7c057a6d 100644
--- a/tauri/src/platform/filesystem.ts
+++ b/tauri/src/platform/filesystem.ts
@@ -27,4 +27,15 @@ export const tauriFilesystem: PlatformFilesystem = {
document.body.removeChild(a);
}
},
+
+ async openFolder(path: string): Promise {
+ try {
+ const { open } = await import('@tauri-apps/plugin-shell');
+ await open(path);
+ return true;
+ } catch (error) {
+ console.error('Failed to open folder:', error);
+ return false;
+ }
+ },
};
diff --git a/web/src/platform/filesystem.ts b/web/src/platform/filesystem.ts
index 1f45a49c..b9d74690 100644
--- a/web/src/platform/filesystem.ts
+++ b/web/src/platform/filesystem.ts
@@ -12,4 +12,10 @@ export const webFilesystem: PlatformFilesystem = {
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
},
+
+ async openFolder(_path: string): Promise {
+ // Browsers cannot open local folders for security reasons
+ // The UI will show the path as a read-only text instead
+ return false;
+ },
};