fix(frontend): ArrowUp rappelle l'historique chat même en multiligne — #304 (cause: le handler ArrowUp remontait dans le texte dès que le draft faisait plus d'une ligne, au lieu de rappeler le message précédent; fix type terminal: ArrowUp déclenche le rappel historique uniquement quand le curseur est à la fin de la dernière ligne du textarea, sinon navigation caret normale préservée; ArrowDown symétrique en fin de draft pour redescendre dans l'historique; QA verte reconfirmée par Git: npm test -- src/features/agents/CustomAgentChatView.test.tsx 71/71, npm run typecheck OK, git diff --check OK; QA fournie: npm run build OK; fichiers .ideai/* tickets/counter/index/attachments et scripts/ hors périmètre; aucun push)
This commit is contained in:
@ -138,6 +138,57 @@ function setMeasuredTextOverflow(
|
||||
fireEvent(window, new Event("resize"));
|
||||
}
|
||||
|
||||
function renderHistoryChat() {
|
||||
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>,
|
||||
);
|
||||
return agent;
|
||||
}
|
||||
|
||||
async function sendHistoryPrompt(
|
||||
composer: HTMLTextAreaElement,
|
||||
agent: ReturnType<typeof renderHistoryChat>,
|
||||
prompt: string,
|
||||
) {
|
||||
const expectedCalls = agent.sendAgentChat.mock.calls.length + 1;
|
||||
fireEvent.change(composer, { target: { value: prompt } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledTimes(expectedCalls));
|
||||
}
|
||||
|
||||
describe("CustomAgentChatView", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@ -532,6 +583,95 @@ describe("CustomAgentChatView", () => {
|
||||
await waitFor(() => expect(composer.value).toBe("draft in progress"));
|
||||
});
|
||||
|
||||
it("recalls history from the exact end of a multiline draft and restores it verbatim", async () => {
|
||||
const agent = renderHistoryChat();
|
||||
const composer = (await screen.findByLabelText(
|
||||
/message CLI custom/,
|
||||
)) as HTMLTextAreaElement;
|
||||
await waitFor(() => expect(agent.reattachAgentChat).toHaveBeenCalled());
|
||||
await sendHistoryPrompt(composer, agent, "historic prompt");
|
||||
|
||||
const multilineDraft = "ligne 1\nligne 2\nfin 🧪 ";
|
||||
fireEvent.change(composer, { target: { value: multilineDraft } });
|
||||
composer.setSelectionRange(multilineDraft.length, multilineDraft.length);
|
||||
Object.defineProperty(composer, "scrollHeight", {
|
||||
value: 480,
|
||||
configurable: true,
|
||||
});
|
||||
composer.scrollTop = 0;
|
||||
|
||||
expect(fireEvent.keyDown(composer, { key: "ArrowUp" })).toBe(false);
|
||||
await waitFor(() => expect(composer.value).toBe("historic prompt"));
|
||||
expect(composer.selectionStart).toBe(composer.value.length);
|
||||
expect(composer.selectionEnd).toBe(composer.value.length);
|
||||
expect(composer.scrollTop).toBe(480);
|
||||
|
||||
expect(fireEvent.keyDown(composer, { key: "ArrowDown" })).toBe(false);
|
||||
await waitFor(() => expect(composer.value).toBe(multilineDraft));
|
||||
expect(composer.selectionStart).toBe(multilineDraft.length);
|
||||
expect(composer.selectionEnd).toBe(multilineDraft.length);
|
||||
expect(composer.scrollTop).toBe(480);
|
||||
});
|
||||
|
||||
it("keeps ArrowUp native away from a simple end caret or during modified/IME input", async () => {
|
||||
const agent = renderHistoryChat();
|
||||
const composer = (await screen.findByLabelText(
|
||||
/message CLI custom/,
|
||||
)) as HTMLTextAreaElement;
|
||||
await waitFor(() => expect(agent.reattachAgentChat).toHaveBeenCalled());
|
||||
await sendHistoryPrompt(composer, agent, "historic prompt");
|
||||
|
||||
const draft = "première\ndeuxième";
|
||||
const nativeCases: Array<{
|
||||
selection: [number, number];
|
||||
event?: Record<string, unknown>;
|
||||
}> = [
|
||||
{ selection: [4, 4] },
|
||||
{ selection: [0, draft.length] },
|
||||
{ selection: [draft.length, draft.length], event: { shiftKey: true } },
|
||||
{ selection: [draft.length, draft.length], event: { ctrlKey: true } },
|
||||
{ selection: [draft.length, draft.length], event: { altKey: true } },
|
||||
{ selection: [draft.length, draft.length], event: { metaKey: true } },
|
||||
{ selection: [draft.length, draft.length], event: { isComposing: true } },
|
||||
{ selection: [draft.length, draft.length], event: { keyCode: 229 } },
|
||||
];
|
||||
for (const testCase of nativeCases) {
|
||||
fireEvent.change(composer, { target: { value: draft } });
|
||||
composer.setSelectionRange(...testCase.selection);
|
||||
expect(
|
||||
fireEvent.keyDown(composer, { key: "ArrowUp", ...testCase.event }),
|
||||
).toBe(true);
|
||||
expect(composer.value).toBe(draft);
|
||||
}
|
||||
|
||||
composer.setSelectionRange(draft.length, draft.length);
|
||||
expect(fireEvent.keyDown(composer, { key: "ArrowUp" })).toBe(false);
|
||||
await waitFor(() => expect(composer.value).toBe("historic prompt"));
|
||||
});
|
||||
|
||||
it("does not consume ArrowUp with empty history and never mutates recalled entries", async () => {
|
||||
const agent = renderHistoryChat();
|
||||
const composer = (await screen.findByLabelText(
|
||||
/message CLI custom/,
|
||||
)) as HTMLTextAreaElement;
|
||||
await waitFor(() => expect(agent.reattachAgentChat).toHaveBeenCalled());
|
||||
|
||||
expect(fireEvent.keyDown(composer, { key: "ArrowUp" })).toBe(true);
|
||||
await sendHistoryPrompt(composer, agent, "original history");
|
||||
fireEvent.keyDown(composer, { key: "ArrowUp" });
|
||||
await waitFor(() => expect(composer.value).toBe("original history"));
|
||||
|
||||
fireEvent.change(composer, { target: { value: "edited recall" } });
|
||||
composer.setSelectionRange(composer.value.length, composer.value.length);
|
||||
fireEvent.keyDown(composer, { key: "ArrowUp" });
|
||||
await waitFor(() => expect(composer.value).toBe("original history"));
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledTimes(2));
|
||||
expect(composer.value).toBe("");
|
||||
expect(fireEvent.keyDown(composer, { key: "ArrowDown" })).toBe(true);
|
||||
});
|
||||
|
||||
it("persists prompt history across a custom chat remount", async () => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
@ -3422,7 +3562,7 @@ describe("CustomAgentChatView", () => {
|
||||
expect(screen.getByRole("option", { name: /\/clean/ })).toBeTruthy();
|
||||
expect(screen.getByRole("option", { name: /\/commit/ })).toBeTruthy();
|
||||
|
||||
fireEvent.keyDown(composer, { key: "ArrowDown" });
|
||||
fireEvent.keyDown(composer, { key: "ArrowUp" });
|
||||
fireEvent.keyDown(composer, { key: "Enter" });
|
||||
|
||||
await waitFor(() => expect(composer.value).toBe("/commit "));
|
||||
|
||||
Reference in New Issue
Block a user