Skip to content

Add adaptive split-view layout for iPad/mobile workspace#3514

Open
juliusmarminge wants to merge 4 commits into
mainfrom
t3code/ipad-responsive-mobile-layout
Open

Add adaptive split-view layout for iPad/mobile workspace#3514
juliusmarminge wants to merge 4 commits into
mainfrom
t3code/ipad-responsive-mobile-layout

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

  • Add an adaptive workspace shell that switches between compact and split-view layouts based on available screen size.
  • Introduce a persistent thread sidebar for wider screens, with search, quick settings, and new-task actions.
  • Update thread and home routes to respect split-view behavior and hide redundant drawer/back navigation when the sidebar is present.
  • Refactor thread navigation grouping into shared logic and add coverage for layout and search filtering behavior.
  • Allow the mobile app to rotate beyond portrait so tablets and foldable-sized windows can use the split layout.

Testing

  • vp check
  • vp run typecheck
  • vp test
  • Added/updated tests for layout breakpoints and thread navigation grouping behavior
  • Not run: native/mobile device UI verification

Note

Medium Risk
Large cross-cutting navigation and native bridge changes affect thread/file/review flows on all form factors; orientation default enables phone landscape, which is a visible behavior change.

Overview
Adds an adaptive workspace shell that turns on split view when the window is wide/tall enough: a persistent thread sidebar (search, grouping, swipe actions), a detail area, and optional inspector columns for files, git, and review changed-files. Root navigation, sheets, and thread routes now branch on usesSplitView (e.g. settings as card vs sheet, no slide animation between threads, empty “select a thread” home). Orientation moves from portrait-only to default so tablets can use landscape split layout.

Review and files gain side-by-side navigators (AdaptiveInspectorLayout), scroll sync between native diff and file list (onVisibleFileChange, scrollToFile / scrollToTop), diff section toolbar restructuring, and idle prewarming plus caching for parsed diff / native row models and source file documents. File tree adds preview-on-press-in preloads, optimistic selection, and list windowing; file routes use setParams / replace where split view keeps one screen mounted.

Native review diff stops sending huge rowsJson / token props through Fabric; payloads go through async imperative calls with off-main decode, generation guards, contentResetKey, and stale-token-patch rejection. New t3-native-controls Expo module exposes UIKit header buttons for the sidebar chrome.

Thread feed in split layout caps chat width, measures pane width for layout, and defers list reveal until scroll-to-end settles to avoid jumpy initial scroll.

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

Note

Add adaptive split-view layout for iPad with inspector panes, sidebar navigation, and orientation support

  • Introduces AdaptiveWorkspaceLayout and related hooks/components to drive a responsive two-pane layout on iPad, gating split view on both width ≥ 720 and height ≥ 600.
  • Adds ThreadNavigationSidebar with search, grouping by repository/project, and swipe-to-archive/delete; refactors drawer to use shared buildThreadNavigationGroups utility that excludes archived threads.
  • Extends ThreadRouteScreen and ReviewSheet with an auxiliary inspector pane (Files/Git) via AdaptiveInspectorLayout; inspector visibility animates with width collapse, opacity, and translation.
  • Reworks native diff view (T3ReviewDiffView, NativeReviewDiffView) to deliver rows/tokens via async imperative calls with background decoding, generation guards, and retry logic, replacing Fabric props; adds onVisibleFileChange event and programmatic scroll.
  • Adds preloadWorkspaceFileContents and prepareSourceFileDocument to warm file content and syntax highlighting caches ahead of selection.
  • Adds a new T3NativeControls Expo module exposing a native iOS header button (T3HeaderButtonView) using SF Symbols.
  • Changes app.config.ts orientation from portrait to default, enabling landscape on iPad.
  • Risk: rows/tokens are no longer set as Fabric props; any native code or tests expecting those props will break. The async retry path in useNativeReviewDiffPayload may silently drop updates if all 4 retries fail.

Macroscope summarized fcaaab4.

- Enable adaptive sidebar navigation on wide mobile windows
- Keep compact single-pane behavior on phones
- Extract and test shared thread grouping and layout logic
@coderabbitai

coderabbitai Bot commented Jun 23, 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: 5bcb9592-9f0f-40a7-9dbe-091c10601c5c

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/ipad-responsive-mobile-layout

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

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

@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 using high effort and found 3 potential issues.

