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.
This commit is contained in:
@@ -27,6 +27,7 @@ export interface TerminalSink {
|
||||
}
|
||||
|
||||
export type SessionEvent = Extract<ServerMessage, { type: 'session_update' | 'session_exit' }>;
|
||||
export type WorktreeEvent = Extract<ServerMessage, { type: 'repo_update' | 'repo_removed' | 'worktree_update' | 'worktree_removed' }>;
|
||||
|
||||
export interface AttachOptions {
|
||||
sessionId: string;
|
||||
@@ -104,6 +105,7 @@ export class WsClient {
|
||||
/** FIFO des attaches en attente : le serveur répond aux 'attach' dans l'ordre */
|
||||
private awaitingAttached: Attachment[] = [];
|
||||
private readonly sessionListeners = new Set<(e: SessionEvent) => void>();
|
||||
private readonly worktreeListeners = new Set<(e: WorktreeEvent) => void>();
|
||||
|
||||
connect(): void {
|
||||
this.stopped = false;
|
||||
@@ -143,13 +145,35 @@ export class WsClient {
|
||||
return promise;
|
||||
}
|
||||
|
||||
/** topics actifs = union des abonnements courants (une seule connexion partagée). */
|
||||
private activeTopics(): Array<'sessions' | 'worktrees'> {
|
||||
const t: Array<'sessions' | 'worktrees'> = [];
|
||||
if (this.sessionListeners.size > 0) t.push('sessions');
|
||||
if (this.worktreeListeners.size > 0) t.push('worktrees');
|
||||
return t;
|
||||
}
|
||||
|
||||
private sendSub(): void {
|
||||
this.sendControl({ type: 'sub', topics: this.activeTopics() });
|
||||
}
|
||||
|
||||
subscribeSessions(listener: (e: SessionEvent) => void): () => void {
|
||||
this.sessionListeners.add(listener);
|
||||
this.connect();
|
||||
if (this.sessionListeners.size === 1) this.sendControl({ type: 'sub', topics: ['sessions'] });
|
||||
this.sendSub();
|
||||
return () => {
|
||||
this.sessionListeners.delete(listener);
|
||||
if (this.sessionListeners.size === 0) this.sendControl({ type: 'sub', topics: [] });
|
||||
this.sendSub();
|
||||
};
|
||||
}
|
||||
|
||||
subscribeWorktrees(listener: (e: WorktreeEvent) => void): () => void {
|
||||
this.worktreeListeners.add(listener);
|
||||
this.connect();
|
||||
this.sendSub();
|
||||
return () => {
|
||||
this.worktreeListeners.delete(listener);
|
||||
this.sendSub();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -288,7 +312,7 @@ export class WsClient {
|
||||
this.ready = true;
|
||||
this.backoffMs = BACKOFF_MIN_MS;
|
||||
this.status.value = 'open';
|
||||
if (this.sessionListeners.size > 0) this.sendControl({ type: 'sub', topics: ['sessions'] });
|
||||
this.sendSub();
|
||||
// ré-attache des terminaux encore ouverts (nouveaux canaux côté serveur)
|
||||
for (const att of this.attachments) {
|
||||
if (!att.closed) this.sendAttach(att);
|
||||
@@ -339,6 +363,13 @@ export class WsClient {
|
||||
for (const cb of this.sessionListeners) cb(msg);
|
||||
return;
|
||||
}
|
||||
case 'repo_update':
|
||||
case 'repo_removed':
|
||||
case 'worktree_update':
|
||||
case 'worktree_removed': {
|
||||
for (const cb of this.worktreeListeners) cb(msg);
|
||||
return;
|
||||
}
|
||||
case 'error': {
|
||||
if (msg.channel !== undefined) {
|
||||
console.warn(`[ws] channel ${msg.channel}: ${msg.code} — ${msg.message}`);
|
||||
|
||||
Reference in New Issue
Block a user