- détachement: le panneau Paramètres du projet devient détachable (mapping historique panel=context → projectSettings dans parseViewWindowParams, DETACHABLE/ViewPanelBody alignés, ProjectsView ne court-circuitant plus le refus); Retry d'échec de détachement préservé - couleurs projet: projectTheme publie idea:project-theme-changed localement + BroadcastChannel inter-webviews (fenêtres OS détachées), garde de révision par projet dans ProjectsView/ViewWindow contre les lectures settings en course — plus de flash/retour au bleu par défaut - conversation custom CLI: transcript_restore_expected ne considère plus l'absence de scrollback volatile comme une restauration échouée quand la session rouvre en SessionPlan::None (continuation vide valide); CustomAgentChatView n'affiche plus le faux « conversation liée n'a pas pu être restaurée » - images: conservation des images de conversation après changement de projet puis retour - menus Panneaux: retrait des entrées Projects et Project Context (panelOrder WebWorkspace, création/switch via tab-bar +, contexte projet fusionné dans Paramètres du projet) - QA verte: tests frontend ciblés window/ViewWindow/ProjectSettingsPanel/ProjectsView.detach/projects/CustomAgentChatView/WebDesktopWorkspace + cargo test -p application lifecycle transcript_restore_expected; fichiers .ideai/* (tickets 308-315, index/counter, agents/test, chat-drafts, system-permissions, project-settings.json, attachments/) et scripts/ hors périmètre préservés en worktree; aucun push
94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
/**
|
|
* Tauri adapter for {@link WindowGateway} (ticket #23). Detaches a View into its
|
|
* own OS window by delegating to the backend's window-lifecycle commands. Like
|
|
* the sibling adapters, this is the only layer allowed to touch
|
|
* `@tauri-apps/api`; components reach it exclusively through the port.
|
|
*
|
|
* Command/event names and payloads below mirror the backend contract owned by
|
|
* DevBackend (build-verified). The backend exposes a single lifecycle channel
|
|
* `view-window://lifecycle`; this adapter narrows it to the port's close-only
|
|
* `onViewWindowClosed` by filtering on `kind === "closed"`.
|
|
*/
|
|
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { listen } from "@tauri-apps/api/event";
|
|
|
|
import type { Unsubscribe } from "@/domain";
|
|
import type {
|
|
PluginLayoutWindowOpenInput,
|
|
PluginLayoutWindowOpenResult,
|
|
ViewWindowClosed,
|
|
ViewWindowSnapshot,
|
|
WindowGateway,
|
|
} from "@/ports";
|
|
|
|
/** The single Tauri event carrying detached-window lifecycle transitions. */
|
|
const VIEW_WINDOW_LIFECYCLE = "view-window://lifecycle";
|
|
|
|
// The backend keeps the historical `context` window identity. Project context
|
|
// now lives inside `projectSettings`, so the adapter owns the compatibility
|
|
// alias while the UI consistently uses the current panel id.
|
|
function toBackendPanel(panel: string): string {
|
|
return panel === "projectSettings" ? "context" : panel;
|
|
}
|
|
|
|
function fromBackendPanel(panel: string): string {
|
|
return panel === "context" ? "projectSettings" : panel;
|
|
}
|
|
|
|
/**
|
|
* Backend lifecycle payload (discriminated on `kind`). Ticket #47 dropped the
|
|
* `projectId` field — a detached window is panel-only and follows the focused
|
|
* project, so lifecycle events carry only `panel` + `label`.
|
|
*/
|
|
interface ViewWindowLifecycle {
|
|
kind: "opened" | "focused" | "closed";
|
|
panel: string;
|
|
label: string;
|
|
}
|
|
|
|
export class TauriWindowGateway implements WindowGateway {
|
|
async openViewWindow(panel: string): Promise<void> {
|
|
// Panel-only window (#47): no project id — the window follows the focused
|
|
// project published on `focused-project://changed`.
|
|
await invoke("open_view_window", { panel: toBackendPanel(panel) });
|
|
}
|
|
|
|
async openPluginLayoutWindow(
|
|
input: PluginLayoutWindowOpenInput,
|
|
): Promise<PluginLayoutWindowOpenResult> {
|
|
return invoke<PluginLayoutWindowOpenResult>("open_plugin_layout_window", {
|
|
input,
|
|
});
|
|
}
|
|
|
|
async closeViewWindow(panel: string): Promise<void> {
|
|
await invoke("close_view_window", { panel: toBackendPanel(panel) });
|
|
}
|
|
|
|
async listOpenViewWindows(): Promise<ViewWindowSnapshot[]> {
|
|
// Backend serialises the snapshots in camelCase (panel/label/visible), so the
|
|
// wire shape matches the port type directly.
|
|
const snapshots = await invoke<ViewWindowSnapshot[]>("list_open_view_windows");
|
|
return snapshots.map((snapshot) => ({
|
|
...snapshot,
|
|
panel: fromBackendPanel(snapshot.panel),
|
|
}));
|
|
}
|
|
|
|
async onViewWindowClosed(
|
|
handler: (event: ViewWindowClosed) => void,
|
|
): Promise<Unsubscribe> {
|
|
// One backend channel for opened/focused/closed; the port only surfaces the
|
|
// close, so drop the other transitions and forward `{ panel }`.
|
|
const unlisten = await listen<ViewWindowLifecycle>(
|
|
VIEW_WINDOW_LIFECYCLE,
|
|
(e) => {
|
|
if (e.payload.kind !== "closed") return;
|
|
handler({ panel: fromBackendPanel(e.payload.panel) });
|
|
},
|
|
);
|
|
return unlisten;
|
|
}
|
|
}
|