Skip to content

T3 Code Mobile [WIP]#2013

Open
juliusmarminge wants to merge 5 commits intomainfrom
t3code/mobile-remote-connect
Open

T3 Code Mobile [WIP]#2013
juliusmarminge wants to merge 5 commits intomainfrom
t3code/mobile-remote-connect

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Apr 14, 2026

⚠️ WARNING :: VERY EARLY

Summary

  • Add a new Expo-based mobile client with remote connection setup, thread browsing, new-thread flows, composer UI, and git action sheets.
  • Move shared remote/runtime, git, thread-detail, and WebSocket state into packages/client-runtime and packages/shared so web and mobile can share the same behavior.
  • Refactor desktop startup and readiness handling to rely on HTTP session readiness, simplify window bootstrap, and remove the old listening-detector path.
  • Rework web connection, composer, sidebar, and git action flows to use the new shared runtime and state management.

Testing

  • Not run (PR content only).

Note

High Risk
Large, cross-cutting addition of a new mobile app with native Expo dependencies and networking/connection persistence; changes include transport/security-adjacent config (e.g., Android cleartext traffic) and extensive new UI/state flows.

Overview
Adds a new apps/mobile Expo app (Expo Router + Uniwind) with themed UI, dev/preview/prod variants (app.config.ts, eas.json), and updated lint/format ignore rules for generated mobile types.

Implements core mobile flows: backend pairing via manual URL or QR scan (persisting connections and reconnect/remove actions), a home screen that groups threads by project and shows live git state, a new-task flow with model/runtime/workspace options and image attachments, and a thread detail experience with an expanded composer (slash/skill/path triggers, attachments preview, provider/model menus) plus haptics/progress overlays.

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

Note

Add T3 Code mobile app with WebSocket RPC transport, review diffs, and git action state

  • Introduces a full React Native / Expo mobile app (apps/mobile) with connection management, home screen, thread feed, git controls, and a diff review sheet with Shiki syntax highlighting and word-level diff highlighting.
  • Adds WsTransport and createWsRpcProtocolLayer to packages/client-runtime for WebSocket RPC connectivity with exponential-backoff reconnection, lifecycle hooks, and resilient subscription retry.
  • Extracts and shares previously web-only logic into packages/client-runtime (environmentConnection, gitActionState, gitBranchState, gitStatusState, shellSnapshotState, threadDetailState) and into packages/shared (composerTrigger, remote, orchestrationTiming) so it can be consumed by both the web app and mobile.
  • Adds a gitGetReviewDiffs RPC endpoint (contracts, server GitCore, and web/mobile clients) that returns structured dirty-worktree and base-branch diff sections with output size limits.
  • Updates the web app to delegate reconnect backoff, transport error classification, pairing URL helpers, and composer trigger detection to the shared packages instead of local implementations.
  • Risk: several web modules now depend on shared package implementations; behavioral changes include resolveRemoteWebSocketConnectionUrl now returning a /ws-suffixed path and reconnect timing governed by DEFAULT_RECONNECT_BACKOFF.

Macroscope summarized 7610d8c.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

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: 750cbdd2-f6cc-45ab-9927-010778b22b6f

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 t3code/mobile-remote-connect

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

@github-actions github-actions bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Apr 14, 2026
}): string | null {
if (!args.hasUpdateFeedConfig) {
return "Automatic updates are not available because no update feed is configured.";
}
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.

Update feed check shadows more specific dev-build message

Medium Severity

The hasUpdateFeedConfig check is evaluated before isDevelopment || !isPackaged, so development builds without a dev-app-update.yml file (the common case) now receive "no update feed is configured" instead of the more informative "only available in packaged production builds" message. The more specific condition (dev/unpackaged) is masked by the more general one (no feed config). The tests adapted by always passing hasUpdateFeedConfig: true for dev scenarios, hiding this real-world behavior change.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fbcf04a. Configure here.

Comment thread apps/mobile/src/features/connection/NewConnectionRouteScreen.tsx
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

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.

There are 3 total unresolved issues (including 2 from previous reviews).

Autofix Details

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

  • ✅ Fixed: Fast mode option applied to all providers indiscriminately
    • Added provider guards to the fast-mode handler in ThreadComposer so fastMode is only applied to claudeAgent and codex providers, matching the existing logic in NewTaskDraftScreen.

Create PR

Or push these changes by commenting:

@cursor push fb5f02f846
Preview (fb5f02f846)
diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx
--- a/apps/mobile/src/features/threads/ThreadComposer.tsx
+++ b/apps/mobile/src/features/threads/ThreadComposer.tsx
@@ -596,10 +596,15 @@
     }
     if (event.startsWith("options:fast-mode:")) {
       const fastMode = event.endsWith(":on");
-      const updated: ModelSelection = {
-        ...currentModelSelection,
-        options: { ...currentModelSelection.options, fastMode: fastMode || undefined },
-      };
+      const updated: ModelSelection =
+        currentModelSelection.provider === "claudeAgent"
+          ? {
+              ...currentModelSelection,
+              options: { ...currentModelSelection.options, fastMode: fastMode || undefined },
+            }
+          : currentModelSelection.provider === "codex"
+            ? { ...currentModelSelection, options: { fastMode: fastMode || undefined } }
+            : currentModelSelection;
       void props.onUpdateModelSelection(updated);
       return;
     }

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

};
void props.onUpdateModelSelection(updated);
return;
}
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.

Fast mode option applied to all providers indiscriminately

Medium Severity

The handleOptionsMenuAction for fast mode spreads options onto every provider's ModelSelection without checking the provider type, unlike the effort and context-window handlers which correctly guard with currentModelSelection.provider === "claudeAgent". This can attach unsupported fastMode options to providers that don't recognize them. The new-task draft screen correctly limits fast mode to claudeAgent and codex only.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a77ea8f. Configure here.

Comment thread packages/client-runtime/src/environmentConnection.ts
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Apr 14, 2026

Approvability

Verdict: Needs human review

9 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

Comment thread packages/client-runtime/src/wsRpcProtocol.ts
Comment on lines +25 to +27
const [status, setStatus] = useState<"loading" | "loaded" | "error">(() =>
faviconUrl && loadedFaviconUrls.has(faviconUrl) ? "loaded" : "loading",
);
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/ProjectFavicon.tsx:25