Autofix Details

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

  • ✅ Fixed: Sidebar shows archived threads
    • Added .filter((thread) => thread.archivedAt === null) in buildThreadNavigationGroups before sorting, matching the same filtering logic used by buildHomeThreadGroups.
  • ✅ Fixed: Split feed uses window width
    • Changed viewportWidth initial state to 0 when layoutVariant === "split" so the first render doesn't use the full window width, and added viewportWidth to LegendList's extraData so rows repaint after onLayout corrects it.
  • ✅ Fixed: Split view hides archive
    • Added useThreadListActions hook and a long-press handler with Archive/Delete options to ThreadNavigationSidebar thread rows, restoring thread management actions in split view.

Create PR

Or push these changes by commenting:

@cursor push b06fdc835f
Preview (b06fdc835f)
diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx
--- a/apps/mobile/src/features/threads/ThreadFeed.tsx
+++ b/apps/mobile/src/features/threads/ThreadFeed.tsx
@@ -1134,7 +1134,9 @@
   const initialScrollReadyRef = useRef(false);
   const lastContentHeightRef = useRef(0);
   const { width: windowWidth } = useWindowDimensions();
-  const [viewportWidth, setViewportWidth] = useState(windowWidth);
+  const [viewportWidth, setViewportWidth] = useState(() =>
+    props.layoutVariant === "split" ? 0 : windowWidth,
+  );
   const [interactionState, setInteractionState] = useState<{
     readonly copiedRowId: string | null;
     readonly expandedWorkGroups: Record<string, boolean>;
@@ -1206,6 +1208,7 @@
       markdownStyles,
       reviewCommentColors,
       userBubbleColor,
+      viewportWidth,
     }),
     [
       copiedRowId,
@@ -1215,6 +1218,7 @@
       markdownStyles,
       reviewCommentColors,
       userBubbleColor,
+      viewportWidth,
     ],
   );
   const presentedFeed = useMemo(

diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
--- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
+++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
@@ -1,7 +1,7 @@
 import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
 import { SymbolView } from "expo-symbols";
-import { useMemo, useState } from "react";
-import { Pressable, ScrollView, StyleSheet, TextInput, View } from "react-native";
+import { useCallback, useMemo, useState } from "react";
+import { Alert, Pressable, ScrollView, StyleSheet, TextInput, View } from "react-native";
 import { useSafeAreaInsets } from "react-native-safe-area-context";
 
 import { AppText as Text } from "../../components/AppText";
@@ -10,6 +10,7 @@
 import { relativeTime } from "../../lib/time";
 import { useThemeColor } from "../../lib/useThemeColor";
 import { useProjects, useThreadShells } from "../../state/entities";
+import { useThreadListActions } from "../home/useThreadListActions";
 import { buildThreadNavigationGroups } from "./thread-navigation-groups";
 import { threadStatusTone } from "./threadPresentation";
 
@@ -24,11 +25,23 @@
   const projects = useProjects();
   const threads = useThreadShells();
   const [searchQuery, setSearchQuery] = useState("");
+  const { archiveThread, confirmDeleteThread } = useThreadListActions();
   const groups = useMemo(
     () => buildThreadNavigationGroups({ projects, threads, searchQuery }),
     [projects, searchQuery, threads],
   );
 
+  const handleThreadLongPress = useCallback(
+    (thread: EnvironmentThreadShell) => {
+      Alert.alert(thread.title, undefined, [
+        { text: "Cancel", style: "cancel" },
+        { text: "Archive", onPress: () => archiveThread(thread) },
+        { text: "Delete", style: "destructive", onPress: () => confirmDeleteThread(thread) },
+      ]);
+    },
+    [archiveThread, confirmDeleteThread],
+  );
+
   const backgroundColor = useThemeColor("--color-drawer");
   const borderColor = useThemeColor("--color-border");
   const foregroundColor = useThemeColor("--color-foreground");
@@ -131,6 +144,7 @@
                       accessibilityLabel={thread.title}
                       accessibilityRole="button"
                       accessibilityState={{ selected }}
+                      onLongPress={() => handleThreadLongPress(thread)}
                       onPress={() => props.onSelectThread(thread)}
                       style={({ pressed }) => [
                         styles.threadRow,

diff --git a/apps/mobile/src/features/threads/thread-navigation-groups.ts b/apps/mobile/src/features/threads/thread-navigation-groups.ts
--- a/apps/mobile/src/features/threads/thread-navigation-groups.ts
+++ b/apps/mobile/src/features/threads/thread-navigation-groups.ts
@@ -33,7 +33,9 @@
 
   return groupProjectsByRepository(input).flatMap((group) => {
     const threads = Arr.sort(
-      group.projects.flatMap((projectGroup) => projectGroup.threads),
+      group.projects
+        .flatMap((projectGroup) => projectGroup.threads)
+        .filter((thread) => thread.archivedAt === null),
       threadActivityOrder,
     );
     const title = group.projects[0]?.project.title ?? group.title;

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

Comment thread apps/mobile/src/features/threads/thread-navigation-groups.ts
Comment thread apps/mobile/src/features/threads/ThreadFeed.tsx Outdated
Comment thread apps/mobile/src/app/index.tsx
@macroscopeapp

macroscopeapp Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a major new feature (adaptive split-view layout for iPad/mobile) with new native iOS modules, new layout system, and significant navigation changes. The scope and introduction of new user-facing capabilities warrants human review, especially given the 4 unresolved medium-severity bug findings.

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

- Adapt iPad/mobile layout for sidebar, inspector, and sheets
- Move review diff payloads off Fabric props and add native scrolling
- Add iOS header button module for consistent toolbar actions
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jun 24, 2026

@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 using high effort and found 5 potential issues.

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

Autofix Details

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

  • ✅ Fixed: Stale tokens after file switch
    • Added contentView.tokensByRowId = [:] in setContentResetKey to clear token state when content resets, matching the pattern already used by setTokensResetKey.
  • ✅ Fixed: All files selection reverts
    • Added an isAllFilesSelectedRef that suppresses onVisibleFileChange scroll-sync events when the user has explicitly selected "All files", preventing the sidebar from reverting to a specific file after the programmatic scroll-to-top animation completes.
  • ✅ Fixed: Thread feed reveals before scroll
    • Added setRevealedThreadId(null) to the threadId change effect so returning to a previously revealed thread correctly hides the feed until the scroll-to-end sequence completes.
  • ✅ Fixed: File tree stuck selection highlight
    • Added setPendingSelection(null) when controlledSelectedPath changes and a 1-second timeout fallback in handleSelectFile to clear optimistic state when the controlled path never catches up.
  • ✅ Fixed: Native payload retry mismatch
    • Changed the hardcoded 'T3ReviewDiffView' string in isPendingNativeViewRegistration to use the NATIVE_REVIEW_DIFF_MODULE_NAME constant ('T3ReviewDiffSurface') so the retry path correctly matches the registered module name.

Create PR

Or push these changes by commenting:

@cursor push 032c0d2718
Preview (032c0d2718)
diff --git a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift
--- a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift
+++ b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift
@@ -554,6 +554,7 @@
     lastVisibleFileId = nil
     pendingScrollFileId = nil
     isProgrammaticScrollActive = false
+    contentView.tokensByRowId = [:]
     scrollView.setContentOffset(.zero, animated: false)
     updateViewportFrame()
     applyInitialRowIndexIfNeeded()

diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts
--- a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts
+++ b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts
@@ -168,7 +168,8 @@
 
 function isPendingNativeViewRegistration(error: unknown): boolean {
   return (
-    error instanceof Error && error.message.includes("Unable to find the 'T3ReviewDiffView' view")
+    error instanceof Error &&
+    error.message.includes(`Unable to find the '${NATIVE_REVIEW_DIFF_MODULE_NAME}' view`)
   );
 }
 

diff --git a/apps/mobile/src/features/files/FileTreeBrowser.tsx b/apps/mobile/src/features/files/FileTreeBrowser.tsx
--- a/apps/mobile/src/features/files/FileTreeBrowser.tsx
+++ b/apps/mobile/src/features/files/FileTreeBrowser.tsx
@@ -148,6 +148,7 @@
   }, [defaultExpanded]);
 
   useEffect(() => {
+    setPendingSelection(null);
     if (!controlledSelectedPath) {
       return;
     }
@@ -175,12 +176,25 @@
       return next;
     });
   }, []);
+  const pendingSelectionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
   const handleSelectFile = useCallback(
     (path: string) => {
+      if (pendingSelectionTimerRef.current !== null) {
+        clearTimeout(pendingSelectionTimerRef.current);
+      }
       setPendingSelection({
         path,
         selectedPathAtPress: controlledSelectedPathRef.current,
       });
+      pendingSelectionTimerRef.current = setTimeout(() => {
+        pendingSelectionTimerRef.current = null;
+        setPendingSelection((current) =>
+          current?.path === path &&
+          current.selectedPathAtPress === controlledSelectedPathRef.current
+            ? null
+            : current,
+        );
+      }, 1000);
       onSelectFile(path);
     },
     [onSelectFile],

diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx
--- a/apps/mobile/src/features/review/ReviewSheet.tsx
+++ b/apps/mobile/src/features/review/ReviewSheet.tsx
@@ -465,8 +465,10 @@
     canHighlight: parsedDiff.kind === "files",
   });
 
