From bcc505ce0f956d80d65d9c972b436c1c4098f376 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Sat, 18 Jul 2026 22:10:39 -0700 Subject: [PATCH] Update Copilot SDK model selection --- src/MauiSherpa.Core/Interfaces.cs | 12 +++ src/MauiSherpa.Core/MauiSherpa.Core.csproj | 2 +- .../Services/CopilotService.cs | 29 ++++++- src/MauiSherpa.MacOS/MauiSherpa.MacOS.csproj | 2 +- src/MauiSherpa/Pages/Copilot.razor | 83 ++++++++++++++++++- 5 files changed, 122 insertions(+), 6 deletions(-) diff --git a/src/MauiSherpa.Core/Interfaces.cs b/src/MauiSherpa.Core/Interfaces.cs index 26124afa..4734c8c7 100644 --- a/src/MauiSherpa.Core/Interfaces.cs +++ b/src/MauiSherpa.Core/Interfaces.cs @@ -2510,6 +2510,8 @@ Task RunAsync( event Action? OnModalClosed; } +public sealed record CopilotModelOption(string Id, string Name); + /// /// Service for interacting with GitHub Copilot /// @@ -2550,6 +2552,16 @@ public interface ICopilotService /// /// Optional model to use Task StartSessionAsync(string? model = null, string? systemPrompt = null); + + /// + /// Get the models available to the authenticated user. + /// + Task> ListModelsAsync(); + + /// + /// Change the model for the active chat session. + /// + Task SetModelAsync(string model); /// /// End the current chat session diff --git a/src/MauiSherpa.Core/MauiSherpa.Core.csproj b/src/MauiSherpa.Core/MauiSherpa.Core.csproj index 38af9e3e..2451bf8b 100644 --- a/src/MauiSherpa.Core/MauiSherpa.Core.csproj +++ b/src/MauiSherpa.Core/MauiSherpa.Core.csproj @@ -38,7 +38,7 @@ - + diff --git a/src/MauiSherpa.Core/Services/CopilotService.cs b/src/MauiSherpa.Core/Services/CopilotService.cs index 3b0c186f..3e9d4c1e 100644 --- a/src/MauiSherpa.Core/Services/CopilotService.cs +++ b/src/MauiSherpa.Core/Services/CopilotService.cs @@ -598,7 +598,8 @@ public async Task StartSessionAsync(string? model = null, string? systemPrompt = try { - _logger.LogInformation($"Starting Copilot session with model: {model ?? "default"}"); + var selectedModel = string.IsNullOrWhiteSpace(model) ? "auto" : model; + _logger.LogInformation($"Starting Copilot session with model: {selectedModel}"); // Get tools from tools service var tools = _toolsService.GetTools(); @@ -609,7 +610,7 @@ public async Task StartSessionAsync(string? model = null, string? systemPrompt = var config = new SessionConfig { - Model = model ?? "claude-opus-4.5", // Use Claude Opus 4.5 as default + Model = selectedModel, Streaming = true, Tools = tools.Cast().ToList(), OnPermissionRequest = HandleSdkPermissionRequest, @@ -657,6 +658,30 @@ public async Task StartSessionAsync(string? model = null, string? systemPrompt = throw; } } + + public async Task> ListModelsAsync() + { + if (_client == null) + { + throw new InvalidOperationException("Not connected to Copilot. Call ConnectAsync first."); + } + + var models = await _client.ListModelsAsync(); + return models + .Select(model => new CopilotModelOption(model.Id, model.Name ?? model.Id)) + .ToList(); + } + + public async Task SetModelAsync(string model) + { + if (_session == null) + { + throw new InvalidOperationException("No active Copilot session. Call StartSessionAsync first."); + } + + await _session.SetModelAsync(model); + _logger.LogInformation($"Copilot session model changed to: {model}"); + } #pragma warning disable GHCP001 private async Task HandleSdkPermissionRequest(PermissionRequest request, PermissionInvocation invocation) diff --git a/src/MauiSherpa.MacOS/MauiSherpa.MacOS.csproj b/src/MauiSherpa.MacOS/MauiSherpa.MacOS.csproj index 926e0f12..dc7f0867 100644 --- a/src/MauiSherpa.MacOS/MauiSherpa.MacOS.csproj +++ b/src/MauiSherpa.MacOS/MauiSherpa.MacOS.csproj @@ -35,7 +35,7 @@ - + diff --git a/src/MauiSherpa/Pages/Copilot.razor b/src/MauiSherpa/Pages/Copilot.razor index b86b9393..5a0ef701 100644 --- a/src/MauiSherpa/Pages/Copilot.razor +++ b/src/MauiSherpa/Pages/Copilot.razor @@ -191,6 +191,15 @@ else
+
+ + +
@* Show suggestions as first item - collapsible *@
@@ -806,6 +815,34 @@ else gap: 1.25rem; } + .model-selector { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + border-bottom: 1px solid var(--border-color); + color: var(--text-secondary); + font-size: 0.8rem; + } + + .model-selector label { + display: flex; + align-items: center; + gap: 0.35rem; + font-weight: 600; + } + + .model-selector select { + min-width: 10rem; + max-width: 16rem; + padding: 0.25rem 0.5rem; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--bg-tertiary); + color: var(--text-primary); + font: inherit; + } + .suggestions-section { background: var(--card-bg); border-radius: 0.75rem; @@ -2180,6 +2217,8 @@ else private CopilotAvailability? availability; private bool isConnecting = false; private bool isBusy = false; + private string selectedModel = "auto"; + private List availableModels = [new("auto", "Auto")]; private bool suggestionsCollapsed = false; private string userInput = ""; private string currentResponse = ""; @@ -2448,7 +2487,8 @@ else try { await CopilotService.ConnectAsync(); - await CopilotService.StartSessionAsync(systemPrompt: _systemPrompt); + await CopilotService.StartSessionAsync(selectedModel, _systemPrompt); + await LoadModelsAsync(); availability = availability is null ? new CopilotAvailability(true, true, null, null, null) : availability with { IsInstalled = true, IsAuthenticated = true, ErrorMessage = null }; @@ -2534,11 +2574,12 @@ else if (!CopilotService.IsConnected) { await CopilotService.ConnectAsync(); + await LoadModelsAsync(); } if (CopilotService.CurrentSessionId == null) { - await CopilotService.StartSessionAsync(systemPrompt: _systemPrompt); + await CopilotService.StartSessionAsync(selectedModel, _systemPrompt); } await CopilotService.SendMessageAsync(message); @@ -2553,6 +2594,44 @@ else } } + private async Task LoadModelsAsync() + { + try + { + var models = await CopilotService.ListModelsAsync(); + availableModels = [ + new CopilotModelOption("auto", "Auto"), + .. models.Where(model => !string.Equals(model.Id, "auto", StringComparison.OrdinalIgnoreCase)) + ]; + } + catch (Exception ex) + { + _logger.LogWarning($"Unable to load Copilot models: {ex.Message}"); + } + } + + private async Task SelectModelAsync(ChangeEventArgs args) + { + var model = args.Value?.ToString(); + if (string.IsNullOrWhiteSpace(model) || string.Equals(model, selectedModel, StringComparison.Ordinal)) + return; + + var previousModel = selectedModel; + selectedModel = model; + + try + { + await CopilotService.SetModelAsync(selectedModel); + currentUsageInfo = null; + } + catch (Exception ex) + { + selectedModel = previousModel; + _logger.LogError($"Unable to change Copilot model: {ex.Message}", ex); + await AlertService.ShowAlertAsync("Model Change Failed", ex.Message); + } + } + private void OnNativeMessageSubmitted(string message) { InvokeAsync(async () =>