The composer returns to a read-only sending phase. Enter captures the draft,
its reference chips, and its attachments; the editor and the attachment rail
stay read-only while Send shows progress; and the Host's acceptance clears
the draft and its attachments in one transaction before the echo renders. A
refusal or failure returns the draft unchanged. A running Session still
shows Stop beside the sending control, and Workspace selection still leaves
a draft whose message is in flight in its Session.
This reverts the editable-while-sending behavior of a7af0509ab5 and the
in-flight attachment accounting of e36c3f9a395, which only that behavior
needed.
The composer stays editable while a prompt is sending, so waiting for an
editable composer no longer waits for the round trip. Consecutive sends in
the queue layout and queued image scenarios now wait until the composer
clears, which happens only after the Host accepted the previous prompt.
Enter still sends one message at a time and waits for the Host, but the
editor stays editable meanwhile: typing, paste, completion, and new
attachments go to the next message, and only another send waits. Once the
Host accepts the message, its attachments leave the draft and its text
leaves the editor: the whole draft while it still equals the sent text,
only the sent text and the whitespace after it when the user added text
after it, and nothing when the user changed the sent text itself. A
running Session keeps Stop beside the sending control, and Workspace
selection leaves a draft with a submission in flight in its Session.
The composer keeps a sent message, its references, and its attachments
read-only in a sending phase until session/prompt resolves, so an
admission listener can hold or refuse a prompt before it appears as sent.
The Session publishes the local echo and leaves the blank layout only
after acceptance; a refusal or failure keeps the draft, and
session/prompt-rejected with presented: true adds no toast.
This carries the client half of deepseek-harness/deepseek-harness#6178.
Tighten goal-round-driver docs and regression assertions for the withdrawn
queued round: continuation disarms when the cancelled turn belonged to other
work, and the removal is a durable canceled next-turn splice.
The goal strip and the queue dock ride directly above the input card, and
both create a stacking context (`isolation: isolate`) so their frosted
panel stays behind their own content. A tooltip bubble left inside that
context resolves its z-index there, where the card — painted later in the
same context — wins: the goal strip's bubble showed a two-pixel sliver
and the queue dock's stayed entirely hidden.
Portal those dock tooltips to the body, where the bubble's z-index
resolves above the whole card. The ui-primitives `portal` contract now
names the capping stacking context beside clipped containers, because
neither container clips its bubble.
The queue-actions aria goldens lose the tooltip line: the captured center
column no longer contains the portaled bubble. The scenario still covers
it by role and adds a hit test at the bubble's center and bottom edge, as
does goal-bar.
The recorded-session scenario edited with a single-line value, so the ARIA
golden could not catch a return to a newline-stripping editor. Edit with a
two-line value and assert the model-bound user message keeps the line break;
with the previous input element the scenario fails on that assertion. Also
refresh the stale input cast and pin that Shift+Enter is not consumed.
* 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 <[email protected]>
The user-bubble decorator rendered every plain run through the
block-level MessageText div, so a decorated single-line message broke
into one line per run and the space between two tokens rendered as a
blank line. The queue dock's read-only row printed row.preview verbatim,
showing the wire session form (@[label](dsh-session:...)) instead of a
readable label. Both predate the Lexical composer; the logged model text
was correct in both cases.
One shared inline projection (reference/user-text.tsx) now owns sent
user text for the bubble and the queue row: plain runs are spans with
white-space policy left to the consumer (bubble pre-wrap, queue nowrap),
and a highest-precedence rule folds the wire session form to its label
chip, shielding the URI from the bare-token scan. The queue edit field
keeps the literal sent text. user-text.client.spec pins the inline
guarantee and every fold rule; queue-actions.e2e locators move to
row-container matching (the projection adds one span layer).
Every consecutive composer send waits out the submit round-trip's
read-only span; the end-of-document caret gesture becomes select-all +
ArrowRight (Cmd/Ctrl+End moves no caret in mac contenteditable), which
lets Lexical's own ancestor scroll walk prove the typing reveal.
textContent/data-placeholder probes replace inputValue/placeholder reads,
evaluate-string selectors move to the composer anchor, queued fills wait
out the submit round-trip's read-only span, and the refreshed aria goldens
drop the hover tooltip the old interaction order happened to capture.
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.
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.