From c7fcfd34829418476698decd0a712913e541ff27 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Jul 2026 19:01:32 -0700 Subject: [PATCH] fix(ui): gate chat sidebar mutations (#113947) --- ui/src/pages/chat/chat-pane-render.ts | 3 ++ .../chat/chat-pane-sidebar-layout.test.ts | 2 + ui/src/pages/chat/chat-pane-sidebar-layout.ts | 3 ++ .../components/chat-sidebar-region.runtime.ts | 43 +++++++++------ .../components/chat-sidebar-region.test.ts | 53 ++++++++++++++++++- 5 files changed, 86 insertions(+), 18 deletions(-) diff --git a/ui/src/pages/chat/chat-pane-render.ts b/ui/src/pages/chat/chat-pane-render.ts index 48185432aa40..41e6cf3360ee 100644 --- a/ui/src/pages/chat/chat-pane-render.ts +++ b/ui/src/pages/chat/chat-pane-render.ts @@ -668,6 +668,9 @@ export class ChatPaneRender extends ChatPaneHeaderRender { focusVersion: state.sidebarFocusVersion, layout: sidebarLayout, narrow: this.paneWidth < SIDEBAR_NARROW_BREAKPOINT_PX, + panelMutationEnabled: { + chat: Boolean(board.activeTabId) && !board.activeTabReadOnly && board.provider.canMutate, + }, panelTemplates, primary, sessionKey: state.sessionKey, diff --git a/ui/src/pages/chat/chat-pane-sidebar-layout.test.ts b/ui/src/pages/chat/chat-pane-sidebar-layout.test.ts index b8a2dab82161..b7a6022a6cac 100644 --- a/ui/src/pages/chat/chat-pane-sidebar-layout.test.ts +++ b/ui/src/pages/chat/chat-pane-sidebar-layout.test.ts @@ -47,6 +47,7 @@ describe("chat pane sidebar layout", () => { focusVersion: 0, layout, narrow, + panelMutationEnabled: {}, panelTemplates: { detail: html`` }, primary: html`
Primary
`, sessionKey: "agent:main:current", @@ -107,6 +108,7 @@ describe("chat pane sidebar layout", () => { focusVersion: 0, layout: openSlot({ columns: [] }, "detail"), narrow: false, + panelMutationEnabled: {}, panelTemplates: { detail: html`` }, primary: html`
Primary
`, sessionKey: "agent:main:current", diff --git a/ui/src/pages/chat/chat-pane-sidebar-layout.ts b/ui/src/pages/chat/chat-pane-sidebar-layout.ts index f89aef77f708..5f1705f2029d 100644 --- a/ui/src/pages/chat/chat-pane-sidebar-layout.ts +++ b/ui/src/pages/chat/chat-pane-sidebar-layout.ts @@ -18,6 +18,7 @@ import { type SidebarColumn, type SidebarLayout, type SidebarPanel, + type SidebarSlotId, } from "./sidebar-layout.ts"; const DETAIL_FULL_MESSAGE_MAX_CHARS = 500_000; @@ -31,6 +32,7 @@ export function renderSidebarRegion(params: { focusVersion: number; layout: SidebarLayout; narrow: boolean; + panelMutationEnabled: Partial>; panelTemplates: SidebarPanelTemplates; primary: TemplateResult; sessionKey: string; @@ -53,6 +55,7 @@ export function renderSidebarRegion(params: { .layout=${params.layout} .panelTemplates=${params.panelTemplates} .panelOpenUrls=${{ discussion: params.discussionOpenUrl }} + .panelMutationEnabled=${params.panelMutationEnabled} .callbacks=${params.callbacks} .sessionKey=${params.sessionKey} .focusPanelId=${params.focusPanelId} diff --git a/ui/src/pages/chat/components/chat-sidebar-region.runtime.ts b/ui/src/pages/chat/components/chat-sidebar-region.runtime.ts index 6c8d91971168..4d13064bdc89 100644 --- a/ui/src/pages/chat/components/chat-sidebar-region.runtime.ts +++ b/ui/src/pages/chat/components/chat-sidebar-region.runtime.ts @@ -39,6 +39,8 @@ class ChatSidebarRegion extends OpenClawLightDomElement { @property({ attribute: false }) layout: SidebarLayout = { columns: [] }; @property({ attribute: false }) panelTemplates: SidebarPanelTemplates = {}; @property({ attribute: false }) panelOpenUrls: Partial> = {}; + @property({ attribute: false }) panelMutationEnabled: Partial> = + {}; @property({ attribute: false }) callbacks: SidebarRegionCallbacks | null = null; @property() sessionKey = ""; @property() focusPanelId = ""; @@ -122,6 +124,10 @@ class ChatSidebarRegion extends OpenClawLightDomElement { this.callbacks?.activatePanel(panelId); } + private canMutatePanel(slot: SidebarSlotId): boolean { + return this.panelMutationEnabled[slot] !== false; + } + private renderHeader(column: SidebarColumn, activePanelId: string, narrow: boolean) { const active = column.panels.find((panel) => panel.id === activePanelId) ?? column.panels[0]; if (!active) { @@ -141,22 +147,25 @@ class ChatSidebarRegion extends OpenClawLightDomElement { without-scroll-controls @wa-tab-show=${(event: CustomEvent<{ name: string }>) => this.activate(event.detail.name)} > - ${column.panels.map( - (panel) => html` + ${column.panels.map((panel) => { + const draggable = !narrow && this.canMutatePanel(panel.slot); + return html` - narrow ? undefined : this.startDrag(event, panel.id)} + draggable ? this.startDrag(event, panel.id) : undefined} @dragend=${() => this.endDrag()} > ${panelTitle(panel.slot)} - `, - )} + `; + })} `; diff --git a/ui/src/pages/chat/components/chat-sidebar-region.test.ts b/ui/src/pages/chat/components/chat-sidebar-region.test.ts index 0280584b4a8b..8744943c751d 100644 --- a/ui/src/pages/chat/components/chat-sidebar-region.test.ts +++ b/ui/src/pages/chat/components/chat-sidebar-region.test.ts @@ -3,7 +3,7 @@ import { html } from "lit"; import { afterEach, describe, expect, it, vi } from "vitest"; import "../../../components/resizable-divider.ts"; -import { mergePanelIntoColumn, openSlot } from "../sidebar-layout.ts"; +import { mergePanelIntoColumn, openSlot, type SidebarSlotId } from "../sidebar-layout.ts"; import "./chat-sidebar-region.runtime.ts"; type Region = HTMLElementTagNameMap["openclaw-chat-sidebar-region"] & { @@ -12,7 +12,10 @@ type Region = HTMLElementTagNameMap["openclaw-chat-sidebar-region"] & { const regions: Region[] = []; -async function createRegion(narrow: boolean) { +async function createRegion( + narrow: boolean, + panelMutationEnabled: Partial> = {}, +) { const shell = document.createElement("div"); shell.className = `sidebar-region ${narrow ? "sidebar-region--narrow" : ""}`; const region = document.createElement("openclaw-chat-sidebar-region") as Region; @@ -22,6 +25,7 @@ async function createRegion(narrow: boolean) { detail: html`
Detail panel
`, discussion: html`
Discussion panel
`, }; + region.panelMutationEnabled = panelMutationEnabled; region.callbacks = { activatePanel: vi.fn(), closeSlot: vi.fn(), @@ -144,6 +148,51 @@ describe("chat sidebar region", () => { ); }); + it("gates chat close while keeping non-chat close controls functional", async () => { + const region = await createRegion(false, { chat: false }); + const closeButtons = () => + Array.from( + regionRoot(region).querySelectorAll(".sidebar-column__actions button"), + ); + const closeButton = (panel: string) => + closeButtons().find((button) => button.getAttribute("aria-label") === `Close ${panel}`); + + expect(closeButton("Chat")).toBeUndefined(); + closeButton("Details")?.click(); + closeButton("Discussion")?.click(); + expect(region.callbacks?.closeSlot).toHaveBeenCalledWith("detail"); + expect(region.callbacks?.closeSlot).toHaveBeenCalledWith("discussion"); + + region.panelMutationEnabled = { chat: true }; + await region.updateComplete; + + closeButton("Chat")?.click(); + closeButton("Details")?.click(); + closeButton("Discussion")?.click(); + expect(region.callbacks?.closeSlot).toHaveBeenCalledWith("chat"); + expect(region.callbacks?.closeSlot).toHaveBeenCalledWith("detail"); + expect(region.callbacks?.closeSlot).toHaveBeenCalledWith("discussion"); + }); + + it("gates chat dragging while keeping non-chat panels draggable", async () => { + const region = await createRegion(false, { chat: false }); + const tabs = () => + Array.from(regionRoot(region).querySelectorAll(".sidebar-column__tab")); + const tab = (panel: string) => + tabs().find((candidate) => candidate.textContent?.trim() === panel); + + expect(tab("Chat")?.draggable).toBe(false); + expect(tab("Details")?.draggable).toBe(true); + expect(tab("Discussion")?.draggable).toBe(true); + + region.panelMutationEnabled = { chat: true }; + await region.updateComplete; + + expect(tab("Chat")?.draggable).toBe(true); + expect(tab("Details")?.draggable).toBe(true); + expect(tab("Discussion")?.draggable).toBe(true); + }); + it("routes boundary drops through the detach callback", async () => { const region = await createRegion(false); const source = regionRoot(region).querySelectorAll(".sidebar-column__tab")[1]!;