feat(ui): profils locaux/LAN OpenAI-compatible dans le first-run et les terminaux (#14)

Câble la surface frontend des profils IA locaux/LAN OpenAI-compatible,
en parité avec l'adapter backend additif (aab4bca).

- domain: types de profil OpenAI-compatible
- first-run: édition/validation du profil dans le FirstRunWizard
- adapters/mock: mock de profil pour les tests
- terminals: rendu des round-trips et erreurs endpoint (role=alert)

Validé QA (frontend GO): typecheck exit 0, vitest 59 fichiers / 566 tests,
0 echec ; couverture timeouts round-trip + erreur endpoint role=alert
prouvees ; pas de regression sur les tests de bail headless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 22:34:50 +02:00
parent aab4bcafb6
commit d89380cdf0
9 changed files with 656 additions and 10 deletions

View File

@ -32,7 +32,7 @@
* fresh one. If the session is gone (was explicitly closed), it opens fresh.
*/
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
@ -108,6 +108,12 @@ export function TerminalView({
}: TerminalViewProps) {
const { terminal } = useGateways();
const containerRef = useRef<HTMLDivElement | null>(null);
// A user-visible launch failure (e.g. an OpenAI-compatible endpoint that is
// unreachable). Rendered as a DOM `role="alert"` banner over the cell so the
// failure is always visible and accessible — not only painted into the xterm
// buffer (which is invisible to assistive tech and absent when xterm can't
// mount). `null` ⇒ no error. The cell stays mounted and IdeA stays usable.
const [openError, setOpenError] = useState<string | null>(null);
// The opener (`open` or the terminal gateway) is read through a ref so the
// effect does NOT depend on its identity. Otherwise every parent re-render
@ -138,6 +144,9 @@ export function TerminalView({
const reattacher = reattachRef.current ?? tgw?.reattach.bind(tgw);
if (!container || !opener) return;
// Fresh (re)mount: clear any prior failure banner before we try to open.
setOpenError(null);
const term = new Terminal({
convertEol: false,
cursorBlink: true,
@ -218,6 +227,10 @@ export function TerminalView({
);
return;
}
// A genuine launch failure (unreachable endpoint, model missing, network
// drop…). Surface it as an accessible DOM banner AND as a red line in the
// buffer. The cell renders as failed; IdeA stays usable (other cells work).
setOpenError(`Échec du lancement de l'agent : ${describe(e)}`);
term.write(
`\r\n\x1b[31mfailed to open terminal: ${describe(e)}\x1b[0m\r\n`,
);
@ -307,10 +320,41 @@ export function TerminalView({
return (
<div
ref={containerRef}
data-testid="terminal-view"
style={{ width: "100%", height: "100%", minHeight: "16rem" }}
/>
style={{
position: "relative",
width: "100%",
height: "100%",
minHeight: "16rem",
}}
>
{/* xterm mounts into this inner node; the error banner is a sibling so
React never fights xterm over the same subtree. */}
<div ref={containerRef} style={{ width: "100%", height: "100%" }} />
{openError && (
<div
role="alert"
data-testid="terminal-error"
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
padding: "0.5rem 0.75rem",
background: "rgba(120, 20, 20, 0.92)",
color: "#fff",
fontSize: 13,
fontFamily:
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
whiteSpace: "pre-wrap",
wordBreak: "break-word",
zIndex: 3,
}}
>
{openError}
</div>
)}
</div>
);
}