Skip to content

Add archived threads and mobile file viewer#3155

Merged
juliusmarminge merged 5 commits into
mainfrom
t3code/debb1f2c
Jun 19, 2026
Merged

Add archived threads and mobile file viewer#3155
juliusmarminge merged 5 commits into
mainfrom
t3code/debb1f2c

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

  • Added archived-thread browsing to mobile, including a dedicated archive screen and shared archived-thread state helpers.
  • Built a mobile file viewer with file-tree navigation, markdown previews, source rendering, and web preview support.
  • Reworked the home screen into a configurable list with environment filtering, project/thread sort controls, and archive/delete thread actions.
  • Improved markdown link handling so file links preserve path, line, and column metadata and can route into in-app file views.
  • Updated the native markdown renderer to support link press delegation, preserved soft breaks, and revised chip/attachment rendering.

Testing

  • Not run (no test/lint commands were executed in this environment).
  • Added and updated unit tests for archived thread sorting, file path parsing, file tree helpers, route parsing, native markdown text, and thread activity logic.

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 + initialRowIndex scroll), markdown, image, and WebView previews; home gains HomeHeader filters/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 queues navigate until registerWebview, then loads the pending URL. Review diff iOS supports initialRowIndex and 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

  • Adds an /settings/archive route that loads archived thread snapshots across environments, with search, environment filter, sort-by-date, and unarchive/delete actions.
  • Adds swipe-to-archive and swipe-to-delete gestures on home screen thread rows with full-swipe haptics on iOS; excludes archived threads from the main list.
  • Adds a /threads/:environmentId/:threadId/files route with a searchable file tree browser, syntax-highlighted source view, image viewer (tap-to-fullscreen), WebView HTML preview, and markdown preview.
  • Tapping file links in thread feed markdown navigates to the in-app file viewer with optional line targeting; external links open via Linking.
  • Shared thread sorting/grouping logic (sortThreads, getLatestThreadForProject, etc.) is extracted into @t3tools/client-runtime/state/thread-sort and project grouping into @t3tools/client-runtime/state/projectGrouping, consumed by both web and mobile.
  • Web preview tab close is now optimistic via beginPreviewSessionClose/cancelPreviewSessionClose, suppressing stale server snapshots for the closed tab and restoring state on failure.
  • Risk: NativeMarkdownTextStyle removes fileBackgroundColor/skillBackgroundColor and adds inlineCodeColor; any consumers passing the old chip background props will need to update.

Macroscope summarized edb6504.

Julius Marminge and others added 2 commits June 18, 2026 13:27
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
@coderabbitai

coderabbitai Bot commented Jun 18, 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: ccf5777c-fd04-4f62-b8d2-fe459e9600b2

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/debb1f2c

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

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jun 18, 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 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 column parameter to buildThreadFilesNavigation and passed presentation.column from the ThreadFeed link press handler so file links with column targets route the full position.
  • ✅ Fixed: Preview file links open externally
    • Added an onLinkPress prop to FileMarkdownPreview and wired it from ThreadFilesRouteScreen with a handler that resolves workspace file links for in-app navigation and falls back to Linking.openURL for external links.
  • ✅ Fixed: Files screen waits forever
    • Added a fallback from selectedThread?.id when routeThreadId is null, mirroring the existing environmentId fallback pattern so the screen no longer hangs on the loading spinner.

Create PR

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.

Comment thread apps/mobile/modules/t3-markdown-text/src/markdownLinks.ts
Comment thread apps/mobile/src/features/files/FileMarkdownPreview.tsx
Comment thread apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

🚀 Expo continuous deployment is ready!

  • Project → t3-code
  • Platforms → android, ios
  • Scheme → t3code-preview
  🤖 Android 🍎 iOS
Fingerprint 27e228668e373d1941548dc669856ecf3fd6261f fa880122b8876ecaafbb21281d21e292d7b09484
Build Details Build Permalink
DetailsDistribution: INTERNAL
Build profile: preview:dev
Runtime version: 27e228668e373d1941548dc669856ecf3fd6261f
App version: 0.1.0
Git commit: 2e1e9aecba70f12439172c4cc2a0443cbb920826
Build Permalink
DetailsDistribution: INTERNAL
Build profile: preview:dev
Runtime version: fa880122b8876ecaafbb21281d21e292d7b09484
App version: 0.1.0
Git commit: 8ef95c479ae1c01cbd33365fe792b762398ffd5d
Update Details Update Permalink
DetailsBranch: pr-3155
Runtime version: 27e228668e373d1941548dc669856ecf3fd6261f
Git commit: f96f70f517c3ba2942340bc7f66b93c27d626b4a
Update Permalink
DetailsBranch: pr-3155
Runtime version: fa880122b8876ecaafbb21281d21e292d7b09484
Git commit: f96f70f517c3ba2942340bc7f66b93c27d626b4a
Update QR

@macroscopeapp

macroscopeapp Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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

@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 default effort and found 2 potential issues.

Fix All in Cursor

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.

Create PR

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

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.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 922c425. Configure here.

Comment thread apps/mobile/src/lib/routes.ts
- 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
@juliusmarminge juliusmarminge merged commit 30034ec into main Jun 19, 2026
15 checks passed
@juliusmarminge juliusmarminge deleted the t3code/debb1f2c branch June 19, 2026 03:24
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