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:
@ -170,4 +170,77 @@ describe("TauriAgentGateway invoke payloads", () => {
|
|||||||
});
|
});
|
||||||
expect(invoke).not.toHaveBeenCalledWith("close_agent_session", expect.anything());
|
expect(invoke).not.toHaveBeenCalledWith("close_agent_session", expect.anything());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("launchAgentChat returns a handle only when launch_agent confirms cellKind chat", async () => {
|
||||||
|
invoke.mockResolvedValueOnce({
|
||||||
|
sessionId: "chat-session-1",
|
||||||
|
cwd: "/repo",
|
||||||
|
rows: 24,
|
||||||
|
cols: 80,
|
||||||
|
cellKind: "chat",
|
||||||
|
assignedConversationId: "conversation-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
const out = await new TauriAgentGateway().launchAgentChat("proj-1", "agent-2", {
|
||||||
|
cwd: "/repo",
|
||||||
|
rows: 24,
|
||||||
|
cols: 80,
|
||||||
|
conversationId: "conversation-0",
|
||||||
|
nodeId: "node-3",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(invoke).toHaveBeenCalledWith("launch_agent", {
|
||||||
|
request: {
|
||||||
|
projectId: "proj-1",
|
||||||
|
agentId: "agent-2",
|
||||||
|
rows: 24,
|
||||||
|
cols: 80,
|
||||||
|
cellKind: "chat",
|
||||||
|
conversationId: "conversation-0",
|
||||||
|
nodeId: "node-3",
|
||||||
|
},
|
||||||
|
onOutput: expect.anything(),
|
||||||
|
});
|
||||||
|
expect(out).toEqual({
|
||||||
|
sessionId: "chat-session-1",
|
||||||
|
cellKind: "chat",
|
||||||
|
assignedConversationId: "conversation-1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("launchAgentChat rejects a PTY-routed launch before returning a fake structured session", async () => {
|
||||||
|
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
invoke.mockResolvedValueOnce({
|
||||||
|
sessionId: "pty-session-1",
|
||||||
|
cwd: "/repo",
|
||||||
|
rows: 24,
|
||||||
|
cols: 80,
|
||||||
|
cellKind: "pty",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
new TauriAgentGateway().launchAgentChat("proj-1", "agent-2", {
|
||||||
|
cwd: "/repo",
|
||||||
|
rows: 24,
|
||||||
|
cols: 80,
|
||||||
|
nodeId: "node-3",
|
||||||
|
}),
|
||||||
|
).rejects.toEqual({
|
||||||
|
code: "STRUCTURED_ROUTED_TO_PTY",
|
||||||
|
message:
|
||||||
|
"custom CLI launch for agent agent-2 in project proj-1 returned cellKind=pty; expected chat. sessionId=pty-session-1; nodeId=node-3",
|
||||||
|
});
|
||||||
|
expect(log).toHaveBeenCalledWith(
|
||||||
|
"[ticket149] launchAgentChat:routed-to-non-chat",
|
||||||
|
expect.objectContaining({
|
||||||
|
projectId: "proj-1",
|
||||||
|
agentId: "agent-2",
|
||||||
|
response: expect.objectContaining({
|
||||||
|
sessionId: "pty-session-1",
|
||||||
|
cellKind: "pty",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
log.mockRestore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import type {
|
|||||||
Agent,
|
Agent,
|
||||||
AgentContextDocument,
|
AgentContextDocument,
|
||||||
EffortSelection,
|
EffortSelection,
|
||||||
|
GatewayError,
|
||||||
ReplyChunk,
|
ReplyChunk,
|
||||||
ResumableAgent,
|
ResumableAgent,
|
||||||
TerminalSession,
|
TerminalSession,
|
||||||
@ -42,10 +43,28 @@ interface LaunchAgentResponse {
|
|||||||
cwd: string;
|
cwd: string;
|
||||||
rows: number;
|
rows: number;
|
||||||
cols: number;
|
cols: number;
|
||||||
|
/** Backend-derived routing: `chat` for structured sessions, `pty` for native TUI. */
|
||||||
|
cellKind?: "chat" | "pty";
|
||||||
/** Conversation id minted by this launch (omitted when nothing was assigned). */
|
/** Conversation id minted by this launch (omitted when nothing was assigned). */
|
||||||
assignedConversationId?: string;
|
assignedConversationId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function structuredLaunchRoutedToPtyError(
|
||||||
|
projectId: string,
|
||||||
|
agentId: string,
|
||||||
|
options: OpenTerminalOptions,
|
||||||
|
response: LaunchAgentResponse,
|
||||||
|
): GatewayError {
|
||||||
|
const actual = response.cellKind ?? "missing";
|
||||||
|
return {
|
||||||
|
code: "STRUCTURED_ROUTED_TO_PTY",
|
||||||
|
message:
|
||||||
|
`custom CLI launch for agent ${agentId} in project ${projectId} ` +
|
||||||
|
`returned cellKind=${actual}; expected chat. ` +
|
||||||
|
`sessionId=${response.sessionId}; nodeId=${options.nodeId ?? "none"}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class TauriAgentGateway implements AgentGateway {
|
export class TauriAgentGateway implements AgentGateway {
|
||||||
listAgents(projectId: string): Promise<Agent[]> {
|
listAgents(projectId: string): Promise<Agent[]> {
|
||||||
return invoke<Agent[]>("list_agents", { projectId });
|
return invoke<Agent[]>("list_agents", { projectId });
|
||||||
@ -194,13 +213,32 @@ export class TauriAgentGateway implements AgentGateway {
|
|||||||
agentId,
|
agentId,
|
||||||
rows: options.rows,
|
rows: options.rows,
|
||||||
cols: options.cols,
|
cols: options.cols,
|
||||||
|
cellKind: "chat",
|
||||||
conversationId: options.conversationId ?? null,
|
conversationId: options.conversationId ?? null,
|
||||||
nodeId: options.nodeId ?? null,
|
nodeId: options.nodeId ?? null,
|
||||||
},
|
},
|
||||||
onOutput: channel,
|
onOutput: channel,
|
||||||
});
|
});
|
||||||
|
if (res.cellKind !== "chat") {
|
||||||
|
const error = structuredLaunchRoutedToPtyError(projectId, agentId, options, res);
|
||||||
|
console.error("[ticket149] launchAgentChat:routed-to-non-chat", {
|
||||||
|
projectId,
|
||||||
|
agentId,
|
||||||
|
request: {
|
||||||
|
rows: options.rows,
|
||||||
|
cols: options.cols,
|
||||||
|
cellKind: "chat",
|
||||||
|
conversationId: options.conversationId ?? null,
|
||||||
|
nodeId: options.nodeId ?? null,
|
||||||
|
},
|
||||||
|
response: res,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
sessionId: res.sessionId,
|
sessionId: res.sessionId,
|
||||||
|
cellKind: "chat",
|
||||||
...(res.assignedConversationId
|
...(res.assignedConversationId
|
||||||
? { assignedConversationId: res.assignedConversationId }
|
? { assignedConversationId: res.assignedConversationId }
|
||||||
: {}),
|
: {}),
|
||||||
|
|||||||
@ -801,6 +801,7 @@ export class MockAgentGateway implements AgentGateway {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
sessionId,
|
sessionId,
|
||||||
|
cellKind: "chat",
|
||||||
...(!options.conversationId
|
...(!options.conversationId
|
||||||
? { assignedConversationId: `mock-conversation-${sessionId}` }
|
? { assignedConversationId: `mock-conversation-${sessionId}` }
|
||||||
: {}),
|
: {}),
|
||||||
|
|||||||
@ -152,6 +152,112 @@ describe("CustomAgentChatView", () => {
|
|||||||
expect(screen.queryByText(/structured session gone/)).toBeNull();
|
expect(screen.queryByText(/structured session gone/)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not duplicate the user prompt when the stream echoes it back", async () => {
|
||||||
|
const agent = {
|
||||||
|
launchAgentChat: vi.fn(),
|
||||||
|
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||||
|
sessionId,
|
||||||
|
scrollback: [],
|
||||||
|
})),
|
||||||
|
sendAgentChat: vi.fn(async (_sessionId: string, _prompt: string, onChunk) => {
|
||||||
|
onChunk({ kind: "userPrompt", text: "hello agent" });
|
||||||
|
onChunk({ kind: "final", content: "done" });
|
||||||
|
}),
|
||||||
|
cancelAgentChat: vi.fn(async () => {}),
|
||||||
|
closeAgentChat: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DIProvider
|
||||||
|
gateways={{
|
||||||
|
agent,
|
||||||
|
system: { pickFile: vi.fn(async () => null) },
|
||||||
|
} as unknown as Gateways}
|
||||||
|
>
|
||||||
|
<CustomAgentChatView
|
||||||
|
projectId="project-1"
|
||||||
|
agentId="agent-1"
|
||||||
|
agentName="Worker"
|
||||||
|
profile={profile}
|
||||||
|
cwd="/repo"
|
||||||
|
nodeId="node-1"
|
||||||
|
sessionId="chat-session-1"
|
||||||
|
conversationId="conversation-1"
|
||||||
|
onSessionId={vi.fn()}
|
||||||
|
onConversationId={vi.fn()}
|
||||||
|
/>
|
||||||
|
</DIProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(agent.reattachAgentChat).toHaveBeenCalledWith(
|
||||||
|
"chat-session-1",
|
||||||
|
expect.any(Function),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||||
|
target: { value: "hello agent" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||||
|
|
||||||
|
await screen.findByText("done");
|
||||||
|
expect(screen.getAllByText("hello agent")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the chat shell bounded with a scrollable message area and fixed composer", async () => {
|
||||||
|
const agent = {
|
||||||
|
launchAgentChat: vi.fn(() => new Promise<never>(() => {})),
|
||||||
|
reattachAgentChat: vi.fn(),
|
||||||
|
sendAgentChat: vi.fn(() => new Promise<void>(() => {})),
|
||||||
|
cancelAgentChat: vi.fn(async () => {}),
|
||||||
|
closeAgentChat: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DIProvider
|
||||||
|
gateways={{
|
||||||
|
agent,
|
||||||
|
system: { pickFile: vi.fn(async () => null) },
|
||||||
|
} as unknown as Gateways}
|
||||||
|
>
|
||||||
|
<CustomAgentChatView
|
||||||
|
projectId="project-1"
|
||||||
|
agentId="agent-1"
|
||||||
|
agentName="Worker with a very long name that must not collide with controls"
|
||||||
|
profile={{
|
||||||
|
...profile,
|
||||||
|
name: "Structured profile with a very long display name",
|
||||||
|
}}
|
||||||
|
cwd="/repo"
|
||||||
|
nodeId="node-1"
|
||||||
|
sessionId={null}
|
||||||
|
conversationId="conversation-1"
|
||||||
|
onSessionId={vi.fn()}
|
||||||
|
onConversationId={vi.fn()}
|
||||||
|
/>
|
||||||
|
</DIProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shell = screen.getByTestId("custom-agent-chat-view");
|
||||||
|
const toolbar = await screen.findByRole("toolbar", {
|
||||||
|
name: "custom agent chat actions",
|
||||||
|
});
|
||||||
|
const scroll = screen.getByTestId("custom-agent-chat-scroll");
|
||||||
|
const composer = screen.getByTestId("custom-agent-chat-composer");
|
||||||
|
const cancel = await screen.findByRole("button", { name: "Cancel" });
|
||||||
|
|
||||||
|
expect(shell.className).toContain("h-full");
|
||||||
|
expect(shell.className).toContain("min-h-0");
|
||||||
|
expect(shell.className).toContain("overflow-hidden");
|
||||||
|
expect(toolbar.className).toContain("overflow-hidden");
|
||||||
|
expect(cancel.className).toContain("shrink-0");
|
||||||
|
expect(scroll.className).toContain("flex-1");
|
||||||
|
expect(scroll.className).toContain("basis-0");
|
||||||
|
expect(scroll.className).toContain("overflow-y-auto");
|
||||||
|
expect(composer.className).toContain("shrink-0");
|
||||||
|
});
|
||||||
|
|
||||||
it("falls back when Tauri surfaces structured session NOT_FOUND as a raw string", async () => {
|
it("falls back when Tauri surfaces structured session NOT_FOUND as a raw string", async () => {
|
||||||
const agent = {
|
const agent = {
|
||||||
launchAgentChat: vi.fn(async () => ({
|
launchAgentChat: vi.fn(async () => ({
|
||||||
@ -476,6 +582,7 @@ describe("CustomAgentChatView", () => {
|
|||||||
const agent = {
|
const agent = {
|
||||||
launchAgentChat: vi.fn(async () => ({
|
launchAgentChat: vi.fn(async () => ({
|
||||||
sessionId: "new-session",
|
sessionId: "new-session",
|
||||||
|
cellKind: "chat",
|
||||||
assignedConversationId: "conversation-2",
|
assignedConversationId: "conversation-2",
|
||||||
})),
|
})),
|
||||||
reattachAgentChat: vi.fn().mockRejectedValueOnce({
|
reattachAgentChat: vi.fn().mockRejectedValueOnce({
|
||||||
@ -514,6 +621,49 @@ describe("CustomAgentChatView", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not publish a session id when custom launch is refused as PTY-routed", async () => {
|
||||||
|
const agent = {
|
||||||
|
launchAgentChat: vi.fn().mockRejectedValueOnce({
|
||||||
|
code: "STRUCTURED_ROUTED_TO_PTY",
|
||||||
|
message:
|
||||||
|
"custom CLI launch for agent agent-1 in project project-1 returned cellKind=pty; expected chat. sessionId=pty-session-1; nodeId=node-1",
|
||||||
|
}),
|
||||||
|
reattachAgentChat: vi.fn(),
|
||||||
|
sendAgentChat: vi.fn(() => new Promise<void>(() => {})),
|
||||||
|
cancelAgentChat: vi.fn(async () => {}),
|
||||||
|
closeAgentChat: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
const onSessionId = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DIProvider
|
||||||
|
gateways={{
|
||||||
|
agent,
|
||||||
|
system: { pickFile: vi.fn(async () => null) },
|
||||||
|
} as unknown as Gateways}
|
||||||
|
>
|
||||||
|
<CustomAgentChatView
|
||||||
|
projectId="project-1"
|
||||||
|
agentId="agent-1"
|
||||||
|
agentName="Worker"
|
||||||
|
profile={profile}
|
||||||
|
cwd="/repo"
|
||||||
|
nodeId="node-1"
|
||||||
|
sessionId={null}
|
||||||
|
conversationId="conversation-1"
|
||||||
|
onSessionId={onSessionId}
|
||||||
|
onConversationId={vi.fn()}
|
||||||
|
/>
|
||||||
|
</DIProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const alert = await screen.findByRole("alert");
|
||||||
|
expect(alert.textContent).toContain("cellKind=pty");
|
||||||
|
expect(agent.launchAgentChat).toHaveBeenCalledTimes(1);
|
||||||
|
expect(agent.reattachAgentChat).not.toHaveBeenCalled();
|
||||||
|
expect(onSessionId).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("recovers when post-launch reattach reports NOT_FOUND", async () => {
|
it("recovers when post-launch reattach reports NOT_FOUND", async () => {
|
||||||
const agent = {
|
const agent = {
|
||||||
launchAgentChat: vi
|
launchAgentChat: vi
|
||||||
|
|||||||
@ -93,6 +93,12 @@ function appendAgentDelta(turns: ChatTurn[], text: string): ChatTurn[] {
|
|||||||
return next;
|
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[] {
|
function foldChunk(turns: ChatTurn[], raw: unknown): ChatTurn[] {
|
||||||
if (!isReplyRecord(raw)) {
|
if (!isReplyRecord(raw)) {
|
||||||
return [...turns, { role: "unknown", text: unknownChunkLabel(raw) }];
|
return [...turns, { role: "unknown", text: unknownChunkLabel(raw) }];
|
||||||
@ -119,7 +125,7 @@ function foldChunk(turns: ChatTurn[], raw: unknown): ChatTurn[] {
|
|||||||
}
|
}
|
||||||
case "userPrompt":
|
case "userPrompt":
|
||||||
case "UserPrompt":
|
case "UserPrompt":
|
||||||
return [...turns, { role: "user", text: String(raw.text ?? raw.prompt ?? "") }];
|
return appendUserPrompt(turns, String(raw.text ?? raw.prompt ?? ""));
|
||||||
default:
|
default:
|
||||||
return [...turns, { role: "unknown", text: unknownChunkLabel(raw) }];
|
return [...turns, { role: "unknown", text: unknownChunkLabel(raw) }];
|
||||||
}
|
}
|
||||||
@ -481,20 +487,31 @@ export function CustomAgentChatView({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="custom-agent-chat-view"
|
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
|
||||||
<div className="min-w-0">
|
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-sm font-medium">{agentName}</div>
|
||||||
<div className="truncate text-xs text-muted">
|
<div className="truncate text-xs text-muted">
|
||||||
CLI custom · {profile.name}
|
CLI custom · {profile.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(opening || busy) && (
|
<div className="flex shrink-0 items-center justify-end gap-2">
|
||||||
<Button size="sm" variant="danger" onClick={() => void cancel()}>
|
{(opening || busy) && (
|
||||||
Cancel
|
<Button
|
||||||
</Button>
|
size="sm"
|
||||||
)}
|
variant="danger"
|
||||||
|
className="shrink-0 whitespace-nowrap"
|
||||||
|
onClick={() => void cancel()}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!supported && (
|
{!supported && (
|
||||||
@ -508,7 +525,11 @@ export function CustomAgentChatView({
|
|||||||
</p>
|
</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 ? (
|
{opening && turns.length === 0 ? (
|
||||||
<div className="flex items-center gap-2 text-sm text-muted">
|
<div className="flex items-center gap-2 text-sm text-muted">
|
||||||
<Spinner size={14} />
|
<Spinner size={14} />
|
||||||
@ -523,7 +544,10 @@ export function CustomAgentChatView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 && (
|
{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">
|
<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>
|
<span className="truncate">Fichier joint: {attachment}</span>
|
||||||
@ -532,7 +556,7 @@ export function CustomAgentChatView({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-end gap-2">
|
<div className="flex min-w-0 items-end gap-2">
|
||||||
<textarea
|
<textarea
|
||||||
aria-label={`message CLI custom ${nodeId}`}
|
aria-label={`message CLI custom ${nodeId}`}
|
||||||
className={cn(
|
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
|
Joindre
|
||||||
</Button>
|
</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
|
Envoyer
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -566,8 +602,8 @@ export function CustomAgentChatView({
|
|||||||
function ChatBubble({ turn }: { turn: ChatTurn }) {
|
function ChatBubble({ turn }: { turn: ChatTurn }) {
|
||||||
if (turn.role === "tool") {
|
if (turn.role === "tool") {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center">
|
<div className="flex min-w-0 justify-center">
|
||||||
<span className="max-w-[80%] rounded-md border border-border bg-raised px-2 py-1 text-xs text-muted">
|
<span className="max-w-[80%] truncate rounded-md border border-border bg-raised px-2 py-1 text-xs text-muted">
|
||||||
{turn.label}
|
{turn.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -575,8 +611,8 @@ function ChatBubble({ turn }: { turn: ChatTurn }) {
|
|||||||
}
|
}
|
||||||
if (turn.role === "final") {
|
if (turn.role === "final") {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-end">
|
<div className="flex min-w-0 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="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>
|
<div className="mb-1 text-xs font-semibold text-success">Task Complete</div>
|
||||||
<p className="whitespace-pre-wrap break-words">{turn.text}</p>
|
<p className="whitespace-pre-wrap break-words">{turn.text}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -585,8 +621,8 @@ function ChatBubble({ turn }: { turn: ChatTurn }) {
|
|||||||
}
|
}
|
||||||
if (turn.role === "error" || turn.role === "unknown") {
|
if (turn.role === "error" || turn.role === "unknown") {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center">
|
<div className="flex min-w-0 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="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: " : ""}
|
{turn.role === "unknown" ? "Chunk inconnu reçu: " : ""}
|
||||||
<span className="whitespace-pre-wrap break-words">{turn.text}</span>
|
<span className="whitespace-pre-wrap break-words">{turn.text}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -596,10 +632,10 @@ function ChatBubble({ turn }: { turn: ChatTurn }) {
|
|||||||
|
|
||||||
const user = turn.role === "user";
|
const user = turn.role === "user";
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex", user ? "justify-start" : "justify-end")}>
|
<div className={cn("flex min-w-0", user ? "justify-start" : "justify-end")}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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",
|
user ? "border-border bg-raised" : "border-primary/25 bg-primary/10",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -161,6 +161,8 @@ export interface ConversationDetails {
|
|||||||
export interface AgentChatHandle {
|
export interface AgentChatHandle {
|
||||||
/** Stable structured session id. */
|
/** Stable structured session id. */
|
||||||
readonly sessionId: string;
|
readonly sessionId: string;
|
||||||
|
/** Backend-confirmed cell routing for this launch. Must be `chat`. */
|
||||||
|
readonly cellKind: "chat";
|
||||||
/** Conversation id assigned by launch when the backend minted one. */
|
/** Conversation id assigned by launch when the backend minted one. */
|
||||||
readonly assignedConversationId?: string;
|
readonly assignedConversationId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user