# Architecture

> How Umbra is put together - the host app, widgets, the control protocol and the CLI - with a map of the source tree.

Umbra pins small web widgets to the Windows desktop. The **C# host** knows nothing about any specific widget; widgets are folders with a `widget.json`. It targets .NET 10 (WinForms + WebView2) on Windows 10 1809+ / Windows 11.

## The big picture

```
 widgets/<id>/ ──► WebHost (WebView2 env, virtual hosts, sandbox)
                        │
 settings.json ──► App ─┼─► WidgetWindow ×N  (borderless, pinned under apps, JS bridge)
                        ├─► SettingsWindow   (HTML panel + message protocol)
                        ├─► Tray / EditPill / ToastService / UpdateService
                        │
 umbractl ──┐           │
 MCP agent ─┼─► named pipe (current user) ─► Control.Execute ─► App
 scripts  ──┘
```

- **Host** (`src/Umbra`): owns settings, creates one borderless window per enabled widget, draws the tray menu and Settings, and serves the control pipe.
- **Widgets** (`widgets/<id>/`, installed to `%APPDATA%\Umbra\widgets`): a `widget.json` plus a web page. They talk to the host through a small JS bridge.
- **Control protocol**: one JSON line per request over a per-user pipe. `Control.cs` is the single place that defines what automation can do.
- **CLI and MCP** (`src/Umbra.Cli`): `umbractl.exe` maps commands onto the protocol and runs the MCP stdio server.

## Source map

| Path | Role |
|---|---|
| `src/Umbra/Program.cs` | Entry: single instance, WebView2 check, starts `App`, serves the control pipe |
| `src/Umbra/App.cs` | Owns settings, widget windows, tray menu, Settings window. `OnUi()` marshals work to the UI thread |
| `src/Umbra/WidgetWindow.cs` | One borderless desktop-pinned window per widget; JS bridge (`fetchJson`, `drag`, `rendered`…); host-drawn right-click menu; `CaptureAsync` |
| `src/Umbra/DesktopPin.cs`, `Native.cs` | Win32: owned by Progman, kept at `HWND_BOTTOM`, no activation, DWM corners |
| `src/Umbra/WebHost.cs` | WebView2 environment, virtual hosts `umbra.local` / `community.umbra.local`, **sandbox** |
| `src/Umbra/SettingsWindow.cs` + `settings/` | Settings panel (HTML) and its message protocol |
| `src/Umbra/EditPill.cs` | The host-drawn "Editing widgets · Snap · Done" pill per monitor |
| `src/Umbra/Tray.cs` | Tray tooltip, the "update ready" badge, the `--restart` flag |
| `src/Umbra/CrashRecovery.cs` | Policy for renderer and browser-process failures |
| `src/Umbra/Community.cs` | Catalog fetch, install and remove, path and type validation |
| `src/Umbra/Themes.cs` | Theme model, token allow-list and validation, generated `:root` CSS |
| `src/Umbra/WidgetManifest.cs`, `Settings.cs` | `widget.json` model and discovery; `settings.json` model |
| `src/Umbra/WidgetStore.cs`, `WidgetTrash.cs` | `umbra.store` data and the 7-day trash |
| `src/Umbra/WidgetCommands.cs`, `CommandService.cs`, `CommandRunner.cs`, `CommandApprovals.cs` | `permissions.commands` |
| `src/Umbra/ToastService.cs`, `Notifier.cs` | `umbra.notify` |
| `src/Umbra/Updates.cs`, `src/Umbra.Updater/` | Update check, download and the update window |
| `src/Umbra/Protocol.cs`, `Control.cs` | Control protocol and its executor |
| `src/Umbra.Cli/` | `umbractl.exe`: `Program.cs`, `CommandLine.cs`, `McpServer.cs`, `Scaffold.cs` |
| `shared/umbra.js`, `umbra.css` | Widget SDK and design tokens served to every widget |
| `widgets/<id>/`, `themes/<id>/` | Catalog widgets and themes; the `index.json` files are generated by `node scripts/catalog.mjs` |
| `schemas/` | `widget.schema.json`, `theme.schema.json` |
| `installer/Umbra.iss`, `build.ps1` | Per-user Inno Setup installer; the version comes from `Directory.Build.props` |
| `tests/Umbra.Tests/` | Unit tests for host and CLI |

Nothing is built into the app: even `sp500` is installed from Browse. The generic mechanism for built-in widgets exists but none ship today.

## Where state lives

`%APPDATA%\Umbra` (or `<AppDir>\data` when [portable](/umbra/docs/internals/portable-mode/)): `settings.json`, `widgets\`, `data\<id>.json`, `themes\`, `trash\`, `approvals.json` and `umbra.log`. `UMBRA_DATA_DIR` overrides it, and tests use a temporary directory.

## Threading

WinForms and WebView2 are single-threaded: `App.OnUi()` marshals work from the pipe server and background tasks onto the UI thread. Control commands run on the pipe's thread and call into `App` through that.

## Related

[Desktop pinning](/umbra/docs/internals/desktop-pinning/) · [WebView2 host](/umbra/docs/internals/webview-host/) · [Security model](/umbra/docs/internals/security/) · [Crash recovery](/umbra/docs/internals/crash-recovery/) · [Dev setup](/umbra/docs/internals/dev-setup/)
