MAUI AI Chat Control — ASP.NET AI Components convergence#274
MAUI AI Chat Control — ASP.NET AI Components convergence#274mattleibow wants to merge 51 commits into
Conversation
A fully composable, drop-in MAUI chat control backed by IChatClient (Microsoft.Extensions.AI) with a Copilot SDK adapter as default backend. Architecture: - CopilotChatView is a ContentView with ControlTemplate (TemplatedParent binding pattern) — never touches user's BindingContext - Every sub-component is replaceable via DataTemplate BindableProperties: 5 message templates + InputTemplate + ApprovalTemplate + WelcomeTemplate + SuggestionItemTemplate - Users can replace the entire ControlTemplate for total visual control - Or just override Copilot* resource keys for color/size tweaks Backend: - Accepts any IChatClient — works with Azure OpenAI, Ollama, etc. - Ships CopilotSdkChatClient: IChatClient adapter wrapping GitHub Copilot SDK - Control owns the chat flow: streaming, tool call correlation, approval - Events for observability: MessageSending/Sent, ResponseReceived, etc. Themes: - DefaultTheme (normal spacing) + CompactTheme (dense layout) - Both with AppThemeBinding for dark/light mode - Copilot-branded indigo/purple accent, cool gray surfaces Package: Microsoft.Maui.CopilotChat (net10.0) Sample: samples/CopilotChat.Sample (MAUI multi-target + DevFlow) Tests: 17 passing (template selector, message model INPC, expand/collapse) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…errors - Microsoft.Maui.CopilotChat no longer depends on GitHub.Copilot.SDK - New Microsoft.Maui.CopilotChat.CopilotSdk package for the adapter - TreatWarningsAsErrors=true on all new projects - Added CopilotSdk.Tests with 5 tests - Fixed FunctionApprovalRequestContent usage (MEAI 10.3.0 API) - Sample app updated to use AddCopilotChatWithCopilotSdk() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes from Opus 4.7: - CRITICAL: Fix approval flow dead end — store pending FunctionApprovalRequestContent, send FunctionApprovalResponseContent on approve/reject, resume streaming - HIGH: Fix ClearMessages CTS race — use Interlocked.Exchange, defer disposal - HIGH: Fix history not flushed on error — move to finally block - MEDIUM: Fix InverseBoolConverter default (false for non-bool) - MEDIUM: Add SystemMessage propertyChanged callback to rebuild history Fixes from GPT 5.5: - CRITICAL: Fix template selector bindings — CLR properties can't be bound in ControlTemplate XAML. Wire selector from OnApplyTemplate code instead. Added propertyChanged callbacks on all 5 message template properties. - HIGH: Fix duplicate streaming — skip AssistantMessageEvent text (only emit deltas) - HIGH: Fix error propagation — await done.Task after loop to surface exceptions - HIGH: Fix singleton session issues — add SemaphoreSlim for init, add ResetSessionAsync - MEDIUM: Fix Dispose sync-over-async deadlock — implement IAsyncDisposable, fire-and-forget in sync Dispose - MEDIUM: Fix EnsureSessionAsync race — double-check locking with SemaphoreSlim - MEDIUM: Add configurable StreamingTimeout (default 5min) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Unit tests (11): - Constructor, Dispose, DisposeAsync (multiple calls safe) - GetService returns null for unknown types - StreamingTimeout default and setter - Empty prompt and no-user-message yield nothing - IChatClient and IAsyncDisposable interface conformance Integration tests with real Copilot SDK (7): - GetResponseAsync basic prompt returns non-empty response - GetStreamingResponseAsync yields text chunks containing expected content - Streaming can be cancelled via CancellationToken - SystemMessage is respected by the model - Multiple sequential calls maintain session context - ResetSessionAsync clears session state - DisposeAsync after use cleans up resources Also fixed sample app to use ProjectReference for DevFlow agent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bugs fixed: - Sample app: ChatClient was null — DI wasn't resolving IChatClient into MainPage. Fixed by injecting IChatClient via constructor DI instead of resolving from Handler.MauiContext. - Sample app: Missing platform entry points (Program.cs, AppDelegate, MainActivity, MainApplication) — app couldn't launch - Sample app: Obsolete MainPage setter — use CreateWindow pattern - Suggestion chips: TapGestureRecognizer binding via RelativeSource AncestorType doesn't work across ControlTemplate boundaries. Fixed by wiring gesture recognizers from OnApplyTemplate code via PART_Suggestions ChildAdded event. - CopilotSdkChatClient: Bundled CLI not found in Mac Catalyst app bundle. Added CliPath config option to use system-installed copilot. - Directory.Build.props: Added ValidateXcodeVersion=false to allow building with newer Xcode than the workload expects. Verified working via DevFlow: - Welcome screen renders correctly - User message right-aligned in indigo bubble - AI streaming response left-aligned with shadow - Multi-turn conversation maintains context - Input field, send button, suggestion chips all functional Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The ConcurrentQueue + Task.Delay(50ms) polling loop was batching all SDK events together — by the time the consumer woke from sleep, multiple delta events had already been queued and were yielded in one burst, appearing as a sudden complete message. Replaced with System.Threading.Channels.Channel<ChatResponseUpdate> which uses async producer-consumer semantics: each TryWrite from the SDK event handler immediately wakes the consumer's ReadAllAsync, yielding each delta chunk individually with no artificial delay. Verified streaming works visually via DevFlow: essay response text grows progressively on screen over multiple seconds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SDK tests Sample app (scaffolded from dotnet new maui -f net10.0): - Shell with 3 tabs: Home, Chat, Settings - Home page with tool descriptions and Open Chat button - Chat page with CopilotChatView + 5 registered tools (weather, calculator, random fact, app navigation, app info) - Settings page to configure system prompt, model, welcome text - Proper DI: IChatClient + SampleTools injected into ChatPage CopilotSdkChatClient fixes: - Now passes ChatOptions.Tools to SessionConfig.Tools — the SDK handles tool invocation natively (no UseFunctionInvocation needed) - Recreates session when tools change between calls Integration tests (14 passing, all with real Copilot SDK): - Text: GetResponse returns text, system message respected - Streaming: multiple chunks, chunks arrive over time (not batched) - Tool calling: single tool invoked, result in response, args received, correct tool selected from multiple, stream includes FunctionCall/ FunctionResult content - Session: context maintained, reset clears context - Cancellation: streaming can be cancelled - Lifecycle: dispose after use, GetService returns session Unit tests (10 passing): - Config defaults, property setting - Constructor, dispose idempotent, streaming timeout - Empty/missing prompts, interface conformance Total: 37 tests (17 control + 10 SDK unit + 10 SDK integration) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Control enhancements: - Avatar support: UserAvatarSource/Text, AssistantAvatarSource/Text, ShowAvatars, AvatarSize BindableProperties. Default templates render circular avatar with initials or image on each message bubble. - Timestamps: Timestamp + IsStreaming on CopilotChatMessage, ShowTimestamps BindableProperty on control - Localizable text: SendButtonText, ApproveButtonText, RejectButtonText, TypingIndicatorText — no more hardcoded strings in ControlTemplate - Identity: UserDisplayName, AssistantDisplayName BindableProperties - AddMessage populates avatar/timestamp from control defaults - IsStreaming set to false when response completes - Fixed fully-qualified namespace usage (System.Text.Json, Channels) Sample app redesigned: - Sidebar layout: main content + persistent chat panel on right - FAB toggle for narrow screens - Inline settings: user/assistant name, show avatars, show timestamps, clear chat - 5 callable tools (weather, calculator, facts, navigation, app info) - Scaffolded from dotnet new maui -f net10.0 - Added network.server entitlement for DevFlow Theme: - Avatar color keys (CopilotUserAvatar*, CopilotAssistantAvatar*) - Localizable button text bindings in ControlTemplate Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dotnet new maui template enables App Sandbox by default, which blocks the Copilot SDK from spawning the copilot CLI process (Operation not permitted). Disabled for this dev sample. Production apps should either disable sandbox or use CopilotClientOptions.CliUrl to connect to an external Copilot server instead of spawning a child process. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sample app is now a 'Copilot Chat Playground' with real-time customization of every control property: - General: welcome icon/title, placeholder, clear chat - Identity: user/assistant names, avatar text, show avatars toggle, avatar size slider - Messages: user/assistant bubble color swatches (6 presets each), accent color swatches, timestamps toggle, spacing slider, font size slider - Localization: send/approve/reject/typing indicator text - Tools: weather, calculator, facts, app info Runtime resource overrides: color swatches update Copilot* resource keys on the control and force template refresh so StaticResource bindings pick up the new values. Also fixed: all icons switched from FluentFilled glyphs (requires bundled font) to standard Unicode emoji that render everywhere. Verified via DevFlow: - Tool calling works (GetCurrentWeather invoked, result displayed) - Avatars render correctly (You/AI circles) - Streaming works progressively - All settings sections visible and functional Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sync Fixes: - Theme templates now use DynamicResource (not StaticResource) for all Copilot* color/size keys. This means ChatView.Resources['CopilotAccent'] = newColor instantly updates rendered bubbles without template refresh. - Placeholder BindableProperty now has propertyChanged that syncs to PART_Input Entry (template binding wasn't propagating reliably) - SendButtonText/ApproveButtonText/RejectButtonText same fix — direct PART_ element sync via propertyChanged callbacks - SyncTemplateProperties() in OnApplyTemplate sets initial values on all PART_ elements - Sample defers WireTextSettings to Loaded event to avoid TextChanged firing during Entry initialization Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The sed that converted StaticResource to DynamicResource also hit converter references (CopilotInverseBool, CopilotIsNotNull). XAML Converter= only accepts StaticResource — DynamicResource causes XamlParseException at startup. Fixed by reverting converter references back to StaticResource while keeping all color/size keys as DynamicResource for live theming. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ggle
Avatar:
- ShowAvatar, AvatarSize, ShowTimestamp now on CopilotChatMessage model
(DataTemplates can't bind to templated parent, only to their item)
- Templates bind to message.ShowAvatar, message.AvatarSize with IsVisible
- Control propertyChanged callbacks propagate to all existing messages
- Avatar uses StrokeShape='RoundRectangle 999' for perfect circle at any size
Timestamps:
- TimestampText property on message ('h:mm tt' format)
- Templates show timestamp below bubble when ShowTimestamp=true
Thinking indicator:
- Shows TypingIndicatorText ('Thinking…') as system message after send
- Removed when first text or tool call content arrives
Compact theme toggle:
- Sample settings has switch to swap DefaultTheme ↔ CompactTheme at runtime
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expert Code Review — PR #274Methodology: 3 independent reviewers with adversarial consensus (batch-split mode: 60 files split across 3 reviewers, 20 each) 13 findings posted as inline comments (5 moderate, 8 minor)
CI Status
Test Coverage AssessmentThe PR includes 3 test projects with 41 tests (17 control unit, 10 SDK unit, 14 integration). However, the integration tests ( Discarded FindingsNo findings were discarded — all survived consensus or qualified under batch-split rules. Notes
|
There was a problem hiding this comment.
Expert Code Review: 13 findings posted inline (5 moderate, 8 minor). See the lean summary comment for full details and methodology.
Generated by Expert Code Review (auto) for issue #274 · ● 12.9M
| MainThread.BeginInvokeOnMainThread(async () => | ||
| { | ||
| await Shell.Current.GoToAsync(route); |
There was a problem hiding this comment.
🟡 MODERATE · 2/3 consensus
NavigateToPage is broken: Shell.Current is null because the app uses Window(new MainPage(...)) — not Shell navigation. This will throw NullReferenceException. Additionally, BeginInvokeOnMainThread fire-and-forgets the navigation, so exceptions are unobserved and the method reports success before navigation completes (or fails).
Suggestion: Either add Shell navigation to the app, or guard with a null check + use MainThread.InvokeOnMainThreadAsync and await the result so failures propagate back to the caller.
There was a problem hiding this comment.
These files no longer exist — the entire codebase was rewritten in commits cb4ce39..f20a9ac (phases 1-9). The old CopilotChat package was replaced with a two-library architecture: Microsoft.Maui.AI.Chat (headless) + Microsoft.Maui.AI.Chat.Controls (MAUI UI). The concerns raised here (Shell navigation, race conditions, CTS leaks) are not present in the new code.
| private async Task DisposeAsyncCore() | ||
| { | ||
| if (_session is not null) | ||
| await _session.DisposeAsync().ConfigureAwait(false); | ||
| if (_client is not null) | ||
| await _client.DisposeAsync().ConfigureAwait(false); | ||
| _initLock.Dispose(); |
There was a problem hiding this comment.
🟡 MODERATE · low confidence — single reviewer (batch split)
Race condition in DisposeAsyncCore: This method disposes _initLock without first acquiring it. If another thread is inside EnsureSessionAsync holding the lock, _initLock.Dispose() will cause ObjectDisposedException in the finally { _initLock.Release(); } of that thread, leaking a partially-constructed session/client.
Suggestion: Acquire _initLock before reading/disposing _session/_client, release it, then dispose the lock. Also check _disposed after acquiring the lock in EnsureSessionAsync.
There was a problem hiding this comment.
This file was deleted — CopilotSdkChatClient no longer exists. The new architecture accepts any IChatClient implementation via DI with no internal session management.
| ResponseReceived?.Invoke(this, assistantMessage); | ||
| } | ||
| else if (string.IsNullOrEmpty(responseText)) | ||
| AddMessage(ChatMessageKind.Assistant, "(no response)"); |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
Spurious "(no response)" in tool-only flows: When the assistant emits only tool/approval events without text, the UI shows a "(no response)" message even though the flow is valid.
Suggestion: Suppress the fallback when tool events or approval requests occurred during the stream — only emit it for genuinely empty responses.
There was a problem hiding this comment.
Resolved — the old CopilotChatView.cs in src/CopilotChat/ was deleted. The new CopilotChatView (in src/AIControls/Microsoft.Maui.AI.Chat.Controls/) uses a completely different architecture with ChatSession handling streaming separately from the UI layer.
| <SignAssembly>false</SignAssembly> | ||
| <NoSwixBuildPlugin>true</NoSwixBuildPlugin> | ||
| <!-- Skip Xcode version check — allows building with newer Xcode than the workload expects --> | ||
| <ValidateXcodeVersion>false</ValidateXcodeVersion> |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
Repo-wide Xcode validation suppression: This disables ValidateXcodeVersion for all projects in the repository, not just the CopilotChat sample. Future contributors using an incompatible Xcode may produce broken iOS binaries without warning.
Suggestion: Move this to the sample's .csproj or a Directory.Build.props inside samples/CopilotChat.Sample/ to limit scope.
There was a problem hiding this comment.
Fixed — moved ValidateXcodeVersion=false out of the repo-wide Directory.Build.props and kept it only in the sample app's .csproj where it's needed. Also fixed the Android SupportedOSPlatformVersion to match the repo default (24.0).
| { | ||
| options.Model = "gpt-4.1"; | ||
| options.SystemMessage = "You are a helpful assistant. Be concise and friendly. When using tools, explain what you're doing."; | ||
| options.CliPath = "/opt/homebrew/bin/copilot"; |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
Hardcoded macOS ARM Homebrew path: /opt/homebrew/bin/copilot only exists on Apple Silicon macOS. Fails on Windows, Intel Mac (/usr/local/bin/copilot), and mobile platforms.
Suggestion: Remove or comment out the hardcoded path — let the SDK resolve copilot from PATH. If needed, conditionalize with #if MACCATALYST.
There was a problem hiding this comment.
Deleted — samples/CopilotChat.Sample/MauiProgram.cs no longer exists. The new sample (samples/AiControlsSample/) uses Azure OpenAI via Microsoft.Extensions.AI.OpenAI with user secrets, no Copilot CLI dependency.
| { | ||
| Model = _config.Model, | ||
| Streaming = true, | ||
| OnPermissionRequest = PermissionHandler.ApproveAll, |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
PermissionHandler.ApproveAll hardcoded: All tool-execution permission requests are auto-approved with no override path via CopilotChatConfiguration. The PR description lists "Approval flow" as a feature, but this SDK client bypasses it at the protocol level.
Suggestion: Add Func<PermissionRequest, Task<bool>>? OnPermissionRequest to CopilotChatConfiguration with ApproveAll as the default, and wire it here.
There was a problem hiding this comment.
Resolved — this file (CopilotSdkChatClient.cs) was deleted in the rewrite. None of these concerns apply to the current code.
| public void Dispose() | ||
| { | ||
| if (_disposed) return; | ||
| _disposed = true; | ||
| // Fire-and-forget async cleanup to avoid sync-over-async deadlock | ||
| _ = DisposeAsyncCore(); |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
Dispose() fire-and-forgets async cleanup: DisposeAsyncCore() is invoked without await and the task is discarded. If GC collects the object before the task completes, _session and _client are silently abandoned. Additionally, a subsequent DisposeAsync() call will no-op due to _disposed being already set, with no guarantee the first fire-and-forget has completed.
Suggestion: Consider throwing InvalidOperationException from Dispose() directing callers to use await using, or use Interlocked.Exchange to ensure at-most-once execution shared between both paths.
There was a problem hiding this comment.
Resolved — this file (CopilotSdkChatClient.cs) was deleted in the rewrite. None of these concerns apply to the current code.
| chunks.Add(update); | ||
|
|
||
| var fullText = string.Join("", chunks | ||
| .SelectMany(c => c.Contents.OfType<TextContent>()) |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
GetResponseAsync drops tool call/result content: Only TextContent is aggregated from streaming updates. FunctionCallContent and FunctionResultContent are silently discarded. Callers inspecting response.Contents for tool-use metadata will see nothing.
Suggestion: Collect all AIContent types into the response message, not just TextContent.
There was a problem hiding this comment.
Resolved — this file (CopilotSdkChatClient.cs) was deleted in the rewrite. None of these concerns apply to the current code.
| { | ||
| // Always flush history, even on cancellation/error, to avoid orphan messages | ||
| if (updates.Count > 0) | ||
| _history.AddMessages(updates); |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
ClearMessages() race with streaming: If a user calls ClearMessages() while streaming is active, the finally block still flushes updates into _history. This means previously-cleared context can reappear in the next prompt's history.
Suggestion: Track a generation/epoch counter incremented by ClearMessages(). In the finally block, skip _history.AddMessages(updates) if the epoch changed during the stream.
There was a problem hiding this comment.
Resolved — the old CopilotChatView.cs in src/CopilotChat/ was deleted. The new CopilotChatView (in src/AIControls/Microsoft.Maui.AI.Chat.Controls/) uses a completely different architecture with ChatSession handling streaming separately from the UI layer.
| }); | ||
|
|
||
| // Start inactivity timeout monitor | ||
| var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); |
There was a problem hiding this comment.
🟡 MODERATE · low confidence — single reviewer (batch split)
CancellationTokenSource leak: CreateLinkedTokenSource registers callbacks on the parent token. timeoutCts is only canceled (line 166) on the happy path but never disposed. If the iterator is abandoned early or an exception is thrown, the linked CTS callbacks remain registered — a per-message memory/handle leak that accumulates over session lifetime.
Suggestion: Wrap in try/finally ensuring timeoutCts.Dispose() is always called.
There was a problem hiding this comment.
Resolved — this file (CopilotSdkChatClient.cs) was deleted in the rewrite. None of these concerns apply to the current code.
Port AI session infrastructure from MauiDojo-agui to Microsoft.Maui.AI: - IAgentSession/AgentSession: streaming chat with IChatClient + FunctionInvocation - ChatMessageViewModel: merged AGUI + CopilotChat message models with MVVM Toolkit - IAgentSessionFactory/AgentSessionFactory: factory pattern with IChatClient - InvocationContext: tool call tracking with typed argument/result helpers - Suggestion: suggestion chip record - ServiceCollectionExtensions: DI registration - AI.slnx solution filter Key change from AGUI: replaced AIAgent with IChatClient + ChatClientBuilder.UseFunctionInvocation() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy CopilotSdkChatClient, CopilotChatConfiguration, and CopilotSdkServiceCollectionExtensions from src/CopilotChat/Microsoft.Maui.CopilotChat.CopilotSdk/ with namespace changed to Microsoft.Maui.AI.CopilotSdk. Project references Microsoft.Maui.AI instead of CopilotChat. Updated AI.slnx to include the new project alongside AI and AI.Controls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create the controls package with AgentChatView backed by IAgentSession (replacing CopilotChatView's IChatClient approach). Port themes, converters, template selectors, SuggestionBar, and AgentLoadingIndicator from CopilotChat and AGUI codebases. Key changes from CopilotChatView: - Session (IAgentSession) replaces ChatClient (IChatClient) - Internal chat engine removed; IAgentSession handles history, streaming, tool execution, and human-in-the-loop - Messages bound directly to Session.Messages - IsBusy bound to Session.IsProcessing - SuggestionPrompts uses IList<Suggestion> instead of IList<string> - Removed approval bar (HITL handled by Session) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New sample app at samples/AI.Sample/ demonstrating: - PlaygroundPage: Customizable chat control settings with AgentChatView - AgenticChatPage: Frontend tool (change_background) showing agent actions - ToolRenderingPage: Weather/math/fact tools with inline results Uses IAgentSession from Microsoft.Maui.AI, AgentChatView from Microsoft.Maui.AI.Controls, and CopilotSdkChatClient backend. Shell flyout with locked sidebar navigation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…at-control # Conflicts: # MauiLabs.slnx
- Remove src/AI/Microsoft.Maui.AI.CopilotSdk/ (now in separate branch) - Remove src/CopilotChat/ (superseded by Microsoft.Maui.AI) - Remove samples/CopilotChat.Sample/ (superseded by AI.Sample) - Remove old CopilotChat test projects - Remove GitHub.Copilot.SDK from Directory.Packages.props - Update AI.Sample to use Azure OpenAI via user secrets - Embed secrets.json at build time (same pattern as Garden sample) - UserSecretsId: ai-attributes-secrets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Merge Microsoft.Maui.AI + Microsoft.Maui.AI.Controls into single project at src/AIControls/Microsoft.Maui.AI.Controls/ - Folder-based separation: Services/, Controls/, Templates/, Converters/, Themes/, Extensions/ - Rename samples/AI.Sample → samples/AiControlsSample - Remove old src/AI/Microsoft.Maui.AI and src/AI/Microsoft.Maui.AI.Controls - Update MauiLabs.slnx references - All builds pass: 0 warnings, 0 errors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…settings - Shell flyout: Locked → Flyout (hamburger), styled header with accent - Playground: chat fills full width, settings in slide-up bottom panel - Added proper nav styling (purple accent, emoji icons, themed colors) - Removed fixed-width sidebar layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… clear - Chat control fills available width (removed 840px cap from template) - Message bubbles max at 960px for readability on ultra-wide - Playground settings: sidebar (docked) on wide screens, overlay on narrow - Clear chat moved to toolbar item - Settings toggle via ⚙️ toolbar item - All pages use same full-width chat pattern - Shell flyout styling with branded header Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Sidebar uses 4*/1* grid columns (proportional instead of fixed 280px) - On narrow screens, settings shows as action sheet popup - Fixed Border StrokeShape (required for content rendering) - Fixed tool chips: replaced FlexLayout with HorizontalStackLayout rows - Added LineBreakMode=NoWrap so labels don't truncate - Fixed obsolete DisplayActionSheet → DisplayActionSheetAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Grid uses ColumnDefinitions='*,Auto' (stable, no dynamic resizing) - Sidebar fixed at 300px via WidthRequest (max capped, content fits) - Chat content centered via MaximumWidthRequest=960 + HorizontalOptions=Center - Toggle shows/hides sidebar cleanly - Removed flawed dynamic ColumnDefinition manipulation that caused 0-width bugs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- AgentChatView now decorates ChatMessageViewModels with avatar/show settings - CollectionChanged handler applies avatar text/source/visibility to new messages - RedecorateAllMessages() propagates changes to existing messages on property change - Sidebar pinned open on wide screens (>=700px), auto-hidden on narrow - Messages use HorizontalOptions=Fill (not Center) — centering is via MaxWidth - Removed toggle/close buttons (sidebar is always-on for playground) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add Plan, Step, StepStatus, PlanConfirmationResult, JsonPatchOperation models - Add test project with 31 tests (ContentTemplateSelector, InvocationContext, Models, AgentSession HITL) - Add HumanInTheLoopPage and SharedStatePage demos - Fix suggestion chips: replace Border+TapGestureRecognizer with Button (fixes tap not firing on Mac Catalyst) - Add test project to AIControls.slnx - Support both Button and custom view children in suggestion layout wiring Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase A (Critical Bug Fixes): - Fix ToChatMessage: preserve FunctionCallContent/FunctionResultContent across turns - Fix reasoning-only messages: include messages with ReasoningText even if Text empty - Fix HITL double-waiter: each WaitForResponse caller gets own TCS - Add Cancel() + IDisposable + Failed event on AgentSession - Fix InvocationContext.GetArgument: handle JsonElement from M.E.AI tools Phase B (Architecture): - Add SidebarTemplate/HeaderTemplate/FooterTemplate + SidebarPlacement to AgentChatView - Add StateContext property for sidebar BindingContext - Add StopButtonText: Send button becomes Stop during streaming - Ship JsonPatch utility (RFC 6902 add/replace/remove) - Ship StateChannel<T> (auto-deserialize state snapshots/deltas) - Add XmlnsDefinition for xmlns:ai="http://schemas.microsoft.com/dotnet/maui/ai" - Split AgentChatView.cs into partial files (Properties + Logic) Phase D (Tests): - ChatMessageViewModelTests: 9 tests (round-trip all content types) - JsonPatchTests: 9 tests (add/replace/remove/nested/bytes) - InvocationContextTests: +4 JsonElement tests - AgentSessionCancelTests: 7 tests (cancel/dispose/failed) - FakeChatClient: configurable test double for streaming - Total: 59 tests passing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…resources - Add AgenticGenerativeUIPage: auto-executing plan with real-time progress - Add PredictiveStatePage: split-pane document editor with accept/reject - Add HaikuPage: haiku carousel with colored backgrounds and navigation - Extract PlanCardView reusable control from HITL page (used by HITL + GenUI) - Add plan card theme resource keys (CopilotPlan*, CopilotConfirm, CopilotReject) - Create CI workflow (.github/workflows/ci-aicontrols.yml) - Register all 8 demo pages in AppShell + MauiProgram - All 59 tests pass, 0 warnings, 0 errors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Critical bug fix: ToChatMessage() now strips FunctionCallContent and
FunctionResultContent from conversation history. These are handled
internally by FunctionInvokingChatClient within a single streaming
session and must not be resent — doing so caused HTTP 400 errors
("assistant message with tool_calls must be followed by tool messages")
on subsequent turns.
Also preserves DataContent (state snapshots) and correctly falls back
to Text property when Contents is empty.
Demo improvements:
- Restructured all 7 demo pages into Demos/ subfolders with READMEs
- Improved HITL system prompt so model calls create_plan AND
confirm_plan in the same turn (enables blocking confirmation flow)
- All 7 demos verified working via DevFlow:
* Agentic Chat: 3-turn tool calling ✅
* Tool Rendering: weather + multi-turn ✅
* Human in the Loop: plan + confirm + execute ✅
* Shared State: recipe update tool ✅
* Agentic GenUI: auto-plan execution ✅
* Predictive State: document streaming ✅
* Haiku Generator: JP/EN haiku with theme ✅
Tests: 60 passing, updated to verify tool content stripping behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the headless chat session engine from MauiAIAnnotations: - ChatSession: wraps IChatClient with streaming, tool approval, history management - IChatSession: headless contract with Changed events, CancelAsync, approval workflow - ChatEntry: immutable record with Timestamp, ToolName, ApprovalState - ContentRole/ToolApprovalState/ChatSessionChangeKind enums - ServiceCollectionExtensions.AddChatSession() DI helper Tests cover: send/stream, streaming accumulation, error handling, system prompt, timestamps, clear/cancel, approval full cycle, edited approvals, identity validation, auto-reject stale approvals, independent sessions, approve-and-follow-up with FunctionInvokingChatClient middleware. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaced the old Controls/Converters/Extensions/Models/Services/Templates scaffold with the full chat UI library ported from Microsoft.Extensions.AI.Maui with updated namespaces: - Microsoft.Extensions.AI.Chat → Microsoft.Maui.AI.Chat - Microsoft.Extensions.AI.Maui → Microsoft.Maui.AI.Controls - Microsoft.Extensions.AI.Maui.Chat → Microsoft.Maui.AI.Controls.Chat - Microsoft.Extensions.AI.Maui.Themes → Microsoft.Maui.AI.Controls.Themes Includes: - Chat/ — ContentContext, ContentTemplate, ContentTemplateSelector, and built-in content views (text, function call/result, error, default, tool approval) - Controls/ — ChatPanelControl (XAML + code-behind) - Themes/ — ChatTheme resource dictionary and ChatThemeKeys constants Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Delete 8 old test files (AgentSession*, ChatMessageViewModel*, ContentTemplateSelector*, etc.) - Create ContentTemplateTests.cs with 21 tests covering: - TextContentTemplate: matching, role filtering, priority - FunctionCallTemplate: matching, tool name filtering, priority - FunctionResultTemplate: matching, tool name filtering - ToolApprovalTemplate: matching, tool name filtering - ErrorContentTemplate: matching - DefaultContentTemplate: match-everything, lowest priority - ContentContext: property exposure, approval resolution text - Priority ordering: tool-specific > generic > default - Add InternalsVisibleTo for test access to GetPriority() - Update AIControls.slnx with all 5 projects - All 42 tests pass (21 chat + 21 controls) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace all AgentChatView + IAgentSession usage with the new ChatPanelControl + ChatSession system across all 8 demo pages: - AgenticChatPage: custom change_background tool with ChatSession - ToolRenderingPage: default tools from DI + custom WeatherResultView - HumanInTheLoopPage: plan sidebar with confirm/reject via chat messages - SharedStatePage: recipe editor with update_recipe tool - PredictiveStatePage: document writer with accept/reject buttons - AgenticGenerativeUIPage: auto-executing plan with progress footer - HaikuPage: haiku display with create_haiku tool - PlaygroundPage: settings sidebar with system prompt and quick prompts New files added for the ToolRendering demo: - WeatherResultTemplate.cs (extends FunctionResultTemplate) - WeatherResultView.xaml/.cs (ContentContextView for weather cards) Also includes the MauiProgram.cs and csproj updates that register ChatSession, IEnumerable<AITool>, and IChatClient with middleware. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- 36 new tests for bindable property defaults, roundtrips, and theme keys - Playground settings: timestamps toggle, tool visibility, corner radius slider, max width slider, placeholder editor, welcome message editor - All 78 tests pass (21 chat + 57 controls), 0 warnings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nd restructure - Rename library: Microsoft.Maui.AI.Controls → Microsoft.Maui.AI.Chat.Controls - Move Chat/ and Controls/ files to project root (flatten hierarchy) - Keep Contents/ and Themes/ as subdirectories - Move test projects to root tests/ folder: - tests/Microsoft.Maui.AI.Chat.Tests/ - tests/Microsoft.Maui.AI.Chat.Controls.Tests/ - Update all namespace references, csproj paths, XAML assembly names - Update solution files (AIControls.slnx, MauiLabs.slnx) - All 78 tests pass, sample app builds with 0 warnings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Convert from ContentView to TemplatedView with replaceable PART_* parts: PART_Header, PART_Messages, PART_WelcomePanel, PART_WelcomeIcon, PART_WelcomeMessage, PART_BusyIndicator, PART_Suggestions, PART_Footer, PART_InputEntry, PART_SendButton, PART_InputArea - Add default ControlTemplate in ChatTheme.xaml with implicit Style - Add AppBuilderExtensions.UseChatControls() for auto-loading theme - Add ChatThemeLoader helper for reliable resource merging - Fix invalid StrokeThickness values in demo XAML (double, not Thickness) - Update all sample pages, tests, and docs to use CopilotChatView name Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two fixes for the NullReferenceException during PlaygroundPage.InitializeComponent(): 1. Move ChatThemeLoader.EnsureLoaded() from CopilotChatView constructor to OnParentSet() — mutating Application.Resources.MergedDictionaries during XAML parsing triggers resource re-evaluation on partially-constructed pages. 2. Initialize SuggestionPrompts with defaultValueCreator returning new List<string>() instead of null — the XAML source generator calls .Add() on the collection when processing <x:String> child elements, causing NullRef at line 620. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix ToolName mismatch: WeatherResultTemplate was looking for 'get_current_weather' but AIFunctionFactory.Create(GetCurrentWeather) registers as 'GetCurrentWeather' (PascalCase method name) - Add TESTING.md with user scenarios to all 8 demo folders - All demos verified working via DevFlow walkthroughs: Playground, Agentic Chat, Tool Rendering (weather card renders), Human in the Loop, Shared State, Predictive State, Agentic Generative UI, Haiku Generator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Suppress XC0022 (compiled bindings warning) in the controls library — TemplateBinding in ControlTemplates triggers false positives with TreatWarningsAsErrors=true - Move ValidateXcodeVersion=false from repo-wide Directory.Build.props to only the sample app csproj (reviewer feedback) - Fix sample Android SupportedOSPlatformVersion: 21 → 24 to match repo default Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy headless engine (70 files) and Blazor components (18 files) from dotnet/aspnetcore branch javiercn/ai-components-e2e-tests as the shared foundation for AI chat convergence. Core (net10.0, zero UI deps): UIAgent, AgentContext, Pipeline, ContentBlock hierarchy, RichText AST (28 nodes), Attributes. Blazor: ChatPage, MessageInput, BlockRenderer, AgentBoundary, SuggestionList — for Blazor Hybrid support. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace all references to the deleted Microsoft.Maui.AI.Chat library (IChatSession, ChatSession, ChatEntry, ContentRole, ToolApprovalState, ChatSessionChangedEventArgs, ChatSessionChangeKind) with the new Microsoft.AspNetCore.Components.AI.Core types (AgentContext, ConversationTurn, ContentBlock, RichContentBlock, FunctionInvocationContentBlock, FunctionApprovalBlock, etc.). Key changes: - csproj: ProjectReference now points to AI.Core - ContentContext: wraps ContentBlock instead of ChatEntry - CopilotChatView: Session is AgentContext; uses RegisterOnTurnAdded, RegisterOnStatusChanged, RegisterOnBlockAdded callbacks instead of the old Changed event; SendMessageAsync replaces SendAsync - TextContentTemplate: matches RichContentBlock; Role is now a string - FunctionCallTemplate/FunctionResultTemplate: match FunctionInvocationContentBlock (Result null vs non-null) - ToolApprovalTemplate/View: match FunctionApprovalBlock; use Approve()/Reject() directly instead of IToolApprovalResponseFactory - ErrorContentTemplate: returns false (errors via AgentContext.Status) - Removed IToolApprovalResponseFactory.cs (no longer needed) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace deleted Microsoft.Maui.AI.Chat (ChatSession) with the Core engine's UIAgent and AgentContext from Microsoft.AspNetCore.Components.AI. Changes: - Remove Microsoft.Maui.AI.Chat project reference from csproj - Remove AddChatSession/SampleTools DI registrations from MauiProgram.cs - Update all 7 demo pages + PlaygroundPage to construct UIAgent with ChatOptions (Instructions for system prompt, Tools for AI tools) and wrap in AgentContext - Update XAML bindings from Path=ChatSession to Path=Session - Fix WeatherResultView to use ContentContext.Block pattern instead of the removed ContentContext.Content property Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create AiControlsBlazorSample that demonstrates the AI Chat Controls using Blazor components in a BlazorWebView. The sample uses ChatPage from Microsoft.AspNetCore.Components.AI.Blazor with a UIAgent backed by Azure OpenAI (IChatClient) and includes a weather tool demo. Key implementation details: - Uses Microsoft.AspNetCore.Components.WebView.Maui for Blazor Hybrid - Removes transitive FrameworkReference Microsoft.AspNetCore.App via MSBuild target since there is no runtime pack for mobile platforms - Falls back to a NoOpChatClient when AI secrets are not configured - Added to AIControls.slnx solution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bug fixes (from Opus 4.7 review): - Fix memory leak: track and dispose ContentBlockChangedSubscription per block - Fix tool results never appearing when ShowToolCalls=false: re-check visibility in OnBlockChanged and add blocks that become visible later - Fix IsBusy state machine: disable input during AwaitingInput status too - Fix PlaygroundPage AgentContext leak on Clear/Apply: dispose old session - Fix PlaygroundPage OnQuickPromptClicked: guard against non-Idle state - Fix ChatThemeLoader static flag: remove to support hot reload/tests - Add propertyChanged hooks for HeaderTemplate, FooterTemplate, WelcomeIcon, and SuggestionPrompts bindable properties Other improvements: - Add upstream wwwroot/ai-chat.css to Blazor project (was missing) - Fix Blazor sample Android build: add MauiIcon, remove manual manifest icon - Update UPSTREAM-CHANGES.md with porting notes and thread-safety request - Fix stale ChatSession references in 4 demo README files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The suggestion chips (PART_Suggestions FlexLayout) were never becoming visible because OnApplyTemplate ran before the XAML parser finished adding items to the ObservableCollection<string> SuggestionPrompts. The fix uses the Loaded event which fires after the full visual tree is constructed and rendered, guaranteeing template parts are resolved and all XAML-set collection items have been added. Also replaced HandlerChanged approach with simpler Loaded handler. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bring over the Roslyn incremental source generator that processes [ToolBlock], [ToolParameter], and [ToolResult] attributes to generate strongly-typed ContentBlockHandler<T> implementations. The source generator: - Matches FunctionCallContent by tool name (not the generic catch-all) - Deserializes arguments into typed properties - Matches FunctionResultContent by CallId and deserializes results - Emits an aggregate AddGeneratedToolBlocks() extension method Changes: - New project: Microsoft.AspNetCore.Components.AI.SourceGenerators (netstandard2.0, copied from upstream gen/ directory) - ContentBlock.Id setter made public (required for generated handlers in consumer assemblies) — noted as upstream change - Sample app: Added WeatherToolBlock with [ToolBlock]/[ToolParameter]/ [ToolResult] attributes demonstrating the pattern - Updated WeatherResultView to use strongly-typed block with JSON fallback - ToolRenderingPage uses AddGeneratedToolBlocks() for handler registration - Added project to MauiLabs.slnx Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When FunctionInvokingChatClient handles tool calls internally (via UseFunctionInvocation() in the DI pipeline), the pipeline streams back FunctionCallContent followed by FunctionResultContent. The AgentContext adds blocks to uninvokedToolBlocks at yield time (when Result is null), but by the time streaming ends, the pipeline has already set the Result on those blocks. Without filtering, AgentContext would re-invoke the tools and send a duplicate tool result message to the LLM, causing the second turn to either error or produce no response. Fix: After the streaming loop, remove blocks whose Result was already set by the pipeline (meaning the tool was handled by the chat client middleware). Also add debug logging when AgentContext enters error state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copies all 80+ test files from dotnet/aspnetcore branch javiercn/ai-components-e2e-tests (src/Components/AI/test/) including: - Engine/ (16 tests): AgentContext, UIAgent, threads, approvals, errors - Pipeline/ (13 tests): BlockMapping, handlers, tool blocks, state mapper - Blocks/ (6 tests): ContentBlock, FunctionApproval/Invocation, Reasoning, RichText - Samples/ (10 tests): S01-S10 end-to-end scenarios with recordings - Components/ (14 tests): Blazor component tests (AgentBoundary, BlockRenderer, etc.) - TestFramework/ (10 files): Blazor test renderer infrastructure - TestHelpers/ (8 files): RecordingChatClient, ResponseEmitters, etc. - Baselines/ (10 recordings): JSON replay files for recorded sessions All 332 upstream tests pass with only two adaptations needed: 1. Added InternalsVisibleTo for the Blazor project 2. Suppressed BL0006 (RenderTree usage warning) in test project Existing 60 MAUI control tests continue to pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reorganize the combined test project into layered projects: - Microsoft.AspNetCore.Components.AI.Core.Tests (247 tests): Engine, Pipeline, Blocks, Samples, and recorded tests - Microsoft.AspNetCore.Components.AI.Blazor.Tests (96 tests): Component rendering, TestFramework, baseline replay tests Also: - Update InternalsVisibleTo in Core and Blazor csprojs - Add Azure.Identity package for recorded tests - Add Microsoft.Extensions.AI.OpenAI for AsIChatClient - Update CI workflow paths to include tests/ directories - Move NullAntiforgeryStateProvider to Blazor.Tests only - Copy baselines to both projects for recording replay All 403 tests pass (247 Core + 96 Blazor + 60 MAUI). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Delete trivial CopilotChatViewPropertyTests (assert default == default) - Add MessageListTests: multi-turn, block accumulation, streaming indicator - Add BlockRendererTests: template matching, priority, tool-specific overrides - Add ContentContextTests: role, toolName, approval state, lifecycle - Add ChatPageTests: session binding, error state, send guards, cancellation - Add SuggestionListTests: suggestion prompts, welcome state - Add TestChatClient: controllable IChatClient with streaming support - Add SessionFactory/BlockFactory: test helpers matching Blazor patterns - Replace Blazor.Tests baseline duplication with MSBuild file linking Total: 418 tests (247 Core + 96 Blazor + 75 MAUI), all passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Drop-in MAUI AI chat control (
CopilotChatView) backed by the ASP.NET Core AI Components engine. Provides a customizable, templated chat UI for any MAUI app with full tool-calling, rich text, reasoning, and approval flows.Architecture
What's Included
Libraries
[ToolBlock]source gen for typed tool parametersCopilotChatView)Test Projects (403 tests total)
Sample Apps
Key Features
[ToolBlock],[ToolParameter])Upstream Tracking
Changes to the copied ASP.NET AI Components code are tracked in
src/AIControls/Microsoft.AspNetCore.Components.AI.Core/UPSTREAM-CHANGES.md. The goal is to keep modifications minimal so we can swap in the official packages when they ship.