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>
120 lines
4.4 KiB
Vue
120 lines
4.4 KiB
Vue
<template>
|
|
<section class="card flex flex-col gap-2">
|
|
<header class="flex flex-wrap items-center gap-2">
|
|
<h2 class="font-semibold text-zinc-100">{{ repo.label }}</h2>
|
|
<BaseBadge v-if="!repo.valid" tone="red">{{ t('repos.invalid') }}</BaseBadge>
|
|
<span class="truncate font-mono text-xs text-zinc-500" :title="repo.path">{{ repo.path }}</span>
|
|
<div class="ml-auto flex items-center gap-2">
|
|
<BaseButton size="sm" :icon="Plus" @click="creating = !creating">{{ t('worktrees.new') }}</BaseButton>
|
|
<BaseButton size="sm" :icon="Scissors" :loading="busy" @click="onPrune">{{ t('worktrees.prune') }}</BaseButton>
|
|
<BaseButton
|
|
size="sm"
|
|
icon-only
|
|
:icon="repo.hidden ? Eye : EyeOff"
|
|
:aria-label="repo.hidden ? t('repos.show') : t('repos.hide')"
|
|
@click="onToggleHidden"
|
|
/>
|
|
<BaseButton variant="danger" size="sm" icon-only :icon="Trash2" :aria-label="t('repos.remove')" @click="onRemove" />
|
|
</div>
|
|
</header>
|
|
|
|
<form v-if="creating" class="card-inset flex flex-wrap items-end gap-2" @submit.prevent="onCreate">
|
|
<label class="flex flex-1 flex-col gap-1 text-xs text-zinc-400">
|
|
{{ t('worktrees.branch') }}
|
|
<input v-model="branch" class="input font-mono" placeholder="feature/…" required />
|
|
</label>
|
|
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
|
{{ t('worktrees.start') }}
|
|
<select v-model="startSession" class="input">
|
|
<option :value="null">{{ t('worktrees.startNone') }}</option>
|
|
<option value="claude">claude</option>
|
|
<option value="bash">bash</option>
|
|
</select>
|
|
</label>
|
|
<label class="flex items-center gap-1 text-xs text-zinc-400"><input v-model="newBranch" type="checkbox" /> {{ t('worktrees.newBranch') }}</label>
|
|
<BaseButton type="submit" variant="primary" size="sm" :loading="busy" :disabled="branch.trim() === ''">
|
|
{{ t('worktrees.create') }}
|
|
</BaseButton>
|
|
</form>
|
|
<p v-if="error" class="text-sm text-red-400">{{ error }}</p>
|
|
|
|
<div class="flex flex-col gap-1.5">
|
|
<WorktreeCard v-for="w in worktrees" :key="w.path" :worktree="w" />
|
|
<p v-if="worktrees.length === 0" class="px-1 text-xs text-zinc-600">{{ t('worktrees.noSession') }}</p>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { Eye, EyeOff, Plus, Scissors, Trash2 } from '@lucide/vue';
|
|
import type { RepoSummary } from '@arboretum/shared';
|
|
import { useWorktreesStore } from '../stores/worktrees';
|
|
import { useWorktreeView } from '../composables/useWorktreeView';
|
|
import { useToastsStore } from '../stores/toasts';
|
|
import WorktreeCard from './WorktreeCard.vue';
|
|
import BaseButton from './ui/BaseButton.vue';
|
|
import BaseBadge from './ui/BaseBadge.vue';
|
|
|
|
const props = defineProps<{ repo: RepoSummary }>();
|
|
const { t } = useI18n();
|
|
const store = useWorktreesStore();
|
|
const view = useWorktreeView();
|
|
const toasts = useToastsStore();
|
|
|
|
// applique le tri/filtre/recherche partagé (toolbar du dashboard) aux worktrees de ce repo.
|
|
const worktrees = computed(() => view.apply(store.worktreesForRepo(props.repo.id)));
|
|
const creating = ref(false);
|
|
const branch = ref('');
|
|
const newBranch = ref(true);
|
|
const startSession = ref<'claude' | 'bash' | null>(null);
|
|
const busy = ref(false);
|
|
const error = ref<string | null>(null);
|
|
|
|
async function onCreate(): Promise<void> {
|
|
busy.value = true;
|
|
error.value = null;
|
|
try {
|
|
await store.createWorktree(props.repo.id, { branch: branch.value.trim(), newBranch: newBranch.value, startSession: startSession.value });
|
|
branch.value = '';
|
|
creating.value = false;
|
|
toasts.success(t('toast.worktreeCreated'));
|
|
} catch (err) {
|
|
error.value = err instanceof Error ? err.message : String(err);
|
|
} finally {
|
|
busy.value = false;
|
|
}
|
|
}
|
|
|
|
async function onPrune(): Promise<void> {
|
|
busy.value = true;
|
|
error.value = null;
|
|
try {
|
|
await store.prune(props.repo.id);
|
|
toasts.success(t('toast.pruned'));
|
|
} catch (err) {
|
|
toasts.error(err);
|
|
} finally {
|
|
busy.value = false;
|
|
}
|
|
}
|
|
|
|
async function onRemove(): Promise<void> {
|
|
try {
|
|
await store.removeRepo(props.repo.id);
|
|
toasts.success(t('toast.repoRemoved'));
|
|
} catch (err) {
|
|
toasts.error(err);
|
|
}
|
|
}
|
|
|
|
async function onToggleHidden(): Promise<void> {
|
|
try {
|
|
await store.setHidden(props.repo.id, !props.repo.hidden);
|
|
} catch (err) {
|
|
toasts.error(err);
|
|
}
|
|
}
|
|
</script>
|