Preserve chat shell identity across toolbar changes

This commit is contained in:
murashit 2026-07-13 13:18:25 +09:00
parent c053888fcd
commit 57cab74ecf
2 changed files with 27 additions and 5 deletions

View file

@ -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, <ChatPanelShell {...mount.props} shellReadModelBinding={mount.shellReadModelBinding} />);
renderUiRoot(container, <ChatPanelShell {...props} shellReadModelBinding={mount.shellReadModelBinding} />);
mount.props = props;
}
function uiRootIntact(container: HTMLElement, showToolbar: boolean): boolean {

View file

@ -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<HTMLElement>(".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<HTMLElement>(".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<HTMLElement>(".codex-panel__region--thread-stream")?.scrollTop).toBe(42);
await act(async () => {
unmountChatPanelShell(container);