feat: découverte automatique des dépôts git (scan + montrer/cacher)

Scan borné du système de fichiers (racines configurables, défaut home ; profondeur/nombre/timeout bornés ; symlinks non suivis ; exclusions node_modules/dotdirs) qui auto-enregistre les nouveaux dépôts. Insertion atomique ON CONFLICT DO NOTHING (idempotence + anti-résurrection d'un dépôt masqué + anti-course). Scan au démarrage + bouton manuel + re-scan périodique (RepoDiscoveryService, démarré dans runDaemon). Colonne repos.hidden : masquer = conservé en DB mais exclu du dashboard et jamais ré-ajouté ; supprimer = re-découvrable. UI : bouton œil par dépôt + bascule afficher-les-masqués sur le dashboard, section Découverte dans les Réglages (racines + intervalle, allow-list stricte). Robustesse : listAllWorktrees tolère l'échec git par dépôt ; flag --no-discover (escape hatch + hermétisme des acceptations).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Johan LEROY
2026-06-18 14:45:12 +02:00
co-authored by Claude Opus 4.8
parent 940ccffeab
commit fa9952bc5c
28 changed files with 824 additions and 34 deletions
+35 -3
View File
@@ -1,5 +1,8 @@
<template>
<PageHeader :title="t('nav.worktrees')">
<BaseButton variant="ghost" size="sm" :icon="ScanSearch" :loading="scanning" @click="onDiscover">
{{ t('repos.scan') }}
</BaseButton>
<BaseButton variant="ghost" size="sm" :icon="RefreshCw" :loading="store.loading" @click="refresh">
{{ t('common.refresh') }}
</BaseButton>
@@ -33,6 +36,9 @@
<template v-else>
<!-- une seule toolbar pilote le tri/filtre des worktrees de toutes les sections -->
<ListToolbar :controls="view" search-placeholder-key="controls.searchPlaceholder" />
<label v-if="hiddenCount > 0" class="flex items-center gap-2 px-1 text-xs text-zinc-400">
<input v-model="showHidden" type="checkbox" /> {{ t('repos.showHidden', { n: hiddenCount }) }}
</label>
<EmptyState v-if="visibleRepos.length === 0" :icon="Search" :title="t('controls.results', 0)" />
<RepoSection v-for="repo in pagedRepos" :key="repo.id" :repo="repo" />
<Pagination
@@ -50,7 +56,7 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { GitBranch, RefreshCw, FolderOpen, Plus, Search } from '@lucide/vue';
import { GitBranch, RefreshCw, FolderOpen, Plus, Search, ScanSearch } from '@lucide/vue';
import { useWorktreesStore } from '../stores/worktrees';
import { useToastsStore } from '../stores/toasts';
import { useWorktreeView } from '../composables/useWorktreeView';
@@ -72,15 +78,29 @@ const view = useWorktreeView();
const newPath = ref('');
const showPicker = ref(false);
const adding = ref(false);
const scanning = ref(false);
const showHidden = ref(false);
// repos triés par libellé ; masqués quand un filtre/recherche worktree actif ne laisse rien dans le repo.
const sortedRepos = computed(() => [...store.repos].sort((a, b) => a.label.localeCompare(b.label)));
const hiddenCount = computed(() => store.repos.filter((r) => r.hidden).length);
// repos visibles : les masqués sont exclus sauf si « afficher les masqués » est coché. Triés par libellé ;
// puis masqués quand un filtre/recherche worktree actif ne laisse rien dans le repo.
const sortedRepos = computed(() =>
[...store.repos]
.filter((r) => showHidden.value || !r.hidden)
.sort((a, b) => a.label.localeCompare(b.label)),
);
const visibleRepos = computed(() =>
view.isActive()
? sortedRepos.value.filter((r) => view.apply(store.worktreesForRepo(r.id)).length > 0)
: sortedRepos.value,
);
// GET /worktrees exclut les repos masqués (perf) → charger paresseusement leurs worktrees au dévoilement.
watch(showHidden, (on) => {
if (on) for (const r of store.repos.filter((x) => x.hidden)) void store.refreshRepoWorktrees(r.id);
});
// pagination des repos (rarement nombreux → masquée par défaut).
const repoPage = ref(1);
const repoPageSize = ref(10);
@@ -110,6 +130,18 @@ function refresh(): void {
void store.fetchAll();
}
async function onDiscover(): Promise<void> {
scanning.value = true;
try {
const res = await store.discover();
toasts.success(t('repos.scanResult', res.added));
} catch (err) {
toasts.error(err);
} finally {
scanning.value = false;
}
}
async function onAddRepo(): Promise<void> {
adding.value = true;
try {