Skip to content

Commit 9acafa6

Browse files
karkarlCopilotsteipeteshanselman
authored
feat(chat): migrate chat surface to Reactor (#1055)
* build: add Reactor dependency Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * feat(chat): migrate chat page to Reactor Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * feat(chat): add annotated scrollbar Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * improve(chat): match seeded composer design Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * improve(chat): refine message timeline interactions Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(chat): position initial history at tail Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(chat): rearm initial tail positioning Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(chat): harden composer paste and tail positioning Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * ci: add ClawSweeper dispatch workflow * Fix chat session picker visibility Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7b9bc681-ce19-4a8b-b06c-94efb63a5f9f * feat(chat): restore slash command palette Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(chat): honor display settings Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(chat): suppress no_reply directives Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(chat): settle returning-user welcome Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(chat): bound queued message list Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(chat): preserve dismissed slash palette Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * chore(chat): checkpoint stable items view Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(chat): follow native items view state Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(chat): follow items view viewport Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(chat): scope Reactor diagnostics Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: Scott Hanselman <scott@hanselman.com> Copilot-Session: 7b9bc681-ce19-4a8b-b06c-94efb63a5f9f
1 parent bc210e0 commit 9acafa6

42 files changed

Lines changed: 4562 additions & 604 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,70 @@ dotnet_naming_style.s_camel_case.required_prefix = s_
138138
# source files and suppress it only for the generated type-info file.
139139
[**/XamlTypeInfo.g.cs]
140140
dotnet_diagnostic.CS0612.severity = none
141+
142+
# Reactor analyzers apply project-wide, but the existing imperative WinUI/XAML
143+
# surfaces are not Reactor render trees. Keep their compatibility exceptions
144+
# local so new Reactor-owned surfaces remain checked by default.
145+
[src/OpenClaw.Tray.WinUI/A2UI/**/*.cs]
146+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
147+
148+
[src/OpenClaw.Tray.WinUI/App.xaml.cs]
149+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
150+
151+
[src/OpenClaw.Tray.WinUI/Helpers/**/*.cs]
152+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
153+
154+
[src/OpenClaw.Tray.WinUI/Helpers/InstanceManagementControls.cs]
155+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
156+
157+
[src/OpenClaw.Tray.WinUI/Pages/**/*.cs]
158+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
159+
160+
[src/OpenClaw.Tray.WinUI/Services/**/*.cs]
161+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
162+
163+
[src/OpenClaw.Tray.WinUI/Services/UpdateCoordinator.cs]
164+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
165+
166+
[src/OpenClaw.Tray.WinUI/Windows/**/*.cs]
167+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
168+
169+
[src/OpenClaw.Tray.WinUI/Chat/ReactorChatHostExtensions.cs]
170+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
171+
172+
[src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs]
173+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
174+
175+
[src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs]
176+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
177+
178+
[src/OpenClaw.Tray.WinUI/Pages/ConfigPage.xaml.cs]
179+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
180+
181+
[src/OpenClaw.Tray.WinUI/Pages/ChannelsPage.xaml.cs]
182+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
183+
184+
[src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs]
185+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
186+
187+
[src/OpenClaw.Tray.WinUI/Pages/DebugPage.xaml.cs]
188+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
189+
190+
[src/OpenClaw.Tray.WinUI/Pages/SandboxPage.xaml.cs]
191+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
192+
193+
[src/OpenClaw.Tray.WinUI/Pages/SessionsPage.xaml.cs]
194+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
195+
196+
[src/OpenClaw.Tray.WinUI/Windows/ChatWindow.xaml.cs]
197+
dotnet_diagnostic.REACTOR_DIALOG_001.severity = none
198+
199+
[src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs]
200+
dotnet_diagnostic.REACTOR_THEME_004.severity = none
201+
202+
[src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs]
203+
dotnet_diagnostic.REACTOR_A11Y_001.severity = none
204+
205+
[src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs]
206+
dotnet_diagnostic.REACTOR_A11Y_001.severity = none
207+
dotnet_diagnostic.REACTOR_THEME_004.severity = none

docs/ARCHITECTURE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ These are the canonical homes. Do not reintroduce private copies elsewhere.
7373
| --- | --- |
7474
| `src/OpenClaw.Tray.WinUI/App.xaml.cs` | `IWindowManager`, `ITrayController`, `IActivationRouter`, `ISettingsChangeCoordinator`, `AppBootstrapper` |
7575
| `src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs` | `ChatSendQueue`, `ChatBridgeEventPump`, `ChatHistoryLoader`, `ChatSnapshotProjector`, `AttachmentMetadataStore` |
76-
| `src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs` | `TimelineScrollController`, `ChatBubbleRenderer`, `ToolCallCardRenderer`, `PermissionRequestCard`, `AttachmentBubbleRenderer` |
76+
| `src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs` | `ReactorChatTimeline` (production `ItemsView` / `ItemContainer`), `ChatBubbleRenderer`, `ToolCallCardRenderer`, `PermissionRequestCard`, `AttachmentBubbleRenderer` |
7777
| `src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs` | `ComposerViewModel`, `SlashCommandPalette`, `AttachmentPreviewStrip`, `VoiceComposerController` |
7878
| `src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs` | `ConnectionPagePlan` (pure), `ConnectionPageViewModel`, gateway row models |
7979
| `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 |
@@ -132,6 +132,8 @@ leading and trailing pipe. Columns, in order:
132132
| 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 | - |
133133
| 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 | - |
134134
| 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 | - |
135+
| 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 |
136+
| 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 |
135137
| 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 |
136138
| 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 |
137139
<!-- LEDGER:END -->

src/OpenClaw.Chat/ChatModels.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ public record ChatDataSnapshot(
216216
IReadOnlyList<OpenClaw.Shared.GatewayCommand>? AvailableCommands = null,
217217
bool CommandsSupported = true,
218218
IReadOnlyDictionary<string, long>? TimelineGenerations = null,
219+
IReadOnlyDictionary<string, long>? HistoryRevisions = null,
219220
IReadOnlyDictionary<string, IReadOnlyList<ChatQueuedMessage>>? QueuedMessagesByThread = null);
220221

221222
/// <summary>

src/OpenClaw.Shared/Models.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1803,7 +1803,7 @@ public class ChatMessageInfo
18031803

18041804
public static bool IsSilentAssistantDirective(string? role, string? text) =>
18051805
string.Equals(role, "assistant", StringComparison.OrdinalIgnoreCase) &&
1806-
string.Equals(text?.Trim(), SilentAssistantDirective, StringComparison.Ordinal);
1806+
string.Equals(text?.Trim(), SilentAssistantDirective, StringComparison.OrdinalIgnoreCase);
18071807

18081808
/// <summary>Session this message belongs to (e.g. "main").</summary>
18091809
public string SessionKey { get; set; } = "";

src/OpenClaw.Tray.WinUI/App.xaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@
6868
<SolidColorBrush x:Key="SetupIndicatorAccentBrush" Color="{ThemeResource SystemColorHighlightColor}" />
6969
<SolidColorBrush x:Key="SetupInactiveDotBrush" Color="{ThemeResource SystemColorGrayTextColor}" />
7070

71-
<SolidColorBrush x:Key="SolidBackgroundFillColorBaseBrush" Color="{ThemeResource SolidBackgroundFillColorBase}" />
72-
<SolidColorBrush x:Key="ControlFillColorDefaultBrush" Color="{ThemeResource ControlFillColorDefault}" />
73-
<SolidColorBrush x:Key="ControlStrokeColorDefaultBrush" Color="{ThemeResource ControlStrokeColorDefault}" />
74-
<SolidColorBrush x:Key="SubtleFillColorSecondaryBrush" Color="{ThemeResource SubtleFillColorSecondary}" />
75-
<SolidColorBrush x:Key="SubtleFillColorTertiaryBrush" Color="{ThemeResource SubtleFillColorTertiary}" />
76-
<SolidColorBrush x:Key="TextFillColorSecondaryBrush" Color="{ThemeResource TextFillColorSecondary}" />
71+
<SolidColorBrush x:Key="SolidBackgroundFillColorBaseBrush" Color="{ThemeResource SystemColorWindowColor}" />
72+
<SolidColorBrush x:Key="ControlFillColorDefaultBrush" Color="{ThemeResource SystemColorButtonFaceColor}" />
73+
<SolidColorBrush x:Key="ControlStrokeColorDefaultBrush" Color="{ThemeResource SystemColorButtonTextColor}" />
74+
<SolidColorBrush x:Key="SubtleFillColorSecondaryBrush" Color="{ThemeResource SystemColorButtonFaceColor}" />
75+
<SolidColorBrush x:Key="SubtleFillColorTertiaryBrush" Color="{ThemeResource SystemColorButtonFaceColor}" />
76+
<SolidColorBrush x:Key="TextFillColorSecondaryBrush" Color="{ThemeResource SystemColorWindowTextColor}" />
7777

7878
<!-- High-contrast follows the system window color so the scrim stays legible. -->
7979
<LinearGradientBrush x:Key="ChatComposerFadeBrush" StartPoint="0,0" EndPoint="0,1">

src/OpenClaw.Tray.WinUI/App.xaml.cs

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ private async Task OnLaunchedAsync(LaunchActivatedEventArgs args)
604604
_settings = new SettingsManager();
605605
// Seed chat tool-call visibility from persisted settings so the timeline
606606
// honors the Settings > Chat "Show tool calls and usage" toggle on launch.
607-
OpenClawTray.Chat.OpenClawChatRoot.SetToolCallsVisible(_settings.ShowChatToolCalls);
607+
OpenClawTray.Chat.OpenClawReactorChatRoot.SetToolCallsVisible(_settings.ShowChatToolCalls);
608608
_previousSettingsSnapshot = _settings.ToSettingsData().ToConnectionSnapshot();
609609
_openTelemetryConnection = new OpenTelemetryEndpointConnection();
610610
await _openTelemetryConnection.ApplyAsync(
@@ -615,7 +615,7 @@ await _openTelemetryConnection.ApplyAsync(
615615
new AppLogger(),
616616
_dispatcherQueue is null
617617
? null
618-
: OpenClawTray.Chat.FunctionalChatHostExtensions.AsPost(_dispatcherQueue));
618+
: OpenClawTray.Chat.ReactorChatHostExtensions.AsPost(_dispatcherQueue));
619619
DiagnosticsJsonlService.Configure(DataPath);
620620

621621
// Central observable model + gateway event handler.
@@ -3466,6 +3466,12 @@ private void OnSettingsCommandCenterRequested(object? sender, EventArgs e)
34663466

34673467
private void OnSettingsSaved(object? sender, EventArgs e)
34683468
{
3469+
if (_settings is not null)
3470+
{
3471+
OpenClawTray.Chat.OpenClawReactorChatRoot.SetToolCallsVisible(
3472+
_settings.ShowChatToolCalls);
3473+
}
3474+
34693475
var currentSnapshot = _settings?.ToSettingsData()?.ToConnectionSnapshot();
34703476
var impact = SettingsChangeClassifier.Classify(_previousSettingsSnapshot, currentSnapshot);
34713477
_previousSettingsSnapshot = currentSnapshot;
@@ -4512,9 +4518,6 @@ public Task SpeakChatTextAsync(string text) =>
45124518

45134519
/// <summary>
45144520
/// Sets speaker mute from any surface (chat window, chat page, voice settings) and persists it.
4515-
/// This public path is NOT store-self-write-suppressed, so an open Settings page still reflects
4516-
/// a mute toggled elsewhere. The Settings-page-originated call goes through the explicit
4517-
/// <see cref="IAppCommands.SetChatSpeakerMuted"/> below, which suppresses its own echo.
45184521
/// </summary>
45194522
public void SetChatSpeakerMuted(bool muted)
45204523
{
@@ -4529,26 +4532,6 @@ public void SetChatSpeakerMuted(bool muted)
45294532
SpeakerMuteChanged?.Invoke(muted);
45304533
}
45314534

4532-
/// <summary>
4533-
/// Settings-page-originated mute: wraps the shared write in a store self-write so it does not
4534-
/// echo an external-change reload back to the Settings view model that triggered it.
4535-
/// </summary>
4536-
void IAppCommands.SetChatSpeakerMuted(bool muted)
4537-
{
4538-
using (SettingsStore?.BeginSelfWrite())
4539-
{
4540-
SetChatSpeakerMuted(muted);
4541-
}
4542-
}
4543-
4544-
/// <summary>
4545-
/// Pushes tool-call visibility into the live chat timeline. Forwards to the shared
4546-
/// static writer so a WinUI-free settings view model can drive it through IAppCommands
4547-
/// without referencing the chat UI directly.
4548-
/// </summary>
4549-
public void SetChatToolCallsVisible(bool visible) =>
4550-
OpenClawTray.Chat.OpenClawChatRoot.SetToolCallsVisible(visible);
4551-
45524535
private static void SendDeepLinkToRunningInstance(string uri)
45534536
{
45544537
try

0 commit comments

Comments
 (0)