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:
@ -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();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user