feat: remote control — Expo mobile app + desktop proxy#1693
feat: remote control — Expo mobile app + desktop proxy#1693sethwebster wants to merge 4 commits intopingdotgg:mainfrom
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
ApprovabilityVerdict: Needs human review Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
844f0bd to
e73b81c
Compare
|
Squashed to a single commit and resolved all review feedback: Cursor Bugbot / Macroscope — Critical & High
Macroscope — Medium
|
e73b81c to
f4be292
Compare
f4be292 to
2931631
Compare
|
Resolved second round of review feedback: Cursor Bugbot — High
Cursor Bugbot — Medium
Macroscope — High
Macroscope — Medium
|
2931631 to
60b4878
Compare
|
Resolved third round: Macroscope — High
Macroscope — Medium
Cursor Bugbot — Medium
|
60b4878 to
31b2d0e
Compare
b3f9656 to
e1a307d
Compare
e1a307d to
2bacb8d
Compare
2bacb8d to
3386c8b
Compare
3386c8b to
1ccd62d
Compare
1ccd62d to
0c85e09
Compare
b04e14b to
3382b4f
Compare
| } | ||
|
|
||
| return reversed; | ||
| } |
There was a problem hiding this comment.
Custom sort/reverse reimplements standard array methods unnecessarily
Low Severity
sortCopy and reverseCopy are hand-rolled reimplementations of [...arr].sort(fn) and [...arr].reverse() with 42 lines of custom insertion-sort and manual-reverse logic. They add no functional benefit over the standard methods (Hermes in Expo SDK 55 supports stable sort per ES2019). Notably, remoteClient.ts in the same app already uses Array.prototype.sort directly, making the custom utilities inconsistent within the codebase itself.
| url.searchParams.set("token", authToken); | ||
| } | ||
| return url.toString(); | ||
| } |
There was a problem hiding this comment.
Auth token exposed in HTTP image URL query params
Low Severity
messageImageUrl places the auth token in the query string of plain HTTP image URLs. While query-param auth is unavoidable for WebSocket connections, HTTP image requests support Authorization headers. Query-param tokens can leak through server access logs, HTTP caches, and Referer headers when images are loaded. Combined with the NSAllowsArbitraryLoads / usesCleartextTraffic settings allowing unencrypted traffic, this means the auth token can be transmitted and logged in cleartext.
| const authToken = input.authToken.trim(); | ||
| if (authToken.length > 0) { | ||
| wsUrl.searchParams.set("token", authToken); | ||
| } |
There was a problem hiding this comment.
Auth token passed in URL query parameter
Medium Severity
The auth token is passed as a query parameter (?token=...) in the WebSocket URL and image attachment URLs. Query parameters are commonly logged by proxies, web servers, and browser history, and may appear in Referer headers. The REMOTE.md doc even describes this as "Auth via query param on WebSocket URL" under the security model. Using an Authorization header or a subprotocol for WebSocket auth, and a cookie or header for image fetches, would avoid leaking the token in logs and URLs.
Additional Locations (1)
|
|
||
| next[insertionIndex + 1] = candidate; | ||
| insertionIndex -= 1; | ||
| } |
There was a problem hiding this comment.
Insertion sort silently injects undefined into typed arrays
Low Severity
In sortCopy, when candidate at insertionIndex is undefined (line 16), the while loop doesn't break and writes candidate (i.e. undefined) into next[insertionIndex + 1] on line 20. This shifts undefined values through the array without the type system catching it, since next is typed as T[]. For dense arrays this is a no-op, but if T includes values that happen to be at holes, the sort can corrupt the output. Using [...values].sort(compareFn) would be simpler and correct.
|
|
||
| process.once("SIGTERM", () => { | ||
| shutdown({ code: 143 }); | ||
| }); |
There was a problem hiding this comment.
Signal lost in dev.mjs shutdown, uses exit code instead
Low Severity
The SIGINT and SIGTERM handlers call shutdown() with only a code (130 / 143) but no signal property. The maybeExit function (line 27-28) only re-raises the signal via process.kill(process.pid, exitSignal) when exitSignal is set. Without it, the process calls process.exit(130) instead of re-raising SIGINT, which breaks signal-chain conventions — parent processes (like shells) can't distinguish a numeric exit from a proper signal death.
Adds an Expo mobile app (apps/mobile/) that connects to the desktop app over the local network via an authenticated WebSocket proxy, providing full remote control of coding sessions from a phone or tablet. Desktop remote access: - DesktopRemoteManager starts HTTP+WS proxy on port 3773 with token auth - Bidirectional WS relay with bounded message buffer (256 max) - QR code pairing via t3remote:// deep link scheme - Network address discovery for LAN/Tailnet endpoints - Constant-time token comparison, TOCTOU mutex on server start Mobile app: - Browse projects and threads, open thread detail with circular reveal - Stream assistant output, send messages with image/clipboard attachments - Answer approval requests and pending user-input prompts - Exponential backoff reconnection (500ms-8s) with grace period UI - Secure credential storage (expo-secure-store on native, AsyncStorage fallback) Server: - ThreadMessageHistoryQuery service with offset pagination for mobile feed - /api/remote/health endpoint for mobile preflight checks Web: - Remote settings UI in sidebar (enable/disable, endpoint URLs, QR codes) - Fix clearPromotedDraftThread to also clear draftsByThreadId - Fix Sidebar.logic.test assertions to match updated class names Code quality: - All bare useEffect calls extracted into named custom hooks - Inline closures wrapped in useCallback for stable references - Markdown styles and glyph lookup tables hoisted to module constants - Regression tests for remoteAccess.ts and remote.ts - parseRemoteAppConnectionUrl returns authToken: string | null
- Replace .toSorted() with sortCopy() for Hermes compatibility - Pass attachments to applyOptimisticUserMessage - Include already-loaded message IDs in streaming update filter
…eak, snapshot refresh - loadToken() now falls back to async storage when secure store unavailable - connect() rejects pending requests before closing socket (matches disconnect()) - requiresSnapshotRefresh returns false for thread.archived/unarchived
5b04ba4 to
16ed6cc
Compare
| <Modal | ||
| animationType="slide" | ||
| presentationStyle="pageSheet" | ||
| allowSwipeDismissal | ||
| visible={props.visible} | ||
| onRequestClose={props.onRequestClose} | ||
| > |
There was a problem hiding this comment.
🟢 Low connection/ConnectionSheet.tsx:165
allowSwipeDismissal is not a valid prop on the React Native Modal component, so it will be silently ignored. On iOS with presentationStyle="pageSheet", swipe-to-dismiss is already enabled by default; if the intent was to disable it, this prop won't achieve that and the modal will still dismiss on swipe. Consider removing the invalid prop, or if swipe prevention is required, use a third-party modal library that supports this feature.
<Modal
animationType="slide"
presentationStyle="pageSheet"
- allowSwipeDismissal
visible={props.visible}🤖 Copy this AI Prompt to have your agent fix this:
In file apps/mobile/src/features/connection/ConnectionSheet.tsx around lines 165-171:
`allowSwipeDismissal` is not a valid prop on the React Native `Modal` component, so it will be silently ignored. On iOS with `presentationStyle="pageSheet"`, swipe-to-dismiss is already enabled by default; if the intent was to disable it, this prop won't achieve that and the modal will still dismiss on swipe. Consider removing the invalid prop, or if swipe prevention is required, use a third-party modal library that supports this feature.
Evidence trail:
apps/mobile/src/features/connection/ConnectionSheet.tsx lines 1 (import shows Modal from 'react-native') and lines 165-171 (Modal usage with allowSwipeDismissal prop). apps/mobile/package.json shows react-native version 0.83.2. Web research confirms React Native 0.83.x Modal does not have allowSwipeDismissal as a valid prop - verified against React Native source (Modal.js, ModalHost.js, ReactModalHostManager.java).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 6 total unresolved issues (including 5 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 16ed6cc. Configure here.
|
|
||
| if (exitSignal !== null) { | ||
| process.kill(process.pid, exitSignal); | ||
| return; |
There was a problem hiding this comment.
Signal re-delivery caught by own once handler
Low Severity
When a child process dies from a signal during shutdown, maybeExit calls process.kill(process.pid, exitSignal) to re-signal itself. However, the process.once("SIGTERM") handler is still registered and catches that signal, calling shutdown() which returns early since shuttingDown is already true. The once handler is then removed, but no process.exit() is ever called. The event loop drains and the process exits with code 0 instead of the intended non-zero exit code. The signal handlers need to be removed before re-signaling, e.g. via process.removeAllListeners on the target signal.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 16ed6cc. Configure here.


Summary
apps/mobile/) connects to the desktop app over the local network via an authenticated WebSocket proxy, providing full remote control of coding sessionsapps/desktop/src/remoteAccess.ts) — HTTP+WS proxy on port 3773 with token auth, QR code pairing, and network address discoverypackages/shared/src/remote.ts) —t3remote://URL scheme for QR-code and URL-based pairing between mobile and desktopThreadMessageHistoryQueryservice with offset-based pagination for mobile's lazy-load feeduseEffectcalls extracted into named custom hooks, inline closures wrapped inuseCallback, markdown styles and glyph tables hoisted to module constantsTest plan
t3remote://connect?serverUrl=...&authToken=...auto-connectsnpx vitest runpasses in all packages (shared: 62, desktop: 53, web: 615, server: 597)Note
Medium Risk
Introduces a large new
apps/mobileExpo client that can connect to remote servers and issue orchestration commands, plus changes desktop runtime (single-instance lock, optional devtools) and dev scripts; issues are likely to be integration/runtime regressions rather than isolated UI bugs.Overview
Adds a new Expo/React Native mobile app (
apps/mobile) for remote orchestration control: connect via server URL + optional auth token (with preflight health check), persist credentials, browse projects/threads, view a thread feed with lazy message pagination, send/queue messages with attachments, respond to approvals/user-input prompts, interrupt running turns, and handle reconnecting UI.Updates desktop development and runtime behavior: replaces the desktop
devscript with a coordinator (scripts/dev.mjs) for bundler/Electron lifecycle, adjusts Electron launcher resolution for dev on macOS, adds a single-instance lock with “focus existing window” behavior, and gates auto-opening DevTools behindT3_DESKTOP_OPEN_DEVTOOLS=1. Also adds a testedcloseWebSockethelper to safely close vs terminate depending on close code.Documentation and repo hygiene: expands
REMOTE.mdwith mobile remote setup/architecture notes and updates.gitignorefor Expo/native artifacts.Reviewed by Cursor Bugbot for commit 16ed6cc. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add Expo mobile app and desktop proxy for remote control of the T3 chat interface
apps/mobileExpo/React Native app that connects to a running desktop instance over WebSocket, displays thread lists and chat feeds, and supports composing messages with image attachmentsRemoteClientclass with reconnection logic, RPC over WebSocket, and a/api/remote/healthhealth-check endpoint on the server for preflight validationgetThreadMessagesPageas a paginated RPC method wired through contracts, server WS handler, and the webNativeApisurfacebuildRemoteAppConnectionUrl,parseRemoteAppConnectionUrl) and a QR code component in web settings so users can connect the mobile app by scanningpackage.jsonMacroscope summarized 16ed6cc.