When faviconUrl changes due to prop updates (e.g., httpBaseUrl or workspaceRoot), status remains at its previous value because useState only initializes on mount. If the previous URL had finished loading (status === "loaded"), the new image renders immediately without showing the folder fallback during its load.

  const [status, setStatus] = useState<"loading" | "loaded" | "error">(() =>
    faviconUrl && loadedFaviconUrls.has(faviconUrl) ? "loaded" : "loading",
  );
+  useEffect(() => {
+    setStatus(
+      faviconUrl && loadedFaviconUrls.has(faviconUrl) ? "loaded" : "loading",
+    );
+  }, [faviconUrl]);
Also found in 1 other location(s)

apps/desktop/src/main.ts:1675

The titleBarOverlay.symbolColor is set once at window creation time based on nativeTheme.shouldUseDarkColors, but is never updated when the theme changes. When SET_THEME_CHANNEL handler sets nativeTheme.themeSource, the titlebar symbol color on Windows/Linux will remain stale, potentially making the minimize/maximize/close buttons hard to see or invisible against the new theme background.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/mobile/src/components/ProjectFavicon.tsx around lines 25-27:

When `faviconUrl` changes due to prop updates (e.g., `httpBaseUrl` or `workspaceRoot`), `status` remains at its previous value because `useState` only initializes on mount. If the previous URL had finished loading (`status === "loaded"`), the new image renders immediately without showing the folder fallback during its load.

Evidence trail:
apps/mobile/src/components/ProjectFavicon.tsx at REVIEWED_COMMIT:
- Lines 19-22: faviconUrl derived from props
- Lines 24-26: useState initializer only runs on mount
- No useEffect to reset status when faviconUrl changes
- Line 28: showImage = faviconUrl && status === "loaded"
- Lines 42-43: folder fallback only shows when !showImage

Also found in 1 other location(s):
- apps/desktop/src/main.ts:1675 -- The `titleBarOverlay.symbolColor` is set once at window creation time based on `nativeTheme.shouldUseDarkColors`, but is never updated when the theme changes. When `SET_THEME_CHANNEL` handler sets `nativeTheme.themeSource`, the titlebar symbol color on Windows/Linux will remain stale, potentially making the minimize/maximize/close buttons hard to see or invisible against the new theme background.

Comment thread apps/mobile/src/features/threads/git/gitSheetComponents.tsx
@cursor
Copy link
Copy Markdown
Contributor

cursor bot commented Apr 14, 2026

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Update feed check shadows more specific dev-build message
    • Swapped the isDevelopment/!isPackaged guard before the hasUpdateFeedConfig check so dev builds receive the specific 'packaged production builds' message, and updated the test to use hasUpdateFeedConfig: false reflecting real-world dev conditions.
  • ✅ Fixed: NewConnectionRouteScreen is unused duplicate of route screen
    • Deleted the unused NewConnectionRouteScreen.tsx which was a dead-code duplicate of the actual route screen at app/connections/new.tsx, confirmed by grep showing zero imports.

Create PR

Or push these changes by commenting:

@cursor push 07aefa635b
Preview (07aefa635b)
diff --git a/apps/desktop/src/updateState.test.ts b/apps/desktop/src/updateState.test.ts
--- a/apps/desktop/src/updateState.test.ts
+++ b/apps/desktop/src/updateState.test.ts
@@ -71,7 +71,7 @@
         platform: "darwin",
         appImage: undefined,
         disabledByEnv: false,
-        hasUpdateFeedConfig: true,
+        hasUpdateFeedConfig: false,
       }),
     ).toContain("packaged production builds");
   });

