Open source Claude Code web/Codex Cloud/Devin/Ramp Inspect alternative
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
[codex] Add more cmux agent skills (#5614) * Add more cmux agent skills * Trim cmux CLAUDE skill index * Keep setup and local dev in CLAUDE * Expose repo skills to Claude and Codex * Keep core testing policies in CLAUDE * Keep pitfalls and release rules in CLAUDE | 3 个月前 | |
Add iOS App Store production lane (#7644) * Add iOS App Store release lane * Fix App Store workflow build output env * Harden App Store release lane dispatch * Fix App Store ASC workflow build-number args * Harden App Store workflow operations * Block App Store external purchase links * Require Apple sign-in entitlement for App Store uploads * Localize App Store billing unavailable copy | 2 个月前 | |
Compact CLAUDE.md and skills docs (#8990) * Compact CLAUDE.md and skills docs Cut ~2k lines of duplication without dropping actionable rules. CLAUDE.md (267 -> 103): `reload.sh --tag` was explained four separate times; now once. Dropped the Ghostty submodule and Release sections, which restated the cmux-ghostty and cmux-release skills, and removed the file:// deeplink block, which contradicted the rule that chat output uses http://127.0.0.1:17320/<tag> and never a file:// URL. Pitfalls compressed from paragraphs to one line each, pointing at the owning skill. skills/ (4849 -> 2819 across 20 skills): the dominant waste was SKILL.md files restating their own references/ verbatim. Kept one canonical statement with expansion in references/. Three rules the root file carried had no skill that covered them, so they moved into cmux-architecture rather than being lost: SPM package group folders with check-workspace-package-groups.py, the Package.resolved tracking policy, and "feature flag means a remote PostHog runtime flag" via CmuxFeatureFlags. The cmuxTests pbxproj wiring requirement was promoted from a reference file into cmux-testing, and the shortcut policy moved into cmux-keyboard-shortcuts with its duplicate removed from cmux-localization. Stale references fixed: - `cd cmuxd && zig build` referenced a directory with zero tracked files; the daemon is Go at daemon/remote/cmd/cmuxd-remote. - Changelog page is web/app/[locale]/(landing)/docs/changelog/page.tsx, and configuration is under the same (landing) segment. - Package CmuxSocketControl does not exist; the real one is CmuxControlSocket, cited twice as the exemplar to copy. - Two rg commands in the localization audit were double-escaped and passed `--` as if it were a glob flag, so they matched nothing and silently passed the audit. Left untouched: the auto-generated cmux-settings reference files, which would drift from their generator. * Dedupe release slash commands release.md, release-local.md, and release-nightly.md each restated the same version-bump and changelog procedure (450 -> 192 lines total). release.md is now the canonical command doc holding the shared prep, changelog guidelines, and contributor-credit format; the other two state only their delta (local build-sign-upload.sh path, and no-PR direct-to- main path with the homebrew-cmux submodule pointer commit). Stale and incorrect instructions fixed: - All three pointed at docs-site/content/docs/changelog.mdx. There is no docs-site/ in the repo; the changelog page renders from CHANGELOG.md. - release.md said to hand-edit 'typically 4 occurrences' of MARKETING_VERSION in project.pbxproj. That leaves CURRENT_PROJECT_VERSION stale, which Sparkle requires to be monotonic and which release-pretag-guard.sh rejects. Unified on scripts/bump-version.sh, which bumps both. - Documented build-sign-upload.sh --allow-overwrite, which matters because pushing a v* tag also fires release.yml, so a local upload can race CI for the same assets. * Address review findings on submodule remotes and build links - tagged-builds.md still told contributors to build chat links from the absolute .app path with a file:// URL, contradicting the rule in CLAUDE.md that chat links use http://127.0.0.1:17320/<tag>. - cmux-ghostty said 'origin is upstream and manaflow is the fork' and pushed to a 'manaflow' remote. .gitmodules points every submodule at manaflow-ai/*, and no checkout has a 'manaflow' remote, so those commands would fail. Both the skill and submodule-safety.md now tell you to check git remote -v, and document adding an explicit 'upstream' remote for syncing from ghostty-org. - submodule-safety.md verified ancestry against <remote>/main even when a feature branch was pushed. Now checks the branch actually pushed. - release.md credited @lawrencechen; the account is @lawrencecchen. Skipped, with reasons: the ~/.agents/skills vs ~/.codex/skills split in cmux-customization is the documented convention (normal install vs skills.sh install), matching cmux-diagnostics. Adding per-entry attribution to the un-credited changelog example entry would contradict the policy three lines above it, which exempts core-team work. | 1 个月前 | |
Document package/file/doc/test policies and add Cursor rules shim (#4919) Adds four CLAUDE.md sections capturing engineering policies that are currently informal: Package architecture (ergonomic, no cycles, broad domain responsibilities, leaf-first extraction), File organization (one major type per file), Documentation (Swift-DocC triple-slash on every public symbol in Packages/), and Test framework (Swift Testing default for new tests, XCUITest for UI tests only). Adds .cursor/rules/main.mdc so Cursor CLI picks up the same CLAUDE.md ruleset that Claude Code reads directly and Codex CLI reads through the existing AGENTS.md -> CLAUDE.md symlink. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> | 3 个月前 | |
Add cmux TUI product and docs pages (#9049) * Add cmux TUI product and docs pages * Use real macOS capture for cmux TUI * Match TUI page to home design * Add native TUI installers and Hunk capture * Move TUI installer into hero * Highlight TUI install commands * Compact TUI install controls * Harden TUI installer delivery | 1 个月前 | |
Remove Swift file length budget (#8125) Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> | 1 个月前 | |
Regenerate app icons from Icon Composer (#1005) * Add Apple Icon Composer source file Store the .icon project file in design/cmux.icon so the icon can be edited in Icon Composer and regenerated from source. * Regenerate all app icons from Icon Composer Use ictool to render light/dark icon PNGs from the .icon source file with proper macOS padding. Updates AppIcon, AppIcon-Debug (with DEV banner), AppIconLight, and AppIconDark imagesets. Adds AppIcon.icon to the Xcode project as a resource. * Address review: fix trailing newline, remove .icon from bundle - Add trailing newline to icon.json files - Remove AppIcon.icon from Copy Bundle Resources (design-time only) | 6 个月前 | |
Merge origin/main into issue-4392-antigravity-cli | 3 个月前 | |
Fix resumed Codex Teams subagent pane backfill (#9180) * test: cover resumed Codex subagent pane backfill * fix: open resumed Codex subagent panes * test: deduplicate Codex resume fixture tracking * fix: harden Codex Teams watcher diagnostics * test: pin Codex watcher diagnostic locale --------- Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> | 1 个月前 | |
Organize SPM packages into Packages/{Shared,iOS,macOS}/ so the folder tree mirrors the workspace groups (#6278) Every Swift package now lives physically under exactly one group directory (Packages/Shared, Packages/iOS, Packages/macOS), so the repo's directory tree and the root workspace's group columns are the same shape. Opening cmux.xcworkspace shows each package under the Shared / iOS / macOS group matching the folder it lives in. Folder is the source of truth. Group = which app(s) consume the package: both apps -> Shared, iOS app only -> iOS, macOS app only -> macOS. check-workspace-package-groups.py mirrors the folders directly; --write regenerates the workspace, --check (in CI, beside check-pbxproj) fails on drift. All boundary-crossing relative paths were rewritten to keep the build intact: inter-package deps same group `../Name` / cross group `../../<Group>/Name`; escaping paths gain one level (vendor `../../../vendor/...`, GhosttyKit `../../../GhosttyKit.xcframework`); macOS project relativePaths, ios/cmuxPackage and Examples deps + project relativePaths, the file-length budget, the iOS conventions lint scopes, the namespace-type baseline, the test-ios change globs, the ci.yml per-package `swift test` loop (now resolves the group dir), and doc/skill references all updated to the nested paths. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 2 个月前 | |
Move diff viewer backend boundary to a Rust sidecar (#7804) * Add Rust diff viewer sidecar * Harden diff sidecar request handling * Close sidecar review gaps * Finish sidecar build and retry integration * Gate sidecar transport and webview checks * Remove sidecar setup and localization gaps * Extract diff sidecar process boundary * Use stdio for native diff sidecar transport * Satisfy Swift file length guard * Make custom scheme test deterministic * Address sidecar review findings * Test incremental diff tree source reuse * Make diff tree streaming linear * Verify diff correctness and streaming performance * Fix diff sidecar review regressions * Harden diff sidecar stdio RPC * Test bounded large diff rendering * Bound large diff UI updates * Harden large diff navigation * Fix diff sidecar isolation warning * Test mobile diff drawer close control * Make mobile diff drawer opaque * Harden mobile diff drawer dismissal * Refactor diff viewer bridge ownership * Preserve diff sidecar pipe ownership * Load diff sessions lazily through Rust * Keep Rust diff sessions alive while rendering * Split diff sidecar helpers from legacy files * Close diff sessions before page navigation * Close diff sessions before navigating * Track active diff sessions through navigation * Refresh generated diff viewer bundle * Keep diff source switching responsive * Open typed diff sessions in place * Update diff CLI file budget * Extract typed diff viewer writer * Build typed diff writer in CLI target * Expose shared diff shortcut payload * Share typed diff writer model types * Allow typed diff fallback input replacement * Open diff loading shell before asset setup * Bound typed branch base resolution * Avoid duplicate diff theme registration * Test custom-scheme asset fetch decoding * Decode deflated assets for diff scheme * Test cancellation of stale diff streams * Cancel stale diff sessions and cap patch writes * test: cover diff sidecar review regressions * fix: bound diff sidecar lifecycle * test: cover sidecar cancellation cleanup * fix: clean up cancelled sidecar process groups * test: require race-free sidecar process groups * fix: handshake sidecar process group startup * test: cover cancellation after patch rename * fix: retain cleanup ownership through registration * fix: bound sidecar startup and shutdown * test: cover branch picker repository switches * fix: close final sidecar lifecycle gaps * test: cover same-repo branch base changes * fix: preserve process group identity through shutdown * Make stale branch picker test state-driven * Test Last Turn switching and abandoned sidecar sessions * Keep typed diff sources and manifests recoverable * Test typed diff selector composition * Compose typed diff selector state * Rebuild diff webview assets * Test orphan cleanup and Last Turn repo switching * Close typed diff lifecycle gaps * Test pending cancellation and rotating orphan cleanup * Bound pending and remote diff resources * Cap sidecar queue and index temp cleanup * Bound server sessions and retain patch ownership * Make patch ownership and HTTP encoding durable * Test empty branch base selection * Keep empty branch and pending patch recovery available * Test branch base survives source switching * Preserve selected branch base across source switches * Retain generated patch ownership until lifecycle cleanup * Serialize token session publication * Keep concurrent diff sessions independently owned * Reconcile session cleanup with manifest lifecycle * Make session publication cancellation safe * Scope cancellation and close transactions correctly * Authorize session close by manifest ownership * Close discarded diff sessions safely * Cancel superseded diff sessions safely * Reserve diff session resources atomically * Protect active diff session patches * Preserve active typed diff sessions * Lease active diff sidecar sessions * Journal diff session resource ownership * Bound diff session recovery artifacts * Harden diff sidecar production artifact * Fix POSIX lock calls on Xcode 26.5 * Fix app-side lease locking on Xcode 26.5 * test: cover typed diff direct page lifecycle * fix: open typed diff session page directly * Fix sidecar verification for spaced paths | 1 个月前 | |
iOS: key all per-Mac state by pairing (device id + instance tag) so sibling builds are first-class (#8936) * feat(ios): stamp workspace and notification rows with the pairing instance tag Workspace and notification payloads carry no Mac identity; the phone attributes rows to the connection they arrived on. That attribution now includes the pairing's app-instance tag: foreground rows are stamped with the active connection's tag in setForegroundWorkspaceState, secondary rows with the subscription's proven tag, and notification feed items with the pairing behind the feed target. Aggregated rows carry macInstanceTag, per-pairing row ids include the tag so sibling builds' workspaces cannot collide, and the feed item identity includes the tag so sibling notifications never dedupe into one row. Works for every existing Mac; no wire change needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ios): aggregate workspaces and notifications per pairing Sibling builds of one Mac are now separate aggregation targets: the one-build-per-device coalesce is removed from secondary candidate selection, the foreground exclusion is pairing-exact so the sibling of the connected build stays a candidate, and subscriptions, per-Mac workspace state, and notification-feed maps are keyed by pairing id (legacy untagged pairings keep device keys). Promotion resolves the exact pairing and tagged switch requests can take the promotion fast path. Workspace mutations route by the row's pairing, opens and notification taps switch to the row's exact build, workspace counts and the machine filter match per build (legacy untagged rows keep matching device-wide), avatar colors stay per physical device, and hiding a pairing tears down exactly that pairing's subscription and feed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: cover sibling-build separation across aggregation, filter, and feed Aggregation ordering now iterates aggregate KEYS (pairing ids since the re-key) instead of state device ids, which returned duplicate device ids for sibling builds and dropped their rows; sibling entries order deterministically by instance tag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ios): keep selection scope self-contained for tag comparison Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ios): address review findings on pairing-scoped feed routing Notification taps compare the exact pairing so a sibling build's notification on the foreground device still switches builds; the aggregate feed status compares owner keys instead of device ids; snapshot stamping derives the tag from the owner key itself so sibling items never dedupe even without a live subscription (covered by a new tagged-owner-key test); hiding the foreground pairing also drops its device-keyed feed snapshot when a sibling stays visible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Restore main's ghostty submodule pin The merge-conflict resolutions staged the worktree's stale ghostty gitlink via git add -A, silently reverting main's pin bump; this branch carries no ghostty changes, so main's pin is authoritative. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ios): close autoreview findings on the pairing key-space migration Secondary refresh validation now checks the subscription under its pairing key instead of the device id, which was tearing down every healthy tagged subscription on refresh. Device-only promotion requests fail closed when sibling builds are both live instead of promoting an arbitrary one. Tagged notification items never fall back to the bare device key, so an offline pairing's mutation no-ops instead of hitting a sibling with a colliding id. Hiding the foreground pairing also removes its device-keyed workspace entry when a sibling stays visible. Workspace-create gating uses the live connection's instance tag rather than the stored isActive flag, which lags promotion. Notification feed scoping preserves the selected build (entry-aware item matching), and dismiss-outbox routing sends only through an unambiguous client for the device, deferring while sibling builds are both live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ios): close round-two review findings on pairing identity boundaries Legacy untagged rows on the foreground device are excluded from secondary aggregation (their pairing id is the foreground's own aggregate key and would overwrite it). The picker's switch decision and the workspace-groups gate compare the live foreground pairing instead of the stored isActive flag, which lags promotion. Computers-screen status lookups query the pairing key first so tagged secondaries keep their connection dot. Notification availability matches the exact selected pairing for every signal, and the alias-selection test asserts the pairing-formed filter entries with sibling exclusion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ios): close round-three findings on legacy identity and promotion Secondary rows are stamped with the subscription's STORED pairing identity so reconstructed owner keys always find their subscription, including upgraded-legacy pairings that adopted a tag at auth time. Device-only promotion requires the device to have a single stored pairing, not merely a single live one, so a reconnect meant for an offline sibling never promotes the other build. Exact pairing scopes exclude unknown-tag rows (they stay under device entries and All Computers). Promotion clears the promoted pairing's feed bookkeeping so the foreground refetch under the device key cannot duplicate rows, and the workspace-detail reconnect passes the row's tag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ios): keep failure downgrades and retained-state pruning pairing-keyed An unreachable sibling's establish failure marks its own pairing entry unavailable instead of the device key (which can be the live foreground sibling), and retained pairing-keyed workspace states with no live subscription are pruned when no longer wanted so a pairing reconnected as foreground via the dial path cannot duplicate its rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ios): dismiss routing requires a single stored sibling Counting live clients was not enough: the emitting build may be offline while a sibling is the sole live candidate, and Mac-local notification ids can collide across builds. Device-scoped dismisses now route only when the device has one stored pairing at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ios): reset foreground feed bookkeeping on sibling build switches The foreground feed lives under the shared device key, so switching to a sibling build left the previous build's snapshot and revision in place and rejected the new build's lower revisions as stale. Both the promotion and dial connect paths now clear the device-keyed feed state when the foreground instance tag changes on the same device. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ios): close remaining round-four findings on feed and status identity Notification-open navigation matches workspaces and surfaces by the item's exact pairing so colliding Mac-local ids on a sibling build fail closed instead of navigating to the wrong workspace. The connection status rollup never overwrites an exact pairing entry and rolls the foreground's device-keyed status only onto its own pairing representative, so an offline sibling can no longer render green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * WIP: typed MacPairingKey owner-key re-key (registry + composite core; not compiling yet) * WIP: typed owner-key re-key compiles (composite, promotion, feed, hidden, actions) * WIP: typed key test-target compiles; MacWorkspaceState.id pairing-unique * WIP: pool suites 98/111; feed reset semantics reapplied; device-level drain admission * WIP: pool+sibling suites converging; per-pairing candidate selection + drain-path replacement retirement * Restore deeplink collision test hints eaten by bulk rewrite * Fix review round 6: sibling promotion demotes previous focus by owner key, feed target ownerKey consistency, offline foreground key captured before identity clear, pairing-aware reconnect, exact-pairing retained-snapshot pruning * Fix review round 7: exact-pairing reconnect decisions, sibling-ambiguity fail-closed deeplink lookups, fail-closed tagged create gate, feed completion by owner key * Fix review round 8: openWorkspace routes by exact pairing, group/reorder gate requires exact foreground pairing, demoted-foreground feed re-keys to pairing * Fix review round 9: foreground terminal lookups scope by live pairing; known-tag row resolution in list apply and create * Fix review round 10: pairing-exact connected-refresh target, live-identity hide disconnect, tag-aware selection remap, allocation-free exact terminal lookup * Fix review round 11: fresh-dial takeover clears pairing-keyed feed source; preparse machine scope entries for row projection * Fix review round 12: foreground-scoped raw-input lookup with unowned-row fallback, exact-lookup no global fallback, ambiguous device-only switch fails closed, tagged secondary feed bootstrap by pairing id, hide authority requires proven live tag * Fix review round 13: untagged selections match only untagged live foreground; recovery flags attribute to the exact recovering pairing --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
Add settings sidebar shell (#3244) * Add settings sidebar shell * Add settings shell prototype app * Migrate settings to native shell layout * Align settings shell with prototype * Remove settings titlebar debug hooks * Use SwiftUI scene for settings window * Improve settings search results * Remove settings detail title * Tune settings window default size * Highlight settings search results * Make settings search highlight persistent * Fade settings search highlight * Expose settings json in settings * Split settings navigation helpers * Limit settings highlight to search clicks * Make settings sidebar scroll latest-wins * Use wand icon for automation settings * Move workspace colors below shortcuts * Address settings window review feedback * Fix settings shortcut routing test cleanup * Fix settings window activation warning --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> | 4 个月前 | |
Fix resumed Codex Teams subagent pane backfill (#9180) * test: cover resumed Codex subagent pane backfill * fix: open resumed Codex subagent panes * test: deduplicate Codex resume fixture tracking * fix: harden Codex Teams watcher diagnostics * test: pin Codex watcher diagnostic locale --------- Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> | 1 个月前 | |
Fix sidebar row clipping during height-changing reorder (#9189) * Fix sidebar row clipping during reorder * test: preserve sidebar viewport during height-changing reorder * fix: preserve sidebar viewport during atomic reorder reload * test: preserve sidebar edits during atomic reorder reload * fix: preserve sidebar edits during atomic reorder reload * refactor: satisfy sidebar review policy | 1 个月前 | |
Fix Claude bridge branch and resume failures (#7882) (#7900) * test: cover inherited Claude bridge sessions * Fix Claude bridge session identity leakage * Fix OMC Claude session identity inheritance * Harden independent Claude launch boundaries * Generate Claude launch environment policy * Preserve Claude Teams respawn trust * Run Claude launch policy test in CI * Preserve Claude auto-naming trust context | 2 个月前 | |
Compact CLAUDE.md and skills docs (#8990) * Compact CLAUDE.md and skills docs Cut ~2k lines of duplication without dropping actionable rules. CLAUDE.md (267 -> 103): `reload.sh --tag` was explained four separate times; now once. Dropped the Ghostty submodule and Release sections, which restated the cmux-ghostty and cmux-release skills, and removed the file:// deeplink block, which contradicted the rule that chat output uses http://127.0.0.1:17320/<tag> and never a file:// URL. Pitfalls compressed from paragraphs to one line each, pointing at the owning skill. skills/ (4849 -> 2819 across 20 skills): the dominant waste was SKILL.md files restating their own references/ verbatim. Kept one canonical statement with expansion in references/. Three rules the root file carried had no skill that covered them, so they moved into cmux-architecture rather than being lost: SPM package group folders with check-workspace-package-groups.py, the Package.resolved tracking policy, and "feature flag means a remote PostHog runtime flag" via CmuxFeatureFlags. The cmuxTests pbxproj wiring requirement was promoted from a reference file into cmux-testing, and the shortcut policy moved into cmux-keyboard-shortcuts with its duplicate removed from cmux-localization. Stale references fixed: - `cd cmuxd && zig build` referenced a directory with zero tracked files; the daemon is Go at daemon/remote/cmd/cmuxd-remote. - Changelog page is web/app/[locale]/(landing)/docs/changelog/page.tsx, and configuration is under the same (landing) segment. - Package CmuxSocketControl does not exist; the real one is CmuxControlSocket, cited twice as the exemplar to copy. - Two rg commands in the localization audit were double-escaped and passed `--` as if it were a glob flag, so they matched nothing and silently passed the audit. Left untouched: the auto-generated cmux-settings reference files, which would drift from their generator. * Dedupe release slash commands release.md, release-local.md, and release-nightly.md each restated the same version-bump and changelog procedure (450 -> 192 lines total). release.md is now the canonical command doc holding the shared prep, changelog guidelines, and contributor-credit format; the other two state only their delta (local build-sign-upload.sh path, and no-PR direct-to- main path with the homebrew-cmux submodule pointer commit). Stale and incorrect instructions fixed: - All three pointed at docs-site/content/docs/changelog.mdx. There is no docs-site/ in the repo; the changelog page renders from CHANGELOG.md. - release.md said to hand-edit 'typically 4 occurrences' of MARKETING_VERSION in project.pbxproj. That leaves CURRENT_PROJECT_VERSION stale, which Sparkle requires to be monotonic and which release-pretag-guard.sh rejects. Unified on scripts/bump-version.sh, which bumps both. - Documented build-sign-upload.sh --allow-overwrite, which matters because pushing a v* tag also fires release.yml, so a local upload can race CI for the same assets. * Address review findings on submodule remotes and build links - tagged-builds.md still told contributors to build chat links from the absolute .app path with a file:// URL, contradicting the rule in CLAUDE.md that chat links use http://127.0.0.1:17320/<tag>. - cmux-ghostty said 'origin is upstream and manaflow is the fork' and pushed to a 'manaflow' remote. .gitmodules points every submodule at manaflow-ai/*, and no checkout has a 'manaflow' remote, so those commands would fail. Both the skill and submodule-safety.md now tell you to check git remote -v, and document adding an explicit 'upstream' remote for syncing from ghostty-org. - submodule-safety.md verified ancestry against <remote>/main even when a feature branch was pushed. Now checks the branch actually pushed. - release.md credited @lawrencechen; the account is @lawrencecchen. Skipped, with reasons: the ~/.agents/skills vs ~/.codex/skills split in cmux-customization is the documented convention (normal install vs skills.sh install), matching cmux-diagnostics. Adding per-entry attribution to the un-credited changelog example entry would contradict the policy three lines above it, which exempts core-team work. | 1 个月前 | |
Reclaim hidden Ghostty renderer memory (#8998) * Add five-tab renderer memory regression test * Reclaim hidden terminal renderers by default * Pin shared Metal pipeline Ghostty build * Pin final Ghostty memory build * Pin competitive Ghostty memory build * Test renderer reclamation catalog defaults * Use catalog renderer reclamation defaults * test: require atomic first renderer presentation * fix: make first renderer presentation atomic * fix: resolve renderer defaults through catalog * Exercise renderer defaults through UserDefaults * Pin forced renderer rebuild Ghostty head * Pin forced rebuild GhosttyKit checksum * Test forced renderer rebuild presentation * Preserve forced renderer rebuild presentation * Make renderer defaults regression test throwable * Pin merged Ghostty renderer reclamation head * Pin final GhosttyKit checksum * Pin reviewed Ghostty renderer retry fix * Pin reviewed Ghostty shader cache follow-up * Add red test for Ghostty Zig version drift * Derive Zig version from pinned Ghostty * Run Ghostty Zig version drift test in CI * Test all Ghostty Zig workflow consumers * Synchronize Ghostty Zig workflows * Test Ghostty Zig helper as TestFlight input * Track Ghostty Zig helper in TestFlight inputs * Pin Ghostty shader failure backoff * Pin Ghostty shader attempt backoff * test: require renderer reclaim deadline scheduling * test: initialize linked Ghostty runtime * fix: schedule renderer reclaim at idle deadlines * test: retain synthetic Ghostty argv * fix: coalesce renderer visibility evaluation * test: retain Ghostty runtime argv * fix: wire renderer visibility coalescing * Pin integrated Ghostty mailbox fix * refactor: inject renderer reclaim scheduler inputs * test: exercise renderer reclaim scheduler lifecycle * fix: bound renderer visibility scheduling * test: look up linked Ghostty runtime dynamically * Validate per-consumer Ghostty Zig wiring * test: require fail-closed Ghostty Zig workflows * fix: fail closed on Ghostty Zig resolution * fix: make renderer scheduling verification deterministic * test: coalesce staggered renderer reclaim deadlines * fix: coalesce renderer reclaim deadlines * Update Ghostty renderer retry artifact * test: measure five-tab renderer memory * test: cover compatible Zig patch releases * fix: accept compatible Zig patch releases * refactor: separate renderer realization surface seam --------- Co-authored-by: Austin Wang <38676809+austinywang@users.noreply.github.com> Co-authored-by: austinpower1258 <austinwang115@gmail.com> | 1 个月前 | |
Fix sidebar row clipping during height-changing reorder (#9189) * Fix sidebar row clipping during reorder * test: preserve sidebar viewport during height-changing reorder * fix: preserve sidebar viewport during atomic reorder reload * test: preserve sidebar edits during atomic reorder reload * fix: preserve sidebar edits during atomic reorder reload * refactor: satisfy sidebar review policy | 1 个月前 | |
iOS: stream Mac browser panes to the phone (pixel-perfect, interactive, dialogs mirrored) (#8298) * docs: iOS browser streaming design * Add mobile browser streaming wire protocol * Add Mac mobile browser stream host * Fix stream session compile (nonisolated encoder init) and momentum end phase * Fix keyCode method shadowing in SyntheticKeyEventFactory * Add iOS browser stream surface package * Add browser stream RPC client plumbing * Wire browser streams through the mobile shell * Integrate browser streams into mobile shell UI * Beacon: detect canvas/WebGL painting via requestAnimationFrame wrap * ci: reload-build gains an ios-simulator platform Builds the unsigned simulator .app and uploads it as an artifact, for callers whose local xcodebuild is unavailable; the sim bundle installs directly via simctl. * ci: build the ios-simulator app arm64-only GhosttyKit's simulator slice is arm64-only, so the generic destination's x86_64 half fails at link; every target simulator is arm64. * Fix display link teardown for Swift 6 nonisolated deinit * Fix frame stall via store-owned decode pipeline; move chrome to bottom floating bar * Self-heal browser stream: force restart past dedupe on recovery, unanswered-input watchdog, keyboard-pinned bottom bar * Add mobile browser dialog wire model and broker * Mirror Mac browser dialogs over mobile RPC * Render mirrored browser dialogs on iOS * Wire mobile browser dialog Mac sources into Xcode project * Capture owner explicitly in basic-auth startPrompt closure * Stack browser dialog buttons vertically for 3+ or long labels * Reserve bottom bar space so chrome never occludes streamed page content * Take main's reconnect route-isolation test (recoveryTask removed by Iroh fix) * Browser bar: always-visible standard controls, drop collapse pill + confusing X/chevron; stop stream on surface exit * Add mobile browser viewport RPC DTOs * Reflow Mac browser streams to phone viewport * iOS: report phone viewport to reflow the streamed Mac browser * Fix streamed browser white-out: force repaint after viewport reflow so idle pages don't capture a blank frame * White-out fix v2: real two-frame scroll repaint nudge + settle-capture burst after reflow * Replace iOS tab switcher surface * Fix iOS switcher integration and verification * Test persistent browser render host portal ownership * Share persistent browser offscreen render hosting * Capture mobile browser streams in persistent render host * Fix switcher initial positioning and accessibility * Test switcher reopening after browser selection * Reset switcher state for each presentation * iOS browser stream: mirror phone frames in the Mac pane instead of blanking it While a browser pane streams to the phone, the live WKWebView renders in the offscreen host at phone width, so the Mac pane went fully blank. Show a read-only, letterboxed, click-through mirror of the exact frames the phone receives (fed from the same capture in MobileBrowserStreamSession at the same cadence), added to the pane's superview on stream start and removed on teardown when the full-width live web view returns. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Speed up browser stream capture on the offscreen render host Continuous JPEG frames were snapshotting with afterScreenUpdates:true, which blocks each takeSnapshot on the host window's screen-update cycle. The stream's offscreen render host lives off all screens at alpha ~0, where macOS throttles that cycle hard, so capture was capped to a few fps: the phone showed "super slow" streaming that barely moved on scroll. Snapshot continuous JPEG frames with afterScreenUpdates:false instead. That captures the currently committed render, which already reflects the new scroll offset, without waiting on the throttled cycle; the dirty loop re-captures to stay current. The rare lossless PNG settle frame keeps afterScreenUpdates:true for a pixel-perfect rest state. Add DEBUG per-capture instrumentation (capture ms, encode ms, byte size, pixel size, unacked count) so stream throughput is measurable from the debug log and capture-bound vs flow-controlled is distinguishable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Revert "Reset switcher state for each presentation" This reverts commit ed9d5f8b46021d585ac3c325b36b2e00652f9d20. * Revert "Test switcher reopening after browser selection" This reverts commit 2bfebb3346e3bab646790d3827ad99bd0cde0f31. * Revert "Fix switcher initial positioning and accessibility" This reverts commit 607ef3392418c2b05c46ceef44a1adeda9aca94c. * Revert "Fix iOS switcher integration and verification" This reverts commit 9cfb18175026c6ea499840aa470379ab0cc051b1. * Revert "Replace iOS tab switcher surface" This reverts commit 89105d342df1d6e45c64099d993ddb22cd5a25e9. * Revert "ci: build the ios-simulator app arm64-only" This reverts commit f5e9324940cbbdb11af6f330ac092b1abc6d632d. * Revert "ci: reload-build gains an ios-simulator platform" This reverts commit 42b65b2300f90cc27a7d01975b684775bd3d9892. * Scope PR to browser streaming: drop switcher residue from title menu and string catalog Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Test replayed browser input requests a stream capture * Keep the stream render host visible to WebKit: on-screen floating window, input-replay dirty, event-driven scroll beacon The persistent render host window sat at (-100000,-100000); AppKit reports a window with no on-screen portion as fully occluded, and WebKit suspends requestAnimationFrame and degrades trusted-event hit testing for occluded hosts. The rAF-throttled dirty beacon therefore never fired during a scroll gesture (one frame per gesture, captured after gesture end) and replayed taps intermittently hit a stale tree and never navigated. Host window now anchors on-screen (bottom-trailing, >=64pt visible, .floating so ordinary windows cannot occlude it) while staying imperceptible (1% alpha, click-through, non-activating). Hardening: every replayed input batch marks the session dirty directly, and the beacon posts scroll/wheel dirt from the event listener with a 16ms throttle instead of waiting for a rAF tick. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Bind the browser-stream keyboard button to real keyboard visibility The button showed the input proxy's focus intent, so a keyboard raised by the address field or a dialog's text field left it stuck on 'Show Keyboard'. The glyph now binds to MobileKeyboardVisibilityObserver (UIKit keyboard notifications); tapping while the keyboard is up resigns whichever responder raised it (shared dismissMobileKeyboard, moved to CmuxMobileSupport) and releases the proxy's focus reasons via the policy's new explicit hide, which never flips into a focus request the way toggling would. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Give dialog text fields a visible input well The dialog card is glass, so the fields' glass background vanished into it and prompt/basic-auth inputs read as labels. Fields now sit in a filled rounded well with a hairline border, the same fill language as the bottom bar's address field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> | 1 个月前 | |
Fix sidebar row clipping during height-changing reorder (#9189) * Fix sidebar row clipping during reorder * test: preserve sidebar viewport during height-changing reorder * fix: preserve sidebar viewport during atomic reorder reload * test: preserve sidebar edits during atomic reorder reload * fix: preserve sidebar edits during atomic reorder reload * refactor: satisfy sidebar review policy | 1 个月前 | |
Merge remote-tracking branch 'origin/main' into issue-8997-memory-growth-panics # Conflicts: # Sources/DockSplitStore+Reset.swift # Sources/DockSplitStore.swift # Sources/Workspace+PanelLifecycle.swift # Sources/Workspace.swift | 1 个月前 | |
Validate production Iroh trust in release gates (#9118) * test(iroh): expose retained production gate identity * fix(iroh): validate production gate trust profile * test(projects): cover synchronized workspace groups * fix(projects): support synchronized workspace groups | 1 个月前 | |
fix: preserve claude-teams tmux routing (#9033) * test: preserve claude teams tmux launch context * fix: preserve claude teams tmux routing * fix: scope claude teams tmux routing * fix: harden claude teams launch routing * fix: close tmux compat review gaps * fix: require inherited tmux launch identity * fix: validate managed launcher context * fix: preserve non-launch management commands * fix: cover managed launcher aliases * fix: validate remote managed launch context * test: cover managed teams launch invariants * fix: keep managed teams shims authoritative * fix: preserve managed launcher compatibility * fix: harden managed launch classification * fix: reject ambiguous Claude debug filters * fix: require context for session hosts * fix: keep managed child identity coherent * test: migrate OMO plugin without a session * fix: preserve non-launch command compatibility * fix: align managed launch policy ownership * test: cover moved teams launch identity * fix: honor shell snapshot argument contract * fix: preserve managed launcher operator commands * fix: preserve managed launcher shell contracts * fix: close managed launcher review gaps * test: keep focused cmux sockets below AF_UNIX limits * fix: require launch context for ultrareview * fix: preserve managed launcher compatibility * fix: preserve Claude passthrough arguments * fix: preserve managed provider passthrough * test: cover managed launcher operator commands * fix: preserve managed launcher team operators * test: cover nested Codex Teams help * fix: pass nested Codex Teams help through * test: cover Claude Teams shell wrapper reentry * fix: harden managed Teams launch identity * test: consolidate managed Teams regressions * test: cover managed provider administrative help * fix: preserve managed provider administrative help * test: cover Claude forward subagent text flag * fix: recognize Claude forward subagent text flag * test: cover OMO subcommand global options * fix: preserve OMO subcommand global options * test: keep Claude import surface-bound * fix: require surface context for Claude import * test: cover Codex Teams help subcommand * fix: pass Codex Teams help through * fix: preserve managed wrapper root help * fix: apply retry binding predicate to both phases * test: handle teammate column equalization * test: model managed tmux focus changes * test: expect tmux-compatible pane IDs * fix: capture RPC session actor immutably | 1 个月前 | |
Regenerate app icons from Icon Composer (#1005) * Add Apple Icon Composer source file Store the .icon project file in design/cmux.icon so the icon can be edited in Icon Composer and regenerated from source. * Regenerate all app icons from Icon Composer Use ictool to render light/dark icon PNGs from the .icon source file with proper macOS padding. Updates AppIcon, AppIcon-Debug (with DEV banner), AppIconLight, and AppIconDark imagesets. Adds AppIcon.icon to the Xcode project as a resource. * Address review: fix trailing newline, remove .icon from bundle - Add trailing newline to icon.json files - Remove AppIcon.icon from Copy Bundle Resources (design-time only) | 6 个月前 | |
iOS: stream Mac browser panes to the phone (pixel-perfect, interactive, dialogs mirrored) (#8298) * docs: iOS browser streaming design * Add mobile browser streaming wire protocol * Add Mac mobile browser stream host * Fix stream session compile (nonisolated encoder init) and momentum end phase * Fix keyCode method shadowing in SyntheticKeyEventFactory * Add iOS browser stream surface package * Add browser stream RPC client plumbing * Wire browser streams through the mobile shell * Integrate browser streams into mobile shell UI * Beacon: detect canvas/WebGL painting via requestAnimationFrame wrap * ci: reload-build gains an ios-simulator platform Builds the unsigned simulator .app and uploads it as an artifact, for callers whose local xcodebuild is unavailable; the sim bundle installs directly via simctl. * ci: build the ios-simulator app arm64-only GhosttyKit's simulator slice is arm64-only, so the generic destination's x86_64 half fails at link; every target simulator is arm64. * Fix display link teardown for Swift 6 nonisolated deinit * Fix frame stall via store-owned decode pipeline; move chrome to bottom floating bar * Self-heal browser stream: force restart past dedupe on recovery, unanswered-input watchdog, keyboard-pinned bottom bar * Add mobile browser dialog wire model and broker * Mirror Mac browser dialogs over mobile RPC * Render mirrored browser dialogs on iOS * Wire mobile browser dialog Mac sources into Xcode project * Capture owner explicitly in basic-auth startPrompt closure * Stack browser dialog buttons vertically for 3+ or long labels * Reserve bottom bar space so chrome never occludes streamed page content * Take main's reconnect route-isolation test (recoveryTask removed by Iroh fix) * Browser bar: always-visible standard controls, drop collapse pill + confusing X/chevron; stop stream on surface exit * Add mobile browser viewport RPC DTOs * Reflow Mac browser streams to phone viewport * iOS: report phone viewport to reflow the streamed Mac browser * Fix streamed browser white-out: force repaint after viewport reflow so idle pages don't capture a blank frame * White-out fix v2: real two-frame scroll repaint nudge + settle-capture burst after reflow * Replace iOS tab switcher surface * Fix iOS switcher integration and verification * Test persistent browser render host portal ownership * Share persistent browser offscreen render hosting * Capture mobile browser streams in persistent render host * Fix switcher initial positioning and accessibility * Test switcher reopening after browser selection * Reset switcher state for each presentation * iOS browser stream: mirror phone frames in the Mac pane instead of blanking it While a browser pane streams to the phone, the live WKWebView renders in the offscreen host at phone width, so the Mac pane went fully blank. Show a read-only, letterboxed, click-through mirror of the exact frames the phone receives (fed from the same capture in MobileBrowserStreamSession at the same cadence), added to the pane's superview on stream start and removed on teardown when the full-width live web view returns. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Speed up browser stream capture on the offscreen render host Continuous JPEG frames were snapshotting with afterScreenUpdates:true, which blocks each takeSnapshot on the host window's screen-update cycle. The stream's offscreen render host lives off all screens at alpha ~0, where macOS throttles that cycle hard, so capture was capped to a few fps: the phone showed "super slow" streaming that barely moved on scroll. Snapshot continuous JPEG frames with afterScreenUpdates:false instead. That captures the currently committed render, which already reflects the new scroll offset, without waiting on the throttled cycle; the dirty loop re-captures to stay current. The rare lossless PNG settle frame keeps afterScreenUpdates:true for a pixel-perfect rest state. Add DEBUG per-capture instrumentation (capture ms, encode ms, byte size, pixel size, unacked count) so stream throughput is measurable from the debug log and capture-bound vs flow-controlled is distinguishable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Revert "Reset switcher state for each presentation" This reverts commit ed9d5f8b46021d585ac3c325b36b2e00652f9d20. * Revert "Test switcher reopening after browser selection" This reverts commit 2bfebb3346e3bab646790d3827ad99bd0cde0f31. * Revert "Fix switcher initial positioning and accessibility" This reverts commit 607ef3392418c2b05c46ceef44a1adeda9aca94c. * Revert "Fix iOS switcher integration and verification" This reverts commit 9cfb18175026c6ea499840aa470379ab0cc051b1. * Revert "Replace iOS tab switcher surface" This reverts commit 89105d342df1d6e45c64099d993ddb22cd5a25e9. * Revert "ci: build the ios-simulator app arm64-only" This reverts commit f5e9324940cbbdb11af6f330ac092b1abc6d632d. * Revert "ci: reload-build gains an ios-simulator platform" This reverts commit 42b65b2300f90cc27a7d01975b684775bd3d9892. * Scope PR to browser streaming: drop switcher residue from title menu and string catalog Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Test replayed browser input requests a stream capture * Keep the stream render host visible to WebKit: on-screen floating window, input-replay dirty, event-driven scroll beacon The persistent render host window sat at (-100000,-100000); AppKit reports a window with no on-screen portion as fully occluded, and WebKit suspends requestAnimationFrame and degrades trusted-event hit testing for occluded hosts. The rAF-throttled dirty beacon therefore never fired during a scroll gesture (one frame per gesture, captured after gesture end) and replayed taps intermittently hit a stale tree and never navigated. Host window now anchors on-screen (bottom-trailing, >=64pt visible, .floating so ordinary windows cannot occlude it) while staying imperceptible (1% alpha, click-through, non-activating). Hardening: every replayed input batch marks the session dirty directly, and the beacon posts scroll/wheel dirt from the event listener with a 16ms throttle instead of waiting for a rAF tick. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Bind the browser-stream keyboard button to real keyboard visibility The button showed the input proxy's focus intent, so a keyboard raised by the address field or a dialog's text field left it stuck on 'Show Keyboard'. The glyph now binds to MobileKeyboardVisibilityObserver (UIKit keyboard notifications); tapping while the keyboard is up resigns whichever responder raised it (shared dismissMobileKeyboard, moved to CmuxMobileSupport) and releases the proxy's focus reasons via the policy's new explicit hide, which never flips into a focus request the way toggling would. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Give dialog text fields a visible input well The dialog card is glass, so the fields' glass background vanished into it and prompt/basic-auth inputs read as labels. Fields now sit in a filled rounded well with a hairline border, the same fill language as the bottom bar's address field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> | 1 个月前 | |
Tune Bonsplit tab bar action lane | 4 个月前 | |
iroh default mobile transport: design + green Swift FFI spike (#5735) * spike: iroh Swift C-FFI harness (macOS + iOS-sim) for mobile transport Minimal Rust staticlib (iroh 1.0.0-rc.1) exposing a blocking C API: bind endpoint, dial by EndpointId via n0 relays/discovery, one bi-stream send/recv. Swift CLI harness with listen (echo) and dial (round-trip proof) modes. build.sh builds aarch64-apple-darwin and aarch64-apple-ios-sim variants; artifacts are gitignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * spike: green cross-platform iroh proof (iOS-sim dials Mac by EndpointId) - link Network.framework for the ios-sim target (netdev nw_path_monitor) - map clean peer close (application code 0) to end-of-stream in recv - line-buffer harness stdout so orchestration can read the endpoint id - README records bindings decision (official iroh-ffi is archived, n0 recommends a custom wrapper), versions, build steps, proof transcript, and the ~7.7MB per-slice binary delta Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * plans: iroh default mobile transport design Substrate swap for the existing length-prefixed mobile-host protocol: iroh QUIC dial-by-EndpointId as the default iOS-to-Mac transport, Tailscale/LAN demoted to an opt-in fallback toggle. Covers the Mac listener seam (MobileHostByteConnection), the phone CmxByteTransport lane, registry route publication, E2E story (QUIC raw-public-key TLS replaces the Noise IK plan on this lane), Keychain key custody, relay strategy (n0 now, self-host later), iOS background/battery policy, hive-design reconciliation, and a 5-PR stacked delivery plan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * spike: iroh route priority 5 so it beats Tailscale in preferredRoute CmxAttachTicket.preferredRoute sorts ascending and lower wins. The Mac publishes debugLoopback at 0 and Tailscale at 10+; the spike's route JSON claimed 20, which would have lost to Tailscale, contradicting the design's iroh-by-default ordering. 5 sits below Tailscale (default) and above debugLoopback (DEBUG/simulator keeps the loopback mock host). Re-ran the cross-platform proof after the change: iOS-sim dialed the Mac by EndpointId, 46 bytes echoed, 1.04s connect, rc=0 both sides. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * plans: EndpointId pinning ships in the first iroh lane, not later Sharpen the security section: the real threat on a substituted route is Stack-token exfiltration (the phone sends its bearer token on every RPC), and iroh is the lane that can close it because the channel is cryptographically bound to the dialed EndpointId. So pinning moves from 'defense in depth later' into PR 3/4: pin at first trust in MobilePairedMacStore (QR = proximity, registry auto-pair = TOFU), refuse to send Stack tokens to a non-matching EndpointId, and surface EndpointId changes for explicit re-trust. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * spike: drain finished stream before connection close Review finding (autoreview P2): finish() only queues the FIN plus buffered stream data, while Connection::close is immediate and abandons buffered data, so a final frame that send() already accepted could be dropped by close(). Wait on SendStream::stopped() (peer acked all finished data) with a 5s bound before closing, so a vanished peer cannot wedge close. Re-ran the cross-platform proof: iOS-sim dial rc=0, mac listener rc=0, 0.50s connect, no drain stall (2s wall total). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 3 个月前 | |
Enforce iPhone+simulator default for iOS verification with an offline install queue (#9232) * Enforce iPhone+simulator default for iOS verification with an offline install queue iOS verification reloads now target BOTH an isolated per-tag simulator (cmux-dev-<slug>, created on demand) and the configured iPhone (CMUX_IPHONE_DEVICE_ID or ~/.config/cmux/iphone-device-id; never hardcoded). When the phone is unreachable at build time, the signed build is parked in a persistent queue (scripts/iphone-install-queue.sh, under ~/Library/Application Support/cmux-dev/iphone-install-queue) and a LaunchAgent (scripts/install-iphone-queue-agent.sh) auto-installs and launches it within seconds of the phone reconnecting, via launchd IOKit matching on Apple USB attach, WatchPaths on the queue, and a periodic network backstop, then sends a cmux notification. Every phone build hard-requires the same-tag Mac dev build: ios/scripts/reload.sh builds the Mac tag first when missing and refuses phone-only otherwise. scripts/ios-sim-install.sh installs cloud-built simulator apps into the isolated simulator for the reload-cloud-ios path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Probe device reachability through the queue script in ios/scripts/reload.sh One probe implementation (iphone-install-queue.sh probe) now decides "unreachable" for both the local and cloud reload paths, including the CMUX_IPHONE_QUEUE_FORCE_UNREACHABLE test hook; select_device still owns name/ambiguity resolution for reachable devices and its failure is treated as unreachable as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address review findings: name-target queueing, enqueue race, fail-closed sim install A --device-name target no longer probes or queues against the DEFAULT device id (queueing for a different phone than the one named would install on the wrong device); name targets error with a hint to use --device-id when unreachable. drain_entry now re-reads enqueued_at before every terminal action so a re-enqueue during an in-flight drain leaves the newer build queued instead of silently deleting or failing it. ios-sim-install.sh fails closed on an unreadable CFBundleIdentifier. Also: quote $tab expansions (SC2295), correct help sed ranges, document the one-time LaunchAgent install in CLAUDE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Nudge PR sync Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
iOS: don't lose saved hosts/IPs on upgrade (paired-Mac backup + restore) (#6405) * ios: failing test — paired-Mac store strands data on future schema version Adds the paired-Mac backup/restore design doc and a red regression test: when an older build opens a paired-macs.sqlite3 whose user_version was bumped by a newer build, the store currently throws unknownSchemaVersion and every read fails, surfacing as total loss of the user's saved hosts even though the rows are still on disk. The fix follows in the next commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: don't strand the paired-Mac store on a newer on-disk schema version runMigrations threw unknownSchemaVersion when user_version exceeded this build's, failing ensureReady and every read — so a user who upgraded (future schema vN) and then ran an older build saw all saved hosts as gone, though the rows were intact. Schema migrations are additive by contract, so older builds can still read the columns/tables they know. Degrade gracefully: log and read existing rows, never reset user_version (no destructive downgrade marker). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * presence: per-user pairedMacs backup collection (server) Adds the first client-owned sync collection. The phone backs up its local saved-host list (including manually typed host/IPs, which live only on-device today) so it survives an app upgrade, bundle-id change, or reinstall. - New POST /v1/sync/paired-macs route → DO RPC backupPairedMacs(teamId, userId, ops), mirroring the trusted heartbeat RPC rather than expanding the live WS inbound surface. - Per-user privacy scoping by physical collection name pairedMacs:<userId> (userId is verified, never client input); outgoing frames are relabeled to the logical `pairedMacs` so the client never sees the suffix. Reuses the whole generic snapshot/delta/tombstone/GC machinery unchanged. - Subscribe forwards the verified x-presence-user-id; the DO pins it on the WS attachment and serves/broadcasts pairedMacs scoped to that user. - Per-user record cap, op bounds, route byte budget (mirrors heartbeat). - bun tests: parse bounds, per-user isolation, cap, relabel, tombstone, no-op idempotency. Full suite 144 pass; typecheck + wrangler dry-run clean. Additive and live-safe: new collection keys only, no class migration, old DO instances ignore the new RPC/collection during rollout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * presence: GET /v1/sync/paired-macs restore path Adds the read side of the per-user backup: DO RPC listPairedMacs returns the live (non-tombstone) saved-host records newest-first, served by GET on the same authenticated, user-scoped route. The phone fetches this on sign-in to restore saved hosts after a reinstall or bundle-id change. Decouples restore from the WS sync client (which is built but not yet wired into the live app). bun test for list ordering + per-user isolation; strict test typecheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * presence: shape-aware equality for pairedMacs (no rev churn on timestamp drift) A backup upsert whose routes/name/active are unchanged but whose lastSeenAt advanced (every route refresh, and every full reconcile push on sign-in) must not re-mint a rev or broadcast a delta. Compare list-shape only, ignoring timestamps, mirroring the device-list collection. Stored lastSeenAt then tracks the last shape change (correct as-of-rev semantics for restore ordering). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: paired-Mac backup uploader + restore-on-sign-in Wires the iOS side of saved-host durability behind the mobilePairedMacBackup flag (DEBUG-on/Release-off, env/UserDefaults overridable): - PairedMacBackupClient: HTTP client for /v1/sync/paired-macs (POST ops, GET restore), auth mirrors PresenceClient/DeviceRegistryService. - BackingUpPairedMacStore: a MobilePairedMacStoring decorator so EVERY paired-Mac mutation flows through one seam — upsert/remove mirror to the DO best-effort (local stays authoritative); the sign-out wipe (removeAll) is NOT mirrored so the server backup survives for the next sign-in. - PairedMacRestore: on the first signed-in read, merge the backup into the local store — LWW by lastSeenAt (never clobber a newer local edit), insert missing hosts, and honor the backup's active host only when local has none (fresh install), so restore never hijacks the device's current active selection. - Composition root wraps the local store with the decorator when the flag is on and a presence URL resolves. Restore goes over HTTP (GET) rather than the WS sync client, which is built but not yet wired into the live app, so this feature is self-contained. No new user-facing strings (silent background backup/restore). swift test: 7 new tests pass (decorator mirroring, restore LWW/active rules, flag). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: make PairedMacRestore an injectable struct (package conventions) The iOS package-conventions lint forbids caseless enums with only static members (namespace-enum/namespace-type). Convert PairedMacRestore to a struct that takes the store + backup as injected dependencies with an instance run(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: address review — restore memoization, retry, team scope, setActive mirror Fixes from Cursor Bugbot / CodeRabbit / Greptile on the backup decorator: - removeAll (sign-out wipe) now resets the restore memo, so a same-launch re-sign-in restores again instead of returning an empty list (this was the exact sign-out→sign-in path; it was silently broken). - fetchAll returns nil on transport/auth failure (vs [] for genuinely empty), and restore is memoized only on a successful fetch — a transient first-launch failure now retries on the next read instead of stranding restore until restart. - Restore is scoped per (account, team), not per account: the backup DO is per-team, so switching teams re-restores (teamIDProvider injected). - Concurrent first reads share one in-flight restore Task, so a second read can't slip past the memo and observe a half-merged store. - setActive now mirrors the affected account scope to the DO (accurate records read back from the local store), so "select a host without connecting, then reinstall" no longer restores a stale active host. markActive upserts mirror the scope too, preserving the single-active invariant in the backup. - remove only mirrors a delete while signed in (no auth-failing noise for anonymous removals). - Migration test asserts user_version is left untouched (no downgrade marker). swift test: 11 backup + 5 migration tests pass; package-conventions lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * mac(dev): auto-publish this Mac's route to the user's pairedMacs backup DEV-only convenience so a fresh dev iOS build never needs a manual host entry. MacPairedMacBackupPublisher (DEBUG-on, env/UserDefaults overridable) registers the iOS-pairing-listener default on (so an attach route exists without toggling a setting), observes MobileHostService.statusUpdates(), and POSTs this Mac's deviceId+displayName+routes (active) to /v1/sync/paired-macs whenever routes change and the user is signed in. Routes are encoded via CmxAttachRoute so the iOS restore decodes them identically. Best-effort and Release-noop, mirroring PresenceHeartbeatClient. Bridges the dev gap where the registry (localhost) and presence devices projection don't deliver the Mac's route to the dev iOS build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * mac(dev): default iOS-pairing listener ON in DEBUG; drop runtime register The dev self-publisher needs the pairing listener bound so an attach route exists. Registering a UserDefaults fallback at runtime was clobbered by the settings runtime registering the catalog default, so move the default to the source: MobileCatalogSection.iOSPairingHost defaults true in DEBUG, false in Release (an explicit user toggle still wins). Release behavior unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * mac: wire MacPairedMacBackupPublisher.swift into cmux.xcodeproj The new file was never added to the Xcode project, so it didn't compile, the AppDelegate reference was an undefined symbol, and every macOS build failed (reload-cloud kept the stale binary; CI would fail too). Add the four pbxproj entries (PBXBuildFile + PBXFileReference + Cloud group + app-target Sources phase), mirroring PresenceHeartbeatClient.swift. Verified: the dev Mac now auto-publishes its route to the user's pairedMacs backup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: refresh AppDelegate Swift file-length budget for the publisher wiring The one-line MacPairedMacBackupPublisher.shared.configure(auth:) call (+ its comment) at the composition root grew AppDelegate.swift by 4 lines, tripping the file-length budget guard. Accept the minor known debt: the wiring belongs next to the other client configures. 17593 -> 17597. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: surface restored saved Macs on the disconnected screen Restoring saved Macs into the local store wasn't visible: the disconnected screen only auto-reconnected and otherwise jumped straight to "add device", so a restored Mac (e.g. on a fresh dev build, or when auto-reconnect can't reach it) never showed. Now the disconnected screen loads saved Macs (which also triggers the backup restore) and lists them for one-tap reconnect, only auto-presenting the pairing sheet when there are none to pick. en+ja localized. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: tapping a saved Mac dialed the phone's own loopback instead of Tailscale A restored/published Mac advertises both a debug_loopback route (127.0.0.1, priority 0) and a tailscale route. DEBUG builds keep .debugLoopback in supportedRouteKinds even on a physical device (for the on-device XCUITest mock host), so firstReconnectHostPortRoute, which picks the lowest-priority supported route, chose 127.0.0.1 — the phone's own loopback — and the connect silently failed without ever trying Tailscale. That made tapping a saved/restored Mac (switchToMac) and stored-Mac reconnect not connect on a device. Fix in route selection, not supportedKinds (XCUITests still need loopback): add preferNonLoopback (true on physical devices, false on the simulator where 127.0.0.1 IS the Mac). When set, a real route always wins over a .debugLoopback route regardless of priority; loopback is used only when it's the sole supported route. Tests cover device-prefers-tailscale, device-loopback-only fallback, and simulator-keeps-loopback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios(multi-mac P1): tag workspaces with their Mac (macDeviceID) Foundation for the aggregated multi-Mac workspace list + machine filtering. Adds macDeviceID to MobileWorkspacePreview (additive, defaulted) and stamps it from the connected Mac's ticket where the workspace list is built. Invisible today (single Mac), but every workspace now records which Mac it's from, which P3 (aggregation) and P4 (group/filter by machine) build on. Design in plans/feat-ios-multi-mac-workspaces/DESIGN.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios(multi-mac P4a): compound workspace filter (read-state × machine) Replaces the single-dimension All/Unread filter enum with a composable struct: readState (all/unread) × machines (Set<macDeviceID>, empty = all), passing both only when a row satisfies both. Expresses "unread on Mac X and Mac Y" directly. The filter menu gains a machine multi-select section that appears once more than one machine is present (single-Mac users see the unchanged All/Unread control); the list views compile unchanged since .all/.matches/.isActive/.emptyStateText are preserved on the struct. en+ja localized. 6 model tests incl. the compound case. Machine names are wired in once aggregation (P3) provides multiple Macs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios(multi-mac P4b): machine-list derivation + prune for the filter Pure, tested helpers the filter UI and aggregation need: machineIDs(in:) gives the distinct machines present in a workspace list (first-appearance order, skips unknown-machine rows) to populate the filter's machine multi-select, and pruneMachines(notIn:) drops selections for machines that vanished so a stale machine filter never silently hides everything. Full model suite 52 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios(multi-mac P2): per-Mac connection pool foundation Introduces MacConnection {macDeviceID, ticket, route, client, generation} and a connections:[macDeviceID:MacConnection] pool + foregroundMacDeviceID on the composite. The foreground attach now records its entry in the pool and teardown clears it. Additive and behavior-preserving (single-Mac == a pool of one); anonymous (empty-id) tickets are not pooled. This is the structure P3 builds on to open read-only connections to the user's other Macs and aggregate their workspaces. Compiles; route + backup tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios(multi-mac P3a): read-only secondary-Mac workspace fetch fetchSecondaryWorkspaceList(for:) opens a short-lived client to another paired Mac (reusing the manualHostTicket + workspace.list path, loopback-deprioritized on device) and returns its workspaces tagged with that Mac's macDeviceID, never touching the foreground connection. refreshSecondaryMacWorkspaces() populates secondaryWorkspacesByMac for every signed-in non-foreground Mac. Additive: not yet merged into the published list, so the single-Mac flow is untouched. Compiles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios(multi-mac P3b): merge other Macs' workspaces into the list (flag-gated) Foreground connect now kicks a background refreshSecondaryMacWorkspaces(), and publishAggregatedWorkspaces() merges the other Macs' rows after the foreground Mac's (de-duped by id, per-Mac order preserved). Gated by multiMacAggregation (env/UserDefaults, DEBUG on / Release off) and a no-op when there are no secondaries, so the single-Mac list is byte-for-byte unchanged. Cleared on teardown. Compiles; route/backup tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios(multi-mac P4): surface the machine multi-select in the filter WorkspaceListView derives the machines present in the (aggregated) workspace list and passes them to the filter menu, so the read-state × machine compound filter's machine section appears once more than one Mac has workspaces. Names come from the device tree; single-Mac shows the unchanged All/Unread control. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios(multi-mac P5): cross-Mac open switches the foreground connection openWorkspace now detects when the tapped workspace belongs to a Mac other than the current foreground connection (aggregated list) and switches the foreground to that Mac before selecting, so the terminal attaches to the right Mac. Gated by multiMacAggregation; no-op for single-Mac. Compiles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: refresh MobileShellComposite file-length budget for multi-Mac code The P2-P5 multi-Mac connection pool + aggregation + cross-Mac open added ~196 lines to MobileShellComposite.swift. The methods call private connect/ticket helpers so they can't move to a separate-file extension; accept the known debt. 5566 -> 5762. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: refresh paired-Mac routes from backup before multi-Mac aggregation The aggregated multi-Mac workspace list only showed the foreground Mac's workspaces because secondary Macs' stored routes went stale: refreshSecondary- MacWorkspaces read the local paired-Mac store, but that store is only restored from the backup once per launch (memoized scope). When a secondary Mac relaunched on a new port and republished its route to the per-user backup, the iPhone never re-read it, so the read-only workspace fetch dialed a dead port and that Mac silently dropped out of the list. Fix: add PairedMacBackupRefreshing.refreshFromBackup(stackUserID:) on BackingUpPairedMacStore, which forces a backup re-fetch + LWW merge (bypassing the once-per-launch memo, coalescing with any in-flight restore). refreshSecondary- MacWorkspaces calls it before loadAll, so secondary routes are current before the fetch. LWW by lastSeenAt means the live foreground route is never clobbered. Principled: routes are kept fresh from the authoritative per-user backup at aggregation time, instead of relying on a single sign-in-time restore. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: auto-connect first reachable Mac so home opens on the integrated list The home fell back to the "Your Macs" picker whenever there was no Mac marked active (or the active Mac's stored route was stale), forcing a manual tap before any workspaces showed. Rework the launch auto-connect so the home comes up connected to all Macs and shows one integrated list, without the picker: - Refresh saved-Mac routes from the per-user backup before dialing (LWW), so a Mac that relaunched on a new port is still reachable instead of failing to the picker. - Connect the explicitly-active Mac when reachable, otherwise the FIRST saved Mac with a usable route, instead of bailing when nothing is marked active. The other Macs are aggregated read-only (refreshSecondaryMacWorkspaces) into the same list, so the home is one integrated cross-Mac workspace list. The picker now only appears as the genuinely-offline fallback (no saved Mac has a usable route). Principled: auto-connect targets any reachable saved Mac with fresh routes, rather than depending on a single persisted "active" selection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: refresh routes from backup before manual Mac switch too switchToMac dialed the in-memory snapshot's routes, so manually switching to a Mac that had relaunched on a new port could fail on a stale route. Apply the same backup-refresh used by auto-connect and aggregation: refresh the per-user backup, re-read the target from the store, then dial its fresh route (falling back to the snapshot if the re-read yields nothing). Completes route-freshness across every saved-Mac connect path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: prefer IP-literal routes over MagicDNS hostnames for Mac connect/aggregation The multi-Mac aggregated list showed only the foreground Mac because the read-only secondary fetch to another Mac timed out. Root cause (confirmed on device via console diagnostics): a Mac can advertise three attach routes — debug_loopback, a MagicDNS hostname (e.g. <node>.<tailnet>.ts.net), and the raw tailscale IP. firstReconnectHostPortRoute picked the first non-loopback route, which was the MagicDNS hostname. MagicDNS doesn't resolve on every client (the phone here), so the attach-ticket request to the hostname timed out and that Mac was silently dropped from the aggregated list. A Mac that only advertises an IP route (no hostname) connected fine, which is why one Mac showed and the other didn't. Fix: among non-loopback routes, prefer one whose host is a numeric IP literal (IPv4/IPv6) over a hostname, since an IP is dialable without DNS. Falls back to a hostname route when no IP route exists, and loopback only as last resort. firstReconnectHostPortRoute is the shared selector for reconnect, manual switch, and secondary aggregation, so this fixes tap-to-connect to hostname-route Macs too. Added isIPLiteralHost + 3 route-selection tests incl. the exact repro. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: re-aggregate other Macs on pull-to-refresh and app foreground The aggregated multi-Mac list fetched each other Mac's workspaces once, on foreground attach. Workspaces created on a secondary Mac afterwards never appeared, because the read-only secondary list is a snapshot, not a live subscription (only the foreground Mac streams workspace.updated). Re-run refreshSecondaryMacWorkspaces from the two natural refresh points: - refreshWorkspaces() (pull-to-refresh) now re-aggregates after reloading the foreground list. - resumeForegroundRefresh() (app returns to foreground) re-aggregates when connected, so switching back to the app surfaces newly-created remote workspaces without a manual pull. Both gated on multiMacAggregationEnabled + an active foreground connection, so single-Mac behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: add transport-agnostic per-Mac workspace state + pure derivation Foundation for deriving the aggregated multi-Mac workspace list from a single source of truth instead of imperatively merging a live foreground list with stale secondary snapshots. MacWorkspaceState is the phone's view of ONE Mac's workspaces (workspaces + groups + liveness), keyed by macDeviceID, carrying NO transport/connection detail. MobileWorkspaceAggregation derives the flat ordered de-duplicated list (foreground first, then by display name) and the group sections as pure functions of [macID: MacWorkspaceState]. Same model + derivation whether each entry is fed by N direct phone->Mac connections (now) or one phone->Durable Object stream delivering per-Mac deltas (planned), so that migration is a transport swap, not a data-model change. 6 derivation tests. Not yet wired into the composite (next commit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: derive the workspace list from per-Mac state (slice 2) Wire the transport-agnostic data structure in: workspacesByMac is now the only stored workspace state, and `workspaces`/`workspaceGroups` are materialized derivations (private(set), assigned only by recomputeDerivedWorkspaceState). The foreground sync stream, secondary fetch, optimistic create-workspace/ terminal, preview, and reset all write per-Mac entries; the derived list recomputes via didSet. Anonymous/manual-ticket foreground uses a sentinel key. Deletes the two-sources-of-truth machinery: publishAggregatedWorkspaces (the re-merge band-aid) and secondaryWorkspacesByMac (the snapshot store). The foreground-update-overwrites-then-re-merges race is gone by construction: each Mac owns its entry, the aggregate is a pure function of them. clearRemoteConnection- Context keeps the offline foreground entry and drops only secondaries. Tests: 56 model+composite tests green (incl. new derivation + create/terminal/ preview paths). Test seam setWorkspacesForTesting replaces direct workspaces assignment. The 6 remaining failures are the pre-existing flaky render-grid timing tests, unchanged by this commit. Next (slice 3): per-Mac live workspace subscriptions feed workspacesByMac so remote-created workspaces appear with no refresh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: live per-Mac workspace subscriptions (slice 3) Each non-foreground Mac now holds a persistent read-only connection with its own live workspace.updated subscription that re-fetches its list on each change and writes its workspacesByMac entry, so a workspace created on another Mac appears with no pull-to-refresh. The derived list recomputes automatically. SecondaryMacSubscription holds the client + a fresh per-connection stream id + the consumer Task. refreshSecondaryMacWorkspaces is now an idempotent reconciler: establish a subscription for each newly-present secondary Mac, drop ones that disappeared or became the foreground. Fully best-effort and additive: any failure (no route, ticket/connect error, stream end) tears that entry down and the pull-to-refresh / foreground re-aggregate path remains the fallback, so a secondary subscription can never crash or block the foreground. Subscriptions are torn down on disconnect/sign-out (teardownSecondaryMacSubscriptions in clearRemoteConnectionContext). This is the N-persistent-connections model approved for now; the same per-Mac entries would later be fed by one phone->Durable Object stream (transport swap, no data-model change). 62 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: never show the Your Macs picker when Macs are saved The auto-connect made the home connect, but the root still fell back to the DisconnectedWorkspaceShellView picker whenever the foreground was not yet connected (initial connect window, or a failed/slow reconnect). Eliminate that: show the integrated workspace list whenever there are saved Macs, auto-connecting in the background, and only show the add-device flow when there are NO saved Macs at all. The list renders whatever has aggregated (foreground + live secondary subscriptions) and its toolbar carries settings/devices/sign-out, so nothing is lost by dropping the picker. Opening a workspace attaches its Mac on demand. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: auto-connect falls through to the next Mac when one is offline reconnectActiveMacIfAvailable picked a single target (active Mac, else first with a route) and connected once; if that Mac had a stored route but was actually down, the connect failed and the home showed "Mac offline" without trying any other reachable Mac. Build an ordered candidate list (active first, then every other Mac with a usable route) and try each via connectManualHost until one connects, so a single offline Mac never blocks the others. The restoring-gate deadline still caps the UI; the loop keeps trying in the background. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios/worker: fix autoreview findings (active-mac deactivation, stale secondary refresh, backup body cap) P1: PairedMacRestore deactivated the currently-active Mac when refreshFromBackup brought a fresher record for it (route refresh before reconnect/aggregation), losing the user's selection. Preserve the existing local active flag when updating an existing record; only honor the backup's active for records missing locally on a fresh install. Regression test added. P2: refreshSecondaryMacWorkspaces (foreground/pull) skipped Macs that already had a subscription, so a suspended/never-pushing secondary stream left a stale snapshot forever. Explicit refresh now reseeds existing secondary clients (and recreates dead ones), so a pull/foreground always updates the aggregate. P2: the paired-Mac backup POST reused the 16 KiB heartbeat cap while accepting up to 200 ops x 2 KiB routes, so legitimate backups 413'd and the best-effort client silently dropped them, staleing the server backup. readBoundedJson now takes a maxBytes; the backup route uses MAX_PAIRED_MAC_BACKUP_BYTES sized to the declared limits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: fix autoreview round 2 (sign-out restore race, stale machine filter blanks list) P1: BackingUpPairedMacStore.removeAll (sign-out wipe) cleared the inFlight map but did not cancel the restore tasks, so a backup fetch suspended across the wipe could resume and re-upsert the previous account's Macs into the emptied local store (privacy boundary). removeAll now cancels in-flight restores, and PairedMacRestore.run checks Task.isCancelled after its fetch and skips all writes. Regression test added. P2: the machine filter was never pruned, so when a filtered Mac left the aggregated list (a secondary disconnected, or fewer than two machines so the filter menu's machine section hid) the stale machine id rejected every row and stranded the user on a blank list with no visible control to clear it. This is the likely "blank black screen" after reconnect churn. WorkspaceListView now prunes filter.machines whenever the present machine set changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: per-machine avatar color + fix autoreview round 3 (wrong foreground key, secondary refetch storm, offline dead-end) Feature: workspaces from the same Mac now share one avatar color in the aggregated list (MachineAvatarPalette, keyed to macDeviceID with a workspace-id fallback and djb2 spread); the symbol still encodes terminal count. Unit-tested. P1: applyRemoteWorkspaceList wrote the foreground Mac's workspaces under the PREVIOUS foreground key because foregroundMacDeviceID was assigned after the apply. On a Mac A->B switch this stored B's list under A's key and the derived list went stale/empty once the id flipped. Set foregroundMacDeviceID before applying. P1: every secondary workspace.updated push awaited a full workspace.list with no coalescing, so a title/progress churn stream queued repeated full scans and MainActor aggregate updates. Added a per-Mac leading+trailing coalesced refresh (SecondaryMacSubscription.refreshTask/refreshPending) — bounded, no cancel/restart starvation. P2: an offline returning user whose auto-reconnect failed fell through to a workspace list whose only affordance (pull-to-refresh) no-ops while disconnected, with no reconnect control — a dead end. Added store.reconnectOrRefresh (reconnect when offline, refresh when connected), wired pull-to-refresh to it, and added a Reconnect button to the offline status row (localized en/ja). Keeps the integrated list as the only surface — no picker screen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios/worker: fix autoreview round 4 (sign-out aggregation race, backup freshness on republish) P1: refreshSecondaryMacWorkspaces captured the account then awaited backup refresh, store load, client creation, and per-Mac fetches before mutating secondaryMacSubscriptions/workspacesByMac, while callers launched it in untracked Tasks. An in-flight pass could resume after sign-out/account switch and write the previous user's Macs/workspaces into the new UI. Added an isAggregationScopeValid guard (signed-in + same account + not cancelled) re-checked after every await before any mutation/connection, routed the pass through a tracked secondaryAggregationTask, and cancel it (plus tear down live secondary subscriptions) on sign-out and full reset. P1: a same-shape backup republish (Mac re-confirming its current live route) no-op'd without advancing the stored lastSeenAt, so the iOS LWW restore skipped the backup and kept dialing a stale local route. upsertRecord gained an opt-in freshnessOf; the paired-Mac path now refreshes lastSeenAt in place (same rev, no delta/broadcast) so restore sees the republish as fresh. Test extended. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios/worker: fix autoreview round 5 (unscoped aggregation read, restore-memo race, unbounded paired-Mac tombstones) P1: refreshSecondaryMacWorkspaces allowed a nil/empty account, so loadAll(stackUserID: nil) would read EVERY locally stored Mac across Stack accounts and could publish another account's workspaces into the UI. Now requires a concrete signed-in user before any load/connection (mirrors loadPairedMacs), keeping the post-await scope checks. P1: per-user paired-Mac delete tombstones were never garbage-collected — the alarm only GC'd the devices collection — so an authenticated client churning create/delete grew synced:/synctomb: storage without bound (the live-record cap resets on delete). Added listTombstonedCollections; the alarm now GCs every per-user pairedMacs:<userId> collection that holds tombstones and folds each next-GC deadline into its schedule. P2: a restore suspended at `await task.value` across a sign-out wipe could resume and re-insert restoredScopes (or clobber a post-wipe inFlight entry), making a same-launch re-sign-in skip the backup restore and show an empty list. Added a resetGeneration bumped by removeAll; both restore paths bail if it changed across the await. Tests: paired-Mac tombstone discovery+GC. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: fix autoreview round 6 (restore cancellation per-await, switchToMac stale cache) P1: PairedMacRestore checked Task.isCancelled only once after fetchAll, so a sign-out wipe landing during loadAll or any later upsert let the loop reinsert the previous account's Macs into the wiped store. Now re-checks after the load and before every write, bailing with completed: false. P1: switchToMac hard-failed unless the target was in the in-memory pairedMacs cache, but the multi-Mac aggregation reads Macs straight from the store, so tapping a freshly-restored secondary Mac's workspace no-op'd and stranded the user on a workspace whose Mac never connected. switchToMac now resolves the target from the store (after the backup refresh), falling back to the cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: redesign devices screen as Computers management view (no connect step) Since workspaces from every Mac now appear together automatically, the device tree's "connect to a device" step is obsolete. Replace it with a Computers screen that manages the Macs signed in to the account: - One row per computer: machine-colored avatar (same color its workspaces use in the list, via the new shared MachineAvatarColors), name, online/last-seen status from durable-object presence, and workspace count. - Remove a computer via swipe or context menu (confirmed) -> forgetMac. - Add a computer via a toolbar + that opens the existing pairing flow (showAddDevice plumbed root -> shell -> list -> screen). - Drop the instance/tag/workspace expansion tree and Connect affordances; delete the now-dead DeviceTreeExpansionStore (+ tests) and the unused tree row snapshots, keeping only DeviceTreePresence. - New mobile.computers.* strings localized en + ja. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: fix autoreview round 7 (Release preview compile break, ineffective computer remove) P1: rootContent referenced WorkspaceListLayoutPreviewView directly, but that type is compiled only under `os(iOS) && DEBUG`, so a Release/iOS archive failed to type-check the branch ("cannot find ... in scope"). Added a DEBUG-wrapped workspaceListLayoutPreview helper (mirroring terminalLayoutPreview) so Release never names the gated type. P2: the Computers list was built from deviceTreeDevices (prefers the team registry), but Remove calls forgetMac, which only deletes the local paired-Mac backup row — so a registry-backed computer reappeared on the next registry load and Remove looked broken. Build the list from pairedMacs instead: this feature's source of truth, the same set that feeds the workspace aggregation and the exact rows forgetMac removes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: track CMUXMobileRootView in swift file-length budget (preview helper) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios/worker: fix autoreview round 8 (restore not cancelled on sign-out, idle-team tombstone GC, stale secondary rows) P1: signOut() never cancelled in-flight paired-Mac restores (it does not call removeAll), so the cancellation guards could not fire on the normal sign-out path; a restore suspended at its backup fetch could resume — possibly authorized with the next account's live token — and write rows for the previous account. Added PairedMacBackupRefreshing.cancelInFlightRestores (cancel tasks + bump reset generation, without wiping the per-user rows) and call it from signOut. P1: backupPairedMacs created delete tombstones but never scheduled an alarm, so an idle team (no presence instances/subscribers) would never wake to GC them and a create/delete churn grew DO storage unbounded. It now schedules the next tombstone-GC deadline for the user's collection after applying ops. P2: when a secondary Mac's event stream ended, the subscription was removed but its workspacesByMac entry stayed marked connected, leaving dead rows in the aggregate that taps routed into. The stream-end teardown now downgrades that Mac's state to unavailable so the rows show offline until a refresh re-establishes it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: fix autoreview round 9 (forget leaves secondary subscription, failed switch still opens workspace) P1: forgetMac removed the store row but, for a SECONDARY Mac, left its live read-only subscription and workspacesByMac entry intact, so the Computers screen's Remove left the forgotten Mac's workspaces in the list (still updating, tappable) until a later aggregation pass. forgetMac now cancels secondaryMacSubscriptions and clears workspacesByMac for that Mac. P2: openWorkspace awaited switchToMac for a cross-Mac workspace but selected the workspace even when the switch failed (no route / failed connect / fell back to the previous Mac), focusing a workspace whose Mac is not the live connection so terminal input targeted the wrong client. switchToMac now returns whether the foreground connection targets that Mac; openWorkspace bails (leaving the user on the list, with the Reconnect affordance) when it does not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: pop the compact stack when a cross-Mac workspace open fails (autoreview round 10 P1) The tap selects a workspace and pushes its detail synchronously, and openWorkspace runs from that detail's task — so an early return on a failed switchToMac left the user inside a workspace whose Mac never became the foreground connection (terminal input would route to the wrong live client). On switch failure, roll the selection back (selectedWorkspaceID = nil) so the compact stack pops to the list, where the offline row's Reconnect / next aggregation pass recovers the Mac. Known follow-ups (autoreview round 10, narrow edges not on the dogfood path): - sign-out-during-restore cancellation is fire-and-forget; the residual race needs the restore fetch bound to the captured account/team in the backup client. - an empty-macDeviceID QR connect keeps the foreground under the anonymous key and does not migrate workspacesByMac/foregroundMacDeviceID when the real id is adopted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: color computers by distinct position, not a colliding hash (fix two Macs both yellow) The avatar color hashed macDeviceID into 8 slots, so two Macs collided on one color ~1/8 of the time — Lawrence's two real device ids both hashed to slot 2 (yellow). Assign a DISTINCT color index per Mac by sorted device id in the aggregation (MobileWorkspaceAggregation.machineColorIndex), stamp it onto each derived workspace (machineColorIndex), and color the Computers rows from the same store map. Different Macs are now guaranteed distinct up to the palette size; the id hash remains only as a fallback outside the aggregated list. Tests added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: promote the live secondary connection on cross-Mac open instead of re-dialing (root-cause fix for "Mac offline") Architectural fix. Tapping a secondary Mac's workspace ran openWorkspace -> switchToMac -> connectManualHost -> connect(), which threw away the already-live, authorized read-only client in secondaryMacSubscriptions and re-dialed the foreground from scratch. That re-dial pipeline has several independent failure points — route re-derivation via refreshFromBackup LWW, the offline preflight, and connect()'s connectionGeneration supersession race — any of which strands the user as "Mac offline" even though a working client to that exact Mac exists. switchToMac now first tries promoteSecondaryToForeground: probe the live secondary client, and on success take ownership of it as the foreground connection (reuse the client/route/ticket, start terminal polling, re-aggregate the demoted Mac) with no re-dial. Falls back to the existing re-dial only when no live connection exists. This makes "offline on a reachable, already-aggregated Mac" unrepresentable. First cut of the larger unification (one MacConnection per Mac, foreground as a selector); the write-only `connections` pool and the duplicate connect path collapse in the follow-up. Orthogonal dev-only gap remains: a secondary on an ephemeral port the phone can't refresh (no dev registry) has no live connection to promote. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: Computers screen — drive the dot from the phone's real connection, show presence + route as diagnostics, refresh live while open The connection dot mixed two sources: the phone's live RPC status for the foreground Mac, but the Durable Object presence worker (the Mac's own heartbeat, not the phone's connection) for every other Mac. So a Mac the phone is actively connected to as a SECONDARY showed not-green because presence (unreliable on dev) didn't report it — exactly the "MacBook Pro not green" case. Now the dot is driven by the phone's own per-Mac connection (store.macConnectionStatuses, derived from each MacWorkspaceState.status: green=connected foreground/secondary, orange=reconnecting, grey=not connected), which updates reactively as subscriptions connect/drop. Presence and the dialable route (host:port) move to a separate diagnostic line, so a mismatch — "online via presence but the phone can't connect" — is a visible tailscale/route signal, and the user can see the exact endpoint. While the sheet is open it re-aggregates every 4s so a dropped Mac reconnects quickly. New strings localized en/ja. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: tap a computer for a comprehensive detail/debug sheet Tapping a row on the Computers screen now pushes MacComputerDetailView with the full per-Mac picture, separated so a connection problem is diagnosable: - Connection: the PHONE's live status to this Mac + workspace count + whether it is the active foreground. - Presence (from the Durable Object presence worker): online/offline + last seen, or "unknown", with a footer explaining that presence is the Mac's heartbeat, not the phone's connection, and that online-but-not-connected = a Tailscale/route problem. - Routes the phone can dial: every saved route (kind + host:port), selectable. - Identity: device id, paired-since, route-updated. - Actions: Reconnect, Remove. Rows are NavigationLinks into the sheet; strings localized en/ja. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: per-Mac custom name, color, and icon — synced across the user's devices Users can rename a computer and give it a custom color (8 swatches or any color) and icon (curated SF Symbols or any emoji) from the computer's detail sheet. The override wins over the Mac-reported name and the automatic color/icon everywhere: the workspace list avatars, the Computers screen, and the detail sheet. Persistence + sync reuse the existing per-user Durable Object paired-Mac backup: - MobilePairedMac + customName/customColor/customIcon; SQLite store v2 migration (additive nullable columns) + setCustomization (preserves the Mac's reported name/routes/active, bumps lastSeenAt for LWW). - PairedMacBackupRecord (Swift + worker) carries the fields; parse + bounds + pairedMacShapeEqual treat them as shape so a change mints a rev and broadcasts. - BackingUpPairedMacStore uploads the COMPLETE current record on every write (so a route refresh never clobbers a customization) and mirrors setCustomization. - PairedMacRestore applies the fields (LWW) so an edit on device A appears on B. - store.updateMacCustomization persists + uploads + re-derives; the aggregation stamps custom color/icon onto each workspace preview. Color is "palette:<n>" or "#RRGGBB"; icon is an SF Symbol name or an emoji (classified by non-ASCII). Strings localized en/ja. Tests: worker customization sync + shape; restore-applies + setCustomization-preserves. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: show a Reconnecting/Reconnect overlay on the terminal when disconnected (fix recurring "black screen") Recurring report: the phone drops its connection (dev route staleness with no registry to refresh) and the workspace detail keeps showing the now-dead terminal surface — an unrendered black screen with only a tiny status pill. The connection is fine to re-establish, but nothing tells the user that or offers an action. WorkspaceDetailView now overlays the terminal with TerminalDisconnectedOverlay whenever macConnectionStatus != .connected: a spinner for .reconnecting, and an offline icon + host + a Reconnect button (-> store.reconnectOrRefresh) for .unavailable. So a dropped connection reads as "Reconnecting…" with a clear action instead of a black void. Localized (reuses mobile.workspace.reconnect). Note: the underlying dev route-refresh gap (a secondary Mac on an ephemeral port the phone can't relearn without the registry) still requires a re-pair on dev; this makes that state visible + recoverable instead of silent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * workers/presence: add wrangler.dev.toml for safe cmux-presence-dev deploys Deploying the dev instance with `wrangler deploy --name cmux-presence-dev` inherits the production presence.cmux.dev custom domain from wrangler.toml (--name only overrides the worker name), STEALING the prod domain from cmux-presence and breaking prod auth (the dev worker uses the dev Stack project). Add a dedicated wrangler.dev.toml (workers_dev = true, no custom domain) so the dev instance stays on its *.workers.dev URL, and point the README at it with a warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * workers/presence: encode a per-developer isolated-worker pattern for concurrent dev Problem: cmux-presence-dev is a SINGLE shared worker — last deploy wins, and an unmerged feature (the paired-Mac backup lives only on its branch) exists only on whoever deployed last, so two people working on the worker clobber each other. Pattern: each developer deploys their own cmux-presence-dev-<slug> via scripts/deploy-dev.sh. Each named worker has its OWN Durable Object namespace, so presence + paired-Mac-backup state is fully isolated per dev — any number of people dogfood worker changes at once without collision. Builds point at it via CMUX_PRESENCE_BASE_URL; the shared cmux-presence-dev stays the integration baseline (the script refuses reserved/prod names). To make a tapped iOS DEVICE build honor the override (it sees no shell env), the resolver now also reads an Info.plist key CMUXPresenceBaseURL — precedence env → UserDefaults → Info.plist → Debug default (tested). README documents the full pattern + guardrails; the remaining wiring (reload baking CMUXPresenceBaseURL into the tagged Info.plist next to CMUXDevTag) is flagged as a TODO. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: track PresenceServiceConfiguration in swift file-length budget Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Harden paired-Mac v2 migration + bound Computers-screen polling Autoreview findings on the multi-Mac PR: [P1] v2 SQLite migration was neither idempotent nor atomic: it ran the three ADD COLUMN statements then bumped user_version separately, so a kill / disk-full / SQLite error after a partial apply stranded the DB at v1 with some v2 columns present. The next launch re-ran ADD COLUMN custom_name and failed with a duplicate-column error, bricking the paired-Mac store. Now each migration step runs inside one transaction (SQLite DDL + PRAGMA user_version are both transactional, so a partial apply rolls back and retries cleanly), and migrateToV2 only adds columns missing from PRAGMA table_info, which also recovers any dogfood device already left half-migrated by the earlier build. Adds a regression test that seeds a partially-applied v2 schema and asserts recovery. [P2] The Computers sheet polled store.reconnectOrRefresh() every 4s while open, which pulled the DO backup over the network and, when disconnected, re-dialed offline Macs on a fixed timer (battery/network fan-out). The online dots (presence) and secondary workspace lists are already push-driven, so the timer now calls a bounded refreshComputersScreen() (local row reload + coalesced foreground refresh only) on a gentler 10s cadence and leaves offline-Mac dialing to presence-push recovery and the explicit pull-to-refresh / per-Mac Reconnect button. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Route multi-Mac workspace mutations + reconnect to the owning Mac Round 2 autoreview findings on the multi-Mac aggregation: [P1] promoteSecondaryToForeground reused a live secondary connection as the new foreground but never started its terminal event stream: it called cancelRemoteOperationTasks() (which does NOT clear terminalEventListenerTask/ ID) and then startTerminalRefreshPolling(), which no-ops while a listener task is still installed. The promoted client got no terminal/workspace/ notification push events, so output stalled until another path restarted the stream. Now stop+start the listener (the existing == listenerID defer guard makes the old listener's async teardown safe). [P2] Aggregated workspace rows can belong to a secondary Mac, but rename/pin/ unread/close all sent to the single foreground remoteClient — wrong Mac, and with a colliding id could mutate a foreground workspace. sendWorkspaceMutation now resolves the workspace's owning Mac (workspaceMutationTarget) and routes to that Mac's client: foreground -> remoteClient + refreshWorkspaces(); a live secondary -> its client + scheduleSecondaryRefresh(); a known offline owner -> no send + snap back (never misroute to foreground). A failed secondary write no longer marks the foreground connection unavailable. [P2] The per-computer detail Reconnect button called reconnectOrRefresh() (foreground/active Mac) and ignored the computer being viewed. It now calls switchToMac(macDeviceID:), which promotes a live secondary to this Mac or re-dials it specifically. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix foreground-Mac ownership: stale rows, switch fast path, test double Round 3 autoreview findings: [P1] Adding setCustomization to MobilePairedMacStoring broke the CmuxSyncStore test target: FakePairedStore conformed with only the old methods. Added a no-op setCustomization to the fake. [P1] On a foreground Mac change (connect A->B, promotion, or a real connect after an anonymous/sign-out session) the previous foreground/anonymous entry was left in workspacesByMac. recomputeDerivedWorkspaceState derives over every entry, so stale rows kept showing and could route actions/opens through stale ownership (regressing the old workspaces = remoteWorkspaces full replacement). Added dropStalePreviousForeground(): on the foreground flip it removes only the old foreground key (never a live/offline secondary, which aggregation re-adds), wired into both the connect path and promoteSecondaryToForeground. [P1] switchToMac's already-foreground fast path trusted the persisted isActive flag, which lags the live connection (promoteSecondaryToForeground writes it via an unawaited Task; stale during reconnect/switch races). It could return success without switching and leave input/mutations on the wrong Mac. Now gates on the live foregroundMacDeviceID == macDeviceID identity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix offline-Mac dot + bake iOS presence override; doc team-scope limit Round 4 autoreview findings: [P2] clearRemoteConnectionContext set the global status unavailable but left the retained offline foreground entry in workspacesByMac with status .connected. macConnectionStatuses (the Computers screen's per-Mac dots) derives from those per-Mac states, so a just-disconnected Mac kept showing a green connected dot. Now downgrade the retained entry to .unavailable. [P2] The CMUXPresenceBaseURL Info.plist override was read by PresenceServiceConfiguration but never baked, so a tapped dev device build ignored a per-developer isolated worker. Wired the bake end to end: added the CMUX_PRESENCE_BASE_URL build setting to ios/Config/Shared.xcconfig (empty default) + the CMUXPresenceBaseURL key in ios/Config/Info.plist, and ios/scripts/reload.sh now passes $CMUX_PRESENCE_BASE_URL at both xcodebuild sites (next to CMUX_DEV_TAG). Release/TestFlight stay empty -> unaffected. Updated the worker README (no longer a TODO). [P2] Documented the per-(account, team) backup vs account-scoped local rows scope gap inline at mirrorAccountScope. Solo/single-team users are unaffected; proper multi-team isolation needs a team_id store column (v3 migration), tracked as a follow-up rather than expanding this upgrade-safety PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Bound Computers timer, key manual reconnects by real id, stop backup clobber Round 5 autoreview findings: [P1] refreshComputersScreen() (the open-sheet 10s timer) delegated to refreshWorkspaces(), which fans out refreshSecondaryMacWorkspaces() to every saved Mac and re-establishes/re-dials missing (offline) subscriptions — the reconnect storm the screen is meant to avoid (my earlier bounding fix was incomplete). It now does a foreground-only reload (riding any in-flight pull-to-refresh) and never initiates the secondary fan-out; recovery stays on presence-push + explicit pull/Reconnect. [P1] A Mac without mobile.attach_ticket.create connects via a synthetic manual-<host>:<port> ticket, and connect() keyed foreground state by ticket.macDeviceID. So a switch/reconnect to such a Mac stamped foreground workspaces with the synthetic id; filters, Computers rows, mutation routing, and aggregation no longer recognized the real Mac as foreground (and could open a duplicate secondary). connect()/connectManualHost now take the real pairedMacDeviceID hint (threaded from switchToMac, reconnect, device-row paths) and key foreground state + the connection pool under it. [P2] The Mac route-publisher omits customName/color/icon, but the worker treated absent fields as part of the record shape, so every Mac heartbeat minted a rev that wiped the user's iOS-set customizations and the next restore cleared them. Fix: iOS uploads now ALWAYS emit the three custom keys (null = reset-to-Auto, authoritative) via a custom encoder; the worker preserves stored customizations for any key an upload OMITS (the Mac), while a present key (iOS) still sets/clears it. Tests on both sides. (Dev worker needs redeploy for dogfood.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Stamp foreground rows with real Mac id; migrate test off private workspaces Round 6 autoreview findings: [P1] remoteWorkspacesPreservingSnapshots stamps each foreground workspace with activeTicket?.macDeviceID (the synthetic manual-<host>:<port> id for an attach-ticket-less Mac), and setForegroundWorkspaceState only restamped nil ids — so round 5's real-id foreground KEY did not reach the rows. The same machine then looked like a different Mac (wrong counts/customizations; openWorkspace tried to switch to a nonexistent Mac). setForegroundWorkspaceState now stamps ALL foreground rows with the resolved foregroundMacDeviceID. [P1] The aggregation refactor made public private(set), but the iOS cmuxFeatureTests still assigned store.workspaces directly (7 sites), breaking the feature test target compile. Migrated them to the existing setWorkspacesForTesting DEBUG seam (reachable via @testable import), which writes the foreground per-Mac state so the derived list recomputes identically. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Drain in-flight restores before sign-out wipe (privacy race) Round 7 autoreview finding: [P1] removeAll() (sign-out wipe) cleared the local store BEFORE cancelling in-flight restores. A restore can pass its Task.isCancelled check, suspend inside inner.upsert, then the wipe runs and only afterwards cancels — but cancellation does not withdraw the already-queued upsert, so the previous account's Mac could be written back into the just-emptied store after sign-out (privacy boundary). removeAll now cancels AND DRAINS (awaits) the in-flight restores before wiping, so every pending write completes first and the wipe is final. Adds a deterministic regression test (GatedUpsertStore) that suspends a restore inside upsert across the wipe and asserts the store ends empty; it fails under the old wipe-then-cancel ordering. (The QuickLook finding the reviewer raised is out-of-scope: it comes from the origin/main merge, not this PR's diff, and the helper flagged it as ignored.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Team-safe backup mirror, non-loopback secondary dial, QR identity re-key Round 8 autoreview findings: [P1] mirrorAccountScope uploaded the WHOLE account's local rows into whichever team the backup client targets, so a multi-team user activating a host could copy other-team hosts into the selected team's per-team DO. Removed the whole-account mirror: upsert(markActive)/setActive now upload only the two records whose active flag actually changes (the newly-active host + the previously-active one, now cleared), preserving the backup's single-active invariant without dumping the account. (Local rows still carry no team id; a full team-scoped store is a separate v3-migration follow-up, but the leak vector is gone.) [P1] makeSecondaryClient proved a non-loopback route to fetch the attach ticket but then dialed supportedRoutes.first, which on a physical phone can be a higher-priority debugLoopback (127.0.0.1) — every secondary subscription dialed the phone itself, so the Mac was unreachable and dropped from aggregation. Now dials the proven route (exact host/port match, else any non-loopback, else first). [P2] A compact/anonymous QR pairing connects with an empty macDeviceID, so foreground state lands under the anonymous key with foregroundMacDeviceID nil. applyHostReportedIdentity adopted the real id into activeTicket but never updated the aggregate key, so the Computers screen showed the Mac as not-connected and aggregation (which excludes foregroundMacDeviceID) could open a DUPLICATE secondary to the same Mac. Added adoptForegroundMacIdentity to move/restamp the foreground per-Mac state and connection-pool entry to the reported id. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Stack teams: team-scoped paired-Mac data + lazy re-scope + nav drawer Implements full Stack-team support on iOS (the team-scope gap autoreview kept flagging is now closed by real per-team scoping rather than a documented caveat). A) Team-scoped local paired-Mac data - v3 SQLite migration adds a nullable team_id column (idempotent, mirrors v2); legacy/pre-v3 rows have NULL team and stay visible under EVERY team (loadAll filter is ) so an upgrade never hides existing hosts. - MobilePairedMac gains teamID; protocol upsert/loadAll/activeMac gain a teamID param with convenience overloads (teamID:nil) so existing call sites compile unchanged. markActive/setActive clear the active flag per (user, team) so activating in team A never deactivates team B. - BackingUpPairedMacStore injects the current team (teamIDProvider) into inner upsert/loadAll/activeMac; PairedMacRestore stamps restored rows with the team whose DO they came from. Multi-team users now only see/dial the active team's Macs. Tests: v2→v3 migration legacy visibility, per-team isolation, decorator injection. B) Lazy re-scope on team switch (keep the live terminal) - MobileShellComposite.currentTeamDidChange() re-subscribes presence, tears down secondary aggregation, invalidates the restore memo, and clears the pairedMacs/registryDevices caches — but never touches the foreground connection, so switching teams does NOT drop the live terminal. Rebuild is lazy (next foreground / Computers .task / pull). CMUXMobileRootView observes selectedTeamID (single mutation path). Test: foreground workspaces survive. C) Left-edge-swipe nav drawer - New MobileNavDrawerView (account header, Stack team list with current checked, Settings, Sign out) + EdgeSwipeDrawerContainer (leading-edge drag + scrim; toolbar button is the primary/accessible entry). Mounted in WorkspaceShellView over both layouts; WorkspaceListView gains a leading drawer button. Tapping a team only writes AuthCoordinator.selectedTeamID (the root re-scopes). en+ja localized. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix sign-out foreground reset, cumulative backup cap, iOS drawer type ref Autoreview round on the teams feature: [P1] signOut seeded the anonymous preview workspacesByMac entry but left foregroundMacDeviceID at the old real Mac id. The next connect() then captured that stale id as previousForegroundKey, so dropStalePreviousForeground (the round-6 stale-row fix) dropped the WRONG key and the preview rows survived alongside the newly-connected Mac. signOut now clears foregroundMacDeviceID and the foreground connection pool before seeding the anonymous entry, so foregroundMacKey matches the seeded key and the next connect drops the anonymous preview correctly. [P1] The new /v1/sync/paired-macs write path capped only LIVE records, so create→delete→repeat churn with fresh ids grew the DO unbounded across the tombstone GC window. Added MAX_PAIRED_MAC_RECORDS_PER_USER (5× live): a brand-new id is refused at the cumulative (live + retained-tombstone) cap; reviving a tombstoned id reuses its slot. Test churns to the cap and asserts new ids are refused while a revive is allowed. Also fixed the iOS archive compile error: MobileNavDrawerView named CMUXAuthTeam (from CMUXAuthCore, not a direct dep of CmuxMobileShellUI). Pass the team's id/displayName fields instead of the type, which also keeps the @Observable off the drawer's row closures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix iOS drawer compile: .rect arg order + gate drawer to iOS - EdgeSwipeDrawerContainer: UnevenRoundedRectangle .rect() wants bottomTrailingRadius before topTrailingRadius. - MobileNavDrawerView uses .listStyle(.insetGrouped) (iOS-only) and is only used on iOS, so gate the whole file behind #if os(iOS) (the package also compiles for macOS). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix WorkspaceListView call: openDrawer must match declaration order openDrawer is declared right after store, so pass it there in both call sites (Swift requires call arguments in declaration order). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Drawer edge swipe: use native UIScreenEdgePanGestureRecognizer The SwiftUI DragGesture edge-strip fought the workspace list's scroll + row swipe actions (SwiftUI gestures don't coordinate with UIScrollView) and felt broken. Replace it with UIKit's UIScreenEdgePanGestureRecognizer — the same system recognizer behind the interactive back gesture — installed on the hosting view via a representable. It has screen-edge priority and coordinates with the scroll view automatically, and now drives the drawer INTERACTIVELY (the panel tracks the finger; commit on release by threshold/velocity). Gated to the compact root list only (isEdgeSwipeEnabled): a pushed detail uses the left edge for the system back swipe and the split layout has its own sidebar gesture, so the edge swipe would conflict there. The ☰ toolbar button opens the drawer in every state regardless (primary, accessible entry). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Replace team drawer with a native inline team picker in Settings Per dogfood feedback, the left-edge swipe drawer felt wrong (Apple also discourages hamburger drawers). Removed it entirely (EdgeSwipeDrawerContainer + MobileNavDrawerView deleted; WorkspaceShellView/WorkspaceListView reverted to the plain layout + the existing top-left Settings button) and put the team picker where it belongs: an INLINE Picker in the Settings sheet's account area — each Stack team is a row with a checkmark on the current one, one tap to switch. The team-scoped data + lazy re-scope (selectedTeamID observed by the root) are unchanged; only the entry point moved from a custom drawer to native Settings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Computers screen: show each Mac's build channel (DEV+tag / Nightly / Stable) The Computers screen now labels which build each Mac runs, to debug 'which build is this host'. Full vertical: - Mac heartbeat (PresenceHeartbeatClient) now sends the app's bundleId alongside the existing CMUX_TAG. - Presence worker (validate/core/do.ts) parses, stores, and echoes bundleId on the instance (optional, bounded; a change re-syncs the device row). - iOS PresenceInstance decodes bundleId; PresenceMap.deviceSummary derives a build label via the new MacBuildChannel helper (a non-default tag => 'DEV · <tag>'; else the bundle-id suffix => Nightly/RC/Staging/Stable). - Computers UI: a small tinted pill next to each Mac's name (MacComputerRow) and a 'Build' row in the detail's Presence section (MacComputerDetailView). Tests: MacBuildChannel label derivation, worker bundleId carry, the Mac heartbeat body emits bundleId. en+ja localization for the new strings. Needs a dev-worker redeploy + mac & iOS rebuild for the value to flow on dogfood. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Build-channel label: component-based parse + handle future RC Align MacBuildChannel with the canonical SocketPathMarkerFiles.variant mapping: the channel is the component right AFTER com.cmuxterm.app (a tagged channel build appends a further .slug, e.g. com.cmuxterm.app.nightly.my-feature), so match the component, not a naive suffix. Adds 'rc' -> 'RC' so a future release-candidate desktop build (com.cmuxterm.app.rc) is labeled correctly the moment it ships, plus debug/dev -> DEV and an unknown future component -> no guess. Tests cover RC, slugged channel bundles, and the dev-tag-wins case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Computers: don't show contradictory 'presence unknown' when connected A Mac the phone is actively connected to (green, with workspaces) was still showing 'Presence: unknown' on its row — contradictory and confusing, since the live connection already proves the Mac is up. Presence is a SEPARATE signal (the Mac's heartbeat to the presence worker), and a dev phone watching the dev worker won't see a Mac that heartbeats to prod — so 'unknown' is common and meaningless next to 'Connected'. Row: when connected and the presence worker has no record, drop the 'Presence: unknown' and show just the route (real presence data still shows). Detail's 'Presence (from server)' section: when connected, say 'no heartbeat (connected directly)' instead of a bare 'unknown'. en+ja localized. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Computers detail: a connected Mac reads 'Online' (connection is the truth) Follow-up to the presence-unknown fix: the root cause is that presence heartbeat is currently a DEV-only feature — stable cmux Macs don't announce presence (Release default OFF, no prod presence URL shipped), so a Mac you're connected to genuinely has no server heartbeat. Showing 'no heartbeat' for a Mac you're actively using reads as broken. Now, when the phone is connected, the detail's Presence section leads with 'Reports: Online' (the live connection proves it) plus a 'Source: this phone's connection (no server heartbeat)' clarifier, and the footer explains presence is a dev-only signal today. The row already shows just the route when connected. en+ja. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Make presence production-ready: prod URL + follow the mobile toggle Presence was dev-only (Release default OFF, no prod URL). Make it ship on stable, gated on the mobile feature per the desired model: default OFF, ON when the user enables mobile. - PresenceSettings.isEnabled: an explicit override still wins, but with no stored value presence now FOLLOWS MobileHostService.isListeningEnabled (the iOS-pairing master switch). Default (mobile off) => off for privacy; turning on mobile pairing turns on presence automatically. Replaces the old DEBUG-on/Release-off. - Mac resolvedServiceURL: Release now defaults to the production worker (presence.cmux.dev) instead of nil, so a stable Mac with mobile on heartbeats to prod. Debug still uses the dev worker. - iOS PresenceServiceConfiguration: Release now defaults to the production worker too, so a stable iOS app subscribes to the same service stable Macs report to (env/UserDefaults/Info.plist overrides unchanged). On merge, CI (presence.yml) deploys the updated worker (bundleId + customization merge + tombstone cap) to prod, completing the production path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address iOS policy review cleanup * Fix paired Mac team scoping and aggregation guards * Satisfy paired Mac autoreview policy gate * Fix paired Mac team scope ownership * Fix quit confirmation reentrancy * Fix scoped backup and workspace action gates * Fix paired Mac legacy claim and selection remap * Fix team active legacy scope * Fix anonymous aggregation and backup actives * Fix visible legacy Mac customization scope * Fix legacy Mac active clearing scope * Make paired Mac backup decode tolerant * Fix stale route writes across team switches * Fix notification deeplink scope and backup URL joining * Provision secrets for isolated presence workers * Propagate paired Mac backup tombstones * Keep stale team loads from clearing current lists * Fix foreground suppression and secondary downgrades * Fix paired Mac backup review findings * Fix paired Mac scope and dismiss flush races * Satisfy iOS package convention lint * Fix visual line copy mode Ghostty API usage --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 2 个月前 | |
Enforce iPhone+simulator default for iOS verification with an offline install queue (#9232) * Enforce iPhone+simulator default for iOS verification with an offline install queue iOS verification reloads now target BOTH an isolated per-tag simulator (cmux-dev-<slug>, created on demand) and the configured iPhone (CMUX_IPHONE_DEVICE_ID or ~/.config/cmux/iphone-device-id; never hardcoded). When the phone is unreachable at build time, the signed build is parked in a persistent queue (scripts/iphone-install-queue.sh, under ~/Library/Application Support/cmux-dev/iphone-install-queue) and a LaunchAgent (scripts/install-iphone-queue-agent.sh) auto-installs and launches it within seconds of the phone reconnecting, via launchd IOKit matching on Apple USB attach, WatchPaths on the queue, and a periodic network backstop, then sends a cmux notification. Every phone build hard-requires the same-tag Mac dev build: ios/scripts/reload.sh builds the Mac tag first when missing and refuses phone-only otherwise. scripts/ios-sim-install.sh installs cloud-built simulator apps into the isolated simulator for the reload-cloud-ios path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Probe device reachability through the queue script in ios/scripts/reload.sh One probe implementation (iphone-install-queue.sh probe) now decides "unreachable" for both the local and cloud reload paths, including the CMUX_IPHONE_QUEUE_FORCE_UNREACHABLE test hook; select_device still owns name/ambiguity resolution for reachable devices and its failure is treated as unreachable as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address review findings: name-target queueing, enqueue race, fail-closed sim install A --device-name target no longer probes or queues against the DEFAULT device id (queueing for a different phone than the one named would install on the wrong device); name targets error with a hint to use --device-id when unreachable. drain_entry now re-reads enqueued_at before every terminal action so a re-enqueue during an in-flight drain leaves the newer build queued instead of silently deleting or failing it. ios-sim-install.sh fails closed on an unreadable CFBundleIdentifier. Also: quote $tab expansions (SC2295), correct help sed ranges, document the one-time LaunchAgent install in CLAUDE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Nudge PR sync Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
Run cmux iOS over authenticated Iroh transport (#7908) * feat(iroh): bridge the production Swift endpoint * feat(iroh): run the mac host transport * test(iroh): reject local binding substitution * test(ios): cover pooled fallback route * fix(iroh): bind discovery to the local app instance * fix(ios): retain successful pooled route * test(iroh): require online-first host policy fallback * feat(iroh): start hosts from verified offline policy * feat(iroh): persist mac offline host policy * fix(iroh): reject partial online binding rotation * test(iroh): reject unvalidated private fallback * feat(iroh): own iOS endpoint and multistream sessions * fix(iroh): revalidate private fallback paths * fix(iroh): accept JSON media type parameters * ci(iroh): test full app on Intel Sonoma * ci(iroh): run transport tests on Intel Sonoma * test(iroh): cover abandoned relay reservations * fix(iroh): expire abandoned relay reservations * feat(iroh): expose admitted host multistream sessions * test(iroh): require bounded incoming streams * fix(iroh): bound peer-created QUIC streams * feat(iroh): defer iOS transport activation * feat(ios): prefer verified Iroh routes * test(iroh): preserve endpoint on preferred port collision * test(iroh): cover LAN rendezvous consistency * fix(iroh): fall back from occupied preferred port * feat(iroh): derive private rotating LAN aliases * fix(iroh): serialize LAN discovery with revocation * feat(iroh): make secure pairing the default * docs(iroh): record offline and LAN trust boundaries * feat(iroh): cache verified client policy offline * docs(iroh): add Apple and proxy launch caveats * docs(iroh): clarify Apple local network prompting * test(iroh): cover offline cache teardown races * fix(iroh): fence offline cache teardown * test(iroh): cover online admission leases * feat(iroh): gate online admission leases * feat(iroh): add authenticated Bonjour LAN fallback * feat(iroh): enforce online revocation leases * test(iroh): cover offline admission leases * test(iroh): cover canonical trust errors * fix(iroh): harden trust broker boundaries * fix(iroh): bound offline admission leases * test(iroh): cover policy refresh revision races * fix(iroh): fence admission policy refreshes * docs(iroh): narrow private network release scope * test(tailscale): reject unbound bearer routes * fix(mobile): state private network boundaries * test(tailscale): reject unbound bearer routes Cover numeric-only Tailscale bearer routes and reject authorization, DNS, and route substitution before transport writes. * docs(iroh): specify NAT authorization barrier * fix(tailscale): bind bearer writes to live tunnel * test(iroh): require acknowledged NAT admission barrier * test(tailscale): reject route-only transport bypass * fix(tailscale): close route-only transport bypass * fix(iroh): acknowledge NAT admission before app streams * test(iroh): hide database failure details * fix(iroh): defer reservation constraint validation * test(iroh): retain revocation monitor after handoff * fix(iroh): retain revocation monitor for connection * test(iroh): reject broker credential redirects * fix(iroh): block broker credential redirects * test(iroh): fail closed on terminal foreground policy * fix(iroh): fail closed on terminal policy refresh * test(iroh): prevent raw fallback after admission failure * fix(iroh): pin authenticated pairings to Iroh * test(auth): reject device registry redirects * test(iroh): lock registration identity and relay bootstrap * fix(iroh): preserve registration trust identity * fix(auth): reject credentialed API redirects * test(iroh): keep direct paths out of cloud storage * fix(iroh): keep direct paths device local * test(iroh): evict remotely closed client sessions * test(iroh): recover dead session on foreground * test(iroh): prevent server path-hint disclosure * fix(iroh): recover suspended client sessions * fix(iroh): keep private paths off server surfaces * test(iroh): reject overlapping LAN bootstrap routes * fix(iroh): reject ambiguous LAN interfaces * test(iroh): bound pending admissions per identity * fix(iroh): limit pending admissions per peer * test(iroh): require owned server event stream * test(auth): bound credentialed HTTP responses * test(iroh): reject concurrent control owners * fix(auth): cap credentialed HTTP responses * test(iroh): cover firewall dependency failures * fix(iroh): bound firewall availability checks * feat(iroh): deliver server events on owned stream * test(iroh): cap stalled firewall work * test(iroh): bound active sessions per binding * fix(iroh): cap stalled firewall work * fix(iroh): cap active sessions per binding * test(iroh): require firewall timeout recovery * fix(iroh): abort stalled firewall checks * fix(ci): isolate Iroh transport test suites * test(iroh): route revocation to broker delete * fix(iroh): send revocation to broker route * test(mobile): bound concurrent RPC work * fix(mobile): cap concurrent RPC work * test(mobile): bound decoded frame batches * fix(mobile): cap decoded frame batches * test(iroh): bound pending Bonjour resolves * test(iroh): gate reserved application lanes * test(iroh): retain failed binding revocations * fix(iroh): bound pending Bonjour resolves * fix(iroh): gate reserved application lanes * docs(iroh): narrow production multistream claims * build(iroh): pin attested Swift fork release * test(iroh): require retry-safe binding revocation * fix(iroh): make binding revocation retry-safe * fix(iroh): durably retry binding revocations * build(iroh): lock iOS Swift fork release * test(iroh): retain Bonjour observation lifetime * test(auth): prepare before raced sign-out clear * test(iroh): quarantine failed sign-out persistence * test(ios): quarantine failed Iroh sign-out * fix(iroh): quarantine incomplete sign-out teardown * fix(iroh): clear host network state in quarantine * fix(auth): quarantine Iroh before sign-out clear * fix(ios): quarantine incomplete Iroh sign-out * fix(mobile): type Iroh binding snapshot * fix(ios): wait for auth clear before Iroh recovery * build(iroh): lock app Swift fork release * fix(iroh): persist secrets in ad-hoc debug builds * test(iroh): require local-only HTTP minter opt-in * feat(iroh): add loopback relay minter runner * test(tailscale): require numeric registry targets * feat(iroh): gate local relay minter HTTP * fix(iroh): normalize local minter opt-in * test(ios): require tagged API origin bake * fix(ios): bake tagged API origin * fix(tailscale): pin MagicDNS remotes to peer IPs * fix(tailscale): reject inactive peer snapshots * build(iroh): pin hardened FFI release * test(auth): preserve auto-login during token reads * test(auth): preserve manual sign-in during token reads * test(iroh): require startup network event delivery * fix(iroh): establish endpoint observation before activation * fix(auth): preserve active session writers * test(iroh): accept existing binding registration responses * fix(iroh): accept existing binding relay status * test(iroh): keep host active after refresh throttling * fix(iroh): preserve host during broker throttling * test(iroh): preserve client during broker throttling * fix(iroh): retain verified policy during broker outages * fix(iroh): decode broker dates on older macOS * test(iroh): reject synthetic network change floods * fix(iroh): observe address changes without feedback loop * test(iroh): accept canonical UUID identity case * fix(iroh): canonicalize pinned device UUIDs * fix(iroh): harden compatibility and private routes * refactor(iroh): split runtime ownership boundaries * test(iroh): repair authorization suite split boundaries * test(iroh): link mobile RPC authorization tests * test(iroh): support compatibility compilers * test(iroh): cover uppercase UUID fallback paths * fix(iroh): canonicalize device UUID authority * test(iroh): support Intel Sonoma compiler * test(iroh): avoid non-Sendable fixture captures * fix(updater): handle Intel-only Sparkle reason * test(iroh): cover bearer and discovery overload * fix(iroh): close route and discovery gaps * fix(ci): close Iroh compatibility regressions * feat(iroh): integrate endpoint-bound relay fleet * fix(ci): wrap command timers for Intel Swift * fix(ios): expose relay deployment to Sendable factory * test(ci): cover private networking on Intel Sonoma * test(ci): support Intel Swift Testing macros * test(iroh): expose relay refresh expiry gap * fix(iroh): retry relay refresh before expiry * fix(ios): serialize Iroh quarantine recovery * refactor(auth): isolate lifecycle revision API * fix(ci): eliminate Iroh Swift 6 warnings * fix(ci): support Intel Xcode 16.2 * fix(ci): mark canvas clock sleep sendable * fix(ci): bridge canvas preferences to main actor * fix(ci): support sidebar git on Xcode 16.2 * fix(ci): mark RPC termination handler sendable * fix(ci): support CLI on Xcode 16.2 * fix(ci): support app target on Xcode 16.2 * fix(ci): finish Xcode 16.2 source compatibility * iroh: point the broker relay fleet at the 7 self-hosted relay.cmux.dev URLs Replaces the 4 hosted iroh.link relays with our self-hosted fleet in both allowlists (web MANAGED_RELAY_URLS + presence worker APPROVED_IROH_RELAY_URLS, kept in lockstep) and the tests that referenced hosted URLs. The self-hosted relays run iroh-relay 1.0.2 behind per-region MIG+L4-LB (zero-downtime upgrades), gated by the cmux EdDSA JWT that /api/relay/token (merged, #7879) mints. * fix(ci): support trailing closure on Xcode 16.2 * ci: allow Intel compatibility suite to finish * test(ci): avoid Xcode 16.2 require recursion * ci: focus Intel compatibility coverage * fix(ci): stabilize replay ownership and Intel budget * test(ios): isolate authoritative resync coverage * feat(iroh): add secure flexible relay policy * test(iroh): split relay runtime coverage * test(iroh): allow self-hosted broker without legacy minter * fix(iroh): make hosted relay minter optional * test(iroh): cover public firewall host fallback * fix(iroh): use public host for firewall checks * fix(iroh): keep accepts and sign-out responsive * test(iroh): reproduce lost online reachability * fix(iroh): republish endpoint online routes * test(iroh): reproduce coalesced route refresh * fix(iroh): replay coalesced route refreshes * refactor(iroh): split oversized runtime files * test(iroh): cover lifecycle refresh races * fix(iroh): fence lifecycle refresh work * test(ios): reproduce loopback dev auto-pair race * test(ios): cover redacted dev Iroh attach URLs * fix(ios): wait for redacted Iroh dev attach ticket * test(ios): reproduce Iroh cold-start attach race * fix(ios): await Iroh before dev auto-pair * feat(iroh): add server-driven relay preferences * feat(iroh): complete relay controls and multistream runtime * ci: rehearse staging migrations from dispatched branch * Make managed Iroh credentials server-driven * feat(iroh): expose redacted live path diagnostics * security(iroh): stage relay policy key rotation * fix(iroh): use instance-scoped host display name * test(iroh): require dev attach targets to prefer identity routes * fix(iroh): prefer identity routes for dev attach * fix(web): include shared relay catalog in Next root * test(web): keep relay catalog inside Next boundary * fix(web): generate relay catalog inside runtime boundaries * test(mobile): cover transport lifetime ownership * fix(mobile): retain Iroh transport lifetime * test(iroh): cover relay policy clock skew * fix(iroh): tolerate bounded relay policy clock skew * test(iroh): cover admitted session lifetime * fix(iroh): separate admission and session lifetimes * test(iroh): cover relay and route renewal stalls * fix(iroh): keep relay routes renewed through storage stalls * test(iroh): cover nonblocking binding persistence * fix(iroh): publish bindings before secure persistence * test(iroh): cover strict transport verification modes * feat(iroh): add strict transport verification modes * test(iroh): await nonblocking relay persistence * test(iroh): cover live peer connection quotas * fix(iroh): bound live sessions per endpoint * test(iroh): cover broker-aware route renewal backoff * fix(iroh): back off broker route renewal retries * test(mobile): cover superseded Iroh transport cleanup * fix(iroh): close unowned mobile sessions * test(iroh): reproduce stale reconnect sessions * fix(iroh): replace stale peer sessions on admission * feat(iroh): add debug transport mode menu * Add regression coverage for Iroh merge blockers * Fix Iroh relay and reconnect merge blockers --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> Co-authored-by: Aziz Albahar <aziz@manaflow.ai> | 1 个月前 | |
Document workspace-action in the cmux skill (#8177) The cmux skill covered windows, workspaces, panes, surfaces, focus, moves, reorder, identify, and trigger-flash, but never mentioned `cmux workspace-action` — the command behind the workspace context-menu actions (set-color, set-description, rename, pin, mark-read, move-up/down, ...). Because those actions live under `workspace-action` rather than as `cmux workspace` subcommands, they were effectively undiscoverable from the skill: an agent reading it (or exploring `cmux workspace --help`) would wrongly conclude there was no CLI to color or describe a workspace. Add a "Context-Menu Actions" section to references/windows-workspaces.md with the full action/flag set and named-color list, plus Fast Start examples and a reference-table hint in SKILL.md so it's found on first look. | 1 个月前 | |
Enforce iPhone+simulator default for iOS verification with an offline install queue (#9232) * Enforce iPhone+simulator default for iOS verification with an offline install queue iOS verification reloads now target BOTH an isolated per-tag simulator (cmux-dev-<slug>, created on demand) and the configured iPhone (CMUX_IPHONE_DEVICE_ID or ~/.config/cmux/iphone-device-id; never hardcoded). When the phone is unreachable at build time, the signed build is parked in a persistent queue (scripts/iphone-install-queue.sh, under ~/Library/Application Support/cmux-dev/iphone-install-queue) and a LaunchAgent (scripts/install-iphone-queue-agent.sh) auto-installs and launches it within seconds of the phone reconnecting, via launchd IOKit matching on Apple USB attach, WatchPaths on the queue, and a periodic network backstop, then sends a cmux notification. Every phone build hard-requires the same-tag Mac dev build: ios/scripts/reload.sh builds the Mac tag first when missing and refuses phone-only otherwise. scripts/ios-sim-install.sh installs cloud-built simulator apps into the isolated simulator for the reload-cloud-ios path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Probe device reachability through the queue script in ios/scripts/reload.sh One probe implementation (iphone-install-queue.sh probe) now decides "unreachable" for both the local and cloud reload paths, including the CMUX_IPHONE_QUEUE_FORCE_UNREACHABLE test hook; select_device still owns name/ambiguity resolution for reachable devices and its failure is treated as unreachable as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address review findings: name-target queueing, enqueue race, fail-closed sim install A --device-name target no longer probes or queues against the DEFAULT device id (queueing for a different phone than the one named would install on the wrong device); name targets error with a hint to use --device-id when unreachable. drain_entry now re-reads enqueued_at before every terminal action so a re-enqueue during an in-flight drain leaves the newer build queued instead of silently deleting or failing it. ios-sim-install.sh fails closed on an unreadable CFBundleIdentifier. Also: quote $tab expansions (SC2295), correct help sed ranges, document the one-time LaunchAgent install in CLAUDE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Nudge PR sync Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
Add native iPhone and iPad Simulator panes (#7857) * tighten simulator shortcut and mutation paths * Harden simulator operation commit boundaries * Close simulator routing and deadline races * Reconcile simulator mutations during teardown * Preserve simulator caller routing context * Route ios commands from caller pane * Preserve explicit simulator routing and input recovery * Serialize simulator text recovery * Align simulator deadlines and selection rollback * Close simulator lifecycle commit races * Reject stale simulator discovery results * Bound simulator re-enable and batch capture * Select the integrated simulator display * Close simulator transition races * Gate simulator automation on readiness * Finish simulator selection generation checks * Bound simulator context work end to end * Verify simulator descendant identities * Validate simulator process ancestry * Bound simulator process shutdown * Supervise simulator command groups * Supervise simulator pane sessions * Contain every simulator subprocess * Test simulator routing and feature flag regressions * Fail closed on stale simulator state * Test simulator lifecycle review regressions * Bound simulator control lifecycles * Test simulator cross-pane ownership regressions * Serialize simulator cross-pane mutations * Test Web Inspector cross-worker ownership * Lease Web Inspector targets across workers * Test Web Inspector stale occupancy handoff * Refresh Web Inspector occupancy during handoff * Test Web Inspector occupancy timeout * Fail closed on incomplete Inspector occupancy * Test Inspector census and release regressions * Close Inspector refresh and release races * Test stale location route teardown * Preserve location route ownership through teardown * Test Simulator launch environment privacy * Test route commit during device switch * Test feature flag telemetry consent * Own Simulator mutations through teardown * Harden Simulator isolation and ownership * Persist Simulator mutation ownership across processes * Honor telemetry consent for remote flags * Preserve failed Simulator cleanup ownership * Propagate Simulator CLI routing errors * Fix Simulator operation task syntax * Inject Simulator ownership and keep context read-only * Resolve restored Simulator context without booting * Test late Simulator display identity publication * Attach Simulator callbacks before display discovery * Test Simulator mutation ownership boundaries * Harden Simulator mutation ownership semantics * Test current Simulator default-screen contract * Support current Simulator default-screen metadata * Isolate Simulator camera transport tests * Test forwarded Simulator screen metadata * Support forwarded Simulator screen metadata * Align Simulator overlay lifecycle test with visibility * Bound Simulator recovery assertion by time * Make Simulator input recovery test deterministic * Test Simulator landscape framebuffer direction * Correct Simulator landscape presentation direction * Bound Simulator frame publication test wait * Register Simulator replay state before delivery * Test Simulator review boundary cases * Test iOS screenshot surface identity errors * Normalize Simulator control boundaries * Isolate Simulator CLI contract environment * Test native Simulator orientation dialects * Unify native Simulator orientation semantics * Test landscape Simulator digitizer coordinates * Map landscape input into native digitizer space * Test stable Simulator app switcher hold * Hold app switcher gesture stationary * Test Simulator app switcher button timing * Open Simulator app switcher with double Home * Test installed iPad DeviceKit chrome fallback * Test bounded Simulator frame publication * Scale Simulator frames to pane geometry * Test Simulator frame ring replacement cleanup * Release obsolete Simulator frame rings * Route IndexNow jobs through configured runner * Remove wall-clock assertions from RPC event tests * Test frame ring adoption race * Retire Simulator frame rings after host adoption * Keep frame completion on MainActor * Snapshot Simulator activity log before lazy layout * Regenerate webview assets after main merge * Test Simulator core readiness ordering * Stream Simulator before optional capability probes * Test control-socket Simulator selection ownership * Exclude active Simulator control action from teardown * Test natural DeviceKit chrome cap geometry * Preserve native DeviceKit chrome artwork geometry * Fix design mode test payload shadowing * Make Simulator replay tests signal-driven * Fix Simulator pane test client conformance * Test immediate Simulator context discovery * Discover Simulator before context reads * Test Simulator production edge cases * Close Simulator production review findings * Fix Simulator integration test fixtures * Fix Simulator CLI routing call site * Fix Simulator focus test fixtures * Fix Simulator app test fixtures * Test Simulator capability hydration readiness * Wait for Simulator capability hydration * Test Simulator review edge cases * Close Simulator production review gaps * Avoid recursive Simulator picker comparison * Isolate Simulator picker observation * Test Simulator application row snapshots * Snapshot Simulator application picker rows * Test static Simulator frame presentation * Drive Simulator frames without display callbacks * Test Simulator visibility remounts * Keep Simulator frames through host remounts * Isolate Simulator visibility regression suite * Test Simulator frame pacing under input load * Pace Simulator framebuffer readback * Localize project surface labels * Test Camera Injector header cache identity * Invalidate Camera Injector cache for headers * Test Simulator RPC capability discovery * Advertise Simulator RPC capabilities * test(simulator): cover interactive frame priority * fix(simulator): prioritize frames after pointer input * test(simulator): cover native tap hold duration * fix(simulator): hold synthetic taps long enough for iPadOS * Test immediate Simulator frame presentation * Present Simulator frames without an extra tick * Test failed Simulator ownership publication * Reject unsafe Simulator ownership claims * test(simulator): cover framebuffer lifecycle bounds * fix(simulator): bound framebuffer lifecycle work * test(simulator): cover review lifecycle regressions * fix(simulator): close review lifecycle gaps * test(simulator): cover routing pacing and consent * fix(simulator): scope routing pacing and consent * test(simulator): cover screenshot and cached log readiness * fix(simulator): prepare capture without eager lifecycle work * test(simulator): report capture-ready live state * fix(simulator): report live capture-ready state * test(simulator): cover control-plane and frame wakeups * fix(simulator): signal frames and preserve errored flag cache * test(simulator): cover publication wakeup races * fix(simulator): bound publication wakeups * test(simulator): cover tool editor shortcut focus * fix(simulator): preserve tool editor focus ownership * test(simulator): cover flag omission and runner injection * fix(simulator): inject async owned command execution * test(simulator): cover file drop proposal readiness * fix(simulator): validate file drop proposals * test(simulator): cover compound inspector cleanup failure * fix(simulator): preserve failed inspector cleanup state * test(simulator): cover device-scoped tool state * fix(simulator): scope tool state to selected device * test(simulator): cover final release blockers * fix(simulator): close final release blockers * test(simulator): make frame scheduling assertions deterministic * test: update Ghostty surface config ABI lock * fix(simulator): clear final build gates * fix(build): import Dock lifecycle workspace types * test(web): isolate feedback route environment * fix(build): disambiguate Dock snapshot types * test(simulator): cover review ownership blockers * fix(simulator): scope camera cleanup ownership * fix(build): make resume policy returns explicit * test(simulator): cover camera cleanup ownership retries * fix(simulator): preserve camera cleanup ownership * fix(build): clear latest main gates * test(simulator): cover final review blockers * fix(simulator): await quit cleanup and index targets * refactor(simulator): satisfy production policy * test(simulator): cover camera cleanup on device switch * fix(simulator): clean camera state before device switch * test(simulator): retain quit cleanup after panel removal * fix(simulator): make quit await durable rollback * test(simulator): cover retained cleanup recovery * fix(simulator): recover retained camera cleanup * test(simulator): cover cleanup side effects * fix(simulator): restore external cleanup state * refactor(simulator): split camera authorization record * test(web): respect delegated discovery order * test(ios): assert transition math deterministically * fix(ci): repair current-main Swift integration * fix(ci): return closed workspace restore result * test(simulator): cover final review regressions * fix(simulator): close final lifecycle gaps * test(simulator): cover review lifecycle findings * fix(simulator): resolve review lifecycle findings * test(simulator): cover durable recovery journals * fix(simulator): persist mutation recovery journals * refactor(simulator): inject durable recovery paths * test(simulator): cover durable journal transitions * fix(simulator): make recovery transitions crash consistent * test(simulator): cover recovery compatibility gaps * fix(simulator): preserve recovery compatibility * test(simulator): cover recovery ownership handoff * fix(simulator): gate recovery ownership handoff * test(simulator): cover duplicate camera journals * fix(simulator): suppress stale durable camera journals * test(simulator): cover journal reconciliation races * fix(simulator): serialize journal reconciliation * test(simulator): cover identical journal paths * fix(simulator): normalize camera journal paths * test(simulator): cover journal URL hints * fix(simulator): compare normalized journal paths * refactor(simulator): split legacy route fixture * test(simulator): observe journal lock contention | 1 个月前 | |
Fix Codex Security scan findings (#7437) * Add security regression coverage * Fix security scan findings * Address security review follow-ups * Fix review regressions in lease cleanup * Fix final security review findings * Fix remaining autoreview findings * Address final autoreview regressions * Keep active identity cleanup best effort * Bound active identity cleanup * Bound SSH cleanup before endpoint minting * Make cleanup retries bounded and releasable * Bound active identity cleanup preflight * Preserve vault grants on retry presign failure * Guard vault grant rollback state * Back off failed expired lease cleanup * Separate vault quota lock namespace * Tighten VM identity cleanup ordering * Fail closed without VM team membership * Bound VM identity cleanup fanout * Rollback endpoint resume on cleanup failure * Remove nondeterministic vault upload test wait * Fail closed on destroy identity cleanup * Recreate Base when active provider VM is gone * Keep Freestyle attach independent of exec probe * Scope provider identity not-found handling * Use reservation tokens for vault upload rollback * Validate vault upload grants at commit * Bound identity cleanup and duplicate vault reservations * Stage vault uploads before commit * Keep vault staging cleanup retryable * Reuse active vault upload staging keys * Preserve legacy vault upload commits * Make vault staging cleanup recoverable * Avoid endpoint resume rollback races * Serialize vault upload grant cleanup * Finalize vault staging outside quota locks * Track superseded vault upload keys | 2 个月前 | |
Preserve fork menu validation fallback identity | 1 个月前 | |
Add public company information page (#9240) Co-authored-by: cmux-lawrence <cmux-lawrence@cmux-lawrences-Mac-mini.local> | 1 个月前 | |
Fix Retina scaling in design mode annotation crops (#8831) * test(browser): reproduce Retina-scaled screenshot crops * fix(browser): crop screenshots at native pixel scale * test: update Ghostty surface config ABI lock * fix: remove redundant diagnostic log await * test(browser): require transparent annotation outlines * fix(browser): keep drawn annotations over live page * test(browser): verify cropped annotation pixels --------- Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> | 1 个月前 | |
iOS: stable Keychain device id + Forget computer (iroh re-key client) (#8888) * iOS: stable Keychain device id + Forget computer (iroh re-key client) Client complement to the broker binding re-key (manaflow-ai/cmux#8883), which changes the iroh binding slot from unique(app_instance_id) to unique(user_id, device_uuid, tag) and replaces the 409 binding_replacement_requires_revocation with a newest-authenticated-wins in-place UPDATE. Two changes make the phone cooperate with that slot: 1. Stable device id across reinstall. The iOS device-registry id moves from UserDefaults (erased on delete/reinstall) to a device-only Keychain item (service com.cmuxterm.deviceRegistry.iosDeviceID.v1, AfterFirstUnlockThisDeviceOnly). A returning phone now presents the same device_uuid and overwrites its own binding in place instead of stranding a fresh one. Keychain is authoritative; a pre-Keychain UserDefaults id is migrated on first read, and the generated id is mirrored back to UserDefaults for downgrade safety. This service is distinct from the iroh endpoint-identity store that sign-out/reinstall wipes, so forgetting the endpoint identity does not churn the slot key. 2. Forget a hidden computer. The per-phone Hidden Computers list gains a destructive Forget action (swipe + context menu, both gated behind a confirmation dialog, mirroring MacComputerRow's Hide) that revokes the Mac's account binding through the user-ownership-scoped broker endpoint. It resolves the binding id at action time via a fresh broker.discover() (so an offline Mac's binding is still listed and revocable), matches by canonical device id plus exact tag when known, revokes each match, then clears the local hidden marker and paired-Mac row. A still-online Mac re-registers and reappears on its next connect. Failure keeps the row and surfaces a toast. New narrow capability MobileIrohMacForgetting keeps the shell store's dependency minimal; en+ja localization added for the Forget copy. * iOS: fail closed on unreadable device id, alert on Forget failure, pin account Address the four P1 review findings on the iroh re-key iOS client branch. Finding 1 (device-id read ambiguity): DeviceIdentityStoring.read() returned an optional, collapsing "no id yet" and "Keychain locked before first unlock" into nil. A background launch before first unlock therefore looked like a fresh install and minted a NEW id, stranding the phone's existing (user, device, tag) binding. read() now returns DeviceIdentityReadResult (.found/.absent/ .unavailable). deviceID(store:defaults:) fails closed on .unavailable: it reuses the legacy UserDefaults mirror if readable, else a per-process ephemeral id that is never persisted, so the durable id is adopted once the store unlocks. A .found id is re-mirrored to UserDefaults (only when it differs) for downgrade safety; a present-but-blank/corrupt item is treated as .absent and re-minted. Finding 2 (account pinning): MobileIrohRuntimeComposition pins the expected account and ensureAccountUnchanged guards Forget so a token-source swap mid-flow can't revoke a binding under the wrong account (MobileIrohForgetError. accountChanged). Finding 3 (Forget ordering): MobileShellComposite forget removes the row before clearing the hidden marker and returns Bool so a failed broker revoke surfaces instead of silently dropping the row. Finding 4 (Forget failure visibility): DeviceTreeView shows a .alert (not a toast) on Forget failure, so the error surfaces even with the Toasts beta flag off. Keys mobile.computers.forget.failureTitle/failureMessage, mobile.common.ok localized en+ja. CmuxMobileShell host-compiles and its 21 DeviceRegistry tests pass (incl. new fail-closed + re-mirror coverage). DeviceTreeView and MobileIrohRuntimeComposition transitively need GhosttyKit, so they compile only in the fleet iOS build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: harden iroh re-key client per review (device-id, session snapshot) Address the P1 findings from review of the iroh re-key client changes. Finding 1 (composition-half): re-resolve the durable device id at each activation via DeviceRegistryService.durableDeviceID(defaults:) instead of capturing it once at root init. A value captured while the durable identity store was unavailable (Keychain locked before first unlock, or a persistent write failure) is an ephemeral throwaway id; registering a binding under it would orphan the retained (user, device, tag) binding. When the durable id is nil, activation now defers (throws .inactive) and retries on the next reconcile once the store becomes readable. The injected resolver is @MainActor () -> String? so it can capture UserDefaults, which is not Sendable under Swift 6. Finding 2: forgetComputer now pins the revoke to one atomic AuthenticatedSessionSnapshot (session generation + account id + both tokens) captured from a single auth-session generation, and the caller passes the row's captured expectedAccountID. Reading the observed identity and the live tokens separately let a lagging observed id authorize a revoke that then ran with a different account's freshly-stored tokens. The broker token source and every mid-flight re-check now require BOTH the generation and the account id to be unchanged, so a sign-out/sign-in (even as the same user) aborts safely. Finding 4: clear the captured scope's durable row and hidden marker unconditionally after a successful revoke. removeStoredPairedMacRow targets the CAPTURED scope, so it cannot touch another account's data; skipping it on a mid-flight scope flip reported success while the row survived, so returning to the old scope showed the supposedly forgotten computer. Tests: activationDefersWhenDurableDeviceIDUnavailable proves no endpoint binds and the retained binding survives when the durable id is unavailable; forgetRemovesCapturedScopeRowEvenWhenScopeFlipsMidRevoke proves the captured account is forwarded and the row is removed on a mid-revoke scope flip; DeviceRegistryRouteSelectionTests cover the durable-id defer/mirror/adopt paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: failing test — forget of team-less Mac deletes wrong team on mid-revoke switch The forget-hidden-computer flow snapshots its owner scope before the async iroh revoke, then deletes the stored row. When the captured scope is team-less (no team selected) and the user switches into a team while the revoke is in flight, local cleanup goes through the team-scoping decorator's plain remove, which substitutes a nil teamID with the now-current team. It deletes that team's row and leaves the forgotten team-less computer behind, so it reappears on returning to no-team. This commit adds only the failing regression test (drives forgetHiddenComputer through a TeamScoped-wrapped store with a mid-revoke team flip); the fix follows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: forget deletes the exact captured scope, not the live team Add removeExactScope to MobilePairedMacStoring: same shape as remove but it never substitutes a nil teamID with the currently-selected team. The team-scope decorator (TeamScopedPairedMacStore) and the backup mirror (BackingUpPairedMacStore) override it to forward the captured teamID verbatim; the base SQLite store, MobileMacCompatible, and IOSBuildScoped decorators inherit the default forward (none of them substitute, so plain remove and removeExactScope are equivalent there). forgetHiddenComputer captures its owner scope before the async iroh revoke, so removeStoredPairedMacRow now deletes via removeExactScope — a mid-revoke team switch can no longer retarget a team-less forget onto the freshly-selected team. Also call clearSavedMacHintWhenNoStoredMacsRemainIfNeeded() on the forget path after reloading, matching the hide path, so forgetting the last stored Mac drops the saved-Mac hint instead of leaving a dangling reference. Makes the prior commit's regression test pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: converge device identity under races, gate snapshot during token transition Device id (FIX #3): adoptOrGenerateDeviceID now goes through Keychain createOrAdopt instead of last-writer-wins write. createOrAdopt does SecItemAdd first and, on errSecDuplicateItem, adopts the value already stored, so two launches racing to mint an id converge on one instead of overwriting each other and registering two device rows against the broker. The UserDefaults mirror is reconciled to the winning id; Keychain stays authoritative and survives app reinstalls so the broker binding is not orphaned. Session snapshot (FIX #1): authenticatedSessionSnapshot() now also requires !sessionTokenTransitionIsActive in both guards, so a snapshot taken mid token rotation cannot hand back a half-swapped session that would drive a redundant re-register. Adds convergence coverage in DeviceRegistryRouteSelectionTests (createOrAdopt adopts the concurrent winner rather than minting a second id). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: correct forget-scope regression test to genuinely catch mid-revoke team flip The committed version of this test asserted contradictory post-conditions, so it did not actually prove removeExactScope deleted the right row. Rewrite it to load the base store once and partition rows by each row's own stamped teamID (loadAll(teamID: nil) returns every team's rows, and loadAll(teamID:) also returns team-less rows, so the returned set must be filtered by teamID to prove which row was deleted). This version is red against the current visibleScope-based removeExactScope: it deletes the flipped team-b row and the team-less row survives, failing at the team-b assertion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: forget deletes the exact captured team scope, no visibleScope re-derivation removeExactScope forwarded through visibleScope/visibleMac, which call inner.loadAll(teamID:): a nil team returns every team's rows and a set team also returns team-less rows, ordered by lastSeenAt descending, so .first could resolve a DIFFERENT team's row than the scope captured before the async revoke and delete that row instead. When the user switches into a team mid-revoke, the team-less forget then deleted the freshly-selected team's row and left the forgotten team-less computer behind. Make removeExactScope a pure pass-through to inner.removeExactScope, honoring the exact (stackUserID, teamID, instanceTag) owner key verbatim; the layers below do not substitute the team. Turns the regression test green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: break corrupt-Keychain mint deadlock; move in-memory device store to tests createOrAdopt, on errSecDuplicateItem, reads the item to converge racing callers on one id. But read() maps a present-but-undecodable item to .absent (so a fresh caller re-mints over garbage), which created a deadlock: a corrupt Keychain item made every SecItemAdd return errSecDuplicateItem while read() kept returning .absent, so the device could never mint a device-registry id and iroh activation stayed permanently disabled. On .absent after a duplicate, overwrite the corrupt item via SecItemUpdate and return desired, or nil (retry a clean add) if a concurrent delete raced it to errSecItemNotFound. .unavailable still defers so a locked-before-first-unlock item is never clobbered. Also relocate the InMemoryDeviceIdentityStore test double out of the production target into the test target; nothing in production or the app referenced it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: hidden-computer unhide spinner tracks its own task, not forget's The unhide Button's ProgressView keyed off forgetTask, so it never spun during an actual unhide and could spin during an unrelated forget. performUnhide sets actionTask; key the unhide spinner off actionTask. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: failing tests for forget deleting wrong paired-Mac scope Two regression tests, RED before the fix (commit adds tests only): - Finding 2 (release-reachable): a team-less pairing shown under a selected team (legacy visibility) is forgotten; the forget captures the LIVE display scope and deletes with it, so removeExactScope(teamID: "team-a") misses the team-less row, the hidden marker is cleared, and the row resurfaces as a normal computer on returning to no-team. - Finding 3 (dev/tagged builds): removeExactScope falls back to the protocol-default remove through MobileMacCompatiblePairedMacStore over IOSBuildScopedPairedMacStore, so an exact-scope team removal also deletes the co-located team-less build-scope fallback row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: forget deletes each pairing's own captured scope, not the live display scope The forget flow captured the live display scope and deleted with it, so a team-less paired-Mac row shown under a selected team (fetchAllMacs legacy visibility) was missed by removeExactScope(teamID: "team-a"); the hidden marker cleared and the row resurfaced (Finding 2, release-reachable). Plumb each row's own stackUserID/teamID through MobileHiddenComputer and delete with the row's own scope. Keep exact-scope removal exact through both store decorators: add removeExactScope overrides to MobileMacCompatiblePairedMacStore and IOSBuildScopedPairedMacStore so the call no longer falls back to the protocol default remove, which over-deleted the team-less build-scope fallback via scopedTeamID(nil) on dev/tagged builds (Finding 3). The pre-existing flip regression test seeded team-less then team-b for the same device+instanceTag, but base upsert claims the team-less row into team-b (moveMacRowScope), collapsing both into one team-b row, so the old assertions passed vacuously (forget deleted a nonexistent owner_key). Reorder the seed (team row first, which a later team-less upsert never claims) so two genuinely independent rows exist, and forget the team-less one explicitly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: failing tests for forget backup-team routing, revoke pinning, broker credential pairing Three autoreview findings on the forget/revoke path, each with a failing regression test. This commit adds only the tests plus the inert API surface they reference; the behavior fixes land in the next commit so CI goes red then green. A. removeExactScope reuses the nil local team for the backup tombstone, so a team-less row forgotten under a selected team routes its backup delete to whatever team is selected at flush time (can wipe the wrong team's backup). New removeExactScope(...backupTeamID:) surface (default forwards to the 4-arg, so behavior is unchanged until BackingUp overrides it next commit). B. forgetHiddenComputer pins the revoke to the LIVE session account instead of the row's owning account, so a row left on screen after an account switch can revoke the new account's binding. Test only; the fix is a one-line arg change. C. The broker reads access and refresh tokens through two independent snapshot calls; a force refresh between them pairs a stale access token with a rotated refresh token. New CmxIrohBrokerCredentials + credentialPair surface (unused by performRequest until next commit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: fix forget backup-team routing, revoke account pinning, broker credential pairing Behavior fixes for the three autoreview findings; the failing tests from the prior commit now pass (CI red -> green). A. BackingUpPairedMacStore.removeMirroring now takes a separate `backupTeam` scope: the local row still deletes under `team` (nil stays nil), but the backup tombstone routes to `backupTeam`. The new removeExactScope(...backupTeamID:) override supplies the captured display team, and MobileShellComposite's forget passes `displayScope.teamID`, so a team-less row forgotten under a selected team tombstones the right per-team Durable Object instead of whatever team is selected at flush time. B. forgetHiddenComputer pins the revoke to `computer.stackUserID ?? scope.userID` (the row's owning account) instead of the live session, so the runtime forget's generation/account check fails closed when a stale row is forgotten after an account switch, rather than revoking the new account's binding. C. CmxIrohTrustBrokerClient.performRequest prefers tokenSource.credentialPair (both tokens from one snapshot) over the two independent closures, and MobileIrohRuntimeComposition supplies a credentialPair closure that captures one authenticatedSessionSnapshot under the same generation/account pinning. A force refresh mid-request can no longer pair a stale access token with a rotated refresh token. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: failing test — session snapshot pairs stale access with rotated refresh authenticatedSessionSnapshot() reads the access and refresh tokens through two separate awaits (currentTokens()), so a concurrent force refresh can rotate the pair between them and hand the broker an old access token with a new refresh token. Neither snapshot guard trips on a plain token rotation. The test scripts that torn store state and asserts the snapshot returns the access minted for the captured refresh, not the stale stored access. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: session snapshot derives access from the captured refresh token authenticatedSessionSnapshot() now reads both tokens through consistentTokenPair(), which captures the refresh token once and mints the access token FOR that exact refresh via freshAccessToken(accessToken: nil, refreshToken:). The returned access always belongs to the returned refresh, so a concurrent forceRefreshAccessToken() can no longer hand the iroh broker an old access token paired with a rotated refresh token. currentTokens() is unchanged for its broader callers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: failing test — forget routes backup tombstone to display team A team-less row's backup was uploaded under the row's own (nil) team scope, but forgetting it routes the tombstone to whatever team it happened to be displayed under. The tombstone lands in the wrong per-team backup scope: the row's real backup survives (and a restore under the row's own scope can resurrect the forgotten row), while a same-device record in the displayed team's backup can be wrongly deleted. Replaces the previous test, which asserted the display-team routing as the desired behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: route forget backup tombstone to the row's own team scope The forget path routed the backup delete to the team the row was displayed under. For a team-less row that team is arbitrary (legacy visibility shows it under every selected team), while upsert stamps the row and uploads its backup under one resolved team, so the row's own team_id is the only client-side value tied to where the backup lives. Display-team routing also split the pending- delete lifecycle across two scopes: the tombstone was written and flushed under the display team's outbox scope, but a restore under the row's own (team-less) scope never saw it and could resurrect the forgotten row locally. Route the tombstone to the row's own captured team, the same scope the backup was uploaded under, keeping outbox key, local apply, flush, and restore- suppression on one scope. This removes the removeExactScope(backupTeamID:) variant entirely; the 4-arg exact-scope delete already carries the row's own team. Residual: a row uploaded while no team was selected client-side had its backup scope resolved server-side, and that resolution is not echoed back or persisted, so no client-only routing can name that scope with certainty. The symmetric nil route re-resolves through the same server path as the upload. Persisting a server-echoed backup team is a cross-stack follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing test — pending-delete replay deletes a surviving sibling row A forget whose backup upload fails leaves its tombstone in the outbox; the next read replays it through the broad remove path. TeamScopedPairedMacStore's remove re-resolves the device under the scope's team, which also returns team-less legacy rows, so with the exact row already deleted locally the replay resolves a SURVIVING unrelated alias of the same device and deletes it — the exact over-deletion the exact-scope forget path exists to prevent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: replay pending backup tombstones through the exact-scope delete A pending tombstone names one exact pairing and its outbox scope key pins the exact (account, team) it was deleted under, so the replay's only job is to finish or confirm that one deletion. Replaying through the broad remove re-resolved visibility on the way down: TeamScopedPairedMacStore looks the device up under the scope's team (which also returns team-less legacy rows) and the build-scope decorator's broad remove drops its team-less fallback alias. In the common failed-upload case the exact row is already deleted, so the broad replay resolved a surviving unrelated alias of the same device and deleted it. Replaying via removeExactScope is a no-op there and, after a crash between the tombstone write and the local delete, removes exactly the named row. Residual: a crash-interrupted BROAD remove now replays exact too, so a team-less build-fallback alias can outlive that narrow window in dev builds; it resurfaces visibly and the next hide drops it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing test — wildcard forget leaves the device's sibling rows saved A row with no instance tag cannot name its broker binding, so forgetting it revokes EVERY binding for the device. The local cleanup deleted only the exact nil-tag row, leaving the device's coexisting tagged rows saved locally while their bindings were just revoked: dead entries that resurface in the computer list until the Mac happens to re-register. A tag-known forget stays narrow on both sides (second test, passing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: match wildcard forget's local cleanup to its revoke breadth A tag-less row cannot name its own broker binding, so forgetting it revokes every binding of the device for the pinned account. Local cleanup deleted only the exact nil-tag row, stranding the device's coexisting tagged rows as dead entries whose bindings were just revoked. After the wildcard revoke the forget now also deletes the device's tagged sibling rows visible in the captured display scope and owned by the pinned account, each through the same exact-scope removal as the primary row. Tag-known forgets stay narrow on both sides. Rows in other teams' scopes are not enumerable through the scoped store rail and self-heal when the Mac re-registers; rows owned by other accounts keep their live bindings and survive. Closes https://github.com/manaflow-ai/cmux/issues/9078. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing test — forget mints a Stack token for every broker leg The forget flow captures one coherent session snapshot up front, but the broker token source re-snapshots on every request, and each snapshot now mints a fresh access token over the network. Discovery plus every sequential revoke each add a Stack round-trip, so forgetting a computer with many bindings can stall for minutes and fail during a Stack outage even though the pinned credentials in hand are valid. The test drives a forget across four broker legs through a broker fake that fetches one credential pair per request, exactly like the real client, and expects a single mint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: reuse the forget's pinned credential pair for every broker leg The forget captures one coherent session snapshot up front; the broker token source now returns that pinned pair after only the cheap local session check (generation + account), instead of re-capturing a snapshot per request. Each snapshot performs a network token mint, so the old path added a Stack round-trip for the discovery and for every sequential revoke: forgetting a computer with many bindings could stall for minutes and fail during a Stack outage despite holding valid credentials. The pinned pair is coherent by construction, and the access token always travels with its refresh token, so the server can re-mint server-side if it expires mid-operation. A mid-forget sign-out or account switch still fails the check and yields nil, so the revoke fails closed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing test — tombstone ignores the server-reported backup team A team-less row uploads with a nil team and the SERVER resolves which per-team Durable Object stores it; that resolution is not derivable client-side and can drift by the time the row is forgotten. The new uploadReportingResolvedTeam seam (default: echo unknown) lets a transport report the verified team an upload was stored under; the failing test shows the backing-up store discards the echo and re-resolves nil at delete time, so the tombstone can land in a different team's backup than the record it is meant to delete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: route delete tombstones to the server-reported backup team A team-less row uploads with a nil team and the presence worker resolves which per-team Durable Object stores it. That resolution is not derivable client-side and can drift by the time the row is forgotten, so re-resolving nil at delete time could send the tombstone to a different team's backup: the forgotten Mac's record survived and restored later, and a same-device record in the wrong team could be deleted. The worker now echoes its verified resolved team in the backup POST and GET responses (from the DO, which receives the verified value). The client persists the echo per pairing in a UserDefaults-backed map owned by the backing-up store, and the tombstone flush groups pending deletes by each pairing's persisted backup team (falling back to the scope's own team when no echo was ever seen), uploading each group to the backup its records actually live in. A flushed pairing's mapping is dropped with its backup record. Legacy rows converge on their next successful upload; restores still fetch the live scope (read-path residual, benign). Closes https://github.com/manaflow-ai/cmux/issues/9076. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — restore drops the backup-team echo; wildcard forget refreshes per sibling Two gaps in the round-4 fixes. Restored rows never pass through the upload path, so the reinstall case (empty mapping store, rows arriving via restore) loses the server's statement of where their backups live: a later forget re-resolves nil and the wrong-backup deletion returns for exactly the restored rows. The snapshot now carries the worker's echoed resolved team so the restore can persist it. And the wildcard forget's cleanup refreshes the paired list per deleted sibling, re-running the backup restore fetch each time — up to the 256-binding snapshot limit of sequential round-trips for one tap; the new test pins the whole cleanup to at most one refresh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: persist the restore snapshot's backup team; batch wildcard cleanup The restore path now records the worker's echoed resolved team for EVERY live record in the snapshot (not just locally-written ones — each record lives in that team's backup regardless of the local merge outcome), so a row restored after a reinstall and forgotten later routes its delete tombstone to the backup it actually lives in instead of re-resolving nil at delete time. The wildcard forget now deletes all of the device's rows first and runs ONE refresh (paired list + registry + reconnect hint) after the batch, instead of reloading per deleted sibling — each per-row reload also re-ran the backup restore fetch because the removal clears the restore memo, so a forget covering many bindings issued that many sequential network round-trips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iroh: make the coherent credential pair the broker token source's only input CmxIrohBrokerTokenSource previously accepted independent access and refresh closures with the coherent pair optional. Several production constructions (iOS reconcile/quarantine paths, macOS host activation) omitted the pair, and their two closures each called auth.currentTokens() separately, so a session transition between the two reads could assemble one session's access token with another's refresh token and fail registration, discovery, or revocation. The pair closure is now the ONLY construction input, so a two-source token assembly is no longer expressible; the single-token accessors are derived from the pair. Every construction site provides a coherent capture: pinned-session pairs for the forget flow, pairs captured together up front for sign-out revokes, and a single currentTokens() call per fetch for the runtime paths. The performRequest legacy two-closure branch is gone. No new regression test: the removed hazard is inexpressible at compile time, and CmxIrohBrokerCredentialPairTests keeps asserting each request performs exactly one atomic capture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-5 review findings A wildcard forget must delete the device's same-account rows in OTHER teams (their bindings were revoked account-wide and an offline Mac cannot re-register to self-heal); the activation broker's credentials must fail closed after an account switch instead of vending the new session's tokens against the old activation; and a legacy device-id whose Keychain migration cannot persist is NOT durable (a reinstall wipes the only copy and strands the slot). Supersedes the adopt-legacy-despite-failed-persist test and the scope-flip test's sibling-survives assertion, both of which pinned the rejected contracts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: pin activation credentials; cross-team wildcard cleanup; defer non-durable legacy id Round-5 review fixes. The activation path now captures one coherent session snapshot, verifies it belongs to the activating account, and pins the broker token source to it (same helper as the forget path): a mid-activation account switch makes every later leg fail closed instead of mutating the new account's broker state against the old activation's endpoint identity. Wildcard forget cleanup now enumerates the device through a new cross-team loadAllInstances seam on the paired-Mac store rail — the team-scoping decorator forwards it verbatim (its live-team substitution is exactly what the cleanup must see past), the build-scope decorator bounds it to its own build scope, and the backup decorator forwards without triggering a restore. Every same-account row of the device is deleted by its own exact scope, matching the account-wide revoke. DeviceRegistryService no longer reports a legacy UserDefaults id as durable when the Keychain migration write fails: the store was readable (id absent) but nothing durable holds the id, so binding activation defers and retries instead of registering a slot a reinstall would strand. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-6 review findings A valid stored access token must be reusable without a network mint (forcing a mint made the session snapshot, and with it broker activation, fail offline despite a usable stored pair); and the persisted backup-team echo must be keyed by the row's own team — the local store deliberately allows the same (account, device, tag) pairing under several teams, so a team-agnostic key let team B's upload overwrite team A's destination and route A's tombstone into B's backup. Fixture fakes gain the SDK's likely-valid reuse semantics; the forget test's mint expectation drops to zero accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iroh: store-level coherent pair, per-request pinned activation source, keyed echo, forget deadline Round-6 review fixes, one architectural piece plus three scoped ones. coherentTokenPair() replaces the always-minting snapshot read: capture the refresh token, resolve a usable access token FOR it (the SDK reuses a valid stored access without the network and mints only otherwise), then re-read the refresh — an unchanged refresh proves no rotation crossed the window, a changed one retries. It runs inside the coordinator's bounded token-touching phase. The session snapshot, the iOS quarantine-recovery source, and the macOS host activation source all read through it, so no torn two-await assembly remains and an offline launch with a valid stored pair succeeds. Activation no longer freezes an activation-time pair for the runtime's lifetime (ordinary force-refresh rotation does not bump the session generation, so a frozen pair went stale and stranded relay refresh and discovery until an unrelated reconcile). The activation gate is now a cheap local identity check — no token read, so offline activation still reaches the cached relay/offline-policy recovery — and every broker request re-checks the account/generation pin and re-reads a coherent pair from the store. The backup-team echo mapping key now includes the row's own team, and the forget revoke loop gets a 60-second operation deadline (deadlineExceeded surfaces the failure; applied revokes stand and a retry re-discovers what remains) instead of up to 256 sequential broker timeouts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-7 review findings An ordinary same-account foreground revalidation must not advance the session generation (every generation-pinned broker source would starve after the first foreground), and a UserDefaults device-id mirror must never be adopted when the Keychain authoritatively reports the id absent — the mirror travels in device backups onto NEW phones while the ThisDeviceOnly Keychain item does not, so adoption would make two physical devices fight over one (user, device, tag) slot on every phone upgrade. Also pins persist-and-reuse of refreshed access tokens across repeated coherent captures (contract coverage: the ephemeral side-store defect is not expressible through the fake), and reworks the fakes to model the live store's stale-refresh-persist semantics. Supersedes the legacy-mirror-adoption migration test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iroh: round-7 identity and credential lifecycle fixes Same-account revalidation no longer bumps the session generation: the bump now happens only on a genuine transition (signed-out -> signed-in, or a different account), so generation-pinned broker sources survive ordinary foreground returns while sign-out/sign-in still fences stale flows. The device id is minted fresh when the Keychain authoritatively reports it absent, never adopted from the UserDefaults mirror (which migrates in phone backups and would collide two physical devices onto one binding slot); the mirror remains trusted only while the Keychain is temporarily unreadable. This deliberately drops the seamless pre-Keychain upgrade migration — a one-time re-pair for existing installs — to prevent a permanent cross-device identity collision on every phone upgrade. The coherent pair now resolves the access token through the LIVE store inside the refresh bracket, so a stale token is refreshed once, persisted, and deduplicated by the SDK instead of re-minted per capture through an ephemeral side store. The long-lived activation source reads a full authenticated snapshot per request (atomic identity+credential capture, transition-checked) validated against the activation pin, closing the check-then-read race. Both credential containers get redacted descriptions so reflection cannot copy live tokens into logs or crash reports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-8 review findings An in-place upgrade (Keychain absent, mirror holding the id the live binding already uses, no witness recorded) must ADOPT the mirror — minting there changes every existing installation's identity once and strands all of their bindings. A mirror whose recorded device witness belongs to ANOTHER phone (a restored backup) must still mint fresh, and a witness matching this phone adopts. These pin the provenance mechanism that separates the two cases the last two rounds traded against each other. (The tests reference the new witness parameter, so this commit is red at compile time without the fix.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iroh: device-witness provenance for the id mirror; pin the macOS broker source The UserDefaults device-id mirror now carries a per-device witness (identifierForVendor — a value a restored phone does not inherit), written on every mirror update. On authoritative Keychain absence the mirror is adopted only when the witness proves it was recorded on THIS device or predates the mechanism (the in-place upgrade population, whose mirror holds the id their live binding already uses); a mismatched witness means a backup restored onto another phone, which mints fresh so two physical devices never share one (user, device, tag) slot. The locked-Keychain fallback applies the same test. Residual: restoring a PRE-witness backup onto a new phone is indistinguishable from an upgrade and adopts — bounded to backups taken before this ships. The macOS host runtime's broker source now mirrors the iOS one: activation verifies the live account, captures the generation, and every request reads an atomic authenticated snapshot validated against that pin, so an A-to-B account switch fails the old runtime's requests closed instead of registering B's credentials against A's endpoint state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-9 review findings A wildcard forget's tombstones must travel in ONE request per destination (a device can carry 256 bindings, and per-row flushes each burn a request timeout); a pending tombstone must be visible to restores of its DESTINATION scope, which must both suppress the deleted record and retry the flush; an unmapped team-less tombstone must PARK instead of shipping with a guessed nil team the server would re-resolve from current account state; and a failed cross-team sibling enumeration is a cleanup failure, not silent success. Legacy tests that modeled the pre-echo worker now arm the echo; the nil-team routing test is superseded by the parked contract, and the crash-intent test becomes the mapping-recovery test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iroh: destination-keyed tombstone outbox, batched wildcard flush, propagated enumeration failure Round-9 review fixes. Pending backup tombstones are now keyed by their DESTINATION scope — the team whose Durable Object actually holds the record (the persisted echo, else the row's own concrete team) — with the row's LOCAL team encoded in each record for exact local replay. A restore of the destination therefore both suppresses the deleted record while its upload is pending and retries the flush, closing the resurrect-and-never-retry gap of local-scope keying. A team-less row with NO verified destination is parked under the nil-team scope and never uploaded with a guessed nil team; parked intents migrate to their destination and flush once a restore's echo recovers the verified mapping. Legacy single-field records decode as local==scope, preserving old outboxes. Residual, documented in code: while parked, a restore of a different team's scope cannot see the intent and may resurrect the record there; re-forgetting that row routes exactly, which is recoverable — unlike a misrouted destructive delete. removeExactScopes batches several rows: local deletes and outbox writes first, then ONE tombstone flush per destination, replacing the per-row flush that gave a wildcard forget up to one network round-trip per row. The composite deletes the primary and all wildcard siblings through one batch and clears markers only after it succeeds, and a failed sibling enumeration now fails the forget instead of silently claiming success after an account-wide revoke. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-10 review findings A TAGGED forget's revoke is also account-wide for that (device, tag) binding, so same-tag rows in other teams must be cleaned too while different-tag rows survive; and reviving one team's row must clear only THAT row's pending tombstone — the destination-keyed outbox can hold same-pairing records for different local teams, and cancelling them all lets another team's forgotten record survive in the backup and restore later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: tag-scoped cross-team forget cleanup; revive clears only its own row's tombstone Round-10 review fixes. Cross-team sibling cleanup now runs for EVERY forget: a tagged revoke kills the (device, tag) binding account-wide, so other teams' same-tag rows are dead and get cleaned, while different-tag rows keep their own live bindings and survive; the tag-less wildcard keeps its every-tag breadth. And a revive clears only the pending tombstone whose LOCAL team matches the re-added row — same-pairing records for other local teams in the same destination stay pending, so their forgotten backup records still get deleted instead of surviving to restore later. Legacy unscoped records decode their local team from the scope they sit in and so match only in the re-added row's own scope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-11 review findings Three confirmed defects, each with a failing test: - A wildcard forget's exact-scope cleanup silently skips rows whose instance tag is incompatible with this build, while the tombstone still flushes and the forget reports success; the revoked-binding row survives to resurface as a dead entry. - Forget clears hidden markers only in the display scope; markers are stored per (user, team), so another team's marker survives its row's deletion and keeps a re-registering Mac unexpectedly hidden there. - A whitespace-only persisted device identity classifies as .found, so the corrupt-item repair deadlocks: the mint path re-reads and adopts the same whitespace value and every launch advertises an invalid opaque device id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: exact-scope deletes match wildcard breadth; markers and identity repair Round-11 review fixes: - The build-compatibility store no longer guards exact-scope deletes. An exact-scope delete targets a row the cleanup explicitly captured from loadAllInstances, and the broker's wildcard revoke is tag-blind, so the local cleanup must cover incompatible tags too; the guard let the tombstone flush and the forget report success while the revoked-binding row survived. Ambient verbs keep the guard. - Forget clears each deleted row's hidden marker in that row's OWN team scope in addition to the display scope. Markers are stored per (user, team); clearing only the display scope left another team's marker to keep a re-registering Mac unexpectedly hidden there. - KeychainDeviceIdentityStore classifies a whitespace-only item as corrupt (.absent), so the duplicate-item repair path overwrites it instead of endlessly re-adopting it as .found; the in-memory test double mirrors the contract, now documented on DeviceIdentityStoring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-12 review findings - A pre-witness UserDefaults mirror is adopted on authoritative Keychain absence with no proof this is the same physical device; a backup taken before the witness shipped restores onto a new phone and clones the old phone's (user, device, tag) binding slot. - A concrete-team restore neither suppresses nor resolves a PARKED unknown-destination tombstone, so the supposedly forgotten computer is resurrected locally and its backup survives every future restore. - A partially failed batched cleanup still runs the post-forget refresh, whose rowless-marker migration clears the deleted primary's hidden marker — the retry entry disappears while the failed sibling row keeps its already-revoked binding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: continuity-gated mirror adoption; parked tombstones suppress and resolve Round-12 review fixes: - Pre-witness mirror adoption now requires device-continuity evidence: a non-migrating artifact proving the install continues on this hardware. The probe is the iroh endpoint identity — in Release an AfterFirstUnlockThisDeviceOnly Keychain item that never travels in a backup, and one every install with a live binding necessarily has. A restored pre-witness backup on a new phone lacks it and mints fresh (no more cloned (user, device, tag) slots); an in-place upgrade with a binding has it and keeps its id; an install that never activated iroh mints harmlessly. Both production device-id callers pass the same probe so concurrent resolutions agree, and the locked-Keychain mirror branch defers instead of trusting a possibly-restored mirror. - Every restore's suppression list now includes the account's PARKED (unknown-destination) tombstones, and a verified team's snapshot echo resolves any parked intent whose pairing it contains: the mapping is recorded under the parked record's own key and the parked scope flushes, migrating the intent to its destination and deleting the backup. A forget the user was told succeeded can no longer be resurrected by the next restore. FakeBackup now honors successful delete uploads in its snapshot, mirroring the server. - The post-forget refresh runs only after COMPLETE cleanup, so a partial batch failure keeps the hidden entry as the retry owner instead of letting the rowless-marker migration clear it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing test — round-13 review finding A forget's cleanup enumerates only the LOCAL store, but backups live in per-team Durable Objects and only the selected team's backup has been restored on this phone. The same device's records in another team's backup get no tombstone even though the wildcard revoke killed their bindings account-wide; switching to that team later restores the supposedly forgotten computer as a dead entry. FakeBackup gains a per-team-bucket mode to model the server's per-team storage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: account-wide forget tombstones; device-id resolution off the UI actor Round-13 review fixes: - A forget now parks one ACCOUNT-WIDE tombstone per forgotten pairing in addition to the routed per-row intents. Backups are per-team Durable Objects and only restored teams have local rows, so the local enumeration cannot match the broker revoke's account-wide breadth; the parked intent suppresses the pairing in EVERY team's restore, each verified snapshot that proves its team holds the pairing gets a direct delete (a tag-less intent is the device-wide wildcard and matches every tag, with the snapshot supplying the concrete tags), and the intent persists until a re-pair revives the pairing. Parked intents no longer migrate to a single destination — no single team could retire an account-wide tombstone. - Durable device-id resolution moved off the MainActor for activation: a private actor captures the identifierForVendor witness with one MainActor hop and runs the Keychain reads/writes, defaults mirror, and continuity probe on its own executor, restoring the off-UI-actor guarantee the merge reconciliation had dropped. DeviceRegistryService gains a nonisolated durableDeviceID(defaults:deviceWitness:...) for such callers, and currentDeviceWitness() is public. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-14 review findings - Parked (account-wide) tombstones replay their local delete only when the nil-team scope itself is requested, so an offline launch after a crash keeps showing the supposedly forgotten computer: crash recovery must be network-independent. - The parked tombstone set retires only on revive and grows by every forget forever — unbounded persisted size and per-restore scan work; retention must be bounded. The forget-deadline scope finding (discovery and in-flight broker calls can suspend past the deadline) is fixed in the same round; it lives in the iOS-only cmuxFeature target, where no host-runnable test exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: network-independent parked replay, bounded retention, full forget deadline Round-14 review fixes: - Both restore entry points now replay the account's PARKED tombstones locally before any backup fetch, so crash recovery (outbox written, local delete never landed) works offline instead of depending on the restore's suppression list reaching the network. - The parked account-wide tombstone set is bounded at 256 entries (matching the discovery wire cap): intents are deduped by identity, stamped with a coarse insertion time via an injected clock, and evicted oldest-first when over the cap — an evicted intent's forget has had the longest time to propagate, and losing one degrades to the pre-account-wide behavior for that single pairing. Routed records' encodings are unchanged, so exact-string outbox clearing still works. - The forget deadline now bounds the WHOLE operation: forgetComputer races credential capture, discovery, backpressure waits, and every revoke against a cancellable sleeper, cancelling in-flight broker work at the deadline instead of only checking between revokes; the per-revoke clock checks remain as a cheap early exit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: fix Swift 6 isolation and stale optional binding in cmuxFeature Round-15 review findings — both compile errors in the iOS-only targets (no host-runnable or CI compile covers them, so no regression test is practical): - deviceLocalIrohIdentityExists (and its directory helper) are nonisolated so the off-main resolver actor's synchronous continuity probe closure can call them without a MainActor hop. - The sign-out test fake still optional-bound credentialPair from before it became the token source's only, non-optional input. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: forget deadline sleeper becomes static — extensions cannot hold storage Round-16 review finding: the cancellable sleeper was declared as an instance stored property inside the extension that hosts the forget flow, which does not compile. Static storage keeps the bounded-timeout shape unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing test — round-17 review finding A completed same-account sign-in (fresh credential exchange while already authenticated) preserves the session generation, so operations pinned to the prior session — the forget flow's frozen credential pair, the activation runtime's pinned source — keep passing the session fence with the replaced session's authority. The sibling round-17 finding (the activation path creates the iroh endpoint identity before the device-id continuity probe checks for it, so a restored pre-witness backup sees its own moments-old identity as continuity evidence) is fixed in the same round; it lives in the iOS-only cmuxFeature target, where no host-runnable test exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: sign-in always advances the session generation; probe before identity Round-17 review fixes: - applySignedInUser now takes an explicit SessionPublication reason: a completed credential exchange (.signIn) always advances the session generation, even for the same account, because the token session was replaced and prior-session pins must fail closed; only .revalidation (foreground/startup re-checks of the already-published session) preserves the generation for the same account. - The activation path resolves the durable device id BEFORE creating the iroh endpoint identity. The continuity probe treats a device-local identity as proof the install continues on this hardware; creating the identity first handed a phone restored from a pre-witness backup its own moments-old identity as evidence and adopted the migrated mirror id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: drop @MainActor child annotation the isolation checker cannot verify The hosted iOS build fails on the forget-deadline task group: "pattern that the region-based isolation checker does not understand how to check" at the @MainActor-annotated child. The plain child hops to the MainActor implicitly at the revokeMatchingBindings call, which is exactly what the annotation expressed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-19 review findings - The upload echo is keyed by the live display team, but loadAll's legacy visibility can match a TEAM-LESS row: the forget then looks the mapping up under the row's own nil team, misses it, and parks the tombstone — undeliverable when the network is down at echo time. - A parked delete suspended in its upload can race a concurrent re-pair on the reentrant actor: the revive clears the intent and uploads the record, the older delete lands after it, and nothing repairs the wiped backup. - A partially failed batch cleanup returns before clearing ANY markers; rows deleted before the failure can never be re-enumerated on retry, so their per-team hidden markers keep a re-registering Mac hidden. FakeBackup gains an on-delete-upload hook (to interleave a mutation inside the uploader's suspension window), record-op application to its buckets, and a post-construction fetch-failure switch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: row-keyed echoes, delete/revive reentrancy fences, narrowed marker cleanup Round-19 review fixes: - The upload echo's mapping is keyed by the ROW's stored team (mac.teamID), not the live display scope: loadAll's legacy visibility matches team-less rows under a selected team, and the forget looks the mapping up under the row's own team — a display-keyed echo was never found, leaving the tombstone parked and undeliverable offline. - Both delete uploaders (the concrete-scope flush and the parked echo resolver) now fence against the actor's reentrancy: any sent tombstone whose outbox record vanished during the upload suspension was revived by a concurrent re-pair, so its current local row is re-uploaded — the stale delete can no longer silently wipe the just-revived backup. The concrete flush also retires only the records it SENT, so intents added during the suspension survive to their own flush, and revived records keep their freshly re-saved mapping. - A partially failed batch cleanup clears the markers of rows it DID delete — narrowly: only the deleted row's own team key and the user-wide key, never the display scope, which the failed scope (the retry owner) shares. Rows deleted before the failure can never be re-enumerated on retry, so this is the only moment their markers can be cleared. FakeBackup applies record uploads to its per-team buckets only; the legacy single-bucket mode serves its seeded list to every team, so applying uploads there would leak one team's mirror into every other team's restore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-20 review findings - The account-wide parked intent is inserted only AFTER the batch's local deletes have awaited; a Mac re-registering during that window clears the routed tombstone but cannot clear the not-yet-created parked intent, which then suppresses the revived pairing forever. - The flush retires sent tombstones by set subtraction computed AFTER its post-upload awaits; a re-pair plus second forget during those awaits re-adds the identical encoded record, which the subtraction silently consumes — an undelivered second tombstone loses its retry. - The persisted backup-team mapping grows without bound: entries retire only when THIS device delivers the pairing's tombstone. Test doubles: a paired-Mac store and a team-mapping store that fire a one-shot hook inside their suspension windows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: park before deletes, atomic flush retirement, bounded team mapping Round-20 review fixes: - removeExactScopes resolves accounts and persists the account-wide parked intents BEFORE the first local-delete suspension, so a Mac re-registering during a delete clears every tombstone covering its pairing — routed and parked alike — instead of leaving a stale account-wide intent that would suppress the revived pairing forever. The parked scope now also dedupes by identity in addPendingDelete and applies the same oldest-first cap there, so a row intent never stacks a second encoding beside its account-wide twin and single exact-scope removes cannot grow the scope unbounded. - The concrete flush retires its sent tombstones atomically in one actor turn right after the upload (synchronous cache read + write), before the mapping-cleanup and repair awaits: a re-pair plus second forget interleaving those awaits re-adds its identical record AFTER retirement and keeps its own retry. - The persisted backup-team mapping is bounded at 512 entries with move-to-newest insertion order and oldest-first eviction; losing an evicted mapping degrades that pairing's next forget to the parked, echo-recovered path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-21 review findings - A parked intent matches later snapshots solely by pairing id and is cleared only by a LOCAL re-pair: when another device re-creates the record, this phone deletes the revival on every restore and keeps the intent forever, making cross-device re-pairing impossible to persist. - The restore echo records every snapshot mapping under the restore team, but LWW can retain a NEWER team-less local row un-stamped; the later forget looks the mapping up under the row's actual nil team, misses, and parks — undeliverable when the network drops. The third round-21 finding (a same-account sign-in advances the session generation but the long-lived activation runtimes stay pinned to the old generation and return nil credentials until restart) is fixed in the same round; it lives in the iOS-only and macOS app targets, where no host-runnable test exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: account-pinned runtimes, revival-aware tombstones, retained-row echoes Round-21 review fixes: - The LONG-LIVED activation runtimes (iOS composition and the macOS host) pin their broker token sources to the ACCOUNT only, not the session generation: every completed sign-in now advances the generation, and a same-account re-sign-in must keep the runtime serviceable — it is the same user, so serving the new session's credentials via the atomic snapshot is correct, where the generation pin stranded the runtime on nil credentials until relaunch. The forget's short-lived frozen pair stays strictly generation-pinned. - The restore echo now fires AFTER the merge and carries, per snapshot record, the RETAINED local row's actual team and the record's creation time. Mappings are keyed by the retained row's own scope (LWW can keep a newer team-less row un-stamped, and the forget looks the mapping up under the row's real team), falling back to the restore scope for records with no local row (the reinstall case). - A snapshot record CREATED after a parked intent's stamp is a REVIVAL — another device re-paired the Mac — and retires the intent instead of feeding it a delete; without this the forgetting phone deleted the revival on every restore forever. Unstamped legacy intents keep the old delete behavior (no boundary is known for them). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-22 review findings - A revived record is recognized only AFTER suppression already filtered it out of the merge; with the completed restore memoized, the re-paired Mac stays missing locally until relaunch. - The revival signal compared client-authored createdAt, which another phone preserves across a re-pair; the genuine revival misclassifies as stale and is deleted on every restore. The record model gains the SERVER-authored serverUpdatedAtMs (decoded from the snapshot, never uploaded). - Restore echoes persist mappings one save per record; the production store rewrites its whole state per save, so a large restore does quadratic UserDefaults work. The mapping protocol gains a batched saveAll (default forwards per entry). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: server-authored revival signal, in-merge revivals, batched mappings Round-22 review fixes: - The worker now surfaces the sync machinery's server-authored per-record write time as serverUpdatedAtMs on the restore read (never accepted from clients — sanitize strips it). Revival classification compares THAT against the tombstone's stamp through a shared skew-margined rule biased toward revival: client-authored createdAt is preserved across re-pairs on other phones and proves nothing. - Restore suppression is now stamp-aware: run() takes suppression entries (pairing + tombstone stamp), and a record every covering tombstone sees as revived MERGES in the same restore instead of being filtered out and stranded behind the completed-restore memo until relaunch. The post-merge echo then retires the covering intents. - Restore echoes persist their mappings through one batched saveAll — the UserDefaults store performs a single read-modify-write of its dictionary and ordering for the whole snapshot instead of a full-state rewrite per record. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-23 review findings - The revival skew allowance accepts server writes up to a minute BEFORE the forget as revivals. Forgetting a currently-online Mac whose backup was route-mirrored seconds earlier is the COMMON case; the allowance bypasses suppression, retires the intent, and the supposedly forgotten Mac restores instead of receiving its delete. - A partial batch failure never records a hidden marker for a FAILED undisplayed sibling: the deleted primary's marker turns rowless and is migrated away, so the sibling — with its already-revoked binding — resurfaces as a normal computer with no Hidden Computers entry left to retry from. The third round-23 finding (the sign-out quarantine's destructive retry captures live credentials without pinning them to the pending revocation's account) is fixed in the same round; it lives in the iOS-only cmuxFeature target, where no host-runnable test exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: strict revival boundary, pinned quarantine retry, sibling retry markers Round-23 review fixes: - The revival boundary is STRICT: only a server write after the tombstone's stamp counts. Forgetting a currently-online Mac whose backup was mirrored seconds earlier is the common case, and the skew allowance let those pre-forget writes bypass suppression and retire the intent. The residual (phone clock behind the server) fails in the recoverable direction: the revival is deleted once and the other device's next mirror re-uploads it with a fresh server stamp. - The sign-out quarantine's destructive retry pins its credentials to the pending revocation's account through the atomic session snapshot, failing closed if the user switched accounts between the guard and the credential capture. - A partial batch failure records a hidden marker for every SURVIVING failed scope in its own team, so an undisplayed sibling with a revoked binding keeps a durable Hidden Computers retry entry even offline — where the account-wide parked intent cannot yet finish the cleanup. Once any restore completes it, the marker turns rowless and the existing migration clears it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing test — round-24 review finding The tombstone stamp is floored to whole seconds while server write times carry milliseconds, so a server write from the same second but BEFORE the forget classifies as a post-forget revival: the intent retires and the stale record restores instead of being deleted. Of the two sibling round-24 findings: the forget deadline race is fixed in the same round (the throwing task group structurally awaits an unresponsive cancelled child past the deadline; it lives in the iOS-only cmuxFeature target with no host-runnable test), and the retained-teams dictionary finding is factually incorrect — assigning a String? through the subscript wraps it (Swift removes only when the assigned expression is already the subscript's doubly-optional type), which the passing restoreEchoTracksTheRetainedTeamlessRow regression proves — but the code switches to updateValue(_:forKey:) to make the retention explicit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: millisecond forget boundary, non-blocking deadline, explicit retention Round-24 review fixes: - Tombstone stamps carry epoch MILLISECONDS with an explicit `ms` unit marker in the encoding (bare-integer third fields from earlier builds decode as whole seconds). Flooring to seconds classified a server write from the same second but before the forget as a revival, retiring the intent and restoring the stale record. - The forget deadline no longer structurally awaits the losing racer: a throwing task group waits for every child, so a revoke suspended on a dependency that ignores cooperative cancellation kept the forget busy past the deadline — the exact stalled-request case it exists to recover from. Unstructured racers resolve a one-shot gate; the deadline returns immediately, cancellation is still requested, and the stalled work unwinds in the background. - The restore's retained-row map uses updateValue(_:forKey:) so the retention of a TEAM-LESS row is explicit rather than relying on optional-wrapping subscript semantics (behavior unchanged — the routed-delete regression already proved the entry was stored). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: failing tests — round-25 review findings (bounded pair) - One tagged instance's revival retires the whole DEVICE-WIDE tombstone, dropping suppression and deletion for a stale different-tag record that exists only in another team's backup. - The account-wide parked record stores a nil local team, so offline crash recovery replays only nil-team rows: a concrete-team row whose local delete never landed survives every offline launch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: exact revival retirement; parked records carry their row's team Round-25 review fixes (the two bounded findings): - A revival retires only its EXACT pairing's intent, and the revive-clear mirrors it: one tagged instance returning no longer retires the device-wide tombstone (or clears it on local re-pair), so a stale different-tag record in another team's backup keeps its suppression and still receives its delete. Per-record revival classification lets the revived pairing through everywhere, so retaining the wildcard intent costs the revival nothing; deletes explicitly spare records every covering intent classifies as revived. - Account-wide parked records preserve the captured ROW's local team, so offline crash recovery replays the exact delete for concrete-team rows (a nil local team replayed only nil-team rows). Coverage semantics are unchanged — suppression and echo matching key on the pairing id alone, and the revive-clear cancels the pairing's intents regardless of the recorded team. The two remaining round-25 findings are deferred with rationale in the PR discussion: cross-clock revival ordering (a sound fix needs server-issued causal revisions — a worker protocol change reintroducing a form of server-side tombstones, which this codebase deliberately retired; the strict boundary fails only in the recoverable direction) and post-deadline task abandonment (every dependency in the revoke path is URLSession-backed and cancellation-aware; the detached racer is cancellation-requested and cannot outlive its own bounded requests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS: widen developmentStoreDirectory to fileprivate for the evidence probe The DEBUG same-device evidence probe struct lives at file scope in MobileIrohRuntimeComposition.swift and cannot reach a type-scoped private static. Caught by the on-device build; host-side SwiftPM tests do not compile the iOS-only cmuxFeature target. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Drop committed review logs from the branch Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Restore main's ghostty submodule pin (theme picker fix from #9218) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> | 1 个月前 | |
Fix hanging cmux theme picker (#9207) (#9211) Bump the ghostty submodule to pick up the theme picker fix, and stop the regression test from misreporting the failure. The picker never rendered a frame, so the test never sent Enter, yet it reported that the picker did not exit after Enter. | 1 个月前 | |
Fix CI: remove broken tests for deleted function, fix review comments - Remove tests for shouldLoadReleaseAppSupportGhosttyConfig (function was deleted but tests survived, breaking CI on all branches) - Fix missed localization on optional-chain notification fallback path - Validate explicit-but-empty --pid as invalid input | 5 个月前 | |
Remove Swift file length budget (#8125) Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> | 1 个月前 | |
Mark markdown-viewer bundles as vendored/generated for Linguist (#5212) Resources/markdown-viewer/ carries ~25MB of vendored third-party runtime JS (mermaid, vega, highlight.js, the Pierre/Shiki diff renderer with per-language grammars + WASM) plus the generated diff-viewer React app build. That now outweighs the Swift source, so GitHub Linguist reported the repo as mostly JavaScript. Mark the directory linguist-vendored (and the built app linguist-generated) so the language stats reflect the actual source. No runtime or build change. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 3 个月前 | |
iOS: view artifacts referenced in agent sessions (#7674) * iOS: view artifacts referenced in agent sessions Adds session-scoped mobile.chat.artifact.{stat,fetch,thumbnail,list} RPCs on the Mac host (capability chat.artifact.v1), scoped to paths the session's transcript actually references (attachment hostPaths, fileEdit filePaths, tool referenced_paths) with symlink-resolved canonical comparison, uniform forbidden errors, and one-level access into referenced directories. Fetch is chunked at 3 MiB raw per frame under the 8 MiB frame cap; thumbnails are downscaled via ImageIO off the main actor through the AgentChatArtifactIndex actor. Parsers now emit .attachment messages for cmux clipboard-materialized image paths in user prompts (so attachments survive transcript reload) and populate referenced_paths on tool-use messages fail-open on the wire. iOS renders attachment thumbnails in the transcript, a full-screen viewer (image/text/binary/too-large/missing/unreachable states), and View file / Browse folder affordances in tool and file-edit detail sheets, all gated on the host capability with graceful degradation against older Macs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS terminal view: tap-a-path + Files button to view on-screen artifacts Extends artifact viewing from the agent-chat surface to the iOS terminal view. New session-independent RPCs mobile.terminal.artifact.{scan,stat,fetch, thumbnail} (capability terminal.artifact.v1), scoped to file paths that currently appear in that terminal's on-screen + scrollback text: the Mac captures its own buffer text on the main actor per request, rebuilds the allowed set, and canonically compares before any stat (uniform forbidden for off-screen/traversal/symlink-escape). Relative tokens resolve against the terminal cwd. Byte IO is factored into a shared ArtifactByteReader reused by the chat handlers unchanged; detection/scope/IO run off the main actor. iOS: a Files button in the terminal toolbar lists the on-screen paths (image kinds show thumbnails) and opens the shared artifact viewer; tapping a path token directly in the terminal opens it, while a miss falls through to normal terminal click so input/scroll/selection are unaffected. ChatArtifactLoader generalized to .chat/.terminal scopes; all gated on terminal.artifact.v1 with graceful degradation. Strings localized EN+JA. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS terminal gallery: Files-app list + icon grid popover for the rendered frame Upgrades the terminal bottom-bar Files button into an inline popover gallery of the artifacts recognized in the currently rendered terminal frame. Two modes like the Files app: a list (thumbnail/icon + name + modified date + size) and a 3-column icon grid, toggled in the popover header; both request the same 256px thumbnail so switching modes reuses the scope-aware thumbnail cache with no refetch. Tapping an item opens the existing artifact viewer. Scan gains visible_only (default false, so tap-a-path and existing callers are unchanged): when true the Mac reads only the visible viewport, not scrollback. TerminalArtifactReference carries optional size + modified_at (unix seconds, fail-open decode). Directories are excluded from gallery results. Per-file fetch/stat/thumbnail authorization against the terminal buffer is unchanged. v1 deliberately covers only the rendered frame; folders and high-artifact-count sessions are left as a documented deferred seam. Strings localized EN+JA. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS terminal: fix soft-wrapped path taps + terminal-scoped viewer errors Tapping a file path that the phone re-wrapped across its narrower terminal width used to truncate at the row boundary (/tmp/.../notes.md opened as notes.m and was denied). The tap hit-tester (now in the shared package so it's unit-tested) reads the surface's real grid column count and stitches a path across soft-wrap continuation rows: when a token fills to the last column and the next row begins with a path continuation, they join with no separator, recursing for multi-row wraps; a next row that is whitespace or a fresh prompt does not over-stitch, and continuation-row taps resolve to the full path. The artifact viewer gains a scope (.chat default, terminal callers pass .terminal) so a terminal-opened file shows terminal-appropriate error copy instead of the chat-flavored 'not referenced by the conversation.' EN+JA. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS terminal: floating "N files" gallery chip as the primary signifier Replaces the buried accessory-bar Files button (which sat off-screen at the far right of the horizontally-scrolling key strip) with an auto-surfacing floating chip above the accessory bar. The chip shows a live count of file paths recognized in the currently rendered terminal frame and fades in only when that count is > 0 (and the terminal.artifact.v1 capability is present, in terminal mode); tapping it opens the same gallery (list/grid) as a detented sheet on iPhone, popover on iPad. The count is computed locally by running the path detector over the visible viewport text on the existing coalesced frame-settle signal (8 quiet frames), never per keystroke/output/render, so typing latency is unaffected; the Mac scan stays authoritative when the gallery opens. The chip sits at z=1050 below the zoom HUD, yields while zooming, and only its pill frame takes touches. .files is removed from the default accessory strip via a one-time idempotent migration (kept in Settings, same shared action path). Stale-count state is reset on capability re-enable and surface reattach so the chip re-appears after a Mac reconnect. Strings localized EN+JA incl. plural. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS terminal: stitch soft-wrapped paths cut right after a trailing char The soft-wrap tap stitcher decided whether a token was cut by the wrap using the trailing-TRIMMED path length, so a path wrapping immediately after a trailing-trimmable character (row ends /tmp/.../notes., next row md) read as not-full-width and did not stitch, resolving /tmp/.../notes instead of /tmp/.../notes.md. The hit-tester now tracks the raw (untrimmed) token extent for the wrap-cut decision, concatenates raw fragments across rows, and applies path detection/trimming once to the final assembled string. Same bug class as the notes.m/d report, at the punctuation boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS terminal gallery: UI refinement pass (fit thumbnails, sheet chrome, chip, viewer) Design-review batch refining the terminal artifact gallery, chip, and viewer: Gallery sheet: image thumbnails now fit (not fill/crop); default system sheet background; removed the fixed height clamp that left dead space at the large detent; wrapped in a NavigationStack with an inline Files title, a Done button, and the list/grid toggle in the toolbar; adaptive grid columns. Gallery items: gray thumbnail card only for images (bare hierarchical, tinted glyphs for text/binary/folder, given a square footprint so grid rows align); localized abbreviated "Jul 9, 2026 · 12 KB" metadata; centered grid labels; Dynamic-Type-safe min heights; pressed-state feedback; combined VoiceOver. Chip: correct singular/plural ("1 file"/"3 files"), a files-appropriate icon instead of the magnifying glass, an adaptive hairline that holds on light and dark terminals, and an upward chevron matching the upward reveal. Viewer: large text now renders in a scrollable UITextView instead of one monolithic Text (no layout stutter); image is cleanly fitted (removed the dead two-axis scroll); the too-large message interpolates the real limit; the unsupported-media state no longer shows "Zero KB". Strings localized EN+JA. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS terminal gallery: sharpen for iOS 26 (Liquid Glass, no glass-on-glass) Owner dogfood: the list/grid view-mode picker was a segmented control sitting on the nav bar's glass (glass-on-glass); it's now a single toolbar Menu button whose icon reflects the current mode. Sharpen pass: the floating chip now uses real iOS 26 Liquid Glass (.glassEffect(.regular.interactive(), in: .capsule)) with a regularMaterial fallback below 26, so it lenses the terminal behind it and drops the redundant border and hard shadow. Grid cells all use a uniform rounded tile (images fit inside, glyphs centered) so a mixed grid no longer looks ragged; the chip icon and gallery glyphs/heights scale with Dynamic Type via @ScaledMetric; the press style no longer dims the whole row (thumbnail included), only a subtle background; a single-file chip is no longer over-padded (min width 120→88). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS terminal gallery: session-wide scope with sections, stable paging, search, disk cache The gallery previously showed only artifacts in the currently rendered frame. It now offers an In view | Session scope switcher (content-area segmented control; Session is the default when the terminal has a bound agent session) backed by a new session-scoped mobile.chat.artifact.gallery RPC (capability chat.artifact.gallery.v1). The transcript index now tracks provenance and last reference position, and the gallery renders Products-first disclosure sections: Created by agent and You attached arrive complete on the first page; Referenced pages ~60 at a time through a generation-pinned cursor ordered by last reference, so pages append without ever reshuffling visible rows; the paging footer lives inside the lazy containers so fetches happen on scroll, not render. Whole-session server-side search returns flat results with provenance subtitles. Files deleted since being referenced render dimmed with a localized 'No longer on your Mac' badge. Session items authorize through the existing session-scoped chat artifact verbs; the terminal scan response carries the bound session id (fail-open). Thumbnails persist in a ~100 MB disk LRU keyed by path+mtime+size+dimension with next-page prefetch, so large sessions scroll smoothly and reopen instantly. Strings localized EN+JA. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS terminal gallery: grid missing badge overlays the tile; Session first in scope picker Dogfood nits: the 'No longer on your Mac' badge in icon-grid cells rendered below the reserved name/metadata frames, floating far from its cell and making that cell taller so the grid row centered its tile out of line with neighbors (the ghost.md misalignment). The badge now overlays the bottom of the tile itself, which also restores uniform cell heights. The scope segmented control orders Session before In view to match the default selection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS terminal gallery: metadata hugs the file title Dogfood nit: the date · size line sat too far below the title. List rows tighten the title/subtitle gap to 2pt; grid cells group name + metadata in one top-aligned block sharing the combined height reserve, so the subtitle no longer renders below the name's empty two-line reserve while cell heights stay uniform. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Artifact discovery: fix gaps found by a 300-transcript real-data audit Owner-reported flakiness (Session tab missing obvious files; 'unavailable' for available files) traced via a new env-gated audit harness that runs the real parsers and gallery derivation over historical Claude/Codex transcripts read-only (CMUX_ARTIFACT_AUDIT=1; prints paths only). Fixes: Relative tool file_paths (incl ../ forms) now resolve lexically against the session working directory at index time, with purely lexical . / .. collapsing (no filesystem access, deterministic for deleted paths), so gallery items and the authorization scope always share one canonical absolute form; the index cache keys on cwd. /tmp and /private/tmp normalize to one identity for dedup, stat, and scope. Codex apply_patch (custom and function-call shapes) extracts every patched path with Created provenance; MultiEdit/NotebookEdit provenance regression-tested. Denial logs now say why (not-in-set vs canonicalization-failed vs stat-failed). Shell-redirection/heredoc-created files remain deliberately undetected (needs a real shell parser; regexing command text would be unsafe authorization). Also: the grid missing-file badge renders in the subtitle slot under the cell (the tile overlay covered the icon), and gallery metadata hugs the title. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Codex artifact discovery: parse modern rollout patch_apply_end events Modern codex CLI rollouts emit tool activity as event_msg payloads instead of response_item function/custom tool calls, so new codex sessions derived zero Created artifacts (a real 59-patch rollout produced nothing). Successful patch_apply_end events now synthesize an apply_patch tool use whose referenced paths are the changes keys plus non-null move_path destinations, flowing into Created provenance; failed events emit nothing; stdout/stderr/diffs never surface. Real-rollout cross-check: 0 -> 49 Created items. Also adds an env-gated single-transcript ground-truth dump test (CMUX_ARTIFACT_DUMP, paths-only) used for the verification pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Session artifact discovery: full parity with the In-view detector Owner reported the In-view tab finding more artifacts than the Session tab — an inversion, since terminal text is the session's own output. An independent adversarial spec set the governing rule (Session must contain every absolute path the shared detector can see in session text) with a per-scenario taxonomy and a hard parity metric over real transcripts. Derivation now scans every text channel with the same detector the terminal uses: assistant prose, thinking, shell command strings, raw pre-truncation tool outputs (Glob/LS listings, grep path:line:col hits, error messages), user prose, and sub-agent (sidechain) activity — all as referenced; only structured mutation channels yield created, and only the mutation target (sidechain Write content blobs no longer become created artifacts). The shared detector gains markdown-link and line:col handling (In view improves in lockstep) and symmetrically rejects junk (bare /, code fragments with interior parens or quotes, template placeholders). Key-agnostic structured values count only as single whitespace-free absolute tokens. /var and /etc fold like /tmp; ~ and file:// forms resolve. Parity audit over 300 real transcripts (150 Claude + 150 Codex, read-only): BEFORE 217 violating transcripts with 115,388 missed path references (codex 145/150); AFTER 0 violations, zero junk in the gallery, median growth 1 item. Adversarial fixtures pin every taxonomy row by ID; 272 package tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix CI: accessory enum stability + drop stray iOS workspace lockfile The files accessory case was inserted before ollama, shifting ollama's persisted rawValue from 30 to 31 and failing the append-only pin in TerminalAccessoryConfigurationTests on both simulator jobs. Move files after ollama (rawValue 31) and pin it. The bar position is unaffected: defaultConfigurableOrder curates placement explicitly. Also untrack ios/cmux.xcworkspace/xcshareddata/swiftpm/Package.resolved, committed by accident in 976512f296; check-package-resolved-policy.py rejects that location and failed workflow-guard-tests before the other guard steps could run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS terminal chip counts the whole session The floating artifact chip counted only path tokens in the visible frame, so it disagreed with the Session-first files sheet it opens. The settle cadence still triggers locally, but when the visible snapshot changes the host now asks the Mac for the bound session's complete gallery count via a count_only terminal artifact scan (no terminal-text capture, no stat; the same derivation snapshot the gallery pages, so missing files still count). TerminalArtifactChipCountState keeps one request in flight with one trailing coalesce; generation guards drop responses from a previous attachment. Falls back to the local frame count when the gallery capability, a bound session, or the RPC is unavailable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix chip-count liveness, pay file-length budget debt, run CmuxAgentChat tests in CI A count response dropped for a surface-generation mismatch (generations bump on every output apply, scroll snap, and grid change) with no trailing request left the chip stuck until the visible text changed. The state machine now distinguishes that drop and re-issues one request tagged with the current generation, bounded to three consecutive re-arms per state generation. A zero session total with path tokens on screen reports the local frame count so the chip stays as the entry point to the In-view tab; a positive session total always wins. Stale tasks can no longer clear a newer request's handle. Pays the whole PR's file-length debt by pure code motion into new sub-500 line files (GhosttySurfaceView+Artifacts, GhosttySurfaceCoordinator+ Artifacts, TerminalArtifactFilesSheet+Content, CodexTranscriptParser+ ToolOutput, and seven more); no budget allowance raised. Adds CmuxAgentChat to the swift-package-tests CI lane so the 274 parity and wire-schema tests run on PRs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Guard terminal artifact denial logs for release builds cmuxDebugLog exists only under #if DEBUG; the three bare denial-log calls in the terminal artifact RPCs failed non-DEBUG compiles (Greptile P1). Route them through a #if DEBUG helper like the chat-side denial log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Review fixes: restore ./file detection, product-terms error copy The round-18 bare-root gate standardized every candidate before the two-component floor, so a single-segment relative token like ./notes.md collapsed to one component and stopped being tappable; the floor now applies only to absolute tokens, which is the shape the junk rule was aimed at. The three artifact invalid-params errors named wire parameters (session_id, workspace_id); they now say what cmux couldn't determine, in English and Japanese. Both from CodeRabbit's full review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> | 1 个月前 | |
chore: update release skill metadata | 6 个月前 | |
Fix frozen terminals after split churn (#12) * Fix blank terminal after split operations and add visual tests ## Blank Terminal Fix - Add `needsRefreshAfterWindowChange` flag in GhosttyTerminalView - Force terminal refresh when view is added to window, even if size unchanged - Add `ghostty_surface_refresh()` call in attachToView for same-view reattachment - Add debug logging for surface attachment lifecycle (DEBUG builds only) ## Bonsplit Migration - Add bonsplit as local Swift package (vendor/bonsplit submodule) - Replace custom SplitTree with BonsplitController - Add Panel protocol with TerminalPanel and BrowserPanel implementations - Add SidebarTab as main tab container with BonsplitController - Remove old Splits/ directory (SplitTree, SplitView, TerminalSplitTreeView) ## Visual Screenshot Tests - Add test_visual_screenshots.py for automated visual regression testing - Uses in-app screenshot API (CGWindowListCreateImage) - no screen recording needed - Generates HTML report with before/after comparisons - Tests: splits, browser panels, focus switching, close operations, rapid cycles - Includes annotation fields for easy feedback ## Browser Shortcut (⌘⇧B) - Add keyboard shortcut to open browser panel in current pane - Add openBrowser() method to TabManager - Add shortcut configuration in KeyboardShortcutSettings ## Screenshot Command - Add 'screenshot' command to TerminalController for in-app window capture - Returns OK with screenshot ID and path ## Other - Add tests/visual_output/ and tests/visual_report.html to .gitignore * Add browser title subscription and set tab height to 30px - Subscribe to BrowserPanel.$pageTitle changes to update bonsplit tabs - Update tab titles in real-time as page navigation occurs - Clean up subscriptions when panels are removed - Set bonsplit tab bar and tab height to 30px (in submodule) * Fix socket API regressions in list_surfaces, list_bonsplit_tabs, focus_pane - list_surfaces: Remove [terminal]/[browser] suffix to keep UUID-only format that clients and tests expect for parsing - list_bonsplit_tabs --pane: Properly look up pane by UUID instead of creating a new PaneID (requires bonsplit PaneID.id to be public) - focus_pane: Accept both UUID strings and integer indices as documented * Fix browser panel stability and keyboard shortcuts - Prevent WKWebView focus lifecycle crashes during split/view reshuffles - Match bracket shortcuts via keyCode (Cmd+Shift+[ / ], Cmd+Ctrl+[ / ]) - Support Ghostty config goto_split:* keybinds when WebView is focused - Add focus_webview/is_webview_focused socket commands and regression tests - Rename SidebarTab to Workspace and update docs * Make ctrl+enter keybind test skippable Skip when the Ghostty keybind isn't configured or when osascript can't send keystrokes (no Accessibility permission), so VM runs stay green. * Auto-focus browser omnibar when blank When a browser surface is focused but no URL is loaded yet, focus the address bar instead of the WKWebView. * Stabilize socket surface indexing * Focus browser omnibar escape; add webview keybind UI tests - Escape in omnibar now returns focus to WKWebView\n- Add UI tests for Cmd+Ctrl+H pane navigation with WebKit focused (including Ghostty config)\n- Avoid flaky element screenshots in UpdatePillUITests on the UTM VM * Fix browser drag-to-split blanks and socket parsing * Fix webview-focused shortcuts and stabilize browser splits - Match ctrl/shift shortcuts by keyCode where needed (Ctrl+H, bracket keys) - Load Ghostty goto_split triggers reliably and refresh on config load - Add debug socket helpers: set_shortcut + simulate_shortcut for tests - Convert browser goto_split/keybind tests to socket-based injection (no osascript) - Bump bonsplit for drag-to-split fixes * Fix split layout collapse and harden socket pane APIs * Stabilize OSC 99 notification test timing * Fix terminal focus routing after split reparent * Support simulate_shortcut enter for focus routing test * Stabilize terminal focus routing test * Fix frozen new terminal tabs after many splits * Fix frozen new terminal tabs after splits * Fix terminal freeze on launch/new tabs * Update ghostty submodule * Fix terminal focus/render stalls after split churn * Fix nested split collapsing existing pane * Fix nested split collapse + stabilize new-surface focus * Update bonsplit submodule * Fix SIGINT test flake * Remove bonsplit tab-switch crossfade * Remove PROJECTS.md * Remove bonsplit tab selection animation * Ignore generated test reports * Middle click closes tab * Revert unintended .gitignore change * Fix build after main merge * Revert "Fix build after main merge" This reverts commit 16bf9816d0856b5385d52f886aa5eb50f3c9d9a4. * Revert "Merge remote-tracking branch 'origin/main' into fix/blank-terminal-and-visual-tests" This reverts commit 7c20fb53fd71fea7a19a3673f2dd73e5f0c783c4, reversing changes made to 0aff107d787bc9d8bbc28220090b4ca7af72e040. * Remove tab close fade animation * Use terminal.fill icon * Make terminal tab icon smaller * Match browser globe tab icon size * Bonsplit: tab min width 48 and tighter close button * Bonsplit: smaller tab title font * Show unread notification badge in bonsplit tabs and improve UI polish Sync unread notification state to bonsplit tab badges (blue dot). Improve EmptyPanelView with Terminal/Browser buttons and shortcut hints. Add tooltips to close tab button and search overlay buttons. * Fix reload.sh single-instance safety check on macOS Replace GNU-only `ps -o etimes=` with portable `ps -o etime=` and parse the dd-hh:mm:ss format manually for macOS compatibility. * Centralize keyboard shortcut definitions into Action enum Replace per-shortcut boilerplate with a single Action enum that holds the label, defaults key, and default binding for each shortcut. All call sites now use shortcut(for:). Settings UI is data-driven via ForEach(Action.allCases). Titlebar tooltips update dynamically when shortcuts are changed. Remove duplicate .keyboardShortcut() modifiers from menu items that are already handled by the event monitor. * Fix WKWebView consuming app menu shortcuts and close panel confirmation Add CmuxWebView subclass that routes key equivalents through the main menu before WebKit, so Cmd+N/Cmd+W/tab switching work when a browser pane is focused. Fix Cmd+W close-panel path: bypass Bonsplit delegate gating after the user confirms the running-process dialog by tracking forceCloseTabIds. Add unit tests (CmuxWebViewKeyEquivalentTests) and UI test scaffolding (MenuKeyEquivalentRoutingUITests) with a new cmux-unit Xcode scheme. * Update CLAUDE.md and PROJECTS.md with recent changes CLAUDE.md: enforce --tag for reload commands, add cleanup safety rules. PROJECTS.md: log notification badge, reload.sh fix, Cmd+W fix, WebView key equiv fix, and centralized shortcuts work. * Keep selection index stable on close * Add concepts page documenting terminology hierarchy New docs page explaining Window > Workspace > Pane > Surface > Panel hierarchy with aligned ASCII diagram. Updated tabs.mdx and splits.mdx to use consistent terminology (workspace instead of tab, surface instead of panel) and corrected outdated CLI command references. * Update bonsplit submodule * WIP: improve split close stability and UI regressions * Close terminal panel on child exit; hide terminal dirty dot * Fix split close/focus regressions and stabilize UI tests * Add unread Dock/Cmd+Tab badge with settings toggle * Fix browser-surface shortcuts and Cmd+L browser opening * Snapshot current workspace state before regression fixes * Update bonsplit submodule snapshot * Stabilize split-close regression capture and sidebar resize assertions * Change default Show Notifications shortcut from Cmd+Shift+I to Cmd+I * Fix update check readiness race, enable release update logging, and improve checking spinner * Restore terminal file drop, fix browser omnibar click focus, and add panel workspace ID mutation for surface moves * Add Cmd+digit workspace hints, titlebar shortcut pills, sidebar drag-reorder, and workspace placement settings * Add v2 browser automation API, surface move/reorder commands, and short-handle ref system to TerminalController * Add CLI browser command surface, --id-format flag, and move/reorder commands * Extend test clients with move/reorder APIs, ref-handle support, and increased timeouts * Harden test runner scripts with deterministic builds, retry logic, and robust socket readiness * Stabilize existing test suites with focus-wait helpers, increased timeouts, and API shape updates * Add terminal file drop e2e regression test * Add v2 browser API, CLI ref resolution, and surface move/reorder test suites * Add unit tests for shortcut hints, workspace reorder, drop planner, and update UI test stabilization * Add cmux-debug-windows skill with snapshot script and agent config * Update project docs: mark browser parity and move/reorder phases complete, add parallel agent workflow guidelines * Update bonsplit submodule: re-entrant setPosition guard, tab shortcut hints, and moveTab/reorderTab API * Add browser agent UX improvements: snapshot refs, placement reuse, diagnostics, and skill docs - Upgrade browser.snapshot to emit accessibility tree text with element refs (eN) - Add right-sibling pane reuse policy for browser.open_split placement - Add rich not_found diagnostics with retry logic for selector actions - Support --snapshot-after for post-action verification on mutating commands - Allow browser fill with empty text for clearing inputs - Default CLI --id-format to refs-first (UUIDs opt-in via --id-format uuids|both) - Format legacy new-pane/new-surface output with short surface refs - Add skills/cmuxterm-browser/ and skills/cmuxterm/ end-user skill docs - Add regression tests for placement policy, snapshot refs, diagnostics, and ID defaults * Update bonsplit submodule: keep raster favicons in color when inactive | 6 个月前 | |
Add Vercel ignore | 7 个月前 | |
Pin Xcode 26 (objectVersion 60) and add pbxproj normalizer + CI guard (#4836) * Add deterministic normalizer for cmux.xcodeproj/project.pbxproj scripts/normalize-pbxproj.py sorts the high-churn sections (PBXBuildFile, PBXFileReference, and the files = (...) arrays inside Sources / Resources / Frameworks / CopyFiles build phases) into a deterministic order keyed on the entry comment plus UUID. The Xcode build does not care about the order of these flat dictionary sections; sorting them just kills the nondeterministic diff noise Xcode generates on every UI touch. Does not touch UUIDs, comments, or PBXGroup children = (...) arrays (navigator order is intentional). Idempotent: a second run produces zero diff. Standalone in this commit so the diff is just the script. The next commit applies the script and bumps objectVersion in one shot, so the resulting churn is contained and never repeated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Pin objectVersion = 60 and normalize pbxproj Bumps objectVersion from 56 to 60 (the format Xcode 16+ and Xcode 26 write by default) and runs scripts/normalize-pbxproj.py once to establish the deterministic baseline. After this commit, future diffs to project.pbxproj show only real changes, not Xcode's nondeterministic section reordering. One-time large diff. No semantic changes to targets, sources, build phases, or settings: pure sort + version pin. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add tracked pre-commit hook that normalizes pbxproj scripts/git-hooks/pre-commit calls scripts/normalize-pbxproj.py on cmux.xcodeproj/project.pbxproj when it is staged and re-stages the result. scripts/install-git-hooks.sh points the clone at this directory via `git config core.hooksPath scripts/git-hooks`, and scripts/setup.sh auto-runs it so devs get the hook without a separate manual step. After this, Xcode's nondeterministic reordering of build-file and file-reference sections is canceled out at commit time. The CI guard in the next commit enforces the rule for anyone who bypasses the hook with --no-verify or who never ran setup. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add CI guard for objectVersion pin and pbxproj normalization scripts/check-pbxproj.sh asserts cmux.xcodeproj/project.pbxproj has objectVersion = 60 (Xcode 26 default) and that the file is normalized per scripts/normalize-pbxproj.py. Wired as a step in the workflow-guard-tests job so every PR is gated. This catches anyone who bypasses the pre-commit hook with --no-verify or who never ran scripts/setup.sh. The error message points at the exact fix path. To bump the pin (e.g., when the team adopts a newer Xcode major), edit EXPECTED_OBJECT_VERSION in this script and the matching line in CLAUDE.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add .xcode-version and document Xcode 26 pin in CLAUDE.md .xcode-version records the major (26.0) for tooling that reads it (xcodes CLI, some CI helpers). CLAUDE.md gains an Xcode toolchain section explaining the pin, the normalizer + pre-commit hook + CI guard mechanics, and the procedure for bumping the pin in the future. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Read .xcode-version as the source of truth in check-pbxproj.sh scripts/check-pbxproj.sh now reads .xcode-version and maps the Xcode major to the expected objectVersion via a one-entry case statement. Bumping the team's Xcode pin becomes a one-file edit (.xcode-version), with a script update only required when Apple actually changes objectVersion in a new Xcode major. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Address CodeRabbit findings on check-pbxproj.sh and pre-commit hook scripts/check-pbxproj.sh now passes "$PBXPROJ" explicitly to normalize-pbxproj.py instead of letting it default to a path relative to the current working directory, so the guard works regardless of where CI invokes it. scripts/git-hooks/pre-commit refuses to run when the working-tree pbxproj has unstaged changes. Previously the hook would normalize the working-tree file and `git add` the result, which silently staged any unstaged hunks the user had deliberately left out of the commit. The hook now exits non-zero with a clear message telling the user to either stage the whole file or stash the unstaged hunks first. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Address Greptile findings: misleading comment + bump-step docs scripts/normalize-pbxproj.py: the comment said "preserve empty lines exactly where they are" but the implementation collapses blanks to a trailing group. Reworded the comment to match the actual behavior. CLAUDE.md: the bump procedure now mentions opening cmux.xcodeproj in the new Xcode so objectVersion gets rewritten automatically. Without that step a developer following the docs alone would update only the pin file and the script case, and the CI guard would fail on their next commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> | 3 个月前 | |
agents | 7 个月前 | |
Bump version to 0.64.20 (#8473) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
Enforce iPhone+simulator default for iOS verification with an offline install queue (#9232) * Enforce iPhone+simulator default for iOS verification with an offline install queue iOS verification reloads now target BOTH an isolated per-tag simulator (cmux-dev-<slug>, created on demand) and the configured iPhone (CMUX_IPHONE_DEVICE_ID or ~/.config/cmux/iphone-device-id; never hardcoded). When the phone is unreachable at build time, the signed build is parked in a persistent queue (scripts/iphone-install-queue.sh, under ~/Library/Application Support/cmux-dev/iphone-install-queue) and a LaunchAgent (scripts/install-iphone-queue-agent.sh) auto-installs and launches it within seconds of the phone reconnecting, via launchd IOKit matching on Apple USB attach, WatchPaths on the queue, and a periodic network backstop, then sends a cmux notification. Every phone build hard-requires the same-tag Mac dev build: ios/scripts/reload.sh builds the Mac tag first when missing and refuses phone-only otherwise. scripts/ios-sim-install.sh installs cloud-built simulator apps into the isolated simulator for the reload-cloud-ios path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Probe device reachability through the queue script in ios/scripts/reload.sh One probe implementation (iphone-install-queue.sh probe) now decides "unreachable" for both the local and cloud reload paths, including the CMUX_IPHONE_QUEUE_FORCE_UNREACHABLE test hook; select_device still owns name/ambiguity resolution for reachable devices and its failure is treated as unreachable as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address review findings: name-target queueing, enqueue race, fail-closed sim install A --device-name target no longer probes or queues against the DEFAULT device id (queueing for a different phone than the one named would install on the wrong device); name targets error with a hint to use --device-id when unreachable. drain_entry now re-reads enqueued_at before every terminal action so a re-enqueue during an in-flight drain leaves the newer build queued instead of silently deleting or failing it. ios-sim-install.sh fails closed on an unreadable CFBundleIdentifier. Also: quote $tab expansions (SC2295), correct help sed ranges, document the one-time LaunchAgent install in CLAUDE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Nudge PR sync Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
Delete 53 CI-dead v1 socket e2e tests superseded by tests_v2 (#7681) These tests/ files import the v1 socket client and are not referenced by any workflow, script, or test. CI runs only three v1 socket e2e files (test_cli_socket_autodiscovery, test_multi_workspace_focus, test_workspace_churn_up_arrow_lag), which are kept. 28 of the deleted files have an identically-named successor in tests_v2/; the rest were runnable only via the manual cmux-vm run-tests-v1.sh glob. Also updates CONTRIBUTING.md's VM test command to the tests_v2 paths (it referenced tests/test_update_timing.py, which no longer existed) and drops a deleted-file mention from docs/agent-browser-port-spec.md. | 2 个月前 | |
docs(browser): bound commercial license scope | 1 个月前 | |
Reversion all 1.x.x to 0.x.x for pre-launch versioning | 6 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
docs(browser): bound commercial license scope | 1 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
Restore README star history chart (#7767) * Restore README star history chart * Update localized star history charts | 2 个月前 | |
merge: sync markdown viewer with main | 3 个月前 | |
Reapply "Merge pull request #239 from manaflow-ai/issue-151-ssh-remote-port-proxying" This reverts commit f7cbbad4342fb1cafb520aedb079cdf4a5730225. | 5 个月前 | |
Scope Biome checks to maintained JS sources (#6008) * fix: scope biome checks * chore: remove redundant biome ignore | 2 个月前 | |
Scope Biome checks to maintained JS sources (#6008) * fix: scope biome checks * chore: remove redundant biome ignore | 2 个月前 | |
Bound app termination with a force-exit watchdog (#6758) (#6837) * test: red regression for termination watchdog (#6758) cmux can hang the main thread for ~30s on Cmd+Q when a clipboard-history manager (Paste, Raycast, Maccy, …) is mid-read of cmux's promised pasteboard data: AppKit's will-terminate gauntlet runs CFPasteboardResolveAllPromisedData, which blocks on a stuck mach round-trip to the pasteboard server until the OS force-kills the app. This is the third "an observer blocks the main thread during quit" report (cf. #6415 PostHog flush, #6381 ghostty lock); the structural gap is that quit has no global "return within N seconds no matter what" guard. Add TerminationWatchdog plus its tests, with the watchdog deliberately inert (it never starts the firing thread) so the tests go red. The end-to- end pasteboard deadlock is not unit-testable — reproducing it requires the real pasteboard server and would wedge the test process — so the tests cover the watchdog mechanism that bounds it. The fix commit starts the thread and arms the watchdog from the terminate path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Bound app termination with a force-exit watchdog (#6758) Implement TerminationWatchdog.arm and arm it from the terminate path so a committed quit always returns within a bounded time, even when AppKit's will-terminate gauntlet wedges on an Apple-owned observer we don't control (CFPasteboardResolveAllPromisedData blocking on a stuck pasteboard-server round-trip while a clipboard-history manager reads cmux's promised data). The watchdog runs on a dedicated background thread with no run-loop, GCD, or main-actor dependency, so it fires even while the main thread is parked in mach_msg. It is armed in prepareForConfirmedAppTermination() — after the critical session/state save and before AppKit posts will-terminate — and, as a backstop, at the start of applicationWillTerminate(). Arming is idempotent, so the two sites and repeated quit attempts never stack threads. If the process has not exited within the deadline it force-exits cleanly, turning a ~30s hang into a bounded quit. This closes the structural gap shared with #6415 and #6381: quit now has a global "return within N seconds no matter what" guard. Fixes #6758 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address review: lock-free watchdog exit, drop singleton (#6758) - Codex/autoreview P1 (correctness): the watchdog's onFire logged a StartupBreadcrumbLog entry (flock + Foundation/file I/O) before _exit. If that logging stalled or contended during an already-wedged termination, the watchdog thread could block before reaching _exit and the quit hang would stay unbounded — defeating the guarantee. Drop the breadcrumb: the firing path is now an unconditional, lock-free _exit (the default onFire), which does zero Foundation/filesystem work before exiting. - Greptile P1 (no-ambient-global-state): replace the TerminationWatchdog.shared singleton with an AppDelegate-owned instance, next to the existing terminate-control state (terminateKillWatchdogTask). The type was already injectable, so this is a small wiring change. - Greptile P2: document why the deadline uses a raw Thread + Thread.sleep rather than a GCD timer (the wedged termination can sit on GCD/run-loop infrastructure, so the firing path must not depend on it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Document lock-not-actor choice in TerminationWatchdog (#6758) cmux-policy (Aziz concurrency) prefers actor isolation over locks for new runtime state. Rejected here with rationale recorded in-code: an actor would force `arm()` async, but it is called synchronously from the terminate delegate methods and the deadline fires on a raw Thread — and the watchdog must not depend on the Swift concurrency runtime, which may itself be wedged during the termination it guards against. This is the same sanctioned NSLock + nonisolated(unsafe) shape TerminalPasteboardService uses for synchronous- callback state. Comment-only change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Determinize TerminationWatchdog test via injected scheduler (#6758) CI's test-determinism gate (scripts/check-test-determinism.py --strict) flagged the prior tests for real sleeps / wall-clock timeouts (sleep-then-assert and assert-on-duration). Invert the time dependency per the gate's contract instead of allowlisting: extract the deadline scheduler as an injectable `DeadlineScheduler`. Production keeps the raw background Thread (`TerminationWatchdog.threadScheduler`); the tests inject a synchronous capturing scheduler and advance the deadline by hand. The tests now assert idempotency (three arms schedule the deadline exactly once) and exactly-once firing with zero real sleeps, timeouts, or wall-clock reads, so they are deterministic by construction. arm() is now a thin idempotent latch over scheduleDeadline(deadline, onFire); behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Use atomic termination watchdog latch * Use non-deprecated termination watchdog latch * Use C11 atomic termination watchdog latch * Save termination state before watchdog fallback * Avoid growing AppDelegate termination path --------- Co-authored-by: cmux <cmux@cmuxs-Mac-mini.local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 2 个月前 | |
Inside-out signing with per-binary entitlements to restore passkeys (#2905) The AuthenticationServices WebAuthn / passkey path on Developer ID Mac apps requires com.apple.application-identifier in the codesigned entitlements blob of the calling process. AS does not fall back to the embedded provisioning profile and does not derive the app id from team-identifier + bundle-id; without that key the ceremony fails with AuthorizationError 1004. PR #2727's nightly build signed every binary inside the bundle (main app plus Contents/Resources/bin/cmux and Contents/Resources/bin/ghostty) with the same cmux.entitlements. Injecting application-identifier into that shared file and re-applying it with --deep pushed the main app's app id onto the CLI helpers, whose code identifiers are 'cmux' and 'ghostty'. That mismatch is what amfi on notarized macOS 26 Tahoe rejects with Launchd job spawn failed / errno 163. Local un-notarized Developer-ID builds survived because amfi is less strict on those. Split into two entitlements files and sign inside-out: - cmux-helper.entitlements: minimal hardened-runtime only (disable-library-validation, allow-jit, allow-unsigned-executable-memory). Used for CLI helpers, which never need app-id, WebAuthn, camera, mic, or AppleEvents. - cmux.entitlements: unchanged in the tree (shared base, no app-id). At sign time the workflow copies it and injects application-identifier and team-identifier for the bundle being built (com.cmuxterm.app for release, com.cmuxterm.app.nightly for nightly). Signing order in both workflows: 1. CLI helpers (bin/cmux, bin/ghostty) signed with cmux-helper.entitlements. 2. Main app bundle signed last with the full injected entitlements, WITHOUT --deep. --deep would overwrite the helper signatures and re-propagate the mismatch. release.yml also gains the same embedded provisioning profile step the nightly gained in #2727, so the shipped com.cmuxterm.app build is authorized to use the WebAuthn browser entitlement too. Verified locally on macOS 26.3.1: re-signed the previous broken nightly bundle using this scheme, it launches, passkey ceremony reaches ASAuthorizationController.performRequests without the 1004 error. Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> | 4 个月前 | |
Fix browser mTLS client certificate challenges (#7040) * Add failing browser client certificate auth tests * Handle browser client certificate challenges * Log browser client certificate keychain failures * Require confirmation before sending client certificates * Disambiguate client certificate picker labels * Harden browser client certificate prompts * Localize client certificate picker strings * Reduce browser auth delegate growth * Skip keychain auth UI during cert lookup * Fail noninteractive keychain cert lookup * Address mTLS certificate review edge cases * Address client certificate review feedback * Own browser auth prompt text formatting * Fix client certificate warning budget * Move browser client certificate logic into CmuxBrowser * Require consent before using client certificates * Cancel client certificate lookups with prompts * Bridge client certificate lookup cancellation * Clean client certificate auth policy issues * Restore bundle-specific debug keychain group | 2 个月前 | |
Fix browser mTLS client certificate challenges (#7040) * Add failing browser client certificate auth tests * Handle browser client certificate challenges * Log browser client certificate keychain failures * Require confirmation before sending client certificates * Disambiguate client certificate picker labels * Harden browser client certificate prompts * Localize client certificate picker strings * Reduce browser auth delegate growth * Skip keychain auth UI during cert lookup * Fail noninteractive keychain cert lookup * Address mTLS certificate review edge cases * Address client certificate review feedback * Own browser auth prompt text formatting * Fix client certificate warning budget * Move browser client certificate logic into CmuxBrowser * Require consent before using client certificates * Cancel client certificate lookups with prompts * Bridge client certificate lookup cancellation * Clean client certificate auth policy issues * Restore bundle-specific debug keychain group | 2 个月前 | |
Fix browser mTLS client certificate challenges (#7040) * Add failing browser client certificate auth tests * Handle browser client certificate challenges * Log browser client certificate keychain failures * Require confirmation before sending client certificates * Disambiguate client certificate picker labels * Harden browser client certificate prompts * Localize client certificate picker strings * Reduce browser auth delegate growth * Skip keychain auth UI during cert lookup * Fail noninteractive keychain cert lookup * Address mTLS certificate review edge cases * Address client certificate review feedback * Own browser auth prompt text formatting * Fix client certificate warning budget * Move browser client certificate logic into CmuxBrowser * Require consent before using client certificates * Cancel client certificate lookups with prompts * Bridge client certificate lookup cancellation * Clean client certificate auth policy issues * Restore bundle-specific debug keychain group | 2 个月前 | |
Fix ghostty bridge header drift | 5 个月前 | |
Fix Iroh relay policy Vercel deployment (#8118) * Add regression coverage for Vercel build inputs * Fix Iroh relay policy Vercel deployment * Update relay catalog workflow paths * Make relay retry test deterministic * Fix Iroh authorization test module import * Account for Iroh test import --------- Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> | 1 个月前 | |
Add global font magnification (#6554) * [cmux] add global font magnification across terminals and chrome * [cmux] address PR review for global font magnification * [cmux] cap global font magnification at 200% to fit chrome budgets * [cmux] keep About title bold, label magnification stepper, robust percent parse * [cmux] scale magnification control text and unclip settings subtitles * fix: make font magnification cover AppKit chrome * fix: address magnification review findings * fix: apply magnification to terminal runtime config * fix: keep sidebar font size unscaled in config * fix: make browser import font actions void * fix: make font magnification helper instantiable * fix: update updater UI package lockfile * fix: scale magnified chrome row metrics * fix: address magnification review feedback * fix: close magnification review gates * fix: close app-host magnification gates * fix: localize magnification setting catalog * fix: hoist font magnification environment * fix: close magnification review follow-ups * fix: restore file explorer keyboard activation * fix: keep inherited terminal font sizes unscaled * fix: restore close and search regressions * fix: keep file explorer header height in sync * fix: keep right sidebar keyboard tests compiling * fix: scale fallback ghostty config font size * fix: keep ghostty config parsing deterministic * fix: address magnification review regressions * fix: satisfy magnification policy gates * fix: satisfy font helper package policy * fix: update sidebar drop metrics fixture * fix: satisfy cmux font policy check --------- Co-authored-by: Gigi Sayfan <the.gigi@gmail.com> | 2 个月前 | |
Revert "docs: add cmux automate skill (#4301)" (#4305) This reverts commit a6ba12b585d667168a6f9c97fb42e562745c4aae. Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> | 3 个月前 |
此翻译由 Claude 生成。如有改进建议,欢迎提交 PR。
cmux
基于 Ghostty 的 macOS 终端,带有垂直标签页和为 AI 编程代理设计的通知系统
English | 日本語 | Tiếng Việt | 简体中文 | 繁體中文 | 한국어 | Deutsch | Español | Français | Italiano | Dansk | Polski | Русский | Bosanski | العربية | Norsk | Português (Brasil) | ไทย | Türkçe | ភាសាខ្មែរ | Українська
功能特性
通知提示环当编程代理需要您注意时,窗格会显示蓝色光环,标签页会高亮 |
|
通知面板在一处查看所有待处理通知,快速跳转到最新未读通知 |
|
内置浏览器在终端旁边分割出浏览器窗格,提供从 agent-browser 移植的可脚本化 API |
|
垂直 + 水平标签页侧边栏显示 git 分支、关联 PR 状态/编号、工作目录、监听端口和最新通知文本。支持水平和垂直分割。 |
|
SSHcmux ssh user@remote 为远程机器创建工作区。浏览器窗格通过远程网络路由,因此 localhost 直接可用。将图片拖入远程会话即可通过 scp 上传。
|
|
Claude Code Teamscmux claude-teams 一条命令运行 Claude Code 的队友模式。队友以原生分割的形式生成,侧边栏显示元数据和通知。无需 tmux。
|
|
- 浏览器导入 — 从 Chrome、Firefox、Arc 及 20 多种浏览器导入 Cookie、历史记录和会话,让浏览器窗格启动即已登录
- 自定义命令 — 在
cmux.json中定义项目专属操作,通过命令面板启动 - 可脚本化 — 通过 CLI 和 socket API 创建工作区、分割窗格、发送按键和自动化浏览器操作
- 原生 macOS 应用 — 使用 Swift 和 AppKit 构建,非 Electron。启动快速,内存占用低。
- 兼容 Ghostty — 读取您现有的
~/.config/ghostty/config配置文件中的主题、字体和颜色设置 - GPU 加速 — 由 libghostty 驱动,渲染流畅
- 键盘快捷键 — 为工作区、分割、浏览器等提供丰富的快捷键
- 开源 — 免费且采用 GPL 许可
安装
DMG(推荐)
打开 .dmg 文件并将 cmux 拖动到"应用程序"文件夹。cmux 通过 Sparkle 自动更新,您只需下载一次。
Homebrew
brew tap manaflow-ai/cmux
brew install --cask cmux
稍后更新:
brew upgrade --cask cmux
首次启动时,macOS 可能会要求您确认打开来自已验证开发者的应用。点击打开即可继续。
为什么做 cmux?
我同时运行大量 Claude Code 和 Codex 会话。之前我用 Ghostty 开了一堆分割窗格,依靠 macOS 原生通知来了解代理何时需要我。但 Claude Code 的通知内容总是千篇一律的"Claude is waiting for your input",没有任何上下文信息,而且标签页一多,连标题都看不清了。
我试过几个编程协调工具,但大多数都是 Electron/Tauri 应用,性能让我不满意。我也更喜欢终端,因为 GUI 协调工具会把你锁定在它们的工作流里。所以我用 Swift/AppKit 构建了 cmux,作为一个原生 macOS 应用。它使用 libghostty 进行终端渲染,并读取您现有的 Ghostty 配置中的主题、字体和颜色设置。
主要新增的是侧边栏和通知系统。侧边栏有垂直标签页,显示每个工作区的 git 分支、关联 PR 状态/编号、工作目录、监听端口和最新通知文本。通知系统能捕获终端序列(OSC 9/99/777),并提供 CLI(cmux notify),您可以将其接入 Claude Code、OpenCode 等代理的钩子。当代理等待时,其窗格会显示蓝色光环,标签页会在侧边栏高亮,这样我就能在多个分割窗格和标签页之间一眼看出哪个需要我。Cmd+Shift+U 可以跳转到最新的未读通知。
内置浏览器拥有从 agent-browser 移植的可脚本化 API。代理可以抓取无障碍树快照、获取元素引用、执行点击、填写表单和执行 JS。您可以在终端旁边分割出浏览器窗格,让 Claude Code 直接与您的开发服务器交互。
所有操作都可以通过 CLI 和 socket API 进行脚本化 — 创建工作区/标签页、分割窗格、发送按键、在浏览器中打开 URL。
The Zen of cmux
cmux 不规定开发者应该如何使用工具。它是一个带有 CLI 的终端和浏览器,其余的由你决定。
cmux 是原语,而非解决方案。它提供终端、浏览器、通知、工作区、分割、标签页,以及控制这一切的 CLI。cmux 不强迫你以特定方式使用编程代理。你用这些原语构建什么,完全取决于你自己。
最优秀的开发者一直在构建自己的工具。还没有人找到与代理协作的最佳方式,那些构建封闭产品的团队也没有找到。最接近自己代码库的开发者会最先找到答案。
给一百万个开发者可组合的原语,他们会比任何自上而下设计的产品团队更快地找到最高效的工作流。
文档
有关 cmux 配置的更多信息,请查看我们的文档。
键盘快捷键
工作区
| 快捷键 | 操作 |
|---|---|
| ⌘ N | 新建工作区 |
| ⌘ 1–8 | 跳转到工作区 1–8 |
| ⌘ 9 | 跳转到最后一个工作区 |
| ⌃ ⌘ ] | 下一个工作区 |
| ⌃ ⌘ [ | 上一个工作区 |
| ⌘ ⇧ W | 关闭工作区 |
| ⌘ ⇧ R | 重命名工作区 |
| ⌥ ⌘ E | 编辑工作区描述 |
| ⌘ B | 切换侧边栏 |
| ⌥ ⌘ B | 切换右侧边栏 |
| ⌘ ⇧ E | 切换右侧边栏焦点 |
界面
| 快捷键 | 操作 |
|---|---|
| ⌘ T | 新建界面 |
| ⌘ ⇧ ] | 下一个界面 |
| ⌘ ⇧ [ | 上一个界面 |
| ⌃ Tab | 下一个界面 |
| ⌃ ⇧ Tab | 上一个界面 |
| ⌃ 1–8 | 跳转到界面 1–8 |
| ⌃ 9 | 跳转到最后一个界面 |
| ⌘ W | 关闭界面 |
分割窗格
| 快捷键 | 操作 |
|---|---|
| ⌘ D | 向右分割 |
| ⌘ ⇧ D | 向下分割 |
| ⌥ ⌘ ← → ↑ ↓ | 按方向切换焦点窗格 |
| ⌘ ⇧ H | 闪烁聚焦面板 |
浏览器
浏览器开发者工具快捷键遵循 Safari 默认设置,可在 设置 → 键盘快捷键 中自定义。
命令面板导航快捷键(包括 ⌃ P)同样可自定义,并可清除以便按键传递到活动终端。
| 快捷键 | 操作 |
|---|---|
| ⌘ ⇧ L | 在分割中打开浏览器 |
| ⌘ L | 聚焦地址栏 |
| ⌘ [ | 后退 |
| ⌘ ] | 前进 |
| ⌘ R | 刷新页面 |
| ⌥ ⌘ I | 切换开发者工具(Safari 默认) |
| ⌥ ⌘ C | 显示 JavaScript 控制台(Safari 默认) |
通知
| 快捷键 | 操作 |
|---|---|
| ⌘ I | 显示通知面板 |
| ⌘ ⇧ U | 跳转到最新未读 |
| ⌥ ⌘ U | 切换当前项的未读状态 |
| ⌃ ⌘ U | 将当前项标记为最早未读并跳转到下一个最新未读 |
查找
| 快捷键 | 操作 |
|---|---|
| ⌘ F | 查找 |
| ⌘ ⇧ F | 在目录中查找 |
| ⌘ G / ⌥ ⌘ G | 查找下一个 / 上一个 |
| ⌥ ⌘ ⇧ F | 隐藏查找栏 |
| ⌘ E | 使用选中内容进行查找 |
终端
| 快捷键 | 操作 |
|---|---|
| ⌘ K | 清除回滚缓冲区 |
| ⌘ C | 复制(有选中内容时) |
| ⌘ V | 粘贴 |
| ⌘ + / ⌘ - | 增大 / 减小字体 |
| ⌘ 0 | 重置字体大小 |
窗口
| 快捷键 | 操作 |
|---|---|
| ⌘ ⇧ N | 新建窗口 |
| ⌘ ⇧ O | 重新打开上一个会话 |
| ⌘ , | 设置 |
| ⌘ ⇧ , | 重新加载配置 |
| ⌘ Q | 退出 |
每夜构建
cmux NIGHTLY 是一个拥有独立 Bundle ID 的单独应用,因此可以与稳定版并行运行。它从最新的 main 提交自动构建,并通过独立的 Sparkle 更新源自动更新。
在 GitHub Issues 或 Discord 的 #nightly-bugs 频道 上报告每夜构建的 bug。
会话恢复
退出 cmux 会保存当前会话。重新启动时,cmux 会恢复应用管理的状态:
- 窗口/工作区/窗格布局
- 工作目录
- 终端回滚缓冲区(尽力恢复)
- 浏览器 URL 和导航历史
cmux 不会为任意实时进程状态做检查点。tmux、vim、shell 和不支持的终端应用会作为普通终端重新打开。
当 hooks 保存了原生会话 ID 时,受支持的 agent 会话可以恢复。请在安装 agent CLI 之后再安装 hooks,以确保其二进制文件位于 PATH 上:
cmux hooks setup
cmux hooks setup codex
cmux hooks setup --agent opencode
cmux hooks setup 会安装它能找到的受支持 agent,并为跳过的 agent 打印摘要。受支持的恢复集成包括 Claude Code、Codex、Grok、OpenCode、Pi、Amp、Cursor CLI、Gemini、Rovo Dev、Copilot、CodeBuddy、Factory 和 Qoder。当在设置中启用了 Claude 集成时,Claude Code 由 cmux Claude wrapper 处理。
高级用户和集成可以把自定义恢复命令绑定到当前终端 surface。这适用于 tmux 会话或自定义 agent CLI 等拥有持久状态的工具:
cmux surface resume set --kind tmux --checkpoint work --shell "tmux attach -t work"
cmux surface resume show --json
cmux surface resume clear --checkpoint work
这个绑定会继续关联到 cmux surface。通过公开 CLI 或 socket 创建的绑定会保存用于检查和手动恢复,除非您为某个签名命令前缀批准了自动恢复。已批准的前缀还会绑定到工作目录和确切的环境变量值(如果存在)。可在 设置 > 终端 > 恢复命令 中查看或编辑批准项。cmux 只会自动运行它标记为可信的恢复绑定,例如从运行中进程检测到的 tmux 绑定或用户批准的前缀。令牌、密码、密钥和 API key 等敏感环境变量键会在保存恢复绑定前被丢弃。
如需让恢复的 agent 终端保持空闲,而不是自动运行其恢复命令,请关闭 设置 > 终端 > 重新打开时恢复 Agent 会话,或在 ~/.config/cmux/cmux.json 中设置:
{
"terminal": {
"autoResumeAgentSessions": false
}
}
这只会禁用自动的 agent 恢复命令。cmux 仍会恢复保存的布局、工作目录、回滚缓冲区和浏览器历史。
如果您需要手动重新应用上次保存的快照,请使用:
文件 > 重新打开上一个会话⌘ ⇧ Ocmux restore-session
在底层,cmux 会在 ~/Library/Application Support/cmux/ 下写入带版本的快照,agent hooks 会在 ~/.cmuxterm/ 下写入会话映射。恢复时,cmux 会先重建布局,然后在启用了自动 agent 恢复时运行受支持 agent 的原生恢复命令。
完整指南请见 https://cmux.com/docs/session-restore。
FAQ
cmux 与 Ghostty 是什么关系?
cmux 不是 Ghostty 的分支。它把 libghostty 作为库用于终端渲染,就像应用使用 WebKit 来呈现网页视图一样。Ghostty 是一个独立的终端;cmux 是构建在其渲染引擎之上的另一款应用。
它支持哪些平台?
目前仅支持 macOS。cmux 是一个原生的 Swift + AppKit 应用。
有 iOS 应用吗?
有,目前处于测试阶段。在 Mobile Connect 窗口中将您的 iPhone 与 Mac 配对,即可从手机连接到您的终端,并可选择转发终端通知。它以 cmux BETA 的形式通过 TestFlight 发布。请参阅 iOS 文档。
cmux 支持哪些编程代理?
全部支持。cmux 是一个终端,因此任何能在终端中运行的代理都开箱即用:Claude Code、Codex、OpenCode、Gemini CLI、Kiro、Aider、Goose、Amp、Cline、Cursor Agent,以及任何其他可从命令行启动的工具。
cmux 能编排多个代理和子代理吗?
可以。当某个代理生成子代理或队友时,cmux 会把它们变成原生窗格和分割,而不是隐藏的后台进程。它支持 Claude Code teams 和 oh-my-opencode 的多模型编排,因此一次运行中的每个代理都可见、可控。
我可以用 cmux 操作远程机器吗?
可以。通过 SSH 打开工作区并连接到远程 tmux 会话,这样代理可以在远程主机上运行,而您从 cmux 来驱动它们。请参阅 SSH 和远程。
通知是如何工作的?
当某个进程需要关注时,cmux 会在窗格周围显示通知提示环、在侧边栏显示未读徽章、弹出通知气泡,以及 macOS 桌面通知。这些会通过标准终端转义序列(OSC 9/99/777)自动触发,您也可以用 cmux CLI 和 agent hooks 来触发它们。任何支持 hooks 或 OSC 的代理都可使用,包括 Claude Code、Codex、OpenCode 和 pi。
cmux 可编程吗?
可以。每个操作都可通过 cmux CLI 和 Unix socket 使用:创建工作区、打开分割窗格、发送输入、读取屏幕内容、截图,以及驱动内置浏览器。请参阅 CLI 参考 和 浏览器自动化 文档。
内置浏览器能做什么?
cmux 可以在终端旁边分割出一个真正的浏览器窗格,并且它完全可编程:导航、抓取 DOM 快照、点击、输入、执行 JavaScript,以及通过同一套 socket API 读取控制台和网络活动。代理用它来验证自己做的网页改动,而无需离开 cmux。请参阅 浏览器自动化。
cmux 有 skills 吗?
有。Skills 是可复用的工作流,您可以将其交给任何运行在 cmux 中的代理,用于诸如 CLI 控制、工作区自动化、设置和浏览器界面等任务。可在 cmux-skills 浏览开放的合集,或阅读 skills 文档。
我可以自定义键盘快捷键吗?
终端键绑定从您的 Ghostty 配置文件(~/.config/ghostty/config)中读取。cmux 专属的快捷键(工作区、分割、浏览器、通知)可在设置中自定义。完整列表请参阅 默认快捷键。
我可以自定义 cmux 吗?
可以。终端渲染使用您的 Ghostty 配置,因此主题、字体、颜色和光标会直接沿用。cmux 自己在 ~/.config/cmux/cmux.json 中的设置控制侧边栏、标签栏、分割窗格和行为,并且每个键盘快捷键都可编辑。请参阅 配置。
我的会话会被保存吗?
会。cmux 在重新启动时会恢复您的窗口、工作区、窗格、工作目录和回滚缓冲区,并且这些状态能在整机重启后保留,而不仅仅是退出应用。像 Claude Code、Codex 和 OpenCode 这样的 agent 会话也会回来。请参阅 会话恢复。
它与 tmux 相比如何?
tmux 是一个在任意终端内运行的终端复用器。cmux 是一个带 GUI 的原生 macOS 应用:垂直标签页、分割窗格、内嵌浏览器和 socket API,全部内置,无需配置文件或前缀键。也就是说,很多人乐于把 cmux 与 SSH 和 tmux 一起使用,而 cmux 可以原生连接到您的远程 tmux 会话(测试版)。
cmux 免费吗?
是的,cmux 免费使用。源代码可在 GitHub 上获取。
我如何支持 cmux?
cmux 免费且开源,并将一直如此。如果您想支持开发并提前体验接下来的功能,包括 cmux AI、iOS 应用和 Cloud VMs,请查看 cmux Founders Edition。
我有功能请求或发现了 bug?
我们很想听到。请在 GitHub 上提交 issue 或 pull request,或者 给我们发邮件。
Star History
参与贡献
参与方式:
- 在 X 上关注我们:@manaflowai、@lawrencecchen、@austinywang
- 加入 Discord 讨论
- 创建和参与 GitHub Issues 和讨论
- 告诉我们您在用 cmux 构建什么
社区
WeChat:扫描二维码加入社区。
Founder's Edition
cmux 免费、开源,并将一直如此。如果您想支持开发并提前体验即将推出的功能:
- 功能请求/Bug 修复优先处理
- 抢先体验:为每个工作区、标签页和面板提供上下文的 cmux AI
- 抢先体验:桌面与手机间终端同步的 iOS 应用
- 抢先体验:云端虚拟机
- 抢先体验:语音模式
- 我的个人 iMessage/WhatsApp
许可证
cmux 以 GPL-3.0-or-later 开源。
如果您的组织无法遵守 GPL,可提供商业许可证。详情请联系 founders@manaflow.com。