Architecture Decision Records

ADR-011: Native macOS limits app (Rust truth, Swift shell)

jackin❯ desktop reuses Rust usage projections through boltffi and limits Swift to native macOS presentation.

Status: Accepted

Decision

jackin❯ desktop is a host process that displays account quota limits without a running Capsule. Rust owns account discovery, config/auth resolution, canonical account identity, deduplication, scheduling, shared cache, single-flight coordination, account selection, quota semantics, severity, provider ordering, and every displayed domain string. Swift owns AppKit/SwiftUI presentation, accessibility, lifecycle, settings, signing, and other macOS integration.

The boundary is a synchronous coarse-grained boltffi facade compiled as a static library and assembled into an arm64 XCFramework. One desktopProjection call returns a complete immutable broker generation under one Rust runtime lock: grouped providers/accounts, selected identities, status-item rows, details/limits, activity, and sanitized diagnostics. Swift obtains it through an off-main-actor scheduler; it never implements HTTP/RPC/OAuth/CLI probes or a second provider matrix.

Product and platform bounds

  • Desktop display domain: Claude, Codex, Amp, Grok, Z.AI, Kimi, MiniMax.
  • Display: remaining/used percentage, reset timing, plan/status, multi-account identity, and provider-supplied quota caps.
  • Excluded: token unit prices, inferred cost, usage/spend history, trends, charts, forecasts, analytics, and unrelated providers.
  • Deployment target: macOS 26.0.
  • Release toolchain: Xcode 26.6 on a fixed macos-26 GitHub runner.
  • Release architecture: Apple Silicon arm64.

Before 1.0 this is latest-only engineering. There is no macOS 14/15 UI lane, compatibility renderer, custom material, or alternate design.

Native hierarchy and Liquid Glass

Liquid Glass is system-owned functional chrome, never content decoration. Production UI contains zero explicit glassEffect, zero GlassEffectContainer, zero app-owned material, and zero custom-glass helper.

RegionHost/componentClassification
Provider statusNSStatusItem / NSStatusBarButtonnative functional chrome, template monochrome
Provider glancetransient NSPopover hosting SwiftUI contentnative host; centered product identity plus focused provider content
Usage shellretained NSWindow, unified NSToolbar, NSSplitViewControllernative functional hierarchy
Usage sidebarsidebar List plus non-interactive generated wordmarknative navigation plus identity
OverviewSwiftUI Tablenative content
Provider detailList, Section, LabeledContent, Picker, ProgressViewnative content/actions
Settingstitled NSWindow plus grouped Formnative configuration

jackin❯ phosphor is an adaptive identity/healthy-state token. Warning and danger retain text plus system semantic color. Provider marks come from the canonical generated SVG assets. The quiet sidebar wordmark is jackin❯ by tailrocks; it is hidden from accessibility because it is not an action. The popover uses the generated template monogram beside jackin❯ desktop, and the Usage detail's native top accessory centers the same text over that pane. Neither identity is interactive or owns material.

Interaction architecture

StatusBarController reconciles native status items from the Rust projection. A primary click opens one real transient NSPopover focused on that provider. The popover has no cross-provider navigation. It starts with centered, non-interactive jackin❯ identity, then provider plus selected account and one Rust-owned activity phrase, Limits, and useful Details without repetition. Its fixed native footer keeps independent semantic icon-only Refresh and Open Usage controls adjacent at the leading edge and places the optional native account picker at the trailing edge. Their SF Symbols omit visible button text while semantic labels, hover help, shortcuts, and system-owned hit regions preserve discovery and accessibility. A secondary click opens the fixed AppKit menu: Open Usage Window, Refresh, Quit jackin❯ desktop.

Cross-provider navigation belongs in the Usage window. UsageWindowController lazily creates and retains one NSWindow. A native NSSplitViewController owns the full-height sidebar and detail while SwiftUI renders their content. The standard .toggleSidebar toolbar item routes through NSSplitViewController.toggleSidebar(_:); its native initial width is retained while its accessibility label changes with the real collapsed state. This is the only visibility authority for the visible control, View menu, and Control-Command-S. An NSSplitViewItemAccessoryViewController centers non-interactive jackin❯ desktop identity over the detail pane and keeps Refresh trailing without a root header or app-painted material. No redundant Usage title appears.

Window entry is intentional:

  • show(focusOn:) explicitly selects Overview or a provider.
  • show() reopens the retained window without changing a valid destination.
  • popover Open Usage calls the explicit provider path.
  • App menu/Dock reopen calls the preserving path.

