* feat(ui-conversation): fold turn process before final answer
* fix(ui-chat): polish turn-process control row from review
* test(web): drive preset slash catalog with gestures
* fix(ui-chat): keep turn process order stable
* fix(ui-chat): preserve prompt order after pagination
Co-authored-by: Yif <877193178@qq.com>
The client-bundle preset pins the production/development exports condition
(lexical's node-condition file selects its flavor with a top-level await a
CJS bundle cannot carry); the composer carries an explicit aria-label (a
div's data-placeholder does not name it the way a textarea placeholder
did); READMEs, the composer.bar slot doc, and the Agent Note record the
editor architecture; web e2e drives the contenteditable surface.
The card-wide `border-box` reached `.fieldMirror`, so the growth cap counted
text plus padding. The inline variant carries none and landed on the declared
line count, while the optionless variant's 16px inset spent two thirds of a
line and delivered its last one as an 8px sliver. The e2e measured only the
inline shape, so nothing caught the drift.
The mirror now takes `box-sizing: content-box`, which states the cap in the
units it is written in, and the cap moves to six lines. The e2e asserts whole
text lines rather than a box height, and covers the optionless shape — asked
straight through the user-questions seam, since a layout metric needs no model
round — including that the reserved empty field is fully covered by its own
control.
A question that carried options collected its free-text answer in a
single-line input: a long sentence scrolled sideways inside one line and
Shift+Enter was inert, so an answer with structure could not be typed.
The optionless question already used a textarea, but a fixed 64-140px box
that never followed the draft.
Both shapes now answer into one AnswerField: a `textarea rows=1` sharing a
grid cell with a hidden mirror that renders the draft plus a trailing
newline and so owns the height. Soft wraps are invisible to a '\n' count,
so the mirror is what grows the box; growth stops at eight lines and the
textarea scrolls from there, keeping the choices the answer belongs to in
view. Enter still continues and submits, Shift+Enter breaks the line, and
the IME guard is unchanged.
Both Workspace surfaces offered "Open local folder…" and "Create a new
workspace" for one outcome. The browse occupant already carries its own
New folder affordance, so picking a directory covered creating one; the
name dialog only added a second vocabulary and a create target the
operator could neither see nor choose.
The surviving entry is named after the outcome — "Add workspace…" — and a
menu now appears only where there is something to choose between: with no
Workspace listed (the add-only sidebar header, or an empty hero list) the
anchor gesture raises the directory flow directly instead of a one-row
popover. An empty list counts as final only after the list baseline lands,
and a composition with no directory-flow occupant hides the sidebar button
rather than offering a dead one.
WorkspaceCreateFlow becomes WorkspacePickFlow (createOnly -> addOnly) and
the injected createWorkspace narrows to { path }. The host's
workspace.create({ name }) branch and `dsh web --workspace-root` lost their
last product consumer; both are marked at the call site for a follow-up.
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 startup-selection flow leaves a fresh world (no Workspace) in the
locked view state, so every e2e scenario that types into the composer
now connects one first via the shared connectFreshWorkspace helper
(hero picker create-by-name dialog; the default 'workspace' name keeps
the session-header cwd assertions intact).
Golden refreshes carry the current composer chrome: the plan/model
control seats are empty until their owning plugins register (the seats
shipped without occupants on this branch), the sidebar shows the
connected workspace group pre-send, and the bash details material
renders Input/code/Output as separate nodes. The cancel scenario polls
the frozen-partial swap instead of counting synchronously — the abort
frame reaches the browser over SSE after the host settles.
Validate replay sidecars and cross-copy failure facts, make browser console tripwires and macOS temp paths deterministic, and wait for asynchronous TUI resume details. Keep the owning docs, translations, and generated catalog aligned.
Every spec now commits at least one golden and the interactive ones one
per distinct end-state (nine new .expected.md):
- live-interactions: cancel.expected.md (frozen partial + 已停止 marker),
error-auth.expected.md (the prompt bubble alone — the committed artifact
of the web-error-surface gap, the diff that flips when error rendering
lands), retry.expected.md (indistinguishable from a clean completion —
retries are deliberately invisible in the transcript).
- question-composer: answered.expected.md (the question resolved into its
tool round trip plus the final reply, takeover gone) beside the existing
waiting-state golden.
- steering: mid-steer.expected.md pins the accepted-but-INVISIBLE state
(the loop drains steering only at the step boundary, so no interjection
bubble exists while the question still blocks — if the client ever
renders pending steers eagerly, this golden flips first) and
settled.expected.md the badged bubble plus obeying reply.
- navigation-panes: waterfall.expected.md and details-open.expected.md
(tool-name header, Input args, Output result) beside the trajectory one.
- lifecycle-chrome: reloaded.expected.md — rendering the same settled
transcript from persistence alone IS the recovery claim.
Fixture inventories extended to the new closed sets; the Agent Note's
expected-outputs policy updated in both languages (per-end-state goldens
for interactive scenarios), pairing re-recorded.
Five browser e2e scenarios over the existing keyless lane, one recorded
base fixture per spec family:
- live-interactions: one tool-free recorded turn + per-run override
sidecars authored in the spec (content single-sourced from the fixture
via deriveReplayScript, minted into a spec-owned temp dir). Cancel uses
a hang patch with a readyFile marker — the marker proves the stream is
parked mid-turn before the Stop click, so mid-stream cancellation is
deterministic by construction (turn/end 'aborted', composer re-enabled).
AUTH pins the non-retryable path: turn/end 'error', zero llm/retry
events, composer recovers; FIXME(web-error-surface) marks the found
product gap (no error copy renders — the client consumes no agent/error
frames and a pre-chunk failure freezes no partial). SERVER retry appends
the fixture's own success after an injected throw and proves llm-retry
end-to-end in the browser via the durable llm/retry record.
- question-composer: the shipped ask_user_question takeover blocks the
turn mid-step on the real userInteraction seam; the test answers through
the composer (the one sanctioned model-content-reactive drive step: the
turn cannot complete without it) and the tool result carries the answer.
Adds the composer waiting-state aria golden.
- steering: steers mid-turn while the composer blocks the step (the
deterministic mid-turn window). The steer rides the real wire
(session.prompt mode:'steer' POSTed from the page; the locked composer
has no steering gesture yet — TODO(web-steer-composer)); downstream is
all product: gateway -> Agent.steer -> step-boundary drain -> durable
steering/message -> SSE -> badged interjection bubble. Record mode
rejects a fixture whose live reply ignored the steer.
Scaffold gains the replayOverride passthrough; specs register in both
tsconfig planes (client exclude, host include).