diff --git a/apps/desktop/src/updateState.ts b/apps/desktop/src/updateState.ts
--- a/apps/desktop/src/updateState.ts
+++ b/apps/desktop/src/updateState.ts
@@ -36,12 +36,12 @@
   disabledByEnv: boolean;
   hasUpdateFeedConfig: boolean;
 }): string | null {
+  if (args.isDevelopment || !args.isPackaged) {
+    return "Automatic updates are only available in packaged production builds.";
+  }
   if (!args.hasUpdateFeedConfig) {
     return "Automatic updates are not available because no update feed is configured.";
   }
-  if (args.isDevelopment || !args.isPackaged) {
-    return "Automatic updates are only available in packaged production builds.";
-  }
   if (args.disabledByEnv) {
     return "Automatic updates are disabled by the T3CODE_DISABLE_AUTO_UPDATE setting.";
   }

diff --git a/apps/mobile/src/features/connection/NewConnectionRouteScreen.tsx b/apps/mobile/src/features/connection/NewConnectionRouteScreen.tsx
deleted file mode 100644
--- a/apps/mobile/src/features/connection/NewConnectionRouteScreen.tsx
+++ /dev/null
@@ -1,253 +1,0 @@
-import { CameraView, useCameraPermissions } from "expo-camera";
-import { Stack, useLocalSearchParams, useRouter } from "expo-router";
-import { SymbolView } from "expo-symbols";
-import { useCallback, useEffect, useState } from "react";
-import { Alert, Pressable, ScrollView, View } from "react-native";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { useThemeColor } from "../../lib/useThemeColor";
-
-import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
-import { ErrorBanner } from "../../components/ErrorBanner";
-import { dismissRoute } from "../../lib/routes";
-import { ConnectionSheetButton } from "./ConnectionSheetButton";
-import { extractPairingUrlFromQrPayload } from "./pairing";
-import { useRemoteConnections } from "../../state/use-remote-environment-registry";
-import { buildPairingUrl, parsePairingUrl } from "./pairing";
-
-export function NewConnectionRouteScreen() {
-  const {
-    connectionError,
-    connectionPairingUrl,
-    connectionState,
-    onChangeConnectionPairingUrl,
-    onConnectPress,
-  } = useRemoteConnections();
-  const router = useRouter();
-  const params = useLocalSearchParams<{ mode?: string }>();
-  const insets = useSafeAreaInsets();
-  const [hostInput, setHostInput] = useState("");
-  const [codeInput, setCodeInput] = useState("");
-  const [isSubmitting, setIsSubmitting] = useState(false);
-  const [showScanner, setShowScanner] = useState(params.mode === "scan_qr");
-  const [cameraPermission, requestCameraPermission] = useCameraPermissions();
-  const [scannerLocked, setScannerLocked] = useState(false);
-
-  const textColor = useThemeColor("--color-icon");
-  const placeholderColor = useThemeColor("--color-placeholder");
-
-  const connectDisabled =
-    isSubmitting || connectionState === "connecting" || hostInput.trim().length === 0;
-
-  useEffect(() => {
-    const { host, code } = parsePairingUrl(connectionPairingUrl);
-    setHostInput(host);
-    setCodeInput(code);
-  }, [connectionPairingUrl]);
-
-  useEffect(() => {
-    if (connectionError) {
-      setIsSubmitting(false);
-    }
-  }, [connectionError]);
-
-  const handleHostChange = useCallback((value: string) => {
-    setHostInput(value);
-  }, []);
-
-  const handleCodeChange = useCallback((value: string) => {
-    setCodeInput(value);
-  }, []);
-
-  const openScanner = useCallback(async () => {
-    if (cameraPermission?.granted) {
-      setScannerLocked(false);
-      setShowScanner(true);
-      return;
-    }
-
-    const permission = await requestCameraPermission();
-    if (permission.granted) {
-      setScannerLocked(false);
-      setShowScanner(true);
-      return;
-    }
-
-    Alert.alert("Camera access needed", "Allow camera access to scan a backend pairing QR code.");
-  }, [cameraPermission?.granted, requestCameraPermission]);
-
-  const closeScanner = useCallback(() => {
-    setShowScanner(false);
-    setScannerLocked(false);
-  }, []);
-
-  const handleQrScan = useCallback(
-    ({ data }: { readonly data: string }) => {
-      if (scannerLocked) {
-        return;
-      }
-
-      setScannerLocked(true);
-
-      try {
-        const pairingUrl = extractPairingUrlFromQrPayload(data);
-        const { host, code } = parsePairingUrl(pairingUrl);
-        setHostInput(host);
-        setCodeInput(code);
-        onChangeConnectionPairingUrl(pairingUrl);
-        setShowScanner(false);
-      } catch (error) {
-        Alert.alert(
-          "Invalid QR code",
-          error instanceof Error ? error.message : "Scanned QR code was not recognized.",
-        );
-      } finally {
-        setTimeout(() => {
-          setScannerLocked(false);
-        }, 600);
-      }
-    },
-    [onChangeConnectionPairingUrl, scannerLocked],
-  );
-
-  const handleSubmit = useCallback(async () => {
-    setIsSubmitting(true);
-
-    try {
-      const pairingUrl = buildPairingUrl(hostInput, codeInput);
-      onChangeConnectionPairingUrl(pairingUrl);
-      await onConnectPress(pairingUrl);
-      dismissRoute(router);
-    } catch {
-      setIsSubmitting(false);
-    }
-  }, [codeInput, hostInput, onChangeConnectionPairingUrl, onConnectPress, router]);
-
-  return (
-    <View collapsable={false} className="flex-1 bg-sheet">
-      <Stack.Screen
-        options={{
-          title: showScanner ? "Scan QR Code" : "Add Backend",
-          headerRight: () => (
-            <Pressable
-              className="h-10 w-10 items-center justify-center rounded-full border border-border bg-secondary"
-              onPress={() => {
-                if (showScanner) {
-                  closeScanner();
-                } else {
-                  void openScanner();
-                }
-              }}
-            >
-              <SymbolView
-                name={showScanner ? "xmark" : "qrcode.viewfinder"}
-                size={showScanner ? 14 : 18}
-                tintColor={textColor}
-                type="monochrome"
-                weight="semibold"
-              />
-            </Pressable>
-          ),
-        }}
-      />
-
-      <ScrollView
-        contentInsetAdjustmentBehavior="automatic"
-        showsVerticalScrollIndicator={false}
-        style={{ flex: 1 }}
-        contentContainerStyle={{
-          paddingHorizontal: 20,
-          paddingTop: 16,
-          paddingBottom: Math.max(insets.bottom, 18) + 18,
-        }}
-      >
-        <View collapsable={false} className="gap-5">
-          {showScanner ? (
-            cameraPermission?.granted ? (
-              <View
-                className="overflow-hidden rounded-[24px]"
-                style={{ borderCurve: "continuous" }}
-              >
-                <CameraView
-                  barcodeScannerSettings={{ barcodeTypes: ["qr"] }}
-                  onBarcodeScanned={handleQrScan}
-                  style={{ aspectRatio: 1, width: "100%" }}
-                />
-              </View>
-            ) : (
-              <View
-                className="items-center gap-3 rounded-[24px] bg-card px-5 py-8"
-                style={{ borderCurve: "continuous" }}
-              >
-                <Text className="text-center text-[14px] leading-[20px] text-foreground-muted">
-                  Camera permission is required to scan a QR code.
-                </Text>
-                <ConnectionSheetButton
-                  compact
-                  icon="camera"
-                  label="Allow camera"
-                  tone="secondary"
-                  onPress={() => {
-                    void openScanner();
-                  }}
-                />
-              </View>
-            )
-          ) : (
-            <View collapsable={false} className="gap-4 rounded-[24px] bg-card p-4">
-              <View collapsable={false} className="gap-1.5">
-                <Text
-                  className="text-[11px] font-t3-bold uppercase text-foreground-muted"
-                  style={{ letterSpacing: 0.8 }}
-                >
-                  Host
-                </Text>
-                <TextInput
-                  autoCapitalize="none"
-                  autoCorrect={false}
-                  keyboardType="url"
-                  placeholder="192.168.1.100:8080"
-                  placeholderTextColor={placeholderColor}
-                  value={hostInput}
-                  onChangeText={handleHostChange}
-                  className="rounded-[14px] border border-input-border bg-input px-4 py-3.5 text-[15px] text-foreground"
-                />
-              </View>
-
-              <View collapsable={false} className="gap-1.5">
-                <Text
-                  className="text-[11px] font-t3-bold uppercase text-foreground-muted"
-                  style={{ letterSpacing: 0.8 }}
-                >
-                  Pairing code
-                </Text>
-                <TextInput
-                  autoCapitalize="none"
-                  autoCorrect={false}
-                  placeholder="abc-123-xyz"
-                  placeholderTextColor={placeholderColor}
-                  value={codeInput}
-                  onChangeText={handleCodeChange}
-                  className="rounded-[14px] border border-input-border bg-input px-4 py-3.5 text-[15px] text-foreground"
-                />
-              </View>
-
-              {connectionError ? <ErrorBanner message={connectionError} /> : null}
-
-              <ConnectionSheetButton
-                icon="plus"
-                label={
-                  isSubmitting || connectionState === "connecting" ? "Pairing..." : "Add backend"
-                }
-                disabled={connectDisabled}
-                tone="primary"
-                onPress={() => {
-                  void handleSubmit();
-                }}
-              />
-            </View>
-          )}
-        </View>
-      </ScrollView>
-    </View>
-  );
-}
\ No newline at end of file

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

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

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 2 potential issues.

There are 5 total unresolved issues (including 3 from previous reviews).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: CARD_SHADOW exports are defined but never imported
    • Removed the unused CARD_SHADOW and CARD_SHADOW_DARK constants and their export statement from ConnectionSheetButton.tsx, as they were never imported anywhere in the codebase.
  • ✅ Fixed: Debug console.log statements left in production code
    • Removed all three console.log statements with the [new task flow] prefix from new-task-flow-provider.tsx (in reset callback, environment init effect, and the dedicated state-logging effect).

Create PR

Or push these changes by commenting:

@cursor push 12dd1bcb97
Preview (12dd1bcb97)
diff --git a/apps/mobile/src/features/connection/ConnectionSheetButton.tsx b/apps/mobile/src/features/connection/ConnectionSheetButton.tsx
--- a/apps/mobile/src/features/connection/ConnectionSheetButton.tsx
+++ b/apps/mobile/src/features/connection/ConnectionSheetButton.tsx
@@ -5,28 +5,6 @@
 import { AppText as Text } from "../../components/AppText";
 import { cn } from "../../lib/cn";
 
-const CARD_SHADOW = Platform.select({
-  ios: {
-    shadowColor: "rgba(23,23,23,0.08)",
-    shadowOffset: { width: 0, height: 4 },
-    shadowOpacity: 1,
-    shadowRadius: 16,
-  },
-  android: { elevation: 3 },
-});
-
-const CARD_SHADOW_DARK = Platform.select({
-  ios: {
-    shadowColor: "#000",
-    shadowOffset: { width: 0, height: 2 },
-    shadowOpacity: 0.18,
-    shadowRadius: 8,
-  },
-  android: { elevation: 4 },
-});
-
-export { CARD_SHADOW, CARD_SHADOW_DARK };
-
 export function ConnectionSheetButton(props: {
   readonly icon: React.ComponentProps<typeof SymbolView>["name"];
   readonly label: string;

diff --git a/apps/mobile/src/features/threads/new-task-flow-provider.tsx b/apps/mobile/src/features/threads/new-task-flow-provider.tsx
--- a/apps/mobile/src/features/threads/new-task-flow-provider.tsx
+++ b/apps/mobile/src/features/threads/new-task-flow-provider.tsx
@@ -189,10 +189,6 @@
   }, []);
 
   const reset = useCallback(() => {
-    console.log("[new task flow] reset", {
-      defaultEnvironmentId: projects[0]?.environmentId ?? null,
-      projectCount: projects.length,
-    });
     setSelectedEnvironmentId(projects[0]?.environmentId ?? "");
     setSelectedProjectKey(null);
     setSelectedModelKey(null);
@@ -216,9 +212,6 @@
       return;
     }
 
-    console.log("[new task flow] initializing environment", {
-      environmentId: projects[0]!.environmentId,
-    });
     setSelectedEnvironmentId(projects[0]!.environmentId);
   }, [projects, selectedEnvironmentId]);
 
