Closes #58: the Context panel hung off two registrations at once (the conversation tab and the right-Sidebar guide entry), with no way to keep just one. A new per-user `defaultPlacement` preference — the first row of the Plugin configuration card — picks who carries the view: the conversation tab, the right Sidebar, or both (the default, so existing deployments see no change). - shared: `DefaultPlacement` vocabulary (`all` | `tab` | `sidebar`) on the settings namespace; the Host schema gains a loose `defaultPlacement` field so a stale persisted value degrades to `all` - client: `placement.ts` mounts each registration through its own factory and, on a preference flip, disposes exactly what the new value drops (the harness's own dynamic-mount idiom); `watchSidebarContextTab` returns its deferred-inject disposer so the sidebar mount can be taken down and put back; the settings card leads with the placement row
52 lines
2.0 KiB
TypeScript
52 lines
2.0 KiB
TypeScript
/**
|
|
* Placement gating for the Context view: the per-user `defaultPlacement`
|
|
* preference (the settings namespace's `defaultPlacement` field) picks which
|
|
* registrations carry it — the conversation tab, the right Sidebar panel, or
|
|
* both.
|
|
*
|
|
* Each mount is a factory returning its own disposer, so a preference flip
|
|
* takes down only what the new value drops and mounts only what it adds; the
|
|
* underlying `slots.inject` / deferred-inject disposers are the harness's own
|
|
* dynamic-mount idiom. A mount landing no disposer (a harness face that
|
|
* returns nothing) simply has nothing to unwind.
|
|
*
|
|
* @module dsh-context/client/placement
|
|
*/
|
|
|
|
import type { ContextSettings } from './settings'
|
|
import type { DefaultPlacement } from '../shared/types'
|
|
|
|
/** One registration factory per placement; each returns its mount's disposer. */
|
|
export interface PlacementMounts {
|
|
tab(): unknown
|
|
sidebar(): unknown
|
|
}
|
|
|
|
/**
|
|
* Mount per the current preference and keep the registrations glued to it
|
|
* until the returned disposer runs.
|
|
*/
|
|
export function watchPlacement(settings: ContextSettings, mounts: PlacementMounts): () => void {
|
|
let tab: (() => void) | undefined
|
|
let sidebar: (() => void) | undefined
|
|
const own = (result: unknown): (() => void) | undefined =>
|
|
typeof result === 'function' ? result as () => void : undefined
|
|
const mount = (placement: DefaultPlacement): void => {
|
|
const wantTab = placement !== 'sidebar'
|
|
const wantSidebar = placement !== 'tab'
|
|
if (wantTab && tab === undefined) tab = own(mounts.tab())
|
|
if (!wantTab && tab !== undefined) { tab(); tab = undefined }
|
|
if (wantSidebar && sidebar === undefined) sidebar = own(mounts.sidebar())
|
|
if (!wantSidebar && sidebar !== undefined) { sidebar(); sidebar = undefined }
|
|
}
|
|
mount(settings.defaultPlacement())
|
|
const unsubscribe = settings.store.subscribe(() => { mount(settings.defaultPlacement()) })
|
|
return () => {
|
|
unsubscribe()
|
|
tab?.()
|
|
sidebar?.()
|
|
tab = undefined
|
|
sidebar = undefined
|
|
}
|
|
}
|