fix(web): keep one composer bar DOM across the no-workspace transition
The composer.bar slot moves from session to session-maybe scope: with no
current session the entry still mounts, the machine faces (keyboard, stop,
command) arrive undefined, and the bar renders its normal DOM inert via the
disabled owner prop. DisabledInputBar and its parallel tree are gone, so the
textarea node survives the cold-start workspace pick instead of flashing
through a remount. A blank session whose workspace was deleted takes the
same inert path through owner props.
test(web): add the composer DOM-continuity acceptance probe
Drives a real dsh web server with headless chromium through the cold-start
-> pick-workspace -> type flow and asserts the composer textarea is the
same DOM node throughout (a marker property must survive). Rerun
prerequisites are in the header comment.
docs(web): session-maybe identity is adoption, not hold-forever
fix(web): session-maybe entries adopt the first session, then remount like strict entries
A session-maybe entry used to keep one React instance across every
transition, so component-local state leaked between sessions once the
composer bar moved to that scope (PermissionSelect's optimistic pick, the
IME composition guard). Identity is now adoption: an incarnation born
session-less holds through the arrival of the first session (the blank
shell's DOM survives the workspace pick), and afterwards behaves exactly
like a strict session entry — a switch or a drop to no-session remounts,
clearing local state by construction. The child key is an incarnation
counter kept in the stable outlet wrapper via render-phase setState.
chore: knip knows the root acceptance probe's playwright dependency
scripts/hero-composer-dom-continuity.mjs resolves playwright through
apps/web's devDependency tree (createRequire), which knip cannot follow;
ignore it at the root workspace.
- gate model selection on steering-placement image carriers from enqueue
until their steering/message event publishes; release the gate when an
admission ends idle without publication (both behaviorally asserted)
- reject session.updateQueue edits carrying non-text blocks at the RPC
boundary (queue edits cannot bypass image admission)
- extend the durable-directory walk past a first-created DSH_HOME to the
deepest pre-existing ancestor
- strip Windows-style separators from attachment display names on POSIX
- verify attachment reads with a header-only probe (digest already proves
the bytes decoded fully at admission); document the read path
- make SessionInputShell.addImages refusal observable and keep workspace
transfers/composer intake from leaking refused drafts
- own ONE recursive image walk (dsh-llm contentHasImage) across apiproxy,
pi-ai, compact-basic, and the DeepSeek text-only assertion
- drop the redundant canonical-base64 regex and the no-op role read
- move AttachmentId/AttachmentError out of types.ts (brand.ts/error.ts);
document why AttachmentError does not extend HarnessError
- document the hard attachments inject in both consumer READMEs
Stats line moves into the InputBar's new footer slot (sharing the card's
width column) and expands to the design's grouped detail row: turns/steps,
LLM and tool wall time, cache hit, and input/output token split, all
derived client-side from the snapshot. The composer stack owns one 8px
rhythm, the seat fades the transcript through a fixed 36px gradient band,
back-to-bottom follows a live --dsh-composer-height, and goal/todo strips
share one 752px tip-fill column.
Unify the /plan claimed hint with the plan placeholder through a locale
namespace, localize slash menu group titles, replace the PermissionSelect
native select with the Menu primitive, add a goal pause verb chain, clamp
anchored popups to the viewport with scroll-into-view and outside-dismiss,
and fix onPasteUpgrade insertedRange to account for the chip trailing gap.
- llm-deepseek: the uncatalogued resolveModel fallback declares text-only
modalities — the wire route is text-only regardless of catalog
membership, so "unknown" must not let the host persist-then-fail images.
- session.selectModel also consults the pending-inbox mirror: a queued
image prompt enters the log only when claimed, after a switch would land.
- attachment store: ensureDurableDirectory syncs every ancestor entry up to
a caller-vouched boundary regardless of what mkdir reports — a raced
"already existed" is not "already durable".
- One image walker (imageBlockIn/imageInEvent) now serves both attachment
authorization and the selection gate; referencedImage therefore also
authorizes references inside wrapped message content.
- InputHub: the scope disposer resolves the conversation service optionally
(teardown/HMR must reach quiescence), and a send failing after its scope
died releases the in-flight drafts instead of restoring them onto a
disposed shell.
- http-bridge destroys declared-oversize requests with connection: close
instead of draining a body the client can trickle indefinitely.
- LlmService validates AND detaches modality arrays identically on the
advisory and exact routes; READMEs record the fourth INVALID_MODEL_INFO
rejection reason.
- CLI provider docs (JSDoc, README pair, Agent Note pair) describe the
reuse behavior; llm-route.spec now parses the SHIPPED cordis.yml through
the production extraction, pinning the row coupling.
- image-display lane pins gallery/rail shape in inline snapshots and the
object-URL scheme this environment must take; stale host.schema comment
dropped.
- resolveLlmRoute: reuse the yml pi-ai row for providers it already routes
(DUPLICATE_ADAPTER boot failure) and detect an unset model by origin, not
by comparison against one deployment default; covered by a new spec.
- LlmService.resolveModelInfoFor preserves (and validates) modality
metadata, arming the host image preflight for exact-route resolution.
- session.selectModel refuses a text-only target once the session log
carries an image on any replayed route; an accepted switch would strand
every later turn with no in-product recovery.
- The composer no longer gates image intake on the handshake activeModel
snapshot (wrong authority for a per-session decision); the host preflight
plus the error strip own capability, deployment limits stay client-side.
- InputHub shell teardown releases the scope's draft images (File objects
and object URLs leaked for the page lifetime).
- session.prompt image parts carry optional alt into the durable block;
ImageBlock documents assistant-side rendering as forward compatibility.
- Assembled built-client lane apps/web/tests/image-display.snapshot.ts pins
the history galleries over the authorized attachment route, the lightbox,
and the composer paste rail; the attachment rail is an accessible group.
- Docs: validateImage on the seam page, fixture byte metadata matches its
PNG, and the Agent Note claims now match the shipped coverage.