diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 8529a475251..6d74a579992 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -174,6 +174,7 @@ "node-addon-api": "^7.1.0", "node-pty": "1.1.0", "os-locale": "^6.0.2", + "pathe": "^2.0.3", "pidtree": "^0.6.0", "pidusage": "^4.0.1", "posthog-js": "1.310.1", diff --git a/apps/desktop/src/lib/trpc/routers/filesystem/index.ts b/apps/desktop/src/lib/trpc/routers/filesystem/index.ts index 18a5a557311..00bb2597cf8 100644 --- a/apps/desktop/src/lib/trpc/routers/filesystem/index.ts +++ b/apps/desktop/src/lib/trpc/routers/filesystem/index.ts @@ -905,6 +905,60 @@ export const createFilesystemRouter = () => { return { copied, errors }; }), + /** + * Read a file by absolute path with size cap and binary detection. + * Used for viewing files outside the current worktree. + */ + readFile: publicProcedure + .input( + z.object({ + filePath: z.string(), + }), + ) + .query( + async ({ + input, + }): Promise< + | { + ok: true; + content: string; + truncated: boolean; + byteLength: number; + } + | { + ok: false; + reason: "not-found" | "too-large" | "binary"; + } + > => { + const MAX_FILE_SIZE = 2 * 1024 * 1024; + try { + const stats = await fs.stat(input.filePath); + if (stats.size > MAX_FILE_SIZE) { + return { ok: false, reason: "too-large" }; + } + + const buffer = await fs.readFile(input.filePath); + + // Binary detection (same as readWorkingFile) + const checkLength = Math.min(buffer.length, BINARY_CHECK_SIZE); + for (let i = 0; i < checkLength; i++) { + if (buffer[i] === 0) { + return { ok: false, reason: "binary" }; + } + } + + return { + ok: true, + content: buffer.toString("utf-8"), + truncated: false, + byteLength: buffer.length, + }; + } catch { + return { ok: false, reason: "not-found" }; + } + }, + ), + exists: publicProcedure .input(z.object({ path: z.string() })) .query(async ({ input }) => { diff --git a/apps/desktop/src/renderer/screens/main/components/CommandPalette/CommandPalette.tsx b/apps/desktop/src/renderer/screens/main/components/CommandPalette/CommandPalette.tsx index 7dad01e01a2..bfb8c6afdd2 100644 --- a/apps/desktop/src/renderer/screens/main/components/CommandPalette/CommandPalette.tsx +++ b/apps/desktop/src/renderer/screens/main/components/CommandPalette/CommandPalette.tsx @@ -62,7 +62,7 @@ export function CommandPalette({ isLoading={isLoading} results={searchResults} getItemValue={(file) => `${file.path} ${query}`} - onSelectItem={(file) => onSelectFile(file.relativePath)} + onSelectItem={(file) => onSelectFile(file.path)} renderItem={(file) => { const { icon: Icon, color } = getFileIcon(file.name, false); return ( diff --git a/apps/desktop/src/renderer/screens/main/components/KeywordSearch/useKeywordSearch.ts b/apps/desktop/src/renderer/screens/main/components/KeywordSearch/useKeywordSearch.ts index fb96a587154..4fcbac69bcd 100644 --- a/apps/desktop/src/renderer/screens/main/components/KeywordSearch/useKeywordSearch.ts +++ b/apps/desktop/src/renderer/screens/main/components/KeywordSearch/useKeywordSearch.ts @@ -85,7 +85,7 @@ export function useKeywordSearch({ const selectMatch = useCallback( (match: KeywordSearchResult) => { useTabsStore.getState().addFileViewerPane(workspaceId, { - filePath: match.relativePath, + filePath: match.path, line: match.line, column: match.column, }); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/components/ChatMastraMessageList/components/UserMessage/UserMessage.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/components/ChatMastraMessageList/components/UserMessage/UserMessage.tsx index 60cf70bcb0c..0ffc951f611 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/components/ChatMastraMessageList/components/UserMessage/UserMessage.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/components/ChatMastraMessageList/components/UserMessage/UserMessage.tsx @@ -1,5 +1,6 @@ import { useCallback, useState } from "react"; import { useTabsStore } from "renderer/stores/tabs/store"; +import { resolveToAbsolutePath } from "../../../../../../ChatPane/ChatInterface/utils/file-paths"; import type { UserMessageActionPayload, UserMessageRestartRequest, @@ -58,9 +59,17 @@ export function UserMessage({ ); const openMentionedFile = useCallback( (filePath: string) => { - addFileViewerPane(workspaceId, { filePath, isPinned: true }); + const absolutePath = resolveToAbsolutePath({ + filePath, + workspaceRoot: workspaceCwd, + }); + if (!absolutePath) return; + addFileViewerPane(workspaceId, { + filePath: absolutePath, + isPinned: true, + }); }, - [addFileViewerPane, workspaceId], + [addFileViewerPane, workspaceCwd, workspaceId], ); const handleCopy = useCallback(() => { if (!fullText) return; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/components/ChatMastraMessageList/components/UserMessage/components/UserMessageText/UserMessageText.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/components/ChatMastraMessageList/components/UserMessage/components/UserMessageText/UserMessageText.tsx index eac5bc19b6a..df3757ab5d4 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/components/ChatMastraMessageList/components/UserMessage/components/UserMessageText/UserMessageText.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/components/ChatMastraMessageList/components/UserMessage/components/UserMessageText/UserMessageText.tsx @@ -1,4 +1,4 @@ -import { normalizeWorkspaceFilePath } from "../../../../../../../../ChatPane/ChatInterface/utils/file-paths"; +import { resolveToAbsolutePath } from "../../../../../../../../ChatPane/ChatInterface/utils/file-paths"; import type { MastraMessage, MastraMessagePart } from "../../types"; import { parseUserMentions } from "../../utils/parseUserMentions"; @@ -36,11 +36,11 @@ export function UserMessageText({ ); } - const normalizedPath = normalizeWorkspaceFilePath({ + const absolutePath = resolveToAbsolutePath({ filePath: segment.relativePath, workspaceRoot: workspaceCwd, }); - const canOpen = Boolean(normalizedPath); + const canOpen = Boolean(absolutePath); return ( + + + Back to {worktreePath?.split("/").pop()} + + + + {breadcrumbSegments.map((segment, i) => ( + + {i > 0 && ( + + )} + + + ))} + + )} +
- {newItemMode && newItemParentPath === worktreePath && ( - - )} + {newItemMode && + newItemParentPath === (effectiveRoot ?? worktreePath) && ( + + )} {isSearching ? ( searchResultEntries.length > 0 ? ( @@ -329,7 +401,7 @@ export function FilesView() { - handleNewFile(worktreePath)}> + handleNewFile(effectiveRoot ?? worktreePath)} + > New File - handleNewFolder(worktreePath)}> + handleNewFolder(effectiveRoot ?? worktreePath)} + > New Folder diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeToolbar/FileTreeToolbar.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeToolbar/FileTreeToolbar.tsx index b048232cbd4..9a823a5177d 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeToolbar/FileTreeToolbar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeToolbar/FileTreeToolbar.tsx @@ -3,9 +3,11 @@ import { Input } from "@superset/ui/input"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { useCallback, useEffect, useRef, useState } from "react"; import { + LuArrowUp, LuChevronsDownUp, LuFilePlus, LuFolderPlus, + LuHouse, LuRefreshCw, LuX, } from "react-icons/lu"; @@ -19,6 +21,8 @@ interface FileTreeToolbarProps { onCollapseAll: () => void; onRefresh: () => void; isRefreshing?: boolean; + onNavigateToParent?: () => void; + onNavigateHome?: (() => void) | undefined; } export function FileTreeToolbar({ @@ -29,6 +33,8 @@ export function FileTreeToolbar({ onCollapseAll, onRefresh, isRefreshing = false, + onNavigateToParent, + onNavigateHome, }: FileTreeToolbarProps) { const [localSearchTerm, setLocalSearchTerm] = useState(searchTerm); const debounceTimeoutRef = useRef | null>(null); @@ -97,6 +103,42 @@ export function FileTreeToolbar({
+ {onNavigateToParent && ( + + + + + + Go to Parent Directory + + + )} + + {onNavigateHome && ( + + + + + + Back to Workspace Root + + + )} +