- POST /api/v1/projects : crée <root>/<name> (mkdir non récursif, 409 si existant), git init optionnel, puis lance une session dedans. Anti-traversal sur le nom (core/project.ts). - Web : NewProjectModal (racine préremplie scanRoots[0], case git init) + bouton dans SessionsListView. - SessionContextBar : badge du groupe (cliquable) + repos couverts avec leur branche dans l'en-tête de session ; session simple = repo+branche déduits du cwd. - 14 tests (unitaires + route).
83 lines
3.1 KiB
Vue
83 lines
3.1 KiB
Vue
<template>
|
|
<!-- Barre de contexte sous l'en-tête : groupe + repos couverts (avec branche). Masquée s'il n'y a
|
|
rien d'utile à montrer (session simple dans un dossier non reconnu → le cwd du header suffit). -->
|
|
<div v-if="shouldShow" class="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-zinc-800 bg-zinc-900/40 px-3 py-1.5 text-xs">
|
|
<RouterLink
|
|
v-if="group"
|
|
:to="{ name: 'group', params: { id: group.id } }"
|
|
class="flex items-center gap-1.5 rounded px-1.5 py-0.5 text-zinc-200 transition-colors hover:bg-zinc-800"
|
|
:title="t('terminal.group')"
|
|
>
|
|
<span class="h-2.5 w-2.5 shrink-0 rounded-full" :style="{ backgroundColor: group.color ?? '#52525b' }" />
|
|
<span class="font-medium">{{ group.label }}</span>
|
|
<span class="text-zinc-500">→</span>
|
|
</RouterLink>
|
|
|
|
<span v-if="group" class="text-zinc-600">{{ t('terminal.covers') }}</span>
|
|
|
|
<span
|
|
v-for="e in entries"
|
|
:key="e.dir"
|
|
class="flex items-center gap-1.5 rounded bg-zinc-950/60 px-1.5 py-0.5"
|
|
:title="e.dir"
|
|
>
|
|
<span class="font-mono text-zinc-300">{{ e.label }}</span>
|
|
<span v-if="e.branch" class="badge bg-zinc-800 font-mono text-zinc-400">{{ e.branch }}</span>
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import type { SessionSummary } from '@arboretum/shared';
|
|
import { useGroupsStore } from '../stores/groups';
|
|
import { useWorktreesStore } from '../stores/worktrees';
|
|
|
|
const props = defineProps<{ session: SessionSummary }>();
|
|
const { t } = useI18n();
|
|
const groups = useGroupsStore();
|
|
const worktrees = useWorktreesStore();
|
|
|
|
const group = computed(() => (props.session.groupId ? (groups.byId(props.session.groupId) ?? null) : null));
|
|
|
|
function basename(dir: string): string {
|
|
return dir.split('/').filter(Boolean).pop() ?? dir;
|
|
}
|
|
|
|
interface CoverEntry {
|
|
dir: string;
|
|
label: string;
|
|
branch: string | null;
|
|
resolved: boolean;
|
|
}
|
|
|
|
// Résout un répertoire couvert vers repo + branche : worktree exact d'abord (branche connue),
|
|
// sinon checkout principal d'un repo, sinon dernier segment du chemin (non résolu).
|
|
function resolve(dir: string): CoverEntry {
|
|
const wt = worktrees.worktrees.find((w) => w.path === dir);
|
|
if (wt) {
|
|
const repo = worktrees.repos.find((r) => r.id === wt.repoId);
|
|
return { dir, label: repo?.label ?? basename(dir), branch: wt.branch, resolved: true };
|
|
}
|
|
const repo = worktrees.repos.find((r) => r.path === dir);
|
|
if (repo) return { dir, label: repo.label, branch: null, resolved: true };
|
|
return { dir, label: basename(dir), branch: null, resolved: false };
|
|
}
|
|
|
|
const entries = computed<CoverEntry[]>(() => {
|
|
const dirs = [props.session.cwd, ...(props.session.addedDirs ?? [])];
|
|
const seen = new Set<string>();
|
|
const out: CoverEntry[] = [];
|
|
for (const d of dirs) {
|
|
if (seen.has(d)) continue;
|
|
seen.add(d);
|
|
out.push(resolve(d));
|
|
}
|
|
return out;
|
|
});
|
|
|
|
// Afficher si on est dans un groupe, ou si au moins un répertoire correspond à un repo connu.
|
|
const shouldShow = computed(() => group.value !== null || entries.value.some((e) => e.resolved));
|
|
</script>
|