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