Add archived threads and mobile file viewer#3155
Conversation
Co-authored-by: codex <codex@users.noreply.github.com>
- Add archive screens and thread list state shared with web - Improve markdown file links, soft breaks, and selectable text layout - Refactor home header and thread list actions
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Column metadata not routed
- Added a
columnparameter tobuildThreadFilesNavigationand passedpresentation.columnfrom the ThreadFeed link press handler so file links with column targets route the full position.
- Added a
- ✅ Fixed: Preview file links open externally
- Added an
onLinkPressprop toFileMarkdownPreviewand wired it fromThreadFilesRouteScreenwith a handler that resolves workspace file links for in-app navigation and falls back toLinking.openURLfor external links.
- Added an
- ✅ Fixed: Files screen waits forever
- Added a fallback from
selectedThread?.idwhenrouteThreadIdis null, mirroring the existingenvironmentIdfallback pattern so the screen no longer hangs on the loading spinner.
- Added a fallback from
Or push these changes by commenting:
@cursor push 9394da0417
Preview (9394da0417)
diff --git a/apps/mobile/src/features/files/FileMarkdownPreview.tsx b/apps/mobile/src/features/files/FileMarkdownPreview.tsx
--- a/apps/mobile/src/features/files/FileMarkdownPreview.tsx
+++ b/apps/mobile/src/features/files/FileMarkdownPreview.tsx
@@ -143,14 +143,21 @@
]);
}
-export function FileMarkdownPreview(props: { readonly markdown: string }) {
+export function FileMarkdownPreview(props: {
+ readonly markdown: string;
+ readonly onLinkPress?: (href: string) => void;
+}) {
const styles = useMarkdownPreviewStyles();
return (
<ScrollView className="flex-1 bg-card" contentContainerStyle={{ padding: 18 }}>
<View className="mx-auto w-full max-w-[760px]">
{hasNativeSelectableMarkdownText() ? (
- <SelectableMarkdownText markdown={props.markdown} textStyle={styles.nativeTextStyle} />
+ <SelectableMarkdownText
+ markdown={props.markdown}
+ textStyle={styles.nativeTextStyle}
+ onLinkPress={props.onLinkPress}
+ />
) : (
<Markdown
options={{ gfm: true }}
diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
--- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
+++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
@@ -2,19 +2,22 @@
import { SymbolView } from "expo-symbols";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useEffect, useMemo, useState } from "react";
-import { ActivityIndicator, Pressable, ScrollView, View } from "react-native";
+import { ActivityIndicator, Linking, Pressable, ScrollView, View } from "react-native";
import {
EnvironmentId,
type ProjectListEntriesResult,
type ProjectReadFileResult,
ThreadId,
} from "@t3tools/contracts";
+import * as Haptics from "expo-haptics";
+import { resolveMarkdownLinkPresentation } from "@t3tools/mobile-markdown-text/links";
import { AppText as Text } from "../../components/AppText";
import { CopyTextButton } from "../../components/CopyTextButton";
import { EmptyState } from "../../components/EmptyState";
import { LoadingScreen } from "../../components/LoadingScreen";
import { cn } from "../../lib/cn";
+import { buildThreadFilesNavigation } from "../../lib/routes";
import { useThemeColor } from "../../lib/useThemeColor";
import { useThreadSelection } from "../../state/use-thread-selection";
import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree";
@@ -25,7 +28,13 @@
import { FileTreeBrowser } from "./FileTreeBrowser";
import { SourceFileSurface } from "./SourceFileSurface";
import { WorkspaceFileWebPreview } from "./WorkspaceFileWebPreview";
-import { basename, fileBreadcrumbs, isBrowserPreviewFile, isMarkdownPreviewFile } from "./filePath";
+import {
+ basename,
+ fileBreadcrumbs,
+ isBrowserPreviewFile,
+ isMarkdownPreviewFile,
+ resolveWorkspaceRelativeFilePath,
+} from "./filePath";
type FileViewMode = "preview" | "source";
@@ -177,6 +186,7 @@
readonly fileError: string | null;
readonly relativePath: string;
readonly initialLine: number | null;
+ readonly onLinkPress?: (href: string) => void;
readonly threadId: ThreadId;
readonly truncated: boolean;
}) {
@@ -224,7 +234,7 @@
</View>
) : null}
{props.activeMode === "preview" && isMarkdown ? (
- <FileMarkdownPreview markdown={props.fileContents} />
+ <FileMarkdownPreview markdown={props.fileContents} onLinkPress={props.onLinkPress} />
) : (
<SourceFileSurface
contents={props.fileContents}
@@ -269,7 +279,8 @@
routeEnvironmentId !== null
? EnvironmentId.make(routeEnvironmentId)
: (selectedThread?.environmentId ?? null);
- const threadId = routeThreadId !== null ? ThreadId.make(routeThreadId) : null;
+ const threadId =
+ routeThreadId !== null ? ThreadId.make(routeThreadId) : (selectedThread?.id ?? null);
const project = selectedThreadProject as {
readonly title?: string;
readonly workspaceRoot?: string;
@@ -316,6 +327,31 @@
[router],
);
+ const handleLinkPress = useCallback(
+ (href: string) => {
+ const presentation = resolveMarkdownLinkPresentation(href);
+ if (presentation.kind === "file") {
+ const relativePath = resolveWorkspaceRelativeFilePath(cwd, presentation.path);
+ if (relativePath) {
+ void Haptics.selectionAsync();
+ router.push(
+ buildThreadFilesNavigation(
+ { environmentId: environmentId!, threadId: threadId! },
+ relativePath,
+ presentation.line,
+ presentation.column,
+ ),
+ );
+ return;
+ }
+ }
+ if (presentation.href) {
+ void Linking.openURL(presentation.href);
+ }
+ },
+ [cwd, environmentId, threadId, router],
+ );
+
if (selectedThread === null || environmentId === null || threadId === null) {
return <LoadingScreen message="Opening files..." messagePlacement="above-spinner" />;
}
@@ -389,6 +425,7 @@
fileContents={fileData?.contents ?? null}
fileError={fileQuery.error}
initialLine={targetLine}
+ onLinkPress={handleLinkPress}
relativePath={selectedPath}
threadId={threadId}
truncated={fileData?.truncated ?? false}
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
@@ -1179,6 +1179,7 @@
{ environmentId: props.environmentId, threadId: props.threadId },
relativePath,
presentation.line,
+ presentation.column,
),
);
}
diff --git a/apps/mobile/src/lib/routes.ts b/apps/mobile/src/lib/routes.ts
--- a/apps/mobile/src/lib/routes.ts
+++ b/apps/mobile/src/lib/routes.ts
@@ -90,11 +90,18 @@
input: ThreadRouteInput | PlainThreadRouteInput,
relativePath?: string | null,
line?: number | null,
+ column?: number | null,
): Href {
const environmentId = String(input.environmentId);
const threadId = String("threadId" in input ? input.threadId : input.id);
- const params: { environmentId: string; threadId: string; path?: string; line?: string } = {
+ const params: {
+ environmentId: string;
+ threadId: string;
+ path?: string;
+ line?: string;
+ column?: string;
+ } = {
environmentId,
threadId,
};
@@ -105,6 +112,9 @@
if (Number.isFinite(line) && Number(line) > 0) {
params.line = String(Math.floor(Number(line)));
}
+ if (Number.isFinite(column) && Number(column) > 0) {
+ params.column = String(Math.floor(Number(column)));
+ }
return {
pathname: "/threads/[environmentId]/[threadId]/files",You can send follow-ups to the cloud agent here.
|
🚀 Expo continuous deployment is ready!
|
ApprovabilityVerdict: Needs human review Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
- Split file tree and file detail routes - Move source rendering to native diff-backed previews - Simplify markdown skill chips and add initial diff scroll position
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Refresh resets native scroll
- Removed the hasAppliedInitialRowIndex = false reset from setRowsJson so that refreshing rows no longer re-triggers the initial scroll; the flag is only reset by setInitialRowIndex when the target actually changes.
- ✅ Fixed: Legacy file query URLs break
- Added a useEffect in ThreadFilesTreeScreen that detects legacy path/line query params and redirects to the new segment-based route via router.replace.
Or push these changes by commenting:
@cursor push 4661ffdf14
Preview (4661ffdf14)
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
@@ -403,7 +403,6 @@
do {
rows = try JSONDecoder().decode([ReviewDiffNativeRow].self, from: data)
contentView.rows = rows
- hasAppliedInitialRowIndex = false
emitDebug("rows-decoded", [
"rows": rows.count,
"firstKind": rows.first?.kind ?? "none",
@@ -412,7 +411,6 @@
} catch {
rows = []
contentView.rows = []
- hasAppliedInitialRowIndex = false
updateContentMetrics()
emitDebug("rows-decode-failed", [
"error": error.localizedDescription,
diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
--- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
+++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
@@ -1,7 +1,7 @@
import Stack from "expo-router/stack";
import { SymbolView } from "expo-symbols";
import { useLocalSearchParams, useRouter } from "expo-router";
-import { useCallback, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
ActivityIndicator,
Linking,
@@ -427,6 +427,16 @@
const router = useRouter();
const [searchQuery, setSearchQuery] = useState("");
const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace();
+ const params = useLocalSearchParams<{ path?: string; line?: string }>();
+ const legacyPath = typeof params.path === "string" ? params.path : null;
+ const legacyLine = typeof params.line === "string" ? params.line : null;
+
+ useEffect(() => {
+ if (legacyPath && environmentId !== null && threadId !== null) {
+ const line = normalizeRouteLine(legacyLine);
+ router.replace(buildThreadFilesNavigation({ environmentId, threadId }, legacyPath, line));
+ }
+ }, [environmentId, legacyLine, legacyPath, router, threadId]);
const entriesQuery = useEnvironmentQuery(
environmentId !== null && cwd !== null
? projectEnvironment.listEntries({You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 922c425. Configure here.
| do { | ||
| rows = try JSONDecoder().decode([ReviewDiffNativeRow].self, from: data) | ||
| contentView.rows = rows | ||
| hasAppliedInitialRowIndex = false |
There was a problem hiding this comment.
Refresh resets native scroll
Medium Severity
Each time rowsJson is decoded, hasAppliedInitialRowIndex is cleared and applyInitialRowIndexIfNeeded runs again. Refreshing or reloading source file content re-sends rows with an initialRowIndex, so the native source viewer can jump back to the deep-linked line after the user has scrolled elsewhere.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 922c425. Configure here.
- Support direct image preview flow across mobile, web, and server asset access - Queue preview navigation until desktop webviews register to avoid race failures - Add coverage for file preview routing, asset URLs, and webview lifetime



Summary
Testing
Not run(no test/lint commands were executed in this environment).Note
Medium Risk
Large new mobile surface (files, archives, WebView) plus native markdown rendering changes; desktop preview navigation timing is behavior-sensitive but covered by a new test.
Overview
Adds mobile archived-thread management (settings entry, list/filter/sort/unarchive/delete) and a thread workspace file browser with tree search, source (native Shiki diff surface +
initialRowIndexscroll), markdown, image, and WebView previews; home gainsHomeHeaderfilters/sort/grouping and thread archive/delete wiring.Native markdown drops custom chip backgrounds in favor of inline file/skill attachments, adds
onLinkPress,preserveSoftBreaks, richer file link path/line/column parsing, and layout fixes for shrink-to-fit bubbles. Desktop preview queuesnavigateuntilregisterWebview, then loads the pending URL. Review diff iOS supportsinitialRowIndexand allows zero-width change bars.Reviewed by Cursor Bugbot for commit edb6504. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add archived threads screen and in-app file viewer to mobile
/settings/archiveroute that loads archived thread snapshots across environments, with search, environment filter, sort-by-date, and unarchive/delete actions./threads/:environmentId/:threadId/filesroute with a searchable file tree browser, syntax-highlighted source view, image viewer (tap-to-fullscreen), WebView HTML preview, and markdown preview.Linking.sortThreads,getLatestThreadForProject, etc.) is extracted into@t3tools/client-runtime/state/thread-sortand project grouping into@t3tools/client-runtime/state/projectGrouping, consumed by both web and mobile.beginPreviewSessionClose/cancelPreviewSessionClose, suppressing stale server snapshots for the closed tab and restoring state on failure.NativeMarkdownTextStyleremovesfileBackgroundColor/skillBackgroundColorand addsinlineCodeColor; any consumers passing the old chip background props will need to update.Macroscope summarized edb6504.