This file is the single source of truth for project conventions, patterns, and lessons learned.
Always read this file at the start of a session. Update it as new knowledge is gained.
Do NOT use store_memory or retrieve from memory tools — use this file instead.
- CI build:
dotnet build ci.slnfanddotnet test ci.slnf(excludes SampleMauiApp which requires MAUI workloads) - Sample app:
dotnet build src/SampleMauiApp -f net10.0-maccatalyst(or-ios,-android) - Local CLI: Always use
dotnet run --project src/MauiDevFlow.CLI --or the built binary — never the globally installed tool when testing local changes - NuGet local testing: Use a low prerelease version (e.g.
0.0.999-as8d33f) to avoid conflicting with published versions. Never install the tool globally when testing locally — usedotnet run --projectinstead - iOS build timing: iOS simulator builds finish faster than expected — check agent status early (~60s) instead of waiting the full 120s
- Incremental build caching: Mac Catalyst builds may use stale linked DLLs. Always clean both Agent AND SampleMauiApp
bin+objfor the target TFM when debugging Agent code changes - Always verify with sample app: Build, deploy (Mac Catalyst is fastest on macOS), and test end-to-end before marking tasks complete
- Do NOT auto-commit or push unless the user explicitly says to
- Release process: Create GitHub release with tag
vX.Y.Zat https://github.com/Redth/MauiDevFlow — always use the correct org/repo link - Versioning:
Directory.Build.propshas shared version for Agent/Blazor. CLI has its own version in its.csproj. Publish workflow uses Git tag asPackageVersion
- Single port: MauiDevFlow uses a single port (default 9223, configurable) for both MAUI native and CDP commands. No WebSocket — CDP uses HTTP POST
/api/cdp - Config file:
.mauidevflow(hidden dotfile, no extension) in project directory. MSBuild targets and CLI both read port from this file - MSBuild port override:
dotnet build -p:MauiDevFlowPort=XXXX. The.targetsfile writes a.g.cswithAssemblyMetadataAttributeusingWriteLinesToFile(notAssemblyAttributeitems, which don't work) - MSBuild target conditions:
Conditionon a target is evaluated beforeBeforeTargetsdependencies run. Don't putConditionon a target that depends on a property set by itsBeforeTargetsdependency — move the Condition to inner tasks/ItemGroups instead - Agent/Blazor are
#if DEBUGonly — never ship in production - Reflection for cross-package wiring:
BlazorDevFlowExtensions.WireAgentCdp()connects Blazor→Agent via reflection (Type.GetType/GetProperty/SetValue) to avoid NuGet dependency between packages
- Broker port: 19223 (default). Agents connect via WebSocket, CLI queries via HTTP
- Agent port assignment range: 10223-10899 (raised from 9223-9899 to avoid collisions with old
.mauidevflowconfig files) - CLI auto-starts broker:
ResolveAgentPort()callsEnsureBrokerRunningAsync()when the TCP check fails. The TCP check MUST be in its own try/catch —Wait()throwsAggregateExceptionon timeout rather than returning false - Agent retries broker: If the broker isn't running at app startup, the agent starts a background reconnection timer (2s, 5s, 10s, 15s backoff, indefinite). Apps launched before the broker auto-register when it comes up
- Agent reports currentPort: Late reconnections include
currentPortin the registration so the broker uses the agent's existing HTTP listener port instead of assigning a new one - Broker testing: When changing broker code, always kill the running broker process, rebuild, and restart before testing. The broker is a detached daemon — old code keeps running until explicitly killed
- Port conflicts with global tool: When debugging MauiDevFlow locally, a broker from the globally installed CLI may already be on port 19223. Use a non-default port for local testing to avoid conflicts
- Agent identity:
SHA256(absolute_csproj_path + "|" + TFM)[:12]in lowercase hex - Android connectivity:
adb reverse tcp:19223 tcp:19223for agent→broker.adb forward tcp:{port} tcp:{port}for CLI→agent - Broken pipe blocking: When spawning background broker via
Process.Start(), must redirect+close stdout/stderr/stdin. OtherwiseConsole.WriteLineblocks (not throws) after parent exits on macOS, preventingHttpListenerfrom processing requests - HttpListener prefix: Use
http://localhost:nothttp://+:on macOS — the latter sometimes fails for WebSocket upgrades
- Global options:
--agent-port,--agent-host,--platformuserootCommand.AddGlobalOption()so they work on all subcommands (e.g.MAUI status --agent-port 9225). Don't add them to individualCommandinitializers - Port resolution priority: Explicit
--agent-port> broker discovery (exact ID → same project → single agent) >.mauidevflowconfig > default 9223
- Chobitsu async behavior: Chobitsu fires
onMessageasynchronously (not in same JS turn assendRawMessage). CDP commands need two JS evals: one to send + capture, one to read the response with a small polling delay - Chobitsu loading: Must be loaded via a static
<script>tag inindex.html. Dynamic script tags fail with MAUI'sapp://scheme, andEvaluateJavaScriptAsyncfails because chobitsu useseval/new Functioninternally which CSP blocks - Blazor JS initializers: DO work in .NET 10 MAUI Blazor Hybrid. Use
beforeStart()to inject script tags - RCL static web assets: Files served at root path (e.g.
chobitsu.js) and_content/{PackageId}/viafetch(), but only root-relative paths work for<script src>tags inapp://scheme - NuGet packaging: MauiDevFlow.Blazor delivers
chobitsu.jsas an RCL static web asset. Two MSBuild targets remove_framework/assets from build and publish manifests to avoid conflicts - GenerateJSModuleManifest=false: Required in MauiDevFlow.Blazor to prevent conflicting
_framework/blazor.modules.jsonwith consuming apps
- Assembly.GetEntryAssembly() returns null on Android/iOS: Must scan
AppDomain.CurrentDomain.GetAssemblies()forAssemblyMetadataAttributevalues - DeviceInfo.Platform throws during DI registration: MAUI isn't fully initialized when
AddMauiDevFlowAgentruns. Fallback usesOperatingSystem.IsAndroid()etc. - SynchronizationContext deadlock:
AddMauiDevFlowAgentruns on the main thread which has aSynchronizationContext. Must useTask.Run(() => ...).GetAwaiter().GetResult()for async broker registration to avoid deadlock - Mac Catalyst sandbox:
Path.GetTempPath()returns~/Library/Containers/{bundleId}/Data/tmp/not/tmp/ - Device compatibility: MAUI TFM compile target (e.g. net10.0-android targets API 36) doesn't mean you need a matching emulator. Apps run on any device at or above
SupportedOSPlatformVersion - TapGestureRecognizer:
SendTappedisinternal voidonTapGestureRecognizer. Call via reflection withBindingFlags.NonPublic, args:(sender, null)
- Buffered logging:
FileLogWriterusesConcurrentQueuebuffer +Timerdrain +ReaderWriterLockSlimfor thread-safe file writes - File sharing on Windows: Always open log files with
FileShare.ReadWrite | FileShare.Deletefor reading.File.ReadAllLines()uses defaultFileShare.Readwhich conflicts with the writer on Windows - Log format: Rotating JSONL files with compact property names (
t,l,c,m,e,sfor source: "native" or "webview") - WebView log capture: JS
console.*intercepted byconsole-intercept.js→ buffered inwindow.__webviewLogs→ drained every 2s by native timer → written to same JSONL files withsource: "webview"