From 4577d0ffc97558a3e9e833bea459094a743e7bb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 09:06:07 +0000 Subject: [PATCH] fix(remote): guard local file-tree reads and show why the tree is unavailable listDirectory, listDirectoriesBatch, getFileModifiedAt, listDirectoryCached, lsWorkspaceWithStatus, getWorkspaceReadme and ensureWorkspaceIndexed read this machine's filesystem at the given path. For a remote repository they now reject with `unsupported:`. The file browser skips those reads and shows REMOTE_FILE_TREE_REASON in the tree pane. The workspace overview shows the same reason instead of silently logging the error and reporting "No files found". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MbYSPHBRmDYwPJpHG33Y4t --- src/components/FileBrowser.tsx | 19 +++++++++++--- src/components/ShowWorkspace.tsx | 13 ++++++--- src/lib/api-extra.ts | 42 +++++++++++++++++++++--------- src/lib/api.ts | 16 +++++++----- src/lib/remote-capabilities.ts | 3 +++ src/lib/remote-path-guards.test.ts | 7 ----- 6 files changed, 67 insertions(+), 33 deletions(-) diff --git a/src/components/FileBrowser.tsx b/src/components/FileBrowser.tsx index 4896917fc..e653518bb 100644 --- a/src/components/FileBrowser.tsx +++ b/src/components/FileBrowser.tsx @@ -96,6 +96,8 @@ import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { Tabs, TabsList, TabsTrigger } from "./ui/tabs"; import { Textarea } from "./ui/textarea"; import { DirectoryBatchLoader } from "./file-browser/directoryBatchLoader"; +import { REMOTE_FILE_TREE_REASON } from "../lib/remote-capabilities"; +import { remoteRepositoryContaining } from "../lib/repository-adapter"; import { flattenExpandedTree } from "./file-browser/flattenTree"; // Helper to check if file is binary @@ -1204,8 +1206,9 @@ export const FileBrowser = ({ })(); const workspacePath = workspace ? getFullWorkspacePath(workspace) : basePath; + const isRemoteTree = remoteRepositoryContaining(basePath) !== null; useSWR( - repoPath + repoPath && !isRemoteTree ? [ "ensure-workspace-indexed", repoPath, @@ -1221,8 +1224,9 @@ export const FileBrowser = ({ data: loadedRootEntries, isLoading: isLoadingDir, mutate: mutateRootEntries, - } = useSWR(basePath ? ["list-directory", basePath] : null, async () => - filterHiddenEntries(await listDirectory(basePath)), + } = useSWR( + basePath && !isRemoteTree ? ["list-directory", basePath] : null, + async () => filterHiddenEntries(await listDirectory(basePath)), ); const rootEntries = loadedRootEntries ?? EMPTY_DIRECTORY_ENTRIES; @@ -2016,7 +2020,14 @@ export const FileBrowser = ({
{/* File Tree */}
- {isLoadingDir && rootEntries.length === 0 ? ( + {isRemoteTree ? ( +

+ {REMOTE_FILE_TREE_REASON} +

+ ) : isLoadingDir && rootEntries.length === 0 ? (
diff --git a/src/components/ShowWorkspace.tsx b/src/components/ShowWorkspace.tsx index 2bbfab0e4..e2a8d3dfc 100644 --- a/src/components/ShowWorkspace.tsx +++ b/src/components/ShowWorkspace.tsx @@ -77,6 +77,8 @@ import { } from "../lib/commitsTabLabel"; import { cn, getFullWorkspacePath, resolveReadmeImageSrc } from "../lib/utils"; import { sumWorkspaceLocFromLog } from "../lib/workspace-stack"; +import { REMOTE_FILE_TREE_REASON } from "../lib/remote-capabilities"; +import { RemoteOperationUnsupportedError } from "../lib/repository-adapter"; import type { AgentReviewComment } from "../lib/api-types-review"; import type { SessionCreationInfo } from "../types/sessions"; import { ChangesDiffViewer } from "./ChangesDiffViewer"; @@ -476,8 +478,11 @@ export const ShowWorkspace = ({ ]); return { entries, readme }; } catch (error) { - console.error("Failed to load workspace overview:", error); - return { entries: [], readme: null as string | null }; + const unavailable = error instanceof RemoteOperationUnsupportedError; + if (!unavailable) { + console.error("Failed to load workspace overview:", error); + } + return { entries: [], readme: null as string | null, unavailable }; } }, ); @@ -1290,7 +1295,9 @@ export const ShowWorkspace = ({ })} {!overviewPending && rootEntries.length === 0 && (
- No files found + {overviewData?.unavailable + ? REMOTE_FILE_TREE_REASON + : "No files found"}
)}
diff --git a/src/lib/api-extra.ts b/src/lib/api-extra.ts index e28ee05d8..4201cb89b 100644 --- a/src/lib/api-extra.ts +++ b/src/lib/api-extra.ts @@ -284,11 +284,21 @@ export const writeAgentCliFiles = ( export const cleanupAgentCliFiles = (paths: string[]): Promise => invoke("cleanup_agent_cli_files", { paths }); -export const getFileModifiedAt = (path: string): Promise => - invoke("get_file_modified_at", { path }); +const FILE_TREE = "Browsing the file tree"; -export const listDirectory = (path: string): Promise => - invoke("list_directory", { path }); +export const getFileModifiedAt = async ( + path: string, +): Promise => { + assertLocalOperation(path, FILE_TREE); + return invoke("get_file_modified_at", { path }); +}; + +export const listDirectory = async ( + path: string, +): Promise => { + assertLocalOperation(path, FILE_TREE); + return invoke("list_directory", { path }); +}; export interface DirectoryBatchResult { path: string; @@ -296,25 +306,31 @@ export interface DirectoryBatchResult { error?: string; } -export const listDirectoriesBatch = ( +export const listDirectoriesBatch = async ( paths: string[], -): Promise => - invoke("list_directories_batch", { paths }); +): Promise => { + for (const path of paths) assertLocalOperation(path, FILE_TREE); + return invoke("list_directories_batch", { paths }); +}; -export const lsWorkspaceWithStatus = ( +export const lsWorkspaceWithStatus = async ( repoPath: string, workspaceId: number | null, -): Promise => - invoke("ls_workspace_with_status", { repoPath, workspaceId }); +): Promise => { + assertLocalOperation(repoPath, FILE_TREE); + return invoke("ls_workspace_with_status", { repoPath, workspaceId }); +}; // Kept as the typed frontend counterpart of the registered Tauri command. // eslint-disable-next-line local/no-unused-exported-ts-functions, local/require-tauri-api-exports-used -export const listDirectoryCached = ( +export const listDirectoryCached = async ( repoPath: string, workspaceId: number | null, parentPath: string, -): Promise => - invoke("list_directory_cached", { repoPath, workspaceId, parentPath }); +): Promise => { + assertLocalOperation(repoPath, FILE_TREE); + return invoke("list_directory_cached", { repoPath, workspaceId, parentPath }); +}; export interface SendArtifactRecord { id: string; diff --git a/src/lib/api.ts b/src/lib/api.ts index 24add155a..a61822c08 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -124,16 +124,18 @@ export const archiveWorkspace = async ( }); }; -export const ensureWorkspaceIndexed = ( +export const ensureWorkspaceIndexed = async ( repoPath: string, workspaceId: number | null, workspacePath: string, -): Promise => - invoke("ensure_workspace_indexed", { +): Promise => { + assertLocalOperation(repoPath, "Indexing workspace files"); + return invoke("ensure_workspace_indexed", { repoPath, workspaceId, workspacePath, }); +}; export const getSetting = (key: string): Promise => invoke("get_setting", { key }); @@ -247,11 +249,13 @@ export const listGitignoredPathSuggestions = ( ): Promise => invoke("list_gitignored_path_suggestions", { repoPath }); -export const getWorkspaceReadme = ( +export const getWorkspaceReadme = async ( repoPath: string, workspaceId: number | null, -): Promise => - invoke("get_workspace_readme", { repoPath, workspaceId }); +): Promise => { + assertLocalOperation(repoPath, "Reading the workspace README"); + return invoke("get_workspace_readme", { repoPath, workspaceId }); +}; export const getWorkspaceFileHunks = ( repoPath: string, diff --git a/src/lib/remote-capabilities.ts b/src/lib/remote-capabilities.ts index 44964b00c..aa085cd64 100644 --- a/src/lib/remote-capabilities.ts +++ b/src/lib/remote-capabilities.ts @@ -9,6 +9,9 @@ export const REMOTE_MERGE_REASON = "Merging a remote workspace is not available yet. Push the branch and merge it from your hosting provider instead."; +export const REMOTE_FILE_TREE_REASON = + "Browsing the file tree is not available for remote repositories yet. Use Go to file to open a file."; + export interface ActionCapability { supported: boolean; reason?: string; diff --git a/src/lib/remote-path-guards.test.ts b/src/lib/remote-path-guards.test.ts index 0191c639b..57a67002d 100644 --- a/src/lib/remote-path-guards.test.ts +++ b/src/lib/remote-path-guards.test.ts @@ -25,18 +25,15 @@ const NOT_REPOSITORY_PATHS = new Set(["cleanupAgentCliFiles"]); const KNOWN_UNGUARDED: string[] = [ "apply_stash", "delete_stash", - "ensure_workspace_indexed", "export_stash_git_patch", "get_cached_pr_ci_status", "get_cached_pr_info", - "get_file_modified_at", "get_git_remote_url", "get_log_timeseries", "get_pr_checks_via_gh", "get_pr_info_via_gh", "get_repo_logs", "get_stash_diff", - "get_workspace_readme", "get_workspace_setup_status", "github_open_or_create_workspace_from_issue", "init_repo", @@ -44,9 +41,6 @@ const KNOWN_UNGUARDED: string[] = [ "jj_check_branch_exists", "list_cached_pr_ci_statuses", "list_cached_pr_statuses", - "list_directories_batch", - "list_directory", - "list_directory_cached", "list_gitignored_path_suggestions", "list_installed_skills", "list_send_artifacts", @@ -55,7 +49,6 @@ const KNOWN_UNGUARDED: string[] = [ "list_workflow_runs", "list_workflows", "load_repo_yaml_config", - "ls_workspace_with_status", "open_or_create_workspace_from_pr", "refresh_pr_branch_status", "refresh_pr_statuses",