fix(windows): fenêtres de panneau détachées suivent le projet en focus côté UI (#47)

Partie frontend. Ajoute un adaptateur focusedProject et un port dédié : la
ViewWindow détachée n'est plus liée à un project_id figé, elle s'abonne à
l'event focused-project émis par la fenêtre principale et affiche le panneau
du projet courant. ProjectsView propage le focus ; le détachement crée une
fenêtre panel-only. Couvert par les tests window/ViewWindow/focusedProject/
ProjectsView.focus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 12:37:45 +02:00
parent 6387fac34f
commit 221cc8be78
14 changed files with 522 additions and 130 deletions

View File

@ -91,6 +91,8 @@ import type {
UiPreferencesGateway,
ViewWindowClosed,
WindowGateway,
FocusedProject,
FocusedProjectGateway,
WorkStateGateway,
} from "@/ports";
import { normalizeProjectWorkState } from "../workStateNormalization";
@ -1094,27 +1096,25 @@ class MockRemoteGateway implements RemoteGateway {
}
/**
* In-memory {@link WindowGateway} for tests/dev (#23). Tracks which views are
* "detached" in a set keyed by `panel|projectId`, and lets tests simulate the
* OS closing a window via {@link simulateOsClose}. `closeViewWindow` also emits
* the close event, mirroring the backend (a programmatic close still notifies).
* In-memory {@link WindowGateway} for tests/dev (#23, panel-only in #47). Tracks
* which views are "detached" in a set keyed by `panel` alone (a panel-only
* window follows the focused project, so no project id is part of the key), and
* lets tests simulate the OS closing a window via {@link simulateOsClose}.
* `closeViewWindow` also emits the close event, mirroring the backend (a
* programmatic close still notifies).
*/
export class MockWindowGateway implements WindowGateway {
/** Currently-open detached windows, keyed `panel|projectId`. */
/** Currently-open detached windows, keyed by `panel`. */
readonly open = new Set<string>();
private readonly listeners = new Set<(e: ViewWindowClosed) => void>();
private key(panel: string, projectId: string): string {
return `${panel}|${projectId}`;
async openViewWindow(panel: string): Promise<void> {
this.open.add(panel);
}
async openViewWindow(panel: string, projectId: string): Promise<void> {
this.open.add(this.key(panel, projectId));
}
async closeViewWindow(panel: string, projectId: string): Promise<void> {
if (this.open.delete(this.key(panel, projectId))) {
this.emit({ panel, projectId });
async closeViewWindow(panel: string): Promise<void> {
if (this.open.delete(panel)) {
this.emit({ panel });
}
}
@ -1128,9 +1128,9 @@ export class MockWindowGateway implements WindowGateway {
}
/** Test hook: simulate the user closing the OS window (fires the event). */
simulateOsClose(panel: string, projectId: string): void {
this.open.delete(this.key(panel, projectId));
this.emit({ panel, projectId });
simulateOsClose(panel: string): void {
this.open.delete(panel);
this.emit({ panel });
}
private emit(event: ViewWindowClosed): void {
@ -1138,6 +1138,38 @@ export class MockWindowGateway implements WindowGateway {
}
}
/**
* In-memory {@link FocusedProjectGateway} for tests/dev (#47). Holds the current
* focus in memory and fans changes out to subscribers — the mock counterpart to
* the backend's shared focused-project state + `focused-project://changed`
* event. `setFocusedProject` always notifies (mirrors the backend emit), so a
* detached-window test can drive a panel window's focus transitions.
*/
export class MockFocusedProjectGateway implements FocusedProjectGateway {
private focused: FocusedProject | null = null;
private readonly listeners = new Set<
(project: FocusedProject | null) => void
>();
async setFocusedProject(project: FocusedProject | null): Promise<void> {
this.focused = project;
for (const l of this.listeners) l(project);
}
async getFocusedProject(): Promise<FocusedProject | null> {
return this.focused;
}
async onFocusedProjectChanged(
handler: (project: FocusedProject | null) => void,
): Promise<Unsubscribe> {
this.listeners.add(handler);
return () => {
this.listeners.delete(handler);
};
}
}
/**
* The pre-filled reference catalogue the mock serves — mirror of the backend's
* **selectable** catalogue (§17.3/D7): only profiles drivable in structured mode
@ -2596,6 +2628,7 @@ export function createMockGateways(): Gateways {
conversation: new MockConversationGateway(),
ticket: new MockTicketGateway(systemGateway),
window: new MockWindowGateway(),
focusedProject: new MockFocusedProjectGateway(),
uiPreferences: new MockUiPreferencesGateway(),
};
}

View File

@ -17,6 +17,7 @@ describe("createMockGateways", () => {
"agent",
"conversation",
"embedder",
"focusedProject",
"git",
"input",
"layout",