@@ -470,24 +463,6 @@
     ],
   );
 
-  useEffect(() => {
-    console.log("[new task flow] state", {
-      availableBranchCount: availableBranches.length,
-      environmentCount: environments.length,
-      logicalProjectCount: logicalProjects.length,
-      selectedEnvironmentId,
-      selectedProjectKey,
-      selectedProjectTitle: selectedProject?.title ?? null,
-    });
-  }, [
-    availableBranches.length,
-    environments.length,
-    logicalProjects.length,
-    selectedEnvironmentId,
-    selectedProject?.title,
-    selectedProjectKey,
-  ]);
-
   return <NewTaskFlowContext.Provider value={value}>{props.children}</NewTaskFlowContext.Provider>;
 }

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

android: { elevation: 4 },
});

export { CARD_SHADOW, CARD_SHADOW_DARK };
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.

CARD_SHADOW exports are defined but never imported

Low Severity

CARD_SHADOW and CARD_SHADOW_DARK are defined and explicitly exported but never imported anywhere in the codebase. These are unused dead exports that add clutter without providing value.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3d3e847. Configure here.

defaultEnvironmentId: projects[0]?.environmentId ?? null,
projectCount: projects.length,
});
setSelectedEnvironmentId(projects[0]?.environmentId ?? "");
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.

Debug console.log statements left in production code

Medium Severity

Multiple console.log calls with the [new task flow] prefix are left in new-task-flow-provider.tsx. These log internal state like environmentId, selectedProjectKey, branch counts, and project titles on every state change — verbose debug output that shouldn't ship in a production mobile app.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3d3e847. Configure here.

@juliusmarminge juliusmarminge changed the title Add mobile remote client and reconnect handling T3 Code Mobile [WIP] Apr 14, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

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.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

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

  • ✅ Fixed: QR scanner lock uses state instead of ref
    • Replaced useState with useRef for the scanner lock so the guard value is read synchronously, preventing duplicate scan processing from rapid native barcode callbacks before React re-renders.

Create PR

Or push these changes by commenting:

@cursor push 9d24bfb15f
Preview (9d24bfb15f)
diff --git a/apps/mobile/src/app/connections/new.tsx b/apps/mobile/src/app/connections/new.tsx
--- a/apps/mobile/src/app/connections/new.tsx
+++ b/apps/mobile/src/app/connections/new.tsx
@@ -1,7 +1,7 @@
 import { CameraView, useCameraPermissions } from "expo-camera";
 import { Stack, useLocalSearchParams, useRouter } from "expo-router";
 import { SymbolView } from "expo-symbols";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
 import { Alert, Pressable, ScrollView, View } from "react-native";
 import { useSafeAreaInsets } from "react-native-safe-area-context";
 import { useThemeColor } from "../../lib/useThemeColor";
@@ -30,7 +30,7 @@
   const [isSubmitting, setIsSubmitting] = useState(false);
   const [showScanner, setShowScanner] = useState(params.mode === "scan_qr");
   const [cameraPermission, requestCameraPermission] = useCameraPermissions();
