Skip to content

fix(devices): unpairing on the phone revokes it on the Host - #2412

Merged
DoodleBears merged 5 commits into
mainfrom
claude/project-thread-hyen2f
Oct 20, 2026
Merged

DoodleBears merged 5 commits into
mainfrom
claude/project-thread-hyen2f

Conversation

@DoodleBears

@DoodleBears DoodleBears commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Why the change

Unpair in the mobile app only forgot the Host on the phone, so the pairing stayed active on the Host. With hosted Connect, the gateway is reachable from the internet, so a pairing the person thinks is gone kept working.

Issue / Discussion: Closes #2409

Special things to note

  • Merge risk: low and reversible.
    • The Host gains one gateway route, POST /botharness-device/unpair. It needs the device's own Device credential and revokes only that device, using the existing pairing.revoke, which also closes its open streams.
    • The phone forgets the Host locally first, which stops its client and lands on the pairing screen. In the background it then unregisters push and calls the route, trying each saved route with an 8-second timeout per route.
    • A Host older than this change answers 404. The phone treats that as "Host kept" and shows the notice.
  • Offline fallback: if no route answers, the phone shows "{host} may still list this phone" and points to that computer's Settings › Devices. Answers of 401 device-revoked or device-unknown count as revoked.
  • Drawer: unpairing the last computer from the computers drawer now closes the drawer immediately.
  • CLI parity: no new Host UI or Bot tool. Desktop revoke already maps to its CLI command.
  • Verification:
    • Automated:
      • device-gateway.test.ts covers three cases: a missing credential gets 401; a device can revoke itself and is refused afterwards; another device is untouched.
      • The apps/mobile suite passes.
      • tsc --noEmit passes for the repo root and for apps/mobile.
    • iOS Simulator acceptance at 687e7cd, with a Host built from this branch. Results and shots are in the comments.
      • Drawer unpair: the drawer closes and the app goes straight to pairing with no "refused" banner. The Host sets revokedAt within about 1 s. The old credential gets 401 device-revoked.
      • Unpair from Settings with the Host offline: the app lands on pairing and shows the notice.
      • Shortened notice text: fits at the default size and at AX5, in light and dark.
    • Earlier rounds found two problems, fixed here:
      • a roughly 7 s wait after confirming Unpair;
      • a race in which the revoke closed the live stream before the local removal finished.
    • Known gap: at AX5 the native iOS alert still clips part of the Done button at its bottom edge. A sheet could replace it in a follow-up.
    • Not run: Android, and a check over a Tailscale route. The revoke logic does not depend on the route.
  • CI test plan: affected. No new config, test directory, or repo-reading test.

Change outline

  • packages/core/src/devices/gateway.ts: DEVICE_UNPAIR_PATH and its handler.
  • apps/mobile/src/host/pairing.ts: revokePairing({ host, carrier }) returns 'revoked' | 'unreachable'.
  • apps/mobile/src/host/host-session.ts: unpair() forgets locally, then returns { revoked: Promise<boolean> } for the background push unregister and Host revoke.
  • apps/mobile/src/ui/unpair-notice.ts: the notice, used from the drawer and from Settings.
  • apps/mobile/src/ui/shell/host-drawer.tsx: closes the drawer when the last computer is unpaired.
  • apps/mobile/src/ui/strings.ts: en and zh copy.
  • changes/2409-unpair-revokes-host.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01PmBSdCWaDy5fcVKc4vm7gn

Summary by CodeRabbit

  • New Features

    • Unpairing a computer now also revokes the phone’s pairing on that computer.
    • The app confirms when the pairing was successfully revoked.
    • If the computer cannot be reached, the app explains that it may still list the phone and provides steps to remove it from Settings › Devices.
    • Available in English and Chinese.
  • Documentation

    • Added release notes describing the updated unpairing behavior.

Issue: #2409

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmBSdCWaDy5fcVKc4vm7gn
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1c0e7a23-85ab-485c-97f2-ffa931702525
📥 Commits

Reviewing files that changed from the base of the PR and between 20779bd and 18e4b74.

📒 Files selected for processing: 9
  • apps/mobile/app/settings/index.tsx
  • apps/mobile/src/host/host-session.ts
  • apps/mobile/src/host/pairing.ts
  • apps/mobile/src/ui/shell/host-drawer.tsx
  • apps/mobile/src/ui/strings.ts
  • apps/mobile/src/ui/unpair-notice.ts
  • changes/2409-unpair-revokes-host.md
  • packages/core/src/devices/gateway.ts
  • packages/core/test/device-gateway.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

📝 Walkthrough

Phone-initiated unpairing now removes the pairing on the host when reachable. If the app cannot confirm revocation, it displays guidance for removing the phone from the host’s device settings.

