The wire type now carries projections?: SessionProjectionsBlock (host-base
landed), so the structural projectionsOf narrowing and its TODO(gui) go away —
Session reads result.value.projections directly at all three installWindow
sites. ProjectionsBaseline stays as the cell framework's structural twin
(React-free layer keeps depending on the type table only) with values typed
Partial<SessionProjectionMap>; the erased walk moves inside resetBaseline
where per-key typing is re-established by schema.parse.
Swap the client runtime's parallel-construction placeholder for
import type from @deepseek-ai/dsh-session-projection/types — the zero-import
outlet, never the package root, whose dsh-agent → dsh-session chain would drag
the host Context.sessions merge into the client program. One type table end to
end (host provider, wire block, client cell, React hook); the spec's test key
now declare-merges the real module. Adds the workspace dep and the tsconfig
project reference.
React half of the session-projection client base: the renderer contract gains
an open-key projections face on SessionMaybeProvideInfo (cellOf(key), distinct
from the static hooks roster), web-react mints projectionHook (per-bundle cache;
per-cell uSES binding via the shared observableHook cache; unresolved keys read
undefined through the absent source so hook order stays constant), standardKit
delivers kit.useProjection, and the runtime merges UseProjection into
SessionStandardProps/SessionMaybeStandardProps (overloads mirror useSession).
3 jsdom specs (kit delivery + live re-render, selector over undefined, faceless
bundle = all absent); existing direct-prop-feed specs gain the one-line stub the
new required seat mandates.
Object layer of the session-projection RFC client base: ProjectionCellSpec/
ProjectionCell/ProjectionCellSet with the single seq-watermark rule (live and
window-replace events share one filter; baseline reset re-seeds value+watermark
unless a newer commit applied; absent key = capability absent), Session
dispatch at appendLive/installWindow (projections block read structurally,
TODO(gui) switch to the interface package), SessionsService.registerProjectionCell
roster (live scopes now + future scopes at mint; disposer sweeps every session),
and the provideInfo projections face (key-addressed bare cell sources).
15 object-layer specs: watermark no-rollback, late-baseline seq rule,
capability absence, schema-failure degrade, duplicate-key throw, resync e2e.
Hand fixes for the findings --fix cannot touch, mirroring the fixes
already applied on the fe-docs feature branch (same file, same shape)
so its eventual rebase resolves cleanly:
- restore the return the no-confusing-void-expression autofix ate in
useAbsentSnapshot (typed S | undefined; hook call kept for hook-order
stability, undefined returned explicitly);
- re-type DOM queries the no-unnecessary-type-assertion autofix broke:
getByRole<HTMLButtonElement>(...) generics instead of the removed
as-casts (the eslint program and the client tsconfig aggregate
disagree about these casts; the generic form satisfies both);
- justified eslint-disable for the deliberate legacy paths: keyCode 229
IME-composition detection, execCommand clipboard fallbacks, lib.dom
clipboard optionality, and the any-typed Reflect.get/this probes in
test fakes;
- drop the dead react/no-danger directive (eslint-plugin-react is not
loaded, so the rule never applied) keeping its shiki rationale;
- delete the tautological 'Z' comparison and the renameTarget null
check already implied by renameBlocked;
- css-module non-null assertions replaced by type widening
(Button className, TAG_CLASS Record) per the established pattern;
- misc: max-len comment wraps, void generic drop in the deferred test
helper, unused type imports, floating selectWorkspace promises voided,
member-delimiter newlines in inline type literals.
unbound-method flags destructuring a method-style member (method
signatures are bivariant and exempt from the this-context check).
These contract members are all plain callbacks — declare them as
property-style function types so consumers can destructure them
without a false this-binding hazard. Type-level only.
Mechanical --fix output over the newly linted .tsx files (indent,
arrow-parens, comma-dangle, member-delimiter-style, unnecessary type
assertions), plus the three generic-arrow test hooks converted to
function declarations up front: the comma-dangle fixer strips the
<T,> disambiguation comma and turns them into parse errors otherwise.
The question composer card is capped against the viewport and scrolls its
option list. `.options` is a flex column whose children defaulted to
`flex-shrink: 1`, so a short seat shrank the rows before overflowing the
scroll container: a row collapsed to its 42px minimum while `.optionCopy`
kept the taller height its wrapped copy needs, and `align-items: center`
then painted that copy outside the row's border box — over the question
title above and the next row below. Measured 6.5px of spill at 900x440 on
the shipped client, 10px at 380px tall, with `.options` reporting
scrollHeight === clientHeight and therefore offering no scrollbar.
`.option` and `.custom` now declare `flex-shrink: 0`, so the shortfall
reaches the scroll container that already owns `overflow-y: auto` — the
behavior the cap was designed for. Only rows whose copy wraps could
reproduce this, which is why the recorded scenario now asks a question
with long option descriptions; the web e2e asserts at three squeezed seat
heights that every row's children stay inside its border box, guarded
against holding vacuously by requiring a wrapped row and a scrolling list.
The remote side re-merged master (i18n workflow round) and restored the
pre-architecture ModelSelector package with the Session-side state
machine. Reconciliation keeps that merge's master content and reasserts
this branch's model architecture: business state lives in ui-model's
shared per-session directory (ctx.models), not on Session/
ConversationSnapshot; the input.model seat entry and the /model popup
stay the two same-source entries; the history modelTarget piggyback stays
removed. The ui-model-selector package, its roster row, and its allowlist
entry go; the smoke-real roster names ui-model.
The seat's dropdown follows figma 496:26454: the root pane is the
Model / Effort cell pair (14/22 label, value in the tertiary tone, right
chevron), each drilling into its own list — the provider-grouped model
list over the shared directory, and the High/Max effort levels. The
trigger (313:14108) shows both values: model name plus effort in the
caption tone. Effort is a client-local display echo on the shared
directory state for now — the design pairs the two as one selection, but
no wire carries a per-session effort override yet (the deepseek adapter's
reasoningEffort is deployment config); the directory state documents that
boundary, so wiring it later is a submit-path change, not a UI one.
Escape backs out of a drilled pane before closing.