Files
IdeA/frontend/src/features/settings/SettingsView.tsx
Blomios 7efa634f23 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>
2026-07-16 23:43:37 +02:00

86 lines
2.9 KiB
TypeScript

/**
* `SettingsView` — the Settings surface shell (ticket #68).
*
* Settings is a **main surface**, not a dockable project view: it deliberately
* has no `PanelId` and stays outside the `viewPlacement` model. It takes over
* the main area while the menu bar stays visible above it.
*
* Ticket #68 gives it a second section, so the section list becomes real
* navigation (a left column) instead of the old single toggle. That also kills
* the alternating "Close AI Profiles" menu label, which never scaled past one
* entry: the menu now names sections, the active one is marked, and closing is
* an explicit action inside the view.
*
* `EmbedderSettings` / `ModelServersPanel` are **not** pulled in here — that is
* a separate lateral rework. The section list is the seam they would slot into.
*/
import { Button, cn } from "@/shared";
import { ProfilesSettings } from "@/features/first-run";
import { DeploymentSettings } from "./DeploymentSettings";
/** The Settings sections, in menu/nav order. */
export type SettingsSection = "aiProfiles" | "deployment";
/** Human labels, shared by the nav column and the `Settings` menu. */
export const SETTINGS_SECTION_LABEL: Record<SettingsSection, string> = {
aiProfiles: "AI Profiles",
deployment: "Deployment",
};
/** Section order — the single source of truth for both nav and menu. */
export const SETTINGS_SECTIONS: SettingsSection[] = ["aiProfiles", "deployment"];
interface SettingsViewProps {
section: SettingsSection;
onSectionChange: (section: SettingsSection) => void;
onClose: () => void;
}
export function SettingsView({
section,
onSectionChange,
onClose,
}: SettingsViewProps) {
return (
<div className="flex min-h-0 flex-1 overflow-hidden">
<nav
aria-label="settings sections"
className="flex w-52 shrink-0 flex-col gap-1 border-r border-border p-3"
>
{SETTINGS_SECTIONS.map((id) => {
const active = id === section;
return (
<button
key={id}
type="button"
aria-current={active ? "page" : undefined}
onClick={() => onSectionChange(id)}
className={cn(
"rounded-md px-3 py-2 text-left text-sm transition-colors",
active
? "bg-raised font-semibold text-content"
: "text-muted hover:bg-raised hover:text-content",
)}
>
{SETTINGS_SECTION_LABEL[id]}
</button>
);
})}
<div className="mt-auto pt-2">
<Button size="sm" variant="ghost" className="w-full" onClick={onClose}>
Close Settings
</Button>
</div>
</nav>
<div className="flex flex-1 justify-center overflow-y-auto p-6">
<div className="w-full max-w-2xl">
{section === "aiProfiles" ? <ProfilesSettings /> : <DeploymentSettings />}
</div>
</div>
</div>
);
}