Changes: Host Pairing Revocation

Layer / File(s) Summary
Authenticated host revocation endpoint
packages/core/src/devices/gateway.ts, packages/core/test/device-gateway.test.ts
The device gateway adds a POST /unpair endpoint. It revokes the authenticated device’s pairing, rejects invalid credentials, and has tests for self-revocation and preserving another device’s pairing.
Mobile unpair and remote revocation
apps/mobile/src/host/pairing.ts, apps/mobile/src/host/host-session.ts
revokePairing sends authenticated requests to saved host routes and returns revoked or unreachable. hostSession.unpair removes the host locally and returns a promise that reports whether revocation was confirmed.
Unpair feedback and fallback guidance
apps/mobile/src/ui/unpair-notice.ts, apps/mobile/src/ui/strings.ts, apps/mobile/app/settings/index.tsx, apps/mobile/src/ui/shell/host-drawer.tsx, changes/2409-unpair-revokes-host.md
The settings screen and host drawer pass unpair results to noticeHostKept. When revocation is unconfirmed, it displays localized instructions to remove the phone from the host’s device settings. English and Chinese strings and changelog entries are added.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant Person
  participant MobileApp
  participant HostGateway
  Person->>MobileApp: Unpair from host
  MobileApp->>HostGateway: Authenticated POST /unpair
  HostGateway-->>MobileApp: Revocation response or unreachable
  MobileApp-->>Person: Show fallback guidance if unconfirmed
Loading
Merge Risk: ⚪ Minimal · up to 18e4b

Unpairing on the phone now also revokes the phone on the computer when reachable, and shows guidance when it cannot confirm. No actionable merge-blocking risk was found.

Pre-merge checks: Passed 3 · Failed 2

Failed Failed checks: 2 warnings

