AGENTS.md@public/files/anarlog/crates/chrome-native-host · git:20260915.997c05b · 2026-09-15 · sha256 25dbd9f05adf446d
AGENTS.md@public/files/anarlog/crates/chrome-native-host git:20260915.997c05bA
Immutable. This exact content is served forever at /api/v1/blob/25dbd9f05adf446d.
# chrome-native-host
Rust binary that bridges the Chrome extension to the Char desktop app.
It is registered as a Chrome Native Messaging host, so Chrome spawns it as a subprocess and communicates over stdio.
## System diagram
```
Google Meet tab
│ chrome.runtime.sendMessage(payload)
▼
background.js (service worker)
│ port.postMessage(payload) [Chrome Native Messaging]
▼
char-chrome-native-host ← this binary
│ writes atomically
▼
chrome_state.json (on disk)
│ polled every 500 ms
▼
GoogleMeetWatcher (crates/detect)
│ fires DetectEvent
▼
Tauri plugin → React/Zustand
```
## Native Messaging wire protocol
Chrome Native Messaging uses **stdin/stdout** with a simple framing:
```
┌─────────────────┬──────────────────────────────┐
│ 4 bytes LE u32 │ N bytes UTF-8 JSON │
│ (message len) │ (message body) │
└─────────────────┴──────────────────────────────┘
```
- Length is **little-endian unsigned 32-bit** integer.
- Body is a UTF-8 JSON object.
- The binary reads messages in a loop until EOF (Chrome closed the port).
## Incoming message contract
Two message types are sent by `background.js`:
### `meeting_state` — meeting is active
```json
{
"type": "meeting_state",
"url": "https://meet.google.com/abc-defg-hij",
"is_active": true,
"muted": false,
"participants": [
{ "name": "Alice", "is_self": true },
{ "name": "Bob", "is_self": false }
]
}
```
### `meeting_ended` — tab closed / navigated away
```json
{
"type": "meeting_ended",
"url": "https://meet.google.com/abc-defg-hij",
"is_active": false
}
```
Any unknown `type` is silently ignored.
## Output: `chrome_state.json`
Written to `{data_dir}/char/chrome_state.json` after every message.
`data_dir` is platform-specific (`dirs::data_dir()`):
- macOS: `~/Library/Application Support`
- Linux: `~/.local/share`
- Windows: `C:\Users\{user}\AppData\Roaming`
**Schema:**
```json
{
"version": 1,
"timestamp_ms": 1700000000000,
"meeting": {
"url": "https://meet.google.com/abc-defg-hij",
"is_active": true,
"muted": false,
"participants": [
{ "name": "Alice", "is_self": true }
]
}
}
```
`meeting` is `null` when the meeting has ended or `is_active` is false.
**Write is atomic** — a `NamedTempFile` in the same directory is written and then renamed,
so the watcher never reads a partial file.
`timestamp_ms` is set to the current Unix epoch milliseconds on every write.
The watcher in `crates/detect` treats state older than 30 seconds as stale.
## Testable units
| Function | What it does | How to test |
| ----------------- | ----------------------------------------------- | -------------------------------------------------------------- |
| `read_message` | Decodes framed bytes from a `Read` | Feed a `Cursor<Vec<u8>>` |
| `process_message` | Maps `IncomingMessage` → `Option<MeetingState>` | Pure function, no I/O |
| `write_state` | Atomically writes JSON to a `Path` | Pass a `tempdir` path |
| `run` | Full loop: read → process → write | Pipe encoded messages through a `Cursor`, assert file contents |
## Bundle configuration
For production, the binary is included in the app bundle as a Tauri `externalBin`
(`tauri.conf.json`), placing it at `Contents/MacOS/char-chrome-native-host` next
to the main executable.
**How `prepare-binaries.mjs` works (runs as `beforeBundleCommand`):**
1. Reads `TAURI_ENV_TARGET_TRIPLE` — the env var Tauri sets to the actual build
target, not the host. This matters for cross-compilation: the CI macOS job
builds both `aarch64-apple-darwin` and `x86_64-apple-darwin` from a single
ARM runner, so host ≠ target on the x86_64 build.
2. Runs `cargo build --release --target $triple -p chrome-native-host` with
`cwd` set to `apps/desktop/src-tauri/` so that `.cargo/config.toml`'s
`target-dir = "target"` applies. Binary lands at
`apps/desktop/src-tauri/target/$triple/release/char-chrome-native-host[.exe]`.
3. Copies it to `binaries/char-chrome-native-host-$triple[.exe]`, which is what
`externalBin: ["binaries/char-chrome-native-host"]` expects.
No CI steps or `build.rs` changes are needed — `beforeBundleCommand` runs inside
`tauri build` after the main Cargo step, so the cross-compilation toolchain is
already present.
For development (`debug_assertions`), no bundling is needed — the runtime resolves
directly to `target/debug/char-chrome-native-host` built by Cargo as part of the workspace.
## Running tests
```
cargo test --package chrome-native-host
```