Native WinUI chat experience#315
Conversation
Replaces the WebView2-hosted gateway web client in both the Hub Chat tab (Pages/ChatPage) and the tray ChatWindow popup with a native WinUI surface built on the vendored Microsoft.UI.Reactor framework and the Chat.UI sample components from microsoft/microsoft-ui-reactor. Vendored (external/reactor/): - Snapshot of microsoft-ui-reactor: Reactor + Reactor.Analyzers + Reactor.Localization.Generator, plus Chat.Model + Chat.UI from samples/apps/chat. MIT-licensed; see external/reactor/NOTICE.md and README.md for refresh procedure. Single edit applied: Reactor.csproj bumped to net10.0-windows10.0.22621.0 with LangVersion=13 to keep the upstream `field` identifier compiling under net10's C# 14 default. src/OpenClaw.Tray.WinUI/Chat/: - OpenClawChatDataProvider: IChatDataProvider implementation that adapts OpenClawGatewayClient events into ChatTimelineReducer events. - OpenClawChatRoot: Reactor root component composing SessionHeader + OpenClawChatTimeline + InputBar + StatusBar. - OpenClawChatTimeline: forked from Chat.UI ChatTimeline; right-aligned pink user bubbles with avatar + sender label, left-aligned subtle assistant cards with star avatar + sender label. - IChatGatewayBridge / GatewayClientChatBridge: testability seam over OpenClawGatewayClient. - ReactorChatHostExtensions: mounts a Reactor host into a XAML <Border>. OpenClaw.Shared additions: - ChatMessageReceived event raised from HandleChatEvent (alongside the existing toast notification path; non-breaking). - chat.history RPC: RequestChatHistoryAsync(sessionKey). - chat.abort RPC: SendChatAbortAsync(runId). - ChatMessageInfo + ChatHistoryInfo models. Adapter behavior (covered by 45 OpenClawChatDataProviderTests): - Maps gateway streaming chat events (state="delta"/"final") to assistant text upserts; user echoes are dropped. - Maps agent stream=tool/job/lifecycle/assistant/reasoning to the corresponding ChatTimelineReducer events. - Tool result/error text extracted from real payload (data.result.content, data.output, data.error, ...) with 4 KB truncation. - Tracks per-thread runId (set on lifecycle.start, cleared on lifecycle.end) so StopResponseAsync can issue a real chat.abort and appends an "Aborted" Status entry on user-initiated stops. - LoadHistoryAsync folds the transcript into the timeline; brackets each assistant message with TurnEnd to avoid the upsert-collapse bug; sorts by timestamp ascending; system role rendered as Status entries. - StatusChanged Disconnected->Connected transition invalidates and reloads per-thread history (spec edge case). - ModelsListUpdated populates StatusBar.AvailableModels (deduped by display name) so the model picker is no longer always empty. XAML shells: - Pages/ChatPage.xaml(.cs) and Windows/ChatWindow.xaml(.cs) replace <WebView2/> with <Border x:Name="ChatHost"/>; mount Reactor in code-behind via ReactorChatHostExtensions. - Drop WebView2-specific toolbar buttons (Refresh/Home/DevTools); keep Open-in-Browser via GatewayChatUrlBuilder. - ChatWindow preserves tray-popup positioning, tool-window styling, and hide-on-deactivate behavior. App lifecycle: - App.ChatProvider singleton created in InitializeGatewayClient and disposed in UnsubscribeGatewayEvents; both chat surfaces share it so state stays consistent across the Hub tab and tray popup. TFM bumps: - OpenClaw.Tray.WinUI MinSDK 10.0.19041.0 -> 10.0.22621.0 (matches vendored Reactor's minimum WinUI requirement). Misc: - Services/DeepLinkHandler trim trailing slash from path: workaround for a pre-existing DeepLinkParser bug where openclaw://agent/?... parses to path "agent/" instead of "agent". Should be fixed at the parser level on master too. - Resources.resw across all 5 locales: 10 obsolete WebView2-specific strings removed. - Helpers/GatewayChatHelper kept (still used by Onboarding overlay) but flagged as legacy/onboarding-only in the doc comment. - DEVELOPMENT.md: new "Native chat surface" section + project-structure update. Validation: - ./build.ps1 clean (Shared, Cli, WinNodeCli, WinUI all OK). - tests/OpenClaw.Shared.Tests: 1162 passed (no change). - tests/OpenClaw.Tray.Tests: 433 passed (was 388 baseline; +45 new OpenClawChatDataProviderTests). - Live tray verified end-to-end: chat.history loads transcript, chat.send + block-streamed assistant deltas + lifecycle terminate correctly, chat.abort wired through StopResponseAsync. Out of scope: Onboarding/Pages/ChatPage.cs and OnboardingWindow's WebView2 overlay still use WebView2 (separate flow, tracked separately). Per-message timestamps, token counts, and model badges in the timeline are not yet plumbed (next iterations). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes the most visible remaining gaps with the WebView2 web chat UI. Per-entry metadata (timestamp + active model) - New ChatEntryMetadata record + adapter-side parallel store (Dictionary<threadId, Dictionary<entryId, ChatEntryMetadata>>) maintained by OpenClawChatDataProvider; doesn't touch the vendored ChatTimelineItem. - Captures the message timestamp from ChatMessageInfo.Ts during chat.history load, from agent events, and uses local "now" for optimistic local-user entries. - Captures the model name from the active session at entry-creation time. - Public OpenClawChatDataProvider.GetEntryMetadata(threadId) returns a defensive copy so the renderer can read it from the UI thread without locking. OpenClawChatTimeline rendering - New OpenClawChatTimelineProps record (extends the upstream ChatTimelineProps fields) carrying EntryMetadata + sender labels + default model. OpenClawChatRoot constructs it. - User entries: pink bubble + 🧑 avatar + footer "<sender> · <h:mm tt>" (right-aligned). - Assistant entries: subtle bordered card + ★ avatar + footer "<agent> · <h:mm tt> · <model>" (left-aligned). Empty-text turns are skipped so we don't render a blank card during the start-of-turn gap. - Tool calls: prominent rounded card with status glyph (✓/✗/⋯), tool name in monospace semi-bold, truncated args, and a scrollable preview of ToolOutput (max ~160px, scrolls beyond) so multi-page exec output doesn't blow out the timeline. Footer shows "Tool · <h:mm tt>". - Reasoning entries: when text is present, render it as an italic muted panel with a "Reasoning" header instead of just the "thinking…" caption. Adapter touches - ApplyEventAndPublish gains an optional ChatEntryMetadata parameter; metadata is assigned to any newly-created entry IDs and never overwrites previously-captured metadata for the same id (so the original turn-start time wins over later delta arrivals). - LoadHistoryAsync rebuilds the per-thread metadata dict in lockstep with the rebuilt timeline, preserving metadata for entries that came in live before the history response arrived. - SendMessageAsync captures meta for the optimistic user entry. Tests (51 OpenClawChatDataProviderTests, was 45): - LoadHistoryAsync_CapturesPerEntryTimestamps - LoadHistoryAsync_AssignsModelFromActiveSession - SendMessageAsync_AssignsTimestampToLocalUserEntry - ChatMessageReceived_AssistantFinal_AssignsMetadata - GetEntryMetadata_MissingThread_ReturnsEmpty - GetEntryMetadata_ReturnsDefensiveCopy Validation: ./build.ps1 clean. Shared 1162 passed, Tray 439 passed. App not launched per session instructions. Out of scope for this iteration: per-message token counts, real avatars (emoji glyph stays for now), expand/collapse toggle on tool cards. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Groups consecutive User-after-User and Assistant-after-Assistant entries into visual "bursts" to reduce vertical clutter — matches the web Control UI's pattern. Mid-burst entries get: - No avatar on the user/assistant side (replaced by a 28px spacer so the bubbles still align with the burst's avatar slot). - No sender/time/model footer. - Tighter top/bottom margins (1px vs 8px) to visually fuse the cards. Burst boundaries are computed during the entries → renderedEntries map based on previous/next entry kinds. Tool/Status/Reasoning entries always break the burst (they aren't grouped). Also: - VStack gap between rendered entries reduced 4 → 2 px for tighter density overall. - The big multi-turn "wall of Field labels" visible in the iteration-1 screenshot collapses to a single avatar + footer per burst. Validation: build clean, tests pass (51 OpenClawChatDataProviderTests, 1162 Shared, 439 Tray total). App not launched per session instructions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an inline placeholder rendered at the bottom of the timeline when the chat surface is between turn-start and the first assistant byte arriving. Uses the same star avatar + italic muted caption as the streaming-empty assistant entry, so the user sees "Field is thinking…" under their just-sent prompt instead of an empty space below the input bar's "Assistant is working…" indicator. - New OpenClawChatTimelineProps.ShowThinkingIndicator parameter (default false) — opt-in so unit-test scenarios stay deterministic. - OpenClawChatRoot computes ShowThinkingIndicator = TurnActive AND (timeline empty OR last entry is not Assistant). Once the first delta arrives, the assistant entry takes over and the indicator hides automatically on the next render. Validation: build clean. Tray tests 439 passed (no behavior changed in the adapter). App not launched per session instructions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…atars Pulled the actual CSS from the gateway-served bundle (http://localhost:18789/assets/index-*.css) and matched the operator-side .chat-* rules exactly so the native WinUI surface looks like the web at the same gateway URL. Concrete changes: - Page background `#F7F2EC` (--bg from dash-light theme). - Assistant bubble: `#E8DDD2` (--bg-muted) bg + `#DDD0C2` (--border) border, 14px corner radius, 10/14 padding, max width 700px — matches `.chat-line.assistant .chat-bubble` in light mode. - User bubble: brown @ 20% (`openclaw#33-6E-48-28`, approximating `--accent-subtle` color-mixed onto cream) + matching border, same geometry. - Avatars switched from 28×28 circles (emoji on light gray) to 36×36 rounded squares (10px radius — matches `.chat-avatar` `var(--radius-md)`). Assistant: cream `#F0E8E0` (--panel-strong) bg with cocoa `#756050` (--muted) `★` glyph in semi-bold 13px. User: brown @ 20% bg with cocoa `#6E4828` (--accent) `🧑` glyph. - Footer ("stamp"): cocoa-gray `#756050` (--muted) at 11px, right- aligned for user, left-aligned for assistant — matches `.chat-stamp`. - Body text color `#4A3828` (--chat-text). - Avatar slot widths adjusted from 28→36 throughout (mid-burst spacers, inline thinking indicator). - Hardcoded assistant sender label to "Field" (was deriving from ChatThread.Title which is the operator client name like "OpenClaw Windows Tray (cli)" and surfaced under both bubbles confusingly). TODO: wire to real agent-name source (agents.list / hello-ok sessionDefaults.defaultAgentId) once available. Validation: Tray tests 51 OpenClawChatDataProviderTests still passing (no behavior change). App rebuilt + relaunched; new palette visible. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…st `ts`) Gateway 2026.4.23 returns `messages[].timestamp` (ms epoch) on chat.history responses, not `ts` as the spec doc suggested. Our parser was only reading `ts`, so per-entry timestamps came through as 0 and the footer rendered as "Field · gpt-5.5" with no time. Now reads `timestamp` first, falls back to `ts` for forward/back compat. Tests still pass (51 OpenClawChatDataProviderTests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the always-expanded tool-call card with the web Control UI's two-chip pattern: a 'Tool call <kind>' chevron-prefixed bubble and (once the result arrives) a 'Tool output <kind>' bubble. Each chip is a clickable Reactor Button that toggles its own expand state; collapsed shows just the chevron + lightning + label + monospace kind, expanded reveals the args (call) or output text (result/error) in a scrollable 240px-max code panel. Matches the dash-light .chat-tool-card visual treatment from the gateway's bundled CSS — radius 8, --bg-muted background, --border border, monospace 11px content. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a User interface section to the Settings page with a single ToggleSwitch (default Off) that flips the chat surface between the new native Reactor UI and the legacy WebView2-hosted gateway UI. * SettingsData / SettingsManager: persist new `UseLegacyWebChat` bool. * SettingsPage: new `UserInterfaceExpander` above Notifications with the toggle and a one-line caption explaining the behavior. * ChatPage (Hub) and ChatWindow (tray popup): host both surfaces (Reactor `ChatHost` + `WebView2`) in the same row and pick one at runtime based on the setting. Toolbar Home/Refresh/DevTools buttons are hidden in Reactor mode and shown in WebView2 mode. * Surface swaps live: ChatPage subscribes to HubWindow.SettingsSaved; ChatWindow subscribes to a new App.SettingsChanged event raised at the end of OnSettingsSaved (after the existing chat-window force- close path). App also exposes `Settings` so windows that aren't hub-owned can read the current preference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reverse-engineered the live gateway WS log: gateway 2026.4.x does not emit stream=tool agent events as the spec suggests. Tool lifecycle flows over stream=item (data.kind=tool, data.phase=start/end, data.title) plus stream=command_output (data.phase=end + text/output). Wire both into OpenClawChatDataProvider.MapAgentEvent: - item kind=tool start -> ChatToolStartEvent(title, kind) - command_output end -> ChatToolOutputEvent(text) - item kind=tool end -> empty output (marks Success for non-shell) - item kind=command -> ignored (child of parent tool) Also: bump Agent event log to 2000 chars; SettingsPage caption uses "chat interface" / "custom Windows interface" (no WebView2 jargon). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a tool chip is expanded, the body now renders three stacked
sections matching the web's `chat-tool-card` blocks:
* Inner header: wrench glyph + capitalized kind (e.g. "Exec",
"Process") in monospace SemiBold 13px, on a slightly darker band.
* Section label: uppercase, SemiBold 11px, with letter-spacing — one
of "TOOL INPUT" (call) or "TOOL OUTPUT" / "TOOL ERROR" (output).
* Code panel: monospace 11px on a cream-tinted background with a
bordered rounded box, line-height 16, scrollable beyond 280px.
Plus: tool input/output that looks like JSON (starts with `{` or `[`
and parses) is pretty-printed with 2-space indentation, mirroring the
web's syntax-highlighted format for action blobs like
`{"action":"poll","sessionId":"dawn-reef",...}`.
No model/protocol changes — pure visual.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The gateway flattens stream=item / command_output detail into plain
assistant text on the chat.history path (verified — the spec docs
this in chat.history's Important display-normalization section). So
historic turns lose the chip pipeline that live runs use.
Add two heuristics in OpenClawChatDataProvider.LoadHistoryAsync:
* LooksLikeSystemControlNote: messages starting with
"System (untrusted):" / "System:" route to a dim Status entry
instead of a full assistant bubble.
* LooksLikeFlattenedToolOutput: messages containing terminator
markers ("Process exited with code", "Command still running
(session", "Exec completed (") or starting with UNC/POSIX paths
(\\wsl.localhost\\..., /usr/, /home/, /var/, /etc/, /tmp/) are
surfaced as a synthetic ChatToolStartEvent + ChatToolOutputEvent
pair so they render as the same compact ▸ chips a live run does.
ClassifyFlattenedToolOutput picks an "exec" or "process" kind label
based on which marker matched, mirroring the live "item" extraction.
No protocol changes — purely client-side reconstruction.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Major UI overhaul aligning the native chat surface with kenehong/
native-chat-v2 styling while preserving the WebView-style tool chip
pipeline:
* New OpenClawComposer Reactor component replaces InputBar+StatusBar:
- Row 1: three compact ComboBoxes (Channel/Model/Reasoning), height
28, fontsize 11, corner-radius 4, padding 8x0.
- Row 2: multi-line TextBox, MinHeight 56, "Message Assistant
(Enter to send)" placeholder, Enter sends.
- Row 3: four right-aligned action buttons (Attach E723, Voice E720,
More E712, Send E724) with Kenny's #0078D4 accent on Send.
- Working spinner + permission banner kept above composer.
* OpenClawChatRoot drops the separate StatusBar row; routes model and
permission changes through the new composer's callbacks. Grid is now
4 rows (header / divider / body / composer) instead of 5.
* OpenClawChatTimeline palette switched from dash-light cocoa to theme
brushes (auto light/dark): user bubble = AccentFillColorDefaultBrush
+ TextOnAccentFillColorPrimaryBrush, assistant bubble =
SubtleFillColorSecondaryBrush + TextFillColorPrimaryBrush. Bubble
corner-radius bumped 14 -> 16. Avatars now circular (radius=size/2).
* ChatEntryMetadata extended with InputTokens/OutputTokens/
ResponseTokens/ContextPercent (nullable). Assistant footer now
rendered via BuildAssistantFooter as a multi-pill row matching the
WebView format: "Field 7:54 PM ↑1475 ↓12 R45.4k 23% ctx
gpt-5.5". Missing pieces silently drop.
* Bumped Chat event log capture from 200 to 2000 chars for ongoing
protocol spelunking.
No tool-chip changes — preserved the existing chevron+lightning
chip rendering per user direction.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Per-entry hover state (UseState<HashSet<string>>) tracks which message rows are under the pointer. Each user / assistant entry wraps its row in OnPointerEntered/Exited handlers via a new WithHoverHandlers helper that flips the entry id in the set. * HoverIcon helper renders a transparent FontIcon Button at Opacity 0 by default, full opacity when hovered. Hit-testing follows opacity so invisible icons don't intercept clicks. Hover bg uses SubtleFillColorSecondaryBrush so the affordance reads on both light and dark themes. * User bubbles now show a Trash glyph (E74D) to the LEFT of the bubble on hover. Click handler stubbed (TODO: wire to provider once delete API exists). * Assistant bubbles now show a Speak glyph (E767) to the RIGHT of the bubble on hover. Click handler stubbed (TODO: wire to TTS pipeline). * Tool chip expanded body brushes (blockBg/blockBorder/blockHeaderBg) switched from hardcoded dash-light cocoa to ControlFillColor*/ SubtleFillColor* / ControlStrokeColor* theme refs so they harmonize with the new Microsoft Fluent palette in both themes. * Removed the hardcoded 1px borders on user / assistant bubbles to match Kenny's Calm variation — accent fill / subtle gray fills read clearly without the extra stroke. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* New ChatSpeakHelper (in-process Windows SpeechSynthesizer wrapper) drives the assistant Speak icon. Independent of the gateway's tts.speak capability so the icon works in pure operator mode. Interrupt-on-click: Pause/Dispose any prior MediaPlayer before starting a new utterance. Caps at 4000 chars to avoid runaway TTS. * Composer Channel ComboBox now lists all available agent thread titles (snapshot.Threads.Title distinct). Selecting an entry calls selectedIdState.Set so the chat surface switches threads without needing the side rail. Composer signature gains AvailableChannels and OnChannelChanged. * Defensive usage extraction in OpenClawGatewayClient.HandleChatEvent: ExtractChatUsage walks several known shapes (usage / tokens / tokenUsage objects with input/output/total/promptTokens/ completionTokens/responseTokens/contextPercent variants) plus top-level fallbacks. Synthesizes total = input + output when only parts are reported. Stays nullable everywhere — the footer pills silently omit when nothing matches. * ChatMessageInfo gains InputTokens / OutputTokens / ResponseTokens / ContextPercent. Provider folds these into ChatEntryMetadata at end-of-turn (special-case: merge into existing entry's metadata when usage arrives on a delta that upserts an already-known entry). * All 1601 tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root cause: gateway's chat.history strips agent stream=item /
command_output detail and replays raw tool output as plain assistant
text. The previous LooksLikeFlattenedToolOutput heuristic only matched
the obvious exec terminator markers ("Process exited with code", etc.)
and POSIX path openings. CLI --help dumps (e.g. running ``openclaw
nodes invoke --help`` via an exec tool) carry none of those markers
and were rendering as huge markdown-formatted assistant bubbles with
H1 ``Options:`` headings — exactly what the user reported.
Strengthen the detector with three new signals (any one matches):
* Opens with the OpenClaw CLI version banner: ``OpenClaw 20...`` /
``OpenClaw v...`` / ``openclaw <verb>``. Catches every variant of
``--help`` output from the openclaw CLI family.
* Contains ``Usage:`` together with one of ``Options:`` / ``Commands:``
/ ``Examples:`` / ``Aliases:`` — generic CLI help layout for any
tool, not just openclaw.
* Has >= 5 ``--flag``/``-x`` tokens (regex ``s_cliFlagRegex``,
word-boundary aware) once the message is >= 200 chars. Dense flag
listings only show up in --help dumps.
Also exposed LooksLikeFlattenedToolOutput / LooksLikeSystemControlNote
/ ClassifyFlattenedToolOutput as ``internal`` and added InternalsVisibleTo
for OpenClaw.Tray.Tests so a new FlattenedToolOutputDetectionTests
suite (29 cases) locks in the recognition: terminator markers,
system-path openings, OpenClaw banner, Usage+Options/Commands layout,
dense flag listings, plus negative cases for normal prose and
short-text edge cases.
Bumped Wizard response payload log limit from 200 to 8000 chars so
the next history fetch is fully captured for any further reverse-
engineering needed.
All 468 tray tests pass (was 439, +29 new).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ROOT CAUSE found via [ChatHistory] diagnostic logging: the gateway (2026.4.x) emits chat.history messages with TWO roles I wasn't handling: * role="toolresult" — the actual exec/command output. Was falling through the switch's default branch and rendering as a giant assistant bubble. Now routed to a synthetic ChatToolStartEvent + ChatToolOutputEvent pair (chip pipeline) regardless of whether the heuristic matches, since the role itself confirms it's tool data. Also accept "tool_result" as a defensive alias. * role="user" with text starting "System (untrusted):" / "System:" — the gateway wraps internal exec result reports as user-role messages. Was rendering as a normal user bubble. Now routed to a dim Status entry, mirroring the existing assistant-role handling. Added [ChatHistory] Logger.Debug instrumentation in the per-message loop: logs role, length, heuristic results (flat/sys), text preview, and routing decision. Critical for spelunking future role variants. Live verification: tray PID 22964 reloaded the chat tab and the log now shows toolresult entries routing to "TOOL chip (role=toolresult, kind='exec')" / "kind='process'", and the System (untrusted) user notes routing to "SYSTEM (dim status, role=user with control prefix)". Tests: added ToolresultRoleAlwaysClassified covering "(no output)", PowerShell errors, and JSON blobs that the heuristic doesn't match on its own. 471 tray tests pass (was 468, +3). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adopt the requested subset of design tokens from kenehong/native-chat-v2 ComponentLibraryWindow + Cat04ToolsPage + NativeChatThread, while preserving our own chip / Reactor architecture. Bubble dimensions (Kenny's exact values): * CornerRadius 16 -> 10 (both user + assistant) * MaxWidth 700 -> 560 * User bubble margin 60,8,12,8 -> 64,4,8,4 (top/bottom 4px gap so consecutive turns sit closer together, matching Kenny's tighter feel) * Assistant bubble margin 12,8,60,8 -> 8,4,64,4 Tool chip status pill (Kenny's Cat04 palette): * Replaced single ✓/✗/⋯ glyph with a colored capsule on the output chip header: "Done" #FF28A050 / "Running" #FFDC781E / "Error" SystemFillColorCriticalBrush — white text, CornerRadius 10, Padding 6,1, matching the web Control UI's Running / Done labels. * Lightning ⚡ glyph color follows the pill so the header still reads status at a glance. Reasoning entry (Kenny's NativeChatThread ThinkingBlock pattern): * Was: muted Border with "Reasoning" SemiBold + italic body always visible. Now: WinUI Expander with header "🧠 Thinking", collapsed by default, body in monospace 12px tertiary. Outer border gets a subtle blue stroke (#FF648CB4) to visually distinguish thought from tool/output. Empty-state caption tweaked to "🧠 thinking…". System notice (Kenny's Cat10 centered colored pill): * Was: dim left-aligned caption inset. Now: centered Border with glyph + text, capsule shape (CornerRadius 12), tinted background at ~18% opacity — `ℹ` for normal status, `⚠` + crimson tint for errors. More visible without crowding the conversation. Skipped (deliberate): * MarkdownPresenter — Kenny's only handles paragraphs / fenced code / inline code / links. Ours (Reactor's full Markdown lib) handles headings, tables, lists, blockquotes — strictly more capable. * Inline always-visible tool card design — flagged as a sensitive area; we keep our compact collapsible chips. All 471 tray tests still pass. No protocol/schema changes — purely visual. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Right-side avatar was VAlign.Bottom and 36px tall, forcing the FlexRow to 36px. The bubble itself was VAlign-Stretch, so a 1-line text bubble pinned its content to the top of the row. Center both bubble and avatar so short user prompts sit centered in their bubble's vertical space. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move padding from TextBlock to the Border itself so that 1-line and multi-line user bubbles render consistently: - Border.Padding (14, 8, 14, 8) gives real horizontal breathing room around the text and symmetric top/bottom padding so the text is naturally centered. - Border.VerticalAlignment = Center centers the bubble within the FlexRow (so the avatar circle and the bubble share the same midline). - No MinHeight: bubble auto-sizes to its content, avoiding the 2-line clipping issue we hit with a fixed minimum height. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Conflicts: # src/OpenClaw.Shared/OpenClawGatewayClient.cs # src/OpenClaw.Tray.WinUI/App.xaml.cs # src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs # src/OpenClaw.Tray.WinUI/Services/SettingsManager.cs # src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw # src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw # src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw # src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw # src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw # src/OpenClaw.Tray.WinUI/Windows/ChatWindow.xaml.cs
Add a 'Debug Overrides' section to the Debug page (above Debug Actions) that lets engineers force the legacy Gateway WebView or the native Companion (Reactor) chat UI on each chat container independently — useful for side-by-side comparison without flipping the global 'Use standard Gateway Chat interface' setting. - New OpenClawTray.Chat.DebugChatSurfaceOverrides: per-process, per-surface (HubChat / TrayChat) override with a Changed event. Resets every app launch (engineering aid only, not persisted). - ChatPage and ChatWindow now consult ResolveUseLegacy(override, Settings.UseLegacyWebChat) and subscribe to Changed for live re-mount. - ChatWindow.RefreshCredentials no longer re-shows the WebView unconditionally; it now respects _webViewMode so the Reactor surface isn't clobbered when the user opens the tray popup with 'Force Companion Chat UI' active. Also: Move padding from inner Markdown to the Border in the assistant bubble so wrapped multi-line content (e.g. 'Heard: Opus 4.7') no longer clips at the bottom edge — same root cause as the user-bubble clipping fixed in 6de38b4. Validated: build OK; OpenClaw.Shared.Tests 1442/1442 pass; OpenClaw.Tray.Tests 785/785 pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lize strings
A multi-part cleanup pass to prepare the native chat work for first
review. No user-visible feature changes; visual behaviour is identical.
== Project structure ==
- Move external/reactor/samples/apps/chat/Chat.{Model,UI}/ into
src/OpenClaw.Chat.Model and src/OpenClaw.Chat.UI (these are part of
our app, not vendored samples).
- Collapse Chat.Model + Chat.UI into a single src/OpenClaw.Chat project
(TFM net10.0-windows10.0.22621.0, namespace OpenClaw.Chat). 6 source
files, no .UI/.Model split.
- Tests project keeps its pure net10.0 TFM by including the model files
(ChatModels.cs, ChatTimelineReducer.cs) directly via <Compile Include>
rather than ProjectReference.
- Rename namespaces ChatSample.Chat.* -> OpenClaw.Chat.
== Reactor pruning ==
- Drop the Reactor.Analyzers vendored project (we don't pack Reactor
ourselves, so the analyzer DLL bundling is dead). Saves ~80 KB.
- Drop dead Chat.UI files that we replaced with our own components:
ChatTimeline, InputBar, StatusBar, Sidebar, LandingPage,
SessionListItem (all replaced by OpenClawChatTimeline /
OpenClawComposer / OpenClawChatRoot).
- Keep SessionHeader.cs + ChatUiHelpers.cs (still consumed).
Note: Reactor's own Core/Hosting/Element are tightly coupled to
Charting/Animation/Hooks/Input/Controls/Yoga, so those cannot be
trimmed without forking the framework.
== Speak hover icon ==
Remove the (non-functional) Speak hover icon on assistant bubbles plus
the ChatSpeakHelper that backed it. Will be re-added later when wired
to a real TTS pipeline.
== Localization ==
Bring the chat surface in line with the rest of the codebase, which
uses x:Uid in XAML and OpenClawTray.Helpers.LocalizationHelper.GetString
in C#, with .resw files under Strings/{en-us,fr-fr,nl-nl,zh-cn,zh-tw}/.
- DebugPage.xaml: x:Uid all 8 strings I added previously for the
'Debug Overrides' section (Hub/Tray Chat UI override combos).
- 33 new Chat_* keys for the Reactor chat code:
- Status pills (Done / Running / Error)
- Tool chip labels (Tool call / Tool output / Tool input / Tool error)
- Reasoning expander header (Thinking)
- Composer placeholders, tooltips (Attach / Voice / More / Send / Stop)
- Permission buttons (Allow / Deny)
- Notification messages (Send failed, Failed to load history, ...)
- Composer reasoning options (Default / Auto / Maximum)
- Empty-state captions
- All 5 locales translated natively (not English placeholders), passing
LocalizationValidationTests' all-or-none-translation assertion.
- OpenClawChatDataProvider.cs gets a tiny LocalizationHelper shim under
#if OPENCLAW_TRAY_TESTS so the test project (no WinAppSDK reference)
still compiles. Mirrors the pattern in DeepLinkHandler.cs.
== Per-surface chat UI override (uncommitted from prior session) ==
(Already in commit d84287e but mentioning here for context.) The
DebugPage 'Debug Overrides' section, which lets engineers force the
legacy WebView or the native Reactor chat per chat container without
flipping the global setting, picked up its localization work in this
commit.
== Validation ==
- ./build.ps1: all 4 projects build clean.
- OpenClaw.Shared.Tests: 1442/1442 passed (22 skipped, pre-existing).
- OpenClaw.Tray.Tests: 785/785 passed, including all
LocalizationValidationTests (parity, placeholder parity,
all-or-none translation, no duplicates).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A rubber-duck critique of this branch (post-merge with upstream/master)
flagged 7 issues. This commit addresses 6 of them; the 7th (split the
1300-line OpenClawChatDataProvider into mapper/replayer/store) is
deliberately deferred — it's a structural cleanup that would be safer
as its own dedicated pass and risks bigger merge conflicts than its
benefit warrants right now.
== HIGH 1 — Snapshots are now real snapshots ==
ChatTimelineState was a record carrying mutable List<>/HashSet<>;
the reducer mutated those collections in place; BuildSnapshotLocked
only shallow-copied the dictionary. Past snapshots could mutate.
- ChatModels.cs: Entries → ImmutableList; LocalNonces → ImmutableHashSet.
- ChatTimelineReducer.cs: rewrote all mutation sites to use Add /
SetItem / Remove returning new instances. Hot-path operations remain
O(log n) — no ToImmutableList rebuilds.
- BuildSnapshotLocked no longer needs deep-copy; immutables handle it.
== HIGH 2 — History/live merge no longer duplicates entries ==
LoadHistoryAsync rebuilt history with fresh sequential IDs (e1, e2…)
then blindly appended live-arrived entries that also start at e1, e2…
Result: ID collisions, duplicate bubbles, broken hover/expand state.
- Re-IDs prior entries when their ID collides with a rebuilt entry's
(sequential schemes are guaranteed to clash).
- NextId rebuilt from the max numeric suffix of existing IDs.
- Content+timestamp dedup is gated: ONLY drops a prior entry as a
semantic duplicate when both sides have non-zero timestamps within
±2 seconds. Missing timestamps fall back to ID-only dedup. Prefers
occasional visible duplication over silent transcript loss.
== HIGH 3 — Hub ChatPage now refreshes its WebView credentials ==
ChatPage cached _chatUrl in Initialize() and reused it on every
SettingsSaved. After pairing or settings changes, the WebView kept
navigating with the old URL+token. (ChatWindow already had a
RefreshCredentials path; ChatPage didn't.)
- TryComputeChatUrl(settings) helper extracted.
- ApplyChatSurface() recomputes _chatUrl from current settings on
every call — covers Init, SettingsSaved, and debug-override
changes uniformly.
== HIGH 4 — Logging audit (no chat content / token leakage) ==
Multiple log paths captured user prompts, assistant text, tool args,
and full chat URLs with ?token=… query strings. TokenSanitizer only
redacts token-like values, not free-form chat content.
- ChatWindow.RefreshCredentials: SafeLogUrl helper strips
query-string + fragment from logged URLs.
- OpenClawGatewayClient.HandleChatEvent: raw payload preview replaced
with role/state/len shape.
- OpenClawGatewayClient wizard payload log: full sanitized body
dropped — replaced with kind={ValueKind} len={N}.
- OpenClawGatewayClient agent event log: 2000-char raw JSON dropped
in favour of stream={X} len={N}.
- OpenClawGatewayClient tool label log: stripped — labels embed
user-provided command/query/url values. Now logs tool name + phase.
- OpenClawChatDataProvider [ChatHistory] debug logs: 120-char content
preview dropped, kept role/len/flat/sys shape diagnostics.
== MEDIUM 5 — Disconnect mid-turn no longer leaves UI 'thinking' ==
When the gateway dropped while a turn was in flight (TurnActive=true),
the timeline kept showing the streaming-in-progress indicator forever.
- OnStatusChanged now detects Connected → Disconnected/Error
transitions and, for every thread with TurnActive=true, synthesises
a localized 'Connection lost — response interrupted.' status entry
+ a ChatTurnEndEvent via the existing reducer. No direct mutation.
- New Chat_Notification_ConnectionInterrupted resource key in all
5 locales (en-us / fr-fr / nl-nl / zh-cn / zh-tw).
- Only fires once per disconnect (does not flap on repeated
Disconnected statuses).
== MEDIUM 6 — ChatPage no longer leaks debug-override subscription ==
OnUnloaded now also -= DebugChatSurfaceOverrides.Changed, alongside
the existing -= SettingsSaved. Initialize is also defensive (-= then
+=) to guard against duplicate subscriptions.
== Test coverage ==
5 new tests in OpenClawChatDataProviderTests.cs:
- LoadHistoryAsync_AfterLiveActivity_DoesNotDuplicateEntries
- LoadHistoryAsync_AfterLiveActivity_PreservesNonDuplicateLiveEntries
- LoadHistoryAsync_WithMissingTimestamps_PreservesAllLiveEntries
- LoadHistoryAsync_WithSameTextDifferentTimestamps_PreservesBoth
- Disconnect_DuringActiveTurn_InjectsInterruptionAndEndsTurn
== Validation ==
Build clean (4/4 projects).
OpenClaw.Shared.Tests: 1442 passed / 22 skipped.
OpenClaw.Tray.Tests: 790 passed (was 785 → +5 net new).
== Deferred ==
LOW 7: OpenClawChatDataProvider does many jobs (gateway sub +
history replay + event mapping + reducer + metadata + reconnect
policy). Would be cleaner split into 3, but the file is currently
correct and the split is invasive. Tracked for a separate pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…chat
A security-focused rubber-duck review of the native chat surface (which
renders markdown content from a partially-trusted gateway) found 1 HIGH
+ 3 MEDIUM issues, plus a follow-up pass added 1 CRITICAL escalation
(autolink clickability). All addressed.
== HIGH 1 — Remote image fetches blocked (SSRF / privacy / tracking) ==
The Reactor Markdown component fetched http(s) image URLs into
BitmapImage / SvgImageSource by default. A compromised gateway, tool,
or prompt-injected model could trigger outbound HTTP requests from the
tray app (tracking pixels, internal-network probing, beacon attacks).
- New ChatMarkdownSanitizer pre-processes text before Markdown parses
it: rewrites  to '[Image: alt]' plaintext + flattens
reference-link defs.
- Defense-in-depth: _markdownOptions.Image callback returns an inert
Caption rather than constructing a BitmapImage, catching reference-
style images and any sanitizer misses.
- Sanitizer respects code spans, fenced code blocks (including those
with up to 3 leading spaces), and indented code blocks (4+ spaces or
tab) to preserve developer pastes verbatim.
== CRITICAL — Autolink/raw-URL hyperlinks blocked ==
After the first sanitizer pass, autolinks (<https://...>) and bare
URLs detected by md4c were STILL being turned into clickable
RichTextHyperlink controls. Reactor's MarkdownOptions.LinkBuilder
hook was declared but never invoked from MarkdownBuilder.LeaveLink.
- Surgical 3-line fix in vendored Reactor's MarkdownBuilder.cs:
LeaveLink now dispatches to Options.LinkBuilder when set, falling
back to RichTextHyperlink otherwise. Tightened the callback type to
Func<RichTextInline[], Uri, RichTextInline?>? to fit the natural
inline-buffer slot.
- Our chat _markdownOptions.LinkBuilder returns an inert RichTextRun
showing 'text (url)' as plain text — no NavigateUri, not clickable.
- Documented as a known local edit in external/reactor/README.md and
external/reactor/NOTICE.md (alongside the existing TFM bump).
== MEDIUM 2 — Live System / toolresult messages no longer dropped ==
OnChatMessageReceived only accepted role=assistant and silently
discarded role=user-with-System-prefix and role=toolresult/tool_result
messages. History DID render these as dim-status / tool chips — so live
events disappeared from the operator's view. Untrusted provenance
hidden.
- Live path now mirrors history:
- role=user with LooksLikeSystemControlNote → ChatStatusEvent (dim)
- role=toolresult / role=tool_result → ChatToolStartEvent +
ChatToolOutputEvent chip pair
- Plain role=user echoes still ignored (no behavioural change there).
== MEDIUM 3 — Untrusted assistant content rendered as inert text ==
When history flattens tool output into role=assistant, our heuristics
(LooksLikeFlattenedToolOutput) try to detect this and route to a
chip. Misses fall through to a normal Markdown bubble. A malicious
tool output that avoids the heuristic could be replayed as trusted-
looking assistant prose with active links.
Resolved as a side effect of CRITICAL+HIGH 1 fixes: every assistant
Markdown bubble now goes through the sanitizer + inert link/image
callbacks regardless of provenance, so the worst case is plain text
with the URL visible (not navigable).
== MEDIUM 4 — Oversized content cannot DoS the chat UI ==
No client-side size cap on assistant/reasoning/status/tool/history
text. Reactor's Markdown component throws above 4 MiB. A 5+ MiB
gateway payload could throw, hang, or break the UI.
- New const MaxEntryTextBytes = 256 * 1024 (256 KB) per message.
- TruncateForChatEntry uses binary search to a UTF-8 byte boundary
(surrogate-safe; not full grapheme-safe, but sufficient).
- TruncateChatEvent applied centrally in ApplyEventAndPublish so all
text-bearing event types are truncated:
AssistantDelta, AssistantFinal, UserMessage, ToolStart, ToolOutput,
Status, Reasoning, ContextChanged, ChannelChanged, ChatHistory,
ToolError, ModelChanged, IntentEvent, PermissionRequest.
- Truncation appends ' … [N bytes truncated]' marker; logged at Debug
level only (not user-visible noise).
== MEDIUM 2 follow-up — Tightened LooksLikeSystemControlNote ==
The original heuristic matched any text starting with 'System:' or
'System (untrusted):', which would hijack legitimate user messages
that happened to start with that text. Now requires BOTH:
1. The 'System (untrusted):' / 'System:' prefix, AND
2. A known structural marker emitted by the gateway:
'Exec completed (', 'Process exited with code', 'Command still
running (session', 'An async command you ran', 'Tool reported',
'exec result for', 'tool_call_', 'Reset session'.
Documented as gateway-shape-coupled: a wording change on the gateway
side would silently downgrade real system notes to full bubbles. Worth
adding a cross-repo contract test in a future pass.
== Test coverage (52 net new tests across this round) ==
ChatMarkdownSanitizerTests (NEW file, ~25 cases):
- Image flattening (standard + reference-style + nested in link)
- Link flattening + autolinks
- FlattenLinkToInertText helper (5 edge cases)
- Code span / fenced / indented code preservation
- Raw HTML <img> treated as text
OpenClawChatDataProviderTests:
- OnChatMessageReceived_LiveToolResult_RendersAsToolChip
- OnChatMessageReceived_LiveToolResult_AlternateRoleSpelling
- OnChatMessageReceived_LiveUserSystemNote_RendersAsStatus
- OnChatMessageReceived_LiveUserPlain_StillIgnored
- OnChatMessageReceived_OversizedContent_IsTruncated
- OnAgentEvent_OversizedToolOutput_IsTruncated
- TruncateForChatEntry: 4 boundary cases + surrogate-pair safety
FlattenedToolOutputDetectionTests:
- LooksLikeSystemControlNote_OnRealSystemNote_ReturnsTrue (8 cases)
- LooksLikeSystemControlNote_OnPlainUserMessageWithSystemPrefix_ReturnsFalse (5 cases)
- TruncateChatEvent coverage for ChatModelChangedEvent,
ChatIntentEvent, ChatPermissionRequestEvent.
== Validation ==
Build clean (4/4 projects).
OpenClaw.Shared.Tests: 1442 passed / 22 skipped.
OpenClaw.Tray.Tests: 842 passed (was 790 before security pass → +52 net new).
== Known follow-ups ==
- _markdownOptions.HtmlBlock is not overridden. Raw HTML blocks reach
Reactor's default. The sanitizer leaves <img> HTML alone, but a
separate audit of Reactor's HTML rendering path is warranted.
- LooksLikeSystemControlNote's marker list is gateway-shape coupled.
Recommend a shared schema or cross-repo contract test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ChatWindow: re-apply SystemBackdrop on first activation. Pre-warmed
windows that were never shown didn't attach the backdrop controller,
so acrylic appeared blank until toggled from the exploration panel.
- ChatWindow XAML: drop x:Uid on popout/close tooltips so the inline
Content (Open in Companion app / Close) wins over stale .resw values.
- Title bar icons: FontSize popout=14, close=11 to visually match the
18px app-icon image and Segoe glyph weights.
- OnPopout now routes to App.ShowHub("chat") instead of opening the
external chat URL in a browser.
- TrayMenuWindow + chat surface backgrounds made transparent when the
active backdrop is Mica/Acrylic so the host SystemBackdrop shows.
- Plus the rest of the ChatExploration v2 work (presets, composer pill
dropdown, agent avatar, send-button styling, header layout).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Conflicts resolved: - ChatWindow.xaml.cs: keep both using directives (OpenClaw.Chat for new namespace + Microsoft.UI.Composition.SystemBackdrops for backdrop fix) - OpenClawComposer.cs: keep ChatExploration v2 conditional Send/Stop + hover states; rename ChatSample.Chat.UI.Res -> OpenClaw.Chat.Res; switch s_reasoningOptions -> ReasoningOptions() (localized) - OpenClawChatTimeline.cs: keep AssistantAvatar() + bubbleSideMargin; drop deleted ChatSpeakHelper reference; use LocalizationHelper for AssistantThinkingFormat Adapt v2 files to refactored namespace and immutable collections: - FakeChatDataProvider: List/HashSet -> ImmutableList/ImmutableHashSet - FakeChatDataProvider, ChatExplorationsWindow: ChatSample.* -> OpenClaw.Chat Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Localize composer button AutomationName via LocalizationHelper (Send/Stop/Attach/Voice/More) - Add AutomationProperties.Name + x:Name to title-bar popout/close buttons; correct ChatWindowOpenInBrowserToolTip resw value to 'Open in Companion app' so x:Uid is safe to re-enable - Esc-to-close keyboard accelerator on ChatWindow content root - Auto-focus composer TextBox on first activation via VisualTreeHelper recursion - AssistantAvatar AutomationName so screen readers announce sender - LiveRegion(Polite) on thinking indicator so streaming status is announced - Bump timestamp/footer/'is thinking' foreground to Secondary brush when chat surface is transparent over Mica/Acrylic backdrop Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Assistant bubble footer: hover reveals Copy + Read aloud at the END of the metadata row (right of timestamp/model) - User bubble footer: hover reveals Copy + Delete on the LEFT, with sender + timestamp anchored at the far right (matches reading direction for right-aligned bubbles) - Wrap each entry in a transparent Border so the WHOLE bounding box (bubble + footer + the gap between) is hit-testable; fixes the bug where moving the pointer down to a hover-revealed icon briefly exited the hover area and dropped the click - Soft pill-shaped icon buttons (Light weight glyph, 13px corner radius) for a calmer look next to the timestamp pills - Singleton SpeechSynthesizer + MediaPlayer for Read aloud so a second click cancels the previous utterance instead of stacking voices - Light markdown stripper so the synthesizer doesn't read backticks, asterisks, link brackets, etc. - Resw keys: Chat_Assistant_Action_Copy / _ReadAloud / Chat_User_Action_Delete Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Harden raw HTML rendering by making the inert text path explicit and document the gateway-coupled system note heuristic. Add native chat coverage for history role routing, retry after history load failure, assistant usage metadata merging, and truncation across rendered chat event fields. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add localized strings for merged chat bubble actions and update the assistant content event test to match the deduped gateway event path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
UseState.Value is a render-time snapshot, so the 700ms Task.Delay continuation read a stale set and Clear() bailed at Contains(key) - leaving the checkmark stuck. Switch ackedActions to UseReducer so the updater always sees the live value; apply the same pattern to the PointerExited prefix-clear path for consistency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move chat provider lifecycle and read-aloud playback out of App.xaml.cs into a dedicated coordinator while preserving the gateway-backed chat transcript used by the native surface. Also keep the native timeline's empty-thread state visible and update the user sender label to match the tray identity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Hide delete hover icon on user bubbles. The action was a no-op that
flashed AckAction's success glyph, misleading users. Restore once the
chat provider can remove prompts from both timeline and gateway
history.
- Close TTS init/dispose race in OpenClawChatCoordinator: take _gate
around the fallback TextToSpeechService initialization and throw
ObjectDisposedException when disposed, preventing leaked native audio
handles on concurrent first-use or dispose-during-init.
- Localize the 256 KB truncation marker in OpenClawChatDataProvider via
a new Chat_TruncationMarkerFormat resource string with a {0} byte
placeholder, added to all five locales.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Emit one chat message for multi-block gateway content - Dispose gateway chat bridge subscriptions with providers - Remount native chat surfaces when the provider changes - Mute capture during manual read-aloud - Keep UI updates on the dispatcher thread - Harden chat truncation around surrogate boundaries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
OpenClawComposer permission banner: - Drop accent fill on Allow so Allow/Deny share the same neutral button treatment (better light/dark/HC contrast; doesn't visually promote the risky action per Fluent guidance). - Vertically center text + buttons in the row. - Switch HStack to Grid([Star, Auto, Auto]) so buttons sit on the right with text taking remaining width, capped via Border MaxWidth=720 to avoid a huge gap on wide windows. - Remove gray SubtleFill background and divider stroke; bump padding to (12,16,12,16) and margin to (24,16,24,16) for breathing room. Explorations / chat root / timeline: - Pre-existing in-progress work on the chat exploration surfaces (preview state plumbing, fake provider, timeline rendering) bundled into the same commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 1 of the native WinUI 3 chat rewrite (see PR openclaw#315 for the Reactor-based reference). OpenClaw.Chat was a separate Windows-targeted project that mixed pure-C# model/reducer types with Reactor-coupled UI. Splitting them across project boundaries created a circular-dependency risk for the upcoming native rewrite (the new XAML controls live in OpenClaw.Tray.WinUI but need the model types). Collapsing it into OpenClaw.Tray.WinUI removes that constraint with no behavior change. Moves (namespace OpenClaw.Chat preserved so consumers keep compiling): - src/OpenClaw.Chat/ChatModels.cs -> src/OpenClaw.Tray.WinUI/Chat/ChatModels.cs - src/OpenClaw.Chat/ChatTimelineReducer.cs -> src/OpenClaw.Tray.WinUI/Chat/ChatTimelineReducer.cs - src/OpenClaw.Chat/ChatUiHelpers.cs -> src/OpenClaw.Tray.WinUI/Chat/ChatUiHelpers.cs - src/OpenClaw.Chat/SessionHeader.cs -> src/OpenClaw.Tray.WinUI/Chat/SessionHeader.cs (still Reactor-based; will be replaced as XAML in a later phase) Removed: - src/OpenClaw.Chat/ project entirely (GlobalUsings + csproj deleted) - ProjectReference to OpenClaw.Chat in OpenClaw.Tray.WinUI.csproj - Solution entry in openclaw-windows-node.slnx Test project updated to compile-include the moved files from their new location. Reactor reference and external/reactor/ are untouched in this commit; they go in a later phase once the new native ChatTimelineView is in place. Validation: - ./build.ps1 (Cli, Shared, WinNodeCli, WinUI all succeed) - dotnet test tests/OpenClaw.Shared.Tests: 1455 passed, 22 skipped - dotnet test tests/OpenClaw.Tray.Tests: 1079 passed, 0 skipped Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Thanks for pushing this forward. The native WinUI chat direction looks promising, and I appreciate the amount of work here. The main thing blocking merge for me right now is not the chat UX itself, but the size and shape of the vendored Reactor dependency. This PR currently brings in a very large I do not think we should merge the full Reactor snapshot as-is. I would like this PR to either split the vendored runtime from the chat UI, or trim Reactor down to the minimum runtime needed by the OpenClaw chat surface before merge. A practical trim path would be:
The goal is not to block native chat; it is to avoid taking ownership of a full UI framework plus unused charting/devtools/data-grid systems when this feature appears to need only the core declarative runtime, markdown rendering, flex layout, hooks, and host bridge. Once the Reactor tree is trimmed or split into a focused prerequisite PR, this will be much easier to review and much safer to merge. |
|
One additional point on the Reactor dependency: we already have an internal declarative WinUI layer in this repo at That existing layer already provides several of the same concepts this PR is bringing in through Reactor:
So before we take a second declarative UI framework into the repo, I think this PR needs an explicit decision point:
The likely FunctionalUI gaps for this chat surface seem concrete and bounded: markdown/rich text rendering, combo boxes/flyouts, better keyed list reconciliation, and streaming timeline performance. Those may be solvable as targeted additions to our own FunctionalUI project without taking ownership of a full external UI framework. If the final decision is still to use Reactor, then I think the earlier trim request still stands: bring only the minimal Reactor runtime needed for those missing capabilities, not the full upstream snapshot. |
Phase 1: Visual Polish & Theme Alignment - Replace hardcoded colors with Fluent theme brushes (AccentFillColor, SubtleFillColor, ControlStroke, TextFillColor variants) - Circular 36x36 avatars: accent circle + user glyph, subtle circle + assistant icon (configurable via ContentPresenter) - Code blocks render as styled Border cards with rounded corners, language header, and CardBackgroundFillColor background - Remove red border from popout button, replace emoji with FontIcon Phase 2: Rich Metadata Footer - Add SenderLabel, ModelName, InputTokens, OutputTokens, ContextPercent fields on ChatMessage (generic, set by IChatService implementations) - MetadataFooter computed property: 'Field · 7:54 PM · ↑1.5k · ↓12 · gpt-5.5' - AssistantMessageControl renders footer after content completion - GatewayChatService extracts model/tokens from lifecycle events - MockChatService populates mock metadata Phase 3: Per-Message Actions - MessageActionRequested event on ChatPanel for consumer wiring (ReadAloud) - ChatMessageAction enum: Copy, ReadAloud, Delete - ChatMessageActionEventArgs for typed event handling Phase 4: Enhanced Tool Call Rendering - Tool-specific display icons: grep→🔍, glob→📂, web_fetch→🌐, bash→$ - Two independently expandable sections: Args (JSON pretty-printed) and Output (full result text) with separate expand/collapse buttons - ArgsJson and ToolOutput properties on ToolCallInfo - GatewayChatService extracts tool args from phase:start data.args and full output from phase:result data.result.content - ToolCallCard.xaml with SubtleFillColorTertiaryBrush + rounded corners Phase 5: Reasoning/Thinking Blocks - ChatReasoningEvent on IChatService for reasoning text deltas - ReasoningContent + HasReasoning properties on ChatMessage - ReasoningBlock.xaml — collapsible with '💭 Reasoning' header, dimmed italic text, expand/collapse toggle - AssistantMessageControl shows ReasoningBlock above content - GatewayChatService detects isReasoning:true in assistant stream events - MockChatService emits fake reasoning before responses Phase 6: Status Rows & Markdown Security - MessageRole.Status + ChatTone enum (Info/Success/Warning/Error/Dim) - StatusTemplate in ChatPanel.xaml — centered toned pill - ChatStatusEvent on IChatService, ChatViewModel adds Status messages - ChatMarkdownSanitizer — pre-sanitizes before Markdig: images→[Image: alt], links→inert 'text (url)', HTML tags stripped, code blocks/spans preserved during scan - MarkdownRenderer.Render() now calls Sanitize() first Phase 7: Load More History & Scroll Polish - MessageTemplateSelector supports StatusTemplate - (Paginated history deferred — requires gateway cursor support) 20 files changed, 70 tests (was 49), all passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new ToolBurstStyle.TaskList variant that renders each step as a row with a status icon (check for done, ProgressRing for in-progress, x for errored), mirroring the AgentRunCard Running steps / Completed steps pattern from native-chat-v2. - ChatExplorationState: register TaskList enum value; default _toolBurstStyle to TaskList - ChatExplorationsPanel: expose TaskList in the tool-burst picker - FakeChatDataProvider: switch the demo exec step to InProgress + add a follow-up assistant bubble so the new style has live data to render - OpenClawChatTimeline: implement the per-step list rendering Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Conflicts: # src/OpenClaw.Shared/OpenClawGatewayClient.cs # src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs # src/OpenClaw.Tray.WinUI/Services/SettingsManager.cs # src/OpenClaw.Tray.WinUI/Windows/ChatWindow.xaml.cs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Run FunctionalUI effect cleanups when chat hosts are disposed so timers and subscriptions do not leak. Clear stale WebView chat navigation when switching surfaces or when credentials are unavailable.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
This PR adds the native WinUI chat experience for the OpenClaw Windows node and makes it selectable/debuggable from
the tray app.
Native chat experience
permission prompts, status rows, and per-message actions.
tool output, and hover-revealed bubble actions.
theme-aware colors, avatars, and footer actions.
Settings and debug options
fallback scenarios.
instead of navigating to an invalid chat URL.
Gateway and chat behavior
tool calls, and tool results.
responses.
Stability and review fixes
behavior, provider event handling, and chat data edge cases.
Validation
.\build.ps1dotnet test .\tests\OpenClaw.Shared.Tests\OpenClaw.Shared.Tests.csproj --no-restoredotnet test .\tests\OpenClaw.Tray.Tests\OpenClaw.Tray.Tests.csproj --no-restoreComing investigation
Alternative approaches are being investigated: using WinUI 3's ListView with KeepLastItemInView, or ScrollView + ItemsRepeater without the use of reactor.