Skip to content

Commit dea331f

Browse files
bkudiessCopilot
authored andcommitted
Redesign Sessions page (Fluent 2 + deep-link to Chat)
Restructures SessionsPage to match the Permissions/Connection shell (ScrollViewer + StackPanel, MaxWidth 900, 4 px grid) and removes UI drift against the openclaw-design skill: - Replace literal emoji empty-state and ad-hoc colors with Segoe Fluent glyphs and SystemFillColor* theme brushes. - Status dot is now a 4-state Fluent map (Success / Caution / Critical / Neutral) with a hover tooltip describing the state. - Primary action is an AccentButton 'Open chat' (Fluent 2 rest/hover/press/disabled via AccentButtonStyle, no Foreground overrides); destructive Reset/Compact/Delete moved to a MenuFlyout behind the row's overflow button. - Filter out cron-spawned sessions (key slot == 'cron') so the conversation list mirrors the macOS companion. - Deep-link from a session row to the Chat surface: SessionsPage stores the session key on HubWindow.PendingChatSessionKey; ChatPage.ShowFunctionalSurface consumes it and remounts the OpenClawChatRoot with initialThreadId, so both the timeline and the composer's session dropdown render the requested session. - Robust MenuFlyout click routing via ResolveSessionKey, which walks back to MenuFlyout.Target when DataContext propagation into popup items fails (canonical WinUI binding quirk). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 75eab85 commit dea331f

9 files changed

Lines changed: 369 additions & 191 deletions

File tree

src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public sealed partial class ChatPage : Page
2626
private HubWindow? _hub;
2727
private MountedFunctionalChat? _functionalHost;
2828
private IChatDataProvider? _mountedProvider;
29+
private string? _mountedThreadId;
2930
private string? _chatUrl;
3031
private bool _webViewInitialized;
3132
private bool _webViewMode;
@@ -212,7 +213,21 @@ private void ShowFunctionalSurface()
212213
var provider = app?.ChatProvider;
213214
Func<string, Task>? readAloud = app is null ? null : app.SpeakChatTextAsync;
214215

215-
if (_functionalHost is not null && ReferenceEquals(_mountedProvider, provider))
216+
// Consume a pending session-key hand-off from SessionsPage so the
217+
// chat root mounts with that thread selected. Remount is the only
218+
// path — OpenClawChatRoot has no live "switch thread" API.
219+
var pendingSessionKey = _hub?.PendingChatSessionKey;
220+
if (pendingSessionKey is not null && _hub is not null)
221+
{
222+
_hub.PendingChatSessionKey = null;
223+
}
224+
var threadIdToMount = pendingSessionKey ?? _mountedThreadId;
225+
var threadChanged = pendingSessionKey is not null
226+
&& !string.Equals(_mountedThreadId, pendingSessionKey, StringComparison.Ordinal);
227+
228+
if (_functionalHost is not null
229+
&& ReferenceEquals(_mountedProvider, provider)
230+
&& !threadChanged)
216231
{
217232
PlaceholderPanel.Visibility = Visibility.Collapsed;
218233
ChatHost.Visibility = Visibility.Visible;
@@ -245,6 +260,7 @@ private void ShowFunctionalSurface()
245260
_functionalHost = CurrentApp.ActiveHubWindow!.MountFunctionalChat(
246261
ChatHost,
247262
provider,
263+
initialThreadId: threadIdToMount,
248264
onReadAloud: readAloud,
249265
onStopSpeaking: () => app?.StopChatSpeaking(),
250266
onVoiceRequest: VoiceTranscribeAsync,
@@ -254,6 +270,7 @@ private void ShowFunctionalSurface()
254270
initialMuted: CurrentApp.Settings?.VoiceTtsEnabled == false,
255271
suppressAutoDispose: true);
256272
_mountedProvider = provider;
273+
_mountedThreadId = threadIdToMount;
257274

258275
// If the V hotkey (or another caller) requested auto-start voice,
259276
// trigger it after the UI thread processes the mount (composer needs
@@ -342,6 +359,7 @@ private void DisposeFunctionalHost()
342359
var host = _functionalHost;
343360
_functionalHost = null;
344361
_mountedProvider = null;
362+
_mountedThreadId = null;
345363
try { host?.Dispose(); } catch { /* tear-down race — non-fatal */ }
346364
}
347365

