Mobile apps · .NET MAUI

Docs › Mobile apps

.NET MAUI

iOS, Android and Mac Catalyst on .NET 10 and later.

Figma-style comments for a running .NET MAUI app. Tap an element, write a note, and it reaches your coding agent (Claude Code, Codex, Cursor, Gemini CLI, Copilot and others) over MCP with a screenshot, the XAML file and line the element was written at, its page, its view model, and the app's recent log. It is the MAUI client of the same Notato server every other Notato SDK uses: the notes, the board, the MCP tools and the status loop (open, acknowledged, resolved, revert) are the same.

Works on iOS, Android and Mac Catalyst with .NET 10 and later. Windows builds, but has no overlay yet.

Quick start#

npx notato dev           # the server your agent reads, on http://localhost:4747

Add the package to the app (dotnet add package Notato.Maui), and in MauiProgram.cs:

using Notato.Maui;

var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
#if DEBUG
builder.UseNotato(options => options.Project = "checkout-app");
#endif

Leaving UseNotato out of a build ships nothing of Notato in it. Run the app: a small dark toolbar appears in the corner. Tap Annotate, tap what you want to comment on, write a note, press Send. The number on Annotate is how many notes the screen has; the chevron folds the toolbar into a round button, and ⋯ opens Notato's menu (notes, settings, packaging in test mode, hiding the toolbar), whose header shows the server and whether it is connected. Ask your agent to "watch Notato and fix what comes in".

The app has to be allowed to talk plain http to the development machine:

The iOS simulator shares the Mac's network, so localhost works there as it is.

On a phone#

A phone cannot reach your machine's localhost (and iOS has no adb reverse). Start the server with a dev tunnel, notato dev --tunnel (see Phones: a dev tunnel; one devtunnel user login first), and there is nothing else to do: it writes the tunnel's https URL and a device token to .notato/device.json at the repository root, and this package's build targets record them in debug builds. When Server is not set, an iPhone, an Android phone and the Android emulator then use the tunnel, sending the device token, and the iOS simulator and Mac Catalyst the server's local address (its port included). Build again after the tunnel's URL changes; Visual Studio's up-to-date check knows the file.

Configuration#

UseNotato() binds the Notato section of builder.Configuration when there is one, then applies the delegate. Any configuration source works; the sample embeds an appsettings.json and adds environment variables over it:

{
  "Notato": {
    "Project": "checkout-app",
    "Mode": "Dev",
    "Server": "http://localhost:4747"
  }
}
builder.Configuration.AddJsonStream(appsettingsStream);
builder.UseNotato();                                                   // the "Notato" section
builder.UseNotato(builder.Configuration.GetSection("Feedback"));       // or another section
builder.UseNotato(o => o.AppVersion = "2.1-beta");                     // the section, then code
OptionDefault
Project(required)Project id on the server. Letters, digits and . _ - @
ModeDevDev, Test or Agent (see below)
Serverhttp://localhost:4747 (none in test mode)An empty string means no server
TokenA project token (notato_…) for a shared notato serve. Only ever sent to the server configured here, never to one typed into the settings
EnabledtrueWhether Notato is on at start-up. The app can switch it at runtime
ShowToolbar, ToolbarPosition, ToolbarCollapsedtrue, BottomRight, falseThe floating toolbar. People can drag it anywhere and fold it into one round button (it opens by itself while annotating); where they leave it, and how, is remembered
ShakeToToggletrueShaking the device shows or hides the toolbar (real devices). It shares the accelerometer if the app reads it too, and only stops it if it started it
AuthorThe name on this person's notes. They can change it in the toolbar's settings
ScreenshotstrueA server that has screenshots off wins either way
MaskInputson in test and agent modeCover inputs (Entry, Editor, SearchBar) in screenshots and leave what is typed out of notes. Password entries always are
RememberRuntimeStatetrueKeep runtime choices (on or off, toolbar, name, a server typed in) across launches
XamlSourceInfotrueTurn on MAUI's XAML source info, so notes say which file and line (Debug builds)
ProjectPathworked out at build timeThe app project's folder from the repository root, put in front of XAML paths
CaptureLogs, LogLimittrue, 50Attach the app's recent ILogger messages and unhandled exceptions
AppName, AppVersionfrom the appRecorded on every note
MaxScreenshotScale2Phones are 3x; 2x is plenty to read and half the size

