# dsh-ui-fortifier Current State ## Introduction A DSH Web UI enhancer plugin: provides feature-module toggles in the Settings page and shows the provider of the current session. ## Overview The plugin has a Host half and a client half. The Host half registers the `ui-fortifier` settings namespace, holding each feature module's toggle state; the client half registers a Settings page (`settings.section`), reads the configuration entries to generate toggle rows automatically, and the index centrally controls each feature module's mounting (see "Module control mechanism"). Toggle state uses the dsh settings service three-layer resolution: schema defaults → `base` layer (plugin config cordis.yml defaults) → `user` layer (user Settings UI override). Each half has its own compiler face (`tsconfig.host.json` sees node globals, `tsconfig.client.json` sees browser globals) with the root `tsconfig.json` as a solution-only aggregate; types shared by both halves live in `src/types.ts` (types only, no runtime code), so the client face references no host file. Since dsh 0.1.5, service methods run under the **caller's Context**: when the client plugin calls `ctx.modelDirectories.directoryFor()`, `this.ctx` is this plugin's fiber, and that method reads `this.ctx.sessions` / `this.ctx.remote.session` internally — so the client `inject` must declare `sessions` / `remote` / `remote.session` exactly as the official `ui-model-selection` does, or it raises `cannot get property "remote.session" without inject`. ## todo ## Features ### Settings page - Description: Adds a "ui-fortifier" tab in Settings to toggle all feature modules. Toggle rows are generated by reading the `ui-fortifier` namespace's configuration entries. - Files: [src/client/index.ts](../../src/client/index.ts), [src/client/settings/FortifierSettingsPage.tsx](../../src/client/settings/FortifierSettingsPage.tsx) - Solution: The client binds settingsScope, reads the namespace value from the describe mirror; the toggle row list is driven by the explicit `MODULE_ORDER` array (`src/client/index.ts`) and rendered in that positional order; writes go through the settings scope's write API. - Approach: Adding a feature requires adding a field to the Host `Config`, an entry in `modules`, and a copy key (`config.`) in `locales.ts`; the Settings page shows the new toggle automatically, with no change to the page code. - Layout constraints: the list content is capped at 520px wide (switches stay near their labels when the panel is dragged wide), with a 0.5px divider between adjacent options. - Switch styling: mirrors the official `ui-settings-plugins` Switch pattern (`role="switch"` + thumb); the thumb is a `border-radius: 50%` full circle paired with `corner-shape: round` — identical to the dsh built-in switches, as required by the ui-theme global superellipse smoothing contract (full-round shapes must pair to keep circular arcs). ### provider-label - Description: Shows the currently selected provider to the left of the model selector (`conversation.input.right` slot). - Files: [src/client/provider-label/index.ts](../../src/client/provider-label/index.ts), [src/client/provider-label/ProviderLabel.tsx](../../src/client/provider-label/ProviderLabel.tsx) - Solution: The component subscribes to the model-directory snapshot via the `useDirectory` inject hook and renders the current-provider field. - Approach: Data source is the current-provider field of the ModelDirectory store. ### model-picker - Description: Provides a cascading model picker in the input tool row (`conversation.input.right`, right of provider-label): providers on the left, that provider's models on the right, solving the difficulty of locating a model when the official grouped list has many providers. - Files: [src/client/model-picker/](../../src/client/model-picker/) - Solution: Registers the `conversation.input.right` list slot (`order: 1000`); the component subscribes to the official shared directory snapshot via `hooks.directory`, with `load`/`select` riding the same `modelDirectories` instance, so state stays in sync with the official selectors (button and slash command). - Panel placement: mounted through `createPortal` into `document.body` with `position: fixed` (`z-index: 1100`), so the columns' `overflow: hidden` clips and the sidebar no longer apply to it; coordinates come from the local `useRightAlignedMenuPosition` — right edge aligned to the trigger, clamped only when that would leave the viewport (the official `useAnchoredPosition` places from the left edge only and exposes no alignment option; the official `ModelSelect` mirrors that logic for the same reason). - Narrow-screen adaptation: the minimum width is `min(420px, calc(100vw - 32px))`, shrinking with the viewport; model names wrap in full via `overflow-wrap: anywhere` instead of being truncated. - Scroll behavior: on open, the current provider and current model scroll into view before paint (`useLayoutEffect`), positioned once per open; switching providers returns the model column to the top; switching back to the current model's provider positions to the current model. - Approach: The official `ui-model-selection` is untouched (single slot with no child slots to inject into; swapping the whole seat would outgrow official updates); reusing the shared store avoids a plugin-private copy; panel open/close is entry-local state. ### session-id-copy - Description: Adds a copy-Session-ID button to the Session header's utility group; the hover hint shows the session ID itself, clicking writes it to the clipboard, and the button briefly swaps to a check mark. - Files: [src/client/session-id-copy/](../../src/client/session-id-copy/) - Solution: Registers the `conversation.session.header.utilities` list slot (`order: -20`, leftmost in that slot); the button is a 28px-tall pill holding an inline hash glyph beside a copy glyph; the session ID comes from the slot's owner parameters (`scope: 'session'`) and is shown as `Session ID: {id}` on hover/focus through the `Tooltip` primitive; copying uses ui-primitives' `writeClipboard` (async Clipboard API first, `execCommand` fallback). - Approach: The three-dot menu (the one holding "download session log") builds its items from a hardcoded array with no injectable slot, so this is an adjacent button rather than a menu item; the dsh repository's `session-log-export` is not modified (harness source is overwritten by dsh upgrades); the feedback mirrors the official `MessageIconActions` brief check-glyph swap; the hash glyph is inlined because the official icon library has no identifier glyph, and the hint uses the `Tooltip` primitive rather than a native `title` (it also covers keyboard focus and never stacks two hints). ### open-dsh-folder - Description: Adds an "Open .dsh folder" button to the Settings header action area that opens `$DSH_HOME` in one click. - Files: [src/client/open-dsh-folder/](../../src/client/open-dsh-folder/), [src/open-dsh-folder.ts](../../src/open-dsh-folder.ts) - Solution: The Host side provides a TypertRemoteService (`uiFortifierRemote`) exposing an `openDshFolder` remote method, auto-mounted on the `/api` gateway through `ctx.reflect`; the client calls it via `connection.rpc.call('/api', 'uiFortifier/openDshFolder')`, and the button registers into the `settings.action` slot only when `connection.isLoopback`. - Windows foreground activation: the shared `openNativePath` runs only `Invoke-Item`, and since explorer creates the window while the host runs in the background, Windows refuses to bring it forward and it lands behind the browser (dsh's own button behaves the same). The Windows branch therefore runs its own PowerShell: after opening, match that folder's explorer window by `LocationURL` to read its `HWND`, synthesize an Alt press/release to become eligible, then call `SetForegroundWindow`. Non-Windows platforms keep the shared `openNativePath`. - Approach: The host endpoint reuses gateway reflection so no typert generator is needed; the remote result carries `opened`/`path` for button feedback and error copy; foreground activation reuses the Alt-synthesis technique harness already validates in its native folder dialog, adding an explicit raise because the target window belongs to explorer. ### settings-frame - Description: Makes the Settings panel draggable and resizable; after browser window changes the panel always stays within the viewport, with geometry persisted to localStorage. - Files: [src/client/settings-frame/](../../src/client/settings-frame/) - Solution: The component registers in the `settings.action` slot, locates the panel on mount via `closest('[role="dialog"]')` and switches it to `position: fixed`; the drag handle is portaled into the header row's first slot and the resize handle into the panel's bottom-right corner (pointer capture + rAF throttle); minimum 480x320, and on window resize oversized width/height shrink and the position clamps (nothing persists while the window is below the minimum, so a transiently flattened geometry never enters storage). - Approach: Panel geometry has no existing slot to carry it, so it is reached through DOM lookup plus inline styles; persistence follows the existing `dsh.conversation.contentWidth` localStorage precedent; unmount restores the CSS defaults. ### Module control mechanism - Description: Each feature module's mount/unmount is controlled centrally by the client index from the Settings toggle state; when disabled, the index unmounts the whole registration and the module itself is unaware of the toggle. - Files: [src/client/index.ts](../../src/client/index.ts) - Solution: The `modules` registry is strongly constrained by `Record`; the index subscribes to the toggle scope, calls the install function when enabled, and calls the returned disposer when disabled. - Approach: Submodules only register themselves; control logic lives in the index, so adding a module is one entry in `modules`.