feat(frontend): panneau Settings Deployment et gateway serveur desktop (#68 F1+F2)
Donne à l'utilisateur la surface pour activer le serveur depuis l'app. - `features/settings/` : `SettingsView`, `DeploymentSettings`, `useDeployment`. - `adapters/desktopServer.ts` + port `DesktopServerGateway` et DTO associés ; adapters mock et http/unsupported alignés (le mode web n'expose pas le contrôle du serveur qui l'héberge). - `ProjectsView` : le `showSettings: boolean` devient une navigation interne `AI Profiles` / `Deployment`. Le libellé alternant « Close AI Profiles » disparaît — Settings existait déjà dans cette vue, la surface évolue au lieu d'ajouter un `PanelId`. CORRECTION D'UN BRIEF FAUX, remontée spontanément par DevFrontend et qui mérite de survivre : le cadrage décrivait le mode `remoteProxyOtherMachine` avec deux champs. `validate_settings` en exige un troisième, `lanBindAddress`, et rejette loopback comme unspecified. Construit selon la spec, chaque save et chaque start en mode 3 aurait échoué — le lot serait parti vert et cassé. Le panneau expose donc un select alimenté par `candidateLanAddresses` fourni par le backend : la règle « le frontend n'invente jamais une IP » tient. QA ré-exécutée par Git avant merge : `npm run typecheck` exit 0 · `npx vitest run` 87 files / 789 passed, 0 échec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -9,9 +9,13 @@
|
||||
* │ MENU BAR Panneaux │ Settings │
|
||||
* ├───────────────────────────────────────────────────────┤
|
||||
* │ MAIN — LayoutGrid (fills the FULL width) │
|
||||
* │ — or the projects manager / welcome when no project │
|
||||
* │ — or Settings / the projects manager / welcome │
|
||||
* └───────────────────────────────────────────────────────┘
|
||||
*
|
||||
* **Settings** (`Settings → AI Profiles | Deployment`, #68) is a main surface
|
||||
* with its own internal section nav — it has no `PanelId` and is outside the
|
||||
* `viewPlacement` model, unlike every panel below.
|
||||
*
|
||||
* The former left sidebar is gone: every panel (context, work, tickets, agents,
|
||||
* templates, skills, permissions, memory, git, projects) is reached from the
|
||||
* single **Panneaux** menu (#26), whose per-panel submenu picks the placement
|
||||
@ -34,7 +38,12 @@ import { useEffect, useState, type ReactNode } from "react";
|
||||
import type { DomainEvent, LayoutInfo } from "@/domain";
|
||||
import { LayoutGrid, LayoutTabs } from "@/features/layout";
|
||||
import { ConversationViewer } from "@/features/conversations";
|
||||
import { ProfilesSettings } from "@/features/first-run";
|
||||
import {
|
||||
SettingsView,
|
||||
SETTINGS_SECTIONS,
|
||||
SETTINGS_SECTION_LABEL,
|
||||
type SettingsSection,
|
||||
} from "@/features/settings";
|
||||
import { GitGraphView } from "@/features/git";
|
||||
import {
|
||||
Button,
|
||||
@ -114,10 +123,13 @@ export function ProjectsView() {
|
||||
// Width (px) of each dock column, driven by the DockRegion resize handle.
|
||||
const [leftDockWidth, setLeftDockWidth] = useState(340);
|
||||
const [rightDockWidth, setRightDockWidth] = useState(340);
|
||||
// Top-level view switch (#16): when true, the main area shows the AI Profiles
|
||||
// settings instead of the project surface. The single menu bar stays visible
|
||||
// so the user can toggle back from Settings → AI Profiles.
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
// Top-level view switch (#16, extended in #68): the open Settings section, or
|
||||
// null when the main area shows the project surface. Settings is a main
|
||||
// surface with its own internal nav — deliberately not a placeable panel. The
|
||||
// menu bar stays visible above it.
|
||||
const [settingsSection, setSettingsSection] = useState<SettingsSection | null>(
|
||||
null,
|
||||
);
|
||||
// The active layout (id + kind), reported by LayoutTabs — the single source of
|
||||
// truth. `kind` decides whether the main area is the terminal grid or the git
|
||||
// graph view.
|
||||
@ -390,17 +402,17 @@ export function ProjectsView() {
|
||||
{
|
||||
id: "settings",
|
||||
label: "Settings",
|
||||
items: [
|
||||
{
|
||||
id: "ai-profiles",
|
||||
label: showSettings ? "Close AI Profiles" : "AI Profiles",
|
||||
active: showSettings,
|
||||
onSelect: () => {
|
||||
setShowSettings((v) => !v);
|
||||
dismissFloating();
|
||||
},
|
||||
// One entry per section (#68). The entries name sections and mark the open
|
||||
// one; closing lives in the view ("Close Settings"), so no label alternates.
|
||||
items: SETTINGS_SECTIONS.map((section) => ({
|
||||
id: section,
|
||||
label: SETTINGS_SECTION_LABEL[section],
|
||||
active: settingsSection === section,
|
||||
onSelect: () => {
|
||||
setSettingsSection(section);
|
||||
dismissFloating();
|
||||
},
|
||||
],
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
@ -621,7 +633,7 @@ export function ProjectsView() {
|
||||
onClose={(id) => void vm.closeTab(id)}
|
||||
projectsPanelOpen={placementOf(placements, "projects") !== "closed"}
|
||||
onOpenProjectsPanel={() => {
|
||||
setShowSettings(false);
|
||||
setSettingsSection(null);
|
||||
setPlacement("projects", "floating");
|
||||
}}
|
||||
/>
|
||||
@ -645,14 +657,14 @@ export function ProjectsView() {
|
||||
|
||||
{/* ── Main: AI Profiles / terminal grid / git graph / welcome ── */}
|
||||
<main className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
{showSettings ? (
|
||||
// Top-level view switch (#16): AI Profiles settings takes over the main
|
||||
// area while the menu bar above stays visible to toggle back.
|
||||
<div className="flex flex-1 justify-center overflow-y-auto p-6">
|
||||
<div className="w-full max-w-2xl">
|
||||
<ProfilesSettings />
|
||||
</div>
|
||||
</div>
|
||||
{settingsSection ? (
|
||||
// Top-level view switch (#16/#68): Settings takes over the main area
|
||||
// while the menu bar above stays visible.
|
||||
<SettingsView
|
||||
section={settingsSection}
|
||||
onSectionChange={setSettingsSection}
|
||||
onClose={() => setSettingsSection(null)}
|
||||
/>
|
||||
) : active && viewerConversationId ? (
|
||||
<ConversationViewer
|
||||
key={`${active.id}-${viewerConversationId}`}
|
||||
@ -740,7 +752,7 @@ export function ProjectsView() {
|
||||
(tab) => tab.id === toast.projectId,
|
||||
);
|
||||
if (projectOpen) vm.activateTab(toast.projectId);
|
||||
setShowSettings(false);
|
||||
setSettingsSection(null);
|
||||
setViewerConversationId(null);
|
||||
setPlacement("work", "floating");
|
||||
setTaskToasts((prev) =>
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
fireEvent,
|
||||
} from "@testing-library/react";
|
||||
|
||||
import { MockAgentGateway, MockGitGateway, MockProfileGateway, MockProjectGateway, MockSystemGateway, MockTemplateGateway, MockWindowGateway, MockWorkStateGateway } from "@/adapters/mock";
|
||||
import { MockAgentGateway, MockDesktopServerGateway, MockGitGateway, MockProfileGateway, MockProjectGateway, MockSystemGateway, MockTemplateGateway, MockWindowGateway, MockWorkStateGateway } from "@/adapters/mock";
|
||||
import type { Gateways } from "@/ports";
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { ProjectsView } from "./ProjectsView";
|
||||
@ -25,6 +25,7 @@ function renderView(
|
||||
// The project gateway drives the primary assertions; agent + profile stubs are
|
||||
// required because ProjectsView now renders AgentsPanel for the active tab.
|
||||
// template gateway is required because ProjectsView now renders TemplatesPanel.
|
||||
// desktopServer is required by Settings → Deployment (#68).
|
||||
const agentGateway = new MockAgentGateway();
|
||||
const gateways = {
|
||||
system,
|
||||
@ -35,6 +36,7 @@ function renderView(
|
||||
git: new MockGitGateway(),
|
||||
workState: new MockWorkStateGateway(),
|
||||
window: new MockWindowGateway(),
|
||||
desktopServer: new MockDesktopServerGateway(),
|
||||
} as unknown as Gateways;
|
||||
return {
|
||||
project,
|
||||
@ -197,7 +199,7 @@ describe("ProjectsView (with MockProjectGateway)", () => {
|
||||
expect(betaTab.getAttribute("aria-selected")).toBe("true");
|
||||
});
|
||||
|
||||
it("toggles the AI Profiles view from the single Settings menu (#16)", async () => {
|
||||
it("opens the Settings surface from the Settings menu and closes it from the view (#16/#68)", async () => {
|
||||
renderView();
|
||||
await waitForIdle();
|
||||
|
||||
@ -210,14 +212,31 @@ describe("ProjectsView (with MockProjectGateway)", () => {
|
||||
expect(await screen.findByLabelText("ai profiles settings")).toBeTruthy();
|
||||
expect(screen.queryByLabelText("project name")).toBeNull();
|
||||
|
||||
// Settings → Close AI Profiles returns to the project surface.
|
||||
openMenuItem("Settings", "Close AI Profiles");
|
||||
// #68: closing is an explicit action in the view, not an alternating menu
|
||||
// label — the label never scaled past one section.
|
||||
fireEvent.click(screen.getByRole("button", { name: "Close Settings" }));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText("project name")).toBeTruthy(),
|
||||
);
|
||||
expect(screen.queryByLabelText("ai profiles settings")).toBeNull();
|
||||
});
|
||||
|
||||
it("navigates between Settings sections from the menu and the nav column (#68)", async () => {
|
||||
renderView();
|
||||
await waitForIdle();
|
||||
|
||||
// The Settings menu now names sections; Deployment opens directly.
|
||||
openMenuItem("Settings", "Deployment");
|
||||
expect(await screen.findByLabelText("deployment settings")).toBeTruthy();
|
||||
expect(screen.queryByLabelText("ai profiles settings")).toBeNull();
|
||||
|
||||
// The internal nav column switches sections without leaving Settings.
|
||||
const nav = screen.getByRole("navigation", { name: "settings sections" });
|
||||
fireEvent.click(within(nav).getByRole("button", { name: "AI Profiles" }));
|
||||
expect(await screen.findByLabelText("ai profiles settings")).toBeTruthy();
|
||||
expect(screen.queryByLabelText("deployment settings")).toBeNull();
|
||||
});
|
||||
|
||||
it("closing a tab removes it from the tab bar", async () => {
|
||||
renderView();
|
||||
await createProject("alpha", "/home/me/alpha");
|
||||
|
||||
Reference in New Issue
Block a user