src/OpenClaw.Tray.WinUI/Pages/SessionsPage.xaml

Lines changed: 230 additions & 134 deletions
Large diffs are not rendered by default.

src/OpenClaw.Tray.WinUI/Pages/SessionsPage.xaml.cs

Lines changed: 103 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,25 @@ private void OnOpenConnectionClick(object sender, RoutedEventArgs e)
8282

8383
public void UpdateSessions(SessionInfo[] sessions)
8484
{
85-
_allSessions = sessions;
86-
_sessionLoading.Complete(sessions.Length);
85+
// Drop cron-spawned sessions (key shape "agent:<id>:cron" — slot is
86+
// the third ":"-separated part). They have their own home on the
87+
// Cron page; surfacing them here overcrowds the conversation list.
88+
_allSessions = sessions
89+
.Where(s => !IsCronSession(s))
90+
.ToArray();
91+
_sessionLoading.Complete(_allSessions.Length);
8792
RebuildChannelTabs();
8893
ApplyFilter();
8994
}
9095

96+
private static bool IsCronSession(SessionInfo s)
97+
{
98+
if (string.IsNullOrEmpty(s.Key)) return false;
99+
var parts = s.Key.Split(':');
100+
return parts.Length >= 3
101+
&& string.Equals(parts[2], "cron", StringComparison.OrdinalIgnoreCase);
102+
}
103+
91104
private void RebuildChannelTabs()
92105
{
93106
if (_allSessions == null) return;
@@ -165,21 +178,17 @@ private void OnAppStateChanged(object? sender, PropertyChangedEventArgs e)
165178

166179
private SessionViewModel ToViewModel(SessionInfo s)
167180
{
168-
var isActive = s.Status == "active" || s.Status == "running";
169-
170-
// Detail line: Provider · Model · Channel
171181
var parts = new List<string>(3);
172182
if (!string.IsNullOrWhiteSpace(s.Provider)) parts.Add(s.Provider!);
173183
if (!string.IsNullOrWhiteSpace(s.Model)) parts.Add(s.Model!);
174184
if (!string.IsNullOrWhiteSpace(s.Channel)) parts.Add(s.Channel!);
175185

176-
// Token display
177186
var hasTokens = s.InputTokens > 0 || s.OutputTokens > 0;
178187
var tokensText = hasTokens
179188
? $"↓{FormatTokenCount(s.InputTokens)} / ↑{FormatTokenCount(s.OutputTokens)}"
180189
: "";
181190

182-
// Context % — ContextTokens is the window size, TotalTokens is usage
191+
// ContextTokens is the window size, TotalTokens is usage.
183192
double contextPercent = 0;
184193
if (s.ContextTokens > 0 && s.TotalTokens > 0)
185194
contextPercent = Math.Min(100.0, (double)s.TotalTokens / s.ContextTokens * 100.0);
@@ -190,52 +199,109 @@ private SessionViewModel ToViewModel(SessionInfo s)
190199
DisplayName = !string.IsNullOrWhiteSpace(s.DisplayName) ? s.DisplayName! : s.Key,
191200
AgeText = s.AgeText,
192201
DetailLine = parts.Count > 0 ? string.Join(" · ", parts) : "",
193-
StatusColor = new SolidColorBrush(isActive ? Colors.LimeGreen : Colors.Gray),
202+
StatusBrush = ResolveStatusBrush(s),
203+
StatusTooltip = ResolveStatusTooltip(s),
194204
TokensText = tokensText,
195205
ContextPercent = contextPercent,
196206
HasTokenData = hasTokens || contextPercent > 0,
197207
CanEdit = _sessionLoading.CanEdit,
198208
};
199209
}
200210

