P3-C: dashboard worktree-first + acceptance P3 (MVP complet)
Vue racine consolidée : repos → worktrees avec état git ET état de session corrélé. Complète le MVP (P2 découverte/reprise + P3-A worktrees + P3-B états fins). - web/lib/ws-client: abonnement multi-topics (sessions + worktrees), subscribeWorktrees. - web/stores/worktrees: repos + worktrees, CRUD, temps réel (repo_update/worktree_*). - web/views/DashboardView (route racine /), components RepoSection + WorktreeCard ; SessionStateBadge réutilisé pour l'état des sessions corrélées. - router: / = dashboard, /sessions = liste à plat (sessions hors worktree), nav croisée. - i18n EN/FR (dashboard/repos/worktrees). - fix: @xterm/headless est CommonJS → chargé via createRequire (l'import nommé ESM échouait sous Node natif, masqué par esbuild en test) ; détecté par l'acceptation. - scripts/acceptance-p3.mjs: repo git tmp → enregistrement, worktree + hook, broadcast WS worktree_update, corrélation session bash, suppression (409 sans force, 200 avec). Vérifs : typecheck, 159 tests, build (vue-tsc), acceptations P1/P2/P3 ALL GREEN. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
126
packages/web/src/stores/worktrees.ts
Normal file
126
packages/web/src/stores/worktrees.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import type {
|
||||
CreateWorktreeRequest,
|
||||
CreateWorktreeResponse,
|
||||
RepoResponse,
|
||||
ReposListResponse,
|
||||
RepoSummary,
|
||||
WorktreeSummary,
|
||||
WorktreesListResponse,
|
||||
} from '@arboretum/shared';
|
||||
import { api } from '../lib/api';
|
||||
import { wsClient, type WorktreeEvent } from '../lib/ws-client';
|
||||
|
||||
export const useWorktreesStore = defineStore('worktrees', () => {
|
||||
const repos = ref<RepoSummary[]>([]);
|
||||
const worktrees = ref<WorktreeSummary[]>([]);
|
||||
const loading = ref(false);
|
||||
const loadError = ref<string | null>(null);
|
||||
let unsubscribe: (() => void) | null = null;
|
||||
|
||||
function upsertRepo(repo: RepoSummary): void {
|
||||
const idx = repos.value.findIndex((r) => r.id === repo.id);
|
||||
if (idx >= 0) repos.value.splice(idx, 1, repo);
|
||||
else repos.value.push(repo);
|
||||
}
|
||||
function removeRepoLocal(id: string): void {
|
||||
repos.value = repos.value.filter((r) => r.id !== id);
|
||||
worktrees.value = worktrees.value.filter((w) => w.repoId !== id);
|
||||
}
|
||||
function upsertWorktree(wt: WorktreeSummary): void {
|
||||
const idx = worktrees.value.findIndex((w) => w.path === wt.path);
|
||||
if (idx >= 0) worktrees.value.splice(idx, 1, wt);
|
||||
else worktrees.value.push(wt);
|
||||
}
|
||||
function removeWorktreeLocal(path: string): void {
|
||||
worktrees.value = worktrees.value.filter((w) => w.path !== path);
|
||||
}
|
||||
|
||||
function onEvent(e: WorktreeEvent): void {
|
||||
if (e.type === 'repo_update') upsertRepo(e.repo);
|
||||
else if (e.type === 'repo_removed') removeRepoLocal(e.repoId);
|
||||
else if (e.type === 'worktree_update') upsertWorktree(e.worktree);
|
||||
else removeWorktreeLocal(e.path);
|
||||
}
|
||||
|
||||
function worktreesForRepo(repoId: string): WorktreeSummary[] {
|
||||
return worktrees.value
|
||||
.filter((w) => w.repoId === repoId)
|
||||
.sort((a, b) => Number(b.isMain) - Number(a.isMain) || a.path.localeCompare(b.path));
|
||||
}
|
||||
|
||||
async function fetchAll(): Promise<void> {
|
||||
loading.value = true;
|
||||
loadError.value = null;
|
||||
try {
|
||||
const [r, w] = await Promise.all([
|
||||
api.get<ReposListResponse>('/api/v1/repos'),
|
||||
api.get<WorktreesListResponse>('/api/v1/worktrees'),
|
||||
]);
|
||||
repos.value = r.repos;
|
||||
worktrees.value = w.worktrees;
|
||||
} catch (err) {
|
||||
loadError.value = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRepoWorktrees(repoId: string): Promise<void> {
|
||||
const w = await api.get<WorktreesListResponse>(`/api/v1/repos/${repoId}/worktrees`);
|
||||
for (const wt of w.worktrees) upsertWorktree(wt);
|
||||
}
|
||||
|
||||
async function addRepo(path: string, label?: string): Promise<RepoSummary> {
|
||||
const res = await api.post<RepoResponse>('/api/v1/repos', { path, ...(label ? { label } : {}) });
|
||||
upsertRepo(res.repo);
|
||||
await refreshRepoWorktrees(res.repo.id);
|
||||
return res.repo;
|
||||
}
|
||||
|
||||
async function removeRepo(id: string): Promise<void> {
|
||||
await api.delete<{ ok: true }>(`/api/v1/repos/${id}`);
|
||||
removeRepoLocal(id);
|
||||
}
|
||||
|
||||
async function createWorktree(repoId: string, req: CreateWorktreeRequest): Promise<CreateWorktreeResponse> {
|
||||
const res = await api.post<CreateWorktreeResponse>(`/api/v1/repos/${repoId}/worktrees`, req);
|
||||
upsertWorktree(res.worktree);
|
||||
return res;
|
||||
}
|
||||
|
||||
async function deleteWorktree(repoId: string, path: string, force: boolean): Promise<void> {
|
||||
await api.delete<{ ok: true }>(`/api/v1/repos/${repoId}/worktrees?path=${encodeURIComponent(path)}&force=${force}`);
|
||||
removeWorktreeLocal(path);
|
||||
}
|
||||
|
||||
async function prune(repoId: string): Promise<void> {
|
||||
await api.post<{ ok: true }>(`/api/v1/repos/${repoId}/worktrees/prune`);
|
||||
await refreshRepoWorktrees(repoId);
|
||||
}
|
||||
|
||||
function startRealtime(): void {
|
||||
unsubscribe ??= wsClient.subscribeWorktrees(onEvent);
|
||||
}
|
||||
function stopRealtime(): void {
|
||||
unsubscribe?.();
|
||||
unsubscribe = null;
|
||||
}
|
||||
|
||||
return {
|
||||
repos,
|
||||
worktrees,
|
||||
loading,
|
||||
loadError,
|
||||
worktreesForRepo,
|
||||
fetchAll,
|
||||
addRepo,
|
||||
removeRepo,
|
||||
createWorktree,
|
||||
deleteWorktree,
|
||||
prune,
|
||||
startRealtime,
|
||||
stopRealtime,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user