Check name Status Explanation Resolution
Linked Issues check Warning Issue [#2409] requires a mobile automated test for Host revocation and the offline fallback. The whole-PR change summary lists the Host gateway test, but it lists no mobile test change or reviewable m… Add a mobile automated test that verifies successful Host revocation and local unpairing when the Host is unreachable, including the fallback result used by the UI.
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 8 files. (1 skipped: 1 … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 3 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: mobile unpairing now revokes the device on the Host.
Description check Passed The description includes the required sections, explains the user problem and implementation, documents merge risk, verification results, known gaps, CI planning, and UI acceptance findings. It also i…
Out of Scope Changes check Passed The changed gateway route, mobile pairing and session logic, fallback notice, drawer behavior, localized strings, changelog, and supporting Host test all implement or support issue [#2409]. No unrelat…
Full details: Description check

Explanation

The description includes the required sections, explains the user problem and implementation, documents merge risk, verification results, known gaps, CI planning, and UI acceptance findings. It also identifies unrun Android, real-device, and Tailscale checks. Screenshots are referenced in comments rather than embedded, and the full required system-text matrix is not documented, but the description is otherwise complete and transparent.

Full details: Out of Scope Changes check

Explanation

The changed gateway route, mobile pairing and session logic, fallback notice, drawer behavior, localized strings, changelog, and supporting Host test all implement or support issue [#2409]. No unrelated change is demonstrated. CLI parity remains unchanged because the PR adds no new Host action.

Full details: Linked Issues check

Explanation

Issue [#2409] requires a mobile automated test for Host revocation and the offline fallback. The whole-PR change summary lists the Host gateway test, but it lists no mobile test change or reviewable mobile test evidence. The implementation covers the required flow: POST /botharness-device/unpair authenticates the device credential, revokes only that device, and mobile unpairing reports unreachable Hosts. The Host self-revocation test covers missing credentials, self-revocation, rejection after revocation, and protection of another device.

Resolution

Add a mobile automated test that verifies successful Host revocation and local unpairing when the Host is unreachable, including the fallback result used by the UI.

Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 8 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Contributor Author

iOS Simulator acceptance at 43151d8 (iOS 27, dev client; QA Host restarted from this branch). Steps 1–3 pass. Screenshots are on claude/2412-ios-shots @ f87b3c7.

  1. Pair: pass. The Host's device list shows the phone as active.
  2. Unpair from the host drawer (⋯ → Unpair → confirm): pass.
    • The Host sets revokedAt on the device, and the phone goes back to the pairing screen.
    • I paired a second test device and unpaired it through /botharness-device/unpair. Its old credential then gets 401 {"code":"device-revoked"} on /botharness-device/api/remote.mux, and a second unpair gets the same 401. The credential was never printed.
    • Small lag: after confirming, the drawer kept showing "My Mac" with an orange, then grey dot for about 7 s before it closed.
  3. Unpair from Settings with the Host stopped: pass. The phone removes the Host and shows "My Mac may still list this phone … On that computer, remove it in DeepSeekBot Settings > Devices." After the Host restarted, that device was still active there, as the notice says.
Light Dark AX5, light AX5, dark

AX5 finding: the notice is a native alert. At the largest text size, the body is cut off at "On that computer," and the Done button sits half outside the alert. A shorter body or a sheet would fit better.

Step 4 (over Tailscale): not run. The QA Host has no Tailscale route, and adding one would change the Mac's persistent tailscale serve config. Revocation is decided on the Host, so step 2 already covers the logic; the route only changes how the request gets there.

Cleanup: all test pairings on the QA Host are revoked. 0 active devices.


Generated by Claude Code

…ground

Acceptance found a 7 s pause after confirming Unpair while the phone waited
on the Host, and the offline notice clipped at the largest text size. The
phone now forgets the Host immediately, revokes afterwards, and shows a
shorter notice only if the Host could not be reached.

Issue: #2409

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmBSdCWaDy5fcVKc4vm7gn
@DoodleBears
DoodleBears marked this pull request as ready for review October 20, 2026 17:43
DoodleBears added a commit that referenced this pull request Oct 20, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author

iOS recheck at c88c74b (iOS 27, dev client; QA Host restarted from this head). Screenshots are on claude/2412-ios-shots @ a4a8348.

  1. Unpair from the drawer with the Host online: the revoke passes, the drawer fails.
    • The Host set revokedAt within about 1 s of tapping Unpair.
    • The drawer does not close. It stays open showing "My Mac" with a grey dot for 6 s or more, while the pairing screen is already behind it. It only closes on a tap outside.
      <img src="https://raw.githubusercontent.com/botharness/deepseekbot/a4a8348f9c9a99e00203d73c5b42383a95a85b27/.github/pr-screenshots/2412/notice-r2-drawer-stale-light.png" width="200">
  2. Unpair from Settings with the Host offline: the shorter text passes, AX5 is partial.
    • The alert reads "My Mac may still list this phone / Couldn't reach it. Remove this phone in its Settings > Devices." At default size it fits in light and dark.
    • At AX5 the whole body now shows, but the Done button is still cut off by the bottom edge of the alert. This looks like the iOS 27 system alert at AX5; a sheet would avoid it.
Default, light Default, dark AX5, light AX5, dark

Cleanup: all QA devices are revoked on the Host, with 0 active. Step 4 (unpair over Tailscale) runs next, with the #2397 device check.


Generated by Claude Code

…paired

Issue: #2409

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmBSdCWaDy5fcVKc4vm7gn

Copy link
Copy Markdown
Contributor Author

iOS Simulator recheck at d4c7745: the drawer passes, but there is a regression. The Host ran on this branch, and the dev client had one paired computer.

  • Pass: after you confirm Unpair on the last paired computer, the drawer closes right away.
  • Pass: the Host revokes the device, which leaves the active list.
  • Fail (regression): the phone doesn't forget the computer. It stays on that computer's Chats list, and every chat is still visible. A red banner reads "Your computer refused this app. This phone was removed from the paired devices on your computer. Pair again". At c88c74b, the app went back to the pairing screen. The state lasted 6 s or more.

Likely cause, inferred: the background revoke's 401 device-revoked arrives before the local removal. The app treats it as "the computer refused this app", so it never drops the local host. No screenshot, because it shows real chat names.


Generated by Claude Code

Revoking first could close the phone's live connection before the local
removal landed, leaving the phone on the old computer's chats under a
'refused' banner. The phone now drops the computer, then unregisters push
and revokes in the background.

Issue: #2409

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmBSdCWaDy5fcVKc4vm7gn
DoodleBears added a commit that referenced this pull request Oct 20, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author

iOS 27 Simulator recheck at 687e7cd: both steps pass. The Host ran on this branch, and the dev client had one paired computer.

  1. Unpair from the drawer: pass. Confirming Unpair closes the drawer, and the app goes straight to the pairing screen. There's no red "refused" banner. The Host sets revokedAt on the device.
  2. Unpair from Settings with the Host offline: pass. The app lands on the pairing screen, then shows "My Mac may still list this phone. Couldn't reach it. Remove this phone in its Settings › Devices."
Drawer unpair Offline Settings unpair

Not caused by this PR: the "Pair with your computer" title sits faded under the status bar. That's the title bug fixed in #2402.


Generated by Claude Code

…-hyen2f

# Conflicts:
#	apps/mobile/src/host/host-session.ts
@DoodleBears
DoodleBears merged commit 3b54528 into main Oct 20, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unpair on the phone should revoke the device on the Host

2 participants