211+
private static Brush ResolveStatusBrush(SessionInfo s)
212+
{
213+
var status = s.Status?.Trim().ToLowerInvariant();
214+
if (status is "error" or "failed" or "failure")
215+
return s_criticalBrush.Value;
216+
if (s.AbortedLastRun)
217+
return s_cautionBrush.Value;
218+
if (status is "active" or "running")
219+
return s_successBrush.Value;
220+
return s_neutralBrush.Value;
221+
}
222+
223+
private static string ResolveStatusTooltip(SessionInfo s)
224+
{
225+
var status = s.Status?.Trim().ToLowerInvariant();
226+
if (status is "error" or "failed" or "failure") return "Error";
227+
if (s.AbortedLastRun) return "Aborted last run";
228+
if (status is "active" or "running") return "Running";
229+
return "Idle";
230+
}
231+
232+
private static readonly Lazy<Brush> s_successBrush =
233+
new(() => (Brush)Application.Current.Resources["SystemFillColorSuccessBrush"]);
234+
private static readonly Lazy<Brush> s_cautionBrush =
235+
new(() => (Brush)Application.Current.Resources["SystemFillColorCautionBrush"]);
236+
private static readonly Lazy<Brush> s_criticalBrush =
237+
new(() => (Brush)Application.Current.Resources["SystemFillColorCriticalBrush"]);
238+
private static readonly Lazy<Brush> s_neutralBrush =
239+
new(() => (Brush)Application.Current.Resources["SystemFillColorNeutralBrush"]);
240+
241+
private void OnOpenChat(object sender, RoutedEventArgs e)
242+
{
243+
if (sender is Button btn && btn.Tag is string key)
244+
{
245+
if (CurrentApp.ActiveHubWindow is HubWindow hub)
246+
{
247+
hub.PendingChatSessionKey = key;
248+
}
249+
((IAppCommands)CurrentApp).Navigate("chat", "sessions");
250+
}
251+
}
252+
201253
private void ChannelSelector_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args)
202254
{
203255
var selected = sender.SelectedItem;
204256
_activeChannel = selected == AllTab ? "all" : (selected?.Text ?? "all");
205257
ApplyFilter();
206258
}
207259

208-
private async void OnResetSession(object sender, RoutedEventArgs e)
260+
private static string? ResolveSessionKey(object sender)
209261
{
210-
if (sender is Button btn && btn.Tag is string key)
262+
if (sender is FrameworkElement fe)
211263
{
212-
var client = CurrentApp.GatewayClient;
213-
if (client == null) { ShowDisconnected(); return; }
214-
try { await client.ResetSessionAsync(key); }
215-
catch (Exception ex) { ShowActionFailure("Reset failed", ex); }
264+
if (fe.DataContext is SessionViewModel vm && !string.IsNullOrEmpty(vm.Key))
265+
return vm.Key;
266+
if (fe.Tag is string tag && !string.IsNullOrEmpty(tag))
267+
return tag;
268+
if (fe is MenuFlyoutItem mfi && mfi.Parent is MenuFlyout mf
269+
&& mf.Target is FrameworkElement target)
270+
{
271+
if (target.DataContext is SessionViewModel targetVm && !string.IsNullOrEmpty(targetVm.Key))
272+
return targetVm.Key;
273+
if (target.Tag is string targetTag && !string.IsNullOrEmpty(targetTag))
274+
return targetTag;
275+
}
216276
}
277+
return null;
278+
}
279+
280+
private async void OnResetSession(object sender, RoutedEventArgs e)
281+
{
282+
if (ResolveSessionKey(sender) is not string key) return;
283+
var client = CurrentApp.GatewayClient;
284+
if (client == null) { ShowDisconnected(); return; }
285+
try { await client.ResetSessionAsync(key); }
286+
catch (Exception ex) { ShowActionFailure("Reset failed", ex); }
217287
}
218288

219289
private async void OnDeleteSession(object sender, RoutedEventArgs e)
220290
{
221-
if (sender is Button btn && btn.Tag is string key)
222-
{
223-
var client = CurrentApp.GatewayClient;
224-
if (client == null) { ShowDisconnected(); return; }
225-
try { await client.DeleteSessionAsync(key); }
226-
catch (Exception ex) { ShowActionFailure("Delete failed", ex); }
227-
}
291+
if (ResolveSessionKey(sender) is not string key) return;
292+
var client = CurrentApp.GatewayClient;
293+
if (client == null) { ShowDisconnected(); return; }
294+
try { await client.DeleteSessionAsync(key); }
295+
catch (Exception ex) { ShowActionFailure("Delete failed", ex); }
228296
}
229297

