@@ -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 ; }
0 commit comments