The configuration is read through IOptionsMonitor, so a source that reloads takes effect without a restart.

At runtime: INotato#

Take INotato from DI (or Feedback.Current where there is no DI) to switch Notato on and off, and to select things:

public partial class DeveloperSettingsPage(INotato notato) : ContentPage
{
    void OnNotatoToggled(object? sender, ToggledEventArgs e)
    {
        if (e.Value) notato.Enable();   // remembered across launches
        else notato.Disable();          // removes the overlay and closes the connection
    }
}
Enable(), Disable(), IsEnabledOn and off. A choice made here wins over Enabled in configuration until ResetRuntimeState()
ShowToolbar(), HideToolbar(), IsToolbarVisibleThe toolbar. Off still lets the app drive Notato from code
StartAnnotating(), StopAnnotating()The next tap selects what is under it
SelectAsync(element)Select an element as if it had been tapped, and open the note for it
AnnotateAsync(element, comment, options)Make a note with no UI, as a person (PeopleOnly keeps it from the agent) or (with AgentName) an agent
AnnotateAsync("#SignIn", comment, options)The same, finding the element with a selector
Annotations, PendingCount, Connection, ConnectionDetail, ChangedWhat Notato knows, for a status line or a badge. Safe to read from any thread
PackageAsync()Test mode: the device's notes as a bundle zip

The sample's Feedback tab does each of these.

What a note carries#

Everything the web SDK sends, in the same schema (environment.platform is maui), so the board, the Markdown and the MCP tools need nothing new:

Mark things up in XAML with xmlns:notato="clr-namespace:Notato.Maui;assembly=Notato.Maui": notato:Feedback.Mask="True" makes an element private, and notato:Feedback.Ignore="True" makes the picker look through it.

Everything you write reaches the agent, notes and replies alike, unless you keep it between people (see what the agent gets):

@name calls one of the server's mention plugins. None is built in and none is needed to reach the agent, so the composer and the reply box show no chips unless the server adds a plugin.

What a note never carries:

Selectors#

A selector is a path through the visual tree, written like CSS so it reads at a glance and the agent can turn it into a search: LoginPage VerticalStackLayout#Form > Button:nth-of-type(2).

ButtonThe control's type (* for any)
#SignInIts AutomationId (or [AutomationId="Sign in"])
[x:Name=Email]Its x:Name
.primaryA StyleClass
:nth-of-type(2)The second child of its parent with that type
:text("Sign in")Its text contains this, ignoring case (for agents; never generated)
space, >Somewhere inside, directly inside

Notato writes the shortest selector that finds only that element on its page, and uses selectors to put pins back on elements after a restart. An AutomationId makes the steadiest one.

Pins#

A pin sits on the element its note was made on, follows it as the page scrolls, and goes faint where the element cannot be found (at the place the note was made). Only notes made with this package get a pin: a note from a web page or another SDK is in the Notes list but has no pin, because its selector means nothing here. A screen shows at most the newest 150 pins (as the React Native and Flutter SDKs do); the Notes list has every note. Pins are looked for together, in one pass over the page about once a second while one is missing, and drawn again only when one moved or changed.

Modes#

How the overlay works#

Notato draws its UI with MAUI controls, in a layer of its own above the app, so it needs nothing from the app's pages and works over Shell, tabs, navigation, modal pages, popups and alerts:

Notato's controls carry explicit styles, so the app's implicit styles do not reach them, and its handler customisations apply only to the app's own controls.

The picker hit-tests the visual tree with the native views' real positions (scroll views and clipping included), topmost first. Parent in the note widens the selection to the element around it.

Physical devices and shared servers#

notato dev listens on loopback only. Your own phones reach it through a dev tunnel (On a phone), and a USB-connected Android phone also with adb reverse. For testers elsewhere, run a shared server (notato serve, see the main README) and give the app its address and a project token in configuration. The token only ever goes to that server (its scheme, host and port): a server typed into the toolbar's settings gets no token, so a note sent there goes without one.

Known limits#