diff --git a/src/servers/api/file-browser/router.ts b/src/servers/api/file-browser/router.ts
index a8a3f4fb..f3a602e1 100644
--- a/src/servers/api/file-browser/router.ts
+++ b/src/servers/api/file-browser/router.ts
@@ -1484,8 +1484,22 @@ router.post('/download', async (ctx) => {
items.push(relPath);
}
- const proc = Bun.spawn(['zip', '-r', '-', ...items], {
- cwd: rootDir,
+ // Zip from the items' own folder, not from the home root.
+ //
+ // `cwd: rootDir` with paths like `Tests/test_folder` put the whole chain from home inside the archive —
+ // unzip it and you got `Tests/test_folder/...` rather than `test_folder/...`. Nobody selecting three
+ // files in a folder is asking for the path to that folder to be part of what they downloaded.
+ //
+ // A mixed selection cannot collapse that way without colliding (two `notes.txt` from different folders
+ // would overwrite), so it keeps the root-relative form, where the paths are what keeps them apart. The
+ // UI only ever selects within one folder, so the second branch is the safety net rather than the case.
+ const parents = new Set(items.map((i) => dirname(resolveUserPath(rootDir, i))));
+ const sameFolder = parents.size === 1;
+ const cwd = sameFolder ? [...parents][0]! : rootDir;
+ const args = sameFolder ? items.map((i) => i.split('/').pop()!) : items;
+
+ const proc = Bun.spawn(['zip', '-r', '-', ...args], {
+ cwd,
stdout: 'pipe',
stderr: 'ignore',
});
diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileViewContainer.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileViewContainer.tsx
index b1520c82..2a46ce74 100644
--- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileViewContainer.tsx
+++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileViewContainer.tsx
@@ -13,6 +13,7 @@ import {
Download,
Mic,
ExternalLink,
+ FolderSearch,
GitBranch,
Eye,
EyeOff,
@@ -39,6 +40,7 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
searching,
searchResults,
handleSearchResultClick,
+ handleShowLocation,
dragging,
handleDragEnter,
handleDragLeave,
@@ -208,6 +210,12 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
Open
+ {/* The other question a search result raises. Clicking it opens it; this goes
+ to the folder it lives in and selects it there, leaving nothing open. */}
+ handleShowLocation(entry)} className="cursor-pointer">
+
+ Show location
+
{absPath && (
handleCopyAbsPath(absPath)} className="cursor-pointer">
diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts
index 3029d1a0..359df8ef 100644
--- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts
+++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts
@@ -4,6 +4,7 @@ import { toast } from 'sonner';
import { useFilesAPI, type DirEntry } from '../../../hooks/useFilesAPI';
import { usePinnedFiles } from '../usePinnedFiles';
import { EDITOR_FILE_PARAM } from '../../CodeEditor/useEditorState';
+import { getFileType } from '../../FileViewer';
import { useTasks, type TaskSummary } from '../useTasks';
import { useAgents, type AgentSummary } from '../useAgents';
import { useUserState } from 'state/useUserState';
@@ -294,6 +295,23 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
setSearchQuery('');
};
+ /**
+ * Go to a search hit's folder and select it, without opening anything.
+ *
+ * Clicking a result opens it, which answers "show me this file" but not "where does this live" — and a
+ * search result's whole context is the path under its name. This is the second question, and it is the
+ * one a `Reveal in Finder` answers everywhere else.
+ */
+ const handleShowLocation = (entry: DirEntry) => {
+ if (!entry.path) return;
+ // The containing folder either way — a directory hit reveals its PARENT, same as a file, because
+ // "where is this" means the folder it sits in, not the folder it is.
+ setCurrentPath(entry.path.substring(0, entry.path.lastIndexOf('/')) || '/');
+ setSearchQuery('');
+ // Selected rather than merely listed, so a folder of forty files does not leave you hunting for it.
+ setSelected(new Set([entry.name]));
+ };
+
const handleNavigate = (path: string) => {
if (basePath !== '/' && !path.startsWith(basePath)) {
setCurrentPath(basePath);
@@ -307,10 +325,18 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
if (entry.type === 'directory') {
const next = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`;
setCurrentPath(next);
- } else {
- const filePath = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`;
- setViewerParams({ view: filePath });
+ return;
}
+ // An archive extracts rather than opening a pane. The viewer has nothing to show for one — it renders
+ // a placeholder reading "Archive file" and the name you can already see in the listing — so a pane
+ // was a step on the way to the only thing anyone opens a zip for. Extract resolves collisions, so a
+ // second double-click makes a sibling folder rather than overwriting the first.
+ if (getFileType(entry.name) === 'archive') {
+ void handleExtract(entry);
+ return;
+ }
+ const filePath = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`;
+ setViewerParams({ view: filePath });
};
const handleCreateDir = async (name: string) => {
@@ -866,6 +892,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
fileScrollRef,
// Handlers
handleSearchResultClick,
+ handleShowLocation,
handleNavigate,
handleOpen,
handleCreateDir,