Skip to content

XAML → Markdown UI Indexer (accessibility-first, source generator)#269

Draft
mattleibow wants to merge 36 commits into
mainfrom
mattleibow/xaml-markdown-indexer
Draft

XAML → Markdown UI Indexer (accessibility-first, source generator)#269
mattleibow wants to merge 36 commits into
mainfrom
mattleibow/xaml-markdown-indexer

Conversation

@mattleibow
Copy link
Copy Markdown
Member

@mattleibow mattleibow commented May 11, 2026

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

Project Description
Microsoft.Maui.AI.Indexer Runtime types (UiPageIndex base class, UiPageEntry) + build targets
Microsoft.Maui.AI.Indexer.Generators Source generator (bundled in the NuGet package)
Microsoft.Maui.AI.Indexer.Tests 104 exact-match tests

How to Use

1. Add the package (one reference — the generator is bundled):

<PackageReference Include="Microsoft.Maui.AI.Indexer" />

2. Build your project. The generator emits:

  • One {Page}_UiIndex.g.cs per XAML page (static class with const string Markdown)
  • One {AssemblyName}UiIndex.g.cs aggregate (inherits UiPageIndex, has Default singleton)

3. Access at runtime — no reflection, no registration, no startup code:

// The generated class follows the AIToolContext pattern:
// {SanitizedAssemblyName}UiIndex : UiPageIndex
var index = MyAppUiIndex.Default;

// Browse all pages
foreach (var page in index.Pages)
    Console.WriteLine($"{page.Name}: {page.Markdown}");

// Find a specific page
var detail = index.FindByName("ProductDetailPage");
Console.WriteLine(detail?.Markdown);

// Find by Shell route
var orders = index.FindByRoute("orders");

// Access a single page's markdown directly
Console.WriteLine(ProductDetailPage_UiIndex.Markdown);

4. Wire as AI tools (see Garden sample UiDiscovery.cs):

public sealed class UiDiscovery
{
    // Reference the generated index directly — no reflection
    private static UiPageIndex Index => MyAppUiIndex.Default;

    [ExportAIFunction("search_ui")]
    [Description("Search the app's UI pages for controls, labels, or features.")]
    public static string SearchUi(string[] searchTerms)
    {
        foreach (var page in Index.Pages)
            // match terms against page.Markdown ...
    }

    [ExportAIFunction("get_page_ui")]
    [Description("Get the full semantic UI description of a specific page.")]
    public static string GetPageUi(string pageName)
    {
        return Index.FindByName(pageName)?.Markdown ?? "Page not found";
    }
}

5. Multi-assembly support — each assembly gets its own generated index. The consumer collects them:

var allPages = MyAppUiIndex.Default.Pages
    .Concat(MyLibraryUiIndex.Default.Pages)
    .ToList();

What the Generator Produces

Actual generated output for MainPage (with cross-file resolution inlining ChatView → CartPane → CartView):

# MainPage
File: Pages/MainPage.xaml

- Label: "Sage app icon"
- Heading (level 1): "Sage"
- Button: "Products" [hint: Opens the product catalog]
- Button: "Orders" [hint: Opens your past orders]
- Button: "New chat" → StartNewSessionCommand [hint: Starts a new conversation]
- [ChatView]:
  - CollectionView: "{Messages}"
    - Empty view:
      - Heading (level 1): "Welcome to Sage, your personal garden shopper"
      - FlexLayout with items from "{AvailableTools}":
        - Each item:
          - Label: "{Name}"
          - Label: "{Description}"
  - When [visible when IsInputVisible = true]:
    - Entry: "{InputText}"
    - Button: "Send" → SendCommand [hint: Sends your message]
  - When [visible when IsApprovalPending = true]:
    - Button: "Approve" → ApproveCommand [hint: Approves the pending action]
    - Button: "Reject" → RejectCommand [hint: Rejects the pending action]
- [CartPane]:
  - Heading (level 1): "Cart"
  - [CartView]:
    - CollectionView: "{Items}" [visible when IsNormalMode = true]
      - Each item:
        - Label: "{Name}"
        - Label: "{QuantityLine}"
    - When [visible when HasItems = true]:
      - Button: "{CartTotal}" → CheckoutCommand [hint: Proceeds to checkout]

Design Principles

Principle Details
Accessibility-first Skips Grid, StackLayout, Border, ScrollView — only semantic content
No reflection Generated code references types directly. Follows the AIToolContext / JsonSerializerContext pattern
App owns search The generator produces markdown. Search strategy (simple string match, RAG, vector DB) is up to the consumer
Per-assembly Each assembly gets its own {Name}UiIndex : UiPageIndex with Default singleton. Multi-assembly apps compose them
Trim-safe No reflection in any path. Pure const strings and static arrays

Features

Feature Details
SemanticProperties extraction Description overrides text, Hint appended, HeadingLevel → heading depth
Cross-file user control resolution <views:CartView/> → inlined with caching for reuse
Nested resolution Page → Outer → Inner chains resolve recursively with cycle detection
CollectionView templates Header, GroupHeader, Items, GroupFooter, Footer, EmptyView — all inlined
BindableLayout templates Parent as container with Each item: children
Conditional visibility [visible when X = true], inverse converters, layout condition groups
DataTrigger conditions [visible when X = True] / [hidden when X = True]
Always-hidden skip IsVisible="False" elements omitted entirely
Promoted containers Layout with Description renders with children preserved
Property-element support ContentPage.Content, ScrollView.Content traversed correctly
Shell routes TabBar, Tab, FlyoutItem, ShellContent with routes
Duplicate name safety Namespace-qualified hint names prevent AddSource collisions

