Skip to content

[codex] Rewrite client connection architecture#2978

Open
juliusmarminge wants to merge 19 commits into
codex/hosted-web-tracing-pocfrom
codex/connection-state-audit
Open

[codex] Rewrite client connection architecture#2978
juliusmarminge wants to merge 19 commits into
codex/hosted-web-tracing-pocfrom
codex/connection-state-audit

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jun 6, 2026

Copy link
Copy Markdown
Member

Summary

Replace the legacy web and mobile connection implementations with a shared Effect-based client runtime, then move the mobile thread experience onto the same current contracts and turn lifecycle behavior as web.

  • introduce environment descriptors, connection drivers, resolution, supervision, authorization, relay discovery, RPC sessions, and lifecycle ownership in @t3tools/client-runtime
  • move runtime state to explicit environment-scoped modules for auth, connections, projects, threads, shell, terminal, filesystem, source control, review, and presentation
  • split @t3tools/client-runtime into explicit subpath exports and migrate web/mobile away from their independent stores and RPC clients
  • add persisted connection catalogs for web, mobile, and desktop, including encrypted desktop storage and mobile legacy migration
  • centralize retry/backoff, connectivity wakeups, credential refresh, account-transition cleanup, session ownership, and subscription recovery
  • preserve and extend the scoped relay tracing introduced by [codex] Trace first-party relay clients #2995 across the rewritten runtime
  • make mobile provider controls capability-driven, including Codex serviceTier instead of the legacy hard-coded Fast Mode toggle
  • rebuild the mobile thread feed around keyboard-aware Legend List scrolling, glass-safe content insets, settled-turn folding, richer work rows, and predictable auto-follow behavior
  • add selectable native markdown with cross-node selection, stronger typography and lists, link/file presentation, Shiki-highlighted code blocks, and code/message copy controls
  • port the latest main turn projection and fold fixes to the shared reducer and mobile presentation, including interim assistant messages, steer boundaries, trailing work, and interrupted turns
  • harden local relay startup so occupied loopback ports fall back correctly, and make agent-activity publishing wait for reconciled T3 Connect credentials instead of reporting a misleading missing-config state

Stack

  1. main
  2. [codex] Trace first-party relay clients #2995 Trace first-party relay clients
  3. [codex] Rewrite client connection architecture #2978 Rewrite client connection architecture ← this PR

Review this PR against #2995. The tracing infrastructure and release configuration remain isolated in the base PR.

Validation

  • vp check
  • vp run typecheck
  • vp run lint:mobile
  • 101 focused mobile, web, client-runtime, server, and shared tests covering thread folds, markdown, provider options, layout, relay startup, and connection state
  • live iOS Simulator verification for scrolling, selection, syntax highlighting, and copy controls

Note

High Risk
Touches encrypted connection catalogs, credential migration, and broad mobile connection/state rewiring—errors could drop saved environments or break cloud pairing until users reconnect.

Overview
Adds desktop persistence for the shared connection catalog: an encrypted on-disk store (Electron safe storage), atomic writes, corrupt/undecryptable catalog discard, and get/set/clear IPC exposed through preload—wired into the main app layer. Safe-storage types move to ElectronSafeStorageService so tests and stores can depend on the interface without the Electron implementation.

On mobile, introduces a dedicated connection/ stack on @t3tools/client-runtime: secure-store catalog with legacy t3code.connections migration, file-backed shell/thread snapshot caches, connectivity and app-lifecycle wakeups, and platform hooks (Clerk relay token, DPoP token store, SSH blocked). Screens shift from the old remote-catalog/registry hooks to workspace/entity atoms, useConnectionController for T3 Cloud rows (switches, trace IDs, relay refresh), and a /settings/auth sheet route with expandable Clerk detent instead of native auth modals. Theme tokens replace hard-coded colors in chrome; favicons use DPoP-aware HTTP headers.

