From 3c71712962ccd8b4148ec1cbdde1cd15f90dbeab Mon Sep 17 00:00:00 2001 From: xmanrui <841206367@qq.com> Date: Wed, 2 Sep 2026 23:18:28 +0800 Subject: [PATCH] fix: support current directory picker errors --- CHANGELOG.md | 5 + lib/client.js | 15 ++- .../client/workspace-directory-picker.js | 17 ++- test/workspace-editor.test.mjs | 125 ++++++++++++------ 4 files changed, 118 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eec0d05..b1dfb27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ This file records the notable changes in each dsh-im release. Its format follows ## [Unreleased] +### Fixed / 修复 + +- 机器人工作区目录选择器现在同时识别新版 DSH 的 `directory-picker/*` 错误码与旧版连字符错误码;native 后端会正确回退到系统目录选择器,已失效的保存路径也会回退到 Host 主目录。 + The bot workspace directory picker now recognizes both current DSH `directory-picker/*` error codes and legacy hyphenated codes, restoring the native system-picker fallback and the Host-home fallback for stale saved paths. + ## [4.7.0] - 2026-09-02 ### Added / 新增 diff --git a/lib/client.js b/lib/client.js index 2d47a27..7c6501c 100644 --- a/lib/client.js +++ b/lib/client.js @@ -2526,6 +2526,15 @@ var import_react_dom = require("react-dom"); function pickerErrorCode(error) { return error?.rpcError?.code ?? error?.code; } +var PICKER_ERROR_KINDS = /* @__PURE__ */ new Map([ + ["directory-picker/unavailable", "unavailable"], + ["directory-picker-unavailable", "unavailable"], + ["directory-picker/unreadable", "unreadable"], + ["directory-unreadable", "unreadable"] +]); +function pickerErrorKind(error) { + return PICKER_ERROR_KINDS.get(pickerErrorCode(error)); +} function pickerErrorDetails(error) { return error?.rpcError?.details ?? error?.details; } @@ -2633,9 +2642,9 @@ function WorkspaceDirectoryPicker({ const initialPath = initialPathRef.current; const initial = await loadDirectory(initialPath || void 0, { reportError: false }); if (!active || initial.aborted || initial.value) return; - const code = pickerErrorCode(initial.error); + const kind = pickerErrorKind(initial.error); const details = pickerErrorDetails(initial.error); - if (code === "directory-picker-unavailable" && details?.capability === "native" && typeof picker.pickDirectory === "function") { + if (kind === "unavailable" && details?.capability === "native" && typeof picker.pickDirectory === "function") { setLoading(true); try { const selected = await picker.pickDirectory(); @@ -2649,7 +2658,7 @@ function WorkspaceDirectoryPicker({ } return; } - if (initialPath && code === "directory-unreadable") { + if (initialPath && kind === "unreadable") { const home = await loadDirectory(void 0, { reportError: false }); if (!active || home.aborted || home.value) return; setError(pickerErrorMessage(home.error)); diff --git a/plugin-src/client/workspace-directory-picker.js b/plugin-src/client/workspace-directory-picker.js index 4846f1a..66bcda2 100644 --- a/plugin-src/client/workspace-directory-picker.js +++ b/plugin-src/client/workspace-directory-picker.js @@ -7,6 +7,17 @@ function pickerErrorCode(error) { return error?.rpcError?.code ?? error?.code; } +const PICKER_ERROR_KINDS = new Map([ + ['directory-picker/unavailable', 'unavailable'], + ['directory-picker-unavailable', 'unavailable'], + ['directory-picker/unreadable', 'unreadable'], + ['directory-unreadable', 'unreadable'], +]); + +function pickerErrorKind(error) { + return PICKER_ERROR_KINDS.get(pickerErrorCode(error)); +} + function pickerErrorDetails(error) { return error?.rpcError?.details ?? error?.details; } @@ -110,9 +121,9 @@ export function WorkspaceDirectoryPicker({ const initialPath = initialPathRef.current; const initial = await loadDirectory(initialPath || undefined, { reportError: false }); if (!active || initial.aborted || initial.value) return; - const code = pickerErrorCode(initial.error); + const kind = pickerErrorKind(initial.error); const details = pickerErrorDetails(initial.error); - if (code === 'directory-picker-unavailable' + if (kind === 'unavailable' && details?.capability === 'native' && typeof picker.pickDirectory === 'function') { setLoading(true); @@ -128,7 +139,7 @@ export function WorkspaceDirectoryPicker({ } return; } - if (initialPath && code === 'directory-unreadable') { + if (initialPath && kind === 'unreadable') { const home = await loadDirectory(undefined, { reportError: false }); if (!active || home.aborted || home.value) return; setError(pickerErrorMessage(home.error)); diff --git a/test/workspace-editor.test.mjs b/test/workspace-editor.test.mjs index 8b3b2e1..3814adb 100644 --- a/test/workspace-editor.test.mjs +++ b/test/workspace-editor.test.mjs @@ -46,23 +46,26 @@ function directoryListing(path, childNames = [], { home = '/workspace', truncate }; } -function nativeUnavailable() { +function nativeUnavailable({ + code = 'directory-picker/unavailable', + capability = 'native', +} = {}) { const error = new Error('Directory browsing is unavailable'); error.rpcError = { - code: 'directory-picker-unavailable', + code, message: error.message, - details: { capability: 'native' }, + details: { capability }, }; return error; } -function nativeDirectoryPicker(selected) { +function nativeDirectoryPicker(selected, unavailable = undefined) { const calls = { list: 0, pick: 0 }; return { calls, async listDirectory() { calls.list += 1; - throw nativeUnavailable(); + throw nativeUnavailable(unavailable); }, async pickDirectory() { calls.pick += 1; @@ -256,7 +259,7 @@ test('WorkspaceEditor keeps the prior folder unselectable when a typed path cann async listDirectory(path) { if (path === missing) { const error = new Error('cannot read requested folder'); - error.rpcError = { code: 'directory-unreadable', message: error.message, details: { path } }; + error.rpcError = { code: 'directory-picker/unreadable', message: error.message, details: { path } }; throw error; } return { @@ -387,6 +390,50 @@ test('WorkspaceEditor falls back to one native picker without restarting after s assert.equal(renderer.root.findAllByProps({ role: 'dialog' }).length, 0); }); +test('WorkspaceEditor keeps the legacy native unavailable code compatible', async () => { + const saved = []; + const picker = nativeDirectoryPicker('/workspace/legacy-native', { + code: 'directory-picker-unavailable', + }); + let renderer; + await act(async () => { + renderer = create(React.createElement(WorkspaceEditor, { + workspace: '/workspace/current', + directoryPicker: picker, + async onSave(value) { saved.push(value); }, + })); + }); + await act(async () => { + renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick(); + await flushMicrotasks(); + }); + + assert.deepEqual(saved, ['/workspace/legacy-native']); + assert.deepEqual(picker.calls, { list: 1, pick: 1 }); +}); + +test('WorkspaceEditor does not treat a non-native unavailable capability as native', async () => { + const picker = nativeDirectoryPicker('/workspace/must-not-pick', { + capability: 'browse', + }); + let renderer; + await act(async () => { + renderer = create(React.createElement(WorkspaceEditor, { + workspace: '/workspace/current', + directoryPicker: picker, + async onSave() { throw new Error('must not save'); }, + })); + }); + await act(async () => { + renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick(); + await flushMicrotasks(); + }); + + assert.deepEqual(picker.calls, { list: 1, pick: 0 }); + assert.equal(renderer.root.findAllByProps({ role: 'alert' }).length, 1); + assert.equal(renderer.root.findAllByProps({ role: 'dialog' }).length, 1); +}); + test('WorkspaceEditor treats native picker cancellation as cancellation, not an error', async () => { let saves = 0; const picker = nativeDirectoryPicker(null); @@ -409,40 +456,42 @@ test('WorkspaceEditor treats native picker cancellation as cancellation, not an assert.equal(renderer.root.findAllByProps({ role: 'dialog' }).length, 0); }); -test('WorkspaceEditor falls back to the Host home when the saved path is unreadable', async () => { - const listed = []; - const saved = []; - const picker = { - async listDirectory(path) { - listed.push(path); - if (path === '/workspace/gone') { - const error = new Error('missing'); - error.rpcError = { code: 'directory-unreadable', message: 'missing', details: { path } }; - throw error; - } - return directoryListing('/workspace', ['projects']); - }, - }; - let renderer; - await act(async () => { - renderer = create(React.createElement(WorkspaceEditor, { - workspace: '/workspace/gone', - directoryPicker: picker, - async onSave(value) { saved.push(value); }, - })); - }); - await act(async () => { - renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick(); - await flushMicrotasks(); - }); +for (const unreadableCode of ['directory-picker/unreadable', 'directory-unreadable']) { + test(`WorkspaceEditor falls back to the Host home for ${unreadableCode}`, async () => { + const listed = []; + const saved = []; + const picker = { + async listDirectory(path) { + listed.push(path); + if (path === '/workspace/gone') { + const error = new Error('missing'); + error.rpcError = { code: unreadableCode, message: 'missing', details: { path } }; + throw error; + } + return directoryListing('/workspace', ['projects']); + }, + }; + let renderer; + await act(async () => { + renderer = create(React.createElement(WorkspaceEditor, { + workspace: '/workspace/gone', + directoryPicker: picker, + async onSave(value) { saved.push(value); }, + })); + }); + await act(async () => { + renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick(); + await flushMicrotasks(); + }); - assert.deepEqual(listed, ['/workspace/gone', undefined]); - await act(async () => { - renderer.root.findByProps({ className: 'dim-directoryPickerPrimary' }).props.onClick(); - await flushMicrotasks(); + assert.deepEqual(listed, ['/workspace/gone', undefined]); + await act(async () => { + renderer.root.findByProps({ className: 'dim-directoryPickerPrimary' }).props.onClick(); + await flushMicrotasks(); + }); + assert.deepEqual(saved, ['/workspace']); }); - assert.deepEqual(saved, ['/workspace']); -}); +} test('WorkspaceEditor moves keyboard focus into and back out of the picker', async () => { let dialogFocus = 0;