230298
private async void OnCompactSession(object sender, RoutedEventArgs e)
231299
{
232-
if (sender is Button btn && btn.Tag is string key)
233-
{
234-
var client = CurrentApp.GatewayClient;
235-
if (client == null) { ShowDisconnected(); return; }
236-
try { await client.CompactSessionAsync(key); }
237-
catch (Exception ex) { ShowActionFailure("Compact failed", ex); }
238-
}
300+
if (ResolveSessionKey(sender) is not string key) return;
301+
var client = CurrentApp.GatewayClient;
302+
if (client == null) { ShowDisconnected(); return; }
303+
try { await client.CompactSessionAsync(key); }
304+
catch (Exception ex) { ShowActionFailure("Compact failed", ex); }
239305
}
240306

241307
private void OnRefresh(object sender, RoutedEventArgs e)
@@ -255,19 +321,14 @@ private void OnRefresh(object sender, RoutedEventArgs e)
255321
_ = client.RequestSessionsAsync();
256322
_ = client.RequestModelsListAsync();
257323

258-
if (RefreshButton.Content is StackPanel)
324+
if (RefreshLabel is not null)
259325
{
260-
// Temporarily update the text inside the StackPanel
261-
var sp = (StackPanel)RefreshButton.Content;
262-
if (sp.Children.Count > 1 && sp.Children[1] is TextBlock tb)
263-
{
264-
tb.Text = "Refreshing...";
265-
_refreshTimer?.Stop();
266-
_refreshTimer = DispatcherQueue.CreateTimer();
267-
_refreshTimer.Interval = TimeSpan.FromSeconds(1);
268-
_refreshTimer.Tick += (t, a) => { tb.Text = "Refresh"; _refreshTimer.Stop(); };
269-
_refreshTimer.Start();
270-
}
326+
RefreshLabel.Text = "Refreshing...";
327+
_refreshTimer?.Stop();
328+
_refreshTimer = DispatcherQueue.CreateTimer();
329+
_refreshTimer.Interval = TimeSpan.FromSeconds(1);
330+
_refreshTimer.Tick += (t, a) => { RefreshLabel.Text = "Refresh"; _refreshTimer.Stop(); };
331+
_refreshTimer.Start();
271332
}
272333
}
273334

@@ -302,7 +363,8 @@ public class SessionViewModel
302363
public string DisplayName { get; set; } = "";
303364
public string AgeText { get; set; } = "";
304365
public string DetailLine { get; set; } = "";
305-
public SolidColorBrush StatusColor { get; set; } = new(Colors.Gray);
366+
public Brush StatusBrush { get; set; } = new SolidColorBrush(Colors.Gray);
367+
public string StatusTooltip { get; set; } = "Idle";
306368
public string TokensText { get; set; } = "";
307369
public double ContextPercent { get; set; }
308370
public bool HasTokenData { get; set; }

src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4275,13 +4275,13 @@ On your gateway host (Mac/Linux), run:
42754275
<data name="SessionsPage_NoActiveSessions.Text" xml:space="preserve">
42764276
<value>No active sessions</value>
42774277
</data>
4278-
<data name="SessionsPage_Reset.Content" xml:space="preserve">
4278+
<data name="SessionsPage_Reset.Text" xml:space="preserve">
42794279
<value>Reset</value>
42804280
</data>
4281-
<data name="SessionsPage_Compact.Content" xml:space="preserve">
4281+
<data name="SessionsPage_Compact.Text" xml:space="preserve">
42824282
<value>Compact</value>
42834283
</data>
4284-
<data name="SessionsPage_Delete.Content" xml:space="preserve">
4284+
<data name="SessionsPage_Delete.Text" xml:space="preserve">
42854285
<value>Delete</value>
42864286
</data>
42874287
<data name="SettingsPage_Settings.Text" xml:space="preserve">

