Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,27 +230,26 @@ Useful local scripts:

## Architecture Overview

### Native chat surface (FunctionalUI + OpenClaw.Chat)
### Native chat surface (Reactor + OpenClaw.Chat)

The Hub Chat tab (`src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml`) and the
tray ChatWindow popup (`src/OpenClaw.Tray.WinUI/Windows/ChatWindow.xaml`)
render their conversations with native WinUI 3 controls via the in-repo
`OpenClawTray.FunctionalUI` helper and `OpenClaw.Chat` model/reducer code.
Reactor components and `OpenClaw.Chat` model/reducer code.
The standard WebView2-hosted gateway web client remains available as a
settings-controlled fallback.

**Layering:**

```
src/OpenClaw.Tray.WinUI/Chat/ OpenClawChatTimeline · OpenClawComposer · OpenClawSessionHeader
src/OpenClaw.Tray.WinUI/Chat/ OpenClawReactorChatRoot · ReactorChatTimeline · ReactorChatComposer
OpenClawChatDataProvider (adapts OpenClawGatewayClient → IChatDataProvider)
OpenClawChatRoot (FunctionalUI component composing the chat surface)
FunctionalChatHostExtensions (mounts FunctionalUI into a XAML <Border>)
IChatGatewayBridge (testability seam over OpenClawGatewayClient)
ReactorChatHostExtensions (mounts Reactor into a XAML <Border>)
IChatGatewayBridge (testability seam over OpenClawGatewayClient)
▲ depends on
src/OpenClaw.Chat/ ChatThread · ChatTimelineState · IChatDataProvider · ChatTimelineReducer
▲ rendered by
src/OpenClawTray.FunctionalUI/ Component · RenderContext · FunctionalHostControl · WinUI elements
Reactor.WinUI Component · hooks · ReactorHostControl · WinUI elements
```

**Lifecycle:**
Expand All @@ -259,12 +258,13 @@ src/OpenClawTray.FunctionalUI/ Component · RenderContext · FunctionalHostCon
created in `InitializeGatewayClient` and disposed inside
`UnsubscribeGatewayEvents`. Both the Hub Chat tab and the tray ChatWindow
consume the same provider - opening either surface shows identical state.
- Each XAML host (`ChatPage`, `ChatWindow`) mounts its own `FunctionalHostControl`
with `ContentTarget` pointing at a `<Border x:Name="ChatHost"/>`. The
- `ReactorChatHostExtensions` mounts a dedicated `ReactorHostControl` for each
XAML surface (`ChatPage`, `ChatWindow`) as the child of its
`<Border x:Name="ChatHost"/>`. The
surrounding chrome (NavigationView, popup header) stays XAML.
- Provider events fire on the WebSocket-receive thread; the provider
marshals `Changed` / `NotificationRequested` callbacks through a
dispatcher post delegate (`DispatcherQueue.AsPost()`), so FunctionalUI
dispatcher post delegate (`DispatcherQueue.AsPost()`), so Reactor
components observe state on the UI thread.