+  const isAllFilesSelectedRef = useRef(false);
   const handleSelectFile = useCallback(
     (fileId: string | null) => {
+      isAllFilesSelectedRef.current = fileId === null;
       commentSelection.clearSelection();
       if (fileId !== null && collapsedFileIds.includes(fileId)) {
         toggleExpandedFile(fileId);
@@ -484,7 +486,7 @@
   const handleVisibleFileChange = useCallback(
     (event: NativeSyntheticEvent<{ readonly fileId?: string }>) => {
       const { fileId } = event.nativeEvent;
-      if (!fileId) {
+      if (!fileId || isAllFilesSelectedRef.current) {
         return;
       }
       reviewFileNavigatorRef.current?.setVisibleFile(fileId);

diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx
--- a/apps/mobile/src/features/threads/ThreadFeed.tsx
+++ b/apps/mobile/src/features/threads/ThreadFeed.tsx
@@ -1322,6 +1322,7 @@
       cancelAnimationFrame(revealSettleFrameRef.current);
       revealSettleFrameRef.current = null;
     }
+    setRevealedThreadId(null);
     initialScrollReadyRef.current = false;
     isNearEndRef.current = true;
     lastContentHeightRef.current = 0;

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

Comment thread apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift
Comment thread apps/mobile/src/features/review/ReviewSheet.tsx
initialScrollReadyRef.current = false;
isNearEndRef.current = true;
lastContentHeightRef.current = 0;
}, [props.threadId]);

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.

Thread feed reveals before scroll

Medium Severity

The threadId effect clears scroll bookkeeping but not revealedThreadId. If the user returns to a thread whose id already matches revealedThreadId, the list renders at full opacity before onListLoad runs its scroll-to-end sequence, so the chat can briefly appear at the wrong scroll offset.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7a04404. Configure here.

Comment thread apps/mobile/src/features/files/FileTreeBrowser.tsx
Comment thread apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts
- Keep sidebar threads swipeable with archive/delete actions
- Hide archived threads from navigation groups
- Stabilize review diff visibility and optimistic file selection

@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 using high effort and found 3 potential issues.

There are 4 total unresolved issues (including 1 from previous review).

Fix All in Cursor

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

  • ✅ Fixed: Selection timeout reverts highlight
    • The timeout handler now checks whether controlledSelectedPathRef.current has caught up to the tapped path before clearing pending selection, preventing the highlight from reverting during slow navigation.
  • ✅ Fixed: Archive hides active thread
    • Added an onCompleted callback to useThreadListActions and an onThreadRemoved prop to the sidebar that triggers router.back() when the currently-selected thread is archived or deleted.
  • ✅ Fixed: Review navigator stale after reset
    • Added an explicit onVisibleFileChange emission with NSNull() in setContentResetKey so React always receives the 'All files' sync after a content reset, regardless of the lastVisibleFileId guard.

Create PR

Or push these changes by commenting:

@cursor push 058e075f4c
Preview (058e075f4c)
diff --git a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift
--- a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift
+++ b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift
@@ -557,6 +557,7 @@
     pendingScrollFileId = nil
     isProgrammaticScrollActive = false
     scrollView.setContentOffset(.zero, animated: false)
+    onVisibleFileChange(["fileId": NSNull()])
     updateViewportFrame()
     applyInitialRowIndexIfNeeded()
   }

diff --git a/apps/mobile/src/features/files/FileTreeBrowser.tsx b/apps/mobile/src/features/files/FileTreeBrowser.tsx
--- a/apps/mobile/src/features/files/FileTreeBrowser.tsx
+++ b/apps/mobile/src/features/files/FileTreeBrowser.tsx
@@ -197,7 +197,11 @@
       });
       pendingSelectionTimeoutRef.current = setTimeout(() => {
         pendingSelectionTimeoutRef.current = null;
-        setPendingSelection((current) => (current?.path === path ? null : current));
+        setPendingSelection((current) => {
+          if (current?.path !== path) return current;
+          if (controlledSelectedPathRef.current === path) return null;
+          return current;
+        });
       }, OPTIMISTIC_SELECTION_TIMEOUT_MS);
       onSelectFile(path);
     },

