fix(chat): réattache persistante CLI custom et rendu tools/MCP/progress enrichi (#188)
Corrige la perte de session à la navigation: le démontage du CustomAgentChatView appelait close_agent_session (destructeur) au lieu de simplement détacher le transport. Désormais: - nouvelle commande Tauri detach_agent_chat (+ ChatBridge::detach) qui libère le canal sans tuer la session ni le scrollback retenu; distinct de close_agent_session qui reste le close destructeur explicite; - le frontend appelle detachAgentChat à l'unmount puis restore sa vue depuis un cache de session (turns + chunkCount) à la réattache; - message « Reprise effectuée: N événements rattrapés… » quand la réattache rejoue plus de chunks que le cache local n'en avait conservé. Enrichit le rendu tools/MCP/progress: - ProgressSettler (chat bridge) émet un progress completed synthétique quand un spinner reste ouvert (nouvelle entité progress, chunk text/final/error): fini les spinners fantômes; - les adapters claude/codex/opencode/openai_compat extraient un aperçu du payload (arguments/input/command/message/params) attaché au progress; - bounded_reply_progress_preview (domain) garde les 4 premières lignes non vides, normalisation CR/LF; - previewText (frontend) multiligne whitespace-pre-wrap, source/nativeEvent affichés sous le titre; suppression du flag settled et du Spinner inline devenus redondants (l'état est porté par le stage + le settler). Tests: detach_agent_chat gateway (Tauri + mock), ProgressSettler, bornage reply_progress, item_preview/tool_preview par adapter, réattache avec cache et notice de rattrapage.
This commit is contained in:
@ -1,9 +1,9 @@
|
||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { useState } from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { DIProvider } from "@/app/di";
|
||||
import type { AgentProfile } from "@/domain";
|
||||
import type { AgentProfile, ReplyChunk } from "@/domain";
|
||||
import { PluginRuntimeProvider } from "@/features/plugins";
|
||||
import {
|
||||
PluginCommandRegistry,
|
||||
@ -12,7 +12,10 @@ import {
|
||||
PluginRuntimeRegistry,
|
||||
} from "@/plugins/runtime";
|
||||
import type { ChatDraftScope, Gateways } from "@/ports";
|
||||
import { CustomAgentChatView } from "./CustomAgentChatView";
|
||||
import {
|
||||
CustomAgentChatView,
|
||||
__clearCustomAgentChatViewCacheForTests,
|
||||
} from "./CustomAgentChatView";
|
||||
|
||||
const profile: AgentProfile = {
|
||||
id: "structured",
|
||||
@ -57,6 +60,11 @@ function addPluginCommand(
|
||||
}
|
||||
|
||||
describe("CustomAgentChatView", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
__clearCustomAgentChatViewCacheForTests();
|
||||
});
|
||||
|
||||
it("repaints attachment labels from user prompt scrollback", async () => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
@ -101,6 +109,75 @@ describe("CustomAgentChatView", () => {
|
||||
expect(screen.getByText("Fichier: clipboard.png")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("detaches on unmount then reattaches with missed scrollback chunks", async () => {
|
||||
let scrollback: ReplyChunk[] = [
|
||||
{ kind: "userPrompt" as const, text: "Initial prompt" },
|
||||
{ kind: "textDelta" as const, text: "Initial reply" },
|
||||
];
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||
sessionId,
|
||||
scrollback,
|
||||
})),
|
||||
detachAgentChat: vi.fn(async () => {}),
|
||||
sendAgentChat: vi.fn(async () => {}),
|
||||
cancelAgentChat: vi.fn(async () => {}),
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
const props = {
|
||||
projectId: "project-1",
|
||||
agentId: "agent-1",
|
||||
agentName: "Worker",
|
||||
profile,
|
||||
cwd: "/repo",
|
||||
nodeId: "node-1",
|
||||
sessionId: "chat-session-1",
|
||||
conversationId: "conversation-1",
|
||||
onSessionId: vi.fn(),
|
||||
onConversationId: vi.fn(),
|
||||
};
|
||||
|
||||
const first = render(
|
||||
<DIProvider
|
||||
gateways={{
|
||||
agent,
|
||||
system: { pickFile: vi.fn(async () => null) },
|
||||
} as unknown as Gateways}
|
||||
>
|
||||
<CustomAgentChatView {...props} />
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Initial reply")).toBeTruthy();
|
||||
first.unmount();
|
||||
await waitFor(() => expect(agent.detachAgentChat).toHaveBeenCalledWith("chat-session-1"));
|
||||
|
||||
scrollback = [
|
||||
...scrollback,
|
||||
{ kind: "toolActivity" as const, label: "Background tool" },
|
||||
{ kind: "final" as const, content: "Completed away" },
|
||||
];
|
||||
|
||||
render(
|
||||
<DIProvider
|
||||
gateways={{
|
||||
agent,
|
||||
system: { pickFile: vi.fn(async () => null) },
|
||||
} as unknown as Gateways}
|
||||
>
|
||||
<CustomAgentChatView {...props} />
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Initial reply")).toBeTruthy();
|
||||
expect(await screen.findByText("Background tool")).toBeTruthy();
|
||||
expect(screen.getByText("Completed away")).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText("Reprise effectuée: 2 événements rattrapés pendant l'absence."),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("stops the live agent with cascade feedback and releases the structured session", async () => {
|
||||
let resolveStop: (value: {
|
||||
agentId: string;
|
||||
@ -614,7 +691,7 @@ describe("CustomAgentChatView", () => {
|
||||
|
||||
expect(screen.getByText("Worker → QA")).toBeTruthy();
|
||||
expect(screen.getByText("Worker → QA").parentElement?.textContent).toContain("started");
|
||||
expect(screen.getAllByRole("status", { name: "Loading" }).length).toBeGreaterThan(0);
|
||||
expect(screen.queryByRole("status", { name: "Loading" })).toBeNull();
|
||||
|
||||
act(() => {
|
||||
emitChunk?.(progress("providerNative", "completed"));
|
||||
@ -638,7 +715,7 @@ describe("CustomAgentChatView", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("stops animating running progress rows once the turn reaches a final chunk", async () => {
|
||||
it("renders running progress without spinner and marks it done on final", async () => {
|
||||
let emitChunk: ((chunk: unknown) => void) | null = null;
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
@ -707,7 +784,7 @@ describe("CustomAgentChatView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
expect(screen.getAllByRole("status", { name: "Loading" }).length).toBeGreaterThan(0);
|
||||
expect(screen.queryByRole("status", { name: "Loading" })).toBeNull();
|
||||
expect(screen.getByText("Progress").parentElement?.textContent).toContain("running");
|
||||
|
||||
act(() => {
|
||||
|
||||
Reference in New Issue
Block a user