**Adding new chat behavior:** model new events in `OpenClaw.Chat`'s
Expand Down
8 changes: 4 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ These are the canonical homes. Do not reintroduce private copies elsewhere.
| --- | --- |
| `src/OpenClaw.Tray.WinUI/App.xaml.cs` | `IWindowManager`, `ITrayController`, `IActivationRouter`, `ISettingsChangeCoordinator`, `AppBootstrapper` |
| `src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs` | `ChatSendQueue`, `ChatBridgeEventPump`, `ChatHistoryLoader`, `ChatSnapshotProjector`, `AttachmentMetadataStore`; pure native tool projection stays in `NativeToolProjector` |
| `src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs` | `ReactorChatTimeline` (production `ItemsView` / `ItemContainer`), `ChatBubbleRenderer`, `ToolCallCardRenderer`, `PermissionRequestCard`, `AttachmentBubbleRenderer` |
| `src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs` | `ComposerViewModel`, `SlashCommandPalette`, `AttachmentPreviewStrip`, `VoiceComposerController` |
| `src/OpenClaw.Tray.WinUI/Chat/ReactorChatTimeline.cs` | `ChatBubbleRenderer`, `PermissionRequestCard`, `AttachmentBubbleRenderer`; tool rendering stays in `ToolCallCardRenderer` |
| `src/OpenClaw.Tray.WinUI/Chat/OpenClawReactorChatRoot.cs` | `ComposerViewModel`, `SlashCommandPalette`, `AttachmentPreviewStrip`, `VoiceComposerController` |
| `src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs` | `ConnectionPagePlan` (pure), `ConnectionPageViewModel`, gateway row models |
| `src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs` | settings read/persist → `SettingsPageViewModel` + `ISettingsStore`; keep gateway-uninstall, uptime timer, saved-indicator, and app-info in the view |
| `src/OpenClaw.Tray.WinUI/Services/NodeService.cs` | `McpServerHost`, `CanvasWindowManager`, `MediaCapabilityHost`, `RecordingConsentService`, `NodeCapabilityRegistry` |
Expand Down Expand Up @@ -135,11 +135,11 @@ leading and trailing pipe. Columns, in order:
| navigation-scope | authoritative | src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml.cs | page view-model activation/deactivation and disposal lifetime | NavigationScopeManager | HubWindow keeps frame navigation back-stack and rail selection | transient page view models are activated on navigation and deactivated then disposed on navigate-away | NavigationScopeManagerTests.NavigatingAway_DeactivatesAndDisposesPreviousViewModel | behavioral | - |
| composition-root | authoritative | src/OpenClaw.Tray.WinUI/App.xaml.cs | presentation-layer service construction and wiring | AppServiceRegistration | App remains the composition root and owns non-DI service lifetimes | one validated root ServiceProvider; App-owned singletons registered as instances are never disposed by the container | AppServiceRegistrationTests.Dispose_DoesNotDisposeAppOwnedInstanceSingletons | behavioral | - |
| node-summary-text | authoritative | src/OpenClaw.Tray.WinUI/App.xaml.cs | node-summary clipboard text formatting | NodeSummaryText | App keeps the clipboard side effect (building the DataPackage and setting clipboard content) | copied node-summary text is projected only by NodeSummaryText.Build (online/offline state, display-name fallback, short id, detail text, newline join) | NodeSummaryTextTests.Build_MultipleNodes_OneLinePerNodeJoinedByNewline | behavioral | - |
| reactor-chat-timeline | authoritative | src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs | production chat message virtualization, row realization, and imperative scroll follow | ReactorChatTimeline through OpenClawReactorChatRoot and ReactorHostControl | OpenClawChatTimeline remains a legacy focused-test surface while its runtime route is migrated | the default chat route mounts one direct ReactorHostControl per XAML chat target; Reactor owns stable-key ItemsView and ItemContainer realization without a custom native list, collection reconciler, or scroll-layout mutation | review-only: user explicitly deferred new tests for this migration; required build and existing shared/tray suites still run | review-only | when Reactor timeline proof coverage replaces the legacy focused UI host coverage |
| reactor-chat-timeline | authoritative | removed legacy FunctionalUI chat timeline | production chat message virtualization, row realization, and imperative scroll follow | ReactorChatTimeline through OpenClawReactorChatRoot and ReactorHostControl | - | the default chat route mounts one direct ReactorHostControl per XAML chat target; Reactor owns stable-key ItemsView and ItemContainer realization without a custom native list, collection reconciler, or scroll-layout mutation | ChatTimelinePresentationTests.ReactorTimeline_UsesNonSelectableItemsViewContainersAndAnnotatedScrollBar | source-shape | when ReactorChatTimeline is replaced as the production virtualization owner |
| chat-tool-activity-renderer | authoritative | src/OpenClaw.Tray.WinUI/Chat/ReactorChatTimeline.cs | production standalone tool-call and grouped activity presentation, summaries, disclosures, and detail rendering | ChatToolActivityPresentation + ToolCallCardRenderer | ReactorChatTimeline projects rows and delegates realization only | consecutive invocation grouping preserves source chronology; stable group identity comes from session, generation, and first tool entry; selectable output remains capped at 240px | ChatToolActivityPresentationTests.Project_GroupsOnlyConsecutiveSpansOfAtLeastTwoTools | behavioral | - |
| chat-history-replay-projection | authoritative | src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs | array-valued history content ordering projection | ChatHistoryReplayProjection | provider applies projected text and tool parts to the reducer | interleaved text, calls, and results replay in source order without clearing active tool correlation | OpenClawChatDataProviderTests.LoadHistoryAsync_InterleavedContentParts_PreserveChronologyAndCorrelation | behavioral | - |
| reactor-tool-rendering-closed | closed | src/OpenClaw.Tray.WinUI/Chat/ReactorChatTimeline.cs | per-tool and grouped activity summary/detail rendering implementation | ToolCallCardRenderer | row projection, virtualization, hover state, assistant runs, and renderer delegation only | ReactorChatTimeline contains no tool detail renderer and delegates both standalone and grouped tool rows | ChatTimelinePresentationTests.ReactorTimeline_DelegatesToolAndActivityRenderingToFocusedOwner | source-shape | when ReactorChatTimeline is replaced as the production virtualization owner |
| functional-chat-default-mount | closed | src/OpenClaw.Tray.WinUI/Chat/FunctionalChatHostExtensions.cs | mounting the FunctionalUI chat tree as the default ChatPage or ChatWindow surface | ReactorChatHostExtensions and OpenClawReactorChatRoot | legacy FunctionalUI chat files may remain for focused compatibility coverage only | ChatPage and ChatWindow mount the Reactor root directly into their existing ChatHost Borders; no FunctionalUI component mounts or nests Reactor on the default path | review-only: user explicitly deferred new tests for this migration; required build and existing shared/tray suites still run | review-only | when legacy FunctionalUI chat surfaces are removed |
| functional-chat-default-mount | closed | src/OpenClaw.Tray.WinUI/Chat/FunctionalChatHostExtensions.cs | mounting the FunctionalUI chat tree as the default ChatPage or ChatWindow surface | ReactorChatHostExtensions and OpenClawReactorChatRoot | - | ChatPage and ChatWindow mount the Reactor root directly into their existing ChatHost Borders through ReactorChatHostExtensions | ChatToolCallsToggleContractTests.ProductionChatSurfaces_MountReactorRoot | source-shape | when ReactorChatHostExtensions is replaced as the authoritative production chat mount owner |
| settings-store | authoritative | src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs | hand-rolled save/echo suppression flags for two-way settings binding | ISettingsStore | PermissionsPage and other surfaces may read SettingsManager directly until migrated | a save originating from Update does not echo Changed to the caller and external saves are republished on the UI thread | SettingsStoreTests.Update_DoesNotEchoChangedToSelf | behavioral | when all settings surfaces read and write through ISettingsStore |
| settings-page-vm | authoritative | src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs | settings load, persist, echo-guard, and auto-save wiring | SettingsPageViewModel | code-behind keeps gateway-uninstall, gateway-info and uptime timer, saved-indicator visual, and app-info population | each settings control persists its field through the store preserving mutate-save-notify order and does not re-persist on external change | SettingsPageViewModelTests.ExternalChange_ReloadsWithoutRePersisting | behavioral | when the Settings page holds no settings persistence logic in code-behind |
<!-- LEDGER:END -->
Expand Down
4 changes: 2 additions & 2 deletions src/OpenClaw.Shared/OpenClawGatewayClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1993,8 +1993,8 @@ private void ClearPendingRequests()
// to preserve the chat approval banner on disconnect-mid-flight. The
// OperationCanceledException thrown here is intentionally a connection
// lifecycle signal, NOT a benign cancel. RunFireAndForget in the tray
// (OpenClawChatRoot) silently swallows OperationCanceledException
// if a caller forwards the OCE up to RunFireAndForget instead of
// chat root silently absorbs OperationCanceledException.
// If a caller forwards the OCE up to RunFireAndForget instead of
// catching it locally, the banner will be cleared with no UI feedback.
// Today OpenClawChatDataProvider.RespondToPermissionAsync correctly
// catches Exception ex; do not narrow that catch.
Expand Down
44 changes: 0 additions & 44 deletions src/OpenClaw.Tray.WinUI/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,7 @@
<SolidColorBrush x:Key="ConnectionCapabilityPillSuccessBrush" Color="{ThemeResource SystemFillColorSuccess}" Opacity="0.14" />
<SolidColorBrush x:Key="ConnectionCapabilityPillCautionBrush" Color="{ThemeResource SystemFillColorCaution}" Opacity="0.14" />
<SolidColorBrush x:Key="ConnectionCapabilityPillCriticalBrush" Color="{ThemeResource SystemFillColorCritical}" Opacity="0.14" />
<SolidColorBrush x:Key="ChatUserBubbleSelectionHighlightBrush" Color="{ThemeResource SystemAccentColorDark2}" />
<x:Double x:Key="ChatAccessibleBorderThickness">1</x:Double>
<x:Boolean x:Key="HubNavigationUseHighContrastIcons">False</x:Boolean>