-  const [scannerLocked, setScannerLocked] = useState(false);
+  const scannerLockedRef = useRef(false);
 
   const textColor = useThemeColor("--color-icon");
   const placeholderColor = useThemeColor("--color-placeholder");
@@ -60,14 +60,14 @@
 
   const openScanner = useCallback(async () => {
     if (cameraPermission?.granted) {
-      setScannerLocked(false);
+      scannerLockedRef.current = false;
       setShowScanner(true);
       return;
     }
 
     const permission = await requestCameraPermission();
     if (permission.granted) {
-      setScannerLocked(false);
+      scannerLockedRef.current = false;
       setShowScanner(true);
       return;
     }
@@ -77,16 +77,16 @@
 
   const closeScanner = useCallback(() => {
     setShowScanner(false);
-    setScannerLocked(false);
+    scannerLockedRef.current = false;
   }, []);
 
   const handleQrScan = useCallback(
     ({ data }: { readonly data: string }) => {
-      if (scannerLocked) {
+      if (scannerLockedRef.current) {
         return;
       }
 
-      setScannerLocked(true);
+      scannerLockedRef.current = true;
 
       try {
         const pairingUrl = extractPairingUrlFromQrPayload(data);
@@ -102,11 +102,11 @@
         );
       } finally {
         setTimeout(() => {
-          setScannerLocked(false);
+          scannerLockedRef.current = false;
         }, 600);
       }
     },
-    [onChangeConnectionPairingUrl, scannerLocked],
+    [onChangeConnectionPairingUrl],
   );
 
   const handleSubmit = useCallback(async () => {

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

}
},
[onChangeConnectionPairingUrl, scannerLocked],
);
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.

QR scanner lock uses state instead of ref

Low Severity

handleQrScan reads scannerLocked from a stale closure since it's a useState value in a useCallback dependency array. Native barcode scanner callbacks can fire multiple times before React re-renders with the updated callback. A useRef would reliably prevent duplicate scan processing, since the ref value is always current across all closure instances.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit efc36b4. Configure here.

truncateOutputAtMaxBytes: true,
},
);
const untrackedPaths = splitNullSeparatedPaths(untrackedOutput, false);
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.

🟠 High Layers/GitCore.ts:1643

readUntrackedReviewDiffs hardcodes false for the truncated parameter when calling splitNullSeparatedPaths, even though runGitStdoutWithOptions may truncate the output. When truncation occurs, the OUTPUT_TRUNCATED_MARKER text corrupts the final path, which then gets passed to git diff --no-index as an invalid filename. Consider detecting the marker or using executeGit directly to pass the actual truncation state to splitNullSeparatedPaths.

-    const untrackedPaths = splitNullSeparatedPaths(untrackedOutput, false);
+    const untrackedPaths = untrackedOutput.endsWith(OUTPUT_TRUNCATED_MARKER)
+      ? splitNullSeparatedPaths(untrackedOutput.slice(0, -OUTPUT_TRUNCATED_MARKER.length), true)
+      : splitNullSeparatedPaths(untrackedOutput, false);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/git/Layers/GitCore.ts around line 1643:

`readUntrackedReviewDiffs` hardcodes `false` for the `truncated` parameter when calling `splitNullSeparatedPaths`, even though `runGitStdoutWithOptions` may truncate the output. When truncation occurs, the `OUTPUT_TRUNCATED_MARKER` text corrupts the final path, which then gets passed to `git diff --no-index` as an invalid filename. Consider detecting the marker or using `executeGit` directly to pass the actual truncation state to `splitNullSeparatedPaths`.

Evidence trail:
- apps/server/src/git/Layers/GitCore.ts lines 1634-1643 (REVIEWED_COMMIT): `runGitStdoutWithOptions` called with `truncateOutputAtMaxBytes: true`, then `splitNullSeparatedPaths(untrackedOutput, false)` hardcodes `false`
- apps/server/src/git/Layers/GitCore.ts lines 852-862 (REVIEWED_COMMIT): `runGitStdoutWithOptions` appends `OUTPUT_TRUNCATED_MARKER` when `stdoutTruncated` is true but only returns the string, not the truncation state
- apps/server/src/git/Layers/GitCore.ts line 44 (REVIEWED_COMMIT): `OUTPUT_TRUNCATED_MARKER = "\n\n[truncated]"`
- apps/server/src/git/Layers/GitCore.ts lines 131-140 (REVIEWED_COMMIT): `splitNullSeparatedPaths` removes last path when `truncated` is true, but won't with hardcoded `false`
- apps/server/src/git/Layers/GitCore.ts lines 1778, 1831 (REVIEWED_COMMIT): Correct usage pattern with `result.stdoutTruncated`

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

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.

There are 7 total unresolved issues (including 6 from previous reviews).

Autofix Details

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

  • ✅ Fixed: Worktree path condition is inverted
    • The ternary condition was indeed inverted, passing null for worktree mode and the path for local mode; flipped the condition so worktree mode correctly receives the selected worktree path.

Create PR

Or push these changes by commenting:

@cursor push 677fe73be8
Preview (677fe73be8)
diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
--- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
+++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
@@ -369,7 +369,7 @@
         modelSelection: modelWithOptions,
         envMode: flow.workspaceMode,
         branch: flow.selectedBranchName,
-        worktreePath: flow.workspaceMode === "worktree" ? null : flow.selectedWorktreePath,
+        worktreePath: flow.workspaceMode === "worktree" ? flow.selectedWorktreePath : null,
         runtimeMode: flow.runtimeMode,
         interactionMode: flow.interactionMode,
         initialMessageText: flow.prompt.trim(),

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

modelSelection: modelWithOptions,
envMode: flow.workspaceMode,
branch: flow.selectedBranchName,
worktreePath: flow.workspaceMode === "worktree" ? null : flow.selectedWorktreePath,
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.

Worktree path condition is inverted

High Severity

The worktreePath ternary is backwards: it passes null when workspaceMode is "worktree" and flow.selectedWorktreePath when it's "local". The worktree path is only meaningful when actually using worktree mode, so the condition needs to be flipped.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1705361. Configure here.

juliusmarminge added a commit that referenced this pull request Apr 14, 2026
Rebuild PR #2013 on top of current origin/main without the duplicated commit lineage.

Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge juliusmarminge force-pushed the t3code/mobile-remote-connect branch from 1705361 to c799180 Compare April 14, 2026 21:23
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

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.

