Skip to content

Commit 4d09797

Browse files
karkarlcalebedenCopilotshanselman
authored
improve: polish hub title bar, notifications, and chat UI (#1041)
* feat: add native lifecycle slash commands Handle exact /new, /reset, and /compact commands through lifecycle RPCs, preserve structured compaction boundaries, and retain safe compatibility behavior for older gateways. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c41e789a-73e3-41ae-ac62-1ff5287599c0 * improve(hub): move Settings nav item to footer under Diagnostics Match the WinUI standard of pinning Settings to the very bottom of the navigation pane. Moves the Settings NavigationViewItem out of MenuItems into FooterMenuItems, placed after Diagnostics. Navigation still resolves via both collections and the high-contrast icon swap already covers footer items, so behavior is unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * improve(chat): make welcome suggestion buttons subtle Drop the default button stroke and elevation depth from the three zero-state prompt suggestions (Say hi, What can you do?, Give me a quick tour of OpenClaw), keeping only a light, low-opacity fill at rest via SubtleFillColorSecondaryBrush. Rounded corners, padding, and hover feedback are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Make chat composer theme-aware in dark mode The composer read as light gray on a dark chat surface and its fade gradient stayed white because FunctionalUI resolved theme brushes by walking the app-level ThemeDictionaries, which do not surface WinUI's XamlControlsResources framework brushes. Controls rendering dark fell back to the light Application.Resources snapshot. Fixes, using only built-in WinUI theme resources (no hardcoded colors, no C# theme helper): - App.xaml: re-declare the exact framework brush keys the chat uses in the Default/Light/HighContrast theme dictionaries, each aliasing its built-in color token via ThemeResource. Values are identical but now reachable by the per-theme walk, so they flip correctly. - FunctionalUI: resolve foreground/background/border resource brushes against the control's ActualTheme and re-apply reactively on ActualThemeChanged/Loaded so brushes flip on a runtime light/dark switch. - Composer: send/stop glyphs and fills, session-picker rows, and the welcome suggestion buttons resolve against ActualTheme instead of snapshotting the app theme; the fade gradient derives from the now-correct base-surface brush so it fades into the dock in dark. - Timeline: timestamp/helper captions resolve TextFillColorSecondary for the timeline root's ActualTheme so they stay legible in dark. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix: harden lifecycle command edge cases Preserve explicit session selection and failed composer sends, retain live entries during authoritative reloads, widen compaction metrics, and clarify lifecycle timeout recovery. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c41e789a-73e3-41ae-ac62-1ff5287599c0 * improve(hub): restyle title bar and float notification banner Merge the pane toggle into the lobster logo (hover reveals a DockLeft panel icon), move the back button to a chevron directly left of a centered search box, and give the title-bar icon buttons a subtle hover/pressed affordance. Make the right-side status/error pill transparent at rest (border kept for its dropdown) and the notification bell a true borderless subtle button. Convert the app notification banner to a native WinUI InfoBar with Title/Message and a standard ActionButton, and float it over the top-right of the content area (right ~2/5) so it never pushes the title bar or nav content down. Long messages wrap and grow the banner taller. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix: order lifecycle commands with queued sends Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c41e789a-73e3-41ae-ac62-1ff5287599c0 * fix(functionalui): resolve theme color via paired brush token ResolveColor threw InvalidOperationException when a raw WinUI color token was absent, which happens in minimal hosts (including the UI test surfaces) that seed only the paired '<key>Brush'. The chat timeline resolves TextFillColorSecondary during render, so the throw aborted the whole mount and the timeline rendered no ItemsRepeater, failing the chat proof tests. Fall back to the paired brush's color before throwing; both forms resolve to the same value in production. Adds a regression test covering the brush-color fallback and the still-missing-token throw. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(chat): eliminate ActualThemeChanged handler accumulation in .Set() lambdas Replace 7 repeated event subscriptions inside .Set() render paths with: - Theme.EnsureThemeCallback: ConditionalWeakTable-based single-subscription utility that stores and replaces the latest callback per control, invoking it immediately and on theme/load events (once, never accumulating). - FunctionalUI modifier path (.BackgroundResource / .Background(Ref(...))): for constant resource keys on Borders and Buttons, reuse the existing TrackThemedBrushes mechanism which already guards first-time subscription. - Control.Background support in ApplyModifiers + ReapplyThemedBrushes: the narrow renderer extension needed for Button backgrounds. Also remove the dead RootGrid Row 1 (formerly the InfoBar, now floating in Row 1 as an overlay). Regression tests prove: - 100 EnsureThemeCallback calls store exactly one callback (latest state) - FunctionalUI renderer applies BackgroundResource to Controls correctly Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 103120e9-d856-456c-885c-1ffd529b8746 * fix(chat): render composer fade gradient dark in dark mode The composer scrim derived its color by reading .Color off the framework SolidBackgroundFillColorBaseBrush out of the visual tree, so its {ThemeResource} re-resolved against the ambient light app theme and the fade read white (#F3F3F3) on a dark page. Add a dedicated ChatComposerFadeBrush to App.xaml ThemeDictionaries with literal per-theme colors (dark #202020, light #F3F3F3, high-contrast follows SystemColorWindowColor) and resolve it as a whole brush per ElementTheme, so it flips correctly and stays aligned with the composer dock fill. App-defined theme dictionaries are reachable by the FunctionalUI theme walk; framework primitives are not. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix: guard authoritative history reload pending by connection generation The _authoritativeHistoryReloadPending flag was consumed in LoadHistoryCoreAsync's finally block without checking whether the completing request belonged to the current connection generation. A stale old-generation completion after disconnect/reconnect could consume the flag and trigger an unintended authoritative reload in the newer generation. Fix: - Only consume/schedule the pending authoritative reload when requestConnectionVersion == _historyConnectionVersion (same guard as _historyInFlight removal) - Clear _authoritativeHistoryReloadPending in AdvanceHistoryGenerationLocked so generation advances invalidate stale pending flags - Add deterministic regression test: StaleGenerationAuthoritativeReload_DoesNotReloadInNewGeneration Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d2982ca8-bb56-4728-85bd-29542ce9d554 * Record paired-review history race fix Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4f0e93ff-d320-454d-b00a-865dac727d0c --------- Co-authored-by: Caleb Eden <58373773+calebeden@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Scott Hanselman <scott@hanselman.com>
1 parent 2fb1da2 commit 4d09797

10 files changed

Lines changed: 747 additions & 164 deletions

File tree

src/OpenClaw.Tray.WinUI/App.xaml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,66 @@
2020
<ResourceDictionary x:Key="Default">
2121
<SolidColorBrush x:Key="SetupIndicatorAccentBrush" Color="{ThemeResource SystemAccentColorLight2}" />
2222
<SolidColorBrush x:Key="SetupInactiveDotBrush" Color="{ThemeResource ControlStrongFillColorDefault}" />
23+
24+
<!-- FunctionalUI resolves a brush for a target ElementTheme by walking the
25+
app-level ThemeDictionaries; the framework's XamlControlsResources
26+
brushes are not surfaced through that enumerable walk, so a control
27+
rendering dark kept the light snapshot (the composer read as light on
28+
a dark page). Re-declaring the exact framework keys here materializes
29+
them per theme with identical values (each aliases its built-in color
30+
token), so the walk flips them correctly. No hardcoded colors. -->
31+
<SolidColorBrush x:Key="SolidBackgroundFillColorBaseBrush" Color="{ThemeResource SolidBackgroundFillColorBase}" />
32+
<SolidColorBrush x:Key="ControlFillColorDefaultBrush" Color="{ThemeResource ControlFillColorDefault}" />
33+
<SolidColorBrush x:Key="ControlStrokeColorDefaultBrush" Color="{ThemeResource ControlStrokeColorDefault}" />
34+
<SolidColorBrush x:Key="SubtleFillColorSecondaryBrush" Color="{ThemeResource SubtleFillColorSecondary}" />
35+
<SolidColorBrush x:Key="SubtleFillColorTertiaryBrush" Color="{ThemeResource SubtleFillColorTertiary}" />
36+
<SolidColorBrush x:Key="TextFillColorSecondaryBrush" Color="{ThemeResource TextFillColorSecondary}" />
37+
38+
<!-- Composer scrim: the chat timeline dissolves into the composer dock via a
39+
vertical fade from transparent to the base surface fill. FunctionalUI
40+
resolves this as a whole brush per ElementTheme and reads it out of the
41+
visual tree, so the stops use LITERAL colors (a {ThemeResource} color
42+
would re-resolve against the ambient app theme and read light on a dark
43+
page). The values mirror SolidBackgroundFillColorBase (dark #202020) so
44+
the fade lands seamlessly on the dock. -->
45+
<LinearGradientBrush x:Key="ChatComposerFadeBrush" StartPoint="0,0" EndPoint="0,1">
46+
<GradientStop Offset="0" Color="#00202020" />
47+
<GradientStop Offset="1" Color="#FF202020" />
48+
</LinearGradientBrush>
2349
</ResourceDictionary>
2450
<ResourceDictionary x:Key="Light">
2551
<SolidColorBrush x:Key="SetupIndicatorAccentBrush" Color="{ThemeResource SystemAccentColorDark1}" />
2652
<SolidColorBrush x:Key="SetupInactiveDotBrush" Color="{ThemeResource ControlStrongFillColorDefault}" />
53+
54+
<SolidColorBrush x:Key="SolidBackgroundFillColorBaseBrush" Color="{ThemeResource SolidBackgroundFillColorBase}" />
55+
<SolidColorBrush x:Key="ControlFillColorDefaultBrush" Color="{ThemeResource ControlFillColorDefault}" />
56+
<SolidColorBrush x:Key="ControlStrokeColorDefaultBrush" Color="{ThemeResource ControlStrokeColorDefault}" />
57+
<SolidColorBrush x:Key="SubtleFillColorSecondaryBrush" Color="{ThemeResource SubtleFillColorSecondary}" />
58+
<SolidColorBrush x:Key="SubtleFillColorTertiaryBrush" Color="{ThemeResource SubtleFillColorTertiary}" />
59+
<SolidColorBrush x:Key="TextFillColorSecondaryBrush" Color="{ThemeResource TextFillColorSecondary}" />
60+
61+
<!-- Light mirror of ChatComposerFadeBrush (SolidBackgroundFillColorBase light #F3F3F3). -->
62+
<LinearGradientBrush x:Key="ChatComposerFadeBrush" StartPoint="0,0" EndPoint="0,1">
63+
<GradientStop Offset="0" Color="#00F3F3F3" />
64+
<GradientStop Offset="1" Color="#FFF3F3F3" />
65+
</LinearGradientBrush>
2766
</ResourceDictionary>
2867
<ResourceDictionary x:Key="HighContrast">
2968
<SolidColorBrush x:Key="SetupIndicatorAccentBrush" Color="{ThemeResource SystemColorHighlightColor}" />
3069
<SolidColorBrush x:Key="SetupInactiveDotBrush" Color="{ThemeResource SystemColorGrayTextColor}" />
70+
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}" />
77+
78+
<!-- High-contrast follows the system window color so the scrim stays legible. -->
79+
<LinearGradientBrush x:Key="ChatComposerFadeBrush" StartPoint="0,0" EndPoint="0,1">
80+
<GradientStop Offset="0" Color="Transparent" />
81+
<GradientStop Offset="1" Color="{ThemeResource SystemColorWindowColor}" />
82+
</LinearGradientBrush>
3183
</ResourceDictionary>
3284
</ResourceDictionary.ThemeDictionaries>
3385

src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,10 +675,44 @@ void SetChatSurfaceHeight(double height)
675675
chatSurfaceHeight.Set(Math.Round(height));
676676
}
677677

678+
// Copilot-style scrim: instead of a hard divider line, the timeline
679+
// dissolves into the composer dock via a vertical gradient that runs
680+
// from transparent at the top to the solid theme-base fill at the
681+
// bottom. The composer dock uses that same base fill, so the fade lands
682+
// seamlessly. The color is resolved from the element's ActualTheme (not
683+
// Application.Resources, which snapshots the default theme) so it flips
684+
// live on a runtime light/dark switch. It never captures pointer input,
685+
// so scrolling and the last message stay live.
686+
static Brush BuildComposerFadeBrush(ElementTheme theme)
687+
{
688+
// Resolve the fade as a WHOLE brush per theme rather than reading a
689+
// color off a walked brush. Reading .Color out of the visual tree
690+
// re-resolves any {ThemeResource} against the ambient (light) app
691+
// theme, which is why the fade previously read white on a dark page.
692+
// ChatComposerFadeBrush is declared with literal colors per theme in
693+
// App.xaml ThemeDictionaries (which the FunctionalUI walk can reach),
694+
// so it flips correctly and stays aligned with the composer dock fill.
695+
// A not-yet-loaded element can report ElementTheme.Default; coerce it
696+
// to Dark so resolution never falls through to a missing app-root key
697+
// (Loaded/ActualThemeChanged re-apply with the real theme).
698+
var resolved = theme == ElementTheme.Light ? ElementTheme.Light : ElementTheme.Dark;
699+
return Theme.ResolveBrush("ChatComposerFadeBrush", resolved);
700+
}
701+
702+
var composerFade = Border(Empty())
703+
.Set(f =>
704+
{
705+
f.Height = 28;
706+
f.VerticalAlignment = VerticalAlignment.Bottom;
707+
f.IsHitTestVisible = false;
708+
Theme.EnsureThemeCallback(f, () => f.Background = BuildComposerFadeBrush(f.ActualTheme));
709+
});
710+
678711
return Grid([GridSize.Star()], [GridSize.Auto, GridSize.Auto, GridSize.Star(), GridSize.Auto],
679712
header.Grid(row: 0, column: 0),
680713
divider.Grid(row: 1, column: 0),
681714
body.Grid(row: 2, column: 0),
715+
composerFade.Grid(row: 2, column: 0),
682716
composer.Grid(row: 3, column: 0)
683717
).OnMount(root =>
684718
{
@@ -881,7 +915,10 @@ Element SuggestionButton(string text) =>
881915
b.Padding = new Thickness(12, 10, 12, 10);
882916
b.CornerRadius = new CornerRadius(8);
883917
b.IsEnabled = !suggestionsDisabled;
884-
});
918+
b.BorderThickness = new Thickness(0);
919+
b.BorderBrush = null;
920+
})
921+
.BackgroundResource("SubtleFillColorSecondaryBrush");
885922

