Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions Components/Layout/SessionSidebar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ else if (IsFlyoutPanel)
<div class="new-session">
<input type="text" id="flyoutSessionNameInput" placeholder="Session name..." @oninput="OnSessionNameInput" />
<div class="session-options-row">
<select @bind="selectedModel" class="model-select">
<select @bind="selectedModel" @bind:after="SaveSelectedModel" class="model-select">
@foreach (var model in availableModels)
{
<option value="@model">@model</option>
Expand Down Expand Up @@ -83,7 +83,7 @@ else
<div class="new-session">
<input type="text" id="sessionNameInput" placeholder="Session name..." @oninput="OnSessionNameInput" />
<div class="session-options-row">
<select @bind="selectedModel" class="model-select">
<select @bind="selectedModel" @bind:after="SaveSelectedModel" class="model-select">
@foreach (var model in availableModels)
{
<option value="@model">@model</option>
Expand Down Expand Up @@ -252,6 +252,7 @@ else
};

private string selectedModel = "claude-opus-4.6";
private string _lastSavedModel = "";
private string sessionFilter = "";
private bool isCreating = false;
private bool hasSessionName = false;
Expand All @@ -270,18 +271,27 @@ else
await OnToggleFlyout.InvokeAsync();
}

private readonly string[] availableModels = new[]
private static readonly string[] _fallbackModels = new[]
{
"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<string> availableModels =>
CopilotService.AvailableModels.Count > 0 ? CopilotService.AvailableModels : _fallbackModels;

private IEnumerable<PersistedSessionInfo> filteredPersistedSessions =>
(string.IsNullOrWhiteSpace(sessionFilter)
Expand All @@ -302,6 +312,16 @@ else
CopilotService.OnSessionComplete += HandleSessionComplete;
RefreshSessions();
LoadPersistedSessions();

// Restore last selected model
var uiState = CopilotService.LoadUiState();
if (!string.IsNullOrEmpty(uiState?.SelectedModel))
selectedModel = uiState.SelectedModel;
}

private void SaveSelectedModel()
{
CopilotService.SaveUiState(currentPage, selectedModel: selectedModel);
}

private HashSet<string> completedSessions = new();
Expand Down Expand Up @@ -334,8 +354,28 @@ else
}
}

private DateTime _lastSidebarRefresh = DateTime.MinValue;
private bool _sidebarRefreshPending;

private void RefreshSessions()
{
var now = DateTime.UtcNow;
if ((now - _lastSidebarRefresh).TotalMilliseconds < 500)
{
if (!_sidebarRefreshPending)
{
_sidebarRefreshPending = true;
_ = Task.Delay(500).ContinueWith(_ => InvokeAsync(() => {
_sidebarRefreshPending = false;
_lastSidebarRefresh = DateTime.UtcNow;
sessions = CopilotService.GetAllSessions().ToList();
LoadPersistedSessions();
StateHasChanged();
}));
}
return;
}
_lastSidebarRefresh = now;
sessions = CopilotService.GetAllSessions().ToList();
LoadPersistedSessions();
InvokeAsync(StateHasChanged);
Expand Down
78 changes: 67 additions & 11 deletions Components/Pages/Dashboard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@
</div>
<span class="status-sep">·</span>
<select class="inline-model-select" value="@GetExpandedModel(session)" @onchange="e => SetExpandedModel(session, e.Value?.ToString())">
@{ var sessionModel = GetSessionModel(session); }
@if (!availableModels.Contains(sessionModel))
{
<option value="@sessionModel">@sessionModel</option>
}
@foreach (var m in availableModels)
{
<option value="@m">@m</option>
Expand Down Expand Up @@ -190,7 +195,7 @@
<button class="card-close" @onclick="() => CloseSession(session.Name)" title="Close session">✕</button>
</div>
<div class="card-context">
<span class="card-model">@session.Model</span>
<span class="card-model">@GetSessionModel(session)</span>
@if (!string.IsNullOrEmpty(session.WorkingDirectory))
{
<span class="card-dir" title="@session.WorkingDirectory">📁 @ShortenPath(session.WorkingDirectory)</span>
Expand All @@ -203,11 +208,14 @@

<div class="card-messages">
@{
List<ChatMessage> 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)
{
<button class="load-more-btn" @onclick="() => LoadMoreCardMessages(session.Name)">▲ Load earlier messages (@(session.History.Count - cardMsgs.Count) more)</button>
}
<ChatMessageList Messages="lastMessages"
<ChatMessageList Messages="cardMsgs"
StreamingContent="@(streamingBySession.TryGetValue(session.Name, out var s) ? s : "")"
CurrentToolName="@(currentToolBySession.TryGetValue(session.Name, out var t) ? t : "")"
ToolActivities="@(toolActivitiesBySession.TryGetValue(session.Name, out var ta) ? ta : new())"
Expand Down Expand Up @@ -449,8 +457,25 @@
");
}

private DateTime _lastRefresh = DateTime.MinValue;
private bool _refreshPending;

private async Task SafeRefreshAsync()
{
// Throttle to max ~4 refreshes per second to avoid blocking UI interactions
var now = DateTime.UtcNow;
if ((now - _lastRefresh).TotalMilliseconds < 250)
{
if (!_refreshPending)
{
_refreshPending = true;
_ = Task.Delay(250).ContinueWith(_ => InvokeAsync(SafeRefreshAsync));
}
return;
}
_lastRefresh = now;
_refreshPending = false;

// Capture all card input values before re-render wipes them
try
{
Expand Down Expand Up @@ -793,6 +818,8 @@
}

private const int DefaultMessageWindow = 25;
private const int DefaultCardMessageWindow = 10;
private Dictionary<string, int> cardMessageCounts = new();

private List<ChatMessage> GetWindowedMessages(string sessionName, IReadOnlyList<ChatMessage> history)
{
Expand All @@ -808,6 +835,26 @@
}
}

private List<ChatMessage> GetCardMessages(string sessionName, IReadOnlyList<ChatMessage> 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;
Expand All @@ -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<string> 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;
Expand Down
23 changes: 23 additions & 0 deletions Components/Pages/Dashboard.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
24 changes: 16 additions & 8 deletions MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading