From 96909dee59534ed0db0a1e0657a18aa61df599b9 Mon Sep 17 00:00:00 2001 From: Qiyuanqiii <267806965+Qiyuanqiii@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:24:40 +0800 Subject: [PATCH 1/8] fix(ui): refine narrow-screen layout, single-row task nav, and squeeze transitions - Auto-collapse sidebar drawer when window squeezes to <= 760px and restore when widened - Single-row compact task navigation bar with icon-only non-active tabs on narrow viewports - Fluid sidebar width with clamp(248px, 28vw, 299px) and smooth drawer transitions - Responsive composer sizing and mobile-friendly stats layout - Make narrative streaming copy button test resilient across default and custom locales Co-authored-by: Gemini --- web/src/v2/app.test.tsx | 30 ++++ web/src/v2/app.tsx | 11 ++ web/src/v2/components/composer-stats.css | 9 ++ web/src/v2/components/conversation.tsx | 16 +- .../components/narrative-streaming.test.tsx | 4 +- web/src/v2/styles.css | 143 +++++++++++++++--- 6 files changed, 183 insertions(+), 30 deletions(-) diff --git a/web/src/v2/app.test.tsx b/web/src/v2/app.test.tsx index e235830b..4ca1e24e 100644 --- a/web/src/v2/app.test.tsx +++ b/web/src/v2/app.test.tsx @@ -206,6 +206,36 @@ describe("V2Workbench inspector navigation", () => { expectDrawerClosed(); }); + it("automatically collapses the sidebar when squeezed to narrow width and restores it when widened", async () => { + let narrowListener: ((event: MediaQueryListEvent) => void) | undefined; + vi.stubGlobal("matchMedia", vi.fn((query: string) => ({ + matches: false, media: query, + addEventListener: vi.fn((type: string, listener: (event: MediaQueryListEvent) => void) => { + if (query === "(max-width: 760px)" && type === "change") narrowListener = listener; + }), + removeEventListener: vi.fn(), + addListener: vi.fn(), removeListener: vi.fn(), dispatchEvent: vi.fn(), onchange: null, + }))); + window.history.replaceState({}, "", `#/threads/${createdThread.id}`); + const client = { hasThreadControl: true, getPage: vi.fn(async (path: string) => ({ + items: path === "/workspaces" ? [workspace] : [createdThread], + page: { limit: 100 }, requestID: path, + })) } as unknown as APIClient; + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + render(); + await screen.findByTestId("v2-conversation"); + expect(screen.getByRole("navigation", { name: "对话导航" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "隐藏侧栏" })).toHaveAttribute("aria-pressed", "true"); + + act(() => { narrowListener?.({ matches: true } as MediaQueryListEvent); }); + expect(screen.queryByRole("navigation", { name: "对话导航" })).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "显示侧栏" })).toHaveAttribute("aria-pressed", "false"); + + act(() => { narrowListener?.({ matches: false } as MediaQueryListEvent); }); + expect(screen.getByRole("navigation", { name: "对话导航" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "隐藏侧栏" })).toHaveAttribute("aria-pressed", "true"); + }); + it("loads past 100 Threads and preserves an older URL selection through failed paging and refresh", async () => { const user = userEvent.setup(); const first = Array.from({ length: 100 }, (_, index) => ({ ...createdThread, diff --git a/web/src/v2/app.tsx b/web/src/v2/app.tsx index be81f6e7..a47031b9 100644 --- a/web/src/v2/app.tsx +++ b/web/src/v2/app.tsx @@ -276,6 +276,17 @@ function V2WorkbenchContent({ client }: { client: APIClient }) { const settingsSection = route.section ?? "general"; const newConversation = route.kind !== "thread"; const [sidebarVisible, setSidebarVisible] = useState(() => !window.matchMedia?.("(max-width: 760px)").matches); + useEffect(() => { + const media = window.matchMedia?.("(max-width: 760px)"); + if (!media) return; + const onChange = (event: MediaQueryListEvent) => setSidebarVisible(!event.matches); + if (typeof media.addEventListener === "function") { + media.addEventListener("change", onChange); + return () => media.removeEventListener("change", onChange); + } + media.addListener?.(onChange); + return () => media.removeListener?.(onChange); + }, []); const [searchOpen, setSearchOpen] = useState(false); const [threadSearch, setThreadSearch] = useState(""); const [appliedThreadSearch, setAppliedThreadSearch] = useState(""); diff --git a/web/src/v2/components/composer-stats.css b/web/src/v2/components/composer-stats.css index 1114d26f..fb6b1a83 100644 --- a/web/src/v2/components/composer-stats.css +++ b/web/src/v2/components/composer-stats.css @@ -57,3 +57,12 @@ line-height: 1.6; overflow-wrap: anywhere; } + +@media (max-width: 760px) { + .v2-composer-stats { + gap: 2px 8px; + margin-top: 3px; + font-size: 11px; + line-height: 18px; + } +} diff --git a/web/src/v2/components/conversation.tsx b/web/src/v2/components/conversation.tsx index bd042127..20e92449 100644 --- a/web/src/v2/components/conversation.tsx +++ b/web/src/v2/components/conversation.tsx @@ -941,27 +941,27 @@ export function V2Conversation({ client, threadID, workspaces, onArchive, onMana {view === "inspector" && (onOpenTool || onOpenInspectorHome) &&