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:
2026-07-16 23:43:37 +02:00
parent 49e1d5a157
commit 7efa634f23
16 changed files with 1458 additions and 31 deletions

View File

@ -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) =>