XAML → Markdown UI Indexer (accessibility-first, source generator)#269
XAML → Markdown UI Indexer (accessibility-first, source generator)#269mattleibow wants to merge 36 commits into
Conversation
Rebased onto latest main as a single commit. Includes: - Microsoft.Maui.AI.Attributes runtime library + source generator - IncludeTools/ExcludeTools filtering on [AIToolSource] - Assembly-wide auto-generated tool context - Property getter/setter tool support - 91 runtime tests + 116 generator tests - Garden Shop sample with chat, catalog, orders, reviews, cart modes - Expandable tool call details in chat UI - CI workflow (ci-ai.yml) + AzDO official pipeline job - NuGet package includes generator DLL in analyzers/ folder - Unique CI version suffix (ci.YYYYMMDD.N) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tributes Separate AI Attributes from the Essentials AI directory (src/AI/) so each product has its own top-level folder: - src/AIAttributes/ — AI Attributes library + source generator - tests/AIAttributes/ — runtime and generator tests - src/AI/ — reserved for Essentials AI Update all ProjectReferences, solution filter, CI workflow, AzDO pipeline, MauiLabs.slnx, and README paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename src/AIAttributes → src/AIExtensions and tests/AIAttributes → tests/AIExtensions to reflect the broader scope of AI extension packages (AI.Navigation will join AI.Attributes under src/AIExtensions/). Rename sample folders and csproj files from AIAttributes.Sample.* to AIExtensions.Sample.* and update all namespaces, project references, CI paths, and documentation links. Project/assembly names (Microsoft.Maui.AI.Attributes) are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…1 compat Microsoft.Extensions.AI.OpenAI 10.4.1 requires OpenAI >= 2.9.1. The EssentialsAI sample was pinned at 2.6.0, causing CS1705 on CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the reflection-based schema generation (GetMethod/GetProperty + CreateFunctionJsonSchema) with per-parameter CreateJsonSchema calls that use the source-generated JsonSerializerOptions. This eliminates all MethodInfo/PropertyInfo lookups from the generated code. Change AIToolMetadataServices to accept JsonTypeInfo<T> instead of JsonSerializerOptions, making all argument deserialization AOT-safe. The generator now emits the concrete JsonTypeInfo<T> expression for each parameter. Mark Microsoft.Maui.AI.Attributes with IsTrimmable=true and IsAotCompatible=true — trim and AOT analyzers report zero warnings. Key changes: - Generator: emit per-parameter schema via AIJsonUtilities.CreateJsonSchema with explicit s_jsonOptions instead of reflection-based CreateFunctionJsonSchema(MethodInfo) - Generator: emit s_jsonOptions field using AIJsonUtilities.DefaultOptions - Generator: emit JsonTypeInfo<T> expressions for GetRequiredArg/GetOptionalArg - AIToolMetadataServices: replace JsonSerializerOptions? parameter with JsonTypeInfo<T> for type-safe, trim-safe deserialization - Runtime lib csproj: add IsTrimmable=true, IsAotCompatible=true - Remove System.Reflection using from generated output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The IsNullable flag incorrectly included '|| p.Type.IsReferenceType', causing all reference-type parameters (e.g. 'string name') to be treated as nullable. This meant non-nullable string parameters were never added to the JSON schema 'required' array, making the schema tell the LLM the param is optional while the runtime throws on missing values. Fix: use only NullableAnnotation.Annotated to determine nullability, matching C# nullable reference type semantics. Add two new schema tests verifying required array contents for both non-nullable reference types and value types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…and conversions - Add EnumParamService and CollectionParamService with tool contexts - Add 9 schema tests (enum, collection, optional, no-params, void return, multi-param, DI exclusion, CancellationToken exclusion) - Add 10 conversion edge case tests (enum, list, dict, DTO, JsonNode, nullable) - Add EnumParameter and CollectionParameter generator inputs - Add 4 generator compilation tests for enum/collection scenarios - Add enum/collection to AllValidInputs_CompileCleanly theory Test count: 110 runtime (was 93) + 122 generator (was 116) = 232 total Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Split the 1268-line AIToolContextGenerator.cs into focused files: - Models.cs (88 lines) — pipeline value types (enums + records) - Diagnostics.cs (74 lines) — diagnostic descriptors and factories - SymbolAnalysis.cs (682 lines) — Roslyn symbol extraction + constants - CodeEmitter.cs (312 lines) — source code generation + string helpers - AIToolContextGenerator.cs (137 lines) — slim entry point with Initialize() Pure structural refactor — generated output is byte-identical. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Gate user secrets embedding on Debug configuration only - Dispose secrets.json stream with 'using' in MauiProgram.cs - Uncomment NSPrivacyAccessedAPICategoryUserDefaults in privacy manifest - Add pull_request types [opened, synchronize, reopened, edited] to ci-ai.yml - Fix DIParameters README garbled sentence - Use explicit SDK version in AzDO AI build job (not useGlobalJson) - Pass actual argument name in AIToolMetadataServices exception - Add clarifying comment on README Remove path in csproj Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the fragile None Remove path — PackRepoRootReadme=false already prevents Arcade from including the repo-root README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update CI workflow name, AzDO pipeline display names, README headers, and sample descriptions to use 'AI Extensions' — reflecting the broader product area that will include AI Navigation alongside AI Attributes. Package/assembly names (Microsoft.Maui.AI.Attributes) unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make src/AIExtensions/README.md a concise landing page with a Packages table that can grow as new packages join (e.g. AI.Navigation). Move the code sample and detailed docs to the per-package NuGet README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t README Restructure so future packages (e.g. AI Navigation) can be added as sibling sub-headings under the AI Extensions section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Skip Xcode version validation repo-wide in Directory.Build.props. Add warning comment on the embedded user-secrets target explaining this is only acceptable for local-only developer samples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ate prompts - Remove unused Title/Icon on ShellContent (tab bar is hidden) - Add descriptive comments on routes in AppShell.xaml and .cs - Document each AIToolSource pattern in GardenShopTools XML doc - Replace generic suggestion prompts with more demonstrative ones Cherry-picked from PR #134 — sample improvements independent of navigation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace ▼/▶ (U+25BC/25B6) expand/collapse toggle and › (U+203A) list chevron with FluentIcons.ChevronDown/ChevronRight glyphs. Unicode geometric symbols may not render on iOS with custom fonts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…appings The previous codepoints (U+E76C, U+E70D) were Segoe Fluent Icons values, not FluentSystemIcons-Filled. In this font: - U+E76C = leaf_three (not chevron_right) - U+E70D = heart_circle (not chevron_down) Correct codepoints from the bundled font: - U+F2B0 = ic_fluent_chevron_right_20_filled - U+F2A3 = ic_fluent_chevron_down_20_filled Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add Android handler mapping to set BackgroundTintList to transparent, matching the existing iOS/MacCatalyst border removal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add project structure for the compile-time XAML → Markdown UI indexer: - Microsoft.Maui.AI.Indexer.Generators (netstandard2.0 source generator) - Microsoft.Maui.AI.Indexer (net10.0 runtime types) - Microsoft.Maui.AI.Indexer.Tests (xUnit + Verify) - Updated AIExtensions.slnf and MauiLabs.slnx Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Complete implementation of the compile-time XAML → Markdown UI indexer:
Source Generator (Microsoft.Maui.AI.Indexer.Generators):
- XamlIndexerGenerator: IIncrementalGenerator reading XAML via AdditionalTexts
- XamlFileParser: Filters to a11y-relevant elements, skips layout containers
- MarkupExtensionParser: Parses {Binding}, {StaticResource}, etc.
- AccessibilityExtractor: Extracts SemanticProperties (Description, Hint, HeadingLevel)
- ConditionalDetector: Detects IsVisible bindings and DataTrigger conditions
- ShellParser: Extracts Shell routes and tab structure
- PageCodeEmitter: Generates per-page .g.cs with const string Markdown
- AggregateCodeEmitter: Generates UiIndex.g.cs with Search/FindByRoute/FindByName
- MarkdownBuilder: Renders semantic tree with inlined templates and conditions
Runtime Library (Microsoft.Maui.AI.Indexer):
- UiPageIndexAttribute and UiProjectIndexAttribute
- Build targets to wire MauiXaml as AdditionalFiles
- NuGet README
Garden Sample Integration:
- Wired indexer into AIExtensions.Sample.Garden.csproj
- Generates 14 page indexes + aggregate with search
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add Description, Hint, and HeadingLevel semantic properties throughout the Garden sample app. This improves screen reader accessibility and provides rich test data for the XAML UI indexer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expert Code Review — PR #269Methodology: 3 independent reviewers with adversarial consensus. Due to the large diff (253 files, 20,980 additions), files were split into 3 batches (infra+source / samples / tests) with each reviewer analyzing one batch. All findings are annotated "low confidence — single reviewer (batch split)" with severity downgraded by one level per protocol. 11 findings posted as inline comments (2 moderate, 9 minor) Overflow FindingsThe following findings could not be posted inline or were lower priority:
CI Status
Test Coverage AssessmentThe PR includes 16 XAML indexer tests and extensive attribute generator tests (snapshot + runtime). Coverage for the source generator core logic is good. Gaps: no test for the
|
There was a problem hiding this comment.
Expert Code Review: 11 findings posted inline (2 moderate, 9 minor). See the lean summary comment for full details and methodology.
Generated by Expert Code Review (auto) for issue #269 · ● 37.7M
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <IsPackable>true</IsPackable> | ||
| <IsShipping>true</IsShipping> | ||
| <PackageId>Microsoft.Maui.AI.Indexer</PackageId> | ||
| <Description>Compile-time XAML UI indexer for .NET MAUI — generates AI-friendly semantic Markdown from XAML pages</Description> | ||
| <RootNamespace>Microsoft.Maui.AI.Indexer</RootNamespace> | ||
| <PackRepoRootReadme>false</PackRepoRootReadme> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <None Include="README.md" Pack="true" PackagePath="/" /> | ||
| <None Include="build\**" Pack="true" PackagePath="build\" /> | ||
| <None Include="build\**" Pack="true" PackagePath="buildTransitive\" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
There was a problem hiding this comment.
🟡 MODERATE · low confidence — single reviewer (batch split)
NuGet package does not include the source generator DLL. Unlike Microsoft.Maui.AI.Attributes.csproj (which has a ProjectReference to its generators project with OutputItemType="Analyzer" and a target to pack it into analyzers/dotnet/cs), this csproj has no such reference.
Scenario: A consumer installs Microsoft.Maui.AI.Indexer from NuGet. The build targets add XAML files as AdditionalTexts, but no source generator processes them — nothing is generated, no compiler error, total silent failure.
Recommendation: Add a ProjectReference + pack target mirroring Microsoft.Maui.AI.Attributes.csproj:
<ProjectReference Include="..\Microsoft.Maui.AI.Indexer.Generators\Microsoft.Maui.AI.Indexer.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />Plus the _ResolveGeneratorPath target to pack the generator into analyzers/dotnet/cs.
There was a problem hiding this comment.
Good catch — this is intentional for now since the package isn't being published yet (it's wired via ProjectReference in the Garden sample). When we package for NuGet, we'll add the ProjectReference with OutputItemType="Analyzer" and the pack target, mirroring the AI Attributes pattern. Tracked for the packaging milestone.
| private static string FormatConstantKey(TypedConstant c) | ||
| { | ||
| if (c.Value is null) return "null"; | ||
| if (c.Value is string s) return "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
Incomplete string escaping. This manual Replace handles \ and " but not \n, \r, \t, null, or other control characters. A keyed service key containing a newline would produce a syntax error in the generated code.
The codebase already has CodeEmitter.Escape (line 302) using SymbolDisplay.FormatLiteral which handles all edge cases correctly.
Recommendation: Replace with SymbolDisplay.FormatLiteral(s, quote: true) to match the existing pattern.
There was a problem hiding this comment.
This file is from the base branch (mattleibow/ai-annotations) and isn't modified by this PR. I'll track this for a separate fix in the AI Attributes PR.
| if (result.CallId is not null && toolCallMessages.TryGetValue(result.CallId, out var toolMsg)) | ||
| { | ||
| toolMsg.ToolResult = resultText; | ||
| OnPropertyChanged(nameof(toolMsg.HasDetails)); |
There was a problem hiding this comment.
🟡 MODERATE · low confidence — single reviewer (batch split)
OnPropertyChanged fires on this (ChatViewModel), not on toolMsg. ObservableObject.OnPropertyChanged is a protected instance method — calling it without a receiver fires on the current instance. ChatViewModel has no HasDetails property, so this notification is entirely useless. Meanwhile, ChatMessageViewModel.HasDetails is a computed property with no [NotifyPropertyChangedFor] on ToolArgs or ToolResult, so the tool-detail expand button stays hidden after results arrive.
Recommendation: Remove this line and add [NotifyPropertyChangedFor(nameof(HasDetails))] to both ToolArgs and ToolResult in ChatMessageViewModel.
There was a problem hiding this comment.
This file is from the base branch (mattleibow/ai-annotations) — the Garden sample's ChatViewModel isn't modified by the indexer PR. Good find though, I'll fix it in a follow-up to the Garden sample.
| { | ||
| private readonly List<Order> _orders = []; | ||
|
|
||
| public IReadOnlyList<Order> Orders => _orders; |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
IReadOnlyList<Order> backed by the mutable List<Order>. External callers can cast to List<Order> and mutate the internal collection, bypassing class invariants. This is a poor pattern to demonstrate in a sample that developers will copy.
Recommendation: Return a defensive copy: public IReadOnlyList<Order> Orders => [.. _orders]; (same pattern already used in CurrentCart.Items).
There was a problem hiding this comment.
This file is from the base branch (mattleibow/ai-annotations). Agreed on the defensive copy pattern — I'll address in a separate Garden sample cleanup.
| public void Schema_void_return_has_null_return_schema() | ||
| { | ||
| var tool = Assert.IsAssignableFrom<AIFunction>( | ||
| StaticMathToolContext.Default.Tools.First(t => t.Name == "negate_number")); | ||
| // negate returns int, should have return schema | ||
| Assert.NotNull(tool.ReturnJsonSchema); |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
Test name/implementation mismatch. Schema_void_return_has_null_return_schema tests negate_number (which returns int) and asserts NotNull — so it's not actually testing the void-return case at all. A regression in void-return schema generation would go undetected.
Recommendation: Add a test that uses an actual void-returning exported function and asserts ReturnJsonSchema is null (or whatever the expected behavior is for void).
There was a problem hiding this comment.
This test file is from the base branch. I'll fix the test name and add proper void-return coverage in a follow-up to the AI Attributes tests.
| private static string EscapeForRawStringLiteral(string text) | ||
| { | ||
| // Raw string literals (""") don't need escaping except for the closing sequence | ||
| // If the text contains """, we'd need more quotes, but this is extremely unlikely | ||
| return text; |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
EscapeForRawStringLiteral is a no-op — returns input unchanged. If any XAML page content contains """, the generated raw string literal will be malformed C# that fails compilation with a cryptic error.
Recommendation: Implement the method: detect if text contains """ and use longer delimiters ("""" ... """"), or switch to a regular escaped string literal which handles all content safely.
There was a problem hiding this comment.
Fixed ✅ — EscapeForRawStringLiteral now detects if the text contains \"\"\" and dynamically extends the delimiter (\"\"\"\" ... \"\"\"\"). Returns a (text, delimiter) tuple. Added a test with triple-quotes in SemanticProperties.Description to verify.
| public static DiagnosticInfo IncludeAndExcludeBothSet(string typeName, Location? location) => | ||
| new( | ||
| "MAUIAI005", | ||
| DiagnosticSeverity.Error, | ||
| $"[AIToolSource(typeof({typeName}))] sets both IncludeTools and ExcludeTools. Use only one.", |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
MAUIAI005 diagnostic is defined but never emitted. IncludeAndExcludeBothSet is never called from SymbolAnalysis.cs — when both IncludeTools and ExcludeTools are set, the generator silently applies both filters sequentially instead of reporting an error.
Recommendation: Add a check in SymbolAnalysis.GetContextModel before the filter logic:
if (includeTools is { Length: > 0 } && excludeTools is { Length: > 0 })
{
diagnostics.Add(DiagnosticInfo.IncludeAndExcludeBothSet(typeName, location));
continue;
}There was a problem hiding this comment.
This file is from the base branch (mattleibow/ai-annotations) — not modified by the indexer PR. I'll add the check in a follow-up to the AI Attributes generator.
| var options = new ChatOptions { Tools = [.. GardenShopTools.Default.Tools] }; | ||
| await SendAndProcessResponseAsync(options); | ||
| } | ||
| catch (Exception ex) |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
OperationCanceledException is caught as an error. When the user starts a new session, _cts.Cancel() triggers OperationCanceledException which is caught here and surfaced as "Error: The operation was canceled." in the new session's message list. Cancellation is a first-class signal, not an error.
Recommendation: Add a preceding catch:
catch (OperationCanceledException) { /* user-initiated cancellation — not an error */ }
catch (Exception ex) { AddMessage(ChatMessageKind.Error, $"Error: {ex.Message}"); }There was a problem hiding this comment.
This file is from the base branch (mattleibow/ai-annotations). Good catch — cancellation should not surface as an error message. I'll fix in a separate Garden sample PR.
| "Grid", "StackLayout", "VerticalStackLayout", "HorizontalStackLayout", | ||
| "FlexLayout", "AbsoluteLayout", "ScrollView", | ||
| "Border", "Frame", "BoxView", "ContentView", "ContentPresenter", | ||
| "Shadow", "RefreshView", "SwipeView", |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
"Shadow" appears in both StructuralElements (here) and IgnoredElements (line 44). Since StructuralElements is checked first (line 134), Shadow is treated as a structural container whose children are walked — but Shadow is a visual effect with no meaningful children in MAUI. The entry in IgnoredElements is dead code.
Recommendation: Remove "Shadow" from StructuralElements and keep it only in IgnoredElements.
There was a problem hiding this comment.
Fixed ✅ — removed Shadow from StructuralElements. It remains only in IgnoredElements where it belongs.
| { | ||
| doc = XDocument.Parse(content!); | ||
| } | ||
| catch |
There was a problem hiding this comment.
🟢 MINOR · low confidence — single reviewer (batch split)
Bare catch swallows all exceptions including critical ones like OutOfMemoryException. In a source generator running inside the compiler process, this could mask serious issues.
Recommendation: Narrow to catch (System.Xml.XmlException) or at minimum catch (Exception) (which doesn't catch SEH exceptions).
There was a problem hiding this comment.
Fixed ✅ — narrowed to catch (System.Xml.XmlException). Other exceptions (OOM, etc.) will now propagate correctly.
Add comprehensive test suites for full coverage: - MarkupExtensionParserTests: bindings, converters, StringFormat, RelativeSource - ConditionalDetectorTests: IsVisible bindings, inverse converters, DataTriggers - ShellParserTests: TabBar, nested Tabs, FlyoutItems, route fallbacks - ControlRenderingTests: Switch, CheckBox, Picker, DatePicker, TimePicker, Image, ActivityIndicator, ProgressBar, RadioButton, SearchBar, Stepper, ImageButton, Editor, promoted structural elements, empty/invalid XAML - AggregateIndexTests: Search, FindByRoute, FindByName, FQN references - EdgeCaseTests: deep nesting, ResourceDictionary skip, custom controls, conditional BindableLayout, heading levels, namespace edge cases Fix ImageButton source rendering in MarkdownBuilder. Generator coverage: 90.2% line rate (77 tests, all passing) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes: - NU5129: Fix NuGet pack paths for build/buildTransitive targets (double-slash in buildTransitive path on macOS) - Remove RegexOptions.Compiled from source generator (anti-pattern) - Implement proper raw string literal delimiter escaping for """ - Remove duplicate 'Shadow' from StructuralElements (dead code) - Narrow bare catch to System.Xml.XmlException - Add explicit parentheses in AccessibilityExtractor conditions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cross-file user control resolution: - New CrossFileResolver that builds x:Class→PageModel lookup, resolves <views:MyControl/> references by inlining their semantic content - Caches resolved controls so reused components (e.g., CartView in both CartPage and CartPane) are parsed once - Deep clones cached elements to prevent shared mutation between pages - Handles nested resolution (Outer→Inner) with self-reference protection - Renders as '- [ControlName]:' with indented children Test rewrite to exact string matching: - 58 tests using Assert.Equal with exact expected strings - No Contains/partial matching — catches whitespace, namespace, semicolons - ExactOutputTests: 31 tests covering all controls, templates, conditions, cross-file resolution, Shell routes, error resilience, generated structure - AdditionalExactTests: 27 tests for remaining controls, edge cases, aggregate index, converter detection, promoted elements Bug fixes: - ActivityIndicator missing ': ' separator in markdown output - User control detection moved before structural element fallback (was being swallowed by the unknown-element walker) Generator coverage: 89.1% (58 exact-match tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
28 new exact-match tests covering: - Emoji in labels, buttons, placeholders, headings, hints - Decorative emoji skipped with empty Description - Emoji override via SemanticProperties.Description - Emoji in CollectionView templates and cross-file user controls - CJK (Japanese) and RTL (Arabic) unicode characters - XML special characters (& < ") - Empty elements (no text Label, no text Button, default Slider) - Nested layouts with multiple semantic children - CollectionView without ItemsSource - Dotted binding paths (User.Profile.Name) - Empty page with no content - ResourceDictionary skipping - Multiple conditions on same page - HeadingLevel edge cases (Level9, None) - Always-hidden elements (IsVisible=False) Total: 86 exact-match tests, all passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Critical/High fixes: - #1: Use namespace+class for unique hint names (prevents AddSource crash when two pages share the same simple class name) - #2: CrossFileResolver uses FQN lookup + ambiguity detection for duplicate simple names across namespaces - #3: CollectionView conditional rendering fixed — no more [[double brackets]], uses unified annotation list builder - #4: Root ContentPage walks children directly, preventing SemanticProperties on root from swallowing the entire page - #5: Visibility conditions on layout containers now propagate as condition group wrappers ('When [visible when X = true]:') - #6: Property-element content (ContentPage.Content, ScrollView.Content) no longer dropped — unknown property elements are transparent by default, only known non-visual ones (Resources, Triggers, etc.) are suppressed - #7: Shell routes stored in UiElement for Shell page markdown Medium fixes: - #8: Promoted containers (Border with Description) now walk children too, preserving actionable descendants like buttons - #9: Unresolved user controls kept as placeholders (previously dropped), important for third-party controls with SemanticProperties - #10: DataTrigger with IsVisible=False setter now correctly inverted to 'hidden when Property = Value' instead of 'visible when' - #11: IsVisible=False elements skipped entirely — not reachable by screen readers, should not appear in accessibility-first index - #12: Aggregate namespace validated as legal C# before emitting - #13: Always use global:: for page references in aggregate, even for no-namespace pages - #14: BindingRegex now requires whitespace after 'Binding' keyword, preventing false matches like {BindingSource} - #15: CrossFileResolver uses in-progress set for cycle detection, preventing partial cache on indirect A→B→A cycles Low fixes: - #16: Dead emptyViewChildren code block removed - #17: Removed unused TemplateVariants from dead CollectionView code 104 exact-match tests, all passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The generator DLL is now packed into analyzers/dotnet/cs/ inside the Microsoft.Maui.AI.Indexer NuGet package. Consumers only need: <PackageReference Include="Microsoft.Maui.AI.Indexer" /> No separate Generators package reference required. Matches the pattern used by Microsoft.Maui.AI.Attributes. Changes: - Add ProjectReference to Generators with OutputItemType=Analyzer - Add _ResolveGeneratorPath target to pack generator into analyzers/ - Remove separate Generators reference from Garden sample csproj - Update README quick start to show single package reference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/evaluate |
Skill Evaluation Results20260512-183712No markdown report |
New tools that let the AI agent search and understand the app's UI: - search_ui(searchTerms): Search across all pages for content matching one or more terms. Returns matching page names with relevant snippets. Example: search for ['cart', 'checkout'] to find shopping pages. - get_page_ui(pageName): Get the full semantic UI description of a specific page. Returns the complete accessibility tree with all controls, bindings, commands, and conditions. - list_app_pages(): List all pages and views with their routes. Gives an overview of the app's structure. These tools enable AI agents to answer questions like: 'Which page has the list of products?' 'Where do I go to checkout?' 'What controls does the product detail page have?' Implementation: - UiIndexRegistry (runtime lib): Reflection-based discovery of all [UiPageIndex] types. Works across generator boundaries — doesn't depend on the generated UiIndex class directly. - UiDiscovery (Garden sample): Three [ExportAIFunction] tools wired into the ChatViewModel's GardenShopTools context. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root cause: The Indexer source generator was bundled into the NuGet package (analyzers/dotnet/cs/) for package consumers, but ProjectReference chains don't flow OutputItemType=Analyzer transitively. The Garden sample needed a direct ProjectReference to the generator project. Fixes: - Re-add direct ProjectReference to Indexer.Generators in Garden csproj (needed for ProjectReference-based development; NuGet consumers get the generator from the analyzers/ folder in the package automatically) - Add UiIndexRegistry.Register() for trim-safe explicit registration - Generate [ModuleInitializer] AutoRegister() in UiIndex.g.cs that auto-registers all pages on assembly load — no user startup code needed - UiIndexRegistry.Instance singleton pattern for UiDiscovery tools Verified with ILSpy: all 14 *_UiIndex types + UiIndex + AutoRegister are present in the compiled assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace UiIndexRegistry/module initializer with a clean base class pattern
matching AIToolContext:
Runtime types (Microsoft.Maui.AI.Indexer):
- UiPageIndex: abstract base class with Pages property and FindByName/
FindByRoute helpers (like AIToolContext has Tools)
- UiPageEntry: immutable page record (name, route, filePath, markdown)
- Removed: UiIndexRegistry, UiProjectIndexAttribute, module initializer
- Zero reflection — all types referenced directly by generated code
Generated code:
- {AssemblyName}UiIndex : UiPageIndex with Default singleton
- Pages override returns static array referencing per-page Markdown consts
- Per-page classes are plain static classes (no attributes needed)
Usage:
var index = AIExtensions_Sample_GardenUiIndex.Default;
var page = index.FindByName("ProductDetailPage");
Console.WriteLine(page?.Markdown);
Garden sample UiDiscovery updated to use the new pattern directly.
No startup code, no registration, no reflection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
XAML → Markdown UI Indexer
A compile-time source generator that creates AI-friendly semantic representations of every XAML page. The indexer outputs what a screen reader would announce — not layout structure — making the entire UI discoverable by AI agents without running the app.
New Projects
Microsoft.Maui.AI.IndexerUiPageIndexbase class,UiPageEntry) + build targetsMicrosoft.Maui.AI.Indexer.GeneratorsMicrosoft.Maui.AI.Indexer.TestsHow to Use
1. Add the package (one reference — the generator is bundled):
2. Build your project. The generator emits:
{Page}_UiIndex.g.csper XAML page (static class withconst string Markdown){AssemblyName}UiIndex.g.csaggregate (inheritsUiPageIndex, hasDefaultsingleton)3. Access at runtime — no reflection, no registration, no startup code:
4. Wire as AI tools (see Garden sample
UiDiscovery.cs):5. Multi-assembly support — each assembly gets its own generated index. The consumer collects them:
What the Generator Produces
Actual generated output for MainPage (with cross-file resolution inlining ChatView → CartPane → CartView):
Design Principles
AIToolContext/JsonSerializerContextpattern{Name}UiIndex : UiPageIndexwithDefaultsingleton. Multi-assembly apps compose themFeatures
<views:CartView/>→ inlined with caching for reuseEach item:children[visible when X = true], inverse converters, layout condition groups[visible when X = True]/[hidden when X = True]IsVisible="False"elements omitted entirelyContentPage.Content,ScrollView.Contenttraversed correctlyAddSourcecollisionsGarden Sample Changes
SemanticProperties(Description, Hint, HeadingLevel) across all XAML pages and viewsUiDiscoveryservice with three[ExportAIFunction]tools:search_ui(searchTerms)— multi-term search across all pages with relevant snippetsget_page_ui(pageName)— full semantic markdown for a pagelist_app_pages()— list all pages with routesTest Coverage
104 exact-match tests using
Assert.Equal— noContainsor partial matching.Future Roadmap
x:DataTypemaui_ui_search)Base branch:
mattleibow/ai-annotations