src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4227,13 +4227,13 @@ Sur votre hôte passerelle (Mac/Linux), exécutez :
42274227
<data name="SessionsPage_NoActiveSessions.Text" xml:space="preserve">
42284228
<value>Aucune session active</value>
42294229
</data>
4230-
<data name="SessionsPage_Reset.Content" xml:space="preserve">
4230+
<data name="SessionsPage_Reset.Text" xml:space="preserve">
42314231
<value>Réinitialiser</value>
42324232
</data>
4233-
<data name="SessionsPage_Compact.Content" xml:space="preserve">
4233+
<data name="SessionsPage_Compact.Text" xml:space="preserve">
42344234
<value>Compacter</value>
42354235
</data>
4236-
<data name="SessionsPage_Delete.Content" xml:space="preserve">
4236+
<data name="SessionsPage_Delete.Text" xml:space="preserve">
42374237
<value>Supprimer</value>
42384238
</data>
42394239
<data name="SettingsPage_Settings.Text" xml:space="preserve">

src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4228,13 +4228,13 @@ Voer op uw gateway-host (Mac/Linux) uit:
42284228
<data name="SessionsPage_NoActiveSessions.Text" xml:space="preserve">
42294229
<value>Geen actieve sessies</value>
42304230
</data>
4231-
<data name="SessionsPage_Reset.Content" xml:space="preserve">
4231+
<data name="SessionsPage_Reset.Text" xml:space="preserve">
42324232
<value>Resetten</value>
42334233
</data>
4234-
<data name="SessionsPage_Compact.Content" xml:space="preserve">
4234+
<data name="SessionsPage_Compact.Text" xml:space="preserve">
42354235
<value>Compact maken</value>
42364236
</data>
4237-
<data name="SessionsPage_Delete.Content" xml:space="preserve">
4237+
<data name="SessionsPage_Delete.Text" xml:space="preserve">
42384238
<value>Verwijderen</value>
42394239
</data>
42404240
<data name="SettingsPage_Settings.Text" xml:space="preserve">

src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4227,13 +4227,13 @@
42274227
<data name="SessionsPage_NoActiveSessions.Text" xml:space="preserve">
42284228
<value>无活动会话</value>
42294229
</data>
4230-
<data name="SessionsPage_Reset.Content" xml:space="preserve">
4230+
<data name="SessionsPage_Reset.Text" xml:space="preserve">
42314231
<value>重置</value>
42324232
</data>
4233-
<data name="SessionsPage_Compact.Content" xml:space="preserve">
4233+
<data name="SessionsPage_Compact.Text" xml:space="preserve">
42344234
<value>压缩</value>
42354235
</data>
4236-
<data name="SessionsPage_Delete.Content" xml:space="preserve">
4236+
<data name="SessionsPage_Delete.Text" xml:space="preserve">
42374237
<value>删除</value>
42384238
</data>
42394239
<data name="SettingsPage_Settings.Text" xml:space="preserve">

src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4227,13 +4227,13 @@
42274227
<data name="SessionsPage_NoActiveSessions.Text" xml:space="preserve">
42284228
<value>沒有作用中的工作階段</value>
42294229
</data>
4230-
<data name="SessionsPage_Reset.Content" xml:space="preserve">
4230+
<data name="SessionsPage_Reset.Text" xml:space="preserve">
42314231
<value>重設</value>
42324232
</data>
4233-
<data name="SessionsPage_Compact.Content" xml:space="preserve">
4233+
<data name="SessionsPage_Compact.Text" xml:space="preserve">
42344234
<value>壓縮</value>
42354235
</data>
4236-
<data name="SessionsPage_Delete.Content" xml:space="preserve">
4236+
<data name="SessionsPage_Delete.Text" xml:space="preserve">
42374237
<value>刪除</value>
42384238
</data>
42394239
<data name="SettingsPage_Settings.Text" xml:space="preserve">

src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public sealed partial class HubWindow : WindowEx
4545
public VoiceService? VoiceServiceInstance { get; set; }
4646
/// <summary>When true, ChatPage should auto-start voice recording on next navigation. Consumed (reset to false) by ChatPage.</summary>
4747
public bool PendingAutoStartVoice { get; set; }
48+
/// <summary>Session key the chat surface should select on its next mount. Consumed (cleared) by ChatPage.</summary>
49+
public string? PendingChatSessionKey { get; set; }
4850
public string? NodeFullDeviceId { get; set; }
4951
private Microsoft.UI.Dispatching.DispatcherQueueTimer? _gatewayNavHideTimer;
5052

0 commit comments

Comments
 (0)