/** * dsh-facet — the browser half. * * **Why this file exists when the host half already declares the schema.** The * Plugins page does not generate a card from a `Config` schema. It draws whatever * browser halves register into the slots it declares, and `plugins.bundle.config` * is the seat for one package's own configuration. Marking a field `volatile()` * makes the Host *serve* that namespace — `settings.describe()` drops an entry * with no volatile field, and `configForms.whileServed()` below refuses to * register a page the Host does not serve — but serving it is not drawing it. No * browser half means no card, however the schema is marked. * * **The two keys below are not the same string, and that is the trap.** Every * other plugin in this installation happens to name them identically, so the * distinction never shows up until it does: * * * the settings DOCUMENT is filed under the plugin's profile entry id — * `facet-policy`, the `id` of this bundle's row in `cordis.patch.yml`. * Exporting `Config` from `index.js` is the registration; this is the key * `configForms.get()` addresses. * * the CARD is filed under the bundle's PACKAGE NAME — `dsh-facet`. That is * what the Plugins page dispatches `plugins.bundle.config` by, and it goes on * the bundle's own page between its description and its rows. * * Bind the form to the wrong one and the card renders, saves, and writes nowhere. * * **Loadable without a bundler.** `react` and the primitives package are both in * the boot seed table; the settings document and the slot ledgers arrive as * Cordis services or slot props. Nothing here reaches into another plugin's * bundle. */ window.__ModuleLoader__.load({ id: 'dsh-facet', factory: (require) => { var module = { exports: {} } var exports = module.exports const react = require('react') const primitives = require('@deepseek-ai/dsh-client-ui-primitives') const { SegmentedControl, SettingsForm, SettingsFormModel, SettingsValueField } = primitives const h = react.createElement /** The settings document's key: this bundle's row id, not its package name. */ const SETTINGS_ENTRY = 'facet-policy' /** The card's key: the bundle's package name, not its row id. */ const BUNDLE_CONFIG_KEY = 'dsh-facet' /** The modes the host schema accepts, in the order the control offers them. */ const MODES = ['ask', 'deny', 'off'] const TITLE = 'Facet' const DESCRIPTION = 'How a call outside the session\u2019s own folder is handled, and which paths are always in scope.' const OVERRIDDEN_LABEL = 'Overridden' const RESET_LABEL = 'Reset to default' const MODE_LABELS = { ask: 'Ask', deny: 'Deny', off: 'Off' } const MODE_OPTIONS = MODES.map((value) => ({ value, label: MODE_LABELS[value] })) const labelStyle = { display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500, color: 'var(--dsw-alias-label-primary)', } const hintStyle = { margin: '6px 0 0', fontSize: '12px', lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)', } const badgeStyle = { marginLeft: '8px', fontSize: '11px', fontWeight: 400, color: 'var(--dsw-alias-label-tertiary)', } /** * A finite-choice field, staged as the chosen value. * * The primitives ship `settingsNumberField` and `settingsTextField` and * deliberately stop there: only the owning plugin knows what its own * vocabulary is. An unrecognised draft returns `undefined`, which marks the * field invalid and blocks the save rather than quietly coercing a typo into * a mode the schema would reject at the Host anyway. * * @param {string} field - field name inside the namespace section. * @param {readonly string[]} choices - the values the host schema accepts. */ function settingsChoiceField(field, choices) { return { field, format: (value) => (typeof value === 'string' && choices.includes(value) ? value : ''), parse: (text) => { const normalised = String(text).trim().toLowerCase() if (normalised === '') return { kind: 'clear' } if (choices.includes(normalised)) return { kind: 'set', value: normalised } return undefined }, } } /** * A whole-list field: comma or newline separated in the control, an array of * non-empty strings in the document. * * Empty stages a clear rather than an empty array. The schema's own default * is `[]`, so writing one would shadow a deployment's composed default with * nothing and leave the field marked overridden when the user meant to * remove their override. * * @param {string} field - field name inside the namespace section. */ function settingsListField(field) { return { field, format: (value) => (Array.isArray(value) ? value.join(', ') : ''), parse: (text) => { const parts = String(text) .split(/[,\n]/) .map((part) => part.trim()) .filter((part) => part !== '') return parts.length === 0 ? { kind: 'clear' } : { kind: 'set', value: parts } }, } } /** The facet page's staged form over the composed `facet-policy` entry. */ var FacetCardController = class { /** * @param {object} scope - the shared configuration form for `facet-policy`. */ constructor(scope) { this.form = new SettingsFormModel(scope, [ settingsChoiceField('mode', MODES), settingsListField('extraRoots'), ]) this.store = this.form.bind(() => this.projection()) } /** @returns {object} the shell's shared state plus this card's two controls. */ projection() { return { ...this.form.shell(), mode: this.form.field('mode'), extraRoots: this.form.field('extraRoots'), } } /** * Build the face the slot registration injects. * * The `hooks` key becomes the component's `useFacetCard` prop — the slot * runtime turns each entry into a bound hook — and the actions supply * `edit`, `resetField`, `save` and `discard` beside it. * * @returns {object} the snapshot hook and the form actions. */ inject() { return { hooks: { facetCard: this.store }, ...this.form.actions() } } /** Release the form subscription. */ dispose() { this.form.dispose() } } /** * Render the facet card's one-liner or its settings form, as the page asks. * * @param {object} props - the view asked for, the bound snapshot, and the form actions. */ function FacetCard(props) { if (props.view === 'summary') return DESCRIPTION const state = props.useFacetCard((snapshot) => snapshot) const disabled = !state.writable || state.saving // The mode is editable as a segmented control rather than through // `SettingsValueField`, because a three-value vocabulary in a free-text box // invites a typo the field can only answer with "invalid". It is still the // same staged draft underneath — the control calls the form's own `edit`, // so nothing is written until Save. const mode = h( 'div', { key: 'mode' }, h( 'label', { style: labelStyle, htmlFor: 'facet-mode' }, 'Enforcement mode', state.mode.overridden ? h('span', { style: badgeStyle }, OVERRIDDEN_LABEL) : null, ), h(SegmentedControl, { id: 'facet-mode', value: state.mode.text === '' ? 'ask' : state.mode.text, options: MODE_OPTIONS, onChange: (next) => props.edit('mode', next), label: 'Enforcement mode', disabled, }), h( 'p', { style: hintStyle }, 'Ask puts each call outside the facet to you as a question. Deny refuses it. Off disables enforcement while leaving the preset in place — useful for telling a scoping problem from an unrelated one.', ), ) const extraRoots = h(SettingsValueField, { key: 'extraRoots', id: 'facet-extra-roots', label: 'Standing grants', hint: 'Paths always in scope, in every session — shared types, a common test harness. Separate them with commas. Relative paths resolve against each session\u2019s own directory, so the same list is meaningful in every workspace.', text: state.extraRoots.text, overridden: state.extraRoots.overridden, invalid: state.extraRoots.invalid, overriddenLabel: OVERRIDDEN_LABEL, resetLabel: RESET_LABEL, invalidLabel: 'Enter one or more paths, separated by commas.', disabled, onEdit: (text) => props.edit('extraRoots', text), onReset: () => props.resetField('extraRoots'), }) return h( SettingsForm, { labels: { unavailable: 'This plugin is not loaded, so it cannot be configured right now.', readOnly: 'This deployment stores settings read-only.', saveFailed: 'The deployment did not accept these values; they were left for you to correct.', save: 'Save', saving: 'Saving\u2026', }, state, onSave: props.save, onDiscard: props.discard, // Column rather than a bare array, so the two controls keep the form's // own spacing instead of sitting flush against each other. children: h('div', { style: { display: 'flex', flexDirection: 'column', gap: '18px' } }, mode, extraRoots), }, ) } /** * Mount the facet settings card while the Host serves its namespace. * * `whileServed` is the gate that makes the `volatile()` markers load-bearing: * the card is registered only while `facet-policy` appears in the Host's * served namespaces. An unserved namespace would be a control that silently * writes nowhere, so it registers nothing at all. * * @param {object} ctx - the browser plugin context. */ function apply(ctx) { // `inject` rather than a required dependency, so a deployment that composed // no settings provider loses the card and keeps everything else this bundle // contributes. ctx.inject(['configForms'], (scoped) => { const card = new FacetCardController(scoped.configForms.get(SETTINGS_ENTRY)) scoped.effect(() => () => card.dispose(), 'dsh-facet: settings form') scoped.effect( () => scoped.configForms.whileServed([SETTINGS_ENTRY], () => scoped.slots.inject('plugins.bundle.config', () => scoped.slots.register( { name: 'plugins.bundle.config', key: BUNDLE_CONFIG_KEY, inject: () => card.inject(), }, FacetCard, ), ), ), 'dsh-facet: settings page', ) }) } module.exports = { name: 'dsh-facet', inject: ['slots'], apply, SETTINGS_ENTRY, BUNDLE_CONFIG_KEY, FacetCard, FacetCardController } return module.exports }, })