feat(p11): temps réel complet (watcher checkout principal + topic settings)

P11-A — branche du checkout principal modifiée hors Arboretum :
- fs-watcher: pin « permanent » non évinçable (pinRepo/unpinRepo, repoPins dans evictIfNeeded)
- worktree-manager: armMainCheckoutWatchers() + arm/désarm sur addRepo/découverte/removeRepo/hidden
- index.ts: armMainCheckoutWatchers() dans runDaemon → git checkout CLI sur le principal → worktree_update <500ms sans watch client

P11-B — réglages en temps réel :
- protocole additif: type SettingsBroadcast (source unique, réutilisé par SettingsResponse), topic 'settings', message settings_update (validés parseClientMessage + gateway)
- core/settings-bus.ts (EventEmitter) ; routes/settings émet après PATCH ; gateway relaie aux abonnés 'settings'
- web: ws-client subscribeSettings + routage ; store settings applySettings/startRealtime ; AppShell abonne globalement ; SettingsView re-sync des drafts scalaires sans écraser une saisie en cours
- tests: protocol (topic settings) + fs-watcher (pinRepo non évincé) ; acceptance-p11.mjs (checkout principal <500ms + settings_update)
This commit is contained in:
2026-06-27 14:08:44 +02:00
parent 92670a796a
commit e8d10b7ec0
16 changed files with 304 additions and 38 deletions

View File

@@ -9,6 +9,7 @@ import {
type RepoSummary,
type ServerMessage,
type SessionSummary,
type SettingsBroadcast,
type WorktreeSummary,
} from '@arboretum/shared';
import type { ClientBinding, PtyManager } from '../core/pty-manager.js';
@@ -16,6 +17,7 @@ import type { DiscoveryService } from '../core/discovery-service.js';
import type { SessionArchiveService } from '../core/session-archive.js';
import type { WorktreeManager } from '../core/worktree-manager.js';
import type { GroupManager } from '../core/group-manager.js';
import type { SettingsBus } from '../core/settings-bus.js';
const HEARTBEAT_MS = 30_000;
@@ -31,6 +33,7 @@ export function registerWsGateway(
sessionArchive: SessionArchiveService,
worktrees: WorktreeManager,
groups: GroupManager,
settingsBus: SettingsBus,
serverVersion: string,
): void {
app.get('/ws', { websocket: true }, (socket: WebSocket, req) => {
@@ -41,6 +44,7 @@ export function registerWsGateway(
let subscribedSessions = false;
let subscribedWorktrees = false;
let subscribedGroups = false;
let subscribedSettings = false;
let alive = true;
// Worktrees « regardés » par CETTE connexion (clé repoId\0path) → push ciblé de worktree_changes.
const watched = new Set<string>();
@@ -85,6 +89,10 @@ export function registerWsGateway(
const onGroupRemoved = (groupId: string): void => {
if (subscribedGroups) send({ type: 'group_removed', groupId });
};
// P11 — un réglage a changé : relayé aux connexions abonnées au topic 'settings'.
const onSettingsUpdate = (settings: SettingsBroadcast): void => {
if (subscribedSettings) send({ type: 'settings_update', settings });
};
// P7 — détail d'un worktree modifié : poussé UNIQUEMENT si cette connexion le regarde.
const onWorktreeChanges = (e: { repoId: string; path: string }): void => {
if (watched.has(watchKey(e.repoId, e.path))) send({ type: 'worktree_changes', ...e });
@@ -100,6 +108,7 @@ export function registerWsGateway(
worktrees.on('worktree_changes', onWorktreeChanges);
groups.on('group_update', onGroupUpdate);
groups.on('group_removed', onGroupRemoved);
settingsBus.on('settings_update', onSettingsUpdate);
const heartbeat = setInterval(() => {
if (!alive) {
@@ -139,6 +148,7 @@ export function registerWsGateway(
subscribedSessions = msg.topics.includes('sessions');
subscribedWorktrees = msg.topics.includes('worktrees');
subscribedGroups = msg.topics.includes('groups');
subscribedSettings = msg.topics.includes('settings');
return;
}
case 'watch': {
@@ -250,6 +260,7 @@ export function registerWsGateway(
worktrees.off('worktree_changes', onWorktreeChanges);
groups.off('group_update', onGroupUpdate);
groups.off('group_removed', onGroupRemoved);
settingsBus.off('settings_update', onSettingsUpdate);
for (const [, st] of channels) manager.detach(st.sessionId, st.binding);
channels.clear();
// Libère le refcount du watcher pour chaque worktree regardé par cette connexion.