The lane boots the host spine through the shared scaffold, so it belongs to
tsconfig.host.json and must stay out of the client-registered apps/web project
— one program cannot hold both sides of the cordis Context merges.
The approval panel replaces the InputBar while a sandbox escalation waits,
and its justification and command are unbounded model text. With no height
cap, a long command grew the card until the refuse/allow row went under the
fold: at 900x700 the action row's bottom landed at y=749, so the user could
read the request and not answer it.
Justification and command now scroll in one region capped at the same height
as the composer's draft area, with the amber strip and the action row outside
it. The cap is one value with two consumers — declared as
--dsh-composer-text-max-height on ConversationRoot's .composerSeat, the
composer chain's only shared ancestor — so the seat cannot cap its two states
differently. The card rebinds the l2 scrollbar pair like every other scrolling
surface on an elevated background.
Covered by a new web e2e scenario that drives the real composition (read-only
session, denied write, the model's escalation retry, answer clicked through
the panel) and measures the live panel at two viewport heights against the
composer's own cap, read off the textarea rather than hardcoded.
`dsh` shipped two config trees that were 43 rows the same: apps/cli/cordis.yml
composed web as 74 flat rows, while the TUI booted examples/tui-agent/cordis.yml
whose single `@deepseek-ai/dsh-tui-demo` row mounted twelve plugins behind a
twenty-key pass-through Config. Neither file was what its location claimed —
apps/cli hardcoded the "example" as the product default and the "demo" bundle
was the application — and every capability change had to be made twice.
- apps/cli/base.cordis.yml holds the 43 shared rows; tui.cordis.yml and
web.cordis.yml are patch lists stating only what differs per surface
- overlays apply as SIBLING patch lists at one include level, because include
patches never cross an include boundary. Precedence: base < surface <
(--config | personal ~/.dsh/config.yaml) < launcher flag/profile patches
- `--config` now applies an overlay INSTEAD OF the personal one, so a demo or
test tree never inherits the user's route; new `--config-replace` boots a file
as the entire tree (the old `--config` behaviour). Both survive /resume
- vendor/include: index each `insert`ed row as it is added so a later patch can
configure or disable it. Upstream built the id index once before the patch
loop, leaving every surface-only row — the whole TUI front door — silently
unpatchable from user config. Logged as local modification 8
- session identity moves to dsh-agent-loop's CONFIGURED_AGENT_IDENTITIES_KEY;
dsh-tui's MAIN_SESSION_ID_KEY is deleted (only the bundle read it)
- delete examples/tui-agent, examples/cordis-agent, packages/examples/tui-demo;
TUI tests → apps/cli/tests, cordis e2e → packages/cordis/tool-cordis/tests,
examples/code-mode survives as an overlay leaf
- `dsh web` gains --config, threaded into AppCLIEntry as an extra overlay
Three latent defects surfaced and are fixed here: the TUI captured the optional
sessionQuery service once at construction and could permanently disable /resume
when it won the mount race; the session-store root silently reverted to a
project-local ./.sessions; --config-replace was dropped by the resume handoff.
Verified by booting each tree through the real Loader (TUI 55 entries, web 75,
zero unsettled) rather than reading YAML. All eight terminal snapshots replay
byte-identically; 14/14 PTY smoke, 112/112 snapshots, 25/25 doc-sync, hygiene
and lint clean.
Add @deepseek-ai/dsh-host-directory-picker-auto, a node-half-only chooser
that samples the host situation once at boot (bind host via a new
httpServer.host getter, SSH markers, platform, DISPLAY/WAYLAND_DISPLAY)
and mounts the matching dual-face backend (-native or -browse) as a real
Loader entry in the in-memory root tree; the effect disposer removes it.
Entry-level mounting keeps the seam's one-row-swaps-both-faces invariant:
the client module table discovers the mounted backend's browser half
exactly as a config row's. apps/cli now composes -auto as its
directory-picker row; composing a backend row directly remains the pin.
Conflicts were the parallel-edit set: packages/README group table (kept
master's session-title wording, re-added the settings row condensed),
doc-budgets AGENTS.md ceiling (took master's 1750), and regenerated
graph/catalog docs plus re-recorded translation pairs. packages/README.md
ceiling 845 -> 850: the table legitimately gained one row for the new
settings group and the row is already minimal.
Two-package capability family mirroring session-persistence/:
- dsh-settings: abstract Settings service — namespace registry with
caller-fiber effect registrations, layered resolution (schema defaults
< composition base < user document), schemastery validation,
per-namespace deep-equal commit detection, and the settings/updated
event. Boot/registration validation fails loud; provider publishes
keep last-good per namespace.
- dsh-settings-local: settings.yaml/.json provider — resolveSpec
defaulting to $DSH_HOME/settings.yaml, chokidar hot reload,
content-equality self-write suppression, atomic 0600 tmp+rename
writes, comment-preserving YAML namespace patching.
Consumers register inside ctx.inject(['settings'], …), so every
composition works unchanged without a mounted provider. Real Loader +
Include composition test proves cordis.yml boot and external-edit hot
propagation; HMR disposal test proves registry cleanup. Both packages
hold per-file 100% coverage.
Doc budgets rise 1705→1710 (AGENTS.md) and 835→845 (packages/README.md):
one structural line per file for the new package group.
Agent Note: .agents/notes/implemented/architecture/2026-07-28-user-settings-seam.md