/** * 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 { Modal, 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), }, ) } /** * The preset id whose sessions get a context button. * * A session records its preset as a projection value, so the button can * appear exactly where the facet is in force and stay out of the way * everywhere else. Rendering it unconditionally would put a control for a * plugin that is not running on every conversation in the deployment. */ const FACET_PRESET = 'facet' /** The composer tool row's entry id, and the button's accessible name. */ const CONTEXT_ENTRY_ID = 'facet-context' const CONTEXT_LABEL = 'Facet context' /** The scope one session has declared, normalised, or an empty declaration. */ function declaredFor(snapshot, sessionId) { const all = snapshot?.value?.scopes const mine = all !== null && typeof all === 'object' ? all[sessionId] : undefined const entry = mine !== null && typeof mine === 'object' ? mine : {} return { focus: typeof entry.focus === 'string' ? entry.focus : '', extras: Array.isArray(entry.extras) ? entry.extras.filter((path) => typeof path === 'string') : [], } } /** * Write one session's whole declaration. * * One path, one write: the panel always supplies the complete declaration it * wants, so a queued edit can never half-apply and leave a session scoped to * something nobody chose. An empty declaration is removed rather than stored, * so a session the user has finished with leaves no residue in the profile * patch — and so clearing both fields means "the session directory", which is * the schema's own default rather than a stored empty override. * * @returns {Promise} whether the Host accepted the write. */ function writeDeclaration(form, sessionId, next) { const focus = typeof next.focus === 'string' ? next.focus.trim() : '' const extras = (next.extras ?? []).map((path) => String(path).trim()).filter((path) => path !== '') const value = { ...(focus === '' ? {} : { focus }), ...(extras.length === 0 ? {} : { extras }), } const op = Object.keys(value).length === 0 ? { op: 'unset', path: ['scopes', sessionId] } : { op: 'set', path: ['scopes', sessionId], value } return Promise.resolve(form.mutate([op])) } /** Shared control styling, so the panel reads as part of the product. */ const controlStyle = { boxSizing: 'border-box', height: '32px', padding: '0 10px', fontSize: '13px', fontFamily: 'inherit', color: 'var(--dsw-alias-label-primary)', background: 'var(--dsw-alias-bg-layer-3)', border: '0.5px solid var(--dsw-alias-border-l4)', borderRadius: 'var(--dsw-radius-md)', } const quietButtonStyle = { ...controlStyle, cursor: 'pointer', color: 'var(--dsw-alias-label-secondary)', background: 'transparent', } const sectionTitleStyle = { margin: '0 0 6px', fontSize: '13px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)', } const bodyStyle = { margin: '0 0 14px', fontSize: '12.5px', lineHeight: '19px', color: 'var(--dsw-alias-label-secondary)' } const listedStyle = { ...bodyStyle, margin: '4px 0 0', fontFamily: 'var(--dsw-font-mono, ui-monospace, monospace)' } /** The crosshair that marks the button: a scope, not a setting. */ const CONTEXT_GLYPH = h( 'svg', { width: 15, height: 15, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': 'true' }, h('circle', { cx: 8, cy: 8, r: 3.2, stroke: 'currentColor', strokeWidth: 1.3 }), h('path', { d: 'M8 1.2v2.6M8 12.2v2.6M1.2 8h2.6M12.2 8h2.6', stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round', }), ) /** * The facet context panel: what this conversation may see, and who decides. * * This is the only surface that writes per-session scope. The agent has no * write path to it — not a narrow one, none — which is what makes "the user * decides what is in scope" a property of the system rather than of the * agent's instructions. * * @param {object} props - the bound form, the session, and the close command. */ function FacetContextPanel(props) { const { form, sessionId, mode, onClose } = props // `form` is the settings document for `facet-policy`, already a store: the // panel renders whatever the Host last accepted, including a write from // another tab. const snapshot = react.useSyncExternalStore( (listener) => form.subscribe(listener), () => form.getSnapshot(), ) const declared = declaredFor(snapshot, sessionId) const [focusDraft, setFocusDraft] = react.useState(declared.focus) const [extraDraft, setExtraDraft] = react.useState('') const [failure, setFailure] = react.useState('') const disabled = snapshot.writable !== true const write = (next) => { writeDeclaration(form, sessionId, next).then( (ok) => setFailure(ok ? '' : 'The deployment did not accept that change.'), () => setFailure('The deployment did not accept that change.'), ) } const rows = declared.extras.map((path) => h( 'li', { key: path, style: { display: 'flex', alignItems: 'center', gap: '8px', marginTop: '6px' } }, h('code', { style: { ...listedStyle, margin: 0, flex: 1, wordBreak: 'break-all' } }, path), h( 'button', { type: 'button', style: { ...quietButtonStyle, height: '26px' }, disabled, onClick: () => write({ ...declared, extras: declared.extras.filter((other) => other !== path) }), }, 'Remove', ), ), ) return h( Modal, { open: true, onClose, title: CONTEXT_LABEL, closeLabel: 'Close', description: `Session ${sessionId}` }, h( 'div', null, h('h3', { style: sectionTitleStyle }, 'What this conversation may see'), h( 'p', { style: bodyStyle }, 'The agent works inside the focus folder. Anything it needs beyond that is listed below, and the agent can narrow the focus on its own but can never widen it — only you can, here.', ), h('label', { style: sectionTitleStyle, htmlFor: 'facet-focus' }, 'Focus'), h( 'div', { style: { display: 'flex', gap: '8px' } }, h('input', { id: 'facet-focus', type: 'text', style: { ...controlStyle, flex: 1 }, placeholder: 'the folder the session started in', value: focusDraft, disabled, 'data-modal-autofocus': true, onChange: (event) => setFocusDraft(event.target.value), onKeyDown: (event) => { if (event.key !== 'Enter') return event.preventDefault() write({ ...declared, focus: focusDraft }) }, }), h( 'button', { type: 'button', style: quietButtonStyle, disabled, onClick: () => write({ ...declared, focus: focusDraft }) }, 'Set', ), h( 'button', { type: 'button', style: quietButtonStyle, disabled, onClick: () => { setFocusDraft('') write({ ...declared, focus: '' }) }, }, 'Clear', ), ), h('p', { style: bodyStyle }, 'Relative to the session folder, or absolute. Empty means the session folder itself.'), h('h3', { style: { ...sectionTitleStyle, marginTop: '18px' } }, 'Also allowed'), declared.extras.length === 0 ? h('p', { style: bodyStyle }, 'Nothing yet. Shared types, project docs, or a sibling folder go here.') : h('ul', { style: { listStyle: 'none', margin: 0, padding: 0 } }, rows), h( 'div', { style: { display: 'flex', gap: '8px', marginTop: '10px' } }, h('input', { id: 'facet-extra', type: 'text', style: { ...controlStyle, flex: 1 }, placeholder: 'src/shared, ../../CONTROLLER.md', value: extraDraft, disabled, onChange: (event) => setExtraDraft(event.target.value), onKeyDown: (event) => { if (event.key !== 'Enter') return event.preventDefault() if (extraDraft.trim() === '') return write({ ...declared, extras: [...declared.extras, extraDraft] }) setExtraDraft('') }, }), h( 'button', { type: 'button', style: quietButtonStyle, disabled, onClick: () => { if (extraDraft.trim() === '') return write({ ...declared, extras: [...declared.extras, extraDraft] }) setExtraDraft('') }, }, 'Add', ), ), h('p', { style: bodyStyle }, 'Relative to the session folder, or absolute — anything the session needs beyond the focus.'), failure === '' ? null : h('p', { style: { ...bodyStyle, color: 'var(--dsw-alias-state-error-primary)' } }, failure), h( 'p', { style: { ...bodyStyle, marginTop: '18px' } }, `Changes take effect on the next tool call. Deployment mode: ${mode ?? 'ask'}.`, ), h('h3', { style: { ...sectionTitleStyle, marginTop: '18px' } }, 'Mode details'), h( 'p', { style: bodyStyle }, 'The mode is set on the plugin card under Plugins, and applies to every session: ' + '"ask" puts a call outside the facet to you as a question; "deny" refuses it outright; ' + '"off" disables enforcement while leaving the preset mounted. Where the deployment has no approval ' + 'channel — as here, under the danger-full-access permission preset — "ask" cannot actually reach you, ' + 'so an out-of-facet call is refused and this panel is the way to widen the scope.', ), h('h3', { style: sectionTitleStyle }, 'How to use'), h( 'ol', { style: { ...bodyStyle, paddingLeft: '18px' } }, h('li', { style: { marginBottom: '6px' } }, 'Start the chat in the repository you want to work in.'), h('li', { style: { marginBottom: '6px' } }, 'Set Focus to the one folder this conversation is about.'), h( 'li', { style: { marginBottom: '6px' } }, 'Add what it needs beyond that — shared types, project docs, a folder it will create a sibling in.', ), h('li', null, 'Let the agent narrow as the work settles; it will ask you before it needs anything more.'), ), ), ) } /** * The composer tool row's button, and the panel it opens. * * Present only for a session running the facet preset, which is what "shows * up when in facet mode" means mechanically: the session's own projection * says which preset it runs, so nothing has to be inferred from the URL or * from the plugin being installed. * * @param {object} props - slot props: the session, `useSessions`, and the form. */ function FacetContextButton(props) { const { sessionId, useSessions, form } = props // Called before the early return, so the hook order is stable across the // sessions that do and do not run the facet. const preset = useSessions((state) => state.byId[sessionId]?.projectionValues?.agentPreset) const [open, setOpen] = react.useState(false) if (preset !== FACET_PRESET) return null return h( react.Fragment, null, h( 'button', { type: 'button', 'aria-label': CONTEXT_LABEL, 'aria-expanded': open, title: `${CONTEXT_LABEL} — what this conversation may see`, onClick: () => setOpen((value) => !value), style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '28px', height: '28px', padding: 0, cursor: 'pointer', color: open ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)', background: open ? 'var(--dsw-alias-interactive-bg-hover)' : 'transparent', border: 0, borderRadius: 'var(--dsw-radius-sm)', }, }, CONTEXT_GLYPH, ), // Mounted only while open, so each visit starts from what the Host holds // rather than from a draft left over from the last one. open ? h(FacetContextPanel, { ...props, onClose: () => setOpen(false) }) : null, ) } /** * 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 form = scoped.configForms.get(SETTINGS_ENTRY) const card = new FacetCardController(form) 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', ) // The context button rides the same form, so the panel and the plugin // card can never disagree about what the document holds. scoped.effect( () => scoped.configForms.whileServed([SETTINGS_ENTRY], () => scoped.slots.inject('conversation.input.left', () => scoped.slots.register( { name: 'conversation.input.left', id: CONTEXT_ENTRY_ID, order: 20, inject: () => ({ form }), }, FacetContextButton, ), ), ), 'dsh-facet: context button', ) }) } module.exports = { name: 'dsh-facet', inject: ['slots'], apply, SETTINGS_ENTRY, BUNDLE_CONFIG_KEY, FACET_PRESET, CONTEXT_ENTRY_ID, FacetCard, FacetCardController, FacetContextButton, FacetContextPanel, declaredFor, writeDeclaration, } return module.exports }, })