feat: workspace fence toggle (workspaceFence pref + one-click off in error surfaces)
- New persisted built-in pref workspaceFence (default true) under the files card's secondary settings modal; host reads it per request via the settings seam (fenceEnabledOf — absent/malformed stays ON). - ensureWorkspacePath/ensureWorkspaceWritePath gain a fence param: disarmed skips the containment assert but keeps realpath resolution; wired through fs.tree/fs.read/fs.write, the media + html routes, and the upload path (writeWorkspaceUpload fence input). - Editor load failures and file-tree level errors show the localized reason plus a one-click global off (FenceErrorNotice) that flips the pref, adopts it into the store, and auto-retries the failed operation (reloadSeq / retryDir) — the raw wire text is never shown. - GitView's two client-side pre-checks (hidden open action + no-op defense) respect the pref; store threaded through TreePanel/FileTree. - i18n: 4 new keys across all 21 dictionaries; tests: fence-off route coverage, prefs parsing, schema default, editor toggles list; docs: guide §8/§8.1 + plans/2026-08-29 design record.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* The workspace-fence refusal surface. The raw wire text (`path "..." is
|
||||
* outside workspace`) is never shown as-is: the editor / file-tree error
|
||||
* slots render the localized reason plus a one-click global off — the click
|
||||
* flips the `workspaceFence` pref through the settings route, adopts the
|
||||
* returned document into the store (so every prefs reader — GitView's open
|
||||
* guard, the settings page — flips with it), and calls `onDisabled` so the
|
||||
* caller retries the failed operation immediately.
|
||||
*/
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { api } from './api.ts'
|
||||
import { t } from './locales.ts'
|
||||
import { parsePrefs } from './prefs.ts'
|
||||
import css from './sidebar.module.css'
|
||||
import type { SidebarStore } from './state.ts'
|
||||
|
||||
export function FenceErrorNotice(props: { store: SidebarStore; onDisabled: () => void }) {
|
||||
const { store, onDisabled } = props
|
||||
const [busy, setBusy] = useState(false)
|
||||
const disable = (): void => {
|
||||
if (busy) return
|
||||
setBusy(true)
|
||||
api.settingsUpdate({ workspaceFence: false }).then((view) => {
|
||||
store.setPrefs(parsePrefs(view.value))
|
||||
onDisabled()
|
||||
}).catch((error: unknown) => {
|
||||
// The fence stays armed on failure (the notice remains, button
|
||||
// re-enabled); the console carries the cause.
|
||||
console.error('workspace fence disable failed', error)
|
||||
setBusy(false)
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className={css.fenceError}>
|
||||
<span>{t('fenceErrorReason')}</span>
|
||||
<Button variant="outline" disabled={busy} onClick={disable}>{t('fenceDisableAction')}</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user