Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4dd78c7
feat(auth): separate filesystem access scopes
juliusmarminge Sep 4, 2026
ba3e78b
docs(auth): name filesystem scope in host boundary
juliusmarminge Sep 4, 2026
97ff4ac
fix(auth): consolidate filesystem scope imports
juliusmarminge Sep 4, 2026
a6ee0b3
style(auth): format filesystem scope changes
juliusmarminge Sep 4, 2026
4e213c8
test(web): retain file preview subscription during scope changes
juliusmarminge Sep 4, 2026
f1ef970
fix(web): preserve unsaved files after permission loss
juliusmarminge Sep 4, 2026
2aea27b
fix(web): keep newer file edits marked unsaved
juliusmarminge Sep 4, 2026
02d240d
fix(mobile): hide cached local diffs without file access
juliusmarminge Sep 4, 2026
7ccecf5
fix(mobile): await file access before choosing workspace defaults
juliusmarminge Sep 4, 2026
5d76ea2
style(web): format filesystem markdown guards
juliusmarminge Sep 4, 2026
02e8074
fix(web): explain missing local diff access
juliusmarminge Sep 4, 2026
4413285
fix(files): wait for permissions before showing denial
juliusmarminge Sep 4, 2026
cdcece1
fix(files): stop waiting for offline permission checks
juliusmarminge Sep 4, 2026
5ac7987
fix(files): wait for the initial environment catalog
juliusmarminge Sep 4, 2026
90ec4b9
fix(files): distinguish pending access from unavailable connections
juliusmarminge Sep 4, 2026
84f3135
fix(clients): wait for file grants before failing assets
juliusmarminge Sep 5, 2026
86c4af5
test(web): isolate pending asset grants from preview controls
juliusmarminge Sep 5, 2026
85f4234
fix(mobile): preserve review selection while grants load
juliusmarminge Sep 5, 2026
12690f5
fix(mobile): report access failures for unavailable reviews
juliusmarminge Sep 5, 2026
b5ce9ef
fix(web): keep filesystem access checks pending
juliusmarminge Sep 5, 2026
a579fe7
fix(web): retain new-folder prompts after listing errors
juliusmarminge Sep 5, 2026
ef94e7e
fix(mobile): report denied local review access
juliusmarminge Sep 5, 2026
b4efabb
fix(web): gate content search on filesystem access
juliusmarminge Sep 5, 2026
672cb6d
fix(clients): gate host media actions by file permission
juliusmarminge Sep 5, 2026
21ac076
fix(clients): wait for media grants before enabling actions
juliusmarminge Sep 5, 2026
6d47f63
test(web): use compatible media action receipts
juliusmarminge Sep 5, 2026
f7ae86f
test(web): clean up highlight fixture frames
juliusmarminge Sep 5, 2026
ec34663
test(web): isolate asset hooks in preview view tests
juliusmarminge Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 55 additions & 9 deletions apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { resolveFilesystemReadAccess } from "@t3tools/client-runtime/state/filesystem";
import { environmentSession } from "../../state/session";
import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native";
import type { MenuAction } from "@react-native-menu/menu";
Expand Down Expand Up @@ -36,6 +38,7 @@ import { useMediaActions, type MediaActionsSource } from "../../lib/mediaActions
import { useThreadSelection } from "../../state/use-thread-selection";
import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree";
import { useEnvironmentQuery } from "../../state/query";
import { useEnvironmentPresentation } from "../../state/presentation";
import { projectEnvironment } from "../../state/projects";
import type { AssetUrlFailureReason } from "../../state/asset-url-state";
import {
Expand Down Expand Up @@ -259,14 +262,15 @@ function useThreadFilesWorkspace(params: {
};
}

function FilesUnavailable() {
function FilesUnavailable({
detail = "This thread does not have an active workspace path.",
}: {
detail?: string;
}) {
return (
<View className="flex-1 items-center justify-center bg-sheet px-6">
<NativeStackScreenOptions options={{ title: "Files" }} />
<EmptyState
title="Files unavailable"
detail="This thread does not have an active workspace path."
/>
<EmptyState title="Files unavailable" detail={detail} />
</View>
);
}
Expand Down Expand Up @@ -313,8 +317,19 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
props.route.params,
);
const revealedInspectorRef = useRef(false);
const fileAccessSession = useEnvironmentQuery(
environmentId !== null ? environmentSession.sessionStateAtom(environmentId) : null,
);
const fileEnvironment = useEnvironmentPresentation(environmentId);
const fileAccess = resolveFilesystemReadAccess({
isCatalogReady: fileEnvironment.isReady,
connection: fileEnvironment.presentation?.connection ?? null,
session: fileAccessSession.data,
sessionError: fileAccessSession.error,
});
const { canReadFiles } = fileAccess;
const entriesQuery = useEnvironmentQuery(
environmentId !== null && cwd !== null && !fileInspector.supported
canReadFiles && environmentId !== null && cwd !== null && !fileInspector.supported
? projectEnvironment.listEntries({
environmentId,
input: { cwd },
Expand Down Expand Up @@ -406,6 +421,14 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
return <LoadingScreen message="Opening files..." messagePlacement="above-spinner" />;
}

if (!canReadFiles) {
if (fileAccess.isPending) {
return <LoadingScreen message="Checking file access..." messagePlacement="above-spinner" />;
}
return (
<FilesUnavailable detail={fileAccess.error ?? "This connection cannot read host files."} />
);
}
if (cwd === null) {
return <FilesUnavailable />;
}
Expand Down Expand Up @@ -517,7 +540,7 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
)}
<FileTreeBrowser
entries={entriesData?.entries ?? []}
error={entriesQuery.error}
error={canReadFiles ? entriesQuery.error : "This connection cannot read host files."}
isPending={entriesQuery.isPending}
searchQuery={searchQuery}
selectedPath={null}
Expand Down Expand Up @@ -621,8 +644,23 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
relativePath !== null &&
!isVideoFile &&
(resolvedActiveMode === "source" || isMarkdownPreviewFile(relativePath));
const fileAccessSession = useEnvironmentQuery(
environmentId !== null ? environmentSession.sessionStateAtom(environmentId) : null,
);
const fileEnvironment = useEnvironmentPresentation(environmentId);
const fileAccess = resolveFilesystemReadAccess({
isCatalogReady: fileEnvironment.isReady,
connection: fileEnvironment.presentation?.connection ?? null,
session: fileAccessSession.data,
sessionError: fileAccessSession.error,
});
const { canReadFiles } = fileAccess;
const fileQuery = useEnvironmentQuery(
environmentId !== null && cwd !== null && relativePath !== null && needsFileContents
canReadFiles &&
environmentId !== null &&
cwd !== null &&
relativePath !== null &&
needsFileContents
? projectEnvironment.readFile({
environmentId,
input: { cwd, relativePath },
Expand Down Expand Up @@ -799,6 +837,14 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
return <LoadingScreen message="Opening file..." messagePlacement="above-spinner" />;
}

if (!canReadFiles) {
if (fileAccess.isPending) {
return <LoadingScreen message="Checking file access..." messagePlacement="above-spinner" />;
}
return (
<FilesUnavailable detail={fileAccess.error ?? "This connection cannot read host files."} />
);
}
if (cwd === null) {
return <FilesUnavailable />;
}
Expand Down Expand Up @@ -926,7 +972,7 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
mediaSource={mediaSource}
resolveVideoUri={assetPreview.refresh}
fileContents={fileData?.contents ?? null}
fileError={fileQuery.error}
fileError={canReadFiles ? fileQuery.error : "This connection cannot read host files."}
initialLine={targetLine}
relativePath={relativePath}
threadId={threadId}
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/features/files/preload-workspace-file.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { AuthFilesystemReadScope } from "@t3tools/contracts";
import { readEnvironmentScope } from "../../state/session";
import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime";
import type { EnvironmentId } from "@t3tools/contracts";
import {
Expand Down Expand Up @@ -30,6 +32,7 @@ export function preloadWorkspaceFileContents(input: {
readonly theme: ReviewDiffTheme;
}): void {
if (
!readEnvironmentScope(input.environmentId, AuthFilesystemReadScope) ||
isWorkspaceBrowserPreviewPath(input.relativePath) ||
isWorkspaceImagePreviewPath(input.relativePath) ||
isVideoPreviewFile(input.relativePath)
Expand Down
34 changes: 28 additions & 6 deletions apps/mobile/src/features/files/thread-file-navigator-pane.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { resolveFilesystemReadAccess } from "@t3tools/client-runtime/state/filesystem";
import { environmentSession } from "../../state/session";
import type { EnvironmentId, ProjectListEntriesResult } from "@t3tools/contracts";
import { SymbolView } from "../../components/AppSymbol";
import { useCallback, useMemo, useState, type ComponentProps } from "react";
Expand All @@ -15,6 +17,7 @@ import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
import { useUniwindTheme } from "../../lib/useUniwindTheme";
import { projectEnvironment } from "../../state/projects";
import { useEnvironmentQuery } from "../../state/query";
import { useEnvironmentPresentation } from "../../state/presentation";
import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
import { FileTreeBrowser } from "./FileTreeBrowser";
import { preloadWorkspaceFileContents } from "./preload-workspace-file";
Expand All @@ -33,11 +36,24 @@ export function ThreadFileNavigatorPane(props: {
const foregroundColor = theme["--color-foreground"];
const sheetColor = theme["--color-sheet"];
const headerScrollEdgeEffects = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
const fileAccessSession = useEnvironmentQuery(
environmentSession.sessionStateAtom(props.environmentId),
);
const fileEnvironment = useEnvironmentPresentation(props.environmentId);
const fileAccess = resolveFilesystemReadAccess({
isCatalogReady: fileEnvironment.isReady,
connection: fileEnvironment.presentation?.connection ?? null,
session: fileAccessSession.data,
sessionError: fileAccessSession.error,
});
const { canReadFiles } = fileAccess;
const entriesQuery = useEnvironmentQuery(
projectEnvironment.listEntries({
environmentId: props.environmentId,
input: { cwd: props.cwd },
}),
canReadFiles
? projectEnvironment.listEntries({
environmentId: props.environmentId,
input: { cwd: props.cwd },
})
: null,
);
const entriesData = entriesQuery.data as ProjectListEntriesResult | null;
const handlePreviewFile = useCallback(
Expand Down Expand Up @@ -71,8 +87,14 @@ export function ThreadFileNavigatorPane(props: {
const fileTree = (
<FileTreeBrowser
entries={entriesData?.entries ?? []}
error={entriesQuery.error}
isPending={entriesQuery.isPending}
error={
canReadFiles
? entriesQuery.error
: fileAccess.isPending
? null
: (fileAccess.error ?? "This connection cannot read host files.")
}
isPending={fileAccess.isPending || entriesQuery.isPending}
searchQuery={searchQuery}
selectedPath={props.selectedPath}
onPreviewFile={handlePreviewFile}
Expand Down
29 changes: 25 additions & 4 deletions apps/mobile/src/features/projects/AddProjectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
createBrowseNavigationCoordinator,
filterFilesystemBrowseEntries,
getFilesystemBrowsePath,
resolveFilesystemReadAccess,
} from "@t3tools/client-runtime/state/filesystem";
import {
appendBrowsePathSegment,
Expand All @@ -34,6 +35,7 @@ import {
import {
AuthOrchestrationOperateScope,
AuthSourceControlWriteScope,
AuthFilesystemReadScope,
CommandId,
type EnvironmentId,
type EnvironmentMachineKind,
Expand All @@ -55,7 +57,8 @@ import { useProjects, useServerConfigs } from "../../state/entities";
import { filesystemEnvironment } from "../../state/filesystem";
import { projectEnvironment } from "../../state/projects";
import { useEnvironmentQuery } from "../../state/query";
import { readEnvironmentScope, useEnvironmentScope } from "../../state/session";
import { environmentSession, useEnvironmentScope, readEnvironmentScope } from "../../state/session";
import { useEnvironmentPresentation } from "../../state/presentation";
import { sourceControlEnvironment } from "../../state/sourceControl";
import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol";
Expand Down Expand Up @@ -301,7 +304,11 @@ function useBrowsePathInput(environment: EnvironmentOption | null, pinnedDirecto
setIsBrowseNavigating(true);
const committed = await browseNavigation.run(
async () => {
if (environment && canPreloadBrowsePath(environmentRuntime?.connectionState)) {
if (
environment &&
readEnvironmentScope(environment.environmentId, AuthFilesystemReadScope) &&
canPreloadBrowsePath(environmentRuntime?.connectionState)
) {
await loadBrowsePath({
environmentId: environment.environmentId,
input: { partialPath: selectedDirectoryPath },
Expand Down Expand Up @@ -785,8 +792,19 @@ function FolderBrowser(props: {
() => (browsePath.directoryPath.length > 0 ? { partialPath: browsePath.directoryPath } : null),
[browsePath.directoryPath],
);
const fileAccessSession = useEnvironmentQuery(
environmentSession.sessionStateAtom(props.environment.environmentId),
);
const fileEnvironment = useEnvironmentPresentation(props.environment.environmentId);
const fileAccess = resolveFilesystemReadAccess({
isCatalogReady: fileEnvironment.isReady,
connection: fileEnvironment.presentation?.connection ?? null,
session: fileAccessSession.data,
sessionError: fileAccessSession.error,
});
const { canReadFiles } = fileAccess;
const browseState = useEnvironmentQuery(
browseInput === null
!canReadFiles || browseInput === null
? null
: filesystemEnvironment.browse({
environmentId: props.environment.environmentId,
Expand All @@ -808,9 +826,12 @@ function FolderBrowser(props: {
return (
<>
<SectionTitle>Browse folders</SectionTitle>
{!canReadFiles && !fileAccess.isPending ? (
<ErrorBanner message={fileAccess.error ?? "This connection cannot browse host folders."} />
) : null}
{browseState.error ? <ErrorBanner message={browseState.error} /> : null}
<ListSection>
{browseState.isPending && browseState.data === null ? (
{fileAccess.isPending || (browseState.isPending && browseState.data === null) ? (
<View className="items-center py-5">
<ActivityIndicator colorClassName={"accent-icon-muted"} />
</View>
Expand Down
14 changes: 8 additions & 6 deletions apps/mobile/src/features/review/ReviewSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -840,12 +840,14 @@ export function ReviewSheet(props: ReviewSheetProps) {
>
{listHeader}
{!selectedSection ? (
<View className="border-b border-border bg-card px-4 py-5">
<Text className="text-sm font-t3-bold text-foreground">No review diffs</Text>
<Text className="text-xs leading-normal text-foreground-muted">
This thread has no ready turn diffs and the worktree diff is empty.
</Text>
</View>
error ? null : (
<View className="border-b border-border bg-card px-4 py-5">
<Text className="text-sm font-t3-bold text-foreground">No review diffs</Text>
<Text className="text-xs leading-normal text-foreground-muted">
This thread has no ready turn diffs and the worktree diff is empty.
</Text>
</View>
)
) : selectedSection.isLoading && selectedSection.diff === null ? (
<View className="items-center gap-3 border-b border-border bg-card px-4 py-6">
<ActivityIndicator size="small" />
Expand Down
Loading
Loading