feat(layout): supporte la création de layouts plugins (customPluginLayout)

Étend le flux de création de layout backend (DTO, usecases, store) et le
frontend (sélecteur, adaptateurs, LayoutTabs/LayoutGrid) pour permettre
d'ouvrir un layout déclaré par un plugin installé (ex. Android Health),
sans passer par le message bloquant "extension backend pas encore livrée".

Ticket #141 — QA vert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 18:34:21 +02:00
parent 168f93df78
commit 45617992ef
29 changed files with 907 additions and 99 deletions

View File

@ -282,6 +282,18 @@ export function applyOperation(
if (!found) throw notFound(op.target);
return { root };
}
case "setPluginLayoutState": {
let found = false;
const root = mapNode(tree.root, (n) => {
if (n.type === "customPluginLayout" && n.node.id === op.target) {
found = true;
return { type: "customPluginLayout", node: { ...n.node, state: op.state } };
}
return n;
});
if (!found) throw notFound(op.target);
return { root };
}
}
}
@ -324,15 +336,10 @@ export function droppedSessions(
}
/**
* Locally patches a `customPluginLayout` node's opaque `state` (#43, F4).
*
* Client-side only: the backend has no `LayoutOperation` variant to persist
* plugin layout state yet (carnet v2 §3.5 — no backend refonte expected for
* F4), so this does NOT call `LayoutGateway.mutateLayout`. It's the same
* "real, in-session, not yet cross-restart-persisted" contract every plugin
* component's `setState` gets: the tree re-renders with the new state
* immediately, but a reload re-fetches the last **persisted** value from the
* backend. Returns `tree` unchanged if no such node is found.
* Pure helper for patching a `customPluginLayout` node's opaque `state`.
* The runtime path persists the same change through `mutate_layout` with
* `setPluginLayoutState`; this helper remains useful for local tree transforms
* and focused parsing tests.
*/
export function setCustomPluginLayoutState(
tree: LayoutTree,