From a74c8c08a11e57f67da25daf971cccd788a319d1 Mon Sep 17 00:00:00 2001 From: Yulin Date: Fri, 21 Aug 2026 14:42:00 +0800 Subject: [PATCH] feat(workspace): add "Open in Finder" to workspace menu Add an "Open in Finder" entry to the workspace three-dot menu so users can open a workspace folder in macOS Finder. The native reveal is backed by a new `harness:open-in-finder` IPC handler (shell.openPath) exposed on the `window.dshDesktop` bridge, and the workspace UI only shows the item when that bridge is available (i.e. inside DSH Desktop, not in a plain browser). --- ...i+dsh-client-ui-workspace+0.1.0-rc.8.patch | 61 +++++++++++++++++++ src/main/index.ts | 9 +++ src/preload/index.ts | 3 +- 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 patches/@deepseek-ai+dsh-client-ui-workspace+0.1.0-rc.8.patch diff --git a/patches/@deepseek-ai+dsh-client-ui-workspace+0.1.0-rc.8.patch b/patches/@deepseek-ai+dsh-client-ui-workspace+0.1.0-rc.8.patch new file mode 100644 index 00000000..be6029c2 --- /dev/null +++ b/patches/@deepseek-ai+dsh-client-ui-workspace+0.1.0-rc.8.patch @@ -0,0 +1,61 @@ +diff --git a/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/client.js b/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/client.js +index 7ca5fd3..4bba9a6 100644 +--- a/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/client.js ++++ b/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/client.js +@@ -456,7 +456,14 @@ window.__ModuleLoader__.load({ + const label = row.workspaceId === void 0 ? t("group.ungrouped") : row.label; + const active = group.expanded && group.containsCurrent; + const [menuOpen, setMenuOpen] = (0, react.useState)(false); +- const workspaceMenuItems = [{ ++ const canOpenInFinder = typeof window !== "undefined" && typeof window.dshDesktop?.openInFinder === "function"; ++ const workspaceMenuItems = []; ++ if (canOpenInFinder) workspaceMenuItems.push({ ++ id: "openInFinder", ++ label: t("menu.openInFinder"), ++ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpen16, {}) ++ }); ++ workspaceMenuItems.push({ + id: "rename", + label: t("rename"), + icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {}) +@@ -465,7 +472,7 @@ window.__ModuleLoader__.load({ + label: t("delete.workspace"), + icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}), + danger: true +- }]; ++ }); + const ownRow = (0, react_jsx_runtime.jsxs)("div", { + className: clsx(Rows_module_css_default.projectRow, menuOpen && Rows_module_css_default.menuOpen), + role: "treeitem", +@@ -504,7 +511,14 @@ window.__ModuleLoader__.load({ + items: workspaceMenuItems, + onSelect: (id) => { + setMenuOpen(false); +- /* v8 ignore next -- workspaceMenuItems carries exactly these two rows today. */ ++ if (id === "openInFinder") { ++ if (row.cwd !== void 0 && typeof window.dshDesktop?.openInFinder === "function") { ++ window.dshDesktop.openInFinder(row.cwd).catch((reason) => { ++ console.warn("open in finder rejected:", reason); ++ }); ++ } ++ return; ++ } + if (id !== "rename" && id !== "delete") return; + if (id === "rename") actions.rename(); + else actions.delete(); +@@ -2212,6 +2226,7 @@ window.__ModuleLoader__.load({ + "search.noMatches": "无匹配会话", + "search.hasMore": "仅显示前 {n} 条结果,请缩小搜索范围。", + "menu.addWorkspace": "添加工作区…", ++ "menu.openInFinder": "在 Finder 中打开", + "picker.loading": "正在加载工作区…", + "conflict.named": "已存在名为“{name}”的工作区。", + "folderError.title": "无法打开文件夹", +@@ -2277,6 +2292,7 @@ window.__ModuleLoader__.load({ + "search.noMatches": "No matching sessions", + "search.hasMore": "Showing the first {n} results. Narrow your search.", + "menu.addWorkspace": "Add workspace…", ++ "menu.openInFinder": "Open in Finder", + "picker.loading": "Loading workspaces…", + "conflict.named": "A workspace named “{name}” already exists.", + "folderError.title": "Couldn’t open folder", diff --git a/src/main/index.ts b/src/main/index.ts index ac510712..9859e0c1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -990,6 +990,15 @@ async function bootstrap(): Promise { ipcMain.handle('harness:show-log', () => { shell.showItemInFolder(join(app.getPath('logs'), 'harness.log')) }) + ipcMain.handle('harness:open-in-finder', async (event, path?: unknown) => { + assertTrustedMainWindowEvent(event) + if (typeof path !== 'string' || path.length === 0) { + throw new Error('A directory path is required.') + } + const errorMessage = await shell.openPath(path) + if (errorMessage) throw new Error(errorMessage) + return { ok: true } + }) ipcMain.removeHandler('harness:open-recovery') ipcMain.handle('harness:open-recovery', async (event, frontendErrorMessage?: unknown) => { assertTrustedMainWindowEvent(event) diff --git a/src/preload/index.ts b/src/preload/index.ts index 93fc3985..e780cdba 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -174,7 +174,8 @@ window.addEventListener('unhandledrejection', (event) => { contextBridge.exposeInMainWorld( 'dshDesktop', Object.freeze({ - restartHarness: (): Promise<{ ok: boolean }> => ipcRenderer.invoke('harness:restart') + restartHarness: (): Promise<{ ok: boolean }> => ipcRenderer.invoke('harness:restart'), + openInFinder: (path: string): Promise<{ ok: boolean }> => ipcRenderer.invoke('harness:open-in-finder', path) }) )