From 66bf130e6229dd92e1d21305f7f3128a0591edda Mon Sep 17 00:00:00 2001 From: SAPHID Date: Mon, 12 Oct 2026 23:04:38 +0000 Subject: [PATCH 1/2] fix(web): keep expanded tool actions usable from the keyboard Co-Authored-By: GPT-6.1 Sol --- .../components/chat/MessagesTimeline.test.tsx | 105 ++++++++++++++++++ .../src/components/chat/MessagesTimeline.tsx | 2 +- 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index 01857ac0004f1..4f687f8b59b17 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -5,10 +5,12 @@ import { CheckpointRef, EnvironmentId, MessageId, + OrchestrationV2TurnItem, ProjectId, RunId, ThreadId, } from "@t3tools/contracts"; +import { DateTime, Schema } from "effect"; import { act, createRef, @@ -276,6 +278,7 @@ beforeAll(async () => { }, 120_000); const ACTIVE_THREAD_ENVIRONMENT_ID = EnvironmentId.make("environment-local"); +const decodeTurnItem = Schema.decodeSync(OrchestrationV2TurnItem); const MESSAGE_CREATED_AT = "2026-03-17T19:12:28.000Z"; describe("timeline tooltip scroll dismissal", () => { @@ -492,6 +495,108 @@ function buildSnapShotTimelineEntry(previewUrl?: string) { } describe("MessagesTimeline", () => { + it.each(["Enter", " "])("leaves nested tool actions usable with %j", async (key) => { + vi.unstubAllGlobals(); + vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + vi.stubGlobal("requestAnimationFrame", () => 0); + vi.stubGlobal("cancelAnimationFrame", () => {}); + Object.defineProperty(window, "matchMedia", { value: matchMedia, configurable: true }); + activityTestState.expanded = true; + const query = await import("../../state/query"); + const querySpy = vi.spyOn(query, "useEnvironmentQuery").mockReturnValue({ + data: null, + dataUpdatedAt: 0, + error: null, + failure: null, + isPending: false, + isSuccess: false, + refresh: vi.fn(), + }); + const item = decodeTurnItem({ + id: "file-change-1", + threadId: "thread-1", + runId: "run-1", + nodeId: null, + providerThreadId: null, + providerTurnId: null, + nativeItemRef: null, + parentItemId: null, + ordinal: 0, + status: "completed", + title: null, + startedAt: null, + completedAt: null, + updatedAt: DateTime.makeUnsafe(MESSAGE_CREATED_AT), + type: "file_change", + fileName: "src/index.ts", + }); + const onOpenTurnDiff = vi.fn(); + const container = document.createElement("div"); + document.body.append(container); + const root = createRoot(container); + const pressKey = async (target: HTMLElement) => { + target.focus(); + const event = new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }); + await act(() => target.dispatchEvent(event)); + return event; + }; + try { + await act(() => + root.render( + , + ), + ); + const row = container.querySelector('[data-v2-item-type="file_change"]')!; + expect(row.getAttribute("aria-expanded")).toBe("false"); + expect((await pressKey(row)).defaultPrevented).toBe(true); + expect(row.getAttribute("aria-expanded")).toBe("true"); + const button = Array.from(row.querySelectorAll("button")).find( + (candidate) => candidate.textContent === "Open diff", + )!; + expect(button).toBeDefined(); + const event = await pressKey(button); + expect(event.defaultPrevented).toBe(false); + expect(button.isConnected).toBe(true); + expect(row.getAttribute("aria-expanded")).toBe("true"); + // jsdom does not synthesize native button activation from keyboard events. + await act(() => button.click()); + expect(onOpenTurnDiff).toHaveBeenCalledWith(item.runId, "src/index.ts"); + expect(row.getAttribute("aria-expanded")).toBe("true"); + expect((await pressKey(row)).defaultPrevented).toBe(true); + expect(row.getAttribute("aria-expanded")).toBe("false"); + } finally { + await act(() => root.unmount()); + querySpy.mockRestore(); + container.remove(); + vi.unstubAllGlobals(); + } + }); + it("shows dynamic tool input without cached output when the row is expanded", async () => { activityTestState.expanded = true; vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index bac737b66ba01..a9375411b2a66 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -5692,7 +5692,7 @@ function WorkEntryLogRow(props: WorkEntryRowProps) { "aria-expanded": expanded, onClick: toggleExpanded, onKeyDown: (e: KeyboardEvent) => { - if (e.key === "Enter" || e.key === " ") { + if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); toggleExpanded(); } From 731872b3c5b210cee6e364873f72f0271d48f6cb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 15:04:10 +0000 Subject: [PATCH 2/2] test(web): clarify nested tool action keyboard coverage --- .../chat/MessagesTimeline.browser.test.tsx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx b/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx index 580ba0278895a..8b251da3be299 100644 --- a/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx @@ -194,22 +194,28 @@ async function click(element: HTMLElement) { } describe("MessagesTimeline work rows", () => { - it.each(["Enter", " "])("leaves nested tool actions usable with %j", async (key) => { + it.each([ + ["Enter", "{Enter}"], + ["Space", " "], + ])("opens a nested diff with %s without collapsing the tool row", async (_name, key) => { const onOpenTurnDiff = vi.fn(); - await renderTimeline([ - work("file-change-1", { - label: "Updated src/index.ts", - tone: "tool", - itemType: "file_change", - toolLifecycleStatus: "completed", - projectedItem: v2Item("file_change", { id: "file-change-1", fileName: "src/index.ts" }), - }), - ], { onOpenTurnDiff }); + await renderTimeline( + [ + work("file-change-1", { + label: "Updated src/index.ts", + tone: "tool", + itemType: "file_change", + toolLifecycleStatus: "completed", + projectedItem: v2Item("file_change", { id: "file-change-1", fileName: "src/index.ts" }), + }), + ], + { onOpenTurnDiff }, + ); const row = container.querySelector('[data-v2-item-type="file_change"]')!; const press = async (target: HTMLElement) => { await act(async () => { target.focus(); - await userEvent.keyboard(key === "Enter" ? "{Enter}" : " "); + await userEvent.keyboard(key); }); }; expect(row.getAttribute("aria-expanded")).toBe("false"); @@ -218,7 +224,7 @@ describe("MessagesTimeline work rows", () => { const openDiff = button("Open diff"); await press(openDiff); expect(onOpenTurnDiff).toHaveBeenCalledWith("run-1", "src/index.ts"); - expect(openDiff.isConnected).toBe(true); + expect(onOpenTurnDiff).toHaveBeenCalledTimes(1); expect(row.getAttribute("aria-expanded")).toBe("true"); await press(row); expect(row.getAttribute("aria-expanded")).toBe("false");