feat(domain,application,infrastructure,backend,web-server,app-tauri,frontend): lot utilisateur #325 #326 — icônes projet avec watermark CLI custom et localisation FR/EN (QA verte)
Lot en un seul commit: les deux features partagent les mêmes contrats (domain/settings.rs, DTO, commandes Tauri/HTTP, web-server) et les mêmes surfaces UI — la séparation par fichiers était impossible sans chirurgie de hunks risquant des états intermédiaires non compilables (convention lot des batches #310-#313 et #315). #325 — icône configurable par projet, watermark CLI custom: - domaine: ProjectIconSet (devicon, simple-icons, lucide, idea), ProjectIconRef validé (nom ASCII 1..=64, -_. internes), champ icon optionnel sur ProjectSettings; sérialisation camelCase et try_from strict - application: UpdateProjectSettings.execute accepte un patch theme_color OU icon (absent = inchangé, null = défaut produit), tests in-dependants avec store mémoire - backend/app-tauri/web-server: ProjectIconRefDto, UpdateProjectSettingsRequestDto avec icon optionnel (désérialisation additive), commandes update_project_settings alignées Tauri/HTTP, tests DTO et HTTP (défaut null, set, clear, nom invalide rejeté) - frontend: catalogue projectIcons (recherche EN), ProjectIconPicker en grille multi-lignes dans Paramètres projet, affichage de l'icône dans ProjectTabs et watermark grand format à faible opacité en fond de CLI custom; ports ProjectSettings.icon, Partial<ProjectSettings>, adapters tauri/mock/http et tests alignés #326 — localisation FR/EN et traduction des textes IdeA: - domaine: IdeaLanguage (fr | en), champ language optionnel sur IdeaUserSettings (absence = défaut produit, série additive validée) - backend/web-server: UserSettingsDto.language, update_user_settings accepte language, tests HTTP alignés; fixtures structured_launch_d3 mises à jour - frontend: i18n/ (catalogue FR/EN, I18nProvider, scan anti-régression des sources visibles), GeneralSettings avec sélecteur de langue dans les paramètres, setupI18n dans vitest; migration de l'ensemble des textes hardcodés vers le catalogue sur toutes les surfaces (agents, layout, projets, tickets, sprints, web, plugins, permissions, git, templates, skills, mémoire, model servers, embedder, first-run, devices, scheduled tasks, workstate, dialogs), FR conservé par défaut; recherche d'icônes #325 en anglais conformément au ticket QA verte sur feature/batch-325-326-project-icons-localisation. Aucun push.
This commit is contained in:
@ -28,6 +28,9 @@ import { useRef, useState } from "react";
|
||||
import type { ServerExposureMode } from "@/domain";
|
||||
import { Button, Field, Input, Panel, cn } from "@/shared";
|
||||
import { useDeployment } from "./useDeployment";
|
||||
import { useI18n } from "@/i18n";
|
||||
|
||||
type Tr = (french: string, english: string) => string;
|
||||
|
||||
interface ModeOption {
|
||||
mode: ServerExposureMode;
|
||||
@ -36,37 +39,35 @@ interface ModeOption {
|
||||
}
|
||||
|
||||
/** The three exposure choices, in increasing order of reach. */
|
||||
const MODE_OPTIONS: ModeOption[] = [
|
||||
const modeOptions = (tr: Tr): ModeOption[] => [
|
||||
{
|
||||
mode: "localOnly",
|
||||
title: "Cet ordinateur uniquement",
|
||||
title: tr("Cet ordinateur uniquement", "This computer only"),
|
||||
description:
|
||||
"Pour utiliser IdeA uniquement sur cet ordinateur. Les appareils distants ne peuvent pas se connecter.",
|
||||
tr("Pour utiliser IdeA uniquement sur cet ordinateur. Les appareils distants ne peuvent pas se connecter.", "Use IdeA only on this computer. Remote devices cannot connect."),
|
||||
},
|
||||
{
|
||||
mode: "remoteProxyLocal",
|
||||
title: "Accès distant, proxy sur cet ordinateur",
|
||||
title: tr("Accès distant, proxy sur cet ordinateur", "Remote access, proxy on this computer"),
|
||||
description:
|
||||
"À utiliser quand le proxy HTTPS tourne sur la même machine qu'IdeA Desktop.",
|
||||
tr("À utiliser quand le proxy HTTPS tourne sur la même machine qu'IdeA Desktop.", "Use when the HTTPS proxy runs on the same machine as IdeA Desktop."),
|
||||
},
|
||||
{
|
||||
mode: "remoteProxyOtherMachine",
|
||||
title: "Accès distant, proxy sur une autre machine",
|
||||
title: tr("Accès distant, proxy sur une autre machine", "Remote access, proxy on another machine"),
|
||||
description:
|
||||
"À utiliser quand le proxy HTTPS tourne sur une autre machine. IdeA n'acceptera que le trafic provenant de ce proxy.",
|
||||
tr("À utiliser quand le proxy HTTPS tourne sur une autre machine. IdeA n'acceptera que le trafic provenant de ce proxy.", "Use when the HTTPS proxy runs on another machine. IdeA will only accept traffic from that proxy."),
|
||||
},
|
||||
];
|
||||
|
||||
const STATE_LABEL: Record<string, string> = {
|
||||
stopped: "Arrêté",
|
||||
starting: "Démarrage…",
|
||||
running: "En cours d'exécution",
|
||||
stopping: "Arrêt…",
|
||||
failed: "Échec",
|
||||
};
|
||||
const stateLabel = (state: string, tr: Tr): string => ({
|
||||
stopped: tr("Arrêté", "Stopped"), starting: tr("Démarrage…", "Starting…"),
|
||||
running: tr("En cours d'exécution", "Running"), stopping: tr("Arrêt…", "Stopping…"), failed: tr("Échec", "Failed"),
|
||||
}[state] ?? state);
|
||||
|
||||
/** Copy-to-clipboard button; degrades to disabled where the API is absent. */
|
||||
function CopyButton({ value, label }: { value: string; label: string }) {
|
||||
const { tr } = useI18n();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const supported =
|
||||
typeof navigator !== "undefined" && Boolean(navigator.clipboard);
|
||||
@ -86,7 +87,7 @@ function CopyButton({ value, label }: { value: string; label: string }) {
|
||||
);
|
||||
}}
|
||||
>
|
||||
{copied ? "Copié" : "Copier"}
|
||||
{copied ? tr("Copié", "Copied") : tr("Copier", "Copy")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@ -105,14 +106,15 @@ function ReadOnlyValue({ value, copyLabel }: { value: string; copyLabel: string
|
||||
|
||||
export function DeploymentSettings() {
|
||||
const vm = useDeployment();
|
||||
const { tr } = useI18n();
|
||||
// "Modifier le port" (#89) jumps into Accès réseau — imperative focus is the
|
||||
// simplest correct answer for a cross-panel affordance on one scrolled page.
|
||||
const portInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
if (!vm.ready || !vm.settings) {
|
||||
return (
|
||||
<Panel aria-label="deployment settings" title="Déploiement">
|
||||
<p className="text-sm text-muted">Chargement…</p>
|
||||
<Panel aria-label={tr("paramètres de déploiement", "deployment settings")} title={tr("Déploiement", "Deployment")}>
|
||||
<p className="text-sm text-muted">{tr("Chargement…", "Loading…")}</p>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@ -124,10 +126,10 @@ export function DeploymentSettings() {
|
||||
const transitioning = status.state === "starting" || status.state === "stopping";
|
||||
|
||||
return (
|
||||
<div aria-label="deployment settings" className="flex flex-col gap-4">
|
||||
<div aria-label={tr("paramètres de déploiement", "deployment settings")} className="flex flex-col gap-4">
|
||||
{/* ── Status ────────────────────────────────────────────────────────── */}
|
||||
<Panel
|
||||
title="Serveur"
|
||||
title={tr("Serveur", "Server")}
|
||||
actions={
|
||||
running || status.state === "stopping" ? (
|
||||
<Button
|
||||
@ -135,7 +137,7 @@ export function DeploymentSettings() {
|
||||
onClick={() => void vm.stop()}
|
||||
disabled={vm.busy || transitioning}
|
||||
>
|
||||
Arrêter
|
||||
{tr("Arrêter", "Stop")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@ -143,7 +145,7 @@ export function DeploymentSettings() {
|
||||
onClick={() => void vm.start()}
|
||||
disabled={vm.busy || transitioning}
|
||||
>
|
||||
Démarrer
|
||||
{tr("Démarrer", "Start")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@ -159,17 +161,17 @@ export function DeploymentSettings() {
|
||||
(status.state === "stopped" || transitioning) && "bg-faint",
|
||||
)}
|
||||
/>
|
||||
<span className="text-content">{STATE_LABEL[status.state]}</span>
|
||||
<span className="text-content">{stateLabel(status.state, tr)}</span>
|
||||
</p>
|
||||
|
||||
{status.localUrl && (
|
||||
<p className="text-xs text-muted">
|
||||
URL locale : <code className="font-mono text-content">{status.localUrl}</code>
|
||||
{tr("URL locale", "Local URL")} : <code className="font-mono text-content">{status.localUrl}</code>
|
||||
</p>
|
||||
)}
|
||||
{status.publicUrl && (
|
||||
<p className="text-xs text-muted">
|
||||
URL publique : <code className="font-mono text-content">{status.publicUrl}</code>
|
||||
{tr("URL publique", "Public URL")} : <code className="font-mono text-content">{status.publicUrl}</code>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -181,7 +183,7 @@ export function DeploymentSettings() {
|
||||
status.error.code === "PORT_IN_USE" ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p role="alert" className="text-sm text-danger">
|
||||
Le serveur n'a pas démarré automatiquement. {status.error.message}
|
||||
{tr("Le serveur n'a pas démarré automatiquement.", "The server did not start automatically.")} {status.error.message}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@ -195,14 +197,14 @@ export function DeploymentSettings() {
|
||||
portInputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
Modifier le port
|
||||
{tr("Modifier le port", "Change port")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void vm.start()}
|
||||
disabled={vm.busy}
|
||||
>
|
||||
Réessayer
|
||||
{tr("Réessayer", "Retry")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -224,18 +226,17 @@ export function DeploymentSettings() {
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-0.5"
|
||||
aria-label="Démarrer le serveur au lancement d'IdeA"
|
||||
aria-label={tr("Démarrer le serveur au lancement d'IdeA", "Start the server when IdeA launches")}
|
||||
checked={settings.autoStart}
|
||||
disabled={vm.autoStartBusy}
|
||||
onChange={(e) => void vm.setAutoStart(e.target.checked)}
|
||||
/>
|
||||
<span className="flex flex-col gap-0.5">
|
||||
<span className="text-sm text-content">
|
||||
Démarrer le serveur au lancement d'IdeA
|
||||
{tr("Démarrer le serveur au lancement d'IdeA", "Start the server when IdeA launches")}
|
||||
</span>
|
||||
<span className="text-xs text-muted">
|
||||
IdeA utilisera les réglages réseau enregistrés ci-dessous au
|
||||
prochain démarrage de l'application desktop.
|
||||
{tr("IdeA utilisera les réglages réseau enregistrés ci-dessous au prochain démarrage de l'application desktop.", "IdeA will use the network settings saved below the next time the desktop application starts.")}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
@ -243,14 +244,14 @@ export function DeploymentSettings() {
|
||||
</Panel>
|
||||
|
||||
{/* ── Exposure (ticket #78 — "Accès réseau" per UX) ────────────────── */}
|
||||
<Panel title="Accès réseau">
|
||||
<Panel title={tr("Accès réseau", "Network access")}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="mode d'accès réseau"
|
||||
aria-label={tr("mode d'accès réseau", "network access mode")}
|
||||
className="flex flex-col gap-2"
|
||||
>
|
||||
{MODE_OPTIONS.map((option) => {
|
||||
{modeOptions(tr).map((option) => {
|
||||
const selected = settings.mode === option.mode;
|
||||
return (
|
||||
<label
|
||||
@ -280,7 +281,7 @@ export function DeploymentSettings() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Field label="Port">
|
||||
<Field label={tr("Port", "Port")}>
|
||||
{({ id }) => (
|
||||
<Input
|
||||
ref={portInputRef}
|
||||
@ -293,12 +294,12 @@ export function DeploymentSettings() {
|
||||
</Field>
|
||||
|
||||
{remote && (
|
||||
<Field label="Origine publique" hint="Exemple : https://idea.example.com">
|
||||
<Field label={tr("Origine publique", "Public origin")} hint={tr("Exemple : https://idea.example.com", "Example: https://idea.example.com")}>
|
||||
{({ id, describedBy }) => (
|
||||
<Input
|
||||
id={id}
|
||||
aria-describedby={describedBy}
|
||||
placeholder="https://idea.example.com"
|
||||
placeholder={tr("https://idea.example.com", "https://idea.example.com")}
|
||||
value={settings.publicOrigin ?? ""}
|
||||
onChange={(e) => vm.setPublicOrigin(e.target.value)}
|
||||
/>
|
||||
@ -310,8 +311,8 @@ export function DeploymentSettings() {
|
||||
<>
|
||||
{/* Addresses come from the backend probe — never invented here. */}
|
||||
<Field
|
||||
label="Adresse LAN d'écoute"
|
||||
hint="L'adresse de cette machine à laquelle le proxy se connectera."
|
||||
label={tr("Adresse LAN d'écoute", "LAN listen address")}
|
||||
hint={tr("L'adresse de cette machine à laquelle le proxy se connectera.", "The address on this machine that the proxy will connect to.")}
|
||||
>
|
||||
{({ id, describedBy }) =>
|
||||
vm.candidateLanAddresses.length > 0 ? (
|
||||
@ -322,7 +323,7 @@ export function DeploymentSettings() {
|
||||
value={settings.lanBindAddress ?? ""}
|
||||
onChange={(e) => vm.setLanBindAddress(e.target.value)}
|
||||
>
|
||||
<option value="">Sélectionner une adresse…</option>
|
||||
<option value="">{tr("Sélectionner une adresse…", "Select an address…")}</option>
|
||||
{vm.candidateLanAddresses.map((address) => (
|
||||
<option key={address} value={address}>
|
||||
{address}
|
||||
@ -333,7 +334,7 @@ export function DeploymentSettings() {
|
||||
<Input
|
||||
id={id}
|
||||
aria-describedby={describedBy}
|
||||
placeholder="192.168.1.42"
|
||||
placeholder={tr("192.168.1.42", "192.168.1.42")}
|
||||
value={settings.lanBindAddress ?? ""}
|
||||
onChange={(e) => vm.setLanBindAddress(e.target.value)}
|
||||
/>
|
||||
@ -342,14 +343,14 @@ export function DeploymentSettings() {
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="IP/CIDR du proxy autorisé"
|
||||
hint="Ce n'est pas l'adresse d'écoute d'IdeA. C'est la machine autorisée à contacter IdeA."
|
||||
label={tr("IP/CIDR du proxy autorisé", "Authorized proxy IP/CIDR")}
|
||||
hint={tr("Ce n'est pas l'adresse d'écoute d'IdeA. C'est la machine autorisée à contacter IdeA.", "This is not IdeA's listen address. It is the machine authorized to contact IdeA.")}
|
||||
>
|
||||
{({ id, describedBy }) => (
|
||||
<Input
|
||||
id={id}
|
||||
aria-describedby={describedBy}
|
||||
placeholder="203.0.113.7 or 203.0.113.0/24"
|
||||
placeholder={tr("203.0.113.7 ou 203.0.113.0/24", "203.0.113.7 or 203.0.113.0/24")}
|
||||
value={settings.trustedProxies.join(", ")}
|
||||
onChange={(e) => vm.setTrustedProxies(e.target.value)}
|
||||
/>
|
||||
@ -357,8 +358,7 @@ export function DeploymentSettings() {
|
||||
</Field>
|
||||
|
||||
<p className="rounded-md border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning">
|
||||
Si votre proxy n'est pas sur cet ordinateur, choisissez ce mode.
|
||||
Sinon, le proxy peut expirer sans erreur IdeA.
|
||||
{tr("Si votre proxy n'est pas sur cet ordinateur, choisissez ce mode. Sinon, le proxy peut expirer sans erreur IdeA.", "If your proxy is not on this computer, choose this mode. Otherwise the proxy may time out without an IdeA error.")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
@ -380,16 +380,16 @@ export function DeploymentSettings() {
|
||||
|
||||
{/* ── Proxy setup: the upstream to paste, backend-provided ──────────── */}
|
||||
{remote && (
|
||||
<Panel title="Configuration du proxy">
|
||||
<Panel title={tr("Configuration du proxy", "Proxy configuration")}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-xs text-muted">
|
||||
Faites pointer votre proxy inverse HTTPS vers cet upstream.
|
||||
{tr("Faites pointer votre proxy inverse HTTPS vers cet upstream.", "Point your HTTPS reverse proxy to this upstream.")}
|
||||
</p>
|
||||
{vm.upstreamUrl ? (
|
||||
<ReadOnlyValue value={vm.upstreamUrl} copyLabel="copier l'URL upstream" />
|
||||
<ReadOnlyValue value={vm.upstreamUrl} copyLabel={tr("copier l'URL upstream", "copy upstream URL")} />
|
||||
) : (
|
||||
<p className="text-xs text-faint">
|
||||
Complétez les paramètres ci-dessus pour obtenir l'URL upstream.
|
||||
{tr("Complétez les paramètres ci-dessus pour obtenir l'URL upstream.", "Complete the settings above to obtain the upstream URL.")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -397,11 +397,10 @@ export function DeploymentSettings() {
|
||||
)}
|
||||
|
||||
{/* ── Pairing moved to its own surface (#77) — leave a signpost ─────── */}
|
||||
<Panel title="Appairage">
|
||||
<Panel title={tr("Appairage", "Pairing")}>
|
||||
<p className="text-sm text-muted">
|
||||
L'appairage est géré dans{" "}
|
||||
<span className="text-content">Paramètres → Appareils</span>, où
|
||||
vous pouvez générer un code et révoquer des appareils.
|
||||
{tr("L'appairage est géré dans", "Pairing is managed in")}{" "}
|
||||
<span className="text-content">{tr("Paramètres → Appareils", "Settings → Devices")}</span>, {tr("où vous pouvez générer un code et révoquer des appareils.", "where you can generate a code and revoke devices.")}
|
||||
</p>
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
37
frontend/src/features/settings/GeneralSettings.tsx
Normal file
37
frontend/src/features/settings/GeneralSettings.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { useState } from "react";
|
||||
import { Panel, Spinner } from "@/shared";
|
||||
import { useI18n } from "@/i18n";
|
||||
import type { IdeaLanguage } from "@/ports";
|
||||
|
||||
export function GeneralSettings() {
|
||||
const { language, setLanguage, t } = useI18n();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function select(next: IdeaLanguage) {
|
||||
if (next === language || busy) return;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await setLanguage(next);
|
||||
} catch {
|
||||
setError(t("settings.language.saveError"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel title={t("settings.language.title")} className="flex flex-col gap-4 p-4">
|
||||
<fieldset disabled={busy} className="flex flex-col gap-3">
|
||||
<legend className="text-sm font-semibold text-content">{t("settings.language.legend")}</legend>
|
||||
<div className="flex gap-5">
|
||||
{(["fr", "en"] as const).map((value) => <label key={value} className="flex items-center gap-2 text-sm text-content"><input type="radio" name="idea-language" value={value} checked={language === value} onChange={() => void select(value)} />{t(`settings.language.${value}`)}</label>)}
|
||||
</div>
|
||||
<p className="text-xs text-muted">{t("settings.language.help")}</p>
|
||||
</fieldset>
|
||||
{busy && <Spinner size={14} />}
|
||||
{error && <p role="alert" className="text-sm text-danger">{error}</p>}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@ -27,12 +27,15 @@ import { DevicesScreen } from "@/features/devices";
|
||||
import { PluginsPanel } from "@/features/plugins";
|
||||
import { DeploymentSettings } from "./DeploymentSettings";
|
||||
import { TerminalSettings } from "./TerminalSettings";
|
||||
import { GeneralSettings } from "./GeneralSettings";
|
||||
import { useI18n } from "@/i18n";
|
||||
|
||||
/** The Settings sections, in menu/nav order. */
|
||||
export type SettingsSection = "aiProfiles" | "terminal" | "deployment" | "devices" | "plugins";
|
||||
export type SettingsSection = "general" | "aiProfiles" | "terminal" | "deployment" | "devices" | "plugins";
|
||||
|
||||
/** Human labels, shared by the nav column and the `Settings` menu (ticket #78 — French). */
|
||||
export const SETTINGS_SECTION_LABEL: Record<SettingsSection, string> = {
|
||||
general: "Général",
|
||||
aiProfiles: "Profils IA",
|
||||
terminal: "Terminal",
|
||||
deployment: "Déploiement",
|
||||
@ -42,6 +45,7 @@ export const SETTINGS_SECTION_LABEL: Record<SettingsSection, string> = {
|
||||
|
||||
/** Section order — the single source of truth for both nav and menu. */
|
||||
export const SETTINGS_SECTIONS: SettingsSection[] = [
|
||||
"general",
|
||||
"aiProfiles",
|
||||
"terminal",
|
||||
"deployment",
|
||||
@ -64,10 +68,15 @@ export function SettingsView({
|
||||
onAssignPluginCapabilities,
|
||||
onClose,
|
||||
}: SettingsViewProps) {
|
||||
const { t } = useI18n();
|
||||
const sectionLabels: Record<SettingsSection, string> = {
|
||||
general: t("settings.general"), aiProfiles: t("settings.aiProfiles"), terminal: t("settings.terminal"),
|
||||
deployment: t("settings.deployment"), devices: t("settings.devices"), plugins: t("settings.plugins"),
|
||||
};
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
<nav
|
||||
aria-label="settings sections"
|
||||
aria-label={t("settings.sections")}
|
||||
className="flex w-52 shrink-0 flex-col gap-1 border-r border-border p-3"
|
||||
>
|
||||
{SETTINGS_SECTIONS.map((id) => {
|
||||
@ -85,21 +94,23 @@ export function SettingsView({
|
||||
: "text-muted hover:bg-raised hover:text-content",
|
||||
)}
|
||||
>
|
||||
{SETTINGS_SECTION_LABEL[id]}
|
||||
{sectionLabels[id]}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="mt-auto pt-2">
|
||||
<Button size="sm" variant="ghost" className="w-full" onClick={onClose}>
|
||||
Fermer les paramètres
|
||||
{t("settings.close")}
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="flex flex-1 justify-center overflow-y-auto p-6">
|
||||
<div className="w-full max-w-2xl">
|
||||
{section === "aiProfiles" ? (
|
||||
{section === "general" ? (
|
||||
<GeneralSettings />
|
||||
) : section === "aiProfiles" ? (
|
||||
<ProfilesSettings />
|
||||
) : section === "terminal" ? (
|
||||
<TerminalSettings />
|
||||
|
||||
@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
|
||||
import type { DefaultAgentTerminalInterface } from "@/ports";
|
||||
import { Panel, Spinner } from "@/shared";
|
||||
import { useGateways } from "@/app/di";
|
||||
import { useI18n } from "@/i18n";
|
||||
|
||||
const choices: Array<{ value: DefaultAgentTerminalInterface; label: string }> = [
|
||||
{ value: "nativeTui", label: "TUI native" },
|
||||
@ -11,6 +12,7 @@ const choices: Array<{ value: DefaultAgentTerminalInterface; label: string }> =
|
||||
|
||||
export function TerminalSettings() {
|
||||
const { userSettings } = useGateways();
|
||||
const { t } = useI18n();
|
||||
const [value, setValue] = useState<DefaultAgentTerminalInterface>("nativeTui");
|
||||
const [busy, setBusy] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@ -27,7 +29,7 @@ export function TerminalSettings() {
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setError("Impossible de charger la préférence de terminal.");
|
||||
if (!cancelled) setError(t("settings.terminal.loadError"));
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setBusy(false);
|
||||
@ -35,7 +37,7 @@ export function TerminalSettings() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [userSettings]);
|
||||
}, [userSettings, t]);
|
||||
|
||||
async function select(next: DefaultAgentTerminalInterface) {
|
||||
const previous = value;
|
||||
@ -49,7 +51,7 @@ export function TerminalSettings() {
|
||||
setValue(saved.defaultAgentTerminalInterface ?? "nativeTui");
|
||||
} catch {
|
||||
setValue(previous);
|
||||
setError("Impossible d’enregistrer la préférence de terminal.");
|
||||
setError(t("settings.terminal.saveError"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@ -59,7 +61,7 @@ export function TerminalSettings() {
|
||||
<Panel title="Terminal" className="flex flex-col gap-4 p-4">
|
||||
<fieldset disabled={busy} className="flex flex-col gap-3">
|
||||
<legend className="text-sm font-semibold text-content">
|
||||
Interface de terminal par défaut
|
||||
{t("settings.terminal.default")}
|
||||
</legend>
|
||||
<div className="flex flex-col gap-2">
|
||||
{choices.map((choice) => (
|
||||
@ -76,7 +78,7 @@ export function TerminalSettings() {
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted">
|
||||
Utilisée à l'ouverture d'un nouveau terminal. Les terminaux déjà ouverts ne sont pas modifiés.
|
||||
{t("settings.terminal.help")}
|
||||
</p>
|
||||
</fieldset>
|
||||
{busy && <Spinner size={14} />}
|
||||
|
||||
Reference in New Issue
Block a user