diff --git a/apps/mobile/src/features/home/useThreadListActions.ts b/apps/mobile/src/features/home/useThreadListActions.ts
--- a/apps/mobile/src/features/home/useThreadListActions.ts
+++ b/apps/mobile/src/features/home/useThreadListActions.ts
@@ -99,11 +99,19 @@
   );
 }
 
-export function useThreadListActions(): {
+export function useThreadListActions(
+  onCompleted?: (action: ThreadListAction, thread: EnvironmentThreadShell) => void,
+): {
   readonly archiveThread: (thread: EnvironmentThreadShell) => void;
   readonly confirmDeleteThread: (thread: EnvironmentThreadShell) => void;
 } {
-  const executeAction = useThreadActionExecutor();
+  const handleCompleted = useCallback(
+    (action: ThreadListAction, thread: EnvironmentThreadShell) => {
+      onCompleted?.(action, thread);
+    },
+    [onCompleted],
+  );
+  const executeAction = useThreadActionExecutor(onCompleted ? handleCompleted : undefined);
 
   const archiveThread = useCallback(
     (thread: EnvironmentThreadShell) => {

diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx
--- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx
+++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx
@@ -238,6 +238,7 @@
               onOpenSettings={() => router.push("/settings")}
               onSelectThread={handleSelectThread}
               onStartNewTask={() => router.push("/new")}
+              onThreadRemoved={() => router.back()}
             />
           </Animated.View>
         ) : null}

diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
--- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
+++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
@@ -23,13 +23,24 @@
   readonly onOpenSettings: () => void;
   readonly onSelectThread: (thread: EnvironmentThreadShell) => void;
   readonly onStartNewTask: () => void;
+  readonly onThreadRemoved?: (thread: EnvironmentThreadShell) => void;
 }) {
   const insets = useSafeAreaInsets();
   const projects = useProjects();
   const threads = useThreadShells();
   const [searchQuery, setSearchQuery] = useState("");
   const openSwipeableRef = useRef<SwipeableMethods | null>(null);
-  const { archiveThread, confirmDeleteThread } = useThreadListActions();
+  const { onThreadRemoved, selectedThreadKey } = props;
+  const handleThreadActionCompleted = useCallback(
+    (_action: unknown, thread: EnvironmentThreadShell) => {
+      const threadKey = scopedThreadKey(thread.environmentId, thread.id);
+      if (threadKey === selectedThreadKey) {
+        onThreadRemoved?.(thread);
+      }
+    },
+    [onThreadRemoved, selectedThreadKey],
+  );
+  const { archiveThread, confirmDeleteThread } = useThreadListActions(handleThreadActionCompleted);
   const groups = useMemo(
     () => buildThreadNavigationGroups({ projects, threads, searchQuery }),
     [projects, searchQuery, threads],

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

Reviewed by Cursor Bugbot for commit ee9b1af. Configure here.

pendingSelectionTimeoutRef.current = setTimeout(() => {
pendingSelectionTimeoutRef.current = null;
setPendingSelection((current) => (current?.path === path ? null : current));
}, OPTIMISTIC_SELECTION_TIMEOUT_MS);

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.

Selection timeout reverts highlight

Medium Severity

The optimistic file selection clears after 1 second, even if the parent's selectedPath prop hasn't updated. This can briefly show the wrong or previous selection during slow navigation, negating the immediate tap feedback.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ee9b1af. Configure here.

icon: "archivebox",
label: "Archive",
onPress: () => archiveThread(thread),
}}

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.

Archive hides active thread

Medium Severity

Archiving or deleting a thread from the split-view sidebar removes it from the list, but the main detail pane's route doesn't update. This can leave the main pane displaying a thread no longer present in the sidebar.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ee9b1af. Configure here.

}
lastVisibleFileId = nil
onVisibleFileChange(["fileId": NSNull()])
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.

Review navigator stale after reset

Medium Severity

After a contentResetKey change scrolls the diff to the top, native code clears lastVisibleFileId but skips onVisibleFileChange when offset is near zero because lastVisibleFileId is already nil. If the review file navigator still holds a prior file for the same section, its highlight can disagree with the reset scroll position until the user scrolls again.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ee9b1af. Configure here.

- Stabilize thread selection routing in the adaptive workspace layout
- Extract and memoize thread sidebar rows to reduce unnecessary rerenders
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