From 2e5def4b9722b41a41f85965bf78f2bab1a1f964 Mon Sep 17 00:00:00 2001 From: Karen Lai <7976322+karkarl@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:50:55 -0700 Subject: [PATCH 01/32] Seed v3 Colophon design system for OpenClaw Windows Hub Carry over the v1 design.json tokens/brand/authority and principles.md verbatim, port all 9 components (Button, Field, Card, Badge, ComposerPicker, ChatBubble, ChatComposer, ChatThread, ExampleScreen) from raw-CSS attrs.style to v3 Auto Layout with token-bound layout/appearance, and add a v3 colophon.jsonc presentation profile. Adds the idempotent Colophon pointer block to AGENTS.md. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .agents/design/colophon.jsonc | 60 ++++ .agents/design/components.jsonc | 471 ++++++++++++++++++++++++++++++++ .agents/design/design.json | 114 ++++++++ .agents/design/principles.md | 55 ++++ AGENTS.md | 23 ++ 5 files changed, 723 insertions(+) create mode 100644 .agents/design/colophon.jsonc create mode 100644 .agents/design/components.jsonc create mode 100644 .agents/design/design.json create mode 100644 .agents/design/principles.md diff --git a/.agents/design/colophon.jsonc b/.agents/design/colophon.jsonc new file mode 100644 index 0000000000..14e0dffbb7 --- /dev/null +++ b/.agents/design/colophon.jsonc @@ -0,0 +1,60 @@ +{ + "$schema": "https://raw.githubusercontent.com/karkarl/colophon/main/extensions/colophon/colophon.schema.json", + "schemaVersion": 1, + "extends": "colophon/default", + "brand": { + "name": "Windows Fluent UI for OpenClaw", + "tagline": "A calm native Windows companion for your AI assistant." + }, + "theme": { + "light": { + "colors": { + "canvas": { "$token": "paper" }, + "sidebar": { "$token": "surface" }, + "foreground": { "$token": "ink" }, + "muted": { "$token": "muted" }, + "border": { "$token": "line" }, + "accent": { "$token": "accent" }, + "accentForeground": { "$token": "accentInk" } + }, + "typography": { + "body": { "$token": "body" }, + "heading": { "$token": "display" }, + "mono": { "$token": "mono" } + }, + "shape": { + "control": { "$token": "sm" }, + "panel": { "$token": "lg" } + } + }, + "dark": { + "colors": { + "canvas": { "$token": "paper", "mode": "dark" }, + "sidebar": { "$token": "surface", "mode": "dark" }, + "foreground": { "$token": "ink", "mode": "dark" }, + "muted": { "$token": "muted", "mode": "dark" }, + "border": { "$token": "line", "mode": "dark" }, + "accent": { "$token": "accent", "mode": "dark" }, + "accentForeground": { "$token": "accentInk", "mode": "dark" } + } + } + }, + "navigation": { + "sections": [ + { "id": "brand", "label": "Brand", "target": "brand" }, + { "id": "components", "label": "Components", "target": "components" } + ], + "components": { + "presentation": "flat-list", + "searchable": true + } + }, + "templates": { + "components": { + "tabs": ["spec", "usage", "demo"], + "layout": "stack", + "tableOfContents": false, + "modeComparison": "light-dark" + } + } +} diff --git a/.agents/design/components.jsonc b/.agents/design/components.jsonc new file mode 100644 index 0000000000..20d898f831 --- /dev/null +++ b/.agents/design/components.jsonc @@ -0,0 +1,471 @@ +// components.jsonc — the UI patterns the OpenClaw Windows team has decided on, as data. +// +// This is a framework-agnostic relational element tree with token-bound Auto Layout +// and optional parent-relative freeform positioning. It renders live in the Colophon +// and Prototype canvases and ports cleanly because layout intent is semantic, not +// executable CSS. This is DESIGN INTENT for the canvas preview — not shipping code. +// +// To ship, port these patterns into the app using the port targets in design.json +// `authority`: most surfaces ship as native WinUI 3 / C# (port via +// github.com/microsoft/win-dev-skills), while the chat surface (ChatBubble, +// ChatComposer, ChatThread) ships via Reactor (github.com/microsoft/microsoft-ui-reactor). +// Don't ship this verbatim; bind Fluent ThemeResource keys, never the preview hex. +// +// Every object node has a stable, unique `id`. Node kinds: +// element { "id", "el", "class"?, "attrs"?, "layout"?, "position"?, "margin"?, "appearance"?, "children"? } +// component { "id", "component", "props"?, "layout"?, "position"?, "margin"?, "appearance"? } +// string "literal" or "{prop}" for prop interpolation +// +// Auto Layout: layout.mode vertical|horizontal|grid|freeform|none; gap/padding/margin +// reference design.json spacing tokens ("1".."8"), never raw px; align/justify/columns/ +// wrap/grow; width/height "fill"|"hug"|number. Freeform (v3): a layout.mode "freeform" +// parent establishes a coordinate system; direct children may set +// position { mode:"absolute", x, y } in px relative to that parent. +// +// Appearance inherits from the parent/class; add only intentional, token-bound +// overrides: fontFamily display|body|mono; textStyle (typography scale name); +// color/background/borderColor (color-token name); radius/shadow (token name); textAlign. +{ + "meta": { + "version": 3, + "updatedBy": "copilot", + "note": "Windows Fluent UI for OpenClaw — ported from the v1 raw-CSS components.jsonc to v3 Auto Layout. WinUI-derived pixel padding is snapped to the nearest 4px spacing token; per-component notes record the source values and any approximation." + }, + "components": [ + { + "name": "Button", + "note": "Primary action. One per view — it's where the eye should land. Padding mirrors WinUI's default ButtonPadding (11,5,11,6), snapped to spacing tokens 3 (12≈11) and 1 (4≈5/6). In-app MinWidth 100px keeps short labels (OK / Connect) from cramping and gives side-by-side buttons a consistent footprint; that minimum has no spacing-token equivalent, so it stays a port note and the preview hugs its label.", + "props": { "children": "Save changes", "variant": "primary" }, + "root": { + "id": "button-root", + "el": "button", + "class": "ds-btn ds-btn-{variant}", + "layout": { + "mode": "horizontal", + "padding": { "x": "3", "y": "1" }, + "align": "center", + "justify": "center", + "width": "hug" + }, + "children": ["{children}"] + } + }, + { + "name": "Field", + "note": "Text input with a quiet label above it — the field is the subject. Input padding mirrors WinUI TextControlThemePadding (10,5,6,6), snapped to tokens 2 (8≈10/6 horizontal) and 1 (4≈5/6 vertical). In-app MinHeight 32px lines a field up with a Button on the same row; that minimum stays a port note.", + "props": { "label": "Gateway address", "placeholder": "127.0.0.1:18789" }, + "root": { + "id": "field-root", + "el": "label", + "class": "ds-field", + "layout": { "mode": "vertical", "gap": "2", "width": "fill" }, + "children": [ + { + "id": "field-label", + "el": "span", + "class": "ds-field-label", + "children": ["{label}"] + }, + { + "id": "field-input", + "el": "input", + "class": "ds-input", + "attrs": { "placeholder": "{placeholder}" }, + "layout": { "padding": { "x": "2", "y": "1" }, "width": "fill" } + } + ] + } + }, + { + "name": "Card", + "note": "Content card. Flat by default — a 1px hairline border, not a drop shadow. Padding mirrors the WinUI Gallery card (16,12) as tokens 4 (16) and 3 (12) exactly, with the `md` radius and a `line`-colored border supplied by the ds-card class. Never nest a card inside a card.", + "props": { "title": "Windows node", "body": "Paired · 6 capabilities exposed over MCP." }, + "root": { + "id": "card-root", + "el": "article", + "class": "ds-card", + "layout": { + "mode": "vertical", + "gap": "2", + "padding": { "x": "4", "y": "3" }, + "width": "fill" + }, + "children": [ + { + "id": "card-title", + "el": "h3", + "class": "ds-card-title", + "children": ["{title}"] + }, + { + "id": "card-body", + "el": "p", + "class": "ds-card-body", + "children": ["{body}"] + }, + { + "id": "card-link", + "el": "a", + "class": "ds-link", + "attrs": { "href": "#" }, + "margin": { "top": "1" }, + "children": ["Open report →"] + } + ] + } + }, + { + "name": "Badge", + "note": "Status pill. Uses semantic color tokens, never a raw hex. Padding tokens 3/1 give the pill a compact footprint; the `pill` radius and tone color come from the ds-badge class.", + "props": { "children": "Connected", "tone": "positive" }, + "root": { + "id": "badge-root", + "el": "span", + "class": "ds-badge ds-badge-{tone}", + "layout": { + "mode": "horizontal", + "padding": { "x": "3", "y": "1" }, + "align": "center", + "width": "hug" + }, + "children": ["{children}"] + } + }, + { + "name": "ComposerPicker", + "note": "Subtle inline picker — text label + chevron, no border until hover in the app. Used for the model and reasoning-effort selectors; reads as a quiet dropdown, not a button. 32px control height with `small` (13/18) muted text and the `sm` radius. (Preview shows the rest state; the transparent rest border is intentionally omitted rather than bound to a token.)", + "props": { "label": "Model" }, + "root": { + "id": "composer-picker-root", + "el": "button", + "attrs": { "type": "button" }, + "layout": { + "mode": "horizontal", + "gap": "1", + "padding": { "x": "2" }, + "align": "center", + "height": 32, + "width": "hug" + }, + "appearance": { + "fontFamily": "body", + "textStyle": "small", + "color": "muted", + "radius": "sm" + }, + "children": [ + { + "id": "composer-picker-label", + "el": "span", + "children": ["{label}"] + }, + { + "id": "composer-picker-chevron", + "el": "svg", + "attrs": { "width": "12", "height": "12", "viewBox": "0 0 24 24", "fill": "none", "aria-hidden": "true", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, + "children": [ + { "id": "composer-picker-chevron-path", "el": "path", "attrs": { "d": "M6 9l6 6 6-6" } } + ] + } + ] + } + }, + { + "name": "ChatBubble", + "note": "Assistant chat bubble: a 36×36 avatar (subtle `surface` fill, hairline `line` border, `pill` radius) to the LEFT of a bubble on a `surface` fill with a `line` border. The `bubble` radius (16px) is friendlier than the 8/12px control radii; padding tokens 4/3 mirror the v1 12/16. Footer reads timestamp · usage · Copy — the same used/context (pct%) string ChatUsageFormatter produces. Mirrors OpenClawChatTimeline; ships via Reactor. Bubble max-width min(80%, 520px) is a Reactor port note (no token equivalent). (User messages ride the accent on the right in-app; this previews the assistant variant.)", + "props": { + "time": "2:14 PM", + "usage": "12.5K/200K (6%)", + "children": "Paired to the gateway — 6 Windows node capabilities are live." + }, + "root": { + "id": "chat-bubble-root", + "el": "div", + "layout": { "mode": "horizontal", "gap": "2", "align": "start" }, + "children": [ + { + "id": "chat-bubble-avatar", + "el": "div", + "attrs": { "aria-hidden": "true" }, + "layout": { "mode": "horizontal", "align": "center", "justify": "center", "width": 36, "height": 36 }, + "appearance": { "background": "surface", "borderColor": "line", "color": "muted", "radius": "pill" }, + "children": [ + { + "id": "chat-bubble-avatar-glyph", + "el": "svg", + "attrs": { "width": "18", "height": "18", "viewBox": "0 0 24 24", "fill": "currentColor", "aria-hidden": "true" }, + "children": [ + { "id": "chat-bubble-avatar-path", "el": "path", "attrs": { "d": "M12 2.5l1.7 5.1a4 4 0 0 0 2.7 2.7l5.1 1.7-5.1 1.7a4 4 0 0 0-2.7 2.7L12 21.5l-1.7-5.1a4 4 0 0 0-2.7-2.7L2.5 12l5.1-1.7a4 4 0 0 0 2.7-2.7L12 2.5z" } } + ] + } + ] + }, + { + "id": "chat-bubble-stack", + "el": "div", + "layout": { "mode": "vertical", "gap": "1", "align": "start" }, + "children": [ + { + "id": "chat-bubble-message", + "el": "div", + "layout": { "mode": "vertical", "padding": { "x": "4", "y": "3" } }, + "appearance": { "fontFamily": "body", "textStyle": "body", "background": "surface", "color": "ink", "borderColor": "line", "radius": "bubble" }, + "children": ["{children}"] + }, + { + "id": "chat-bubble-footer", + "el": "div", + "layout": { "mode": "horizontal", "gap": "2", "padding": { "x": "2" }, "align": "center" }, + "appearance": { "fontFamily": "body", "textStyle": "caption", "color": "muted" }, + "children": [ + { "id": "chat-bubble-time", "el": "span", "children": ["{time}"] }, + { "id": "chat-bubble-dot", "el": "span", "attrs": { "aria-hidden": "true" }, "children": ["·"] }, + { "id": "chat-bubble-usage", "el": "span", "children": ["{usage}"] }, + { + "id": "chat-bubble-copy", + "el": "button", + "attrs": { "type": "button", "title": "Copy", "aria-label": "Copy message" }, + "layout": { "mode": "horizontal", "align": "center", "justify": "center", "width": 20, "height": 20 }, + "appearance": { "color": "muted", "radius": "sm" }, + "children": [ + { + "id": "chat-bubble-copy-glyph", + "el": "svg", + "attrs": { "width": "14", "height": "14", "viewBox": "0 0 24 24", "fill": "none", "aria-hidden": "true", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, + "children": [ + { "id": "chat-bubble-copy-rect", "el": "rect", "attrs": { "x": "9", "y": "9", "width": "11", "height": "11", "rx": "2" } }, + { "id": "chat-bubble-copy-path", "el": "path", "attrs": { "d": "M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1" } } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + { + "name": "ChatComposer", + "note": "Chat input. A rounded `surface` panel (`md` radius) with a hairline `line` border wrapping a borderless, transparent, auto-growing textarea over a toolbar. Left cluster = Add, model picker, reasoning-effort picker; right cluster = mic + Send. Only Send spends the accent; every other control stays subtle so the toolbar reads as quiet chrome. Textarea min 56 / max 200px growth is a Reactor port note. (Preview shows the rest state.)", + "props": { + "placeholder": "Message OpenClaw…", + "model": "Claude Opus 4.8", + "effort": "High" + }, + "root": { + "id": "chat-composer-root", + "el": "div", + "layout": { "mode": "vertical", "gap": "2", "padding": "2", "width": "fill" }, + "appearance": { "background": "surface", "borderColor": "line", "radius": "md" }, + "children": [ + { + "id": "chat-composer-input", + "el": "textarea", + "attrs": { "rows": "2", "placeholder": "{placeholder}" }, + "layout": { "padding": "2", "width": "fill" }, + "appearance": { "fontFamily": "body", "textStyle": "body", "color": "ink" } + }, + { + "id": "chat-composer-toolbar", + "el": "div", + "layout": { "mode": "horizontal", "gap": "2", "align": "center", "justify": "space-between", "width": "fill" }, + "children": [ + { + "id": "chat-composer-left", + "el": "div", + "layout": { "mode": "horizontal", "gap": "1", "align": "center" }, + "children": [ + { + "id": "chat-composer-add", + "el": "button", + "attrs": { "type": "button", "title": "Add context", "aria-label": "Add context" }, + "layout": { "mode": "horizontal", "align": "center", "justify": "center", "width": 32, "height": 32 }, + "appearance": { "color": "muted", "radius": "sm" }, + "children": [ + { + "id": "chat-composer-add-glyph", + "el": "svg", + "attrs": { "width": "18", "height": "18", "viewBox": "0 0 24 24", "fill": "none", "aria-hidden": "true", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, + "children": [ + { "id": "chat-composer-add-path", "el": "path", "attrs": { "d": "M12 5v14M5 12h14" } } + ] + } + ] + }, + { "id": "chat-composer-model", "component": "ComposerPicker", "props": { "label": "{model}" } }, + { "id": "chat-composer-effort", "component": "ComposerPicker", "props": { "label": "{effort}" } } + ] + }, + { + "id": "chat-composer-right", + "el": "div", + "layout": { "mode": "horizontal", "gap": "1", "align": "center" }, + "children": [ + { + "id": "chat-composer-mic", + "el": "button", + "attrs": { "type": "button", "title": "Dictate", "aria-label": "Dictate" }, + "layout": { "mode": "horizontal", "align": "center", "justify": "center", "width": 32, "height": 32 }, + "appearance": { "color": "muted", "radius": "sm" }, + "children": [ + { + "id": "chat-composer-mic-glyph", + "el": "svg", + "attrs": { "width": "18", "height": "18", "viewBox": "0 0 24 24", "fill": "none", "aria-hidden": "true", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, + "children": [ + { "id": "chat-composer-mic-rect", "el": "rect", "attrs": { "x": "9", "y": "3", "width": "6", "height": "11", "rx": "3" } }, + { "id": "chat-composer-mic-path", "el": "path", "attrs": { "d": "M5 11a7 7 0 0 0 14 0M12 18v3" } } + ] + } + ] + }, + { + "id": "chat-composer-send", + "el": "button", + "attrs": { "type": "button", "title": "Send", "aria-label": "Send" }, + "layout": { "mode": "horizontal", "align": "center", "justify": "center", "width": 32, "height": 32 }, + "appearance": { "background": "accent", "color": "accentInk", "radius": "sm" }, + "children": [ + { + "id": "chat-composer-send-glyph", + "el": "svg", + "attrs": { "width": "18", "height": "18", "viewBox": "0 0 24 24", "fill": "none", "aria-hidden": "true", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, + "children": [ + { "id": "chat-composer-send-path", "el": "path", "attrs": { "d": "M12 19V5M5 12l7-7 7 7" } } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + { + "name": "ChatThread", + "note": "A short thread: the \"does chat hang together\" check — a user turn (accent bubble on the right) and an assistant turn (with its Copy affordance + context-usage readout), then the composer. Thread max-width 560px is a Reactor port note.", + "root": { + "id": "chat-thread-root", + "el": "div", + "layout": { "mode": "vertical", "gap": "3", "width": "fill" }, + "children": [ + { + "id": "chat-thread-user", + "el": "div", + "layout": { "mode": "horizontal", "justify": "end", "width": "fill" }, + "children": [ + { + "id": "chat-thread-user-stack", + "el": "div", + "layout": { "mode": "vertical", "gap": "1", "align": "end" }, + "children": [ + { + "id": "chat-thread-user-bubble", + "el": "div", + "layout": { "mode": "vertical", "padding": { "x": "4", "y": "3" } }, + "appearance": { "fontFamily": "body", "textStyle": "body", "background": "accent", "color": "accentInk", "radius": "bubble" }, + "children": ["Can you list the Windows node capabilities?"] + }, + { + "id": "chat-thread-user-footer", + "el": "div", + "layout": { "mode": "horizontal", "gap": "2", "padding": { "x": "2" }, "align": "center" }, + "appearance": { "fontFamily": "body", "textStyle": "caption", "color": "muted" }, + "children": [ + { "id": "chat-thread-user-time", "el": "span", "children": ["2:14 PM"] } + ] + } + ] + } + ] + }, + { + "id": "chat-thread-assistant", + "component": "ChatBubble", + "props": { "time": "2:14 PM", "usage": "12.5K/200K (6%)", "children": "Paired to the gateway — 6 capabilities are live, including system.run and clipboard access." } + }, + { "id": "chat-thread-composer", "component": "ChatComposer" } + ] + } + }, + { + "name": "ExampleScreen", + "note": "A representative Connection screen composed from the pieces above — the \"does it hang together\" check for the whole system. The status board is a freeform layer so the two cards can overlap into a Fluent-style dashboard, exercising v3 absolute positioning alongside Auto Layout.", + "root": { + "id": "example-screen-root", + "el": "section", + "class": "ds-screen", + "layout": { "mode": "vertical", "gap": "5", "width": "fill" }, + "children": [ + { + "id": "example-screen-head", + "el": "header", + "class": "ds-screen-head", + "layout": { "mode": "horizontal", "gap": "4", "align": "start", "justify": "space-between", "width": "fill" }, + "children": [ + { + "id": "example-screen-heading", + "el": "div", + "layout": { "mode": "vertical", "gap": "1" }, + "children": [ + { "id": "example-screen-eyebrow", "el": "p", "class": "ds-eyebrow", "children": ["Connection"] }, + { "id": "example-screen-title", "el": "h2", "class": "ds-screen-title", "children": ["OpenClaw Windows Hub"] } + ] + }, + { + "id": "example-screen-status", + "component": "Badge", + "props": { "tone": "positive", "children": "Connected" } + } + ] + }, + { + "id": "example-screen-board", + "el": "div", + "class": "ds-grid", + "layout": { "mode": "freeform", "width": "fill", "height": 176 }, + "children": [ + { + "id": "example-screen-gateway", + "component": "Card", + "position": { "mode": "absolute", "x": 0, "y": 0 }, + "props": { "title": "Gateway", "body": "Paired to OpenClawGateway · last seen just now." }, + "layout": { "width": 280, "height": 148 } + }, + { + "id": "example-screen-node", + "component": "Card", + "position": { "mode": "absolute", "x": 248, "y": 28 }, + "props": { "title": "Windows node", "body": "6 capabilities exposed over MCP." }, + "layout": { "width": 280, "height": 148 } + } + ] + }, + { + "id": "example-screen-row", + "el": "div", + "class": "ds-row", + "layout": { "mode": "horizontal", "gap": "4", "align": "end", "wrap": true, "width": "fill" }, + "children": [ + { + "id": "example-screen-field", + "component": "Field", + "props": { "label": "Gateway address", "placeholder": "127.0.0.1:18789" }, + "layout": { "grow": true } + }, + { + "id": "example-screen-connect", + "component": "Button", + "props": { "children": "Connect", "variant": "primary" } + } + ] + } + ] + } + } + ] +} diff --git a/.agents/design/design.json b/.agents/design/design.json new file mode 100644 index 0000000000..76551ef296 --- /dev/null +++ b/.agents/design/design.json @@ -0,0 +1,114 @@ +{ + "$schema": "https://agents.design/schema/v1", + "meta": { + "version": 1, + "updatedBy": "xaml+manual", + "note": "These tokens are the source of truth for DESIGN (brand, color, type, spacing, principles) and are framework-agnostic — components.jsx shows design intent for the canvas preview, not shipping code. They were captured by manual inspection of the repo's WinUI 3 / XAML and C# surfaces (the CSS/JSX scanner finds no files in a XAML repo) and map to Windows Fluent system resources (SystemAccentColor, Mica, Segoe UI Variable). To ship, port the design into native code via the port targets in `authority`. Edit in the Design System canvas or directly." + }, + "authority": { + "designSource": "self", + "owner": "OpenClaw Windows (openclaw/openclaw-windows-node)", + "syncProcess": "Tokens are captured by manual inspection of the app's WinUI 3 XAML/C# Fluent resources; re-inspect and update this file when the app's ThemeResource mappings or SystemAccentColor usage change.", + "port": { + "authoritySource": "Native WinUI 3 / C# (Windows Fluent)", + "syncSource": "https://github.com/microsoft/win-dev-skills", + "helperAgent": "win-dev-skills" + }, + "portOverrides": [ + { + "area": "chat", + "components": ["ChatBubble", "ChatComposer", "ChatThread"], + "authoritySource": "Reactor (Microsoft.UI.Reactor)", + "syncSource": "https://github.com/microsoft/microsoft-ui-reactor", + "helperAgent": "" + } + ] + }, + "brand": { + "name": "Windows Fluent UI for OpenClaw", + "tagline": "A calm native Windows companion for your AI assistant.", + "description": "Windows Fluent UI for OpenClaw is the design system for OpenClaw's native Windows companion suite — a WinUI 3 system-tray app that connects your PC to OpenClaw, the AI-powered personal assistant. The app pairs your PC to an OpenClaw gateway, exposes allowlisted Windows node capabilities over MCP, and guides first-run pairing through a setup wizard. Its interface follows Windows Fluent design conventions — Mica surfaces, the user's system accent, and Segoe UI Variable type — so it feels like a quiet, native extension of the OS rather than a bolted-on dashboard.", + "surface": "product", + "voice": "Clear, respectful, and native to Windows. Explain pairing, permissions, and security plainly. State what will happen and why; never alarmist, never hype.", + "personality": ["native", "trustworthy", "calm", "precise"], + "antiReferences": [ + "custom-skinned controls that fight the OS theme", + "neon gradients and glassmorphism overload", + "ignoring the user's system accent or light/dark preference", + "cramming a web dashboard into a desktop window", + "hard-coded hex that breaks in dark or high-contrast mode" + ] + }, + "colors": [ + { "name": "ink", "value": "#1b1b1b", "resource": "TextFillColorPrimaryBrush", "themes": { "dark": "#ffffff", "highContrast": "#ffffff" }, "usage": "Primary text, headings. Fluent TextFillColorPrimary on light." }, + { "name": "paper", "value": "#f3f3f3", "resource": "ApplicationPageBackgroundThemeBrush", "themes": { "dark": "#202020", "highContrast": "#000000" }, "usage": "App / page background. Mica base / ApplicationPageBackground (light)." }, + { "name": "surface", "value": "#fbfbfb", "resource": "CardBackgroundFillColorDefaultBrush", "themes": { "dark": "#2b2b2b", "highContrast": "#000000" }, "usage": "Cards, flyouts, inputs. Fluent CardBackgroundFillColorDefault." }, + { "name": "muted", "value": "#5d5d5d", "resource": "TextFillColorSecondaryBrush", "themes": { "dark": "#c8c8c8", "highContrast": "#ffffff" }, "usage": "Secondary text, captions. Fluent TextFillColorSecondary/Tertiary." }, + { "name": "line", "value": "#e5e5e5", "resource": "CardStrokeColorDefaultBrush", "themes": { "dark": "#303030", "highContrast": "#ffffff" }, "usage": "Borders, dividers. Fluent CardStrokeColorDefault / DividerStroke." }, + { "name": "accent", "value": "#0067c0", "resource": "AccentFillColorDefaultBrush", "themes": { "dark": "#4cc2ff", "highContrast": "#ffff00" }, "usage": "Primary actions, links, selection, focus. Follows SystemAccentColor (default AccentDark1 on light)." }, + { "name": "accentInk", "value": "#ffffff", "resource": "TextOnAccentFillColorPrimaryBrush", "themes": { "dark": "#000000", "highContrast": "#000000" }, "usage": "Text/glyphs on accent fills." }, + { "name": "positive", "value": "#0f7b0f", "resource": "SystemFillColorSuccessBrush", "themes": { "dark": "#6ccb5f", "highContrast": "#3ff23f" }, "usage": "Success / connected states. Fluent SystemFillColorSuccess." }, + { "name": "warning", "value": "#9d5d00", "resource": "SystemFillColorCautionBrush", "themes": { "dark": "#fcd116", "highContrast": "#ffb000" }, "usage": "Warnings / attention. Fluent SystemFillColorCaution." }, + { "name": "critical", "value": "#c42b1c", "resource": "SystemFillColorCriticalBrush", "themes": { "dark": "#ff99a4", "highContrast": "#ff5449" }, "usage": "Errors, disconnected, destructive. Fluent SystemFillColorCritical." }, + { "name": "subtleHover", "value": "rgba(0,0,0,0.037)", "resource": "SubtleFillColorSecondaryBrush", "themes": { "dark": "rgba(255,255,255,0.0605)", "highContrast": "transparent" }, "usage": "Hover fill for subtle/transparent controls (icon buttons, inline pickers). Fluent SubtleFillColorSecondary (light #09000000)." }, + { "name": "subtlePressed", "value": "rgba(0,0,0,0.024)", "resource": "SubtleFillColorTertiaryBrush", "themes": { "dark": "rgba(255,255,255,0.0419)", "highContrast": "transparent" }, "usage": "Pressed fill for subtle/transparent controls. Fluent SubtleFillColorTertiary (light #06000000)." } + ], + "typography": { + "display": { + "family": "\"Segoe UI Variable Display\", \"Segoe UI Variable\", \"Segoe UI\", system-ui, sans-serif", + "weights": [400, 600], + "usage": "Page and section titles. Segoe UI Variable Display optical size." + }, + "body": { + "family": "\"Segoe UI Variable Text\", \"Segoe UI Variable\", \"Segoe UI\", system-ui, sans-serif", + "weights": [400, 600], + "usage": "Body copy, UI labels, buttons. Segoe UI Variable Text optical size." + }, + "mono": { + "family": "\"Cascadia Mono\", \"Cascadia Code\", Consolas, \"SFMono-Regular\", monospace", + "weights": [400], + "usage": "Commands, endpoints, tokens, logs, timestamps. Used heavily across setup and diagnostics surfaces." + }, + "scale": [ + { "name": "display", "size": "40px", "lineHeight": "50px", "weight": 600, "role": "display", "tracking": "-0.01em" }, + { "name": "title", "size": "28px", "lineHeight": "36px", "weight": 600, "role": "display" }, + { "name": "heading", "size": "20px", "lineHeight": "28px", "weight": 600, "role": "body" }, + { "name": "body", "size": "14px", "lineHeight": "20px", "weight": 400, "role": "body" }, + { "name": "small", "size": "13px", "lineHeight": "18px", "weight": 400, "role": "body" }, + { "name": "caption", "size": "12px", "lineHeight": "16px", "weight": 400, "role": "body" } + ] + }, + "spacing": { + "unit": 4, + "scale": [ + { "name": "1", "value": "4px" }, + { "name": "2", "value": "8px" }, + { "name": "3", "value": "12px" }, + { "name": "4", "value": "16px" }, + { "name": "5", "value": "24px" }, + { "name": "6", "value": "32px" }, + { "name": "7", "value": "48px" }, + { "name": "8", "value": "64px" } + ] + }, + "radii": [ + { "name": "sm", "value": "4px", "usage": "Buttons, inputs, small controls. Fluent ControlCornerRadius." }, + { "name": "md", "value": "8px", "usage": "Cards, dialogs, flyouts. Fluent OverlayCornerRadius." }, + { "name": "lg", "value": "12px", "usage": "Large panels and grouped surfaces." }, + { "name": "bubble", "value": "16px", "usage": "Chat message bubbles — intentionally friendlier than control radii. Mirrors OpenClawChatTimeline CornerRadius 16." }, + { "name": "pill", "value": "999px", "usage": "Status pills, badges, toggles." } + ], + "shadows": [ + { "name": "sm", "value": "0 1px 2px rgba(0,0,0,0.10)" }, + { "name": "md", "value": "0 4px 16px rgba(0,0,0,0.12)" }, + { "name": "lg", "value": "0 12px 40px rgba(0,0,0,0.16)" } + ], + "principles": [ + "Defer to Windows. Theme off SystemAccentColor and Fluent resources so the app follows the user's accent, light/dark, and high-contrast choices.", + "One accent, and it means \"act here\". Selection, focus, and primary actions share the system accent; don't spend it on decoration.", + "Hierarchy over decoration — size, weight, and space do the work before color does.", + "State must be legible: connected, pairing, and error states read clearly via positive/warning/critical, never color alone.", + "No hard-coded hex in UI. Use tokens/theme resources so dark and high-contrast modes stay correct.", + "Motion is a cue, not a garnish: 120-200ms ease-out, no bounce." + ] +} diff --git a/.agents/design/principles.md b/.agents/design/principles.md new file mode 100644 index 0000000000..5baaa5cd30 --- /dev/null +++ b/.agents/design/principles.md @@ -0,0 +1,55 @@ +# Design principles — Windows Fluent UI for OpenClaw + +> This is the prose companion to `design.json`. Designers write here; developers +> and Copilot read here. Keep it short enough that people actually read it. +> This is the design system for the **OpenClaw Windows Hub** app. + +## Design leads, native code ships +These files are the source of truth for **design** — tokens, component intent, and +principles — and are framework-agnostic. The React in `components.jsx` is design +intent for the canvas preview, not shipping code. How a design becomes shipping code +is recorded in `design.json` under `authority`: + +- **Default:** surfaces ship as **native WinUI 3 / C#** (Windows Fluent). Port the + design into XAML/C# using [win-dev-skills](https://github.com/microsoft/win-dev-skills). +- **Chat surface** (`ChatBubble`, `ChatComposer`, `ChatThread`): ships via + **[Reactor](https://github.com/microsoft/microsoft-ui-reactor)**, the React-style + framework for this app's chat. (Reactor has no dedicated porting agent yet — WIP.) + +When you need a value the design doesn't cover, add it here first, then port it into +the native/Reactor implementation via the matching source above — don't hard-code a +one-off in feature code. + +## The one-line brief +A calm native Windows companion for your AI assistant. It should feel like a +quiet, trustworthy extension of Windows — Fluent surfaces, the user's own +accent color — not a web dashboard wearing a desktop window. + +## Voice +- Clear, respectful, and native to Windows. Say what will happen and why. +- Explain pairing, permissions, and security plainly — never alarmist, never hype. +- No hype words ("supercharge", "seamless", "revolutionary", "effortless"). + +## Information hierarchy +1. **One subject per screen.** Decide what the screen is *for* and let that element win. +2. **Size and weight before color.** Establish the hierarchy in grayscale first; color is the last 10%. +3. **One accent.** `accent` = "act here" (selection, focus, primary action). If everything is accented, nothing is. +4. **Generous vertical rhythm.** Prefer the `space-5`/`space-6` steps between groups; let content breathe. + +## Do +- Defer to Windows: theme off `SystemAccentColor` and Fluent resources so the app + follows the user's accent, light/dark, and high-contrast settings. +- Use Segoe UI Variable for text and Cascadia Mono for commands, endpoints, tokens, and logs. +- Make connection state legible: connected/pairing/error read via `positive`/`warning`/`critical`. +- Keep motion to 120–200ms ease-out. Animate to explain, not to impress. + +## Don't +- Don't hard-code hex in UI — it breaks in dark and high-contrast modes. Use tokens/theme resources. +- Don't put gray text on a colored background. +- Don't nest cards inside cards, or wrap every block in a card. +- Don't reach for bounce/elastic easing; it reads as dated and AI-generated. +- Don't fight the OS theme with custom-skinned controls or glassmorphism overload. + +## Anti-references +If a mock could be mistaken for a neon-gradient SaaS landing page, or ignores the +user's system accent and light/dark preference — start over. diff --git a/AGENTS.md b/AGENTS.md index 6666929806..031386d48d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,3 +130,26 @@ Important current facts: - `GatewayConnectionManager` owns operator/node connection state. UI surfaces should observe it or call its reconnect/disconnect APIs instead of constructing parallel gateway clients. - Chat/canvas/tray actions must visibly route users to Connection settings when pairing is incomplete or credentials are missing; avoid silent no-ops. - MCP-only mode (`EnableMcpServer=true`, `EnableNodeMode=false`) must start local `NodeService` without requiring a gateway credential. + + +## Design system + +This repository has a living design system at [`.agents/design/`](.agents/design/). These files are the **source of truth for design** — tokens, component intent, and principles — and are framework-agnostic. `components.jsonc` shows design intent for preview; it is **not** shipping code. +**Read it before creating or changing any UI.** To ship, port the design into this app's implementation using the port target(s) below — don't copy `components.jsonc` verbatim. + +- `.agents/design/design.json` — design tokens: brand, colors, typography, spacing, radii, shadows, principles. +- `.agents/design/components.jsonc` — the component patterns to reuse (structure, variants, states). +- `.agents/design/principles.md` — voice, information hierarchy, and do/don't guidance. + +Generate UI from these tokens and patterns: use token names (e.g. `accent`, `ink`, spacing step `4`, radius `md`), not raw hex or ad-hoc px; reuse the documented components instead of inventing new ones; honor the brand voice; and avoid the system's listed anti-references. If you need a value the system doesn't cover, add it to `.agents/design/` rather than hard-coding a one-off. + +**Port targets** — how a design becomes shipping code here (design stays the source of truth; these say what each surface ships as and which reference/skill to port it with): +- Default: ships as Native WinUI 3 / C# (Windows Fluent) — port via https://github.com/microsoft/win-dev-skills (helper agent: win-dev-skills) +- area "chat", components ChatBubble, ChatComposer, ChatThread: ships as Reactor (Microsoft.UI.Reactor) — port via https://github.com/microsoft/microsoft-ui-reactor (no helper agent yet) +- Canonical implementation owner: OpenClaw Windows (openclaw/openclaw-windows-node) +- Examples kept aligned via: Tokens are captured by manual inspection of the app's WinUI 3 XAML/C# Fluent resources; re-inspect and update this file when the app's ThemeResource mappings or SystemAccentColor usage change. + +Colors in `design.json` are **preview-only** swatches. When a color has a `resource` (e.g. a WinUI `ThemeResource` key), bind that resource in code — never hard-code the preview hex — so light, dark, and high-contrast themes stay correct. The shipping implementation is canonical; treat `components.jsonc` and the token values as derived visual examples, not a parallel upstream source. + +Managed by [Colophon](https://github.com/karkarl/colophon) — edit `.agents/design/` to change the system; this block only points to it. + From c657cca969fe56f659d1ea6fbc9ae23f1acffaae Mon Sep 17 00:00:00 2001 From: Karen Lai <7976322+karkarl@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:14:41 -0700 Subject: [PATCH 02/32] Reconcile chat composer/thread/picker with shipping Reactor UI ComposerPicker now follows the Fluent ComboBox pattern (bordered control fill + hairline border, ink value, right-aligned muted chevron). ChatComposer and ChatThread are realigned to the actual ReactorChatComposer/ReactorChatTimeline implementation: - ChatComposer toolbar: adds session picker, speaker, and settings; notes the Send<->Stop toggle; binds ControlFill/ControlStroke (control/controlLine) instead of Card resources; 12px outer margin. - ChatThread/ChatBubble: user bubble uses the softer accentSubtle (AccentFillColorSecondaryBrush) with a hover Copy; assistant bubble uses subtleHover fill + controlLine border, adds Read aloud, and max-width 720. Adds 3 tokens the system did not cover: control (ControlFillColorDefaultBrush), controlLine (ControlStrokeColorDefaultBrush), accentSubtle (AccentFillColorSecondaryBrush). Preview hex only; code binds the resource keys. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac8d03a2-7198-43dc-8506-709986d6e1d3 --- .agents/design/components.jsonc | 920 +++++++++++++++++++++++++++----- .agents/design/design.json | 5 +- 2 files changed, 791 insertions(+), 134 deletions(-) diff --git a/.agents/design/components.jsonc b/.agents/design/components.jsonc index 20d898f831..936cc3f7bd 100644 --- a/.agents/design/components.jsonc +++ b/.agents/design/components.jsonc @@ -1,30 +1,3 @@ -// components.jsonc — the UI patterns the OpenClaw Windows team has decided on, as data. -// -// This is a framework-agnostic relational element tree with token-bound Auto Layout -// and optional parent-relative freeform positioning. It renders live in the Colophon -// and Prototype canvases and ports cleanly because layout intent is semantic, not -// executable CSS. This is DESIGN INTENT for the canvas preview — not shipping code. -// -// To ship, port these patterns into the app using the port targets in design.json -// `authority`: most surfaces ship as native WinUI 3 / C# (port via -// github.com/microsoft/win-dev-skills), while the chat surface (ChatBubble, -// ChatComposer, ChatThread) ships via Reactor (github.com/microsoft/microsoft-ui-reactor). -// Don't ship this verbatim; bind Fluent ThemeResource keys, never the preview hex. -// -// Every object node has a stable, unique `id`. Node kinds: -// element { "id", "el", "class"?, "attrs"?, "layout"?, "position"?, "margin"?, "appearance"?, "children"? } -// component { "id", "component", "props"?, "layout"?, "position"?, "margin"?, "appearance"? } -// string "literal" or "{prop}" for prop interpolation -// -// Auto Layout: layout.mode vertical|horizontal|grid|freeform|none; gap/padding/margin -// reference design.json spacing tokens ("1".."8"), never raw px; align/justify/columns/ -// wrap/grow; width/height "fill"|"hug"|number. Freeform (v3): a layout.mode "freeform" -// parent establishes a coordinate system; direct children may set -// position { mode:"absolute", x, y } in px relative to that parent. -// -// Appearance inherits from the parent/class; add only intentional, token-bound -// overrides: fontFamily display|body|mono; textStyle (typography scale name); -// color/background/borderColor (color-token name); radius/shadow (token name); textAlign. { "meta": { "version": 3, @@ -35,43 +8,68 @@ { "name": "Button", "note": "Primary action. One per view — it's where the eye should land. Padding mirrors WinUI's default ButtonPadding (11,5,11,6), snapped to spacing tokens 3 (12≈11) and 1 (4≈5/6). In-app MinWidth 100px keeps short labels (OK / Connect) from cramping and gives side-by-side buttons a consistent footprint; that minimum has no spacing-token equivalent, so it stays a port note and the preview hugs its label.", - "props": { "children": "Save changes", "variant": "primary" }, + "props": { + "children": "Save changes", + "variant": "primary" + }, "root": { "id": "button-root", "el": "button", "class": "ds-btn ds-btn-{variant}", "layout": { "mode": "horizontal", - "padding": { "x": "3", "y": "1" }, + "padding": { + "x": "3", + "y": "1" + }, "align": "center", "justify": "center", "width": "hug" }, - "children": ["{children}"] + "children": [ + "{children}" + ] } }, { "name": "Field", "note": "Text input with a quiet label above it — the field is the subject. Input padding mirrors WinUI TextControlThemePadding (10,5,6,6), snapped to tokens 2 (8≈10/6 horizontal) and 1 (4≈5/6 vertical). In-app MinHeight 32px lines a field up with a Button on the same row; that minimum stays a port note.", - "props": { "label": "Gateway address", "placeholder": "127.0.0.1:18789" }, + "props": { + "label": "Gateway address", + "placeholder": "127.0.0.1:18789" + }, "root": { "id": "field-root", "el": "label", "class": "ds-field", - "layout": { "mode": "vertical", "gap": "2", "width": "fill" }, + "layout": { + "mode": "vertical", + "gap": "2", + "width": "fill" + }, "children": [ { "id": "field-label", "el": "span", "class": "ds-field-label", - "children": ["{label}"] + "children": [ + "{label}" + ] }, { "id": "field-input", "el": "input", "class": "ds-input", - "attrs": { "placeholder": "{placeholder}" }, - "layout": { "padding": { "x": "2", "y": "1" }, "width": "fill" } + "attrs": { + "placeholder": "{placeholder}" + }, + "layout": { + "padding": { + "x": "2", + "y": "1" + }, + "width": "fill" + } } ] } @@ -79,7 +77,10 @@ { "name": "Card", "note": "Content card. Flat by default — a 1px hairline border, not a drop shadow. Padding mirrors the WinUI Gallery card (16,12) as tokens 4 (16) and 3 (12) exactly, with the `md` radius and a `line`-colored border supplied by the ds-card class. Never nest a card inside a card.", - "props": { "title": "Windows node", "body": "Paired · 6 capabilities exposed over MCP." }, + "props": { + "title": "Windows node", + "body": "Paired · 6 capabilities exposed over MCP." + }, "root": { "id": "card-root", "el": "article", @@ -87,7 +88,10 @@ "layout": { "mode": "vertical", "gap": "2", - "padding": { "x": "4", "y": "3" }, + "padding": { + "x": "4", + "y": "3" + }, "width": "fill" }, "children": [ @@ -95,21 +99,31 @@ "id": "card-title", "el": "h3", "class": "ds-card-title", - "children": ["{title}"] + "children": [ + "{title}" + ] }, { "id": "card-body", "el": "p", "class": "ds-card-body", - "children": ["{body}"] + "children": [ + "{body}" + ] }, { "id": "card-link", "el": "a", "class": "ds-link", - "attrs": { "href": "#" }, - "margin": { "top": "1" }, - "children": ["Open report →"] + "attrs": { + "href": "#" + }, + "margin": { + "top": "1" + }, + "children": [ + "Open report →" + ] } ] } @@ -117,54 +131,96 @@ { "name": "Badge", "note": "Status pill. Uses semantic color tokens, never a raw hex. Padding tokens 3/1 give the pill a compact footprint; the `pill` radius and tone color come from the ds-badge class.", - "props": { "children": "Connected", "tone": "positive" }, + "props": { + "children": "Connected", + "tone": "positive" + }, "root": { "id": "badge-root", "el": "span", "class": "ds-badge ds-badge-{tone}", "layout": { "mode": "horizontal", - "padding": { "x": "3", "y": "1" }, + "padding": { + "x": "3", + "y": "1" + }, "align": "center", "width": "hug" }, - "children": ["{children}"] + "children": [ + "{children}" + ] } }, { "name": "ComposerPicker", - "note": "Subtle inline picker — text label + chevron, no border until hover in the app. Used for the model and reasoning-effort selectors; reads as a quiet dropdown, not a button. 32px control height with `small` (13/18) muted text and the `sm` radius. (Preview shows the rest state; the transparent rest border is intentionally omitted rather than bound to a token.)", - "props": { "label": "Model" }, + "note": "Fluent ComboBox. A bordered control (`control` fill, `controlLine` hairline border, `sm`/ControlCornerRadius) showing the current value in `ink` with a right-aligned `muted` chevron (Segoe Fluent \\uE70D, previewed ~12px). 32px min height; horizontal padding tokens 3/2 approximate Fluent ComboBoxPadding. Used for the model, reasoning-effort, and session selectors in the composer; opens a flyout list of single-select (RadioMenuItem) options in-app. ComboBox MinWidth (64px) and the dropdown list are Reactor port notes. (Previously previewed as a borderless subtle picker; realigned to the Fluent ComboBox pattern the user asked for.)", + "props": { + "label": "Model" + }, "root": { "id": "composer-picker-root", "el": "button", - "attrs": { "type": "button" }, + "attrs": { + "type": "button", + "role": "combobox", + "aria-haspopup": "listbox", + "aria-label": "{label}" + }, "layout": { "mode": "horizontal", - "gap": "1", - "padding": { "x": "2" }, + "gap": "3", + "padding": { + "left": "3", + "right": "2" + }, "align": "center", + "justify": "space-between", "height": 32, "width": "hug" }, "appearance": { "fontFamily": "body", - "textStyle": "small", - "color": "muted", + "textStyle": "body", + "color": "ink", + "background": "control", + "borderColor": "controlLine", "radius": "sm" }, "children": [ { "id": "composer-picker-label", "el": "span", - "children": ["{label}"] + "children": [ + "{label}" + ] }, { "id": "composer-picker-chevron", "el": "svg", - "attrs": { "width": "12", "height": "12", "viewBox": "0 0 24 24", "fill": "none", "aria-hidden": "true", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, + "attrs": { + "width": "12", + "height": "12", + "viewBox": "0 0 24 24", + "fill": "none", + "aria-hidden": "true", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "appearance": { + "color": "muted" + }, "children": [ - { "id": "composer-picker-chevron-path", "el": "path", "attrs": { "d": "M6 9l6 6 6-6" } } + { + "id": "composer-picker-chevron-path", + "el": "path", + "attrs": { + "d": "M6 9l6 6 6-6" + } + } ] } ] @@ -172,7 +228,7 @@ }, { "name": "ChatBubble", - "note": "Assistant chat bubble: a 36×36 avatar (subtle `surface` fill, hairline `line` border, `pill` radius) to the LEFT of a bubble on a `surface` fill with a `line` border. The `bubble` radius (16px) is friendlier than the 8/12px control radii; padding tokens 4/3 mirror the v1 12/16. Footer reads timestamp · usage · Copy — the same used/context (pct%) string ChatUsageFormatter produces. Mirrors OpenClawChatTimeline; ships via Reactor. Bubble max-width min(80%, 520px) is a Reactor port note (no token equivalent). (User messages ride the accent on the right in-app; this previews the assistant variant.)", + "note": "Assistant chat bubble: a 36×36 avatar (subtle `surface` fill, hairline `controlLine` border, `pill` radius) to the LEFT of a bubble on a `subtleHover` fill with a `controlLine` border. The `bubble` radius (16px) is friendlier than the 8/12px control radii; padding tokens 4/3 mirror the app's 16/12. Footer reads timestamp · usage · Copy · Read aloud — Copy and Read aloud are hover-gated 20×20 subtle icon buttons in-app, and usage is the used/context (pct%) string ChatUsageFormatter produces. Mirrors ReactorChatTimeline's assistant row; ships via Reactor. Bubble max-width min(80%, 720px) and the run-start avatar gating are Reactor port notes (no token equivalent). (User messages ride the softer `accentSubtle` on the right in-app; this previews the assistant variant.)", "props": { "time": "2:14 PM", "usage": "12.5K/200K (6%)", @@ -181,21 +237,50 @@ "root": { "id": "chat-bubble-root", "el": "div", - "layout": { "mode": "horizontal", "gap": "2", "align": "start" }, + "layout": { + "mode": "horizontal", + "gap": "2", + "align": "start" + }, "children": [ { "id": "chat-bubble-avatar", "el": "div", - "attrs": { "aria-hidden": "true" }, - "layout": { "mode": "horizontal", "align": "center", "justify": "center", "width": 36, "height": 36 }, - "appearance": { "background": "surface", "borderColor": "line", "color": "muted", "radius": "pill" }, + "attrs": { + "aria-hidden": "true" + }, + "layout": { + "mode": "horizontal", + "align": "center", + "justify": "center", + "width": 36, + "height": 36 + }, + "appearance": { + "background": "surface", + "borderColor": "controlLine", + "color": "muted", + "radius": "pill" + }, "children": [ { "id": "chat-bubble-avatar-glyph", "el": "svg", - "attrs": { "width": "18", "height": "18", "viewBox": "0 0 24 24", "fill": "currentColor", "aria-hidden": "true" }, + "attrs": { + "width": "18", + "height": "18", + "viewBox": "0 0 24 24", + "fill": "currentColor", + "aria-hidden": "true" + }, "children": [ - { "id": "chat-bubble-avatar-path", "el": "path", "attrs": { "d": "M12 2.5l1.7 5.1a4 4 0 0 0 2.7 2.7l5.1 1.7-5.1 1.7a4 4 0 0 0-2.7 2.7L12 21.5l-1.7-5.1a4 4 0 0 0-2.7-2.7L2.5 12l5.1-1.7a4 4 0 0 0 2.7-2.7L12 2.5z" } } + { + "id": "chat-bubble-avatar-path", + "el": "path", + "attrs": { + "d": "M12 2.5l1.7 5.1a4 4 0 0 0 2.7 2.7l5.1 1.7-5.1 1.7a4 4 0 0 0-2.7 2.7L12 21.5l-1.7-5.1a4 4 0 0 0-2.7-2.7L2.5 12l5.1-1.7a4 4 0 0 0 2.7-2.7L12 2.5z" + } + } ] } ] @@ -203,38 +288,181 @@ { "id": "chat-bubble-stack", "el": "div", - "layout": { "mode": "vertical", "gap": "1", "align": "start" }, + "layout": { + "mode": "vertical", + "gap": "1", + "align": "start" + }, "children": [ { "id": "chat-bubble-message", "el": "div", - "layout": { "mode": "vertical", "padding": { "x": "4", "y": "3" } }, - "appearance": { "fontFamily": "body", "textStyle": "body", "background": "surface", "color": "ink", "borderColor": "line", "radius": "bubble" }, - "children": ["{children}"] + "layout": { + "mode": "vertical", + "padding": { + "x": "4", + "y": "3" + } + }, + "appearance": { + "fontFamily": "body", + "textStyle": "body", + "background": "subtleHover", + "color": "ink", + "borderColor": "controlLine", + "radius": "bubble" + }, + "children": [ + "{children}" + ] }, { "id": "chat-bubble-footer", "el": "div", - "layout": { "mode": "horizontal", "gap": "2", "padding": { "x": "2" }, "align": "center" }, - "appearance": { "fontFamily": "body", "textStyle": "caption", "color": "muted" }, + "layout": { + "mode": "horizontal", + "gap": "2", + "padding": { + "x": "2" + }, + "align": "center" + }, + "appearance": { + "fontFamily": "body", + "textStyle": "caption", + "color": "muted" + }, "children": [ - { "id": "chat-bubble-time", "el": "span", "children": ["{time}"] }, - { "id": "chat-bubble-dot", "el": "span", "attrs": { "aria-hidden": "true" }, "children": ["·"] }, - { "id": "chat-bubble-usage", "el": "span", "children": ["{usage}"] }, + { + "id": "chat-bubble-time", + "el": "span", + "children": [ + "{time}" + ] + }, + { + "id": "chat-bubble-dot", + "el": "span", + "attrs": { + "aria-hidden": "true" + }, + "children": [ + "·" + ] + }, + { + "id": "chat-bubble-usage", + "el": "span", + "children": [ + "{usage}" + ] + }, { "id": "chat-bubble-copy", "el": "button", - "attrs": { "type": "button", "title": "Copy", "aria-label": "Copy message" }, - "layout": { "mode": "horizontal", "align": "center", "justify": "center", "width": 20, "height": 20 }, - "appearance": { "color": "muted", "radius": "sm" }, + "attrs": { + "type": "button", + "title": "Copy", + "aria-label": "Copy message" + }, + "layout": { + "mode": "horizontal", + "align": "center", + "justify": "center", + "width": 20, + "height": 20 + }, + "appearance": { + "color": "muted", + "radius": "sm" + }, "children": [ { "id": "chat-bubble-copy-glyph", "el": "svg", - "attrs": { "width": "14", "height": "14", "viewBox": "0 0 24 24", "fill": "none", "aria-hidden": "true", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, + "attrs": { + "width": "14", + "height": "14", + "viewBox": "0 0 24 24", + "fill": "none", + "aria-hidden": "true", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [ + { + "id": "chat-bubble-copy-rect", + "el": "rect", + "attrs": { + "x": "9", + "y": "9", + "width": "11", + "height": "11", + "rx": "2" + } + }, + { + "id": "chat-bubble-copy-path", + "el": "path", + "attrs": { + "d": "M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1" + } + } + ] + } + ] + }, + { + "id": "chat-bubble-readaloud", + "el": "button", + "attrs": { + "type": "button", + "title": "Read aloud", + "aria-label": "Read message aloud" + }, + "layout": { + "mode": "horizontal", + "align": "center", + "justify": "center", + "width": 20, + "height": 20 + }, + "appearance": { + "color": "muted", + "radius": "sm" + }, + "children": [ + { + "id": "chat-bubble-readaloud-glyph", + "el": "svg", + "attrs": { + "width": "14", + "height": "14", + "viewBox": "0 0 24 24", + "fill": "none", + "aria-hidden": "true", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, "children": [ - { "id": "chat-bubble-copy-rect", "el": "rect", "attrs": { "x": "9", "y": "9", "width": "11", "height": "11", "rx": "2" } }, - { "id": "chat-bubble-copy-path", "el": "path", "attrs": { "d": "M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1" } } + { + "id": "chat-bubble-readaloud-poly", + "el": "polygon", + "attrs": { + "points": "11 5 6 9 2 9 2 15 6 15 11 19 11 5" + } + }, + { + "id": "chat-bubble-readaloud-path", + "el": "path", + "attrs": { + "d": "M15.5 8.5a5 5 0 0 1 0 7M19 5a9 9 0 0 1 0 14" + } + } ] } ] @@ -248,75 +476,305 @@ }, { "name": "ChatComposer", - "note": "Chat input. A rounded `surface` panel (`md` radius) with a hairline `line` border wrapping a borderless, transparent, auto-growing textarea over a toolbar. Left cluster = Add, model picker, reasoning-effort picker; right cluster = mic + Send. Only Send spends the accent; every other control stays subtle so the toolbar reads as quiet chrome. Textarea min 56 / max 200px growth is a Reactor port note. (Preview shows the rest state.)", + "note": "Chat input. A rounded `control` panel (`md` radius) with a hairline `controlLine` border wrapping a borderless, transparent, auto-growing textarea over a toolbar. Left cluster = Add, session picker, model picker, reasoning-effort picker; right cluster = Dictate (mic), Speaker, Settings, and the primary Send. Only Send spends the accent (it swaps to a subtle Stop while a turn is active); every other control stays subtle so the toolbar reads as quiet chrome. Textarea min 56 / max 200px growth and the 12px outer margin are Reactor port notes; the app tightens top padding to 2px below token 1. (Preview shows the rest state.)", "props": { "placeholder": "Message OpenClaw…", + "session": "Default", "model": "Claude Opus 4.8", "effort": "High" }, "root": { "id": "chat-composer-root", "el": "div", - "layout": { "mode": "vertical", "gap": "2", "padding": "2", "width": "fill" }, - "appearance": { "background": "surface", "borderColor": "line", "radius": "md" }, + "layout": { + "mode": "vertical", + "gap": "2", + "padding": "2", + "width": "fill" + }, + "margin": "3", + "appearance": { + "background": "control", + "borderColor": "controlLine", + "radius": "md" + }, "children": [ { "id": "chat-composer-input", "el": "textarea", - "attrs": { "rows": "2", "placeholder": "{placeholder}" }, - "layout": { "padding": "2", "width": "fill" }, - "appearance": { "fontFamily": "body", "textStyle": "body", "color": "ink" } + "attrs": { + "rows": "2", + "placeholder": "{placeholder}" + }, + "layout": { + "padding": "2", + "width": "fill" + }, + "appearance": { + "fontFamily": "body", + "textStyle": "body", + "color": "ink" + } }, { "id": "chat-composer-toolbar", "el": "div", - "layout": { "mode": "horizontal", "gap": "2", "align": "center", "justify": "space-between", "width": "fill" }, + "layout": { + "mode": "horizontal", + "gap": "2", + "align": "center", + "justify": "space-between", + "width": "fill" + }, "children": [ { "id": "chat-composer-left", "el": "div", - "layout": { "mode": "horizontal", "gap": "1", "align": "center" }, + "layout": { + "mode": "horizontal", + "gap": "2", + "align": "center" + }, "children": [ { "id": "chat-composer-add", "el": "button", - "attrs": { "type": "button", "title": "Add context", "aria-label": "Add context" }, - "layout": { "mode": "horizontal", "align": "center", "justify": "center", "width": 32, "height": 32 }, - "appearance": { "color": "muted", "radius": "sm" }, + "attrs": { + "type": "button", + "title": "Add context", + "aria-label": "Add context" + }, + "layout": { + "mode": "horizontal", + "align": "center", + "justify": "center", + "width": 32, + "height": 32 + }, + "appearance": { + "color": "muted", + "radius": "sm" + }, "children": [ { "id": "chat-composer-add-glyph", "el": "svg", - "attrs": { "width": "18", "height": "18", "viewBox": "0 0 24 24", "fill": "none", "aria-hidden": "true", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, + "attrs": { + "width": "18", + "height": "18", + "viewBox": "0 0 24 24", + "fill": "none", + "aria-hidden": "true", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, "children": [ - { "id": "chat-composer-add-path", "el": "path", "attrs": { "d": "M12 5v14M5 12h14" } } + { + "id": "chat-composer-add-path", + "el": "path", + "attrs": { + "d": "M12 5v14M5 12h14" + } + } ] } ] }, - { "id": "chat-composer-model", "component": "ComposerPicker", "props": { "label": "{model}" } }, - { "id": "chat-composer-effort", "component": "ComposerPicker", "props": { "label": "{effort}" } } + { + "id": "chat-composer-session", + "component": "ComposerPicker", + "props": { + "label": "{session}" + } + }, + { + "id": "chat-composer-model", + "component": "ComposerPicker", + "props": { + "label": "{model}" + } + }, + { + "id": "chat-composer-effort", + "component": "ComposerPicker", + "props": { + "label": "{effort}" + } + } ] }, { "id": "chat-composer-right", "el": "div", - "layout": { "mode": "horizontal", "gap": "1", "align": "center" }, + "layout": { + "mode": "horizontal", + "gap": "2", + "align": "center" + }, "children": [ { "id": "chat-composer-mic", "el": "button", - "attrs": { "type": "button", "title": "Dictate", "aria-label": "Dictate" }, - "layout": { "mode": "horizontal", "align": "center", "justify": "center", "width": 32, "height": 32 }, - "appearance": { "color": "muted", "radius": "sm" }, + "attrs": { + "type": "button", + "title": "Dictate", + "aria-label": "Dictate" + }, + "layout": { + "mode": "horizontal", + "align": "center", + "justify": "center", + "width": 32, + "height": 32 + }, + "appearance": { + "color": "muted", + "radius": "sm" + }, "children": [ { "id": "chat-composer-mic-glyph", "el": "svg", - "attrs": { "width": "18", "height": "18", "viewBox": "0 0 24 24", "fill": "none", "aria-hidden": "true", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, + "attrs": { + "width": "18", + "height": "18", + "viewBox": "0 0 24 24", + "fill": "none", + "aria-hidden": "true", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, "children": [ - { "id": "chat-composer-mic-rect", "el": "rect", "attrs": { "x": "9", "y": "3", "width": "6", "height": "11", "rx": "3" } }, - { "id": "chat-composer-mic-path", "el": "path", "attrs": { "d": "M5 11a7 7 0 0 0 14 0M12 18v3" } } + { + "id": "chat-composer-mic-rect", + "el": "rect", + "attrs": { + "x": "9", + "y": "3", + "width": "6", + "height": "11", + "rx": "3" + } + }, + { + "id": "chat-composer-mic-path", + "el": "path", + "attrs": { + "d": "M5 11a7 7 0 0 0 14 0M12 18v3" + } + } + ] + } + ] + }, + { + "id": "chat-composer-speaker", + "el": "button", + "attrs": { + "type": "button", + "title": "Read responses aloud", + "aria-label": "Read responses aloud" + }, + "layout": { + "mode": "horizontal", + "align": "center", + "justify": "center", + "width": 32, + "height": 32 + }, + "appearance": { + "color": "muted", + "radius": "sm" + }, + "children": [ + { + "id": "chat-composer-speaker-glyph", + "el": "svg", + "attrs": { + "width": "18", + "height": "18", + "viewBox": "0 0 24 24", + "fill": "none", + "aria-hidden": "true", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [ + { + "id": "chat-composer-speaker-poly", + "el": "polygon", + "attrs": { + "points": "11 5 6 9 2 9 2 15 6 15 11 19 11 5" + } + }, + { + "id": "chat-composer-speaker-path", + "el": "path", + "attrs": { + "d": "M15.5 8.5a5 5 0 0 1 0 7M19 5a9 9 0 0 1 0 14" + } + } + ] + } + ] + }, + { + "id": "chat-composer-settings", + "el": "button", + "attrs": { + "type": "button", + "title": "Chat settings", + "aria-label": "Chat settings" + }, + "layout": { + "mode": "horizontal", + "align": "center", + "justify": "center", + "width": 32, + "height": 32 + }, + "appearance": { + "color": "muted", + "radius": "sm" + }, + "children": [ + { + "id": "chat-composer-settings-glyph", + "el": "svg", + "attrs": { + "width": "18", + "height": "18", + "viewBox": "0 0 24 24", + "fill": "none", + "aria-hidden": "true", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [ + { + "id": "chat-composer-settings-circle", + "el": "circle", + "attrs": { + "cx": "12", + "cy": "12", + "r": "3" + } + }, + { + "id": "chat-composer-settings-path", + "el": "path", + "attrs": { + "d": "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" + } + } ] } ] @@ -324,16 +782,46 @@ { "id": "chat-composer-send", "el": "button", - "attrs": { "type": "button", "title": "Send", "aria-label": "Send" }, - "layout": { "mode": "horizontal", "align": "center", "justify": "center", "width": 32, "height": 32 }, - "appearance": { "background": "accent", "color": "accentInk", "radius": "sm" }, + "attrs": { + "type": "button", + "title": "Send", + "aria-label": "Send" + }, + "layout": { + "mode": "horizontal", + "align": "center", + "justify": "center", + "width": 32, + "height": 32 + }, + "appearance": { + "background": "accent", + "color": "accentInk", + "radius": "sm" + }, "children": [ { "id": "chat-composer-send-glyph", "el": "svg", - "attrs": { "width": "18", "height": "18", "viewBox": "0 0 24 24", "fill": "none", "aria-hidden": "true", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, + "attrs": { + "width": "18", + "height": "18", + "viewBox": "0 0 24 24", + "fill": "none", + "aria-hidden": "true", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, "children": [ - { "id": "chat-composer-send-path", "el": "path", "attrs": { "d": "M12 19V5M5 12l7-7 7 7" } } + { + "id": "chat-composer-send-path", + "el": "path", + "attrs": { + "d": "M12 19V5M5 12l7-7 7 7" + } + } ] } ] @@ -347,36 +835,136 @@ }, { "name": "ChatThread", - "note": "A short thread: the \"does chat hang together\" check — a user turn (accent bubble on the right) and an assistant turn (with its Copy affordance + context-usage readout), then the composer. Thread max-width 560px is a Reactor port note.", + "note": "A short thread: the \"does chat hang together\" check — a user turn (softer `accentSubtle` bubble on the right, with a hover Copy) and an assistant turn (avatar + Copy / Read aloud / context-usage readout), then the composer. Thread/bubble max-width 720px is a Reactor port note.", "root": { "id": "chat-thread-root", "el": "div", - "layout": { "mode": "vertical", "gap": "3", "width": "fill" }, + "layout": { + "mode": "vertical", + "gap": "3", + "width": "fill" + }, "children": [ { "id": "chat-thread-user", "el": "div", - "layout": { "mode": "horizontal", "justify": "end", "width": "fill" }, + "layout": { + "mode": "horizontal", + "justify": "end", + "width": "fill" + }, "children": [ { "id": "chat-thread-user-stack", "el": "div", - "layout": { "mode": "vertical", "gap": "1", "align": "end" }, + "layout": { + "mode": "vertical", + "gap": "1", + "align": "end" + }, "children": [ { "id": "chat-thread-user-bubble", "el": "div", - "layout": { "mode": "vertical", "padding": { "x": "4", "y": "3" } }, - "appearance": { "fontFamily": "body", "textStyle": "body", "background": "accent", "color": "accentInk", "radius": "bubble" }, - "children": ["Can you list the Windows node capabilities?"] + "layout": { + "mode": "vertical", + "padding": { + "x": "4", + "y": "3" + } + }, + "appearance": { + "fontFamily": "body", + "textStyle": "body", + "background": "accentSubtle", + "color": "accentInk", + "radius": "bubble" + }, + "children": [ + "Can you list the Windows node capabilities?" + ] }, { "id": "chat-thread-user-footer", "el": "div", - "layout": { "mode": "horizontal", "gap": "2", "padding": { "x": "2" }, "align": "center" }, - "appearance": { "fontFamily": "body", "textStyle": "caption", "color": "muted" }, + "layout": { + "mode": "horizontal", + "gap": "2", + "padding": { + "x": "2" + }, + "align": "center" + }, + "appearance": { + "fontFamily": "body", + "textStyle": "caption", + "color": "muted" + }, "children": [ - { "id": "chat-thread-user-time", "el": "span", "children": ["2:14 PM"] } + { + "id": "chat-thread-user-time", + "el": "span", + "children": [ + "2:14 PM" + ] + }, + { + "id": "chat-thread-user-copy", + "el": "button", + "attrs": { + "type": "button", + "title": "Copy", + "aria-label": "Copy message" + }, + "layout": { + "mode": "horizontal", + "align": "center", + "justify": "center", + "width": 20, + "height": 20 + }, + "appearance": { + "color": "muted", + "radius": "sm" + }, + "children": [ + { + "id": "chat-thread-user-copy-glyph", + "el": "svg", + "attrs": { + "width": "14", + "height": "14", + "viewBox": "0 0 24 24", + "fill": "none", + "aria-hidden": "true", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [ + { + "id": "chat-thread-user-copy-rect", + "el": "rect", + "attrs": { + "x": "9", + "y": "9", + "width": "11", + "height": "11", + "rx": "2" + } + }, + { + "id": "chat-thread-user-copy-path", + "el": "path", + "attrs": { + "d": "M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1" + } + } + ] + } + ] + } ] } ] @@ -386,9 +974,16 @@ { "id": "chat-thread-assistant", "component": "ChatBubble", - "props": { "time": "2:14 PM", "usage": "12.5K/200K (6%)", "children": "Paired to the gateway — 6 capabilities are live, including system.run and clipboard access." } + "props": { + "time": "2:14 PM", + "usage": "12.5K/200K (6%)", + "children": "Paired to the gateway — 6 capabilities are live, including system.run and clipboard access." + } }, - { "id": "chat-thread-composer", "component": "ChatComposer" } + { + "id": "chat-thread-composer", + "component": "ChatComposer" + } ] } }, @@ -399,27 +994,57 @@ "id": "example-screen-root", "el": "section", "class": "ds-screen", - "layout": { "mode": "vertical", "gap": "5", "width": "fill" }, + "layout": { + "mode": "vertical", + "gap": "5", + "width": "fill" + }, "children": [ { "id": "example-screen-head", "el": "header", "class": "ds-screen-head", - "layout": { "mode": "horizontal", "gap": "4", "align": "start", "justify": "space-between", "width": "fill" }, + "layout": { + "mode": "horizontal", + "gap": "4", + "align": "start", + "justify": "space-between", + "width": "fill" + }, "children": [ { "id": "example-screen-heading", "el": "div", - "layout": { "mode": "vertical", "gap": "1" }, + "layout": { + "mode": "vertical", + "gap": "1" + }, "children": [ - { "id": "example-screen-eyebrow", "el": "p", "class": "ds-eyebrow", "children": ["Connection"] }, - { "id": "example-screen-title", "el": "h2", "class": "ds-screen-title", "children": ["OpenClaw Windows Hub"] } + { + "id": "example-screen-eyebrow", + "el": "p", + "class": "ds-eyebrow", + "children": [ + "Connection" + ] + }, + { + "id": "example-screen-title", + "el": "h2", + "class": "ds-screen-title", + "children": [ + "OpenClaw Windows Hub" + ] + } ] }, { "id": "example-screen-status", "component": "Badge", - "props": { "tone": "positive", "children": "Connected" } + "props": { + "tone": "positive", + "children": "Connected" + } } ] }, @@ -427,21 +1052,36 @@ "id": "example-screen-board", "el": "div", "class": "ds-grid", - "layout": { "mode": "freeform", "width": "fill", "height": 176 }, + "layout": { + "mode": "horizontal", + "width": "fill", + "height": 176, + "gap": "3" + }, "children": [ { "id": "example-screen-gateway", "component": "Card", - "position": { "mode": "absolute", "x": 0, "y": 0 }, - "props": { "title": "Gateway", "body": "Paired to OpenClawGateway · last seen just now." }, - "layout": { "width": 280, "height": 148 } + "props": { + "title": "Gateway", + "body": "Paired to OpenClawGateway · last seen just now." + }, + "layout": { + "width": 280, + "height": 148 + } }, { "id": "example-screen-node", "component": "Card", - "position": { "mode": "absolute", "x": 248, "y": 28 }, - "props": { "title": "Windows node", "body": "6 capabilities exposed over MCP." }, - "layout": { "width": 280, "height": 148 } + "props": { + "title": "Windows node", + "body": "6 capabilities exposed over MCP." + }, + "layout": { + "width": 280, + "height": 148 + } } ] }, @@ -449,18 +1089,32 @@ "id": "example-screen-row", "el": "div", "class": "ds-row", - "layout": { "mode": "horizontal", "gap": "4", "align": "end", "wrap": true, "width": "fill" }, + "layout": { + "mode": "horizontal", + "gap": "4", + "align": "end", + "wrap": true, + "width": "fill" + }, "children": [ { "id": "example-screen-field", "component": "Field", - "props": { "label": "Gateway address", "placeholder": "127.0.0.1:18789" }, - "layout": { "grow": true } + "props": { + "label": "Gateway address", + "placeholder": "127.0.0.1:18789" + }, + "layout": { + "grow": true + } }, { "id": "example-screen-connect", "component": "Button", - "props": { "children": "Connect", "variant": "primary" } + "props": { + "children": "Connect", + "variant": "primary" + } } ] } diff --git a/.agents/design/design.json b/.agents/design/design.json index 76551ef296..d5b95d715c 100644 --- a/.agents/design/design.json +++ b/.agents/design/design.json @@ -51,7 +51,10 @@ { "name": "warning", "value": "#9d5d00", "resource": "SystemFillColorCautionBrush", "themes": { "dark": "#fcd116", "highContrast": "#ffb000" }, "usage": "Warnings / attention. Fluent SystemFillColorCaution." }, { "name": "critical", "value": "#c42b1c", "resource": "SystemFillColorCriticalBrush", "themes": { "dark": "#ff99a4", "highContrast": "#ff5449" }, "usage": "Errors, disconnected, destructive. Fluent SystemFillColorCritical." }, { "name": "subtleHover", "value": "rgba(0,0,0,0.037)", "resource": "SubtleFillColorSecondaryBrush", "themes": { "dark": "rgba(255,255,255,0.0605)", "highContrast": "transparent" }, "usage": "Hover fill for subtle/transparent controls (icon buttons, inline pickers). Fluent SubtleFillColorSecondary (light #09000000)." }, - { "name": "subtlePressed", "value": "rgba(0,0,0,0.024)", "resource": "SubtleFillColorTertiaryBrush", "themes": { "dark": "rgba(255,255,255,0.0419)", "highContrast": "transparent" }, "usage": "Pressed fill for subtle/transparent controls. Fluent SubtleFillColorTertiary (light #06000000)." } + { "name": "subtlePressed", "value": "rgba(0,0,0,0.024)", "resource": "SubtleFillColorTertiaryBrush", "themes": { "dark": "rgba(255,255,255,0.0419)", "highContrast": "transparent" }, "usage": "Pressed fill for subtle/transparent controls. Fluent SubtleFillColorTertiary (light #06000000)." }, + { "name": "control", "value": "#ffffff", "resource": "ControlFillColorDefaultBrush", "themes": { "dark": "#2d2d2d", "highContrast": "#000000" }, "usage": "Fill for input/entry controls — combo boxes, text fields, the chat composer. Fluent ControlFillColorDefault (distinct from the Card surface used for cards/flyouts)." }, + { "name": "controlLine", "value": "#ececec", "resource": "ControlStrokeColorDefaultBrush", "themes": { "dark": "#353535", "highContrast": "#ffffff" }, "usage": "Border for input/entry controls — combo boxes, text fields, composer, avatars. Fluent ControlStrokeColorDefault (distinct from the Card/divider stroke used for cards)." }, + { "name": "accentSubtle", "value": "#1a75c7", "resource": "AccentFillColorSecondaryBrush", "themes": { "dark": "#47b6ef", "highContrast": "#ffff00" }, "usage": "Softer accent fill for the user's own chat bubble. Fluent AccentFillColorSecondary (SystemAccent at ~90%) — quieter than the full accent reserved for primary actions." } ], "typography": { "display": { From cc86739888f9a58d762534c75ed3a8423e6162a5 Mon Sep 17 00:00:00 2001 From: Karen Lai <7976322+karkarl@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:33:19 -0700 Subject: [PATCH 03/32] Port composer pickers to Fluent ComboBox; point port authority at WinAppCli Reactor port of the design system's ComposerPicker into the shipping composer: ReactorChatComposer.PickerButton now renders a bordered Fluent ComboBox (ControlFill fill, ControlStroke hairline border, primary-text value, right-aligned muted chevron pinned by a Star/Auto grid, radius 4, MinWidth 64, padding 12/8) instead of a borderless subtle button. Session/model/reasoning selection still opens the existing MenuFlyout of RadioMenuItems. design.json: add a winapp (WinAppCli, https://github.com/microsoft/WinAppCli) tooling reference to authority and a toolingSource on both the default WinUI 3 port and the Reactor chat override, since XAML and Reactor now build/package through winapp. Update the composer chevron contract test to assert the ComboBox styling. Remove em dashes from the Colophon pointer block in AGENTS.md so the repo doc validator (no em dashes in user-facing docs) passes. Validation: build.ps1 OK; OpenClaw.Shared.Tests 3989 passed; OpenClaw.Tray.Tests 2897 passed; design-system validators (componentsio/profileio/validateDesignDir) OK; rubber-duck review clean. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac8d03a2-7198-43dc-8506-709986d6e1d3 --- .agents/design/design.json | 7 +++ AGENTS.md | 20 ++++----- .../Chat/ReactorChatComposer.cs | 43 ++++++++++++------- .../ChatTimelinePresentationTests.cs | 10 ++++- 4 files changed, 53 insertions(+), 27 deletions(-) diff --git a/.agents/design/design.json b/.agents/design/design.json index d5b95d715c..ad8c28b942 100644 --- a/.agents/design/design.json +++ b/.agents/design/design.json @@ -9,9 +9,15 @@ "designSource": "self", "owner": "OpenClaw Windows (openclaw/openclaw-windows-node)", "syncProcess": "Tokens are captured by manual inspection of the app's WinUI 3 XAML/C# Fluent resources; re-inspect and update this file when the app's ThemeResource mappings or SystemAccentColor usage change.", + "tooling": { + "name": "winapp (WinAppCli)", + "source": "https://github.com/microsoft/WinAppCli", + "role": "The Windows App Development CLI is the single build/packaging tool for both port targets below. Both the WinUI 3 (XAML) shell and the Reactor chat surface now build, package, and generate app identity, manifests, and certificates through winapp, so treat it as the port authority's build toolchain." + }, "port": { "authoritySource": "Native WinUI 3 / C# (Windows Fluent)", "syncSource": "https://github.com/microsoft/win-dev-skills", + "toolingSource": "https://github.com/microsoft/WinAppCli", "helperAgent": "win-dev-skills" }, "portOverrides": [ @@ -20,6 +26,7 @@ "components": ["ChatBubble", "ChatComposer", "ChatThread"], "authoritySource": "Reactor (Microsoft.UI.Reactor)", "syncSource": "https://github.com/microsoft/microsoft-ui-reactor", + "toolingSource": "https://github.com/microsoft/WinAppCli", "helperAgent": "" } ] diff --git a/AGENTS.md b/AGENTS.md index 031386d48d..35cabba139 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -134,22 +134,22 @@ Important current facts: ## Design system -This repository has a living design system at [`.agents/design/`](.agents/design/). These files are the **source of truth for design** — tokens, component intent, and principles — and are framework-agnostic. `components.jsonc` shows design intent for preview; it is **not** shipping code. -**Read it before creating or changing any UI.** To ship, port the design into this app's implementation using the port target(s) below — don't copy `components.jsonc` verbatim. +This repository has a living design system at [`.agents/design/`](.agents/design/). These files are the **source of truth for design** (tokens, component intent, and principles) and are framework-agnostic. `components.jsonc` shows design intent for preview; it is **not** shipping code. +**Read it before creating or changing any UI.** To ship, port the design into this app's implementation using the port target(s) below. Don't copy `components.jsonc` verbatim. -- `.agents/design/design.json` — design tokens: brand, colors, typography, spacing, radii, shadows, principles. -- `.agents/design/components.jsonc` — the component patterns to reuse (structure, variants, states). -- `.agents/design/principles.md` — voice, information hierarchy, and do/don't guidance. +- `.agents/design/design.json` - design tokens: brand, colors, typography, spacing, radii, shadows, principles. +- `.agents/design/components.jsonc` - the component patterns to reuse (structure, variants, states). +- `.agents/design/principles.md` - voice, information hierarchy, and do/don't guidance. Generate UI from these tokens and patterns: use token names (e.g. `accent`, `ink`, spacing step `4`, radius `md`), not raw hex or ad-hoc px; reuse the documented components instead of inventing new ones; honor the brand voice; and avoid the system's listed anti-references. If you need a value the system doesn't cover, add it to `.agents/design/` rather than hard-coding a one-off. -**Port targets** — how a design becomes shipping code here (design stays the source of truth; these say what each surface ships as and which reference/skill to port it with): -- Default: ships as Native WinUI 3 / C# (Windows Fluent) — port via https://github.com/microsoft/win-dev-skills (helper agent: win-dev-skills) -- area "chat", components ChatBubble, ChatComposer, ChatThread: ships as Reactor (Microsoft.UI.Reactor) — port via https://github.com/microsoft/microsoft-ui-reactor (no helper agent yet) +**Port targets**: how a design becomes shipping code here (design stays the source of truth; these say what each surface ships as and which reference/skill to port it with): +- Default: ships as Native WinUI 3 / C# (Windows Fluent), port via https://github.com/microsoft/win-dev-skills (helper agent: win-dev-skills) +- area "chat", components ChatBubble, ChatComposer, ChatThread: ships as Reactor (Microsoft.UI.Reactor), port via https://github.com/microsoft/microsoft-ui-reactor (no helper agent yet) - Canonical implementation owner: OpenClaw Windows (openclaw/openclaw-windows-node) - Examples kept aligned via: Tokens are captured by manual inspection of the app's WinUI 3 XAML/C# Fluent resources; re-inspect and update this file when the app's ThemeResource mappings or SystemAccentColor usage change. -Colors in `design.json` are **preview-only** swatches. When a color has a `resource` (e.g. a WinUI `ThemeResource` key), bind that resource in code — never hard-code the preview hex — so light, dark, and high-contrast themes stay correct. The shipping implementation is canonical; treat `components.jsonc` and the token values as derived visual examples, not a parallel upstream source. +Colors in `design.json` are **preview-only** swatches. When a color has a `resource` (e.g. a WinUI `ThemeResource` key), bind that resource in code, never hard-code the preview hex, so light, dark, and high-contrast themes stay correct. The shipping implementation is canonical; treat `components.jsonc` and the token values as derived visual examples, not a parallel upstream source. -Managed by [Colophon](https://github.com/karkarl/colophon) — edit `.agents/design/` to change the system; this block only points to it. +Managed by [Colophon](https://github.com/karkarl/colophon). Edit `.agents/design/` to change the system; this block only points to it. diff --git a/src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs b/src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs index ffd8aab27d..040ebfbdbc 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs @@ -211,36 +211,49 @@ Element PickerButton( bool enabled, double maxLabelWidth) { + // Fluent ComboBox visual per the design system's ComposerPicker: a + // bordered ControlFill/ControlStroke control showing the value in primary + // text with a right-aligned muted chevron. The Star/Auto grid pins the + // chevron to the trailing edge (ComboBox space-between) while the button + // still hugs its content up to MinWidth. Selection still opens the caller's + // MenuFlyout of RadioMenuItems. return Button( - HStack( - 4, + Grid( + [GridSize.Star(), GridSize.Auto], + [GridSize.Auto], TextBlock(label) + .Grid(row: 0, column: 0) + .VAlign(VerticalAlignment.Center) + .HAlign(HorizontalAlignment.Left) .MaxWidth(maxLabelWidth) - .FontSize(13) + .FontSize(14) .TextTrimming(TextTrimming.CharacterEllipsis) .TextWrapping(TextWrapping.NoWrap), TextBlock("\uE70D") - .Margin(2, 4, 0, 0) + .Grid(row: 0, column: 1) + .Margin(8, 0, 0, 0) + .VAlign(VerticalAlignment.Center) .AccessibilityView(Microsoft.UI.Xaml.Automation.Peers.AccessibilityView.Raw) .FontFamily(FluentIconCatalog.SymbolThemeFontFamily) - .FontSize(10)), + .FontSize(12) + .Foreground(Theme.SecondaryText)), () => { }) .AutomationName(automationName) - .Foreground(Theme.SecondaryText) + .Foreground(Theme.PrimaryText) .Resources(resources => resources - .Set("ButtonBackground", Theme.Ref("SubtleFillColorTransparentBrush")) - .Set("ButtonBackgroundPointerOver", Theme.SubtleFill) - .Set("ButtonBackgroundPressed", Theme.Ref("SubtleFillColorTertiaryBrush")) - .Set("ButtonBorderBrush", Theme.Ref("SubtleFillColorTransparentBrush")) - .Set("ButtonBorderBrushPointerOver", Theme.Ref("SubtleFillColorTransparentBrush")) - .Set("ButtonBorderBrushPressed", Theme.Ref("SubtleFillColorTransparentBrush"))) + .Set("ButtonBackground", Theme.ControlFill) + .Set("ButtonBackgroundPointerOver", Theme.Ref("ControlFillColorSecondaryBrush")) + .Set("ButtonBackgroundPressed", Theme.ControlFillTertiary) + .Set("ButtonBorderBrush", Theme.ControlStroke) + .Set("ButtonBorderBrushPointerOver", Theme.ControlStroke) + .Set("ButtonBorderBrushPressed", Theme.ControlStroke)) .Height(32) .MinHeight(32) - .MinWidth(0) - .Padding(8, 0, 8, 0) + .MinWidth(64) + .Padding(12, 0, 8, 0) .CornerRadius(controlCornerRadius) .IsEnabled(enabled) - .BorderThickness(0) + .BorderThickness(1) .AutomationId(automationId) .Set(button => ComposerAutomationVisibility.Prepare(button)) .OnUnmount(control => ComposerAutomationVisibility.Detach( diff --git a/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs b/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs index e258ba47a1..affaf32856 100644 --- a/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs +++ b/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs @@ -221,7 +221,7 @@ public void ReactorTimeline_RequeuesOnlyForCompletedHistoryReplacement() } [Fact] - public void ReactorComposer_OffsetsPickerChevronRightAndUp() + public void ReactorComposer_StylesPickersAsFluentComboBox() { var composer = File.ReadAllText(Path.Combine( TestRepositoryPaths.GetRepositoryRoot(), @@ -230,7 +230,13 @@ public void ReactorComposer_OffsetsPickerChevronRightAndUp() "Chat", "ReactorChatComposer.cs")); - Assert.Contains(".Margin(2, 4, 0, 0)", composer); + // Pickers follow the design system's ComposerPicker (Fluent ComboBox): + // a bordered ControlFill/ControlStroke control with a trailing muted chevron, + // not the old borderless subtle button with an up-offset chevron. + Assert.Contains(".Set(\"ButtonBackground\", Theme.ControlFill)", composer); + Assert.Contains(".Set(\"ButtonBorderBrush\", Theme.ControlStroke)", composer); + Assert.Contains(".Margin(8, 0, 0, 0)", composer); + Assert.DoesNotContain(".Margin(2, 4, 0, 0)", composer); } [Fact] From fb1eaa38d55fc957ac05a1c9690c7df8031a719e Mon Sep 17 00:00:00 2001 From: Karen Lai <7976322+karkarl@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:16:18 -0700 Subject: [PATCH 04/32] Revert composer pickers to subtle dropdown style The bordered Fluent ComboBox port regressed the composer's session/model/reasoning selectors. Restore the subtle inline dropdown the surface is meant to ship: ReactorChatComposer.PickerButton goes back to a borderless, transparent button (SubtleFillColorTransparent resting, subtle hover/press, muted value + trailing chevron with a small right/up offset). Update the design system's ComposerPicker to the same subtle pattern (transparent fill, no border, muted text/chevron, gap/padding tokens 1/2) and rename the contract test to ReactorComposer_StylesPickersAsSubtleDropdown. The WinAppCli port authority (design.json) is unchanged. Validation: build.ps1 OK; OpenClaw.Shared.Tests 3989 passed; OpenClaw.Tray.Tests 2897 passed; design-system validators OK. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac8d03a2-7198-43dc-8506-709986d6e1d3 --- .agents/design/components.jsonc | 13 ++--- .../Chat/ReactorChatComposer.cs | 49 ++++++++----------- .../ChatTimelinePresentationTests.cs | 18 ++++--- 3 files changed, 36 insertions(+), 44 deletions(-) diff --git a/.agents/design/components.jsonc b/.agents/design/components.jsonc index 936cc3f7bd..25e3f12fca 100644 --- a/.agents/design/components.jsonc +++ b/.agents/design/components.jsonc @@ -155,7 +155,7 @@ }, { "name": "ComposerPicker", - "note": "Fluent ComboBox. A bordered control (`control` fill, `controlLine` hairline border, `sm`/ControlCornerRadius) showing the current value in `ink` with a right-aligned `muted` chevron (Segoe Fluent \\uE70D, previewed ~12px). 32px min height; horizontal padding tokens 3/2 approximate Fluent ComboBoxPadding. Used for the model, reasoning-effort, and session selectors in the composer; opens a flyout list of single-select (RadioMenuItem) options in-app. ComboBox MinWidth (64px) and the dropdown list are Reactor port notes. (Previously previewed as a borderless subtle picker; realigned to the Fluent ComboBox pattern the user asked for.)", + "note": "Subtle inline dropdown. A borderless, transparent picker showing the current value in `muted` secondary text with a trailing `muted` chevron (Segoe Fluent \\uE70D, previewed ~10px). 32px min height; horizontal padding token `2`, gap token `1`. Resting fill is transparent (SubtleFillColorTransparent); hover uses `subtleHover` (SubtleFillColorSecondary) and press uses `subtlePressed` (SubtleFillColorTertiary), so the picker stays quiet in the composer toolbar and never competes with the primary Send action. Used for the model, reasoning-effort, and session selectors; opens a flyout list of single-select (RadioMenuItem) options in-app. The chevron carries a small right/up offset (previewed 2,4,0,0) to sit tight against the value; the flyout list is a Reactor port note.", "props": { "label": "Model" }, @@ -170,22 +170,19 @@ }, "layout": { "mode": "horizontal", - "gap": "3", + "gap": "1", "padding": { - "left": "3", + "left": "2", "right": "2" }, "align": "center", - "justify": "space-between", "height": 32, "width": "hug" }, "appearance": { "fontFamily": "body", - "textStyle": "body", - "color": "ink", - "background": "control", - "borderColor": "controlLine", + "textStyle": "small", + "color": "muted", "radius": "sm" }, "children": [ diff --git a/src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs b/src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs index 040ebfbdbc..eb4daf9c91 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs @@ -211,49 +211,42 @@ Element PickerButton( bool enabled, double maxLabelWidth) { - // Fluent ComboBox visual per the design system's ComposerPicker: a - // bordered ControlFill/ControlStroke control showing the value in primary - // text with a right-aligned muted chevron. The Star/Auto grid pins the - // chevron to the trailing edge (ComboBox space-between) while the button - // still hugs its content up to MinWidth. Selection still opens the caller's - // MenuFlyout of RadioMenuItems. + // Subtle inline picker per the design system's ComposerPicker: a + // borderless, transparent control showing the current value in muted + // secondary text with a trailing muted chevron. Hover/press use the + // subtle fills so the picker stays quiet in the composer toolbar and + // never competes with the primary Send action. Selection still opens the + // caller's MenuFlyout of RadioMenuItems. return Button( - Grid( - [GridSize.Star(), GridSize.Auto], - [GridSize.Auto], + HStack( + 4, TextBlock(label) - .Grid(row: 0, column: 0) - .VAlign(VerticalAlignment.Center) - .HAlign(HorizontalAlignment.Left) .MaxWidth(maxLabelWidth) - .FontSize(14) + .FontSize(13) .TextTrimming(TextTrimming.CharacterEllipsis) .TextWrapping(TextWrapping.NoWrap), TextBlock("\uE70D") - .Grid(row: 0, column: 1) - .Margin(8, 0, 0, 0) - .VAlign(VerticalAlignment.Center) + .Margin(2, 4, 0, 0) .AccessibilityView(Microsoft.UI.Xaml.Automation.Peers.AccessibilityView.Raw) .FontFamily(FluentIconCatalog.SymbolThemeFontFamily) - .FontSize(12) - .Foreground(Theme.SecondaryText)), + .FontSize(10)), () => { }) .AutomationName(automationName) - .Foreground(Theme.PrimaryText) + .Foreground(Theme.SecondaryText) .Resources(resources => resources - .Set("ButtonBackground", Theme.ControlFill) - .Set("ButtonBackgroundPointerOver", Theme.Ref("ControlFillColorSecondaryBrush")) - .Set("ButtonBackgroundPressed", Theme.ControlFillTertiary) - .Set("ButtonBorderBrush", Theme.ControlStroke) - .Set("ButtonBorderBrushPointerOver", Theme.ControlStroke) - .Set("ButtonBorderBrushPressed", Theme.ControlStroke)) + .Set("ButtonBackground", Theme.Ref("SubtleFillColorTransparentBrush")) + .Set("ButtonBackgroundPointerOver", Theme.SubtleFill) + .Set("ButtonBackgroundPressed", Theme.Ref("SubtleFillColorTertiaryBrush")) + .Set("ButtonBorderBrush", Theme.Ref("SubtleFillColorTransparentBrush")) + .Set("ButtonBorderBrushPointerOver", Theme.Ref("SubtleFillColorTransparentBrush")) + .Set("ButtonBorderBrushPressed", Theme.Ref("SubtleFillColorTransparentBrush"))) .Height(32) .MinHeight(32) - .MinWidth(64) - .Padding(12, 0, 8, 0) + .MinWidth(0) + .Padding(8, 0, 8, 0) .CornerRadius(controlCornerRadius) .IsEnabled(enabled) - .BorderThickness(1) + .BorderThickness(0) .AutomationId(automationId) .Set(button => ComposerAutomationVisibility.Prepare(button)) .OnUnmount(control => ComposerAutomationVisibility.Detach( diff --git a/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs b/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs index affaf32856..62d214639d 100644 --- a/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs +++ b/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs @@ -221,7 +221,7 @@ public void ReactorTimeline_RequeuesOnlyForCompletedHistoryReplacement() } [Fact] - public void ReactorComposer_StylesPickersAsFluentComboBox() + public void ReactorComposer_StylesPickersAsSubtleDropdown() { var composer = File.ReadAllText(Path.Combine( TestRepositoryPaths.GetRepositoryRoot(), @@ -230,13 +230,15 @@ public void ReactorComposer_StylesPickersAsFluentComboBox() "Chat", "ReactorChatComposer.cs")); - // Pickers follow the design system's ComposerPicker (Fluent ComboBox): - // a bordered ControlFill/ControlStroke control with a trailing muted chevron, - // not the old borderless subtle button with an up-offset chevron. - Assert.Contains(".Set(\"ButtonBackground\", Theme.ControlFill)", composer); - Assert.Contains(".Set(\"ButtonBorderBrush\", Theme.ControlStroke)", composer); - Assert.Contains(".Margin(8, 0, 0, 0)", composer); - Assert.DoesNotContain(".Margin(2, 4, 0, 0)", composer); + // Pickers follow the design system's ComposerPicker: a borderless, subtle + // dropdown (transparent fill, subtle hover, muted value + chevron), not a + // bordered ControlFill/ControlStroke ComboBox. The chevron carries a small + // right/up offset to sit tight against the value text. + Assert.Contains(".Set(\"ButtonBackground\", Theme.Ref(\"SubtleFillColorTransparentBrush\"))", composer); + Assert.Contains(".Set(\"ButtonBackgroundPointerOver\", Theme.SubtleFill)", composer); + Assert.Contains(".Margin(2, 4, 0, 0)", composer); + Assert.DoesNotContain(".Set(\"ButtonBackground\", Theme.ControlFill)", composer); + Assert.DoesNotContain(".Set(\"ButtonBorderBrush\", Theme.ControlStroke)", composer); } [Fact] From eaa530f0932934ebf08c2ac2c18b02d2e91ed3d7 Mon Sep 17 00:00:00 2001 From: Karen Lai <7976322+karkarl@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:18:58 -0700 Subject: [PATCH 05/32] Revert composer picker production code; keep design-system seed only This PR is design-system seeding only. Restore src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs and tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs to their main state, removing the composer picker code port entirely. The design system (.agents/design/*) still describes the subtle inline dropdown that the app actually ships, and design.json keeps the WinAppCli port authority. No production code or tests are changed by this PR. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac8d03a2-7198-43dc-8506-709986d6e1d3 --- src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs | 6 ------ .../ChatTimelinePresentationTests.cs | 10 +--------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs b/src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs index eb4daf9c91..ffd8aab27d 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/ReactorChatComposer.cs @@ -211,12 +211,6 @@ Element PickerButton( bool enabled, double maxLabelWidth) { - // Subtle inline picker per the design system's ComposerPicker: a - // borderless, transparent control showing the current value in muted - // secondary text with a trailing muted chevron. Hover/press use the - // subtle fills so the picker stays quiet in the composer toolbar and - // never competes with the primary Send action. Selection still opens the - // caller's MenuFlyout of RadioMenuItems. return Button( HStack( 4, diff --git a/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs b/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs index 62d214639d..e258ba47a1 100644 --- a/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs +++ b/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs @@ -221,7 +221,7 @@ public void ReactorTimeline_RequeuesOnlyForCompletedHistoryReplacement() } [Fact] - public void ReactorComposer_StylesPickersAsSubtleDropdown() + public void ReactorComposer_OffsetsPickerChevronRightAndUp() { var composer = File.ReadAllText(Path.Combine( TestRepositoryPaths.GetRepositoryRoot(), @@ -230,15 +230,7 @@ public void ReactorComposer_StylesPickersAsSubtleDropdown() "Chat", "ReactorChatComposer.cs")); - // Pickers follow the design system's ComposerPicker: a borderless, subtle - // dropdown (transparent fill, subtle hover, muted value + chevron), not a - // bordered ControlFill/ControlStroke ComboBox. The chevron carries a small - // right/up offset to sit tight against the value text. - Assert.Contains(".Set(\"ButtonBackground\", Theme.Ref(\"SubtleFillColorTransparentBrush\"))", composer); - Assert.Contains(".Set(\"ButtonBackgroundPointerOver\", Theme.SubtleFill)", composer); Assert.Contains(".Margin(2, 4, 0, 0)", composer); - Assert.DoesNotContain(".Set(\"ButtonBackground\", Theme.ControlFill)", composer); - Assert.DoesNotContain(".Set(\"ButtonBorderBrush\", Theme.ControlStroke)", composer); } [Fact] From ba2beaee15b01acdc9c7889263854076c167b061 Mon Sep 17 00:00:00 2001 From: Karen Lai <7976322+karkarl@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:53:05 -0700 Subject: [PATCH 06/32] Match ChatComposer preview to the shipping composer Update the ChatComposer sample values and placeholder to mirror the real Reactor composer (Message Assistant placeholder; Main session / Claude Sonnet 5 / Default pickers), and note that a picker with a single option (the session picker with one session) renders disabled so its label reads lighter than its enabled neighbours. Design data only; no code or token changes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac8d03a2-7198-43dc-8506-709986d6e1d3 --- .agents/design/components.jsonc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.agents/design/components.jsonc b/.agents/design/components.jsonc index 25e3f12fca..8d7f009e4f 100644 --- a/.agents/design/components.jsonc +++ b/.agents/design/components.jsonc @@ -155,7 +155,7 @@ }, { "name": "ComposerPicker", - "note": "Subtle inline dropdown. A borderless, transparent picker showing the current value in `muted` secondary text with a trailing `muted` chevron (Segoe Fluent \\uE70D, previewed ~10px). 32px min height; horizontal padding token `2`, gap token `1`. Resting fill is transparent (SubtleFillColorTransparent); hover uses `subtleHover` (SubtleFillColorSecondary) and press uses `subtlePressed` (SubtleFillColorTertiary), so the picker stays quiet in the composer toolbar and never competes with the primary Send action. Used for the model, reasoning-effort, and session selectors; opens a flyout list of single-select (RadioMenuItem) options in-app. The chevron carries a small right/up offset (previewed 2,4,0,0) to sit tight against the value; the flyout list is a Reactor port note.", + "note": "Subtle inline dropdown. A borderless, transparent picker showing the current value in `muted` secondary text with a trailing `muted` chevron (Segoe Fluent \\uE70D, previewed ~10px). 32px min height; horizontal padding token `2`, gap token `1`. Resting fill is transparent (SubtleFillColorTransparent); hover uses `subtleHover` (SubtleFillColorSecondary) and press uses `subtlePressed` (SubtleFillColorTertiary), so the picker stays quiet in the composer toolbar and never competes with the primary Send action. When there is nothing to choose (for example the session picker with a single session) the picker is disabled and its value reads a shade lighter than the enabled muted text. Used for the model, reasoning-effort, and session selectors; opens a flyout list of single-select (RadioMenuItem) options in-app. The chevron carries a small right/up offset (previewed 2,4,0,0) to sit tight against the value; the flyout list is a Reactor port note.", "props": { "label": "Model" }, @@ -473,12 +473,12 @@ }, { "name": "ChatComposer", - "note": "Chat input. A rounded `control` panel (`md` radius) with a hairline `controlLine` border wrapping a borderless, transparent, auto-growing textarea over a toolbar. Left cluster = Add, session picker, model picker, reasoning-effort picker; right cluster = Dictate (mic), Speaker, Settings, and the primary Send. Only Send spends the accent (it swaps to a subtle Stop while a turn is active); every other control stays subtle so the toolbar reads as quiet chrome. Textarea min 56 / max 200px growth and the 12px outer margin are Reactor port notes; the app tightens top padding to 2px below token 1. (Preview shows the rest state.)", + "note": "Chat input. A rounded `control` panel (`md` radius) with a hairline `controlLine` border wrapping a borderless, transparent, auto-growing textarea over a toolbar. Left cluster = Add, session picker, model picker, reasoning-effort picker; right cluster = Dictate (mic), Speaker, Settings, and the primary Send. Only Send spends the accent (it swaps to a subtle Stop while a turn is active, and reads disabled/greyed until the textarea has content); every other control stays subtle so the toolbar reads as quiet chrome. Each picker (see ComposerPicker) is a subtle borderless dropdown with muted value text; a picker with nothing to choose (for example the session picker when only one session exists) renders disabled, so its label reads a shade lighter than its enabled neighbours, as with `Main session` in the preview. Textarea min 56 / max 200px growth and the 12px outer margin are Reactor port notes; the app tightens top padding to 2px below token 1. (Preview shows the rest state with the placeholder visible.)", "props": { - "placeholder": "Message OpenClaw…", - "session": "Default", - "model": "Claude Opus 4.8", - "effort": "High" + "placeholder": "Message Assistant (Enter to send)", + "session": "Main session", + "model": "Claude Sonnet 5", + "effort": "Default" }, "root": { "id": "chat-composer-root", From ada5a629f923c5e2734c4414691fa8241aa833d7 Mon Sep 17 00:00:00 2001 From: Karen Lai <7976322+karkarl@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:03:12 -0700 Subject: [PATCH 07/32] design: give chat-component buttons the ds-btn reset class The ComposerPicker and the chat icon/send/copy buttons rendered with the browser default button bevel in the Colophon preview because they were the only authored el:button nodes without the canonical ds-btn reset class (border: 1px solid transparent). Adding it drops the retro OS chrome so the subtle borderless pickers render as intended; inline appearance still drives radius, color, and the Send accent fill. Design-only, no production changes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac8d03a2-7198-43dc-8506-709986d6e1d3 --- .agents/design/components.jsonc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.agents/design/components.jsonc b/.agents/design/components.jsonc index 8d7f009e4f..8566b7c6c1 100644 --- a/.agents/design/components.jsonc +++ b/.agents/design/components.jsonc @@ -162,6 +162,7 @@ "root": { "id": "composer-picker-root", "el": "button", + "class": "ds-btn", "attrs": { "type": "button", "role": "combobox", @@ -357,6 +358,7 @@ { "id": "chat-bubble-copy", "el": "button", + "class": "ds-btn", "attrs": { "type": "button", "title": "Copy", @@ -414,6 +416,7 @@ { "id": "chat-bubble-readaloud", "el": "button", + "class": "ds-btn", "attrs": { "type": "button", "title": "Read aloud", @@ -536,6 +539,7 @@ { "id": "chat-composer-add", "el": "button", + "class": "ds-btn", "attrs": { "type": "button", "title": "Add context", @@ -614,6 +618,7 @@ { "id": "chat-composer-mic", "el": "button", + "class": "ds-btn", "attrs": { "type": "button", "title": "Dictate", @@ -671,6 +676,7 @@ { "id": "chat-composer-speaker", "el": "button", + "class": "ds-btn", "attrs": { "type": "button", "title": "Read responses aloud", @@ -724,6 +730,7 @@ { "id": "chat-composer-settings", "el": "button", + "class": "ds-btn", "attrs": { "type": "button", "title": "Chat settings", @@ -779,6 +786,7 @@ { "id": "chat-composer-send", "el": "button", + "class": "ds-btn", "attrs": { "type": "button", "title": "Send", @@ -908,6 +916,7 @@ { "id": "chat-thread-user-copy", "el": "button", + "class": "ds-btn", "attrs": { "type": "button", "title": "Copy", From 3b62bbf5ec13a9191b40b14571312aa0108ea422 Mon Sep 17 00:00:00 2001 From: Karen Lai <7976322+karkarl@users.noreply.github.com> Date: Mon, 14 Sep 2026 10:43:14 -0700 Subject: [PATCH 08/32] design: render composer subtle pickers/buttons as WinUI subtle controls The composer pickers and toolbar icon buttons ship as WinUI subtle DropDownButtons / transparent Buttons (SubtleFillColorTransparent rest, SubtleFillColorSecondary hover, SubtleFillColorTertiary press, ControlCornerRadius, TextFillColorSecondary glyph) - verified against the canonical ReactorChatComposer PickerButton and WinUI Fluent resources. In the web preview these were rendering as native