feat(chat): valide le routing cellKind=chat côté frontend et corrige UX
- Ajoute cellKind dans AgentChatHandle et vérifie que launchAgentChat reçoit cellKind:chat - Rejette avec erreur STRUCTURED_ROUTED_TO_PTY si le backend route vers PTY - Ajoute cellKind:chat dans MockAgentGateway.launchAgentChat - Corrige l'overflow du layout CustomAgentChatView (bounded shell avec scroll + composer fixe) - Dédouble les prompts utilisateur quand le stream les echo - Ajoute les tests launchAgentChat returns handle only when confirms chat et launchAgentChat rejects PTY-routed launch - Ajoute les tests does not duplicate user prompt when echoed et keeps chat shell bounded Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -93,6 +93,12 @@ function appendAgentDelta(turns: ChatTurn[], text: string): ChatTurn[] {
|
||||
return next;
|
||||
}
|
||||
|
||||
function appendUserPrompt(turns: ChatTurn[], text: string): ChatTurn[] {
|
||||
const last = turns[turns.length - 1];
|
||||
if (last?.role === "user" && last.text === text) return turns;
|
||||
return [...turns, { role: "user", text }];
|
||||
}
|
||||
|
||||
function foldChunk(turns: ChatTurn[], raw: unknown): ChatTurn[] {
|
||||
if (!isReplyRecord(raw)) {
|
||||
return [...turns, { role: "unknown", text: unknownChunkLabel(raw) }];
|
||||
@ -119,7 +125,7 @@ function foldChunk(turns: ChatTurn[], raw: unknown): ChatTurn[] {
|
||||
}
|
||||
case "userPrompt":
|
||||
case "UserPrompt":
|
||||
return [...turns, { role: "user", text: String(raw.text ?? raw.prompt ?? "") }];
|
||||
return appendUserPrompt(turns, String(raw.text ?? raw.prompt ?? ""));
|
||||
default:
|
||||
return [...turns, { role: "unknown", text: unknownChunkLabel(raw) }];
|
||||
}
|
||||
@ -481,20 +487,31 @@ export function CustomAgentChatView({
|
||||
return (
|
||||
<div
|
||||
data-testid="custom-agent-chat-view"
|
||||
className="flex h-full min-h-0 flex-col bg-surface text-content"
|
||||
className="flex h-full min-h-0 flex-col overflow-hidden bg-surface text-content"
|
||||
>
|
||||
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
role="toolbar"
|
||||
aria-label="custom agent chat actions"
|
||||
className="flex shrink-0 items-center gap-2 overflow-hidden border-b border-border px-3 py-2"
|
||||
>
|
||||
<div className="min-w-0 flex-1 overflow-hidden">
|
||||
<div className="truncate text-sm font-medium">{agentName}</div>
|
||||
<div className="truncate text-xs text-muted">
|
||||
CLI custom · {profile.name}
|
||||
</div>
|
||||
</div>
|
||||
{(opening || busy) && (
|
||||
<Button size="sm" variant="danger" onClick={() => void cancel()}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex shrink-0 items-center justify-end gap-2">
|
||||
{(opening || busy) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
className="shrink-0 whitespace-nowrap"
|
||||
onClick={() => void cancel()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!supported && (
|
||||
@ -508,7 +525,11 @@ export function CustomAgentChatView({
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div ref={scrollRef} className="flex min-h-0 flex-1 flex-col gap-2 overflow-auto px-3 py-3">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
data-testid="custom-agent-chat-scroll"
|
||||
className="flex min-h-0 flex-1 basis-0 flex-col gap-2 overflow-x-hidden overflow-y-auto px-3 py-3"
|
||||
>
|
||||
{opening && turns.length === 0 ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted">
|
||||
<Spinner size={14} />
|
||||
@ -523,7 +544,10 @@ export function CustomAgentChatView({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-col gap-2 border-t border-border bg-raised/40 p-2">
|
||||
<div
|
||||
data-testid="custom-agent-chat-composer"
|
||||
className="flex shrink-0 flex-col gap-2 border-t border-border bg-raised/40 p-2"
|
||||
>
|
||||
{attachment && (
|
||||
<div className="flex items-center justify-between gap-2 rounded-md border border-border bg-surface px-2 py-1 text-xs text-muted">
|
||||
<span className="truncate">Fichier joint: {attachment}</span>
|
||||
@ -532,7 +556,7 @@ export function CustomAgentChatView({
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex min-w-0 items-end gap-2">
|
||||
<textarea
|
||||
aria-label={`message CLI custom ${nodeId}`}
|
||||
className={cn(
|
||||
@ -551,10 +575,22 @@ export function CustomAgentChatView({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="ghost" disabled={!supported || opening || busy} onClick={() => void pickAttachment()}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="shrink-0 whitespace-nowrap"
|
||||
disabled={!supported || opening || busy}
|
||||
onClick={() => void pickAttachment()}
|
||||
>
|
||||
Joindre
|
||||
</Button>
|
||||
<Button size="sm" disabled={!canSend} loading={busy} onClick={() => void send()}>
|
||||
<Button
|
||||
size="sm"
|
||||
className="shrink-0 whitespace-nowrap"
|
||||
disabled={!canSend}
|
||||
loading={busy}
|
||||
onClick={() => void send()}
|
||||
>
|
||||
Envoyer
|
||||
</Button>
|
||||
</div>
|
||||
@ -566,8 +602,8 @@ export function CustomAgentChatView({
|
||||
function ChatBubble({ turn }: { turn: ChatTurn }) {
|
||||
if (turn.role === "tool") {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<span className="max-w-[80%] rounded-md border border-border bg-raised px-2 py-1 text-xs text-muted">
|
||||
<div className="flex min-w-0 justify-center">
|
||||
<span className="max-w-[80%] truncate rounded-md border border-border bg-raised px-2 py-1 text-xs text-muted">
|
||||
{turn.label}
|
||||
</span>
|
||||
</div>
|
||||
@ -575,8 +611,8 @@ function ChatBubble({ turn }: { turn: ChatTurn }) {
|
||||
}
|
||||
if (turn.role === "final") {
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<div className="max-w-[86%] rounded-md border border-success/50 bg-success/10 px-3 py-2 text-sm text-content">
|
||||
<div className="flex min-w-0 justify-end">
|
||||
<div className="min-w-0 max-w-[86%] overflow-hidden rounded-md border border-success/50 bg-success/10 px-3 py-2 text-sm text-content">
|
||||
<div className="mb-1 text-xs font-semibold text-success">Task Complete</div>
|
||||
<p className="whitespace-pre-wrap break-words">{turn.text}</p>
|
||||
</div>
|
||||
@ -585,8 +621,8 @@ function ChatBubble({ turn }: { turn: ChatTurn }) {
|
||||
}
|
||||
if (turn.role === "error" || turn.role === "unknown") {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<div className="max-w-[86%] rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-xs text-danger">
|
||||
<div className="flex min-w-0 justify-center">
|
||||
<div className="min-w-0 max-w-[86%] overflow-hidden rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-xs text-danger">
|
||||
{turn.role === "unknown" ? "Chunk inconnu reçu: " : ""}
|
||||
<span className="whitespace-pre-wrap break-words">{turn.text}</span>
|
||||
</div>
|
||||
@ -596,10 +632,10 @@ function ChatBubble({ turn }: { turn: ChatTurn }) {
|
||||
|
||||
const user = turn.role === "user";
|
||||
return (
|
||||
<div className={cn("flex", user ? "justify-start" : "justify-end")}>
|
||||
<div className={cn("flex min-w-0", user ? "justify-start" : "justify-end")}>
|
||||
<div
|
||||
className={cn(
|
||||
"max-w-[82%] rounded-lg border px-3 py-2 text-sm text-content",
|
||||
"min-w-0 max-w-[82%] overflow-hidden rounded-lg border px-3 py-2 text-sm text-content",
|
||||
user ? "border-border bg-raised" : "border-primary/25 bg-primary/10",
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user