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);