Skip to content

Native WinUI chat experience#315

Merged
shanselman merged 53 commits into
openclaw:masterfrom
RBrid:user/rbrid/ChatUI2
May 12, 2026
Merged

Native WinUI chat experience#315
shanselman merged 53 commits into
openclaw:masterfrom
RBrid:user/rbrid/ChatUI2

Conversation

@RBrid
Copy link
Copy Markdown
Contributor

@RBrid RBrid commented May 12, 2026

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

  • Adds the native WinUI chat surface as an alternative to the standard Gateway Chat WebView.
  • Renders user/assistant turns natively, including message grouping, assistant reasoning, tool calls/tool results,
    permission prompts, status rows, and per-message actions.
  • Adds native chat timeline controls such as copy, read aloud, delete affordances, compact tool chips, expanded
    tool output, and hover-revealed bubble actions.
  • Aligns the native chat visuals with the current OpenClaw/Fluent styling, including acrylic/chat shell polish,
    theme-aware colors, avatars, and footer actions.

Settings and debug options

  • Adds the Settings option to use the standard Gateway Chat interface instead of the native WinUI chat surface.
  • Adds per-surface chat UI override/debug support so native vs standard chat can be toggled for validation and
    fallback scenarios.
  • Improves tray/chat launch behavior when chat credentials or chat URLs are unavailable, showing an error state
    instead of navigating to an invalid chat URL.

Gateway and chat behavior

  • Wires native chat to gateway chat history and live chat events.
  • Handles cumulative assistant stream/final messages, reasoning text, lifecycle/status events, permission events,
    tool calls, and tool results.
  • Preserves history replay ordering while keeping live assistant final-message reconciliation from duplicating
    responses.
  • Routes read-aloud through the configured TTS path and guards fallback TTS cleanup.

Stability and review fixes

  • Fixes stuck native-chat spinner state after lifecycle/provider errors.
  • Fixes duplicated assistant responses when final assistant messages arrive after lifecycle end.
  • Improves input responsiveness by avoiding timeline re-renders on every composer keystroke.
  • Hardens markdown sanitization around malformed links/images, reference definitions, and bounded scan behavior.
  • Adds regression coverage for reducer turn lifecycle, duplicate assistant reconciliation, markdown sanitizer
    behavior, provider event handling, and chat data edge cases.

Validation

  • .\build.ps1
  • dotnet test .\tests\OpenClaw.Shared.Tests\OpenClaw.Shared.Tests.csproj --no-restore
  • dotnet test .\tests\OpenClaw.Tray.Tests\OpenClaw.Tray.Tests.csproj --no-restore

Coming investigation

Alternative approaches are being investigated: using WinUI 3's ListView with KeepLastItemInView, or ScrollView + ItemsRepeater without the use of reactor.

RBrid and others added 30 commits May 6, 2026 19:36
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 ![alt](src) 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>
RBrid and others added 16 commits May 11, 2026 10:35
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>
@RBrid RBrid marked this pull request as ready for review May 12, 2026 02:14
RBrid added a commit to RBrid/openclaw-windows-node that referenced this pull request May 12, 2026
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>
@shanselman
Copy link
Copy Markdown
Contributor

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 external/reactor tree as a project reference from OpenClaw.Tray.WinUI. From the PR file list, this is roughly 291 Reactor source files inside a 348-file PR, and it adds broad subsystems that the OpenClaw chat surface does not appear to use: charting/D3, data grid/property grid/validation controls, devtools/MCP/debug hosting, ETW/layout-cost tooling, data providers, etc. That is a lot of long-term code ownership and dependency surface for a single chat feature.

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:

  1. Start with a compile-trim rather than guessing file-by-file. In external/reactor/src/Reactor/Reactor.csproj, temporarily exclude unused feature folders with Compile Remove, then build iteratively.

    <ItemGroup>
      <Compile Remove="Charting\**\*.cs" />
      <Compile Remove="Controls\**\*.cs" />
      <Compile Remove="Data\**\*.cs" />
      <Compile Remove="Hosting\Devtools\**\*.cs" />
      <Compile Remove="Hosting\Etw\**\*.cs" />
      <Compile Remove="Hosting\LayoutCost\**\*.cs" />
    </ItemGroup>
  2. Keep the pieces that the current chat implementation actually depends on:

    • Core
    • Elements
    • Hooks
    • the minimal Hosting pieces needed for ReactorHost
    • Markdown
    • Yoga / Microsoft.UI.Reactor.Layout, since FlexRow, FlexColumn, .Flex(...), and FlexPanel depend on it
  3. Remove package references that only support excluded features. For example, Microsoft.Diagnostics.Tracing.TraceEvent looks tied to ETW/layout-cost support, and System.Drawing.Common looks tied to preview/devtools screenshot capture. If those folders are removed, those package refs should go too.

  4. After the compile trim passes, physically delete the excluded folders rather than leaving dead vendored code in the repo.

  5. Update external/reactor/README.md and NOTICE.md to describe this as a trimmed Reactor runtime, not a full upstream snapshot. Please include:

    • which folders were retained
    • which folders were intentionally removed
    • why the retained folders are needed by OpenClaw chat
    • how to refresh from upstream and reapply the local edits
  6. Validate the trimmed result with at least:

    dotnet build external\reactor\src\Reactor\Reactor.csproj
    dotnet build src\OpenClaw.Tray.WinUI\OpenClaw.Tray.WinUI.csproj -r win-x64
    .\build.ps1
    dotnet test .\tests\OpenClaw.Shared.Tests\OpenClaw.Shared.Tests.csproj --no-restore
    dotnet test .\tests\OpenClaw.Tray.Tests\OpenClaw.Tray.Tests.csproj --no-restore

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.

@shanselman
Copy link
Copy Markdown
Contributor

One additional point on the Reactor dependency: we already have an internal declarative WinUI layer in this repo at src/OpenClawTray.FunctionalUI/FunctionalUI.cs, and it is actively used by the onboarding flow.

That existing layer already provides several of the same concepts this PR is bringing in through Reactor:

  • Component / Component<TProps>
  • UseState
  • UseEffect
  • props-driven rendering
  • FunctionalHostControl
  • factories for common WinUI controls
  • VStack, HStack, Grid, ScrollView
  • navigation primitives
  • tests under tests/OpenClawTray.FunctionalUI.Tests

So before we take a second declarative UI framework into the repo, I think this PR needs an explicit decision point:

  1. Preferably, build the native chat surface on top of OpenClawTray.FunctionalUI and extend that internal layer with only the primitives chat needs.
  2. If Reactor is still required, please document exactly what FunctionalUI cannot currently do and why those gaps are better solved by vendoring Reactor instead of extending our in-repo layer.

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.

ranjeshj added a commit to ranjeshj/openclaw-windows-node that referenced this pull request May 12, 2026
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>
kenehong and others added 7 commits May 12, 2026 11:56
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>
@shanselman shanselman merged commit 08cab69 into openclaw:master May 12, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants