diff --git a/src/features/chat/panel/shell.dom.tsx b/src/features/chat/panel/shell.dom.tsx index 9d61748a..2a4073d0 100644 --- a/src/features/chat/panel/shell.dom.tsx +++ b/src/features/chat/panel/shell.dom.tsx @@ -42,8 +42,7 @@ export function renderChatPanelShell(container: HTMLElement, props: ChatPanelShe container.addClass("codex-panel"); const existing = shellMounts.get(container); const mount = existing?.stateStore === props.stateStore ? existing : createShellMount(container, props); - mount.props = props; - renderMountedShell(container, mount); + renderMountedShell(container, mount, props); } export function unmountChatPanelShell(container: HTMLElement | null): void { @@ -75,13 +74,14 @@ function createShellMount(container: HTMLElement, props: ChatPanelShellProps): C return mount; } -function renderMountedShell(container: HTMLElement, mount: ChatPanelShellMount): void { +function renderMountedShell(container: HTMLElement, mount: ChatPanelShellMount, props: ChatPanelShellProps): void { if (!uiRootIntact(container, mount.props.showToolbar)) { unmountUiRoot(container); container.replaceChildren(); } syncStatusBarClearance(container); - renderUiRoot(container, ); + renderUiRoot(container, ); + mount.props = props; } function uiRootIntact(container: HTMLElement, showToolbar: boolean): boolean { diff --git a/tests/features/chat/panel/shell.test.tsx b/tests/features/chat/panel/shell.test.tsx index 7ac7187f..4197c18c 100644 --- a/tests/features/chat/panel/shell.test.tsx +++ b/tests/features/chat/panel/shell.test.tsx @@ -282,8 +282,9 @@ describe("ChatPanelShell", () => { }); }); - it("removes and restores the toolbar from shell props without replacing the body regions", async () => { + it("removes and restores the toolbar without losing composer or thread viewport state", async () => { const store = createChatStateStore(); + store.dispatch({ type: "composer/draft-set", draft: "toolbar continuity" }); const container = document.createElement("div"); document.body.appendChild(container); const parts = shellParts(); @@ -296,6 +297,12 @@ describe("ChatPanelShell", () => { expect(container.querySelector(".codex-panel__toolbar")).toBeNull(); expect(container.querySelector(".codex-panel__region--thread-stream")).not.toBeNull(); expect(container.querySelector(".codex-panel__region--composer")).not.toBeNull(); + const initialComposer = composer(container); + const initialThreadStream = container.querySelector(".codex-panel__region--thread-stream"); + if (!initialThreadStream) throw new Error("Missing thread stream"); + initialComposer.focus(); + initialComposer.setSelectionRange(2, 9); + initialThreadStream.scrollTop = 42; await act(async () => { renderChatPanelShell(container, { stateStore: store, showToolbar: true, parts }); @@ -304,6 +311,21 @@ describe("ChatPanelShell", () => { expect(container.querySelector(".codex-panel__toolbar")).not.toBeNull(); expect(container.firstElementChild?.classList.contains("codex-panel__toolbar")).toBe(true); + expect(document.activeElement).toBe(composer(container)); + expect(composer(container).selectionStart).toBe(2); + expect(composer(container).selectionEnd).toBe(9); + expect(container.querySelector(".codex-panel__region--thread-stream")?.scrollTop).toBe(42); + + await act(async () => { + renderChatPanelShell(container, { stateStore: store, showToolbar: false, parts }); + await settleShellEffects(); + }); + + expect(container.querySelector(".codex-panel__toolbar")).toBeNull(); + expect(document.activeElement).toBe(composer(container)); + expect(composer(container).selectionStart).toBe(2); + expect(composer(container).selectionEnd).toBe(9); + expect(container.querySelector(".codex-panel__region--thread-stream")?.scrollTop).toBe(42); await act(async () => { unmountChatPanelShell(container);