diff --git a/apps/mobile/src/components/AndroidScreenHeader.tsx b/apps/mobile/src/components/AndroidScreenHeader.tsx
index 46bc2c7c0912..f672830d3645 100644
--- a/apps/mobile/src/components/AndroidScreenHeader.tsx
+++ b/apps/mobile/src/components/AndroidScreenHeader.tsx
@@ -18,6 +18,7 @@ export function AndroidHeaderIconButton(props: {
readonly icon: AppSymbolName;
readonly onPress?: () => void;
readonly disabled?: boolean;
+ readonly filled?: boolean;
}) {
return (
diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
index 29bbdb49c890..e2ecbfe304e1 100644
--- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
+++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
@@ -49,6 +49,9 @@ import {
} from "../layout/native-mail-search-toolbar";
import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar";
import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
+import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
+import { useAtomSet, useAtomValue } from "@effect/atom-react";
+import { AsyncResult } from "effect/unstable/reactivity";
import { ThreadRouteScreen } from "../threads/ThreadRouteScreen";
import { FileMarkdownPreview } from "./FileMarkdownPreview";
import { FileTreeBrowser } from "./FileTreeBrowser";
@@ -547,6 +550,12 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
readonly mode: FileViewMode;
} | null>(null);
const [previewRevision, setPreviewRevision] = useState(0);
+ const preferencesResult = useAtomValue(mobilePreferencesAtom);
+ const savePreferences = useAtomSet(updateMobilePreferencesAtom);
+ const markdownPreviewEnabled =
+ AsyncResult.isSuccess(preferencesResult) &&
+ preferencesResult.value.markdownPreviewEnabled === true;
+ const primaryColor = useUniwindTheme()["--color-primary"];
const previewKey = JSON.stringify([environmentId, cwd, relativePath, previewRevision]);
const [fullScreenPreview, setFullScreenPreview] = useState(null);
const isVideoFile = relativePath !== null && isVideoPreviewFile(relativePath);
@@ -554,14 +563,29 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
relativePath !== null && !isVideoFile && isWorkspaceBrowserPreviewPath(relativePath);
const isImageFile =
relativePath !== null && !isVideoFile && isWorkspaceImagePreviewPath(relativePath);
+ const isMarkdownFile = relativePath !== null && isMarkdownPreviewFile(relativePath);
const canPreview =
relativePath !== null &&
(isMarkdownPreviewFile(relativePath) || isBrowserFile || isImageFile || isVideoFile);
const activeMode =
- relativePath !== null && modeOverride?.path === relativePath
- ? modeOverride.mode
- : defaultViewMode(relativePath);
+ relativePath !== null && targetLine !== null
+ ? "source"
+ : relativePath !== null && modeOverride?.path === relativePath
+ ? modeOverride.mode
+ : isMarkdownFile && markdownPreviewEnabled
+ ? "preview"
+ : defaultViewMode(relativePath);
const resolvedActiveMode = isVideoFile ? "preview" : canPreview ? activeMode : "source";
+ const handleToggleMarkdownMode = useCallback(() => {
+ if (relativePath === null || targetLine !== null) return;
+ const next: FileViewMode = resolvedActiveMode === "preview" ? "source" : "preview";
+ setModeOverride({ path: relativePath, mode: next });
+ void savePreferences({ markdownPreviewEnabled: next === "preview" });
+ }, [relativePath, resolvedActiveMode, savePreferences, targetLine]);
+ const markdownToggleLabel =
+ isMarkdownFile && resolvedActiveMode === "preview"
+ ? "Show markdown source"
+ : "Show rendered markdown";
const assetPreviewPath = isBrowserFile || isImageFile || isVideoFile ? relativePath : null;
const assetPreview = useWorkspaceFileAssetUrlState({
cwd,
@@ -843,6 +867,14 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
onBack={handleBack}
trailing={
<>
+ {isMarkdownFile && targetLine === null ? (
+
+ ) : null}
{fileInspector.supported ? (
) : null}
+ {isMarkdownFile && targetLine === null ? (
+
+ ) : null}
{fileMenuActions.some(({ inline }) => inline) ? (
diff --git a/apps/mobile/src/persistence/mobile-preferences.ts b/apps/mobile/src/persistence/mobile-preferences.ts
index f14a73f15cb2..a27cdc8aaab9 100644
--- a/apps/mobile/src/persistence/mobile-preferences.ts
+++ b/apps/mobile/src/persistence/mobile-preferences.ts
@@ -26,6 +26,8 @@ export interface Preferences {
readonly markdownFontSize?: number;
readonly codeFontSize?: number | null;
readonly codeWordBreak?: boolean;
+ /** Device-local mirror of web's `renderMarkdown` preference for markdown file previews. */
+ readonly markdownPreviewEnabled?: boolean;
readonly connectOnboardingOptOutAccounts?: ReadonlyArray;
readonly collapsedProjectGroups?: readonly string[];
/** @deprecated Kept temporarily so older OTA bundles retain the selected mode. */
@@ -95,6 +97,7 @@ function sanitizePreferences(parsed: Preferences): Preferences {
markdownFontSize?: number;
codeFontSize?: number | null;
codeWordBreak?: boolean;
+ markdownPreviewEnabled?: boolean;
connectOnboardingOptOutAccounts?: ReadonlyArray;
collapsedProjectGroups?: readonly string[];
projectGroupingEnabled?: boolean;
@@ -144,6 +147,9 @@ function sanitizePreferences(parsed: Preferences): Preferences {
preferences.codeFontSize = parsed.codeFontSize;
}
if (typeof parsed.codeWordBreak === "boolean") preferences.codeWordBreak = parsed.codeWordBreak;
+ if (typeof parsed.markdownPreviewEnabled === "boolean") {
+ preferences.markdownPreviewEnabled = parsed.markdownPreviewEnabled;
+ }
if (Array.isArray(parsed.connectOnboardingOptOutAccounts)) {
preferences.connectOnboardingOptOutAccounts = parsed.connectOnboardingOptOutAccounts.filter(
(account): account is string => typeof account === "string",