There are 7 total unresolved issues (including 6 from previous reviews).

Autofix Details

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

  • ✅ Fixed: Route file duplicates entire feature component inline
    • Replaced the duplicated ~250-line inline implementation in apps/mobile/src/app/connections/new.tsx with a simple import and render of NewConnectionRouteScreen from the features/connection/ module, matching the delegation pattern used by other route files.

Create PR

Or push these changes by commenting:

@cursor push 59b1d755d9
Preview (59b1d755d9)
diff --git a/apps/mobile/src/app/connections/new.tsx b/apps/mobile/src/app/connections/new.tsx
--- a/apps/mobile/src/app/connections/new.tsx
+++ b/apps/mobile/src/app/connections/new.tsx
@@ -1,253 +1,5 @@
-import { CameraView, useCameraPermissions } from "expo-camera";
-import { Stack, useLocalSearchParams, useRouter } from "expo-router";
-import { SymbolView } from "expo-symbols";
-import { useCallback, useEffect, useState } from "react";
-import { Alert, Pressable, ScrollView, View } from "react-native";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { useThemeColor } from "../../lib/useThemeColor";
+import { NewConnectionRouteScreen } from "../../features/connection/NewConnectionRouteScreen";
 
-import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
-import { ErrorBanner } from "../../components/ErrorBanner";
-import { dismissRoute } from "../../lib/routes";
-import { ConnectionSheetButton } from "../../features/connection/ConnectionSheetButton";
-import { extractPairingUrlFromQrPayload } from "../../features/connection/pairing";
-import { useRemoteConnections } from "../../state/use-remote-environment-registry";
-import { buildPairingUrl, parsePairingUrl } from "../../features/connection/pairing";
-
-export default function ConnectionsNewRouteScreen() {
-  const {
-    connectionError,
-    connectionPairingUrl,
-    connectionState,
-    onChangeConnectionPairingUrl,
-    onConnectPress,
-  } = useRemoteConnections();
-  const router = useRouter();
-  const params = useLocalSearchParams<{ mode?: string }>();
-  const insets = useSafeAreaInsets();
-  const [hostInput, setHostInput] = useState("");
-  const [codeInput, setCodeInput] = useState("");
-  const [isSubmitting, setIsSubmitting] = useState(false);
-  const [showScanner, setShowScanner] = useState(params.mode === "scan_qr");
-  const [cameraPermission, requestCameraPermission] = useCameraPermissions();
-  const [scannerLocked, setScannerLocked] = useState(false);
-
-  const textColor = useThemeColor("--color-icon");
-  const placeholderColor = useThemeColor("--color-placeholder");
-
-  const connectDisabled =
-    isSubmitting || connectionState === "connecting" || hostInput.trim().length === 0;
-
-  useEffect(() => {
-    const { host, code } = parsePairingUrl(connectionPairingUrl);
-    setHostInput(host);
-    setCodeInput(code);
-  }, [connectionPairingUrl]);
-
-  useEffect(() => {
-    if (connectionError) {
-      setIsSubmitting(false);
-    }
-  }, [connectionError]);
-
-  const handleHostChange = useCallback((value: string) => {
-    setHostInput(value);
-  }, []);
-
-  const handleCodeChange = useCallback((value: string) => {
-    setCodeInput(value);
-  }, []);
-
-  const openScanner = useCallback(async () => {
-    if (cameraPermission?.granted) {
-      setScannerLocked(false);
-      setShowScanner(true);
-      return;
-    }
-
-    const permission = await requestCameraPermission();
-    if (permission.granted) {
-      setScannerLocked(false);
-      setShowScanner(true);
-      return;
-    }
-
-    Alert.alert("Camera access needed", "Allow camera access to scan a backend pairing QR code.");
-  }, [cameraPermission?.granted, requestCameraPermission]);
-
-  const closeScanner = useCallback(() => {
-    setShowScanner(false);
-    setScannerLocked(false);
-  }, []);
-
-  const handleQrScan = useCallback(
-    ({ data }: { readonly data: string }) => {
-      if (scannerLocked) {
-        return;
-      }
-
-      setScannerLocked(true);
-
-      try {
-        const pairingUrl = extractPairingUrlFromQrPayload(data);
-        const { host, code } = parsePairingUrl(pairingUrl);
-        setHostInput(host);
-        setCodeInput(code);
-        onChangeConnectionPairingUrl(pairingUrl);
-        setShowScanner(false);
-      } catch (error) {
-        Alert.alert(
-          "Invalid QR code",
-          error instanceof Error ? error.message : "Scanned QR code was not recognized.",
-        );
-      } finally {
-        setTimeout(() => {
-          setScannerLocked(false);
-        }, 600);
-      }
-    },
-    [onChangeConnectionPairingUrl, scannerLocked],
-  );
-
-  const handleSubmit = useCallback(async () => {
-    setIsSubmitting(true);
-
-    try {
-      const pairingUrl = buildPairingUrl(hostInput, codeInput);
-      onChangeConnectionPairingUrl(pairingUrl);
-      await onConnectPress(pairingUrl);
-      dismissRoute(router);
-    } catch {
-      setIsSubmitting(false);
-    }
-  }, [codeInput, hostInput, onChangeConnectionPairingUrl, onConnectPress, router]);
-
-  return (
-    <View collapsable={false} className="flex-1 bg-sheet">
-      <Stack.Screen
-        options={{
-          title: showScanner ? "Scan QR Code" : "Add Backend",
-          headerRight: () => (
-            <Pressable
-              className="h-10 w-10 items-center justify-center rounded-full border border-border bg-secondary"
-              onPress={() => {
-                if (showScanner) {
-                  closeScanner();
-                } else {
-                  void openScanner();
-                }
-              }}
-            >
-              <SymbolView
-                name={showScanner ? "xmark" : "qrcode.viewfinder"}
-                size={showScanner ? 14 : 18}
-                tintColor={textColor}
-                type="monochrome"
-                weight="semibold"
-              />
-            </Pressable>
-          ),
-        }}
-      />
-
-      <ScrollView
-        contentInsetAdjustmentBehavior="automatic"
-        showsVerticalScrollIndicator={false}
-        style={{ flex: 1 }}
-        contentContainerStyle={{
-          paddingHorizontal: 20,
-          paddingTop: 16,
-          paddingBottom: Math.max(insets.bottom, 18) + 18,
-        }}
-      >
-        <View collapsable={false} className="gap-5">
-          {showScanner ? (
-            cameraPermission?.granted ? (
-              <View
-                className="overflow-hidden rounded-[24px]"
-                style={{ borderCurve: "continuous" }}
-              >
-                <CameraView
-                  barcodeScannerSettings={{ barcodeTypes: ["qr"] }}
-                  onBarcodeScanned={handleQrScan}
-                  style={{ aspectRatio: 1, width: "100%" }}
-                />
-              </View>
-            ) : (
-              <View
-                className="items-center gap-3 rounded-[24px] bg-card px-5 py-8"
-                style={{ borderCurve: "continuous" }}
-              >
-                <Text className="text-center text-[14px] leading-[20px] text-foreground-muted">
-                  Camera permission is required to scan a QR code.
-                </Text>
-                <ConnectionSheetButton
-                  compact
-                  icon="camera"
-                  label="Allow camera"
-                  tone="secondary"
-                  onPress={() => {
-                    void openScanner();
-                  }}
-                />
-              </View>
-            )
-          ) : (
-            <View collapsable={false} className="gap-4 rounded-[24px] bg-card p-4">
-              <View collapsable={false} className="gap-1.5">
-                <Text
-                  className="text-[11px] font-t3-bold uppercase text-foreground-muted"
-                  style={{ letterSpacing: 0.8 }}
-                >
-                  Host
-                </Text>
-                <TextInput
-                  autoCapitalize="none"
-                  autoCorrect={false}
-                  keyboardType="url"
-                  placeholder="192.168.1.100:8080"
-                  placeholderTextColor={placeholderColor}
-                  value={hostInput}
-                  onChangeText={handleHostChange}
-                  className="rounded-[14px] border border-input-border bg-input px-4 py-3.5 text-[15px] text-foreground"
-                />
-              </View>
-
-              <View collapsable={false} className="gap-1.5">
-                <Text
-                  className="text-[11px] font-t3-bold uppercase text-foreground-muted"
-                  style={{ letterSpacing: 0.8 }}
-                >
-                  Pairing code
-                </Text>
-                <TextInput
-                  autoCapitalize="none"
-                  autoCorrect={false}
-                  placeholder="abc-123-xyz"
-                  placeholderTextColor={placeholderColor}
-                  value={codeInput}
-                  onChangeText={handleCodeChange}
-                  className="rounded-[14px] border border-input-border bg-input px-4 py-3.5 text-[15px] text-foreground"
-                />
-              </View>
-
-              {connectionError ? <ErrorBanner message={connectionError} /> : null}
-
-              <ConnectionSheetButton
-                icon="plus"
-                label={
-                  isSubmitting || connectionState === "connecting" ? "Pairing..." : "Add backend"
-                }
-                disabled={connectDisabled}
-                tone="primary"
-                onPress={() => {
-                  void handleSubmit();
-                }}
-              />
-            </View>
-          )}
-        </View>
-      </ScrollView>
-    </View>
-  );
+export default function ConnectionsNewRoute() {
+  return <NewConnectionRouteScreen />;
 }

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

</ScrollView>
</View>
);
}
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.