Garden Sample Changes

  • Added SemanticProperties (Description, Hint, HeadingLevel) across all XAML pages and views
  • Added UiDiscovery service with three [ExportAIFunction] tools:
    • search_ui(searchTerms) — multi-term search across all pages with relevant snippets
    • get_page_ui(pageName) — full semantic markdown for a page
    • list_app_pages() — list all pages with routes

Test Coverage

104 exact-match tests using Assert.Equal — no Contains or partial matching.

Suite Tests Covers
ExactOutputTests 31 All controls, templates, conditions, cross-file, Shell, errors
AdditionalExactTests 27 Remaining controls, aggregate, converters, edge cases
EmojiAndUnicodeTests 28 Emoji, CJK, RTL, XML entities, empty elements
ReviewFindingTests 18 Duplicate names, layout conditions, property elements, cycles

Future Roadmap

Feature Impact
ViewModel discovery via x:DataType Links UI to data layer
DataTemplateSelector variant detection All template options per list
RAG embedding generation (sample) Vector search over UI
MCP tool integration (maui_ui_search) AI agents query via DevFlow
Missing a11y diagnostics Warn on elements without SemanticProperties

Base branch: mattleibow/ai-annotations

mattleibow and others added 27 commits May 11, 2026 18:11
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>
Copilot AI review requested due to automatic review settings May 11, 2026 23:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@github-actions
Copy link
Copy Markdown
Contributor

Expert Code Review — PR #269

Methodology: 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 Findings

The following findings could not be posted inline or were lower priority:

# Severity File Line(s) Finding
12 🟢 Minor samples/.../MainViewModel.cs 57–70 CancellationToken not propagated to AI tool methods — navigation side effects can fire after session teardown
13 🟢 Minor samples/.../CartViewModel.cs 62–73 SyncCollection always creates new VMs and replaces items even when already in correct position, defeating in-place diffing
14 🟢 Minor samples/.../PreferencesOrderArchive.cs 21–26 Read-increment-write on Preferences counter is not atomic; concurrent AI tool calls can produce duplicate order IDs
15 🟢 Minor samples/.../OrdersViewModel.cs 12 Transient VM registers with WeakReferenceMessenger without paired unregister; each navigation creates a new listener
16 🟢 Minor tests/.../XamlFileParserTests.cs 308–311 Parse_SkipsXamlWithoutXClass uses XAML with x:Key but missing xmlns:x declaration — test passes because XML parsing fails, not because missing x:Class is handled
17 🟢 Minor tests/.../XamlFileParserTests.cs 316–319 Test only asserts one specific file is absent; doesn't verify UiIndex.g.cs aggregate exists or is correct
18 🟢 Minor tests/.../GeneratorCompilationTests.cs 39–62 GetGeneratedSource silently falls back to SyntaxTrees.Last() when context class not found, masking missing output
19 🟢 Minor eng/Versions.props 12–19 Side-effect version bumps: Microsoft.Extensions.AI 10.3.0→10.4.1, OpenAI 2.6.0→2.9.1, CommunityToolkit.Mvvm 8.4.0→8.4.2 affect existing EssentialsAI product
20 🟢 Minor src/.../AccessibilityExtractor.cs 27, 31, 35 Operator precedence in semantic property matching lacks explicit parentheses — correct behavior but fragile for refactoring
21 🟢 Minor src/.../MarkdownBuilder.cs 120 Complex inline string formatting with .TrimStart/.Replace chains has edge cases with bracket formatting

CI Status

  • license/cla: passed
  • build / build (macos-latest) — CI-AI: passed
  • build / build (windows-latest) — CI-AI: passed
  • build / build (ubuntu-24.04) — Linux GTK4: passed
  • build / build (macos-latest) — one workflow (not CI-AI) failed
  • 🔄 build-macos / build (macos-latest) — in progress

Test Coverage Assessment

The 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 """ raw string edge case in PageCodeEmitter, no test for IncludeTools + ExcludeTools both set, void-return schema test is misnamed/mislabeled.

Generated by Expert Code Review · 3 independent reviewers with adversarial consensus

Generated by Expert Code Review (auto) for issue #269 · ● 37.7M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +19
<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>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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("\"", "\\\"") + "\"";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +143 to +148
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +66 to +70
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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +61 to +65
public static DiagnosticInfo IncludeAndExcludeBothSet(string typeName, Location? location) =>
new(
"MAUIAI005",
DiagnosticSeverity.Error,
$"[AIToolSource(typeof({typeName}))] sets both IncludeTools and ExcludeTools. Use only one.",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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}"); }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed ✅ — removed Shadow from StructuralElements. It remains only in IgnoredElements where it belongs.

{
doc = XDocument.Parse(content!);
}
catch
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed ✅ — narrowed to catch (System.Xml.XmlException). Other exceptions (OOM, etc.) will now propagate correctly.

@mattleibow mattleibow changed the base branch from main to mattleibow/ai-annotations May 12, 2026 04:45
Copy link
Copy Markdown
Member

@jfversluis jfversluis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build errors

mattleibow and others added 6 commits May 12, 2026 18:36
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 (&amp; &lt; &quot;)
- 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>
@mattleibow
Copy link
Copy Markdown
Member Author

/evaluate

@github-actions
Copy link
Copy Markdown
Contributor

Skill Evaluation Results

20260512-183712

No markdown report

Full results

@mattleibow mattleibow changed the base branch from mattleibow/ai-annotations to main May 12, 2026 19:11
mattleibow and others added 3 commits May 12, 2026 21:48
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>
@mattleibow mattleibow marked this pull request as draft May 13, 2026 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants