@@ -270,7 +295,7 @@ export const Markdown = memo(function Markdown(props: MarkdownProps) {
)}
plugins={streamdownPlugins}
remarkPlugins={remarkPlugins}
- components={markdownComponents}
+ components={readOnly ? markdownReadOnlyComponents : markdownComponents}
mode={isAnimating ? "streaming" : "static"}
dir="auto"
parseIncompleteMarkdown
@@ -279,14 +304,14 @@ export const Markdown = memo(function Markdown(props: MarkdownProps) {
caret={isAnimating ? "block" : undefined}
animated={false}
linkSafety={{
- enabled: true,
+ enabled: !readOnly,
renderModal: (modalProps) =>
,
}}
{...(isAnimating ? {} : { shikiTheme: ["github-light", "github-dark"] as const })}
controls={{
- code: { copy: true, download: false },
- mermaid: { copy: true, download: false, fullscreen: true, panZoom: true },
- table: { copy: true, download: false, fullscreen: true },
+ code: { copy: !readOnly, download: false },
+ mermaid: { copy: !readOnly, download: false, fullscreen: !readOnly, panZoom: !readOnly },
+ table: { copy: !readOnly, download: false, fullscreen: !readOnly },
}}
translations={streamdownTranslations}
>
diff --git a/crates/agent-gui/src/components/chat/fileTypeIcons.tsx b/crates/agent-gui/src/components/chat/fileTypeIcons.tsx
index 1a97826e..8316bf73 100644
--- a/crates/agent-gui/src/components/chat/fileTypeIcons.tsx
+++ b/crates/agent-gui/src/components/chat/fileTypeIcons.tsx
@@ -167,6 +167,8 @@ const EXT_ICON: Record
= {
kts: FileTypeKotlin,
dart: FileTypeDart,
log: FileTypeLog,
+ csv: FileTypeExcel,
+ tsv: FileTypeExcel,
xls: FileTypeExcel,
xlsx: FileTypeExcel,
doc: FileTypeWord,
@@ -267,6 +269,8 @@ const EXT_ICON_SVG = {
kts: FileTypeKotlinSvg,
dart: FileTypeDartSvg,
log: FileTypeLogSvg,
+ csv: FileTypeExcelSvg,
+ tsv: FileTypeExcelSvg,
xls: FileTypeExcelSvg,
xlsx: FileTypeExcelSvg,
doc: FileTypeWordSvg,
diff --git a/crates/agent-gui/src/components/project-tools/ProjectFileTreePanel.tsx b/crates/agent-gui/src/components/project-tools/ProjectFileTreePanel.tsx
index f408e34f..11f6852f 100644
--- a/crates/agent-gui/src/components/project-tools/ProjectFileTreePanel.tsx
+++ b/crates/agent-gui/src/components/project-tools/ProjectFileTreePanel.tsx
@@ -9,10 +9,11 @@ import {
ChevronRight,
Copy,
Edit3,
+ ExternalLink,
+ Eye,
FilePenLine,
Folder,
FolderOpen,
- ImageIcon,
Loader2,
Plus,
RefreshCw,
@@ -23,7 +24,10 @@ import {
import { Button } from "../ui/button";
import { useConfirmDialog } from "../ui/confirm-dialog";
import { Input } from "../ui/input";
-import { isWorkspaceImagePath } from "../workspace-editor/workspaceImagePreview";
+import {
+ isWorkspaceEditablePreviewPath,
+ isWorkspacePreviewPath,
+} from "../workspace-editor/workspaceImagePreview";
type FileTreeKind = "file" | "dir";
@@ -66,6 +70,7 @@ type ContextMenuState = {
const ROOT_PATH = "";
const DEFAULT_MAX_RESULTS = 1000;
const SEARCH_MAX_RESULTS = 80;
+const FILE_TREE_AUTO_REFRESH_MS = 3000;
function basename(path: string) {
const normalized = path.replace(/\\/g, "/").replace(/\/+$/, "");
@@ -184,11 +189,21 @@ export function ProjectFileTreePanel(props: {
const selectedNode = state.nodes[state.selectedPath] ?? state.nodes[ROOT_PATH];
const selectedPath = selectedNode?.path ?? ROOT_PATH;
const canMutate = initialized && Boolean(projectPathKey && cwd);
+ const stateRef = useRef(state);
+ const queryRef = useRef(query);
useEffect(() => {
onSyncStateChangeRef.current = onSyncStateChange;
}, [onSyncStateChange]);
+ useEffect(() => {
+ stateRef.current = state;
+ }, [state]);
+
+ useEffect(() => {
+ queryRef.current = query;
+ }, [query]);
+
const setProjectState = useCallback(
(updater: (state: FileTreeState) => FileTreeState) => {
if (!projectPathKey) return;
@@ -208,7 +223,7 @@ export function ProjectFileTreePanel(props: {
}, []);
const loadChildren = useCallback(
- async (path: string, options?: { force?: boolean }) => {
+ async (path: string, options?: { force?: boolean; silent?: boolean }) => {
if (!projectPathKey || !cwd.trim()) return;
let shouldLoad = true;
setProjectState((current) => {
@@ -217,6 +232,10 @@ export function ProjectFileTreePanel(props: {
shouldLoad = false;
return current;
}
+ if (node.loading && options?.silent) {
+ shouldLoad = false;
+ return current;
+ }
if ((node.loaded || node.loading) && !options?.force) {
shouldLoad = false;
return current;
@@ -226,7 +245,11 @@ export function ProjectFileTreePanel(props: {
initialized: true,
nodes: {
...current.nodes,
- [path]: { ...node, loading: true, error: undefined },
+ [path]: {
+ ...node,
+ loading: options?.silent ? node.loading : true,
+ error: options?.silent ? node.error : undefined,
+ },
},
};
});
@@ -281,7 +304,9 @@ export function ProjectFileTreePanel(props: {
[path]: {
...node,
loading: false,
- error: toErrorMessage(error, t("projectTools.fileTree.readFailed")),
+ error: options?.silent
+ ? node.error
+ : toErrorMessage(error, t("projectTools.fileTree.readFailed")),
},
},
};
@@ -345,6 +370,26 @@ export function ProjectFileTreePanel(props: {
void loadChildren(ROOT_PATH);
}, [initialized, loadChildren, projectPathKey]);
+ useEffect(() => {
+ if (!initialized || !projectPathKey || !cwd.trim()) return;
+ const interval = window.setInterval(() => {
+ const snapshot = stateRef.current;
+ const pathsToReload = Array.from(new Set([ROOT_PATH, ...snapshot.expanded])).filter(
+ (path) => {
+ const node = snapshot.nodes[path];
+ return node?.kind === "dir" && node.loaded;
+ },
+ );
+ for (const path of pathsToReload) {
+ void loadChildren(path, { force: true, silent: true });
+ }
+ if (queryRef.current.trim()) {
+ setSearchRefreshKey((current) => current + 1);
+ }
+ }, FILE_TREE_AUTO_REFRESH_MS);
+ return () => window.clearInterval(interval);
+ }, [cwd, initialized, loadChildren, projectPathKey]);
+
useEffect(() => {
void projectPathKey;
setContextMenu(null);
@@ -468,7 +513,7 @@ export function ProjectFileTreePanel(props: {
syncFileTreeState({ selectedPath: targetPath, bumpStateVersion: true });
const rect = panelRef.current?.getBoundingClientRect();
const menuWidth = 220;
- const menuHeight = targetKind === "file" ? 292 : 260;
+ const menuHeight = targetKind === "file" ? 360 : 294;
const panelLeft = rect?.left ?? 0;
const panelTop = rect?.top ?? 0;
const panelWidth = rect?.width ?? window.innerWidth;
@@ -675,6 +720,42 @@ export function ProjectFileTreePanel(props: {
[selectedPath],
);
+ const openContainingDirectory = useCallback(
+ async (targetPath = selectedPath) => {
+ if (!targetPath) return;
+ setActionError(null);
+ try {
+ await invoke("fs_open_workspace_path", {
+ workdir: cwd,
+ path: targetPath,
+ mode: "reveal",
+ });
+ } catch (error) {
+ setActionError(
+ toErrorMessage(error, t("projectTools.fileTree.openContainingDirectoryFailed")),
+ );
+ }
+ },
+ [cwd, selectedPath, t],
+ );
+
+ const openExternalFile = useCallback(
+ async (targetPath = selectedPath) => {
+ if (!targetPath) return;
+ setActionError(null);
+ try {
+ await invoke("fs_open_workspace_path", {
+ workdir: cwd,
+ path: targetPath,
+ mode: "open",
+ });
+ } catch (error) {
+ setActionError(toErrorMessage(error, t("projectTools.fileTree.openExternalFailed")));
+ }
+ },
+ [cwd, selectedPath, t],
+ );
+
const insertMention = useCallback(
(targetPath = selectedPath) => {
const targetNode = state.nodes[targetPath];
@@ -964,17 +1045,32 @@ export function ProjectFileTreePanel(props: {
setContextMenu(null);
}}
>
- {isWorkspaceImagePath(contextPath) ? (
-
+ {isWorkspacePreviewPath(contextPath) ? (
+
) : (
)}
{t(
- isWorkspaceImagePath(contextPath)
- ? "projectTools.fileTree.previewImage"
+ isWorkspacePreviewPath(contextPath)
+ ? "projectTools.fileTree.previewFile"
: "projectTools.fileTree.openFile",
)}
+ {!isWorkspaceEditablePreviewPath(contextPath) ? (
+
+ ) : null}
>
) : null}
@@ -1046,6 +1142,19 @@ export function ProjectFileTreePanel(props: {
? t("projectTools.fileTree.copiedPath")
: t("projectTools.fileTree.copyPath")}
+
}
>
-