886923
return Border(
887924
VStack(12,

src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,14 @@ static Element TimelineInset(Element child, double top = 2, double bottom = 2) =
10041004
// window backdrop. On Mica/Acrylic the system tint is translucent,
10051005
// so Tertiary text can fall below WCAG AA. Bump to Secondary when
10061006
// the chat surface is transparent over a host backdrop.
1007-
var chatStampFg = themeBrush("TextFillColorSecondaryBrush");
1007+
// Timestamps / helper captions sit directly on the window backdrop (no
1008+
// bubble behind them), so a snapshot from Application.Resources renders
1009+
// the light-theme secondary color and vanishes in dark mode. Drive this
1010+
// brush from the built-in TextFillColorSecondary token for the timeline
1011+
// root's ActualTheme instead (wired below) so it stays legible after a
1012+
// runtime light/dark switch.
1013+
var chatStampFg = new SolidColorBrush(
1014+
Theme.ResolveColor("TextFillColorSecondary", ElementTheme.Default));
10081015
var chatTextFg = themeBrush("TextFillColorPrimaryBrush");
10091016
// Tool chips: very subtle background tint + light border so they
10101017
// read as a secondary surface distinct from the filled assistant
@@ -3006,7 +3013,11 @@ bool BurstIsNestable(System.Collections.Generic.List<ChatTimelineItem> b)
30063013
prevLastEntryIdRef.Current = lastEntryId;
30073014
prevEntryCountRef.Current = entryCount;
30083015
})
3009-
).Background(chatPageBg).Grid(row: 0, column: 0)
3016+
).Set(rootBorder =>
3017+
{
3018+
Theme.EnsureThemeCallback(rootBorder, () =>
3019+
chatStampFg.Color = Theme.ResolveColor("TextFillColorSecondary", rootBorder.ActualTheme));
3020+
}).Background(chatPageBg).Grid(row: 0, column: 0)
30103021
);
30113022
}
30123023
}

0 commit comments

Comments
 (0)