Union resolutions throughout — the fixture serves both the goal and plan
projection units (catalog gains /goal beside /plan; the retired
goal-fixture sample command yields to the real goal mirror), the mux
baseline spec expects all four unit frames, and the tsconfig paths /
Model Experience allowlist carry both domains' outlets.
The sparkle glyph gained a second consumer (GoalBar beside the Others
tool-row variant), which the duplication gate correctly flags as a clone —
the package-local-until-shared rule its comment recorded has run its
course. Both copies collapse into the shared ic_ds_* family; the icon-set
count test grows to 57.
The five ui-conversation machinery specs (apply-inject, chat-apply,
chat-toolview-slot, service-orchestration, selection-survival — now .tsx)
and the web app/app-shell specs assemble through the runtime instead of
hand-built Context + SlotsService + fake-session scaffolding per suite.
Session behavior mocks are typed against ISession, so incomplete fakes
fail at compile time.
Feature packages now reach these domains through interface types only:
ISession/SessionFace (identity + prompt/cancel/loadOlder + the useSession
snapshot source), ISessions, IWorkspaces, ILayout, IConversation, and the
existing SlashServiceContract now actually mounted on Context.slash. The
concrete services implement their face; wire-pump and assembly entry
points stay on the classes. The provide-channel materialization and
current-projection logic moves into SessionProvideChannel so the
production service and the client test runtime share one implementation.
The workspaces service consumes sessions through the narrow SessionsPort.
Plan mode is entered through /plan only — the select control is retired.
The conversation.input.plan seat (now right of the access-mode control)
renders a read-only Plan chip while the projection's effective target is
plan mode; its hover x executes /plan off, and the chip follows the folded
target (appears on /plan immediately, disappears on /plan off) with frames
correcting either way. While plan mode is targeted the composer textarea's
placeholder switches to the plan-task wording — InputBar reads the same
projection through the standard-kit useProjection (the TodoDock posture:
a type-only key merge, no domain service edge), and owner placeholders
still win.
plan-mode contributes the RFC's complete double-event example: command/run
records named plan set the wanted target (off -> false, else true),
plan/mode commits and clears it, and view derives {active, pending}.
Pending is thereby a pure replay quantity recovered from the log alone.
The plan key merges into SessionProjectionMap from src/types.ts, served
through the new ./types and ./client outlets (session-title template);
compositions without the registry are unaffected.
Sweep the silently auto-merged leftovers of the superseded #587 wire layer:
the PlanModeState interface and planMode/setPlanMode entries in the sessions
API and rpc map, the fixture plan RPC spec, the composer.controls README
paragraph and spec assertions, the old-format plan-mode snapshot, and the
two Agent Notes written against the RPC design (to be rewritten for the
projection reattachment). ChatView/PendingCard keep the approval-only
pending rendering; QuestionComposer improvements ride along unchanged.
Resolution follows the reattachment plan: the #587 wire layer (planMode/
setPlanMode RPC, prompt.planMode, client Session projection fences) is
dropped in favor of the session-projection base now on master; master
structure wins in all shared files. Kept from the PR side: the approval-only
pending filter in ChatView (questions render solely through the composer
takeover) and the auto-merged QuestionComposer improvements. The deleted
host/runtime package and retired test carriers are removed with master.
The check added last commit derived its elevated set from the sheets that
already rebind, which cannot catch the omission it targets: such a set only
confirms what someone already remembered, and a surface nobody has rebound
yet defines itself as unelevated. Review found the case that proves it —
TodoPanel scrolls in .list on a --dsw-specific-tip card, the same dark rung
as the menu surface, unrebound and with the derived check green.
Resolves the set from the palette's own dark elevation ladder instead: the
surface tokens whose dark value lands on bg-layer-2 or bg-layer-3, which is
the step the l1/l2 split encodes. A new palette token on an elevated rung is
in scope the moment it is defined.
Scope is by token family rather than geometry: only --dsw-alias-bg-* and
--dsw-specific-* name a surface. The button, interactive, and markdown
families reach the same rungs while naming a control or an inline span that
no scroll container renders a bar against, and shape cannot separate them
since a floating button carries a radius, a shadow, and a fixed size —
ChatView's .toBottom pill was the false positive that showed this.
Adds the missing TodoPanel rebind. Mutation-checked all four rebinds in
turn: each is named with its surface. The palette anchoring has its own
control — narrowing the family pattern turns it red on --dsw-specific-menu.
Review found three scroll containers sitting on surfaces the rebinding
contract covers, none of which rebound: ui-primitives' shared Menu card
on --dsw-specific-menu (the surface PopupSelectView already rebinds for),
and the composer input and question composer cards, both on
--dsw-specific-input-major. Each rendered the l1 thumb, which differs
from l2 only in the dark palette and only on that surface, so a
light-palette screenshot and a code read both look correct.
Adds the mechanical check that would have caught them instead of leaving
it to inspection: a sheet that scrolls somewhere and paints a known
elevated surface somewhere must rebind. The elevated set is derived from
the sheets that already rebind, since a rebinding rule paints the surface
whose elevation it declares, so a new elevated surface joins the set by
rebinding rather than by anyone updating a list. Surface-level rather than
element-level because the card and the descendant that scrolls are
separate rules and CSS text does not say which contains which. Verified by
reverting each of the three fixes in turn: the check names the sheet and
the surface every time.
Also commits snapshots/sidebar-scrollbar/geometry.expected.md, the
resolved scrollbar style and geometry in both palettes. The aria goldens
the other web scenarios commit cannot carry a CSS-only change, since it
alters no DOM and no accessible name and leaves their trees
byte-identical. Absolute coordinates stay out: they track font metrics and
the laid-out sidebar width, so committing them would document the platform
and force a per-platform re-record.
Review follow-ups: the three new notify loops (currentProvideInfo
subscribers, ui-skill lexicon listeners, late-registration controller
setup) now contain per-callback failures so one faulty consumer cannot
starve the rest, abort the list projection pass, or poison the source
roster with no disposer; controller lexicon polling drops a throwing
source with a console record like the candidate path. The ui-slash
README (both languages) now states the late-registration warm and the
subscribeLexicon contract, and the scenario suite drives a typed /name
token gaining its decoration when the roll settles with no further
input.
Registrant-private reactive facts previously reached components as raw
observables that each component subscribed by hand (InputBar notices/
lexicon via uSES, SettingsRoot via a version/subscribe/getter triple).
The inject face now carries a reserved hooks compartment of bare
sources; the renderer binds each into a use<Name> selector hook through
the same machinery as the provide channel, so components consume
useNotices/useLexicon/useSections and never see a subscription
primitive. InputBar and SettingsRoot are the first two consumers.
The decoration scan read a mutable lexicon() aggregation during render
with no subscription, so a catalog settling or a child spawning after
prewarm left drafted tokens undecorated until an unrelated re-render.
The controller now publishes the aggregation as a snapshot store fed by
a new optional SlashSource.subscribeLexicon hook (ui-skill notifies on
settle/invalidate, ui-subagent forwards the session-list feed), the
composer keyboard face exposes it as an observable, and InputBar
subscribes through uSES. Sources registered after scope birth now warm
and join live controllers via a service broadcast.
A provider roster change under a stable current id rematerialized every
scope's bundle but nothing notified React: SessionProvider resolved the
bundle from a current-id subscription only, so mounted entries kept the
obsolete hook/prop schema until an unrelated re-render. The sessions
service now owns an atomic currentProvide observable fed by both current
writes and roster changes; the renderer host exposes it as
sessions.provide, replacing the current/provideInfo/maybeProvideInfo
trio, and both providers subscribe to it.
Tool row summaries strip the session workspace root; the running sweep
becomes a glare-band overlay (deepsuite ShimmerText pattern); assistant
nodes that render nothing no longer split tool-row groups; block and
tool-row spacing collapse to one 16px rhythm.
Lint: eslint --fix over the merge-crossed projection/command files (arrow
parens, trailing commas, unnecessary assertions), Extract<> replaces the
keyof-map & string intersections no-redundant-type-constituents rejects,
the fold-adapter's merge loop drops its non-null assertions for a
bounds-carrying cursor, one JSDoc line wrapped under max-len (api-catalog
regenerated). Snapshots: the four TUI goldens re-recorded for the merged
event-count shift (the durable command lifecycle adds one event to the
seeded diagnostics log). The headless advanced-toolchain snapshot passes
on CI and fails locally in this sandbox both with and without these
changes (30s child timeout — environment-bound, tracked in the ledger).
- StateDot ongoing: gradient spin ring replaced by an 8-cell pixel chase
(2px matrix cells, stepped trail, no tweening)
- Chat: streaming pulse block replaced by a turn-level 4-pixel chase at the
flow tail — rides the whole running turn (first-token wait, tools,
streaming) instead of flickering with partial presence
- Tool rows (ToolRow/BashRow): running no longer swaps the icon for a dot;
an animated mask band sweeps the row content, gliding off on exit via
mask-position transition
- Composer: send/stop unified on the blue fill (bigger stop glyph, static
white arrow), textarea box-sizing overflow fix, settling phase hides the
composer while replay decides hero vs docked, workspace-placeholder
fallback disables the bar
- Hero: glow moved behind (z-index) with lower opacity; tool-row hover icon
crossfade at 100ms
- The hero workspace chip is a selector: no-live-selection states (cold
start, workspace deleted from the sidebar after the list is ready) now
render a "Choose workspace" placeholder (closed-folder icon) instead of
resurrecting the deleted folder name via the session cwd; the cwd-derived
name still bridges the initial list load. Stale pending picks clear when
their workspace leaves a ready list.
- The expanded sidebar wordmark starts a new session (visuals unchanged,
pointer cursor only); the collapsed rail logo keeps its expand toggle.
- The centered hero composer stack gains a 32px foot for visual balance.
Menus: keep 12px viewport clearance with internal scroll, pin workspace
create actions in a footer, and pre-render portal lists hidden so the
first painted frame is already at its final position (no open jump).
Tool rows: 14px icons, secondary titles, no hover fill, and a hover
chevron preview on in-place expandable rows. Settings: 800x600 layer-2
panel over a blurred mask, hover states, and wrapping selector cubes;
ModelSelect surface tokens now match the Menu primitive.
commandId crosses three boundaries (session log, wire admission response,
client flow pairing), so per the branded-id rule it becomes
Branded<'CommandId'>, declared in a new pure @deepseek-ai/dsh-commands/brand
outlet (the dsh-llm/brand shape: type + constructor, no Context merges, so
wire and client programs can name it without loading the host plugin). The
event payloads, CommandExecution, and the executor mint carry the brand; the
wire schema gains commandIdSchema as the domain's single brand-cast point
(the approvals precedent); CommandNode and the fixture's fabrication cast
follow type-only.
The consumer-side restated declare-merges retire (user ruling: one home per
projection key): TodoPanel imports the todos merge and TodoItem through
@deepseek-ai/dsh-tool-todo/client, and the manager takes the title merge
through @deepseek-ai/dsh-session-title/client — both pure-type outlets
re-exporting the domain's single-source types.ts, so no host value import or
Context merge enters the client program (type-only edges, exempt from the
plugin value-import ban). Workspace deps and tsconfig references added.
Also aligns the fixture's empty-log tail block with the host convention
(asOfSeq -1 with empty values, block always present on tail requests).