Ships a new iOS T3ComposerEditor Expo module (UIKit rich text, atomic skill/file chips, image paste) plus CSS tokens for inline skills/glass. Smaller fixes: cloud auth setHeaders shape, Tailscale orElseSucceed, SSH IPC imports from client-runtime subpaths, mobileRuntimeruntime in settings/agent code.

Reviewed by Cursor Bugbot for commit f32e94b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Rewrite client connection architecture to use atom-based environment state

  • Replaces direct RPC/store-based environment access across web, mobile, and desktop with a new layered connection architecture: connectionLayer, EnvironmentRegistry, and EnvironmentSupervisor manage environment lifecycle, persistence, and reconnection with exponential backoff.
  • Introduces per-platform (web/mobile/desktop) connection runtimes with typed ConnectionTarget variants (Primary, Bearer, Relay, SSH), encrypted catalog persistence, and cache-backed shell/thread snapshots.
  • Adds comprehensive atom families for environment-scoped operations (VCS, terminal, server config, orchestration, shell, projects, threads, source control, auth, relay discovery) consumed via new useEnvironmentQuery and useAtomSet hooks.
  • Replaces @t3tools/client-runtime root imports with explicit subpath exports (e.g. /relay, /rpc, /state/vcs); a new ESLint rule enforces this.
  • Introduces DesktopConnectionCatalogStore for encrypted catalog persistence on desktop and managedRelayAccessTokenStore using SecureStore on mobile; session tokens are now cached with JWT-expiry awareness.
  • Mobile thread feed gains turn-folding, selectable native markdown (iOS), a reconnect/unavailable composer pill, and ComposerEditor replacing plain TextInput.
  • LocalApi browser implementation no longer proxies to a live backend; server.* and shell.* methods always reject until pairing.
  • Risk: Many internal APIs changed shape (cwdworkspaceRoot, nametitle, turnDiffSummariescheckpoints, orchestrationStatusstatus, closedstopped); any consumers outside the monorepo relying on @t3tools/client-runtime root imports or these field names will break.

Macroscope summarized f32e94b.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1f7b3268-66d9-438c-9c60-65a92eed066a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/connection-state-audit

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

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jun 6, 2026
@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

🚀 Expo continuous deployment is ready!

  • Project → t3-code
  • Platforms → android, ios
  • Scheme → t3code-preview
  🤖 Android 🍎 iOS
Fingerprint cb27f25382c2604a1d596a8a5c17dc851b3f401c 5046c3d6f52335d7c1c932c37636c0cc001e96e4
Build Details Build Permalink
DetailsDistribution: INTERNAL
Build profile: preview:dev
Runtime version: cb27f25382c2604a1d596a8a5c17dc851b3f401c
App version: 0.1.0
Git commit: eb1e048dd463cb65dd7b06450c4319535e4e5a85
Build Permalink
DetailsDistribution: INTERNAL
Build profile: preview:dev
Runtime version: 5046c3d6f52335d7c1c932c37636c0cc001e96e4
App version: 0.1.0
Git commit: eb1e048dd463cb65dd7b06450c4319535e4e5a85
Update Details Update Permalink
DetailsBranch: pr-2978
Runtime version: cb27f25382c2604a1d596a8a5c17dc851b3f401c
Git commit: eb1e048dd463cb65dd7b06450c4319535e4e5a85
Update Permalink
DetailsBranch: pr-2978
Runtime version: 5046c3d6f52335d7c1c932c37636c0cc001e96e4
Git commit: eb1e048dd463cb65dd7b06450c4319535e4e5a85
Update QR

