mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-08 22:00:50 +08:00
fix: support current directory picker errors
This commit is contained in:
parent
88ef12ecc8
commit
3c71712962
4 changed files with 118 additions and 44 deletions
|
|
@ -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 / 新增
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue