diff --git a/src/lib/components/AddRepositoryModal.svelte b/src/lib/components/AddRepositoryModal.svelte
index 51a18ac..d620cd1 100644
--- a/src/lib/components/AddRepositoryModal.svelte
+++ b/src/lib/components/AddRepositoryModal.svelte
@@ -1,4 +1,6 @@
+
+
+
+
+
+{#if open}
+
+
+
+
+
+
Select Folder
+
+
+
+
+
+ {#if parent}
+
+ {/if}
+
+ {browsePath}
+
+ {#if loading}
+
+ {/if}
+
+
+
+
+ {#if browseError}
+
{browseError}
+ {:else if entries.length === 0 && !loading}
+
No subdirectories found
+ {:else}
+
+ {#each entries as entry (entry.path)}
+ -
+
+
+ {/each}
+
+ {/if}
+
+
+
+
+
{browsePath}
+
+
+
+
+
+
+
+{/if}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index e3151b0..d17cfce 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -8,9 +8,11 @@
let { data }: { data: PageData } = $props();
- // Local mutable copy; refreshRepositories() keeps it up to date after mutations.
- // Intentionally captures initial value from server load — mutations happen via fetch.
- let repositories = $state(data.repositories ?? []); // svelte-disable state_referenced_locally
+ // Initialized empty; $effect syncs from data prop on every navigation/reload.
+ let repositories = $state([]);
+ $effect(() => {
+ repositories = data.repositories ?? [];
+ });
let showAddModal = $state(false);
let confirmDeleteId = $state(null);
let activeJobIds = $state>({});
diff --git a/src/routes/api/v1/fs/browse/+server.ts b/src/routes/api/v1/fs/browse/+server.ts
new file mode 100644
index 0000000..2da2ee5
--- /dev/null
+++ b/src/routes/api/v1/fs/browse/+server.ts
@@ -0,0 +1,36 @@
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import fs from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+
+export const GET: RequestHandler = ({ url }) => {
+ const rawPath = url.searchParams.get('path') ?? os.homedir();
+ const target = path.resolve(rawPath);
+
+ let entries: { name: string; path: string; isGitRepo: boolean }[] = [];
+ let error: string | null = null;
+ let resolved = target;
+
+ try {
+ const items = fs.readdirSync(target, { withFileTypes: true });
+ entries = items
+ .filter((d) => d.isDirectory() && !d.name.startsWith('.'))
+ .map((d) => {
+ const full = path.join(target, d.name);
+ const isGitRepo = fs.existsSync(path.join(full, '.git'));
+ return { name: d.name, path: full, isGitRepo };
+ })
+ .sort((a, b) => {
+ // git repos first, then alphabetical
+ if (a.isGitRepo !== b.isGitRepo) return a.isGitRepo ? -1 : 1;
+ return a.name.localeCompare(b.name);
+ });
+ } catch (e) {
+ error = (e as NodeJS.ErrnoException).code === 'EACCES' ? 'Permission denied' : 'Path not found';
+ }
+
+ const parent = target !== path.parse(target).root ? path.dirname(target) : null;
+
+ return json({ path: resolved, parent, entries, error });
+};
diff --git a/src/routes/repos/[id]/+page.svelte b/src/routes/repos/[id]/+page.svelte
index 86e6891..6da717a 100644
--- a/src/routes/repos/[id]/+page.svelte
+++ b/src/routes/repos/[id]/+page.svelte
@@ -7,9 +7,13 @@
let { data }: { data: PageData } = $props();
- // Local mutable copies updated via fetch — intentionally capturing initial server values.
- let repo = $state(data.repo); // svelte-disable state_referenced_locally
- let recentJobs = $state(data.recentJobs ?? []); // svelte-disable state_referenced_locally
+ // Initialized empty; $effect syncs from data prop on every navigation/reload.
+ let repo = $state({} as Repository & { versions?: RepositoryVersion[] });
+ let recentJobs = $state([]);
+ $effect(() => {
+ if (data.repo) repo = data.repo;
+ recentJobs = data.recentJobs ?? [];
+ });
let showDeleteConfirm = $state(false);
let activeJobId = $state(null);
let errorMessage = $state(null);