feat(cli): historique de messages navigable haut/bas dans la CLI custom + contrôles de cellule visibles pendant busy — persistance localStorage du transcript custom (CustomAgentChatView), layering des contrôles sous l'overlay busy (LayoutGrid), bump pointeur IdeaSDK (docs plugin minimal + hot reload) (QA verte: 65 tests CustomAgentChatView, 25 tests LayoutGrid chat/layering, npm build frontend OK)

This commit is contained in:
2026-09-08 09:39:35 +02:00
parent 154728b828
commit fc3d124574
6 changed files with 350 additions and 48 deletions

View File

@ -140,6 +140,7 @@ function setMeasuredTextOverflow(
describe("CustomAgentChatView", () => {
beforeEach(() => {
vi.clearAllMocks();
window.localStorage.clear();
__clearCustomAgentChatViewCacheForTests();
__resetProjectCliInteractionActivityForTests();
});
@ -236,6 +237,144 @@ describe("CustomAgentChatView", () => {
expect(screen.queryByRole("button", { name: "Ouvrir en TUI native" })).toBeNull();
});
it("recalls previously sent prompts with arrow up and down", async () => {
const agent = {
launchAgentChat: vi.fn(),
reattachAgentChat: vi.fn(async (sessionId: string) => ({
sessionId,
scrollback: [],
})),
sendAgentChat: vi.fn(
async (_sessionId: string, prompt: string, onChunk: (chunk: ReplyChunk) => void) => {
onChunk({ kind: "final", content: `answer ${prompt}` });
},
),
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"
layoutId="layout-1"
nodeId="node-1"
sessionId="chat-session-1"
conversationId="conversation-1"
onSessionId={vi.fn()}
onConversationId={vi.fn()}
/>
</DIProvider>,
);
const composer = await screen.findByLabelText(/message CLI custom/) as HTMLTextAreaElement;
await waitFor(() => expect(agent.reattachAgentChat).toHaveBeenCalled());
fireEvent.change(composer, { target: { value: "first prompt" } });
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledWith(
"chat-session-1",
"first prompt",
expect.any(Function),
));
fireEvent.change(composer, { target: { value: "second prompt" } });
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledWith(
"chat-session-1",
"second prompt",
expect.any(Function),
));
fireEvent.keyDown(composer, { key: "ArrowUp" });
await waitFor(() => expect(composer.value).toBe("second prompt"));
fireEvent.keyDown(composer, { key: "ArrowUp" });
await waitFor(() => expect(composer.value).toBe("first prompt"));
fireEvent.keyDown(composer, { key: "ArrowDown" });
await waitFor(() => expect(composer.value).toBe("second prompt"));
fireEvent.change(composer, { target: { value: "draft in progress" } });
fireEvent.keyDown(composer, { key: "ArrowUp" });
await waitFor(() => expect(composer.value).toBe("second prompt"));
fireEvent.keyDown(composer, { key: "ArrowDown" });
await waitFor(() => expect(composer.value).toBe("draft in progress"));
});
it("persists prompt history across a custom chat remount", async () => {
const agent = {
launchAgentChat: vi.fn(),
reattachAgentChat: vi.fn(async (sessionId: string) => ({
sessionId,
scrollback: [],
})),
sendAgentChat: vi.fn(
async (_sessionId: string, prompt: string, onChunk: (chunk: ReplyChunk) => void) => {
onChunk({ kind: "final", content: `answer ${prompt}` });
},
),
cancelAgentChat: vi.fn(async () => {}),
closeAgentChat: vi.fn(async () => {}),
};
const props = {
projectId: "project-1",
agentId: "agent-1",
agentName: "Worker",
profile,
cwd: "/repo",
layoutId: "layout-1",
nodeId: "node-1",
sessionId: "chat-session-1",
conversationId: "conversation-1",
onSessionId: vi.fn(),
onConversationId: vi.fn(),
};
const view = render(
<DIProvider
gateways={{
agent,
system: { pickFile: vi.fn(async () => null) },
} as unknown as Gateways}
>
<CustomAgentChatView {...props} />
</DIProvider>,
);
let composer = await screen.findByLabelText(/message CLI custom/) as HTMLTextAreaElement;
fireEvent.change(composer, { target: { value: "remember me" } });
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledWith(
"chat-session-1",
"remember me",
expect.any(Function),
));
view.unmount();
render(
<DIProvider
gateways={{
agent,
system: { pickFile: vi.fn(async () => null) },
} as unknown as Gateways}
>
<CustomAgentChatView {...props} />
</DIProvider>,
);
composer = await screen.findByLabelText(/message CLI custom/) as HTMLTextAreaElement;
fireEvent.keyDown(composer, { key: "ArrowUp" });
await waitFor(() => expect(composer.value).toBe("remember me"));
});
it("repaints attachment labels from user prompt scrollback", async () => {
const agent = {
launchAgentChat: vi.fn(),