<!-- Composer scrim: the chat timeline dissolves into the composer dock via a
vertical fade from transparent to the base surface fill. FunctionalUI
resolves this as a whole brush per ElementTheme and reads it out of the
visual tree, so the stops use LITERAL colors (a {ThemeResource} color
would re-resolve against the ambient app theme and read light on a dark
page). The values mirror SolidBackgroundFillColorBase (dark #202020) so
the fade lands seamlessly on the dock. -->
<LinearGradientBrush x:Key="ChatComposerFadeBrush" StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#00202020" />
<GradientStop Offset="1" Color="#FF202020" />
</LinearGradientBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="SetupIndicatorAccentBrush" Color="{ThemeResource SystemAccentColorDark1}" />
Expand All @@ -66,15 +52,7 @@
<SolidColorBrush x:Key="ConnectionCapabilityPillSuccessBrush" Color="{ThemeResource SystemFillColorSuccess}" Opacity="0.14" />
<SolidColorBrush x:Key="ConnectionCapabilityPillCautionBrush" Color="{ThemeResource SystemFillColorCaution}" Opacity="0.14" />
<SolidColorBrush x:Key="ConnectionCapabilityPillCriticalBrush" Color="{ThemeResource SystemFillColorCritical}" Opacity="0.14" />
<SolidColorBrush x:Key="ChatUserBubbleSelectionHighlightBrush" Color="{ThemeResource SystemAccentColorDark2}" />
<x:Double x:Key="ChatAccessibleBorderThickness">1</x:Double>
<x:Boolean x:Key="HubNavigationUseHighContrastIcons">False</x:Boolean>

<!-- Light mirror of ChatComposerFadeBrush (SolidBackgroundFillColorBase light #F3F3F3). -->
<LinearGradientBrush x:Key="ChatComposerFadeBrush" StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#00F3F3F3" />
<GradientStop Offset="1" Color="#FFF3F3F3" />
</LinearGradientBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="SetupIndicatorAccentBrush" Color="{ThemeResource SystemColorHighlightColor}" />
Expand All @@ -89,32 +67,10 @@
<SolidColorBrush x:Key="ConnectionCapabilityPillSuccessBrush" Color="{ThemeResource SystemColorWindowColor}" />
<SolidColorBrush x:Key="ConnectionCapabilityPillCautionBrush" Color="{ThemeResource SystemColorWindowColor}" />
<SolidColorBrush x:Key="ConnectionCapabilityPillCriticalBrush" Color="{ThemeResource SystemColorWindowColor}" />
<SolidColorBrush x:Key="ChatUserBubbleSelectionHighlightBrush" Color="{ThemeResource SystemColorHighlightColor}" />
<x:Double x:Key="ChatAccessibleBorderThickness">2</x:Double>
<x:Boolean x:Key="HubNavigationUseHighContrastIcons">True</x:Boolean>

<!-- High-contrast follows the system window color so the scrim stays legible. -->
<LinearGradientBrush x:Key="ChatComposerFadeBrush" StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="Transparent" />
<GradientStop Offset="1" Color="{ThemeResource SystemColorWindowColor}" />
</LinearGradientBrush>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>

<Style x:Key="ChatUserBubbleSelectionStyle" TargetType="RichTextBlock">
<Setter Property="SelectionHighlightColor" Value="{ThemeResource ChatUserBubbleSelectionHighlightBrush}"/>
</Style>
<Style x:Key="ChatToolCardBorderStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
<Setter Property="BorderBrush" Value="{ThemeResource ControlStrokeColorDefaultBrush}"/>
<Setter Property="BorderThickness" Value="{ThemeResource ChatAccessibleBorderThickness}"/>
</Style>
<Style x:Key="ChatCompactionCardStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
<Setter Property="BorderBrush" Value="{ThemeResource ControlStrokeColorDefaultBrush}"/>
<Setter Property="BorderThickness" Value="{ThemeResource ChatAccessibleBorderThickness}"/>
</Style>

<!-- Hub NavigationView selection indicator follows the user's Windows accent. -->
<SolidColorBrush x:Key="NavigationViewSelectionIndicatorForeground"
Color="{ThemeResource SystemAccentColor}"/>
Expand Down
2 changes: 1 addition & 1 deletion src/OpenClaw.Tray.WinUI/Chat/ChatMarkdownSanitizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ internal static class ChatMarkdownSanitizer
/// <summary>
/// Flatten a parsed Markdown link's display text + destination URI
/// into a single inert plain-text string. Used by the
/// <c>OpenClawChatTimeline</c> rendering path so
/// <c>ReactorChatTimeline</c> rendering path so
/// that links the parser DOES emit (bare URLs, autolinks
/// <c>&lt;https://…&gt;</c>) collapse to non-clickable text instead
/// of <see cref="System.Windows.Documents.Hyperlink"/>-style runs.
Expand Down
25 changes: 25 additions & 0 deletions src/OpenClaw.Tray.WinUI/Chat/ChatTimelinePresentationContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using OpenClaw.Chat;

namespace OpenClawTray.Chat;

/// <summary>
/// Presentation inputs shared by the Reactor chat timeline and its focused card renderers.
/// </summary>
public sealed record ChatTimelinePresentationContext(
string? SessionId,
IReadOnlyList<ChatTimelineItem> Entries,
bool HasMoreHistory,
Action? OnLoadMoreHistory,
IReadOnlyDictionary<string, ChatEntryMetadata>? EntryMetadata = null,
long TimelineGeneration = 0,
string UserSenderLabel = "OpenClaw Windows Tray",
string AssistantSenderLabel = "Field",
string? DefaultModel = null,
string? DefaultUsageSummary = null,
bool ShowThinkingIndicator = false,
bool ShowToolCalls = true,
int ToolCallsCollapseVersion = 0,
Func<string, Task>? OnReadAloud = null,
Action? OnStopSpeaking = null,
int ScrollToBottomToken = 0,
Action<string, string>? OnPermissionResponse = null);
Loading
Loading