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

@ -12,6 +12,7 @@ import {
MockAgentGateway,
MockGitGateway,
MockProfileGateway,
MockFocusedProjectGateway,
MockProjectGateway,
MockSystemGateway,
MockTemplateGateway,
@ -24,7 +25,7 @@ import { ProjectsView } from "./ProjectsView";
async function renderWithProject() {
const project = new MockProjectGateway();
const created = await project.createProject("alpha", "/p/a");
await project.createProject("alpha", "/p/a");
const agentGateway = new MockAgentGateway();
const windowGateway = new MockWindowGateway();
const gateways = {
@ -36,6 +37,7 @@ async function renderWithProject() {
git: new MockGitGateway(),
workState: new MockWorkStateGateway(),
window: windowGateway,
focusedProject: new MockFocusedProjectGateway(),
} as unknown as Gateways;
render(
<DIProvider gateways={gateways}>
@ -46,7 +48,7 @@ async function renderWithProject() {
await waitFor(() =>
expect(within(screen.getByRole("tablist")).getAllByRole("tab")).toHaveLength(1),
);
return { windowGateway, projectId: created.id };
return { windowGateway };
}
/**
@ -62,7 +64,7 @@ function pickPlacement(panelTitle: string, placementLabel: string) {
describe("ProjectsView detach to OS window (#23)", () => {
it("« Fenêtre détachée » detaches a view: opens the OS window and marks the slot detached", async () => {
const { windowGateway, projectId } = await renderWithProject();
const { windowGateway } = await renderWithProject();
// A docked view is visible in the main window first…
pickPlacement("Git", "Ancré à gauche");
@ -72,7 +74,7 @@ describe("ProjectsView detach to OS window (#23)", () => {
pickPlacement("Git", "Fenêtre détachée");
await waitFor(() =>
expect(windowGateway.open.has(`git|${projectId}`)).toBe(true),
expect(windowGateway.open.has("git")).toBe(true),
);
// The slot is detached ⇒ nothing renders for Git in the main window.
await waitFor(() =>
@ -82,11 +84,11 @@ describe("ProjectsView detach to OS window (#23)", () => {
});
it("re-toggles the placement out of detached when the OS window closes", async () => {
const { windowGateway, projectId } = await renderWithProject();
const { windowGateway } = await renderWithProject();
pickPlacement("Git", "Fenêtre détachée");
await waitFor(() =>
expect(windowGateway.open.has(`git|${projectId}`)).toBe(true),
expect(windowGateway.open.has("git")).toBe(true),
);
// The « Fenêtre détachée » submenu leaf shows the active marker (●) while
@ -103,7 +105,7 @@ describe("ProjectsView detach to OS window (#23)", () => {
fireEvent.click(screen.getByRole("button", { name: "Panneaux" })); // close
// The backend reports the OS window closed ⇒ the slot leaves "detached".
windowGateway.simulateOsClose("git", projectId);
windowGateway.simulateOsClose("git");
openGitSubmenu();
await waitFor(() =>
expect(detachedLeaf().textContent).not.toContain("●"),