-
@session.Model
+
@GetSessionModel(session)
@if (!string.IsNullOrEmpty(session.WorkingDirectory))
{
๐ @ShortenPath(session.WorkingDirectory)
@@ -203,11 +208,14 @@
@{
- List lastMessages;
- try { lastMessages = session.History.TakeLast(6).ToList(); }
- catch { lastMessages = new(); }
+ var cardMsgs = GetCardMessages(session.Name, session.History);
+ var hasMoreCard = session.History.Count > cardMsgs.Count;
+ }
+ @if (hasMoreCard)
+ {
+
}
- InvokeAsync(SafeRefreshAsync));
+ }
+ return;
+ }
+ _lastRefresh = now;
+ _refreshPending = false;
+
// Capture all card input values before re-render wipes them
try
{
@@ -793,6 +818,8 @@
}
private const int DefaultMessageWindow = 25;
+ private const int DefaultCardMessageWindow = 10;
+ private Dictionary cardMessageCounts = new();
private List GetWindowedMessages(string sessionName, IReadOnlyList history)
{
@@ -808,6 +835,26 @@
}
}
+ private List GetCardMessages(string sessionName, IReadOnlyList history)
+ {
+ var limit = cardMessageCounts.TryGetValue(sessionName, out var c) ? c : DefaultCardMessageWindow;
+ try
+ {
+ if (history.Count <= limit) return history.ToList();
+ return history.Skip(history.Count - limit).ToList();
+ }
+ catch (InvalidOperationException)
+ {
+ return history.ToArray().TakeLast(limit).ToList();
+ }
+ }
+
+ private void LoadMoreCardMessages(string sessionName)
+ {
+ var current = cardMessageCounts.TryGetValue(sessionName, out var c) ? c : DefaultCardMessageWindow;
+ cardMessageCounts[sessionName] = current + 15;
+ }
+
private void LoadMoreExpandedMessages(string sessionName)
{
var current = expandedMessageCounts.TryGetValue(sessionName, out var c) ? c : DefaultMessageWindow;
@@ -816,23 +863,32 @@
// === Model, plan mode, font, token helpers ===
- private readonly string[] availableModels = new[]
+ private static readonly string[] _fallbackModels = new[]
{
- "claude-opus-4.6", "claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-4.5",
- "gpt-5.2", "gpt-5.1", "gpt-5", "gpt-5-mini", "gemini-3-pro-preview",
+ "claude-opus-4.6", "claude-opus-4.6-fast", "claude-opus-4.5", "claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-4.5",
+ "gpt-5.2", "gpt-5.2-codex", "gpt-5.1", "gpt-5.1-codex", "gpt-5.1-codex-max", "gpt-5.1-codex-mini", "gpt-5", "gpt-5-mini", "gpt-4.1",
+ "gemini-3-pro-preview",
};
+ private IReadOnlyList availableModels =>
+ CopilotService.AvailableModels.Count > 0 ? CopilotService.AvailableModels : _fallbackModels;
- private string GetExpandedModel(AgentSessionInfo session)
+ private string GetSessionModel(AgentSessionInfo session)
{
- if (usageBySession.TryGetValue(session.Name, out var u) && !string.IsNullOrEmpty(u.Model) && availableModels.Contains(u.Model))
+ // Priority: 1) actual model from usage events, 2) user override, 3) session config, 4) first available
+ if (usageBySession.TryGetValue(session.Name, out var u) && !string.IsNullOrEmpty(u.Model))
return u.Model;
if (modelOverrideBySession.TryGetValue(session.Name, out var m))
return m;
- if (!string.IsNullOrEmpty(session.Model) && session.Model != "resumed" && availableModels.Contains(session.Model))
+ if (!string.IsNullOrEmpty(session.Model) && session.Model != "resumed")
return session.Model;
return availableModels[0];
}
+ private string GetExpandedModel(AgentSessionInfo session)
+ {
+ return GetSessionModel(session);
+ }
+
private void SetExpandedModel(AgentSessionInfo session, string? model)
{
if (string.IsNullOrEmpty(model)) return;
diff --git a/Components/Pages/Dashboard.razor.css b/Components/Pages/Dashboard.razor.css
index b368a1b0c3..bd00797ac9 100644
--- a/Components/Pages/Dashboard.razor.css
+++ b/Components/Pages/Dashboard.razor.css
@@ -113,6 +113,12 @@
border-color: rgba(255,255,255,0.15);
}
+.session-card:focus-within {
+ border-color: rgba(78,168,209,0.6);
+ box-shadow: 0 0 0 1px rgba(78,168,209,0.3), 0 0 16px rgba(78,168,209,0.15);
+ background: rgba(78,168,209,0.06);
+}
+
/* === Expanded card โ mirrors Chat (Home.razor) layout === */
.expanded-card {
display: flex;
@@ -437,6 +443,23 @@
border-radius: 8px;
}
+.card-messages .load-more-btn {
+ background: rgba(78,168,209,0.1);
+ border: 1px solid rgba(78,168,209,0.2);
+ border-radius: 6px;
+ color: rgba(78,168,209,0.7);
+ font-size: 0.7rem;
+ padding: 0.3rem 0.6rem;
+ cursor: pointer;
+ text-align: center;
+ flex-shrink: 0;
+}
+
+.card-messages .load-more-btn:hover {
+ background: rgba(78,168,209,0.2);
+ color: #4ea8d1;
+}
+
.card-empty {
color: rgba(255,255,255,0.3);
text-align: center;
diff --git a/MauiProgram.cs b/MauiProgram.cs
index 2fdc31c9e9..b6f8588d8e 100644
--- a/MauiProgram.cs
+++ b/MauiProgram.cs
@@ -60,18 +60,26 @@ public static MauiApp CreateMauiApp()
#if MACCATALYST
builder.ConfigureLifecycleEvents(events =>
{
- events.AddiOS(ios => ios.SceneWillConnect((scene, session, options) =>
+ events.AddiOS(ios =>
{
- if (scene is UIWindowScene windowScene)
+ ios.SceneWillConnect((scene, session, options) =>
{
- var titlebar = windowScene.Titlebar;
- if (titlebar != null)
+ if (scene is UIWindowScene windowScene)
{
- titlebar.TitleVisibility = UITitlebarTitleVisibility.Hidden;
- titlebar.Toolbar = null;
+ var titlebar = windowScene.Titlebar;
+ if (titlebar != null)
+ {
+ titlebar.TitleVisibility = UITitlebarTitleVisibility.Hidden;
+ titlebar.Toolbar = null;
+ }
}
- }
- }));
+ });
+ ios.OnActivated(app =>
+ {
+ // Clear dock badge when app becomes active
+ app.ApplicationIconBadgeNumber = 0;
+ });
+ });
});
#endif
diff --git a/Services/CopilotService.cs b/Services/CopilotService.cs
index f15547eeb3..b31a47eacf 100644
--- a/Services/CopilotService.cs
+++ b/Services/CopilotService.cs
@@ -91,6 +91,7 @@ private static string FindProjectDir()
public string? ActiveSessionName => _activeSessionName;
public ChatDatabase ChatDb => _chatDb;
public ConnectionMode CurrentMode { get; private set; } = ConnectionMode.Embedded;
+ public List AvailableModels { get; private set; } = new();
public CopilotService(ChatDatabase chatDb, ServerManager serverManager, WsBridgeClient bridgeClient)
{
@@ -227,6 +228,9 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default)
OnStateChanged?.Invoke();
+ // Fetch available models dynamically
+ _ = FetchAvailableModelsAsync();
+
// Restore previous sessions (includes subscribing to untracked server sessions in Persistent mode)
await RestorePreviousSessionsAsync(cancellationToken);
}
@@ -1207,7 +1211,7 @@ void Invoke(Action action)
var uTokenLimit = uData?.GetType().GetProperty("TokenLimit")?.GetValue(uData) as int?;
var uInputTokens = uData?.GetType().GetProperty("InputTokens")?.GetValue(uData) as int?;
var uOutputTokens = uData?.GetType().GetProperty("OutputTokens")?.GetValue(uData) as int?;
- if (!string.IsNullOrEmpty(uModel) && state.Info.Model == "resumed")
+ if (!string.IsNullOrEmpty(uModel))
state.Info.Model = uModel;
Invoke(() => OnUsageInfoChanged?.Invoke(sessionName, new SessionUsageInfo(uModel, uCurrentTokens, uTokenLimit, uInputTokens, uOutputTokens)));
break;
@@ -1217,7 +1221,7 @@ void Invoke(Action action)
var aModel = aData?.GetType().GetProperty("Model")?.GetValue(aData)?.ToString();
var aInput = aData?.GetType().GetProperty("InputTokens")?.GetValue(aData) as int?;
var aOutput = aData?.GetType().GetProperty("OutputTokens")?.GetValue(aData) as int?;
- if (!string.IsNullOrEmpty(aModel) && state.Info.Model == "resumed")
+ if (!string.IsNullOrEmpty(aModel))
state.Info.Model = aModel;
if (aInput.HasValue || aOutput.HasValue)
{
@@ -1232,6 +1236,17 @@ void Invoke(Action action)
state.Info.IsProcessing = false;
Invoke(() => OnStateChanged?.Invoke());
break;
+
+ case SessionModelChangeEvent modelChange:
+ var newModel = modelChange.Data?.NewModel;
+ if (!string.IsNullOrEmpty(newModel))
+ {
+ state.Info.Model = newModel;
+ Debug($"Session '{sessionName}' model changed to: {newModel}");
+ Invoke(() => OnUsageInfoChanged?.Invoke(sessionName, new SessionUsageInfo(newModel, null, null, null, null)));
+ Invoke(() => OnStateChanged?.Invoke());
+ }
+ break;
default:
break;
@@ -1376,6 +1391,7 @@ private void CompleteResponse(SessionState state)
// Fire completion notification
var summary = response.Length > 100 ? response[..100] + "..." : response;
OnSessionComplete?.Invoke(state.Info.Name, summary);
+ IncrementBadge();
// Auto-dispatch next queued message
if (state.Info.MessageQueue.Count > 0)
@@ -1782,7 +1798,7 @@ public async ValueTask DisposeAsync()
}
}
- public void SaveUiState(string currentPage, string? activeSession = null, int? fontSize = null)
+ public void SaveUiState(string currentPage, string? activeSession = null, int? fontSize = null, string? selectedModel = null)
{
try
{
@@ -1791,7 +1807,8 @@ public void SaveUiState(string currentPage, string? activeSession = null, int? f
{
CurrentPage = currentPage,
ActiveSession = activeSession ?? _activeSessionName,
- FontSize = fontSize ?? existing?.FontSize ?? 20
+ FontSize = fontSize ?? existing?.FontSize ?? 20,
+ SelectedModel = selectedModel ?? existing?.SelectedModel
};
var json = JsonSerializer.Serialize(state);
File.WriteAllText(UiStateFile, json);
@@ -1905,6 +1922,60 @@ public void SetSessionAlias(string sessionId, string alias)
catch { }
return null;
}
+
+ // Dock badge for completed sessions
+ private int _badgeCount;
+
+ private void IncrementBadge()
+ {
+#if MACCATALYST || IOS
+ _badgeCount++;
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ try { UIKit.UIApplication.SharedApplication.ApplicationIconBadgeNumber = _badgeCount; }
+ catch { }
+ });
+#endif
+ }
+
+ public void ClearBadge()
+ {
+#if MACCATALYST || IOS
+ _badgeCount = 0;
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ try { UIKit.UIApplication.SharedApplication.ApplicationIconBadgeNumber = 0; }
+ catch { }
+ });
+#endif
+ }
+
+ private async Task FetchAvailableModelsAsync()
+ {
+ try
+ {
+ if (_client == null) return;
+ var modelList = await _client.ListModelsAsync();
+ if (modelList != null && modelList.Count > 0)
+ {
+ var models = modelList
+ .Where(m => !string.IsNullOrEmpty(m.Name))
+ .Select(m => m.Name!)
+ .OrderBy(m => m)
+ .ToList();
+ if (models.Count > 0)
+ {
+ AvailableModels = models;
+ Debug($"Loaded {models.Count} models from SDK");
+ OnStateChanged?.Invoke();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug($"Failed to fetch models: {ex.Message}");
+ }
+ }
}
public class UiState
@@ -1912,6 +1983,7 @@ public class UiState
public string CurrentPage { get; set; } = "/";
public string? ActiveSession { get; set; }
public int FontSize { get; set; } = 20;
+ public string? SelectedModel { get; set; }
}
public class ActiveSessionEntry