* feat: unified Hub window with NavigationView, slim tray menu, and inline toggles
Consolidate 8 separate windows into a single Hub app:
- New HubWindow with NavigationView (Chat, Home, Activity, Settings pages)
- Chat page embeds gateway Control UI via WebView2 (default landing page)
- Home page with live status cards, quick actions, and activity feed
- Settings page with Expander sections, Test Connection, SSH tunnel fields
- Activity page with category filters and live ActivityStreamService binding
- Slim tray menu: status + 3 inline toggles + Hub/QuickSend + Settings/Exit
- Acrylic backdrop on tray flyout, auto-collapsing nav, page transitions
- Deep links (openclaw://) redirected to Hub pages
- Deleted 5 old windows: StatusDetail, ActivityStream, NotificationHistory, WebChat, Settings
- WebView2 event handler cleanup on page Unloaded (code review fix)
- Deferred page init to avoid null Settings during Frame.Navigate
25 files changed, 1350 insertions(+), 2033 deletions(-) — net code reduction
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: settings validation, CSS sidebar hiding, toggle labels, and SSH tunnel support
- Settings save validates gateway URL and SSH tunnel fields before saving
- Test Connection supports SSH tunnel mode (starts temp tunnel for test)
- SSH toggle auto-updates gateway URL field (shows loopback in tunnel mode)
- Chat page injects CSS to hide web Control UI sidebar (no dual navigation)
- Tray toggle switches hide On/Off labels to prevent clipping
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: full native UI with 12 pages, config editor, dual connection, and live gateway data
New pages: Sessions, Channels, Usage, Nodes, Cron, Skills, Config, About
- Sessions: live session list with reset/delete/compact actions
- Channels: channel health cards with start/stop controls
- Usage: cost breakdown, provider stats, daily costs
- Nodes: node inventory with capabilities and device ID copy
- Cron: scheduled jobs with run/remove actions (gateway wired)
- Skills: installed skills status (gateway wired)
- Config: TreeView + detail panel with editable values via config.set protocol
- About: version info, debug tools, documentation links
Infrastructure:
- Dual WebSocket connection: operator client (UI data) + node client (commands)
- 14 new gateway methods: cron.*, skills.*, config.* with JsonElement.Clone()
- Data caching in HubWindow for instant page navigation
- Session startedAt parsing fix (handles number + string timestamps)
- Hub window synced after settings reconnect
- Tray test updated for Auto scrollbar
Build: 0 errors | Tests: 774 passing (652 shared + 122 tray)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* UX Round 2: Agent events, discovery, pairing, models, presence, menu redesign, timer removal
Features:
- Agent Events page with stream filters and 400-event ring buffer
- Gateway discovery via mDNS (Zeroconf) with scan UI in General page
- Node/Device pairing UI with approve/reject in Nodes page
- Models list in Sessions page via models.list gateway method
- Instances page rewired to presence data from handshake snapshot
- Context cards in tray menu (session summary + token usage bars)
- Pairing pending count in tray menu
Menu & UX:
- Merged status + toggle into rich header card with status dot
- Permission toggles section (browser, camera, exec, canvas, screen)
- Renamed hub to Windows Companion with smart disconnect navigation
- Custom title bar with live connection status + gateway version
- Single-click tray -> chat, double-click -> hub
- Chat window pre-warmed on startup, hides instead of closing
Architecture:
- Removed all background timers (10s poll + 30s health check)
- On-demand data loading only (pages fetch on navigation)
- Fixed ParseSessions to merge instead of clear-then-rebuild (no flicker)
- Fixed HandleAgentEvent sessionKey parsing (was reading from root, not payload)
- Symmetric subscribe/unsubscribe for all new gateway events
- Caches cleared on disconnect, seeded into HubWindow on open
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Restructure navigation: agents as hierarchical nav items under Gateway
- Replace flat Agent section with hierarchical Agents > {agentId} > {sub-pages} structure
- Move Conversations shortcut to top level, pointing to SessionsPage
- Remove AgentSelectorNav ComboBox, replaced with dynamic nav tree (RebuildAgentNavItems)
- Add FindAndSelectNavItem for recursive nested nav item selection
- Add agent: tag parsing (ResolveAgentPageType, ParseAgentIdFromTag)
- Update NavigateTo with legacy flat tag mapping to new agent: prefix format
- Strip Zone B (Agent Roster) and Zone C (Capability Toggles) from HomePage
- Remove Node Mode toggle from SettingsPage (lives in CapabilitiesPage)
- Update command palette to use agent-scoped tags
- NavigateToDefault now goes to Home instead of Conversations
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* UX Round 3: Hearth redesign, hierarchical nav, command palette, agent APIs
Navigation restructure:
- 16 flat pages → domain-grouped hierarchical nav (Gateway, This Computer)
- Each agent gets expandable nav item with Sessions, Events, Skills, Workspace
- Dynamic agent nav built from agents.list gateway response
- Nodes nested under Instances (superset relationship)
- Cron moved to gateway section (gateway-wide, not per-agent)
- Connection page extracted from Settings into Gateway section
- Settings simplified to local-only (startup, notifications)
New pages:
- ConnectionPage — gateway URL, token, SSH tunnel, discovery, status
- CapabilitiesPage — device toggles + node status indicator
- WorkspacePage — TabView per-file viewer via agents.files.list/get
- BindingsPage — routing rules viewer (channel→agent)
- ConversationsPage — cross-agent session browser (legacy)
Command palette (Ctrl+K / Ctrl+F):
- Inline overlay with light dismiss (click outside, Escape, Enter)
- 20+ navigation + 5 toggle + dynamic session commands
- Fuzzy substring filtering
Home page (The Hearth):
- Molty status indicator with colored ring
- Natural language status text
- Quick action buttons
Agent-scoped data:
- Sessions, Skills pass agentId to gateway calls
- Agent Events filtered by sessionKey prefix
- Workspace scoped to current agent in hierarchy
Real gateway APIs:
- agents.list → dynamic nav + agent roster
- agents.files.list/get → workspace file viewer
- Cached agents list for hub seeding on open
Architecture:
- Canvas window styled with Mica + custom title bar
- Open Canvas menu item uses NodeService.ShowCanvasWindow()
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(tray): add rich session tooltips and connected devices section
- Add rich ToolTip to each session label showing model, provider,
channel, thinking level, token breakdown, context window, status,
and age
- Add Connected Devices section between context summary and Permissions
with online indicator dots, platform badges, and rich tooltips
- Show connected client count from presence data in status header subtitle
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Redesign session and device tray menu with rich compact cards
Replace plain text session/device rows with structured Grid cards featuring:
- Status dot (green/amber/gray) + name + model/platform badge + chevron
- Token usage with percentage and color-coded progress bar
- Channel badges and capability icons for devices
- Section headers with right-aligned summary stats
Add AddFlyoutCustomItem() to TrayMenuWindow for custom UIElement
flyout items with hover-to-show and click-to-navigate behavior.
Build detailed side flyout panels with headers, token breakdowns,
capability listings, and session metadata.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* UX Round 4: Chat popup, rich tray menu, schema config editor
Chat panel:
- Tray-anchored borderless popup (bottom-right, DPI-aware)
- WS_EX_TOOLWINDOW + no caption/frame (hidden from taskbar)
- Auto-hide on deactivate, instant show (no animation — WebView2 incompatible)
- Single-click toggle, double-click opens Hub (400ms detection)
- Chat window recreated on settings change (stale URL fix)
Tray menu:
- Rich 2-line session cards: status dot, model badge, token progress bar
- Rich device cards: capability emoji strip, platform badge
- Flyout detail panels: non-interactive TextBlocks (not menu items)
- Session flyout: model/provider, channel, ASCII token bar, thinking/verbose
- Device flyout: capabilities merged with commands (cap as header, cmds indented)
- Dynamic capability toggles under local device (from node.Capabilities)
- Flyout dismisses on any menu item hover (including separators/headers/toggles)
- Section headers with aggregates (sessions/tokens, online/caps)
- AddFlyoutCustomItem + AddToggleItem indent support
Schema config editor:
- SchemaConfigEditor UserControl renders from JSON Schema
- Supports string/number/boolean/enum/array/nested objects
- Sensitive field detection (PasswordBox)
- Fallback RenderConfigDirectly when schema unavailable
- Config detail panel uses schema for selected tree node
- Pending changes preserved on save failure
Command palette:
- Rebuilt as inline overlay Grid (light dismiss: Escape + click-outside)
- Ctrl+F added as alternate shortcut
- TextBox replaces AutoSuggestBox (proper Escape handling)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* UX Round 5: Connection dashboard, tray polish, review fixes
Connection Management:
- Redesigned ConnectionPage with 6-section card layout: status card with
live gateway info, gateway discovery picker with mDNS scan, setup code
paste (from openclaw qr), manual connection expander, device identity
card with pairing status and copyable approval command, connection log
- Auto-discovery when disconnected or unconfigured
- Setup code applies both bootstrapToken and Token for immediate connect
- PreferredGatewayId persisted in settings
Tray Menu Polish:
- Compact 3-column ToggleButton grid for capability toggles (all 7 shown)
- Split header subtitle into 2 lines (connection details + node status)
- Auth failure warning shown inline
- Reconnect and Connection quick actions
- Dropped 'Open' prefix from action items, added bottom padding
Hub Window:
- Removed XAML KeyboardAccelerators (caused tooltip flicker on hover)
- Replaced with PreviewKeyDown handler for Ctrl+K/F
- Added ReconnectAction, LastGatewaySelf, node state properties
Connection Lifecycle Fixes (from 3-model rubber-duck review):
- Capability toggles now use ReconnectNodeServiceOnly() instead of full
teardown — no longer kills gateway client or chat window
- Reconnect action uses lightweight ReconnectGateway() (preserves chat)
- SyncHubNodeState() pushes live pairing/identity to hub on every
node status and pairing change
- Gateway matching uses host:port comparison (not full URL with scheme)
- Discovery service disposed on page Unloaded
- Connection log refreshes on every status change
- SanitizeUrl guards against port -1
- Null-conditional restored on _hub?.RaiseSettingsSaved()
- Synthesized current gateway entry doesn't mutate cached list
Other:
- Instant single-click chat toggle (removed double-click debounce)
- Catch-all ShowHub(action) for menu nav tags
- SSH tunnel section flattened (removed redundant nested expander)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* UX Round 6: Token bar ProgressBar, title bar search, MCP toggle
Session flyout:
- Replaced ASCII token bar (█░) with WinUI ProgressBar (green/orange/red)
- Built flyout as native UIElement (StackPanel) instead of text items
- Added AddFlyoutCustomItem(UIElement, UIElement, action) overload
Title bar search:
- Replaced hidden command palette overlay with AutoSuggestBox in title bar
- Standard Windows pattern, always visible, Ctrl+K/F focuses it
- Lobster icon 14px → 20px, title shortened to 'OpenClaw'
- Removed overlay XAML, smoke layer, and palette methods
MCP server toggle:
- Added Local MCP Server card on Capabilities page
- Toggle, endpoint URL display, Copy Token/URL buttons
- Shows token readiness status
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix session flyout to match device flyout style
Replaced custom UIElement session flyout panel with simple
TrayMenuFlyoutItem list matching the device flyout pattern.
ProgressBar stays in the main menu session card only.
Removed unused AddFlyoutCustomItem(UIElement, UIElement) overload
and ShowCascadingFlyoutElement helper.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* UX Round 7: Nav restructure, App MCP, config editor, review fixes
Navigation:
- Sessions, Agent Events, Skills promoted to top-level with agent filter
- Agents folded to one level (direct nav → Workspace)
- Instances merged into Nodes with Connected Clients section
- Title restored to 'OpenClaw Windows Companion'
- Title bar: 48px height, responsive search (Ctrl+E), lobster 20px
Config page:
- Schema-driven tree (objects only, no leaf nodes)
- Editor + Raw JSON tabs
- config.patch sends { raw, baseHash } (hash from config.get response)
- Subtitle shows actual config file path from gateway
- All expanders open by default
App MCP capability (10 tools):
- app.navigate/status/sessions/agents/nodes/config.get
- app.settings.get/set with security allowlist (no secrets)
- app.menu/search for tray and command palette testing
- All handlers return structured data (not stringified JSON)
- Sessions filter by session key prefix (not channel)
Bug fixes:
- AgentEventsPage: init NRE guard, filter applies to display
- CapabilitiesPage: MCP toggle suppress during init
- SessionsPage: removed unused agent filter
- Config save: proper baseHash from gateway hash field
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* 5-model adversarial review fixes + regression tests
Fixes from Opus 4.7, Sonnet 4.5, GPT-5.4, GPT-5.2, Opus 4.6 reviews:
- Guard IndexOutOfRangeException on empty session Status
- Fix TCS hang when DispatcherQueue unavailable in app.navigate
- Static s_emptyObject replacing leaked JsonDocument in config tree
- Always prune stale sessions (removed incomingKeys.Count > 0 guard)
- try/catch/finally on 6 async void handlers (Channels, Sessions, Config)
- Seed ALL cached data before NavigateToDefault in ShowHub
- Move CurrentStatus/_cachedCommands inside DispatcherQueue in UpdateStatus
- Raw JSON tab uses 'parsed' not wrapper object
- Null-safe Subtitle in search handler
- Invalidate command cache on agent switch
- Dispose SemaphoreSlim in GatewayDiscoveryService
Regression tests (9 new):
- AppCapabilityTests: category, commands, CanHandle, handlers, errors
- SessionInfo empty Status guard
- ParseSessions empty array clears sessions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Tray menu: header redesign, dismiss fix, session reliability
Header:
- Split into brand header (🦞 OpenClaw) + Gateway section
- Gateway section: status dot, version/host:port, node status, labeled
ToggleButton ('Connected'/'Disconnected') with tooltip
- Gateway info clickable → opens Connection page
- Menu dismisses after connect/disconnect toggle (avoids stale header)
Dismiss:
- Unified 150ms delayed foreground check for all deactivation cases
- Checks this window, flyout child, and owner parent before dismissing
- Fixes: click-away dismisses everything, hover between items doesn't
- Set _isShown=true in ShowAtCursor (was missing, broke dismiss guard)
Sessions:
- Removed connection status gate — show cached sessions always
- Zero gateway requests on menu open (health check was clearing sessions
via ParseSessions in the response)
- Session cards click → 'sessions' top-level route
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Connection UX: localhost probe, auth errors, token prompt, gateway switching
Discovery:
- Localhost probe enumerates listening TCP ports via GetActiveTcpListeners
- Probes for gateway HTML signature (<title>OpenClaw Control</title>)
- Excludes MCP port (8765) to avoid false positives
- Runs in parallel with mDNS, results merged
Connection page:
- Auth error InfoBar with contextual guidance (token/pairing/password/signature)
- HubWindow.LastAuthError forwarded from OnAuthenticationFailed
- Cleared on successful connect and new connection attempts
- Token prompt always shows when switching gateways (pre-fills current token)
- Cancel button on token prompt
- Discovery list refreshes after connecting to show ✓
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Prepare UX experiments branch for PR
Fix gateway discovery host resolution, harden branch-introduced security paths, complete localization coverage, remove newly introduced dead code, refresh documentation, wire functional UX flows, and stabilize the Config page rendering path.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix MCP-only tray startup
Initialize the local node service when MCP mode is enabled even if gateway node mode is disabled, so MCP-only tray launches start the HTTP server used by integration tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Ranjesh Jaganathan <ranjeshj@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
356 lines
17 KiB
Markdown
356 lines
17 KiB
Markdown
# A2UI v0.8 — Implementation Grading
|
||
|
||
This grades two implementations against the v0.8 spec
|
||
(<https://a2ui.org/specification/v0.8-a2ui/>):
|
||
|
||
- **Lit reference** at `C:\Users\andersonch\Code\openclaw\vendor\a2ui\renderers\lit\src\0.8`
|
||
- **Native WinUI** in this repo at `src/OpenClaw.Tray.WinUI/A2UI/`
|
||
|
||
The Lit code looks like the canonical browser renderer the OpenClaw
|
||
canvas host ships; the WinUI code is this repo's branch
|
||
`feat/a2ui-native-winui`.
|
||
|
||
Citations use repo-local paths. Lit paths are anchored at the OpenClaw
|
||
checkout: `openclaw\vendor\a2ui\renderers\lit\src\0.8\`. WinUI paths are
|
||
anchored at `src/OpenClaw.Tray.WinUI/A2UI/`.
|
||
|
||
## Method
|
||
|
||
For each spec area, deductions land in two buckets:
|
||
|
||
- **Gap** — implementation is missing or wrong vs. spec. Letter grade penalty.
|
||
- **Good deviation** — implementation does something the spec _doesn't say
|
||
to do_, but it's the correct call. Listed but doesn't penalize.
|
||
|
||
Grades are A–F, separately for Lit and WinUI. There is no curving —
|
||
"A" means it would pass a strict spec audit and a strict security
|
||
audit; "B" means it works for normal traffic but fails under a hostile
|
||
agent; etc.
|
||
|
||
---
|
||
|
||
## Scorecard
|
||
|
||
| Area | Lit | WinUI | Notes |
|
||
| --- | --- | --- | --- |
|
||
| Component coverage (catalog completeness) | A | A | both 18/18 |
|
||
| Component property completeness | B | A− | Lit has 4 documented TODOs; WinUI has minor distribution mappings |
|
||
| Streaming / JSONL parsing | B | A | Lit: lenient; WinUI: lenient + size caps |
|
||
| Data binding / `A2UIValue` | B+ | A | Lit auto-parses JSON strings (surprising); WinUI strict RFC 6901 |
|
||
| Action transport | B | A | Lit: DOM event passthrough; WinUI: debounced + single-flight + fallback queue + gateway tag protocol |
|
||
| Action context security | D | A | Lit punts to host; WinUI scopes to declared `dataBinding` and redacts secrets |
|
||
| Theming | A− | A− | Equivalent power; different idioms |
|
||
| URL safety / SSRF | F | A− | Lit unrestricted; WinUI HTTPS+allowlist for `Image`/`Video`/`AudioPlayer`, plus DNS-rebinding pin on `Image` fetches only |
|
||
| Modal lifecycle | A− | A | Both work; WinUI uses native `ContentDialog` |
|
||
| List virtualization | C | A | Lit builds all items; WinUI uses `ItemsRepeater` w/ recycling |
|
||
| Bi-directional binding (write-back) | A | A | Both implement; spec is silent (good deviation) |
|
||
| Markdown in `Text` | B+ | n/a | Lit's enhancement is real but increases attack surface |
|
||
| Test coverage | D | A− | Lit: 1 model test, no per-component; WinUI: render matrix + scale + integration |
|
||
| Spec deviations called out (good ones) | B | A | Lit's improvements partially offset its gaps |
|
||
| **Overall** | **B−** | **A−** | |
|
||
|
||
The two "A" grades have very different shapes:
|
||
|
||
- **Lit** is a smaller codebase that gets the happy path right, with two
|
||
notable **good** deviations (Markdown rendering, bi-directional binding)
|
||
but several papercut **gaps** and a **non-trivial security delta**
|
||
inherited from a "the host will sanitize" posture.
|
||
- **WinUI** is significantly more code, fills almost every gap, and adds
|
||
defenses the spec doesn't ask for. Its remaining minus comes from the
|
||
things it _doesn't_ do yet (List `template` mode, Row wrap, `MultipleChoice.variant`).
|
||
|
||
---
|
||
|
||
## Lit implementation — detailed deductions
|
||
|
||
### Documented `TODO` gaps
|
||
|
||
Verbatim TODOs in `vendor/a2ui/.../ui/root.ts` and component files:
|
||
|
||
| Property | File:Line | Status |
|
||
| --- | --- | --- |
|
||
| `Divider.thickness` / `axis` / `color` | `ui/root.ts:317` | type declared, value not applied to `<hr>` |
|
||
| `MultipleChoice.maxAllowedSelections` | `ui/root.ts:334` | accepted but not enforced |
|
||
| `TextField.validationRegexp` | `ui/root.ts:367` | not applied to `<input>` |
|
||
| `DateTimeInput.outputFormat` | `ui/datetime-input.ts:159` | placeholder; always uses browser format |
|
||
| `MultipleChoice.selections` resolution | `ui/multiple-choice.ts:87–103` | logic incomplete when `selections` is path-bound |
|
||
| `AudioPlayer.description` | `ui/audio.ts` | spec'd property silently dropped |
|
||
|
||
Letter penalty: **−1 step on Component Property Completeness** (A → B).
|
||
|
||
### `A2UIValue.path` resolver auto-parses JSON-shaped strings
|
||
|
||
`data/model-processor.ts:198–225` detects `valueString` payloads that look
|
||
like `{...}` or `[...]` and **silently parses them as JSON**. The intent is
|
||
"developer convenience"; the consequence is that a string literal containing
|
||
a `[` or `{` becomes a structured value. This is a **gap** because the spec
|
||
distinguishes `valueString` from `valueArray`/`valueMap` precisely so the
|
||
agent can be unambiguous. Letter penalty: **−1 step on data binding**.
|
||
|
||
### URLs are passed through to the DOM
|
||
|
||
`ui/image.ts:67–74` binds `<img src="${url}">` directly. There is no
|
||
allowlist for `data:` / `javascript:` / `file:` / private-IP hosts, no
|
||
SSRF protection, no DNS rebinding defense. The WinUI impl has all of
|
||
these. The host **may** sanitize before forwarding URLs, but the
|
||
renderer offers no defense in depth. Letter penalty: **−2 steps on URL
|
||
safety** (this is the F).
|
||
|
||
### Component registry allows arbitrary custom elements
|
||
|
||
`vendor/a2ui/.../ui/root.ts:118–140, 441–471` lets the embedding app set
|
||
`enableCustomElements = true` and then renders any `<component>` whose tag
|
||
is registered in `componentRegistry`. This is **beyond spec** — useful for
|
||
extensibility, dangerous for catalog-strict mode. **Not graded as a gap**
|
||
since it's behind a flag, but it's worth flagging at the host level.
|
||
|
||
### One unit test covers everything
|
||
|
||
`vendor/a2ui/.../model.test.ts` exercises `A2uiMessageProcessor` for
|
||
`beginRendering` and `surfaceUpdate`. There are **no per-component
|
||
render tests, no event-dispatch tests, no markdown sanitizer tests, no
|
||
data-binding edge-case tests**. Letter penalty: **−2 steps on test
|
||
coverage** (D).
|
||
|
||
### Good deviations
|
||
|
||
- **Markdown rendering** in `Text` (`ui/text.ts`, `ui/directives/markdown.ts`).
|
||
HTML blocks wrapped in `<iframe sandbox="">`; code blocks escaped via
|
||
`sanitizer.escapeNodeText`. The spec says plain string. Whether this
|
||
counts as good depends on the threat model — see
|
||
[the Text/Markdown divergence](#text-markdown-divergence).
|
||
- **Signal-driven re-render** via `@lit-labs/signals`. Cleaner reactivity
|
||
than naive `requestUpdate()`.
|
||
- **Bi-directional binding** in `CheckBox`, `TextField`, `Slider`,
|
||
`DateTimeInput`. Spec is silent on write-back; both impls add it.
|
||
|
||
---
|
||
|
||
## WinUI implementation — detailed deductions
|
||
|
||
### Property-coverage misses
|
||
|
||
| Property | File:Line | Status |
|
||
| --- | --- | --- |
|
||
| `Row.distribution` `spaceBetween/Around/Evenly` | `Rendering/Renderers/ContainerRenderers.cs:10–32` | all three collapse to `HorizontalAlignment.Stretch` (WinUI `StackPanel` doesn't natively express justify-content) |
|
||
| `Row.wrap` (multi-row) | n/a | not implemented; would need a custom `Panel` |
|
||
| `List.template` mode | `Rendering/Renderers/ContainerRenderers.cs:57–159` | only `explicitList` supported |
|
||
| `MultipleChoice.variant` (`chips`) | `Rendering/Renderers/InteractiveRenderers.cs:279–430` | always `ComboBox`/`ListView` |
|
||
| `MultipleChoice.filterable` | same | not honored |
|
||
| `TextField.validationRegexp` | `Rendering/Renderers/InteractiveRenderers.cs:98–199` | not enforced |
|
||
| `Tabs` close / reorder | `Rendering/Renderers/ContainerRenderers.cs:187–235` | disabled |
|
||
| Component `weight` | `Protocol/A2UIProtocol.cs:111–151` | parsed but not applied |
|
||
|
||
Letter penalty: **−1 step on Component Property Completeness**, but
|
||
balanced by being the only impl that fills the corresponding Lit gaps
|
||
(`maxAllowedSelections` is enforced; `Divider.axis` is honored).
|
||
|
||
### Action context scoping (the centerpiece win)
|
||
|
||
`Rendering/IComponentRenderer.cs:183–249` (`BuildActionContext`):
|
||
|
||
1. Collect `allowed` paths from the component's explicit `dataBinding`
|
||
array, or — if absent — implicitly walk every `A2UIValue.path` referenced
|
||
by the component's own properties.
|
||
2. For each `action.context[]` entry, resolve only if `IsAllowedPath`
|
||
matches (exact or ancestor with `/` boundary).
|
||
3. Strip secret paths via `SecretRedactor` (`Rendering/SecretRedactor.cs`):
|
||
- Registered paths (e.g., obscured `TextField` fields).
|
||
- Substring denylist: `password`, `secret`, `token`.
|
||
|
||
This blocks the trivial "exfiltrate the whole tree" attack without
|
||
requiring the host to know about A2UI internals. The Lit impl can't
|
||
do this because it dispatches `action` straight through.
|
||
|
||
### URL safety — DNS rebinding defense (Image fetches)
|
||
|
||
`Rendering/MediaResolver.cs:57–95`:
|
||
|
||
```csharp
|
||
new SocketsHttpHandler {
|
||
ConnectCallback = async (ctx, ct) => {
|
||
var addresses = await Dns.GetHostAddressesAsync(ctx.DnsEndPoint.Host, ct);
|
||
foreach (var ip in addresses) {
|
||
if (!IsPublicAddress(ip)) throw ...; // loopback, RFC1918, link-local, multicast
|
||
}
|
||
// connect to resolved IP, not hostname (no second DNS lookup)
|
||
},
|
||
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
|
||
};
|
||
```
|
||
|
||
Plus an allowlist gate in `IsAllowed(url)`. Closes a TOCTOU window
|
||
between an allowlist check and the actual TCP connect. The Lit impl
|
||
does none of this.
|
||
|
||
**Limitation: this pin is image-only.** `Video`/`AudioPlayer` route through
|
||
`MediaSource.CreateFromUri`, which performs its own DNS resolution at
|
||
playback time outside the resolver. The HTTPS+allowlist gate still
|
||
applies to those URLs, but the connect-time IP check does not — see
|
||
`MediaResolver.TryResolveMediaUri`. A local-proxy approach was scoped
|
||
out of the v0.8 native renderer; the allowlist is the load-bearing
|
||
defense for media playback.
|
||
|
||
### Streaming hardening
|
||
|
||
`Protocol/A2UIProtocol.cs:176–367` and `Hosting/A2UIRouter.cs`:
|
||
|
||
| Cap | Value |
|
||
| --- | --- |
|
||
| Max line length | 1 MiB |
|
||
| Max components per surface | 2000 |
|
||
| Max entries per `dataModelUpdate` | 1024 |
|
||
| Max key length | 256 |
|
||
| Max string value | 64 KiB |
|
||
| Max `valueMap` depth | 32 |
|
||
| Max render depth | 64 |
|
||
|
||
All limits log + drop, never throw. Cycle detection in `_renderingIds`
|
||
prevents id-loops in malformed surfaces.
|
||
|
||
### Component diff on `surfaceUpdate`
|
||
|
||
`Hosting/SurfaceHost.cs:ApplyComponents` compares incoming defs (name,
|
||
weight, properties JSON-string) against the previous set and **skips
|
||
rebuild if unchanged**. Effect: a re-emitted surface preserves
|
||
`TextBox` caret position, scroll offset, and `Tabs` selection. The
|
||
spec calls for "structural diffing"; this is a heuristic that catches
|
||
the most common case (agent re-emits whole surface).
|
||
|
||
### Modal as native `ContentDialog`
|
||
|
||
`Rendering/Renderers/ContainerRenderers.cs:237–284` wires up a
|
||
`ContentDialog` whose `Content` is the `contentChild` and whose trigger
|
||
is the `entryPointChild` wrapped in a transparent `Button`. Spec leaves
|
||
the modal _shape_ open; the WinUI impl gives it the full platform-modal
|
||
treatment (focus trap, ESC dismiss, screen-reader announcement).
|
||
|
||
### List virtualization
|
||
|
||
`Rendering/Renderers/ContainerRenderers.cs:57–159` uses an
|
||
`ItemsRepeater` with a `ChildIdTemplate` cache keyed by component id.
|
||
Recycled elements are pulled from the cache so their data-binding
|
||
subscriptions stay alive across scrolling. The Lit impl has no
|
||
virtualization.
|
||
|
||
### Test surface
|
||
|
||
| Project | Focus |
|
||
| --- | --- |
|
||
| `OpenClaw.Shared.Tests/A2UICapabilitySecurityTests.cs` | protocol, secret redaction |
|
||
| `OpenClaw.Tray.UITests/A2UIRenderingTests.cs` | per-component XAML rendering, data binding, live updates |
|
||
| `OpenClaw.Tray.UITests/A2UIControlMatrixTests.cs` | property matrix coverage |
|
||
| `OpenClaw.Tray.UITests/A2UIDashboardScaleTest.cs` | 1000+ component stress |
|
||
| `OpenClaw.Tray.UITests/A2UIThemeTests.cs` | theme parsing |
|
||
| `OpenClaw.Tray.UITests/A2UISvgTests.cs` | SVG decode + 8s timeout |
|
||
| `OpenClaw.Tray.IntegrationTests/A2UICanvasIntegrationTests.cs` | end-to-end MCP smoke + PNG capture |
|
||
|
||
Coverage merged across all three suites via `dotnet-coverage` (per the
|
||
auto-memory note). Letter grade A−; the missing step is that the
|
||
gateway-action transport unit tests aren't fully isolated (depend on a
|
||
fake `WindowsNodeClient`).
|
||
|
||
### Good deviations
|
||
|
||
| Deviation | File | Why it's good |
|
||
| --- | --- | --- |
|
||
| DNS rebinding defense (image fetches) | `Rendering/MediaResolver.cs:57–95` | spec doesn't ask but a hostile agent can otherwise pivot through the image fetch path to internal HTTP services. Does not extend to `Video`/`AudioPlayer` — see "URL safety" section. |
|
||
| Action context allowlist | `Rendering/IComponentRenderer.cs:183–249` | minimum-information principle; spec leaves this open |
|
||
| Secret denylist | `Rendering/SecretRedactor.cs` | catches `/auth/sessionToken` style names automatically |
|
||
| `surfaceUpdate` diff | `Hosting/SurfaceHost.cs` | preserves caret/scroll/selection on re-emit |
|
||
| Single-flight gate on action dispatch | `Actions/IActionSink.cs:27–142` | prevents fallback dequeue racing fresh send |
|
||
| Per-surface theme scope | `Hosting/SurfaceHost.cs ApplyThemeToScope` | multi-surface tab views don't bleed themes |
|
||
| `IA2UITelemetry` seam | `Telemetry/IA2UITelemetry.cs` | structured events instead of log scraping |
|
||
| Single-handler `Func` events on `CanvasCapability` | reviewed in commit `5b9c468` | catches accidental multi-subscribe instead of silent `Delegate.Combine` |
|
||
| MCP bearer token in Settings UI | `SettingsPage.xaml.cs` | quality-of-life for MCP setup, kept out of action payloads |
|
||
|
||
---
|
||
|
||
## Side-by-side: where they diverge meaningfully
|
||
|
||
### `Text` / Markdown divergence
|
||
|
||
The Lit impl renders Markdown; the WinUI impl renders plain text. This is
|
||
the **biggest functional UX difference** between the two.
|
||
|
||
Lit's defense is `iframe sandbox=""` for HTML blocks plus
|
||
`escapeNodeText` for code. That's a reasonable sandbox model in the
|
||
browser — but every line still expands the renderer's attack surface
|
||
beyond the spec's "plain string" promise.
|
||
|
||
For ms-windows-node, parity is **probably not worth chasing** unless
|
||
the agent surfaces depend on it: WinUI doesn't have a built-in
|
||
Markdown engine, and adding one means importing a dependency that has
|
||
to be kept in lockstep with Lit's rendering choices to avoid surfaces
|
||
that look right in the browser and broken on Windows. The defensible
|
||
choice is to ask the agent to emit explicit `Text + usageHint`
|
||
hierarchies instead of inline Markdown.
|
||
|
||
### List performance
|
||
|
||
If a surface includes a `List` of 200+ items, the Lit renderer will
|
||
build all 200 children before paint. WinUI builds ~10 (whatever fits
|
||
the viewport) and recycles as the user scrolls. For this repo's
|
||
typical agent surfaces (dashboards, conversation panels) this is the
|
||
single biggest performance delta.
|
||
|
||
### Action security model
|
||
|
||
The two impls have completely different threat models:
|
||
|
||
- **Lit + browser canvas host**: assume the embedding app is
|
||
trustworthy and will sanitize. The renderer is a thin presenter.
|
||
- **WinUI tray**: assume the renderer talks to a hostile agent over an
|
||
arbitrary network. Apply policy in the renderer.
|
||
|
||
Neither is wrong, but a host that wants Lit-grade isolation has to
|
||
build the same allowlist/denylist logic that WinUI bakes in. In
|
||
practice that means anyone embedding the Lit renderer outside
|
||
OpenClaw's canvas host needs to **wrap action handlers**, never just
|
||
forward them.
|
||
|
||
---
|
||
|
||
## Known deviations by category
|
||
|
||
For PR reviewers — quick "is this OK?" reference.
|
||
|
||
| Deviation | Spec status | Lit | WinUI | Verdict |
|
||
| --- | --- | --- | --- | --- |
|
||
| Bi-directional data-model write on user input | silent | ✓ | ✓ | Good — spec assumes it implicitly |
|
||
| Markdown in `Text` | violation (plain string) | ✓ | ✗ | Lit: useful but expands attack surface; WinUI: stay plain |
|
||
| Custom-element registry beyond catalog | violation (catalog-strict) | ✓ (flag) | ✗ | Risk; only enable in trusted hosts |
|
||
| `valueString` auto-parsed as JSON | violation (type erasure) | ✓ | ✗ | Bug-shaped; rely on `valueMap`/`valueArray` |
|
||
| Hard size caps on stream / model | silent | ✗ | ✓ | Good — DoS defense |
|
||
| URL allowlist on media | silent | ✗ | ✓ | Good — SSRF defense |
|
||
| DNS-rebinding defense (image fetches) | silent | ✗ | ✓ | Good — beyond allowlist. Image only; `Video`/`AudioPlayer` rely on the allowlist alone (OS media stack re-resolves at playback). |
|
||
| Action context allowlist | silent | ✗ | ✓ | Good — minimum information |
|
||
| Secret-path redaction | silent | ✗ | ✓ | Good — keeps tokens off the wire |
|
||
| Component diff on `surfaceUpdate` | "structural diffing" (vague) | ✗ | ✓ | Good — preserves UI state |
|
||
| `List` virtualization | "should virtualize" | ✗ | ✓ | Good — required for non-trivial surfaces |
|
||
| `Modal` as native `ContentDialog` | shape open | `<dialog>` | `ContentDialog` | Both fine |
|
||
| `MultipleChoice` single-mode writes scalar | spec implies array | array | scalar | WinUI's reads tolerate either; talk to your agent format |
|
||
| `validationRegexp` (TextField) | spec property | ✗ TODO | ✗ | Both have a gap here |
|
||
|
||
---
|
||
|
||
## Recommended follow-ups (not part of grading)
|
||
|
||
These are the changes that would close the remaining minuses:
|
||
|
||
**WinUI (A− → A)**
|
||
- Honor `MultipleChoice.variant` (`chips`) and `filterable`.
|
||
- Apply `TextField.validationRegexp` (the catalog says it's a string;
|
||
compile + on-change validate).
|
||
- Consider `List.template` mode for surfaces that bind a list to a
|
||
data-model array (also unblocks v0.9 readiness).
|
||
- Add unit tests for `GatewayActionTransport` payload shape.
|
||
|
||
**Lit (B− → B+ or higher)**
|
||
- Resolve the four documented `TODO`s (Divider, TextField,
|
||
DateTimeInput, MultipleChoice).
|
||
- Add per-component render tests and a markdown-sanitizer test suite.
|
||
- Add at least an opt-in URL allowlist for media components.
|
||
- Document the `enableCustomElements` flag's risk surface for
|
||
embedding apps.
|