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

@ -937,40 +937,84 @@ export interface TicketGateway {
* Payload of the OS window-lifecycle event that fires when a detached view
* window closes (ticket #23). Lets the main window re-toggle the view's
* placement out of `"detached"`.
*
* Ticket #47: a detached window is now **panel-only** and follows the main
* window's focused project rather than embedding a project id, so the close
* event no longer carries a `projectId` — the placement is keyed by `panel`.
*/
export interface ViewWindowClosed {
/** The detached panel id (a `PanelId`, kept as `string` at the port seam). */
panel: string;
/** The project the detached window was showing. */
projectId: string;
}
/**
* Detaching a View into its own OS window (ticket #23).
* Detaching a View into its own OS window (ticket #23, reworked in #47).
*
* The backend owns the Tauri `WebviewWindow` lifecycle (create/focus/close,
* anti-duplicate registry). This port is the UI's only door to it — components
* never call `invoke()` directly. `panel` is a `PanelId`-shaped string; it is
* typed as `string` here so ports need not depend on the `features/` layer.
*
* A detached window is **panel-only**: it no longer carries a project id and
* instead follows the focused project published through
* {@link FocusedProjectGateway}. Windows are therefore keyed by `panel` alone.
*/
export interface WindowGateway {
/**
* Opens — or focuses, if already open — a separate OS window rendering only
* `panel` for `projectId` (the backend's `open_view_window` command).
* `panel` (the backend's `open_view_window` command). The window follows the
* main window's focused project; no project id is passed.
*/
openViewWindow(panel: string, projectId: string): Promise<void>;
/** Closes the detached window for `panel`/`projectId` if one is open. */
closeViewWindow(panel: string, projectId: string): Promise<void>;
openViewWindow(panel: string): Promise<void>;
/** Closes the detached window for `panel` if one is open. */
closeViewWindow(panel: string): Promise<void>;
/**
* Subscribes to detached-window close events (OS close or programmatic). The
* handler receives the `{ panel, projectId }` that closed. Returns an
* unsubscribe.
* handler receives the `{ panel }` that closed. Returns an unsubscribe.
*/
onViewWindowClosed(
handler: (event: ViewWindowClosed) => void,
): Promise<Unsubscribe>;
}
/**
* A focused-project snapshot (ticket #47). Mirrors the backend
* `FocusedProjectDto` — the minimal identity a panel-only window needs to mount
* a project-scoped view (id for the read-models, name for chrome, root as the
* agents panel's cwd base).
*/
export interface FocusedProject {
id: string;
name: string;
root: string;
}
/**
* The focused-project channel between the main window and detached panel-only
* windows (ticket #47).
*
* The main window is the single writer: it publishes the currently active
* project (or `null` when none is open) via {@link setFocusedProject}. Detached
* windows are readers: they read the current focus at mount
* ({@link getFocusedProject}) then track changes via
* {@link onFocusedProjectChanged}. This decouples a restored panel window from
* any stale embedded project id — with no focus it shows an "open a project"
* shell and mounts no project-scoped panel.
*/
export interface FocusedProjectGateway {
/** Publishes the focused project (or `null` when no project is active). */
setFocusedProject(project: FocusedProject | null): Promise<void>;
/** Reads the current focused project; `null` when none is focused. */
getFocusedProject(): Promise<FocusedProject | null>;
/**
* Subscribes to focused-project changes. The handler receives the new focus
* (or `null`). Returns an unsubscribe.
*/
onFocusedProjectChanged(
handler: (project: FocusedProject | null) => void,
): Promise<Unsubscribe>;
}
/**
* Thin UI-preferences port (ticket #29). Persists small, per-surface pieces of
* UI state (like the ticket filters) so they survive a restart / reopen. This is
@ -1018,5 +1062,6 @@ export interface Gateways {
conversation: ConversationGateway;
ticket: TicketGateway;
window: WindowGateway;
focusedProject: FocusedProjectGateway;
uiPreferences: UiPreferencesGateway;
}