PresentationStore owns selection validity. Provider removal/disablement normalizes Usage to Overview and moves stale popover focus to the first valid provider. Views do not conceal invalid owner state with local fallback.

Overview is a hierarchical native Table: provider parents contain exact account children across Provider, Account, Plan or status, Remaining, and Reset columns. Provider detail keeps the deeper order: identity, optional account picker, Details, then Limits. Popover Open Usage carries the exact provider and account key.

Standard commands use the native app menu and responder chain: Command-R, Command-comma, Command-W, Control-Command-S, and Escape.

Rust presentation ownership

Rust builds the ordered UsageDetailPresentation consumed by Capsule and jackin❯ desktop. Each row has stable position-derived identity, Rust-owned label/display text, grouped layout lines, and geometry-only meter percentage and severity. Swift iterates these rows mechanically; it does not split pace copy, join quota strings, inspect raw provider buckets, or invent missing-field text.

The same rule applies to status items, focused popover, Overview inventory, and account menus: provider membership, order, labels, reset text, status, and quota values originate in Rust. Screen-sharing privacy may hide a projected status-item value but cannot replace it with new semantics.

Loading, empty, refreshing, fresh, stale-last-good, depleted-with-reset, needs-login, unsupported, provider failure, and global bridge failure remain separate states. One Rust identity activity reads Updating… only during an active generation and becomes one terminal recency/exception phrase afterward. Complete projection decode is the only replacement boundary; transient failure preserves exact last-good data and selection, while generation guards reject stale resumes. Global and provider failures expose native Retry actions.

Credential and snapshot ownership

Host credentials are resolved by Rust from the read-only global/workspace/role catalog. Important roots include Claude Keychain/file credentials, Codex ~/.codex/auth.json, provider home sessions, and configured provider-key capabilities. Secrets remain behind host-local handles.

One host-only Rust broker is the sole provider-call and state writer. Each canonical account has one generation; every active caller, including force, joins it. A bounded wait timeout never releases ownership while provider work continues. Terminal result, last-good quota, failure count, success cooldown, and provider retry deadline commit as one private atomic crash-recoverable envelope. Coordination/state failure is fail-closed and performs zero provider calls. Provider Retry-After is never shortened; when absent, one persisted exponential deadline is shared by every process.

Desktop attaches directly through its host client. A Capsule receives only a per-container relay at /jackin/run/usage.sock, authorized by the exact credential capabilities forwarded at launch. No global broker socket, account tree, credential catalog, or broker state is mounted into a container. Credentials created only inside a Capsule are out of scope pending a separate secure-enrollment design.

Claude Keychain reads use the shared claude_keychain_scope helper. Explicit denial is terminal for that service for the process, prevents repeated prompts, and does not affect other providers. Missing/headless-interaction results still permit file/environment fallback according to Rust policy.

Verification boundaries

Synthetic F00–F14 visual-QA fixtures activate only through explicit --fixture argv, show a persistent Fixture badge, and never call the bridge, network, or real credentials. Frozen projection variants drive account selection and Updating… transitions through the same production AppKit/SwiftUI hosts as live data. Environment variables cannot activate fixtures. Debug-target extraction remains deferred maintenance.

Architecture tests enforce no custom glass/material, no Swift-composed domain strings, no hard-coded provider display names outside fixtures, and no probe imports. Real-host UI tests verify native commands, focused-popover routing, full-height sidebar geometry, a stationary dynamically labeled toggle, detail-centered identity, focus/retained-window continuity, minimum-size scrolling, visible recovery, and accessibility audits for popover, Overview, and provider detail.

Distribution

Release output is jackin-desktop-<VERSION>-aarch64-apple-darwin.zip containing exactly JackinDesktop.app. Publication requires Developer ID hardened-runtime signing, notarytool acceptance, staple, Gatekeeper assessment, SHA-256 sidecar, Sigstore bundle, CycloneDX SBOM, and GitHub provenance attestation. The first Homebrew cask update is independently reviewed and never auto-merged.

Secret-free release validation proves assembly, bundle verification, release-mode rejection of ad-hoc output, and read-only reconciliation. Public distribution remains externally gated until an operator provisions Apple credentials and completes the first notarized/cask proof.

Consequences

  • Capsule and jackin❯ desktop cannot drift in quota wording or order without a shared Rust contract/test failure.
  • Native macOS behavior owns navigation, focus, toolbar, popover, and material.
  • Cross-provider comparison stays in Usage; the popover stays provider-focused.
  • New display behavior requires a Rust projection or an explicit architecture decision, not Swift-side inference.
  • Capture-gated provider wire formats remain unimplemented until authentic operator-owned evidence exists.

On this page