Comment thread apps/web/src/environments/runtime/service.ts Outdated
Comment thread apps/web/src/connection/appQueries.ts Outdated
Comment thread apps/web/src/connection/storage.ts
Comment thread apps/mobile/src/app/settings/environments.tsx
Comment thread apps/web/src/state/terminalSessions.ts
Comment thread packages/client-runtime/src/connection/resolver.ts Outdated
Comment thread packages/client-runtime/src/state/threads.ts
@juliusmarminge juliusmarminge force-pushed the codex/connection-state-audit branch from 16c9aba to 2a29e34 Compare June 7, 2026 19:53
@juliusmarminge juliusmarminge changed the title Harden remote connection state handling across mobile, web, and relay [codex] Rewrite client connection architecture Jun 7, 2026
@juliusmarminge juliusmarminge changed the base branch from main to codex/connection-infra-otel-base June 7, 2026 19:53
@juliusmarminge juliusmarminge force-pushed the codex/connection-state-audit branch from 2a29e34 to b976d5f Compare June 7, 2026 20:09
Comment on lines +87 to +113
const openDatabase = Effect.fn("web.connectionStorage.openDatabase")(function* () {
return yield* Effect.callback<IDBDatabase, ConnectionTransientError>((resume) => {
if (typeof indexedDB === "undefined") {
resume(
Effect.fail(catalogError("open", "IndexedDB is unavailable in this browser context.")),
);
return;
}
const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION);
request.addEventListener("upgradeneeded", () => {
if (!request.result.objectStoreNames.contains(CATALOG_STORE_NAME)) {
request.result.createObjectStore(CATALOG_STORE_NAME);
}
if (!request.result.objectStoreNames.contains(SHELL_STORE_NAME)) {
request.result.createObjectStore(SHELL_STORE_NAME);
}
if (!request.result.objectStoreNames.contains(THREAD_STORE_NAME)) {
request.result.createObjectStore(THREAD_STORE_NAME);
}
});
request.addEventListener("error", () => {
resume(Effect.fail(catalogError("open", request.error ?? "Unknown IndexedDB error")));
});
request.addEventListener("success", () => {
resume(Effect.succeed(request.result));
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium connection/webConnectionStorage.ts:87

When indexedDB.open() needs to upgrade the schema but another tab holds an older version, the blocked event fires and the Effect never completes — neither success nor error handlers execute until the blocking tab closes. If that tab never closes, openDatabase hangs indefinitely. Consider handling the blocked event by resuming with a descriptive error or triggering a retry with timeout.

+    request.addEventListener("blocked", () => {
+      resume(Effect.fail(catalogError("open", "Database upgrade blocked by another tab")));
+    });
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/connection/webConnectionStorage.ts around lines 87-113:

When `indexedDB.open()` needs to upgrade the schema but another tab holds an older version, the `blocked` event fires and the Effect never completes — neither `success` nor `error` handlers execute until the blocking tab closes. If that tab never closes, `openDatabase` hangs indefinitely. Consider handling the `blocked` event by resuming with a descriptive error or triggering a retry with timeout.

Evidence trail:
apps/web/src/connection/webConnectionStorage.ts lines 87-114 at REVIEWED_COMMIT: `openDatabase` registers handlers for `upgradeneeded` (line 96), `error` (line 107), and `success` (line 110), but not for `blocked`. IndexedDB spec: https://w3c.github.io/IndexedDB/#request-api — the `blocked` event fires on IDBOpenDBRequest when the open operation is blocked by existing connections, and `success`/`error` don't fire until the block is resolved.

@juliusmarminge juliusmarminge force-pushed the codex/connection-state-audit branch 2 times, most recently from fb36d5e to 8de82fb Compare June 7, 2026 20:23
Comment thread apps/web/src/connection/webConnectionPlatform.ts Outdated
Comment thread apps/mobile/src/features/cloud/CloudAuthProvider.tsx Outdated
@juliusmarminge juliusmarminge force-pushed the codex/connection-state-audit branch 4 times, most recently from 9ba3552 to a01b7f9 Compare June 7, 2026 20:45
@juliusmarminge juliusmarminge force-pushed the codex/connection-infra-otel-base branch from ced8bdf to 22b4b24 Compare June 7, 2026 20:50
@juliusmarminge juliusmarminge force-pushed the codex/connection-state-audit branch 4 times, most recently from 409fd6c to ecd84a7 Compare June 7, 2026 21:06
Base automatically changed from codex/connection-infra-otel-base to main June 8, 2026 03:21
@juliusmarminge juliusmarminge force-pushed the codex/connection-state-audit branch from ecd84a7 to f5c0afe Compare June 8, 2026 04:09
@juliusmarminge juliusmarminge changed the base branch from main to codex/hosted-web-tracing-poc June 8, 2026 04:09
Comment thread packages/client-runtime/src/connection/registry.ts Outdated
);
if (!isSignedIn || !userId) {
setManagedRelaySession(appAtomRegistry, null);
if (previousAccount !== null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium cloud/managedAuth.tsx:59

On first render when signed out, previousAccount is undefined, so undefined !== null evaluates to true and queueAccountCleanup() runs unnecessarily. This triggers removeRelayEnvironments() and relay client token cache reset even though no account was ever established. Change the condition to if (previousAccount) so cleanup only happens when transitioning from an actual signed-in state.

-      if (previousAccount !== null) {
+      if (previousAccount) {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/cloud/managedAuth.tsx around line 59:

On first render when signed out, `previousAccount` is `undefined`, so `undefined !== null` evaluates to `true` and `queueAccountCleanup()` runs unnecessarily. This triggers `removeRelayEnvironments()` and relay client token cache reset even though no account was ever established. Change the condition to `if (previousAccount)` so cleanup only happens when transitioning from an actual signed-in state.

Evidence trail:
apps/web/src/cloud/managedAuth.tsx lines 26, 35, 37, 57-61, 63 at REVIEWED_COMMIT. Line 26: ref initialized to `undefined`. Line 35: `previousAccount = observedAccountRef.current` (undefined on first render). Line 59: condition `previousAccount !== null` doesn't account for `undefined`. Line 63: correctly checks all three states (`!== undefined && !== null && !== userId`).

? (activeSavedEnvironmentRuntime?.connectionState ?? "disconnected")
: "connected";
const primaryEnvironmentId = primaryEnvironment?.environmentId ?? null;
const activeEnvironment =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium components/ChatView.tsx:1119

The refactored code removed the guard that excluded the primary environment from unavailability checks. Now activeEnvironmentUnavailable is computed for all environments including the primary, so if the primary's connection.phase is temporarily "connecting" or "available" during startup, the UI shows an unavailable banner and blocks message dispatch — behavior that never occurred before.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/ChatView.tsx around line 1119:

The refactored code removed the guard that excluded the primary environment from unavailability checks. Now `activeEnvironmentUnavailable` is computed for all environments including the primary, so if the primary's `connection.phase` is temporarily `"connecting"` or `"available"` during startup, the UI shows an unavailable banner and blocks message dispatch — behavior that never occurred before.

Evidence trail:
Old guard at MERGE_BASE in ChatView.tsx: `activeThread.environmentId !== primaryEnvironmentId` check in `activeSavedEnvironmentRecord` assignment; new code at REVIEWED_COMMIT ChatView.tsx lines 1119-1123 with no primary guard; `AVAILABLE_CONNECTION_STATE` defined at packages/client-runtime/src/connection/model.ts:148-154 with `phase: "available"`; `activeEnvironmentUnavailable` used to block sends at ChatView.tsx:2806, ChatView.tsx:3427; used to block revert at ChatView.tsx:2746; used for banner at ChatView.tsx:1353; `isConnecting` always false at ChatView.tsx:904 (`_setIsConnecting` unused).

Comment thread apps/web/src/hooks/useSettings.ts
Comment thread apps/web/src/state/query.ts
Comment thread apps/web/src/components/ProviderUpdateLaunchNotification.tsx
Comment thread apps/mobile/src/features/threads/use-project-actions.ts
Comment thread apps/mobile/src/connection/storage.ts
Comment thread packages/client-runtime/src/connection/supervisor.ts
Comment thread apps/mobile/src/features/threads/new-task-flow-provider.tsx
Comment on lines +1808 to +1835
(activeThread.session !== null && activeThread.session.status !== "stopped")),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium components/ChatView.tsx:1808

The envLocked check at line 1808 only excludes status !== "stopped", but the new session model has two additional terminal states "interrupted" and "error". Threads with these statuses will incorrectly have envLocked = true, blocking users from changing branch/env-mode settings on effectively terminated threads. Consider updating the condition to also exclude "interrupted" and "error".

-      (activeThread.session !== null && activeThread.session.status !== "stopped")),
+      (activeThread.session !== null &&
+        activeThread.session.status !== "stopped" &&
+        activeThread.session.status !== "interrupted" &&
+        activeThread.session.status !== "error")),
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/ChatView.tsx around line 1808:

The `envLocked` check at line 1808 only excludes `status !== "stopped"`, but the new session model has two additional terminal states `"interrupted"` and `"error"`. Threads with these statuses will incorrectly have `envLocked = true`, blocking users from changing branch/env-mode settings on effectively terminated threads. Consider updating the condition to also exclude `"interrupted"` and `"error"`.

Evidence trail:
packages/contracts/src/orchestration.ts:249-257 (OrchestrationSessionStatus defines: idle, starting, running, ready, interrupted, stopped, error); apps/web/src/session-logic.ts:1252-1260 (derivePhase treats stopped/interrupted/error all as 'disconnected' terminal states); apps/web/src/components/ChatView.tsx:1805-1809 (envLocked check only excludes 'stopped'); apps/web/src/components/ChatView.tsx:1816 (envLocked blocks onEnvironmentChange); apps/web/src/components/ChatView.tsx:2497 (envLocked blocks canOverrideServerThreadEnvMode); apps/web/src/components/ChatView.tsx:3820 (envLocked passed to child component)

@juliusmarminge juliusmarminge force-pushed the codex/hosted-web-tracing-poc branch from 03c78cc to 4c38462 Compare June 10, 2026 22:28
@juliusmarminge juliusmarminge force-pushed the codex/hosted-web-tracing-poc branch from 4c38462 to f37abe6 Compare June 10, 2026 23:28
@juliusmarminge juliusmarminge force-pushed the codex/connection-state-audit branch from 04d6d46 to 582b006 Compare June 10, 2026 23:28

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Spurious relay cleanup on load
    • Added previousObservedAccount !== undefined guard to the signed-out branch to match the account-switch branch, preventing cleanup when no user was ever observed as signed in.

Create PR

Or push these changes by commenting:

@cursor push 0d6918a8b3
Preview (0d6918a8b3)
diff --git a/apps/mobile/src/features/cloud/CloudAuthProvider.tsx b/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
--- a/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
+++ b/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
@@ -72,7 +72,7 @@
       previousTokenProviderRef.current = null;
       setAgentAwarenessRelayTokenProvider(null);
       setManagedRelaySession(appAtomRegistry, null);
-      if (previousObservedAccount !== null) {
+      if (previousObservedAccount !== undefined && previousObservedAccount !== null) {
         void queueAccountCleanup(previous);
       }
       return;

You can send follow-ups to the cloud agent here.

Comment thread apps/mobile/src/features/cloud/CloudAuthProvider.tsx
Comment thread apps/web/src/state/sourceControlActions.ts
Comment thread apps/mobile/src/state/thread-outbox.ts
juliusmarminge and others added 5 commits June 11, 2026 01:28
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
…t and tracing

- Updated the EnvironmentSupervisor to handle connection states more effectively, including new states for connecting and available.
- Introduced tracing for connection attempts to capture detailed error information and improve debugging.
- Removed the environment runtime state management code as it was deemed unnecessary.
- Adjusted tests to reflect changes in connection state handling and ensure proper functionality.
- Enhanced error handling in relay tracing to ensure safe error propagation without affecting application behavior.
- Refactored EnvironmentRegistry to utilize new EnvironmentServices and EnvironmentServicesFactory.
- Replaced runtime-related methods with service-based methods for better abstraction.
- Introduced new layers for environment services, including commands and threads.
- Removed deprecated rpcGenerationChanges and other unused methods from EnvironmentRegistryService.
- Updated tests to reflect changes in the registry and runtime structure.
- Cleaned up unused imports and adjusted types accordingly.
…ment

- Deleted filesystemBrowseState and sourceControlDiscoveryState modules along with their associated tests.
- Removed related imports from index.ts and knownEnvironment.ts.
- Updated knownEnvironment tests to remove unused HTTP base URL checks.
- Refactored shellSnapshotState and threadDetailState to simplify interfaces.
- Cleaned up vcsRefState and vcsStatusState by removing unused types and functions.
@juliusmarminge juliusmarminge force-pushed the codex/hosted-web-tracing-poc branch from f37abe6 to fe78bce Compare June 11, 2026 08:28
juliusmarminge and others added 11 commits June 11, 2026 01:29
Split connection, authorization, RPC, state, and platform concerns into composable modules. Migrate web and mobile consumers to narrow subpath exports and remove legacy stores, barrels, and compatibility directories.

Co-authored-by: codex <codex@users.noreply.github.com>
- Implemented `useEnvironmentQuery` for handling environment data fetching and error management.
- Created state management for relay, review, server, session, shell, source control, terminal, and threads.
- Introduced hooks for managing terminal sessions and actions, including attaching, writing, resizing, and clearing terminals.
- Added support for source control actions such as pulling, publishing repositories, and managing threads.
- Enhanced VCS integration with actions for listing refs and managing worktrees.
- keep subscriptions alive across transport and supervisor replacement
- bound probes and improve retry, authorization, and state projections
- add deterministic regression coverage for connection lifecycle failures

Co-authored-by: codex <codex@users.noreply.github.com>
- self-heal persisted connection catalogs and clean stale temporary data
- preserve bearer authorization across edits and attachment requests
- centralize mobile runtime reuse and background outbox draining

Co-authored-by: codex <codex@users.noreply.github.com>
- project server snapshots and request latency into focused atoms
- surface slow RPC requests without coupling shared runtime to React
- reuse the web runtime and make hosted connection discovery resilient

Co-authored-by: codex <codex@users.noreply.github.com>
- recover desktop connection catalogs from partial writes
- remove stale temporary catalog files after recovery
- trust incoming relay trace context only after authentication

Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Comment thread apps/mobile/src/lib/nativeMarkdownText.ts
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge juliusmarminge force-pushed the codex/connection-state-audit branch from a71ceef to 25717b6 Compare June 11, 2026 08:38
- Add UIKit-backed iOS composer module for atomic skill and file tokens
- Unify mobile chrome colors and shared composer token parsing
- Update web mention handling and add token layout tests

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stale tokens break selection mapping
    • Extracted a currentValidTokens() helper that filters tokens against the current value string, and used it in displayOffset(forSourceOffset:) so stale tokens no longer participate in offset calculations.

Create PR

Or push these changes by commenting:

@cursor push b5abc57d3c
Preview (b5abc57d3c)
diff --git a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift
--- a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift
+++ b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift
@@ -464,12 +464,7 @@
     let result = NSMutableAttributedString()
     let source = value as NSString
     var cursor = 0
-    let validTokens = tokens.filter {
-      $0.start >= cursor &&
-        $0.end > $0.start &&
-        $0.end <= source.length &&
-        source.substring(with: NSRange(location: $0.start, length: $0.end - $0.start)) == $0.source
-    }
+    let validTokens = currentValidTokens()
 
     for token in validTokens {
       if token.start < cursor {
@@ -678,11 +673,12 @@
 
   private func displayOffset(forSourceOffset sourceOffset: Int) -> Int {
     let boundedOffset = max(0, min((value as NSString).length, sourceOffset))
+    let active = currentValidTokens()
     var collapsedLength = 0
-    for token in tokens where token.end <= boundedOffset {
+    for token in active where token.end <= boundedOffset {
       collapsedLength += max(0, token.end - token.start - 1)
     }
-    if let token = tokens.first(where: { $0.start < boundedOffset && boundedOffset < $0.end }) {
+    if let token = active.first(where: { $0.start < boundedOffset && boundedOffset < $0.end }) {
       return token.start - collapsedLength + 1
     }
     return boundedOffset - collapsedLength
@@ -723,9 +719,9 @@
     return try? JSONDecoder().decode(type, from: data)
   }
 
-  private func tokensMatchCurrentValue() -> Bool {
+  private func currentValidTokens() -> [ComposerTokenPayload] {
     let source = value as NSString
-    return tokens.allSatisfy {
+    return tokens.filter {
       $0.start >= 0 &&
         $0.end > $0.start &&
         $0.end <= source.length &&
@@ -733,19 +729,12 @@
     }
   }
 
+  private func tokensMatchCurrentValue() -> Bool {
+    currentValidTokens().count == tokens.count
+  }
+
   private func documentMatchesExpectedTokens() -> Bool {
-    let source = value as NSString
-    let expectedSources = tokens.compactMap { token -> String? in
-      guard token.start >= 0,
-            token.end > token.start,
-            token.end <= source.length,
-            source.substring(
-              with: NSRange(location: token.start, length: token.end - token.start)
-            ) == token.source else {
-        return nil
-      }
-      return token.source
-    }
+    let expectedSources = currentValidTokens().map(\.source)
     var renderedSources: [String] = []
     textView.attributedText.enumerateAttribute(
       .attachment,

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit c99480f. Configure here.

return token.start - collapsedLength + 1
}
return boundedOffset - collapsedLength
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale tokens break selection mapping

Medium Severity

displayOffset(forSourceOffset:) adjusts the caret using every entry in tokens, but makeAttributedDocument() only renders tokens whose start/end/source still match value. After edits or while React props lag the native text, stale token ranges can remain in tokens and still participate in offset math (including the “cursor inside token” branch), so controlled selection and rebuilds can place the caret at the wrong display index.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c99480f. Configure here.

Comment on lines +166 to +168
...(context.headingLevel ? { headingLevel: context.headingLevel } : {}),
...(context.depth ? { depth: context.depth } : {}),
...(context.spacing ? { spacing: context.spacing } : {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low lib/nativeMarkdownText.ts:166

Lines 166-168 use truthy checks for headingLevel, depth, and spacing, which silently drops these properties when their value is 0. Since appendDocumentBlock passes depth: 0 at the document level (line 592), a depth of 0 becomes indistinguishable from undefined, breaking downstream logic that relies on explicit zero values. Lines 169-175 already use !== undefined checks for other numeric properties; consider applying the same pattern to lines 166-168.

-    ...(context.headingLevel ? { headingLevel: context.headingLevel } : {}),
-    ...(context.depth ? { depth: context.depth } : {}),
-    ...(context.spacing ? { spacing: context.spacing } : {}),
+    ...(context.headingLevel !== undefined
+      ? { headingLevel: context.headingLevel }
+      : {}),
+    ...(context.depth !== undefined ? { depth: context.depth } : {}),
+    ...(context.spacing !== undefined ? { spacing: context.spacing } : {}),
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/lib/nativeMarkdownText.ts around lines 166-168:

Lines 166-168 use truthy checks for `headingLevel`, `depth`, and `spacing`, which silently drops these properties when their value is `0`. Since `appendDocumentBlock` passes `depth: 0` at the document level (line 592), a `depth` of `0` becomes indistinguishable from `undefined`, breaking downstream logic that relies on explicit zero values. Lines 169-175 already use `!== undefined` checks for other numeric properties; consider applying the same pattern to lines 166-168.

Evidence trail:
apps/mobile/src/lib/nativeMarkdownText.ts lines 166-175 (truthy check vs !== undefined check pattern), line 564 (depth = 0 default), line 592 (depth passed into RunContext), line 128 (sameRunStyle compares .depth), apps/mobile/src/native/NativeMarkdownSelectableText.ios.tsx line 22 (run.depth used in key join)

- Adjust composer spacing and chrome measurements
- Tighten the patched UITextView chip rendering metrics
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant