Fix kbRoot browse ignoring Typert Result from directoryPicker.pick.

Unwrap { ok, value } so the chosen folder path lands in the field and auto-saves.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-13 11:22:58 +08:00
parent f64f1279e7
commit fcdd11ac59
4 changed files with 165 additions and 57 deletions

View file

@ -412,13 +412,14 @@ export class NetxopsCardController {
this.kbUiError = null
this.store.set(this.projection())
const applyPath = (path: unknown): void => {
if (typeof path !== 'string' || path.trim() === '') {
// Cancel / empty — no error
return
}
this.form.actions().edit('kbRoot', path.trim())
const applyPath = (path: string): void => {
this.form.actions().edit('kbRoot', path)
void this.refreshKbPreview()
// Selecting a folder should land immediately — don't require a separate Save.
void this.form.save().then(() => {
void this.refreshKbStatus()
void this.refreshKbPreview()
})
}
const fail = (error: unknown): void => {
@ -426,10 +427,24 @@ export class NetxopsCardController {
this.store.set(this.projection())
}
/**
* `remote.directoryPicker.pick()` returns a Typert Result `{ ok, value }`,
* not a bare string (see dsh ui-workspace Navigation.pickDirectory).
*/
const acceptPickResult = (result: unknown): void => {
const path = unwrapDirectoryPickResult(result)
if (path === null) return // user cancelled
if (path === undefined) {
fail(`unexpected directoryPicker result: ${safeJson(result)}`)
return
}
applyPath(path)
}
const picker = this.kbDirectoryPicker
if (picker !== undefined) {
void picker.pick()
.then(applyPath)
.then(acceptPickResult)
.catch(fail)
.finally(() => {
this.kbBrowseInFlight = false
@ -447,25 +462,7 @@ export class NetxopsCardController {
return
}
void call('/api', 'directoryPicker/pick', { args: {} })
.then((result) => {
// Remotes often return the path directly, or { ok, value }.
if (typeof result === 'string' || result === null) {
applyPath(result)
return
}
if (result !== null && typeof result === 'object') {
const row = result as { ok?: boolean; value?: unknown; error?: { message?: string } }
if (row.ok === false) {
fail(row.error?.message || 'directoryPicker/pick failed')
return
}
if ('value' in row) {
applyPath(row.value)
return
}
}
applyPath(result)
})
.then(acceptPickResult)
.catch(fail)
.finally(() => {
this.kbBrowseInFlight = false
@ -625,3 +622,41 @@ function refOf(snapshot: SettingsScopeSnapshot<NetxopsSettings>): string {
const declared = snapshot.value?.tokenCredentialRef
return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF
}
/**
* Normalize directoryPicker pick outcomes.
* @returns trimmed path; `null` = cancelled; `undefined` = unparseable.
*/
function unwrapDirectoryPickResult(result: unknown): string | null | undefined {
if (result === null) return null
if (typeof result === 'string') {
const trimmed = result.trim()
return trimmed === '' ? null : trimmed
}
if (result === null || typeof result !== 'object' || Array.isArray(result)) return undefined
const row = result as {
ok?: boolean
value?: unknown
error?: { message?: string }
}
if (row.ok === false) {
throw new Error(row.error?.message || 'directoryPicker/pick failed')
}
if ('value' in row) {
if (row.value === null) return null
if (typeof row.value === 'string') {
const trimmed = row.value.trim()
return trimmed === '' ? null : trimmed
}
return undefined
}
return undefined
}
function safeJson(value: unknown): string {
try {
return JSON.stringify(value)
} catch {
return String(value)
}
}

View file

@ -92,11 +92,11 @@ export const en: Record<NetxopsLocaleKey, string> = {
groupInPreset: 'In Netx Ops preset',
groupPublic: 'Publish to other presets',
kbRoot: 'Package root',
kbRootHint: 'Folder with MANIFEST.json (operator-subset v1.0). Empty = pure netx.',
kbRootHint: 'Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves.',
kbBrowse: 'Browse…',
kbBrowseUnavailable: 'Directory picker unavailable — paste an absolute folder path, then Save.',
kbBrowseFailed: 'Browse failed: {detail}',
kbSaveHint: 'Click Save after choosing a path to apply the knowledge base.',
kbSaveHint: 'Browse writes and saves automatically. Manual paste still needs Save.',
kbStatusConfigured: 'Knowledge base: {operator} ({country}) v{version}',
kbStatusUnconfigured: 'Knowledge base: not configured (pure netx)',
kbStatusError: 'Knowledge base: error — {detail}',
@ -165,11 +165,11 @@ export const zh: Record<NetxopsLocaleKey, string> = {
groupInPreset: '在 Netx Ops 预设中启用',
groupPublic: '对其他预设公开',
kbRoot: '知识包根目录',
kbRootHint: '含 MANIFEST.json 的运营商子集包(v1.0)。留空=纯 netx。',
kbRootHint: '含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存。',
kbBrowse: '浏览…',
kbBrowseUnavailable: '目录选择器不可用 — 请粘贴绝对目录路径后点保存。',
kbBrowseFailed: '浏览失败:{detail}',
kbSaveHint: '选好路径后请点「保存」,知识库才会生效。',
kbSaveHint: '点「浏览」选目录后会自动写入并保存;若是手粘路径仍需点保存。',
kbStatusConfigured: '知识库: {operator}({country}) v{version}',
kbStatusUnconfigured: '知识库: 未配置(纯 netx)',
kbStatusError: '知识库: 错误 — {detail}',