Files
dsh-context/src/client/placement.ts
T
bowenliang123 a4add07664 feat: add a placement preference (tab / sidebar / both) for the Context view
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
2026-09-11 13:44:21 +08:00

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
}
}