feat(frontend): système de plugins — runtime, menus, layouts custom (#43)

Lots F1-F4 : runtime de chargement/registre plugin, extension des menus
existants, panneau de gestion des plugins, types de layout custom
(sélecteur, fallback, cellule dédiée) branchés sur le port plugin.
Suite npm typecheck/test verte (947/947).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 07:37:11 +02:00
parent bb35641715
commit ac726d075e
41 changed files with 3245 additions and 24 deletions

View File

@ -45,6 +45,8 @@ import {
type SettingsSection,
} from "@/features/settings";
import { GitGraphView } from "@/features/git";
import { usePluginMenus, usePluginRuntime } from "@/features/plugins";
import type { WhenContext } from "@/plugins/runtime";
import {
Button,
DockRegion,
@ -112,7 +114,7 @@ function isTerminalBackgroundTaskEvent(
export function ProjectsView() {
const vm = useProjects();
const { system, window: windowGateway, focusedProject } = useGateways();
const { system, window: windowGateway, focusedProject, git } = useGateways();
const [name, setName] = useState("");
const [root, setRoot] = useState("");
// Placement of every open view (#22): each panel is "closed" (absent),
@ -144,6 +146,45 @@ export function ProjectsView() {
const active = vm.openTabs.find((t) => t.id === vm.activeTabId) ?? null;
// `gitRepository` signal for the plugin `when` mini-language (#43, F3 —
// Architect-mandated, blocking for #43 closure): whether the *current*
// project is a git repository, checked for real via `GitGateway.branches`
// (a non-repo project rejects this call; a repo resolves it). Re-checked on
// every project switch. `agentSelected`/`terminalFocused`/
// `layoutCellFocused` remain accepted v1 debt — no global focus signal
// exists at the menu-bar level yet — and stay `false` (see the F3 report's
// open point).
const [gitRepository, setGitRepository] = useState(false);
useEffect(() => {
if (!active) {
setGitRepository(false);
return;
}
let cancelled = false;
git
.branches(active.id)
.then(() => {
if (!cancelled) setGitRepository(true);
})
.catch(() => {
if (!cancelled) setGitRepository(false);
});
return () => {
cancelled = true;
};
}, [git, active?.id]);
// Plugin contribution menus (#43, F3).
const { registry: pluginRegistry } = usePluginRuntime();
const pluginWhenCtx: WhenContext = {
projectOpen: active !== null,
gitRepository,
agentSelected: false,
terminalFocused: false,
layoutCellFocused: false,
};
const pluginMenus = usePluginMenus(pluginRegistry, pluginWhenCtx);
// Reset the active layout whenever the active project changes. `activeLayout`
// is only repopulated asynchronously by `LayoutTabs` (which re-fetches the new
// project's layouts). Without this reset, the stale id of the *previous*
@ -391,29 +432,40 @@ export function ProjectsView() {
{
id: "panels",
label: "Panneaux",
items: panelOrder.map((panel) => ({
id: panel,
label: PANEL_TITLE[panel],
active: placementOf(placements, panel) !== "closed",
onSelect: () => {},
submenu: placementSubmenu(panel),
})),
// Native items first, plugin-contributed items after in their own
// (discreetly-labelled) group (#43, F3, carnet §7.2).
items: [
...panelOrder.map((panel) => ({
id: panel,
label: PANEL_TITLE[panel],
active: placementOf(placements, panel) !== "closed",
onSelect: () => {},
submenu: placementSubmenu(panel),
})),
...pluginMenus.itemsFor("panels"),
],
},
// Top-level plugin menus render between Panneaux and Paramètres (#43,
// carnet §0 UX decision + §7.1).
...pluginMenus.topLevelMenus,
{
id: "settings",
label: "Paramètres",
// One entry per section (#68). The entries name sections and mark the open
// one; closing lives in the view ("Fermer les paramètres"), so no label
// alternates.
items: SETTINGS_SECTIONS.map((section) => ({
id: section,
label: SETTINGS_SECTION_LABEL[section],
active: settingsSection === section,
onSelect: () => {
setSettingsSection(section);
dismissFloating();
},
})),
// alternates. Plugin-contributed items are appended after (#43, F3).
items: [
...SETTINGS_SECTIONS.map((section) => ({
id: section,
label: SETTINGS_SECTION_LABEL[section],
active: settingsSection === section,
onSelect: () => {
setSettingsSection(section);
dismissFloating();
},
})),
...pluginMenus.itemsFor("settings"),
],
},
];
@ -691,6 +743,10 @@ export function ProjectsView() {
cwd={active.root}
layoutId={activeLayout?.id}
onOpenConversation={openConversation}
onOpenPluginsSettings={() => {
setSettingsSection("plugins");
dismissFloating();
}}
/>
)}
</>