From 2d5ff4768cd2df5008881ebd5dad765b5baee580 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 8 Aug 2026 04:48:55 -0700 Subject: [PATCH] improve(ui): consolidate dashboard chat visibility into one Chat/Split/Dashboard toggle (#120487) * improve(ui): consolidate dashboard chat visibility into one Chat/Split/Dashboard toggle The dashboard face spread chat visibility across three separate affordances: a two-option face toggle, an icon-only dock dropdown among the header actions, and a floating vertically-rotated "Show chat" button on the board edge that users could not discover. One concept, three controls. The header now renders a single segmented control - Chat | Split | Dashboard - where Split is dashboard plus docked chat and Dashboard is board-only (the former "Hide chat"). A compact caret attached to the pill picks the dock side (left/right/bottom) and only appears in Split mode when dock mutation is allowed; read-only sessions keep the plain two-option Chat | Dashboard toggle. The floating reopen button, the standalone dock menu button, and their CSS (including the mobile overrides) are deleted, so the header ends up with fewer buttons than before. Verified in the mock-gateway dev harness: mode mapping, caret dropdown, unhide-to-remembered-side, and hidden dock rendering board-only. * fix(ci): unexport board dock settings union and repair stale plugin-sdk baseline BoardVisibleChatDock lost its last external consumer when the board view switch consolidated dock controls; keep it file-local so the production unused-export scan stays clean. Also regenerate docs/.generated/plugin-sdk-api-baseline.sha256: the session-catalog module hash was stale on main (check fails on clean origin/main after the agent-attribution reverts), which blocks every PR's check-plugin-sdk-api-baseline job. * docs(web): describe the Chat/Split/Dashboard switch and dock-side picker --- docs/web/dashboard-architecture.md | 9 +- docs/web/dashboards.md | 14 +- ui/src/i18n/locales/en.ts | 3 +- ui/src/lib/board/settings.ts | 4 +- .../pages/chat/board-session-surface.test.ts | 159 +++++++++++++---- ui/src/pages/chat/board-session-surface.ts | 165 ++++++++---------- ui/src/pages/chat/chat-pane-header.ts | 42 +++-- ui/src/pages/chat/chat-pane-render.ts | 2 - .../pages/chat/components/chat-pane-header.ts | 3 +- ui/src/styles/chat/board.css | 83 ++------- ui/src/styles/layout.mobile.css | 12 -- 11 files changed, 273 insertions(+), 223 deletions(-) diff --git a/docs/web/dashboard-architecture.md b/docs/web/dashboard-architecture.md index 025e65cc347e..48529d91e724 100644 --- a/docs/web/dashboard-architecture.md +++ b/docs/web/dashboard-architecture.md @@ -58,10 +58,11 @@ Principles: - **Graduation:** agent calls `show_widget` in any chat → widget renders inline in the transcript exactly as today → hover shows **Pin to dashboard** → widget appears on the session's board. The agent can pass `pin: true` to do the same. -- **Board view:** a session with a board gets a face toggle (Chat / Dashboard). - Board view = tab strip (only when >1 tab) + fluid grid + docked chat pane. - Chat dock is resizable, movable (left/right/bottom), and collapsible exactly - like the sidebar. Per-tab dock state is remembered. +- **Board view:** a session with a board gets a view switch (Chat / Split / + Dashboard). Split = tab strip (only when >1 tab) + fluid grid + docked chat + pane; Dashboard is the same without the chat. The chat dock is resizable and + movable (left/right/bottom) via the switch's dock picker. Per-tab dock state + is remembered. - **Drag:** user drags widgets; grid auto-compacts (widgets float up, neighbors reflow). Resize by handle snaps to size steps. No pixel placement — for anyone. diff --git a/docs/web/dashboards.md b/docs/web/dashboards.md index 6c0746ce2973..a5493e569a4e 100644 --- a/docs/web/dashboards.md +++ b/docs/web/dashboards.md @@ -8,9 +8,10 @@ title: "Session Dashboards" Every thread in the Control UI has two faces: the conversation you know, and a **dashboard** — a grid of live widgets your agent builds for you. A thread with -no widgets is just chat. The moment a widget is pinned, a **Chat | Dashboard** -toggle appears in the header, and the dashboard becomes the main surface with -your chat docked beside it. +no widgets is just chat. The moment a widget is pinned, a **Chat | Split | +Dashboard** switch appears in the header: Chat is the conversation alone, Split +shows the dashboard with your chat docked beside it, and Dashboard gives the +board the whole surface. There is nothing to set up and no separate app to configure: dashboards are a core feature, owned by the thread, stored with the agent, and they survive @@ -59,9 +60,10 @@ never needs the agent. - **Tabs.** A board can have several pages — say, an overview tab and a focused tab with one big widget. Each tab remembers its own chat-dock position. -- **Docked chat.** On the dashboard face, your conversation docks to the - left, right, or bottom, resizes like the sidebar, and can be hidden - entirely — the agent still hears you when you bring it back. +- **Docked chat.** In Split, your conversation docks to the left, right, or + bottom — pick the side from the small arrow on the header switch — and + resizes like the sidebar. Choose Dashboard to hide the chat entirely; the + agent still hears you when you bring it back. - **Agent parity.** Everything you can do, the agent can do with its `dashboard` tool: add, update, move, resize, and remove widgets, manage tabs, switch the visible tab, and move or hide the chat dock. Ask "put the diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index a6d0e8c2ba4f..3c73dbe193b2 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -4465,14 +4465,13 @@ export const en: TranslationMap = { board: { faceLabel: "Thread face", chatFace: "Chat", + splitFace: "Split", dashboardFace: "Dashboard", dockMenu: "Chat dock: {dock}", dockLeft: "Dock chat left", dockRight: "Dock chat right", dockBottom: "Dock chat bottom", - dockHidden: "Hide chat", resizeDock: "Resize chat dock", - reopenChat: "Show chat", workboardCard: "Workboard card: {title}, {status}", defaultTab: "Main", mockPlaceholder: "Board view seam · {tabs} tabs · {widgets} widgets", diff --git a/ui/src/lib/board/settings.ts b/ui/src/lib/board/settings.ts index 4718b9448133..159a0181e813 100644 --- a/ui/src/lib/board/settings.ts +++ b/ui/src/lib/board/settings.ts @@ -1,7 +1,9 @@ import type { SessionBoardFace } from "../../../../src/shared/session-types.js"; export type BoardFace = SessionBoardFace; -export type BoardVisibleChatDock = "bottom" | "left" | "right"; +// Persisted-settings union; the render-layer equivalent is VisibleBoardDock +// (chat-pane-shared.ts), derived from the protocol BoardTab shape. +type BoardVisibleChatDock = "bottom" | "left" | "right"; export type BoardSessionView = { activeTabId?: string; diff --git a/ui/src/pages/chat/board-session-surface.test.ts b/ui/src/pages/chat/board-session-surface.test.ts index 6c97f245ae4d..f1a9499e0407 100644 --- a/ui/src/pages/chat/board-session-surface.test.ts +++ b/ui/src/pages/chat/board-session-surface.test.ts @@ -3,9 +3,8 @@ import { html, render } from "lit"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { boardProviderForSession } from "../../lib/board/provider.ts"; -import type { BoardTab } from "../../lib/board/types.ts"; import { installBrowserHistoryIsolation } from "../../test-helpers/browser-history.ts"; -import { renderBoardFaceToggle, renderBoardSessionSurface } from "./board-session-surface.ts"; +import { renderBoardSessionSurface, renderBoardViewSwitch } from "./board-session-surface.ts"; const containers: HTMLElement[] = []; @@ -41,7 +40,6 @@ describe("board session shell", () => { snapshot: provider.snapshot$.value, activeTabId: "main", dock: "right" as const, - reopenDock: "right" as const, dockSize: { height: 300 }, chat: html`
chat
`, divider: html`
`, @@ -53,7 +51,6 @@ describe("board session shell", () => { selectTab: () => {}, }, widgetFrameUrl: (name: string, revision: number) => provider.widgetFrameUrl(name, revision), - onDockChange: () => {}, }; render( @@ -77,35 +74,142 @@ describe("board session shell", () => { expect(unlinked.querySelector("openclaw-workboard-card-chip")).toBeNull(); }); - it("shows the face toggle only when a board exists", () => { - const withoutBoard = createContainer(); - const withBoard = createContainer(); - + it("renders nothing without a board", () => { + const container = createContainer(); render( - renderBoardFaceToggle(false, "chat", () => {}), - withoutBoard, - ); - render( - renderBoardFaceToggle(true, "chat", () => {}), - withBoard, + renderBoardViewSwitch({ + hasBoard: false, + face: "chat", + dock: "right", + canChangeDock: true, + onSelectMode: () => {}, + onDockSideChange: () => {}, + }), + container, ); - expect(withoutBoard.querySelector("wa-radio-group")).toBeNull(); - expect(withBoard.querySelectorAll("wa-radio")).toHaveLength(2); + expect(container.querySelector(".chat-pane__face-switch")).toBeNull(); + expect(container.querySelector("wa-radio-group")).toBeNull(); + expect(container.querySelector("wa-dropdown")).toBeNull(); }); - it("supports keyboard face selection", () => { + it.each([ + ["chat", "left", "chat"], + ["chat", "right", "chat"], + ["chat", "bottom", "chat"], + ["chat", "hidden", "chat"], + ["dashboard", "left", "split"], + ["dashboard", "right", "split"], + ["dashboard", "bottom", "split"], + ["dashboard", "hidden", "dashboard"], + ] as const)("maps %s with a %s dock to the %s mode", (face, dock, activeMode) => { const container = createContainer(); - const onChange = vi.fn(); - render(renderBoardFaceToggle(true, "chat", onChange), container); + render( + renderBoardViewSwitch({ + hasBoard: true, + face, + dock, + canChangeDock: true, + onSelectMode: () => {}, + onDockSideChange: () => {}, + }), + container, + ); + + expect( + [...container.querySelectorAll("wa-radio")].map((radio) => radio.getAttribute("value")), + ).toEqual(["chat", "split", "dashboard"]); + expect( + container.querySelector("wa-radio.settings-segmented__btn--active")?.getAttribute("value"), + ).toBe(activeMode); + }); + + it("falls back to the two face options when dock changes are unavailable", () => { + const container = createContainer(); + render( + renderBoardViewSwitch({ + hasBoard: true, + face: "dashboard", + dock: "right", + canChangeDock: false, + onSelectMode: () => {}, + onDockSideChange: () => {}, + }), + container, + ); + + expect( + [...container.querySelectorAll("wa-radio")].map((radio) => radio.getAttribute("value")), + ).toEqual(["chat", "dashboard"]); + expect( + container.querySelector("wa-radio.settings-segmented__btn--active")?.getAttribute("value"), + ).toBe("dashboard"); + expect(container.querySelector("wa-dropdown")).toBeNull(); + }); + + it.each([ + ["chat", "right", true, false], + ["dashboard", "right", true, true], + ["dashboard", "hidden", true, false], + ["dashboard", "right", false, false], + ] as const)( + "shows the dock caret for face=%s dock=%s canChangeDock=%s: %s", + (face, dock, canChangeDock, hasCaret) => { + const container = createContainer(); + const onDockSideChange = vi.fn(); + render( + renderBoardViewSwitch({ + hasBoard: true, + face, + dock, + canChangeDock, + onSelectMode: () => {}, + onDockSideChange, + }), + container, + ); + + const dropdown = container.querySelector("wa-dropdown.chat-pane__dock-caret"); + expect(dropdown !== null).toBe(hasCaret); + if (!dropdown) { + return; + } + const items = [...dropdown.querySelectorAll("wa-dropdown-item")]; + expect(items.map((item) => item.getAttribute("value"))).toEqual(["left", "right", "bottom"]); + expect(items.find((item) => item.hasAttribute("checked"))?.getAttribute("value")).toBe( + "right", + ); + const left = dropdown.querySelector('wa-dropdown-item[value="left"]'); + Reflect.set(left ?? {}, "value", "left"); + dropdown.dispatchEvent( + new CustomEvent("wa-select", { bubbles: true, detail: { item: left } }), + ); + expect(onDockSideChange).toHaveBeenCalledWith("left"); + }, + ); + + it("emits the selected view mode", () => { + const container = createContainer(); + const onSelectMode = vi.fn(); + render( + renderBoardViewSwitch({ + hasBoard: true, + face: "chat", + dock: "right", + canChangeDock: true, + onSelectMode, + onDockSideChange: () => {}, + }), + container, + ); const group = container.querySelector("wa-radio-group"); if (group) { - group.value = "dashboard"; + group.value = "split"; group.dispatchEvent(new Event("change", { bubbles: true })); } - expect(onChange).toHaveBeenCalledWith("dashboard"); + expect(onSelectMode).toHaveBeenCalledWith("split"); }); it.each(["left", "right", "bottom"] as const)("lays out the %s dock", (dock) => { @@ -116,7 +220,6 @@ describe("board session shell", () => { snapshot: provider.snapshot$.value, activeTabId: "main", dock, - reopenDock: "right", dockSize: { height: 300 }, chat: html`
chat
`, divider: html`
`, @@ -128,7 +231,6 @@ describe("board session shell", () => { selectTab: () => {}, }, widgetFrameUrl: (name, revision) => provider.widgetFrameUrl(name, revision), - onDockChange: () => {}, }), container, ); @@ -139,16 +241,14 @@ describe("board session shell", () => { expect(container.querySelector("openclaw-board-view")).not.toBeNull(); }); - it("collapses hidden chat to a reopen affordance", () => { + it("renders the hidden dock as board-only", () => { const container = createContainer(); const provider = boardProviderForSession("agent:main:main"); - const onDockChange = vi.fn<(dock: BoardTab["chatDock"]) => void>(); render( renderBoardSessionSurface({ snapshot: provider.snapshot$.value, activeTabId: "main", dock: "hidden", - reopenDock: "left", dockSize: { height: 300 }, chat: html`
chat
`, divider: html`
`, @@ -160,16 +260,13 @@ describe("board session shell", () => { selectTab: () => {}, }, widgetFrameUrl: (name, revision) => provider.widgetFrameUrl(name, revision), - onDockChange, }), container, ); expect(container.querySelector("[data-test-chat]")).toBeNull(); expect(container.querySelector(".board-session-surface--dock-hidden")).not.toBeNull(); - const reopen = container.querySelector(".board-session-surface__reopen"); - reopen?.click(); - expect(onDockChange).toHaveBeenCalledWith("left"); + expect(container.querySelector("openclaw-board-view")).not.toBeNull(); }); it("preserves the board while the bottom chat mounts only for that dock", () => { @@ -178,7 +275,6 @@ describe("board session shell", () => { const props = { snapshot: provider.snapshot$.value, activeTabId: "main", - reopenDock: "left" as const, dockSize: { height: 300 }, chat: html`
chat
`, divider: html`
`, @@ -190,7 +286,6 @@ describe("board session shell", () => { selectTab: () => {}, }, widgetFrameUrl: (name: string, revision: number) => provider.widgetFrameUrl(name, revision), - onDockChange: () => {}, }; render(renderBoardSessionSurface({ ...props, dock: "right" }), container); diff --git a/ui/src/pages/chat/board-session-surface.ts b/ui/src/pages/chat/board-session-surface.ts index 1bac543e4188..0be9f07f59ae 100644 --- a/ui/src/pages/chat/board-session-surface.ts +++ b/ui/src/pages/chat/board-session-surface.ts @@ -5,13 +5,14 @@ import { icons } from "../../components/icons.ts"; import { renderSettingsSegmented } from "../../components/settings-ui.ts"; import { t } from "../../i18n/index.ts"; import { isMockBoardEnabled, type BoardViewCallbacks } from "../../lib/board/provider.ts"; -import type { BoardFace, BoardVisibleChatDock } from "../../lib/board/settings.ts"; +import type { BoardFace } from "../../lib/board/settings.ts"; import type { BoardTab } from "../../lib/board/types.ts"; import type { BoardObserverContext, BoardViewSnapshot, BoardWidgetFrameUrl, } from "../../lib/board/view-types.ts"; +import type { VisibleBoardDock } from "./chat-pane-shared.ts"; export type BoardChatDockSize = { height: number; @@ -28,7 +29,6 @@ type BoardSessionSurfaceProps = { observer?: BoardObserverContext; activeTabId: string; dock: BoardTab["chatDock"]; - reopenDock: BoardVisibleChatDock; dockSize: BoardChatDockSize; chat: TemplateResult; divider: TemplateResult; @@ -37,7 +37,6 @@ type BoardSessionSurfaceProps = { callbacks: BoardViewCallbacks; widgetFrameUrl: BoardWidgetFrameUrl; workboardCardChip?: WorkboardCardChipProps | null; - onDockChange: (dock: BoardTab["chatDock"]) => void; }; let boardViewLoad: Promise | null = null; @@ -60,93 +59,94 @@ export async function ensureBoardViewElement(): Promise { return true; } -export function renderBoardFaceToggle( - hasBoard: boolean, - face: BoardFace, - onChange: (face: BoardFace) => void, -) { - if (!hasBoard) { - return nothing; - } - return html` -
- ${renderSettingsSegmented({ - value: face, - ariaLabel: t("chat.board.faceLabel"), - options: [ - { value: "chat", label: t("chat.board.chatFace") }, - { value: "dashboard", label: t("chat.board.dashboardFace") }, - ], - onChange: (value) => onChange(value), - })} -
- `; -} +type BoardViewMode = "chat" | "split" | "dashboard"; -function dockIcon(dock: BoardTab["chatDock"]) { - if (dock === "left") { - return icons.panelLeftOpen; - } - if (dock === "bottom") { - return icons.panelBottomOpen; - } - if (dock === "hidden") { - return icons.eyeOff; - } - return icons.panelRightOpen; -} - -function dockLabel(dock: BoardTab["chatDock"]): string { +function dockLabel(dock: VisibleBoardDock): string { if (dock === "left") { return t("chat.board.dockLeft"); } if (dock === "bottom") { return t("chat.board.dockBottom"); } - if (dock === "hidden") { - return t("chat.board.dockHidden"); - } return t("chat.board.dockRight"); } -export function renderBoardDockMenu( - hasBoard: boolean, - face: BoardFace, - dock: BoardTab["chatDock"], - onChange: (dock: BoardTab["chatDock"]) => void, -) { - if (!hasBoard || face !== "dashboard") { +export function renderBoardViewSwitch(props: { + hasBoard: boolean; + face: BoardFace; + dock: BoardTab["chatDock"]; + canChangeDock: boolean; + onSelectMode: (mode: BoardViewMode) => void; + onDockSideChange: (dock: VisibleBoardDock) => void; +}) { + if (!props.hasBoard) { return nothing; } + + const mode: BoardViewMode = + props.face === "chat" ? "chat" : props.dock === "hidden" ? "dashboard" : "split"; + const segmented = props.canChangeDock + ? renderSettingsSegmented({ + value: mode, + ariaLabel: t("chat.board.faceLabel"), + options: [ + { value: "chat", label: t("chat.board.chatFace") }, + { value: "split", label: t("chat.board.splitFace") }, + { value: "dashboard", label: t("chat.board.dashboardFace") }, + ], + onChange: (value) => props.onSelectMode(value), + }) + : renderSettingsSegmented({ + value: props.face, + ariaLabel: t("chat.board.faceLabel"), + options: [ + { value: "chat", label: t("chat.board.chatFace") }, + { value: "dashboard", label: t("chat.board.dashboardFace") }, + ], + onChange: (value) => props.onSelectMode(value), + }); + const visibleDock = props.dock === "hidden" ? null : props.dock; + const showDockCaret = mode === "split" && props.canChangeDock && visibleDock !== null; + return html` - ) => { - const value = event.detail.item.value; - if (value === "left" || value === "right" || value === "bottom" || value === "hidden") { - onChange(value); - } - }} - > - - ${(["left", "right", "bottom", "hidden"] as const).map( - (candidate) => html` - - ${dockLabel(candidate)} - - `, - )} - +
+ ${segmented} + ${showDockCaret && visibleDock + ? html` + ) => { + const value = event.detail.item.value; + if (value === "left" || value === "right" || value === "bottom") { + props.onDockSideChange(value); + } + }} + > + + ${(["left", "right", "bottom"] as const).map( + (candidate) => html` + + ${dockLabel(candidate)} + + `, + )} + + ` + : nothing} +
`; } @@ -186,17 +186,6 @@ export function renderBoardSessionSurface(props: BoardSessionSurfaceProps) {
${renderBoardView(props)} ${props.dock === "bottom" ? html`${props.divider}${renderChatDock(props)}` : nothing} -
`; } diff --git a/ui/src/pages/chat/chat-pane-header.ts b/ui/src/pages/chat/chat-pane-header.ts index f9ffc69f1b95..f8eff7089571 100644 --- a/ui/src/pages/chat/chat-pane-header.ts +++ b/ui/src/pages/chat/chat-pane-header.ts @@ -19,7 +19,7 @@ import { copyToClipboard } from "../../lib/clipboard.ts"; import { isGatewayMethodAdvertised } from "../../lib/gateway-methods.ts"; import { readSessionMethodAccess } from "../../lib/session-method-access.ts"; import { parseAgentSessionKey } from "../../lib/sessions/session-key.ts"; -import { renderBoardDockMenu, renderBoardFaceToggle } from "./board-session-surface.ts"; +import { renderBoardViewSwitch } from "./board-session-surface.ts"; import { ChatPaneContext } from "./chat-pane-context.ts"; import { headerPlatformByClient } from "./chat-pane-shared.ts"; import { readChatSessionActionAccess } from "./chat-session-action-access.ts"; @@ -60,6 +60,8 @@ export abstract class ChatPaneHeader extends ChatPaneContext { workspaceGit: boolean, ) { const board = this.resolveBoardView(); + const canChangeBoardDock = + board.hasBoard && !board.activeTabReadOnly && board.provider.canMutate; const workspace = resolveChatPaneWorkspace({ session: row, agentWorkspace: row?.worktree ? undefined : agentWorkspace, @@ -193,9 +195,35 @@ export abstract class ChatPaneHeader extends ChatPaneContext { variant="session" >` : nothing, - faceControl: renderBoardFaceToggle(board.hasBoard, board.face, (face) => { - this.syncChatSidebarForDock(face === "dashboard" ? board.dock : "hidden"); - this.persistBoardSessionView({ face }); + faceControl: renderBoardViewSwitch({ + hasBoard: board.hasBoard, + face: board.face, + dock: board.dock, + canChangeDock: canChangeBoardDock, + onSelectMode: (mode) => { + if (!canChangeBoardDock) { + const face = mode === "chat" ? "chat" : "dashboard"; + this.syncChatSidebarForDock(face === "dashboard" ? board.dock : "hidden"); + this.persistBoardSessionView({ face }); + return; + } + if (mode === "chat") { + this.syncChatSidebarForDock("hidden"); + this.persistBoardSessionView({ face: "chat" }); + return; + } + this.persistBoardSessionView({ face: "dashboard" }); + if (mode === "split") { + if (board.dock === "hidden") { + this.handleBoardDockChange(board.reopenDock); + } else { + this.syncChatSidebarForDock(board.dock); + } + } else if (board.dock !== "hidden") { + this.handleBoardDockChange("hidden"); + } + }, + onDockSideChange: (dock) => this.handleBoardDockChange(dock), }), sharingControl: sharingMethodsSupported ? renderChatSessionSharing({ @@ -222,12 +250,6 @@ export abstract class ChatPaneHeader extends ChatPaneContext { row && void this.setSessionMember(row, identityId, member), }) : nothing, - boardDockAction: renderBoardDockMenu( - board.hasBoard && !board.activeTabReadOnly && board.provider.canMutate, - board.face, - board.dock, - (dock) => this.handleBoardDockChange(dock), - ), nativeGateways: this.nativeGateways, gatewaysSnapshot: this.gatewaysSnapshot, onboarding: this.onboarding, diff --git a/ui/src/pages/chat/chat-pane-render.ts b/ui/src/pages/chat/chat-pane-render.ts index ef64d6054402..98140bedbabf 100644 --- a/ui/src/pages/chat/chat-pane-render.ts +++ b/ui/src/pages/chat/chat-pane-render.ts @@ -581,7 +581,6 @@ export class ChatPane extends ChatPaneHeader { }, activeTabId: board.activeTabId, dock: board.dock, - reopenDock: board.reopenDock, dockSize: this.boardChatDockSize, chat, divider: this.renderBoardDivider("bottom"), @@ -601,7 +600,6 @@ export class ChatPane extends ChatPaneHeader { } satisfies BoardViewCallbacks, widgetFrameUrl: (name, revision) => board.provider.widgetFrameUrl(name, revision), workboardCardChip, - onDockChange: (dock) => this.handleBoardDockChange(dock), }) : chat; const discussion = this.buildSessionDiscussionPanel(state, state.sessionKey.trim()); diff --git a/ui/src/pages/chat/components/chat-pane-header.ts b/ui/src/pages/chat/components/chat-pane-header.ts index 2a0983e33fc3..c107ea5ace6b 100644 --- a/ui/src/pages/chat/components/chat-pane-header.ts +++ b/ui/src/pages/chat/components/chat-pane-header.ts @@ -52,7 +52,6 @@ type ChatPaneHeaderProps = { presence?: TemplateResult | typeof nothing; faceControl?: TemplateResult | typeof nothing; sharingControl?: TemplateResult | typeof nothing; - boardDockAction?: TemplateResult | typeof nothing; nativeGateways?: NativeGatewaysCapability | null; gatewaysSnapshot?: NativeGatewaysSnapshot | null; onboarding?: boolean; @@ -432,7 +431,7 @@ export function renderChatPaneHeader(props: ChatPaneHeaderProps) { : nothing} ${renderGatewayPicker(props)}
- ${props.boardDockAction ?? nothing} ${props.terminalAction} ${props.discussionAction} + ${props.terminalAction} ${props.discussionAction} ${props.catalog ? nothing : html`${props.diffAction} ${props.backgroundTasksAction} ${props.workspaceAction}`} diff --git a/ui/src/styles/chat/board.css b/ui/src/styles/chat/board.css index 8382c5d68697..8afd26dd94dc 100644 --- a/ui/src/styles/chat/board.css +++ b/ui/src/styles/chat/board.css @@ -3,12 +3,18 @@ flex: 0 0 auto; align-items: center; margin-left: auto; + border-radius: var(--radius-full); + background: color-mix(in srgb, var(--panel-strong) 76%, transparent); } .chat-pane__face-switch .settings-segmented { flex-wrap: nowrap; border-radius: var(--radius-full); - background: color-mix(in srgb, var(--panel-strong) 76%, transparent); + background: transparent; +} + +.chat-pane__face-switch--split .settings-segmented { + border-radius: var(--radius-full) 0 0 var(--radius-full); } .chat-pane__face-switch .settings-segmented__btn { @@ -25,12 +31,21 @@ margin-left: 0; } -.chat-pane__board-dock-menu { +.chat-pane__dock-caret { display: inline-flex; } -.chat-pane__board-dock-menu svg, -.board-session-surface__reopen svg { +.chat-pane__dock-caret-trigger { + width: 24px; + min-width: 24px; + height: 24px; + min-height: 24px; + padding: 0; + border-left: 1px solid color-mix(in srgb, var(--border) 72%, transparent); + border-radius: 0 var(--radius-full) var(--radius-full) 0; +} + +.chat-pane__dock-caret svg { width: 15px; height: 15px; fill: none; @@ -163,66 +178,6 @@ openclaw-workboard-card-chip { z-index: 2; } -.board-session-surface__reopen[hidden] { - display: none; -} - -.board-session-surface__reopen { - position: absolute; - z-index: 3; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - min-width: 34px; - min-height: 34px; - padding: 7px; - border: 1px solid var(--border); - border-radius: var(--radius-md); - color: var(--muted); - background: color-mix(in srgb, var(--panel-strong) 92%, transparent); - box-shadow: var(--shadow-sm); - cursor: pointer; -} - -.board-session-surface__reopen:hover, -.board-session-surface__reopen:focus-visible { - color: var(--text); - border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); -} - -.board-session-surface__reopen:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; -} - -.board-session-surface__reopen span { - font-size: 11px; - font-weight: 600; - writing-mode: vertical-rl; -} - -.board-session-surface__reopen--left { - top: 50%; - left: 8px; - transform: translateY(-50%); -} - -.board-session-surface__reopen--right { - top: 50%; - right: 8px; - transform: translateY(-50%); -} - -.board-session-surface__reopen--bottom { - right: 12px; - bottom: 8px; -} - -.board-session-surface__reopen--bottom span { - writing-mode: horizontal-tb; -} - @media (max-width: 560px) { .chat-pane__face-switch .settings-segmented__btn { padding-inline: 7px; diff --git a/ui/src/styles/layout.mobile.css b/ui/src/styles/layout.mobile.css index 23cf82a4c37f..db5df80d880b 100644 --- a/ui/src/styles/layout.mobile.css +++ b/ui/src/styles/layout.mobile.css @@ -148,18 +148,6 @@ html:not(.openclaw-native-macos):not(.openclaw-native-nav):not(.openclaw-native- box-shadow: 0 -12px 32px color-mix(in srgb, black 16%, transparent); } -.shell--mobile-nav .board-session-surface__reopen { - top: auto; - right: 12px; - bottom: 8px; - left: auto; - transform: none; -} - -.shell--mobile-nav .board-session-surface__reopen span { - writing-mode: horizontal-tb; -} - .shell--mobile-nav .sidebar-resizer { display: none; }