diff --git a/crates/agent-gateway/web/src/App.tsx b/crates/agent-gateway/web/src/App.tsx index b089ab6c..f882ea2f 100644 --- a/crates/agent-gateway/web/src/App.tsx +++ b/crates/agent-gateway/web/src/App.tsx @@ -2236,6 +2236,38 @@ export default function App() { [activateWorkspaceProject, checkWorkspaceProjectDirectory], ); + const handleBrowseWorkspaceProjectInFileTree = useCallback( + async (project: WorkspaceProject) => { + if (!(await checkWorkspaceProjectDirectory(project))) { + return; + } + const pathKey = workspaceProjectPathKey(project.path); + if (!pathKey) { + return; + } + + if (isMobileSidebarLayout()) { + setSidebarOpen(false); + } + setActiveView("chat"); + setProjectToolsPanelOpen(true); + activateWorkspaceProject(project); + setSettings((prev) => + updateProjectToolsFileTreeOpen( + updateCustomSettings(prev, { + projectToolsPanel: { + ...prev.customSettings.projectToolsPanel, + activeTab: "fileTree", + }, + }), + pathKey, + true, + ), + ); + }, + [activateWorkspaceProject, checkWorkspaceProjectDirectory, setSettings], + ); + const handleOpenCreateWorkspaceProject = useCallback(() => { setProjectPickerOpen(true); }, []); @@ -5891,6 +5923,7 @@ export default function App() { onCreateProject={handleOpenCreateWorkspaceProject} onSelectProject={handleSelectWorkspaceProject} onNewConversationForProject={handleNewConversationForProject} + onBrowseProjectInFileTree={handleBrowseWorkspaceProjectInFileTree} onStartRenamingProject={handleStartRenamingWorkspaceProject} onProjectRenameDraftChange={setProjectRenameDraft} onCommitProjectRename={handleCommitWorkspaceProjectRename} diff --git a/crates/agent-gateway/web/src/components/chat/ChatHistorySidebar.tsx b/crates/agent-gateway/web/src/components/chat/ChatHistorySidebar.tsx index 1bd5f057..0de88184 100644 --- a/crates/agent-gateway/web/src/components/chat/ChatHistorySidebar.tsx +++ b/crates/agent-gateway/web/src/components/chat/ChatHistorySidebar.tsx @@ -15,6 +15,7 @@ import { ChevronRight, Edit3, Folder, + FolderTree, Link2, MessageSquareText, MoreHorizontal, @@ -75,6 +76,7 @@ type ChatHistorySidebarProps = { onCreateProject?: () => void; onSelectProject?: (project: WorkspaceProject) => void; onNewConversationForProject?: (project: WorkspaceProject) => void; + onBrowseProjectInFileTree?: (project: WorkspaceProject) => void; onStartRenamingProject?: (project: WorkspaceProject) => void; onProjectRenameDraftChange?: (value: string) => void; onCommitProjectRename?: () => void; @@ -642,6 +644,7 @@ const ProjectRow = memo(function ProjectRow(props: { renameDraft: string; onSelectProject: (project: WorkspaceProject) => void; onNewConversationForProject: (project: WorkspaceProject) => void; + onBrowseProjectInFileTree?: (project: WorkspaceProject) => void; onStartRenamingProject: (project: WorkspaceProject) => void; onProjectRenameDraftChange: (value: string) => void; onCommitProjectRename: () => void; @@ -660,6 +663,7 @@ const ProjectRow = memo(function ProjectRow(props: { renameDraft, onSelectProject, onNewConversationForProject, + onBrowseProjectInFileTree, onStartRenamingProject, onProjectRenameDraftChange, onCommitProjectRename, @@ -698,6 +702,10 @@ const ProjectRow = memo(function ProjectRow(props: { onSetProjectPinned(project, !isPinned); }, [isPinned, onSetProjectPinned, project]); + const handleBrowseInFileTree = useCallback(() => { + onBrowseProjectInFileTree?.(project); + }, [onBrowseProjectInFileTree, project]); + if (isPendingRemove) { return (
@@ -936,6 +944,12 @@ const ProjectRow = memo(function ProjectRow(props: { ) : null} + {onBrowseProjectInFileTree ? ( + + + {t("chat.workspaceBrowseInFileTree")} + + ) : null} @@ -1034,6 +1048,7 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi onCreateProject, onSelectProject, onNewConversationForProject, + onBrowseProjectInFileTree, onStartRenamingProject, onProjectRenameDraftChange, onCommitProjectRename, @@ -1088,6 +1103,9 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi const handleNewConversationForProject = useStableEvent((project: WorkspaceProject) => { onNewConversationForProject?.(project); }); + const handleBrowseProjectInFileTree = useStableEvent((project: WorkspaceProject) => { + onBrowseProjectInFileTree?.(project); + }); const handleStartRenamingProject = useStableEvent((project: WorkspaceProject) => { onStartRenamingProject?.(project); }); @@ -1631,6 +1649,9 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi renameDraft={projectRenameDraft} onSelectProject={handleSelectProject} onNewConversationForProject={handleNewConversationForProject} + onBrowseProjectInFileTree={ + onBrowseProjectInFileTree ? handleBrowseProjectInFileTree : undefined + } onStartRenamingProject={handleStartRenamingProject} onProjectRenameDraftChange={handleProjectRenameDraftChange} onCommitProjectRename={handleCommitProjectRename} diff --git a/crates/agent-gateway/web/src/i18n/config.ts b/crates/agent-gateway/web/src/i18n/config.ts index a14d164c..038d22a5 100644 --- a/crates/agent-gateway/web/src/i18n/config.ts +++ b/crates/agent-gateway/web/src/i18n/config.ts @@ -35,6 +35,7 @@ export const translations: Record> = { "chat.workspaceUnpin": "取消置顶", "chat.workspaceRename": "修改标题", "chat.workspaceRemove": "移除工作空间", + "chat.workspaceBrowseInFileTree": "在文件树浏览", "chat.workspaceRemoveConfirm": "移除「{name}」?", "chat.workspaceRemoveRunning": "后台任务运行中,暂时不能移除。", "chat.workspaceRemoveDescription": "会删除此工作空间下的历史对话,不会删除文件夹。", @@ -1121,6 +1122,7 @@ export const translations: Record> = { "chat.workspaceUnpin": "Unpin", "chat.workspaceRename": "Rename", "chat.workspaceRemove": "Remove workspace", + "chat.workspaceBrowseInFileTree": "Browse in File Tree", "chat.workspaceRemoveConfirm": "Remove \"{name}\"?", "chat.workspaceRemoveRunning": "A background task is running, so this workspace cannot be removed yet.", "chat.workspaceRemoveDescription": diff --git a/crates/agent-gui/src/components/chat/ChatHistorySidebar.tsx b/crates/agent-gui/src/components/chat/ChatHistorySidebar.tsx index 07be5ff6..24619b05 100644 --- a/crates/agent-gui/src/components/chat/ChatHistorySidebar.tsx +++ b/crates/agent-gui/src/components/chat/ChatHistorySidebar.tsx @@ -14,6 +14,8 @@ import { ChevronRight, Edit3, Folder, + FolderOpen, + FolderTree, Link2, MessageSquareText, MoreHorizontal, @@ -66,6 +68,8 @@ type ChatHistorySidebarProps = { onCreateProject?: () => void; onSelectProject?: (project: WorkspaceProject) => void; onNewConversationForProject?: (project: WorkspaceProject) => void; + onBrowseProjectInFileTree?: (project: WorkspaceProject) => void; + onBrowseProjectInSystemFileManager?: (project: WorkspaceProject) => void; onStartRenamingProject?: (project: WorkspaceProject) => void; onProjectRenameDraftChange?: (value: string) => void; onCommitProjectRename?: () => void; @@ -407,6 +411,8 @@ const ProjectRow = memo(function ProjectRow(props: { renameDraft: string; onSelectProject: (project: WorkspaceProject) => void; onNewConversationForProject: (project: WorkspaceProject) => void; + onBrowseProjectInFileTree?: (project: WorkspaceProject) => void; + onBrowseProjectInSystemFileManager?: (project: WorkspaceProject) => void; onStartRenamingProject: (project: WorkspaceProject) => void; onProjectRenameDraftChange: (value: string) => void; onCommitProjectRename: () => void; @@ -425,6 +431,8 @@ const ProjectRow = memo(function ProjectRow(props: { renameDraft, onSelectProject, onNewConversationForProject, + onBrowseProjectInFileTree, + onBrowseProjectInSystemFileManager, onStartRenamingProject, onProjectRenameDraftChange, onCommitProjectRename, @@ -463,6 +471,14 @@ const ProjectRow = memo(function ProjectRow(props: { onSetProjectPinned(project, !isPinned); }, [isPinned, onSetProjectPinned, project]); + const handleBrowseInFileTree = useCallback(() => { + onBrowseProjectInFileTree?.(project); + }, [onBrowseProjectInFileTree, project]); + + const handleBrowseInSystemFileManager = useCallback(() => { + onBrowseProjectInSystemFileManager?.(project); + }, [onBrowseProjectInSystemFileManager, project]); + if (isPendingRemove) { return (
@@ -703,6 +719,21 @@ const ProjectRow = memo(function ProjectRow(props: { ) : null} + {onBrowseProjectInFileTree ? ( + + + {t("chat.workspaceBrowseInFileTree")} + + ) : null} + {onBrowseProjectInSystemFileManager ? ( + + + {t("chat.workspaceBrowseInSystemFileManager")} + + ) : null} @@ -801,6 +832,8 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi onCreateProject, onSelectProject, onNewConversationForProject, + onBrowseProjectInFileTree, + onBrowseProjectInSystemFileManager, onStartRenamingProject, onProjectRenameDraftChange, onCommitProjectRename, @@ -853,6 +886,12 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi const handleNewConversationForProject = useStableEvent((project: WorkspaceProject) => { onNewConversationForProject?.(project); }); + const handleBrowseProjectInFileTree = useStableEvent((project: WorkspaceProject) => { + onBrowseProjectInFileTree?.(project); + }); + const handleBrowseProjectInSystemFileManager = useStableEvent((project: WorkspaceProject) => { + onBrowseProjectInSystemFileManager?.(project); + }); const handleStartRenamingProject = useStableEvent((project: WorkspaceProject) => { onStartRenamingProject?.(project); }); @@ -1355,6 +1394,14 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi renameDraft={projectRenameDraft} onSelectProject={handleSelectProject} onNewConversationForProject={handleNewConversationForProject} + onBrowseProjectInFileTree={ + onBrowseProjectInFileTree ? handleBrowseProjectInFileTree : undefined + } + onBrowseProjectInSystemFileManager={ + onBrowseProjectInSystemFileManager + ? handleBrowseProjectInSystemFileManager + : undefined + } onStartRenamingProject={handleStartRenamingProject} onProjectRenameDraftChange={handleProjectRenameDraftChange} onCommitProjectRename={handleCommitProjectRename} diff --git a/crates/agent-gui/src/i18n/config.ts b/crates/agent-gui/src/i18n/config.ts index c16178cf..8b1a4819 100644 --- a/crates/agent-gui/src/i18n/config.ts +++ b/crates/agent-gui/src/i18n/config.ts @@ -39,9 +39,12 @@ export const translations: Record> = { "chat.workspaceUnpin": "取消置顶", "chat.workspaceRename": "修改标题", "chat.workspaceRemove": "移除工作空间", + "chat.workspaceBrowseInFileTree": "在文件树浏览", + "chat.workspaceBrowseInSystemFileManager": "在资源管理器浏览", "chat.workspaceRemoveConfirm": "移除「{name}」?", "chat.workspaceRemoveRunning": "后台任务运行中,暂时不能移除。", "chat.workspaceRemoveDescription": "会删除此工作空间下的历史对话,不会删除文件夹。", + "chat.workspaceOpenSystemFileManagerFailed": "打开资源管理器失败", "chat.exitConfirmTitle": "退出 LiveAgent?", "chat.exitConfirmSubtitle": "当前仍有终端任务在运行。", "chat.exitConfirmRunningLabel": "正在运行的 Terminal", @@ -1186,10 +1189,13 @@ export const translations: Record> = { "chat.workspaceUnpin": "Unpin", "chat.workspaceRename": "Rename", "chat.workspaceRemove": "Remove workspace", + "chat.workspaceBrowseInFileTree": "Browse in File Tree", + "chat.workspaceBrowseInSystemFileManager": "Browse in System File Manager", "chat.workspaceRemoveConfirm": "Remove \"{name}\"?", "chat.workspaceRemoveRunning": "A background task is running, so this workspace cannot be removed yet.", "chat.workspaceRemoveDescription": "This deletes conversations under the workspace, but it does not delete the folder.", + "chat.workspaceOpenSystemFileManagerFailed": "Failed to open the file manager", "chat.exitConfirmTitle": "Exit LiveAgent?", "chat.exitConfirmSubtitle": "Terminal tasks are still running.", "chat.exitConfirmRunningLabel": "Running Terminal sessions", diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx index 16473f7f..6973998c 100644 --- a/crates/agent-gui/src/pages/ChatPage.tsx +++ b/crates/agent-gui/src/pages/ChatPage.tsx @@ -2,6 +2,7 @@ import type { Context, UserMessage } from "@earendil-works/pi-ai"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { getCurrentWebview } from "@tauri-apps/api/webview"; +import { revealItemInDir } from "@tauri-apps/plugin-opener"; import { type SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { MacOsTitleBarSpacer, MacOsTitleBarToggle } from "../components/MacOsTitleBarSpacer"; import { ChatHistorySidebar } from "../components/chat/ChatHistorySidebar"; @@ -1009,6 +1010,52 @@ export function ChatPage(props: ChatPageProps) { [activateWorkspaceProject, checkWorkspaceProjectDirectory], ); + const handleBrowseWorkspaceProjectInFileTree = useCallback( + async (project: WorkspaceProject) => { + if (!(await checkWorkspaceProjectDirectory(project))) { + return; + } + const pathKey = workspaceProjectPathKey(project.path); + if (!pathKey) { + return; + } + + setActiveView("chat"); + setProjectToolsPanelOpen(true); + activateWorkspaceProject(project); + setSettings((prev) => + updateProjectToolsFileTreeOpen( + updateCustomSettings(prev, { + projectToolsPanel: { + ...prev.customSettings.projectToolsPanel, + activeTab: "fileTree", + }, + }), + pathKey, + true, + ), + ); + }, + [activateWorkspaceProject, checkWorkspaceProjectDirectory, setSettings], + ); + + const handleBrowseWorkspaceProjectInSystemFileManager = useCallback( + async (project: WorkspaceProject) => { + if (!(await checkWorkspaceProjectDirectory(project))) { + return; + } + + try { + await revealItemInDir(project.path.trim()); + } catch (error) { + const message = asErrorMessage(error, t("chat.workspaceOpenSystemFileManagerFailed")); + setHistoryError(message); + setErrorMessage(message); + } + }, + [checkWorkspaceProjectDirectory, setErrorMessage, setHistoryError, t], + ); + const handleOpenCreateWorkspaceProject = useCallback(async () => { try { const picked = await invoke("system_pick_folder", { @@ -4094,6 +4141,8 @@ export function ChatPage(props: ChatPageProps) { onCreateProject={handleOpenCreateWorkspaceProject} onSelectProject={handleSelectWorkspaceProject} onNewConversationForProject={handleNewConversationForProject} + onBrowseProjectInFileTree={handleBrowseWorkspaceProjectInFileTree} + onBrowseProjectInSystemFileManager={handleBrowseWorkspaceProjectInSystemFileManager} onStartRenamingProject={handleStartRenamingWorkspaceProject} onProjectRenameDraftChange={setProjectRenameDraft} onCommitProjectRename={handleCommitWorkspaceProjectRename}