Repository navigation
Conversation
Globe+Arrow keys produce Home/End/PageUp/PageDown at the hardware level on macOS (keyCodes 115/119/116/121). These couldn't be used as keyboard shortcuts because: 1. storedKey(from:) didn't map their keyCodes to glyph symbols 2. keyEquivalent/menuItemKeyEquivalent had no SwiftUI/AppKit mappings 3. matchShortcut character matching fails for function-key Unicode chars 4. The "require at least one modifier" guard rejected bare Globe+Arrow (after stripping .function, zero modifiers remain) 5. The normalizedFlags.isEmpty fast path in handleCustomShortcut returned early, skipping all shortcut matching for unmodified navigation keys Mirrors the existing arrow key (←→↑↓) pattern. Adds glyph symbols ↖↘⇞⇟ across all five shortcut system touch points, exempts navigation keys from the modifier requirement, and lets their keyCodes through the fast path to reach shortcut matching.
pandec
force-pushed
the
feat/home-end-pageup-pagedown-shortcut-keys-clean
branch
from
March 29, 2026 20:10
6095e00 to
f7866dc
Compare
pandec
pushed a commit
that referenced
this pull request
Jul 30, 2026
…w-ai#9071) * test(iroh): expose reconnect outage gaps * fix(iroh): keep reconnects alive through outages * fix(ios): signal reconnect deadlines without sleeping * test(iroh): cover close attribution diagnostics * feat(iroh): attribute connection closes and path events * iroh: re-key binding slot to (user, device, tag), newest-auth-wins The active-binding slot was keyed on app_instance_id with a unique index, so a reinstall, sign-out/in, or key rotation produced a fresh app instance that collided with its own past self and got a 409 binding_replacement_requires_revocation. That stranded the App Store review Mac behind a stale non-revoked binding for 17h with no client-side recovery. Re-key the slot to (user_id, device_uuid, tag), partial-unique where revoked_at is null. A registration for an existing slot now overwrites it in place (newest authenticated registration wins) and preserves the binding row id so existing pair grants keep resolving. No generation gate: a reinstall resets identity_generation to 1, and gating on it would reintroduce the wedge. The endpoint id stays globally unique, re-checked excluding self so a slot can rotate its own key. Drop the per-device (8) and per-account (32) binding caps, the stale-binding recycler, and the bindingQuota plumbing; the challenge-issuance quota is kept. Advisory locks move from iroh:app:<appInstance> to iroh:slot:<user>:<device>:<tag> so same-slot registrations serialize. Migration collapses any duplicate active (user, device, tag) rows (keep most recently seen, soft-revoke the rest, revoke their pair grants, bump LAN discovery generation), drops active_app_instance_unique, and adds active_slot_unique. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: stable Keychain device id + Forget computer (iroh re-key client) Client complement to the broker binding re-key (manaflow-ai#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. * iroh: mint new binding id on endpoint rotation, add active-binding sanity cap Address the two P1 review findings on the re-key branch. Finding 1 (ABA wedge): register reused the same binding id when an existing slot re-registered with a rotated endpoint key. A peer host that had denied the OLD endpoint tuple keeps the denial keyed on binding id, so the rotated device was permanently denied behind its own past self. Now a same-endpoint registration is treated as a heartbeat and updates in place (stable id, no ABA), while a rotated endpoint on an existing slot soft-revokes the old row (revokedReason "slot_reincarnated", cleared ports/path hints) and inserts a NEW binding id, carrying live pair grants (initiator + acceptor) onto the new id so pairings follow the device without a re-pair. No lanDiscoveryGeneration bump: a device rotating its own key is not an account-wide trust revocation. Finding 2 (unbounded growth): under unique(user, device, tag) a stuck client spamming fresh tuples could grow the active row set without bound. Add IROH_ACTIVE_BINDING_SANITY_CAP (512) enforced only on the genuinely-new-slot path, evicting the oldest-seen bindings (LRU by lastSeenAt) with reason "active_binding_cap_evicted". No-op for every normal account (a handful of bindings; heavy multi-tag dev at most low hundreds). Tests: reinstall now asserts new-id semantics + retired-row reason; added grant-carry and cap-eviction coverage. 33 DB-behavior tests and 26 route-layer tests pass against isolated Postgres; typecheck clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 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> * Recover the Mac iroh host runtime from terminal failure without relaunch A non-transient broker rejection (401/403/404/409, invalid response) tears CmxIrohHostRuntime down into a terminal .failed phase. That fail-closed teardown is deliberate, but nothing ever rebuilt the runtime: MobileHostIrohRuntime.retryIfNeeded() only re-synced LAN publication while it held a runtime reference, and no timer retried a failed activation. A Mac whose registration was rejected once stayed unregistered until sign-out/sign-in, a Settings-triggered restart, or an app relaunch (the 17-hour App Store review 409 wedge). Recovery is now owned by the macOS composition root, level-triggered through the existing reconcile path: - Every failed activation and every runtime self-teardown into .failed (reported through the existing handleDeactivation callback, filtered by lifecycle revision so deliberate stops are ignored) arms one pending rebuild with bounded exponential backoff (30s doubling to a 1h cap, jittered, via CmxIrohRetrySchedule and an injected clock). - retryIfNeeded() now rebuilds a .failed runtime immediately on any external wake signal (network path change, app-level retry) and resets the backoff ladder, instead of only re-syncing LAN state. - Each reconcile cancels the pending attempt and re-derives recovery from its own outcome: success resets the ladder, failure re-arms it, sign-out/deactivation ends it. The new package test pins the contract this depends on: a rejected registration refresh fails closed (endpoint torn down, deactivation notified) and the same runtime accepts start() again once the broker allows registration. The two-commit red/green structure does not apply because the wedge lives in app-target singleton wiring that has no practical automated harness; the package test guards the enabling semantics instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iroh: harden binding re-key against ABA wedge, LAN staleness, and cap churn Address review findings on the slot re-key path: - Heartbeat-in-place now requires every signed grant-identity field (endpoint id, platform, identity generation) to be unchanged, not just the endpoint id. Overwriting platform/generation on a live binding id would let a still-valid grant signed against the old value mismatch the current binding, so a host records this id in its permanent denial set — the exact ABA wedge the fresh-id path exists to prevent. Any divergence now falls through to reincarnation and mints a fresh id. - Reincarnation retires the old slot through revokeActiveBindings instead of a bespoke soft-revoke. That rotates lanDiscoveryGeneration (so a displaced install can no longer derive future LAN rendezvous aliases) and marks the retired binding's pair grants revoked. - Drop the pair-grant foreign-key carry-over. iroh_pair_grant_issuances is an audit-only ledger of compact JWS tokens already returned to clients; reassigning the FK cannot rewrite a held token, and re-keying forces a re-pair anyway because the token names the dead endpoint. Carrying the FK only made the JTI audit point at a binding it was never signed for. - Sanity cap now rejects a genuinely-new slot at the cap (IrohQuotaExceededError code active_binding_limit) instead of evicting the oldest-seen binding, so a stuck client spamming fresh device/tag tuples can no longer shed the account's real, older hosts and phones. Update iroh-db-behavior and iroh-trust-broker tests to the corrected contract. 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> * iroh: reject over-cap registrations, gate stale challenges, document deviceUuid contract Review round 2 for the binding re-key. - Sanity cap: keep the reject-not-evict semantics (over-cap registrations throw IrohQuotaExceededError so a churning client can never shed real hosts) and hold the value at 512, well above any legitimate multi-tag developer's low-hundreds active-slot count. (An earlier draft lowered it to 256 citing an iOS 'maximumBindingCount' wire limit; no such constant exists — the only 256 in the client is MobileSyncFrameCodec's per-read frame cap on the terminal RPC transport, unrelated to iroh discovery responses. Dropped that false rationale.) - Challenge-freshness gate: reject a registration whose challenge was minted before the slot's current registeredAt. Registrations for one slot serialize under the slot advisory lock; without this, a delayed/replayed older challenge could land second and overwrite or reincarnate away the newer incarnation, an out-of-order wedge. A live heartbeat's own challenge is always newer, so it passes; registeredAt only advances on insert/reincarnation, so it is the right high-water mark. - schema: document that deviceUuid MUST be stable across reinstalls or a reinstall orphans the old active slot; the client owns this (iOS now derives it from a Keychain identity that survives reinstall), the DB cannot enforce it. - test: the mac->ios platform change on one slot reincarnates (revoke old id + mint new) instead of overwriting in place, so a still-valid grant signed against the old platform can't ABA-wedge into the host's permanent denial set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iroh: map active-slot unique violation to typed 409 databaseConflict only mapped the endpoint-unique index (23505 -> endpoint_already_bound); a violation on the new (user, device, tag) active-slot partial unique index fell through to a raw IrohDatabaseError (HTTP 500). The slot advisory lock serializes same-slot registrations so this is unreachable in practice, but map it defensively to a typed 409 (slot_registration_superseded) so a concurrent newest-wins race surfaces as a retryable conflict instead of a 500. 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> * iroh: add failing test for reversed heartbeat completion Two heartbeats for one live slot, minted older-then-newer, completing in reverse: the newer lands first and takes the slot, then the delayed older challenge lands second. Without a registration high-water mark that advances on the in-place heartbeat update, the older challenge passes the staleness gate and clobbers the newer incarnation's mutable fields (appInstanceId here) back to a stale value until the next heartbeat self-heals. This commit adds only the failing test; the fix follows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iroh: advance registration high-water mark on heartbeat; pin sanity cap to client wire limit Finding 3 (reversed heartbeat completion): the in-place heartbeat update left registeredAt frozen at the slot's original insert time, so two reversed heartbeats both cleared the staleness gate and the later-landing OLDER challenge clobbered the newer refresh. Stamp registeredAt to the applied challenge's createdAt on the heartbeat path too, making it a true monotonic high-water mark of the newest challenge that has landed (the gate already guarantees challenge.createdAt >= registeredAt, so it only moves forward). Turns the added reversed-completion regression test from red to green. Finding 1 (cap above client wire limit): lower IROH_ACTIVE_BINDING_SANITY_CAP from 512 to 256 to match the iOS discovery decoder's maximumBindingCount. The broker's discoverySnapshot returns every active binding uncapped, and the client rejects any snapshot carrying more than 256 bindings; admitting a 257th active slot would make the account's own discovery response undecodable on every device. The existing sanity-cap test references the constant symbolically, so it tracks the new value automatically. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iroh: add failing test for reversed challenge completion on a fresh slot Covers the empty-slot ordering case the heartbeat test does not: two challenges minted older->newer for a slot that does not exist yet, the older landing first through the insert path. The genuinely newer registration, landing second, must refresh the slot rather than be rejected as superseded. Fails on current code because the insert stamps registeredAt with its own landing time instead of the challenge mint time, setting the high-water mark above the newer challenge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iroh: seed insert high-water mark from challenge mint time The staleness gate treats registeredAt as the mint time of the newest challenge that has landed, and the heartbeat path already stamps challenge.createdAt. The insert/reincarnation path still stamped the register-request landing time, so an older challenge that created the slot could set the high-water mark above a newer outstanding challenge's mint time and get it wrongly rejected as challenge_superseded, stranding the older registration. Stamp challenge.createdAt on insert too, making registeredAt an ordering-consistent high-water mark on every write path. 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> * fix(iroh): harden lifecycle and close attribution * test(iroh): decode Effect failures through public API * test(ios): require stable simulator device identity * fix(ios): seed simulator Iroh device identity * test(iroh): accept unscoped workspace events in rollover gate * fix(iroh): validate fresh rollover events by topic * test(iroh): cover release closeout regressions * fix(iroh): preserve trusted connection recovery * test(iroh): cover redaction and binding cap semantics * fix(iroh): harden release lifecycle boundaries * fix(iroh): clear retry inspection on scope exit * test(iroh): reproduce multi-Mac release gate targeting * fix(iroh): pin release gate to foreground Mac * fix(ios): isolate durable identity defaults safely * test(iroh): reproduce release gate readiness race * fix(iroh): require stable gate readiness * test(ios): reproduce stale reconnect client clobber * fix(ios): reject stale reconnect before client mutation * test(ios): reproduce restored identity and backup scope leaks * fix(ios): preserve device and backup scope identity * chore(iroh): adopt continuous relay token handoff * test(ios): cover exact release-gate simulator targeting * fix(ios): target release gate simulator by identifier * test(ios): reproduce release gate output sink displacement * fix(ios): isolate release gate terminal observation * test(ios): reproduce stale release gate workspace identity * fix(ios): reacquire long-lived release gate workspace * test(ios): cover complete relay refresh suspension * fix(ios): suspend every automatic relay renewal lane --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
pandec
pushed a commit
that referenced
this pull request
Oct 15, 2026
* Isolate CodeRouter from normal agent state * Pin resilient Stack logout backend
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Globe+Arrow keys produce Home/End/PageUp/PageDown at the hardware level on macOS (keyCodes 115/119/116/121). These keys couldn't be used as keyboard shortcuts because the shortcut system didn't handle their keyCodes or Unicode function-key characters. For example, binding Home to "Focus Pane Left" — the recording would silently fail, and even if manually configured, matching would fail because unmodified navigation keys were short-circuited by the
normalizedFlags.isEmptyfast path inhandleCustomShortcut.Mirrors the existing arrow key (←→↑↓) pattern across two files.
Touch points (7 changes across 2 files)
storedKey(from:)— map keyCodes 115/119/116/121 to glyphs ↖↘⇞⇟ so the recorder captures themStoredShortcut.from(event:)— exempt navigation keys from "require at least one modifier" guard (bare Globe+Arrow strips.function, leaving zero modifiers)matchShortcut— early keyCode-based match before character matching (which fails for function-key Unicode)handleCustomShortcut— let navigation keyCodes through thenormalizedFlags.isEmptyfast pathkeyEquivalent— map glyphs to SwiftUI.home/.end/.pageUp/.pageDownmenuItemKeyEquivalent— map glyphs to AppKitNSHomeFunctionKeyetc.keyCodeForShortcutKey— reverse glyph→keyCode lookupRelated:
Test plan