feat(web): sélecteur de répertoire (parcourir au lieu de taper le chemin)

Ajoute un navigateur de dossiers réutilisable sur les deux champs qui
exigeaient un chemin absolu tapé à la main : « Répertoire de travail »
(nouvelle session) et « Chemin du dépôt » (ajout de repo). Un bouton
« Parcourir… » déplie un panneau inline ; le champ texte reste pour
taper/coller un chemin connu. Pour l'ajout de repo, les sous-dossiers
qui sont des dépôts git sont signalés par un badge.

Serveur : nouvel endpoint GET /api/v1/fs/list (authentifié par le hook
preValidation global), ne renvoie que des noms de sous-dossiers (jamais
de contenu de fichier). Valide le chemin via resolve() (clampe à la
racine, neutralise « .. »), masque les dotfiles par défaut (showHidden),
annote les dépôts git (markRepos), tolère les entrées illisibles.

Tests : packages/server/test/fs-routes.test.ts (10 cas — listing, tri,
hidden, markRepos, rejets 400/404, auth 401). Suite : 185/185 verts.
This commit is contained in:
2026-06-17 13:04:10 +02:00
parent a33c5d0904
commit 3acf27d48f
9 changed files with 378 additions and 0 deletions

View File

@@ -35,10 +35,18 @@
<option value="bash">bash</option>
</select>
</label>
<button type="button" class="btn whitespace-nowrap" @click="showPicker = !showPicker">{{ t('fs.browse') }}</button>
<button type="submit" class="btn-primary whitespace-nowrap" :disabled="creating || newCwd.trim() === ''">
{{ creating ? t('sessions.creating') : t('sessions.newSession') }}
</button>
</form>
<DirectoryPicker
v-if="showPicker"
mode="dir"
:initial-path="newCwd"
@select="onPickCwd"
@close="showPicker = false"
/>
<p v-if="createError" class="text-sm text-red-400">{{ createError }}</p>
<p v-if="actionError" class="text-sm text-red-400">{{ actionError }}</p>
@@ -112,6 +120,7 @@ import { useSessionsStore } from '../stores/sessions';
import LanguageSwitcher from '../components/LanguageSwitcher.vue';
import SessionStateBadge from '../components/SessionStateBadge.vue';
import DialogPrompt from '../components/DialogPrompt.vue';
import DirectoryPicker from '../components/DirectoryPicker.vue';
const { t, locale } = useI18n();
const router = useRouter();
@@ -120,6 +129,7 @@ const store = useSessionsStore();
const newCwd = ref('');
const newCommand = ref<'claude' | 'bash'>('claude');
const showPicker = ref(false);
const creating = ref(false);
const createError = ref<string | null>(null);
const killArmedId = ref<string | null>(null);
@@ -136,6 +146,11 @@ function formatDate(iso: string): string {
return new Date(iso).toLocaleString(locale.value);
}
function onPickCwd(path: string): void {
newCwd.value = path;
showPicker.value = false;
}
async function onCreate(): Promise<void> {
creating.value = true;
createError.value = null;