From e18f67cb7af60ba7c790330ce81c7754e6344f2e Mon Sep 17 00:00:00 2001 From: juan <2930882+juacker@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:35:05 +0200 Subject: [PATCH] feat(artifacts): open binary files & the folder with the OS app Two artifacts-drawer improvements: 1. Binary/rich files (pdf, office docs, images, archives, media, db, fonts, binaries) now open in the OS default application instead of the in-app text preview, which failed with a UTF-8 error. viewer_for_path classifies these as "external"; clicking such a tree row launches the OS app (open_workspace_path target "system", from the #80 machinery). The preview error state also gains an "Open with the system app" button for unrecognized binary extensions that still fall through. 2. New "open the workspace folder in your file browser" icon in the artifacts drawer header (Files/Explorer/Finder), alongside the existing open-in-editor / open-in-terminal actions. --- src-tauri/src/commands/workspace.rs | 46 +++++++++++++++++++ .../WorkspaceFilePreviewPanel.module.css | 15 ++++++ src/components/WorkspaceFilePreviewPanel.tsx | 26 ++++++++++- src/pages/Workspace.tsx | 41 ++++++++++++++++- 4 files changed, 124 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/commands/workspace.rs b/src-tauri/src/commands/workspace.rs index da5fa72..e7d222e 100644 --- a/src-tauri/src/commands/workspace.rs +++ b/src-tauri/src/commands/workspace.rs @@ -476,9 +476,42 @@ fn viewer_for_path(path: &Path) -> String { return "json".to_string(); } + // Binary / rich formats the in-app preview can't render as UTF-8 text. + // The frontend treats "external" by launching the OS default app for the + // file (see `open_workspace_path` target "system") instead of opening the + // text preview panel, which would otherwise fail with a UTF-8 error. + if is_external_viewer_ext(&ext) { + return "external".to_string(); + } + "text".to_string() } +/// Extensions whose files are opened with the OS default application rather +/// than the in-app text preview. Kept deliberately broad across documents, +/// archives, media, and binaries — anything a dev workspace might hold that +/// is not UTF-8 text. +fn is_external_viewer_ext(ext: &str) -> bool { + matches!( + ext, + // Documents + "pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" | "odt" | "ods" + | "odp" | "rtf" | "epub" + // Images (no inline image preview yet — open externally) + | "png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp" | "ico" | "tiff" | "tif" | "avif" + // Audio / video + | "mp3" | "wav" | "flac" | "ogg" | "m4a" | "aac" + | "mp4" | "mov" | "mkv" | "webm" | "avi" | "wmv" | "m4v" + // Archives + | "zip" | "tar" | "gz" | "tgz" | "bz2" | "xz" | "7z" | "rar" | "zst" + // Binaries / other + | "exe" | "dll" | "so" | "dylib" | "bin" | "o" | "a" + | "wasm" | "class" | "jar" + | "sqlite" | "db" | "parquet" + | "woff" | "woff2" | "ttf" | "otf" | "eot" + ) +} + /// Best-effort MIME type from a file extension, used to build `data:` URIs /// when inlining preview resources. Falls back to `application/octet-stream` /// for unknown extensions — browsers still load most binary assets from a @@ -3206,4 +3239,17 @@ mod tests { ))); assert!(!is_protected_artifact_path(Path::new("images/pic.png"))); } + + #[test] + fn viewer_for_path_classifies_binary_as_external() { + use std::path::Path; + assert_eq!(viewer_for_path(Path::new("report.pdf")), "external"); + assert_eq!(viewer_for_path(Path::new("photo.PNG")), "external"); + assert_eq!(viewer_for_path(Path::new("archive.zip")), "external"); + assert_eq!(viewer_for_path(Path::new("data.sqlite")), "external"); + // Text/known viewers are unchanged. + assert_eq!(viewer_for_path(Path::new("notes.md")), "markdown"); + assert_eq!(viewer_for_path(Path::new("main.rs")), "text"); + assert_eq!(viewer_for_path(Path::new("data.json")), "json"); + } } diff --git a/src/components/WorkspaceFilePreviewPanel.module.css b/src/components/WorkspaceFilePreviewPanel.module.css index 093a48b..0e2b45e 100644 --- a/src/components/WorkspaceFilePreviewPanel.module.css +++ b/src/components/WorkspaceFilePreviewPanel.module.css @@ -300,3 +300,18 @@ background: var(--color-bg-elevated); } +.openExternalButton { + margin-top: 12px; + padding: 6px 12px; + font-size: 12px; + border: 1px solid var(--color-border-light); + border-radius: var(--radius-sm); + background: var(--color-bg-elevated); + color: var(--color-text-primary); + cursor: pointer; +} + +.openExternalButton:hover { + border-color: var(--color-primary); + color: var(--color-primary); +} diff --git a/src/components/WorkspaceFilePreviewPanel.tsx b/src/components/WorkspaceFilePreviewPanel.tsx index 543ee7d..f006abd 100644 --- a/src/components/WorkspaceFilePreviewPanel.tsx +++ b/src/components/WorkspaceFilePreviewPanel.tsx @@ -305,10 +305,25 @@ const renderBody = ( htmlBundle: string | null, bundling: boolean, onMarkdownLinkClick: (event: React.MouseEvent) => void, + onOpenExternal: () => void, ) => { if (!file) return null; if (file.error) { - return