Route file duplicates entire feature component inline

Medium Severity

apps/mobile/src/app/connections/new.tsx is a near-identical copy of NewConnectionRouteScreen.tsx in features/connection/. The route file reimplements all QR scanning, form input, and submission logic instead of importing the existing feature component. Any future bug fix applied to one copy will be missed in the other, leading to divergent behavior.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c799180. Configure here.

Comment thread apps/mobile/src/lib/repositoryGroups.ts
Comment on lines +72 to +78
<SymbolView
name="xmark"
size={9}
tintColor="#ffffff"
type="monochrome"
weight="bold"
/>
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 components/ComposerAttachmentStrip.tsx:72

On Android, SymbolView with name="xmark" renders nothing because expo-symbols expects an object with platform-specific keys ({ android: string, web: string }), not a plain string. This causes the remove button to appear as an empty circle on Android. Pass an object with android and web keys to ensure the icon renders on both platforms.

-              <SymbolView
-                name="xmark"
-                size={9}
-                tintColor="#ffffff"
-                type="monochrome"
-                weight="bold"
-              />
Also found in 1 other location(s)

apps/mobile/src/features/threads/review/ReviewCommentComposerSheet.tsx:21

On iOS, ui-monospace is a CSS keyword that does not work as a fontFamily value in React Native. React Native requires actual iOS font family names like Menlo or Courier. Using ui-monospace will cause the text to fall back to the default proportional font, defeating the intent of displaying monospace code.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/mobile/src/components/ComposerAttachmentStrip.tsx around lines 72-78:

On Android, `SymbolView` with `name="xmark"` renders nothing because `expo-symbols` expects an object with platform-specific keys (`{ android: string, web: string }`), not a plain string. This causes the remove button to appear as an empty circle on Android. Pass an object with `android` and `web` keys to ensure the icon renders on both platforms.

Evidence trail:
1. apps/mobile/src/components/ComposerAttachmentStrip.tsx lines 72-78 - SymbolView uses `name="xmark"` (plain string)
2. https://docs.expo.dev/versions/latest/sdk/symbols/ - Official expo-symbols documentation states: "If you only pass a string, it is treated as an SF Symbol name and renders only on iOS. On Android and web, nothing will be rendered unless you provide a `fallback`" and shows the `name` prop type as `SFSymbol | { android: AndroidSymbol, ios: SFSymbol, web: AndroidSymbol }`

Also found in 1 other location(s):
- apps/mobile/src/features/threads/review/ReviewCommentComposerSheet.tsx:21 -- On iOS, `ui-monospace` is a CSS keyword that does not work as a `fontFamily` value in React Native. React Native requires actual iOS font family names like `Menlo` or `Courier`. Using `ui-monospace` will cause the text to fall back to the default proportional font, defeating the intent of displaying monospace code.

Comment thread packages/client-runtime/src/gitStatusState.ts
Comment thread packages/client-runtime/src/environmentConnection.ts
Comment thread apps/mobile/src/features/threads/use-project-actions.ts
juliusmarminge and others added 2 commits April 15, 2026 10:33
Rebuild PR #2013 on top of current origin/main without the duplicated commit lineage.

