Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b37f9c4
feat(auth): separate filesystem access scopes
juliusmarminge Sep 4, 2026
326dbd7
docs(auth): name filesystem scope in host boundary
juliusmarminge Sep 4, 2026
08f8d32
fix(auth): consolidate filesystem scope imports
juliusmarminge Sep 4, 2026
7d62ab6
style(auth): format filesystem scope changes
juliusmarminge Sep 4, 2026
2c1eee3
test(web): retain file preview subscription during scope changes
juliusmarminge Sep 4, 2026
278a25d
fix(web): preserve unsaved files after permission loss
juliusmarminge Sep 4, 2026
eefc5f9
fix(web): keep newer file edits marked unsaved
juliusmarminge Sep 4, 2026
3dbcabc
fix(mobile): hide cached local diffs without file access
juliusmarminge Sep 4, 2026
09e236e
fix(mobile): await file access before choosing workspace defaults
juliusmarminge Sep 4, 2026
b04c3bd
style(web): format filesystem markdown guards
juliusmarminge Sep 4, 2026
f1c732a
fix(web): explain missing local diff access
juliusmarminge Sep 4, 2026
f49735c
fix(files): wait for permissions before showing denial
juliusmarminge Sep 4, 2026
9fb3e74
fix(files): stop waiting for offline permission checks
juliusmarminge Sep 4, 2026
cea8f76
fix(files): wait for the initial environment catalog
juliusmarminge Sep 4, 2026
541f554
fix(files): distinguish pending access from unavailable connections
juliusmarminge Sep 4, 2026
f0358ec
fix(clients): wait for file grants before failing assets
juliusmarminge Sep 5, 2026
1960a43
test(web): isolate pending asset grants from preview controls
juliusmarminge Sep 5, 2026
c02b8ab
fix(mobile): preserve review selection while grants load
juliusmarminge Sep 5, 2026
c13fbf2
fix(mobile): report access failures for unavailable reviews
juliusmarminge Sep 5, 2026
db6a3a7
fix(web): keep filesystem access checks pending
juliusmarminge Sep 5, 2026
43b28aa
fix(web): retain new-folder prompts after listing errors
juliusmarminge Sep 5, 2026
0e1f1a8
fix(mobile): report denied local review access
juliusmarminge Sep 5, 2026
8e94a38
fix(web): gate content search on filesystem access
juliusmarminge Sep 5, 2026
d7bafb2
fix(clients): gate host media actions by file permission
juliusmarminge Sep 5, 2026
e7f5f95
fix(clients): wait for media grants before enabling actions
juliusmarminge Sep 5, 2026
0023bd3
test(web): use compatible media action receipts
juliusmarminge Sep 5, 2026
271a735
test(web): clean up highlight fixture frames
juliusmarminge Sep 5, 2026
f9754c3
test(web): isolate asset hooks in preview view tests
juliusmarminge Sep 5, 2026
8336893
refactor(web): resolve file access through one hook
juliusmarminge Sep 6, 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