Imported from kristofferR/Carrier (
AGENTS.md). Install upstream withnpx skills add kristofferR/Carrier. Copyright stays with the author.
Carrier — project guide & agent handoff
Contributor pull requests
- PRs from contributors other than
kristofferRmust includeAI models used: Noneif no AI helped create or edit the contribution. Otherwise, list every model used to create or edit code, tests, or PR text by its most specific available name and version, for exampleAI models used: GPT-6 Astra. - Include each model's reasoning level or effort when the tool exposes it, for
example
Reasoning levels: GPT-6 Astra: high. If unavailable, sayReasoning levels: Unavailable (not exposed by tool). Never guess. Missing or unavailable reasoning levels do not block a PR. - Routine automated review bots need not be listed. Put this disclosure in the PR description, never in commit authorship or co-author trailers.
Carrier is a tiny Tauri v2 desktop client for Facebook Messenger (wraps
facebook.com/messages in a chrome-stripped native window). Repo:
kristofferR/Carrier (use the kristofferR GitHub account). v1.0.0 is
released. Cross-platform: macOS, Windows, and Linux (CI builds all six
targets). The "macOS theme rendering" section below is platform-specific; on
Windows/Linux the window chrome follows set_theme directly and there's none of
the WKWebView/title-bar trouble.
Kris's machines: always install diagnostics builds
On Kris's MacBook and Omarchy, install only debug builds with the diagnostics
Cargo feature (--debug --features diagnostics). This is a permanent build policy,
not a version pin. Never install public release artifacts, Homebrew casks, or AUR
packages on these machines. Public packaging remains for other users.
Use the managed carrier-debug-update path in docs/debug-installs.md.
It follows CI-built debug drafts made before public builds (or manually from main),
stages updates while Carrier runs, and installs
only while quit. Preserve a live failure until Kris authorizes restarting it.
Do not overwrite the app manually or drop matching symbols. MCP/DevTools, native
logging, backtraces, and instrumentation must be enabled for every personal build.
Build / run / install
# from repo root
bun install
bun run tauri build # release-ish bundle (debug symbols unless --release config)
bun run tauri build --debug --features diagnostics --bundles app # fast debug .app only
# signed macOS release (Developer ID) — for the real install:
export APPLE_SIGNING_IDENTITY="Developer ID Application: Kristoffer Risanger (S5Q742QZEL)"
bun run tauri build # uses src-tauri/tauri.conf.release.json via the CI flow
# install without a Gatekeeper prompt (rm -rf on /Applications is blocked by the safety net):
ditto "<path>/Carrier.app" "/Applications/Carrier.app"
xattr -dr com.apple.quarantine "/Applications/Carrier.app"
Gate before every commit (CI mirrors this):
cd src-tauri && cargo fmt && cargo clippy --all-targets -- -D warnings && cargo test --lib
bun run check # Biome lint + tsc typecheck + bun test + rebuild the inject bundles
The injected scripts are TypeScript now: edit inject/src/, never
src-tauri/inject/*.js (generated; messenger.css and the dev-only
mcp-bridge.js are still hand-maintained). After changing inject/src/, run
bun run build:inject and commit the regenerated bundles alongside the source
— CI fails if they drift. Pure logic lives in inject/src/messenger/lib/ with
bun test coverage; add tests there when extending it.
Releases use curated notes, not a generator. Before pushing a v* tag, inspect
the exact tag range and merged PRs and write the release title/body with human or
agent judgment, using direct sections such as What's New, Bug Fixes, and
Internal only when useful. Create the GitHub draft first, for example:
gh release create v1.4.0 --draft --target main --title v1.4.0 --notes-file release-notes.md
git tag v1.4.0 <release-commit>
git push origin v1.4.0
.github/workflows/release.yml requires a non-empty curated draft, builds 6
targets, signs + notarizes macOS, attaches artifacts without rewriting the title
or body, and publishes it. Apple/Tauri secrets are set in the repo.
tauri-mcp — dev webview inspection
Lets an agent inspect/drive the running webview: execute_js, query_page
(DOM), take_screenshot (background — no window popping to the foreground),
click/type, etc. Plugin: P3GLEG/tauri-plugin-mcp.
On main (committed d8a25a6).
- Gated behind the Cargo
mcpfeature → release builds never compile it. - Build with it:
bun run tauri build --debug --features diagnostics --bundles app. Debug builds are marked — the window title reads "Carrier (debug)". .mcp.jsonregisters thetauri-mcpserver — approve the project MCP server when a new session prompts for it.- A "Carrier (debug)" build must be RUNNING for the tools to connect — build it
with the command above, then
dittoit into/Applications. - For Hide Names & Avatars selector work, first run the dev-only sanitized probe
through MCP
execute_jswith code__carrier_mcp_privacy_probe__. It reports live Messenger selector hits, rectangles, computedfilter, and sanitized ancestor/attribute shapes; it must not expose message text, raw numeric IDs, image URLs, oralt/aria-labelcontents. Prefer extending this probe over guessing selectors or dumping raw DOM when debugging privacy blur coverage. - Notification work has the same treatment:
__carrier_mcp_notification_row_probe__(conversation-row and message-row shape — where emoji sprites sit, which images a row carries) and__carrier_mcp_sender_avatar_probe__(what the sender-avatar harvest would take, what it cached, and whether each visible group preview's sender resolves to a face). Both report classifications and counts only.
Architecture
src-tauri/src/lib.rs— the Rust shell: window/tray/menu/settings/theme,on_navigation(off-site → default browser),on_download(media only, blocks executables), updater.src-tauri/inject/— injected at document-start:messenger.css(hides FB chrome + theme/compact/login CSS),messenger.js(shortcuts, zoom, image viewer, notifications incl. mute / hide-preview privacy, unread badge, force-theme, hide names & avatars, login tidy),panel.js(toast, settings/update bridge).messenger.js/panel.jsare generated — the typed sources live ininject/src/(one module per feature; pure, unit-tested logic underinject/src/messenger/lib/), bundled back to single IIFE files bybun run build:inject(esbuild, config ininject/build.ts). Init order ininject/src/messenger/index.tsmirrors the old single-file section order — capture-phase listeners on the same event fire in registration order, so keep it.dist/settings.html— the standalone Settings window.- IPC model (important): the FB page is a remote origin, so it cannot
call Carrier's own commands. Page→backend goes through Tauri plugins
(
plugin:opener|open_url,plugin:window|set_theme/set_badge_count,plugin:event|emit) and core events the Rust side handles viaapp.listen_any(carrier:open-settings,carrier:check-updates,carrier:unread, andcarrier:notify— the new-message notification bridge, emitted viaplugin:event|emitand rendered natively). Settings are pushed to the page aswindow.__CARRIER_SETTINGS__+ acarrier:settingsevent.
macOS theme rendering — hard-won, do NOT re-litigate
The forced light/dark theme (Settings → Theme) was a long rabbit hole on macOS.
These traps are already worked around in code — don't redo them:
- Tauri's
set_background_colorinverts white→black on macOS (tauri#12349), so the window background is set directly via objc. - WKWebView is opaque white on macOS and bleeds through the title bar / login surround — it's forced transparent via a private API.
- The title bar only themes at window creation, so a theme switch recreates the windows (the page reloads; the login session survives via cookies).
- The login page is light-only; its dark mode is applied by JS off the forced theme.
Current state
v1.0.0 is released; main is the trunk. Live work lives on GitHub, not here —
gh issue list / gh pr list for open bugs, enhancements, and WIP branches.
pre-v1.0 is a kept snapshot base.
Conventions
- GitHub: kristofferR (
gh auth switch -u kristofferR). Trigger CodeRabbit only throughcrq(never post@coderabbitai reviewdirectly). Acrq autoreviewdaemon may be running. - Batch dependency maintenance into one PR per update cycle. When several Dependabot PRs are open, consolidate their current safe updates with the rest of the Bun, Cargo, and pinned GitHub Actions audit, validate the combined branch, open one PR, then close the superseded bot PRs. Do not queue, review, and merge dependency PRs one by one unless the maintainer explicitly asks.
- Commits: branch off by default — though the maintainer may explicitly ask for a
direct push to
main(as with the post-v1.0 merge above). One logical change, no AI attribution orClaude-Session:footer, non-closing issue refs (Ref #5).