Co-authored-by: codex <codex@users.noreply.github.com>
- Add syntax-highlighted diff rendering and cached review state
- Support pasted/attached images in review comments
- Improve mobile review sheet handling for large or partial diffs

Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge juliusmarminge force-pushed the t3code/mobile-remote-connect branch from c799180 to f86d466 Compare April 15, 2026 17:37
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

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.

There are 8 total unresolved issues (including 7 from previous reviews).

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue. You can view the agent here.

Reviewed by Cursor Bugbot for commit f86d466. Configure here.

(s) => new Date(s.latestActivityAt),
Order.Date,
),
});
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.

Repository group sort uses ascending instead of descending

Medium Severity

When merging repository groups, the latestActivityAt comparison is inverted. compareIsoDateDescending returns a positive number when right (the new activity) is more recent, so the condition > 0 picks the older timestamp as the group's latest activity rather than the newer one.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f86d466. Configure here.

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.

Bugbot Autofix determined this is a false positive.

The logic correctly selects the more recent timestamp: when the new candidate is newer, compareIsoDateDescending returns positive and the ternary picks it; when existing is newer, the result is negative and the ternary keeps the existing value.

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

Comment thread apps/mobile/src/features/threads/review/shikiReviewHighlighter.ts
};
return pipe(
parsed.connections ?? [],
Arr.filter((c) => !!c.environmentId && !!c.bearerToken?.trim()),
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/storage.ts:29

When stored JSON contains malformed entries (e.g., null values in the connections array), the filter callback crashes with a TypeError on line 29 because it accesses c.environmentId before checking if c is nullish. Consider adding a null check: Arr.filter((c) => !!c && !!c.environmentId && !!c.bearerToken?.trim()).

Suggested change
Arr.filter((c) => !!c.environmentId && !!c.bearerToken?.trim()),
Arr.filter((c) => !!c && !!c.environmentId && !!c.bearerToken?.trim()),
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/mobile/src/lib/storage.ts around line 29:

When stored JSON contains malformed entries (e.g., `null` values in the `connections` array), the filter callback crashes with a `TypeError` on line 29 because it accesses `c.environmentId` before checking if `c` is nullish. Consider adding a null check: `Arr.filter((c) => !!c && !!c.environmentId && !!c.bearerToken?.trim())`.

Evidence trail:
apps/mobile/src/lib/storage.ts lines 23-31 at REVIEWED_COMMIT - the filter callback `(c) => !!c.environmentId && !!c.bearerToken?.trim()` accesses c.environmentId without first checking if c is nullish. The data comes from JSON.parse with only a type assertion (line 23-25), not runtime validation.

Crypto-Virus pushed a commit to Crypto-Virus/t3code that referenced this pull request Apr 15, 2026
Rebuild PR pingdotgg#2013 on top of current origin/main without the duplicated commit lineage.

Co-authored-by: codex <codex@users.noreply.github.com>
- Switch review screen to a virtualized list layout
- Queue diff highlighting and default to the JS engine
- Add word-diff helpers and tests
scss: () => import("@shikijs/langs/scss"),
less: () => import("@shikijs/langs/less"),
xml: () => import("@shikijs/langs/xml"),
svg: () => import("@shikijs/langs/xml"),
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 review/shikiReviewHighlighter.ts:83

When a file with an .svg extension is highlighted, resolveLanguageFromPath returns "svg" as the language identifier. However, the svg entry in languageImports loads the @shikijs/langs/xml module, which Shiki registers under the ID "xml". This causes highlighter.codeToTokensBase to request "svg", a language that was never registered, resulting in failed or fallback highlighting for SVG files.

-  svg: () => import("@shikijs/langs/xml"),
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/mobile/src/features/threads/review/shikiReviewHighlighter.ts around line 83:

When a file with an `.svg` extension is highlighted, `resolveLanguageFromPath` returns `"svg"` as the language identifier. However, the `svg` entry in `languageImports` loads the `@shikijs/langs/xml` module, which Shiki registers under the ID `"xml"`. This causes `highlighter.codeToTokensBase` to request `"svg"`, a language that was never registered, resulting in failed or fallback highlighting for SVG files.

Evidence trail:
apps/mobile/src/features/threads/review/shikiReviewHighlighter.ts lines 84 (`svg: () => import("@shikijs/langs/xml")`), 108-136 (languageAliases without 'svg'), 327-357 (loadSingleLanguage adds 'language' to loadedLanguages but Shiki registers the module's internal ID), 359-383 (resolveLanguageFromPath returns 'candidate' not the registered ID). @shikijs/langs package.json (https://github.com/shikijs/shiki/blob/main/packages/langs/package.json) shows no './svg' export. tm-grammars README (https://github.com/shikijs/textmate-grammars-themes/tree/main/packages/tm-grammars) confirms xml has no 'svg' alias.

Comment on lines +211 to +213
while ((ranges[rangeIndex]?.end ?? 0) <= tokenStart) {
rangeIndex += 1;
}
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.

🔴 Critical review/reviewWordDiffs.ts:211

In applyDiffRangesToTokens, when rangeIndex exceeds ranges.length, the while loop at line 211 becomes infinite because (ranges[rangeIndex]?.end ?? 0) evaluates to 0 when rangeIndex is out of bounds, and 0 <= tokenStart is always true for non-negative offsets. This causes an infinite loop when processing tokens that appear after the last range. Consider changing the condition to check rangeIndex < ranges.length before accessing the range.

-    while ((ranges[rangeIndex]?.end ?? 0) <= tokenStart) {
-      rangeIndex += 1;
-    }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/mobile/src/features/threads/review/reviewWordDiffs.ts around lines 211-213:

In `applyDiffRangesToTokens`, when `rangeIndex` exceeds `ranges.length`, the while loop at line 211 becomes infinite because `(ranges[rangeIndex]?.end ?? 0)` evaluates to `0` when `rangeIndex` is out of bounds, and `0 <= tokenStart` is always true for non-negative offsets. This causes an infinite loop when processing tokens that appear after the last range. Consider changing the condition to check `rangeIndex < ranges.length` before accessing the range.

Evidence trail:
apps/mobile/src/features/threads/review/reviewWordDiffs.ts lines 188-220 (REVIEWED_COMMIT): Function `applyDiffRangesToTokens` shows line 211 with the while loop `while ((ranges[rangeIndex]?.end ?? 0) <= tokenStart) { rangeIndex += 1; }`. The early return at line 193 checks `ranges.length === 0` but doesn't prevent the infinite loop when ranges exist but have all been exhausted during processing.

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