The composer's single primary action now stops a running continuable
child: Session.cancel() routes a continuable address through
subagent.interrupt (ordinary sessions keep session.cancel; one-shot
addresses stay uncancellable), and InputBar switches the same Send button
to Stop while a continuable child runs. A running parent-offline child
keeps the default composer with disabled input so that Stop stays
reachable; the read-only takeover returns once it stops.
Refs #1535
A browserless keyless-replay e2e boots the shipped web composition, holds
a continuable child's turn open with a replay hang entry, queues a
follow-up and interrupts over plain HTTP, and proves from the real
session state that the turn aborted, the follow-up parked without a new
turn, and a waking send resumed the preserved FIFO order.
Refs #1535
The web app project excludes every e2e file (they are host-plane programs)
and tsconfig.host.json includes them one by one; the new scenario joins
both lists so it keeps typecheck coverage without dragging host sources
into the client project.
A model entry's reasoningEfforts dict declares its selectable thinking
levels — key = offered level, value = the wire spelling dispatch sends;
only off may leave the value empty (supported, send nothing). false
strips reasoning from a catalog model; every level is materialized
explicitly into pi-ai's thinkingLevelMap so nobody has to know pi-ai's
asymmetric absent-key defaulting. compat.thinkingFormat and
compat.supportsReasoningEffort become configurable on the route and per
model (model > route > catalog entry > pi-ai's URL-derived guess),
openai-completions only, so a private gateway speaking the DeepSeek
reasoning dialect no longer depends on its URL being recognizable.
Record-typed drift gates pin both enums to pi-ai's, and an unserviceable
declaration is refused at the write that produced it, naming route,
model, and level.
The chatFileMentions service (provided by ui-deliverables beside its
turn-tail entry, reached via ctx.get) resolves inline-code tokens in the
closing message against the turn's produced locations: exact path or
unique basename links, ambiguity and unknowns stay inert. MarkdownText
gains the optional fileMentions seam — settled renders only, never
inside anchors.
A row's stored profile could not tell a hand-declared gateway from a
shipped provider whose models someone narrowed — both look identical from
outside the adapter — so the Models page had no way to mark the routes a
deployment added itself.
The directory entry now carries `declared`, answered by the owning adapter
against its own installed catalog, and the page renders a Custom tag from
it. Absence stays "this adapter draws no such distinction" rather than
"shipped", so a route no adapter claims is labelled neither way.
Also records the default-route work's Agent Note and the e2e evidence for
all three changes: the composer switch writing the section, and the Models
page declaring a route with its own reasoning effort.
`assistantActionsSeqs` picked the last content-text assistant of each turn
from the finalized transcript alone. That quantity is stable only once the
turn closes: while a turn is still producing steps, the narration written
before a tool call is the last content assistant so far, so copy, branch,
and the clock appeared under an intermediate sentence for as long as the
tool ran and then moved down to the next step's text.
Pass `ConversationSnapshot.turnEnds` into the derivation and grant the row
only inside a turn that has a durable `turn/end`. This is the same
completion fact the branch control and the `Ran for` label already read, so
the three parts of one row now agree; mid-turn narration owns nothing, and
the seat appears once under the settled answer.
`hasContentText` moves to chat-flow.ts so the ownership gate and
AssistantMarkdown's mount gate cannot drift apart.
apps/web/tests/turn-tail-actions.e2e.ts pins both states through the
assembled application: a hang sidecar on the second model call parks a turn
whose first step narrated before calling bash, and the two goldens hold the
parked flow and the flow after stopping.
The hero's backdrop ellipse is sized 1051/776 of the hero box so its blur
scales with the input card, which means it reaches past the column whenever
the column is narrower than the glow. `[data-conversation-scroll]` declared
only `overflow-y: auto`, and a box that scrolls in one axis computes the
other axis's initial `visible` to `auto` — so that bleed came back as a real
horizontal scrollbar, 24–95px of range across ordinary laptop widths.
Declare `overflow-x: hidden` on the column instead of leaving the second axis
to be derived. Clipping is unchanged (the box already clipped both axes); the
declaration withdraws only the bar and the user gesture.
A completed pwsh foreground call now presents as the bash tool's terminal
card with the parsed exit-status pill instead of a generic console fence,
and the collapsed row classifies as the shell family (Pwsh title). The
marker-to-exit-status parse moves from dsh-tool-bash's private render
module into the @deepseek-ai/dsh-bash seam so both shell tools share one
inverse of the marker contract (the bash-env precedent). The Web UI needs
no per-tool code: the terminal-card bridge maps any card:'terminal' view.
Coverage: dsh-bash owns the parse edge cases; the tool-pwsh presenter
suite mirrors tool-bash's; the client row-model suite pins the Pwsh shell
row; the new keyless pwsh-terminal web lane seeds an authored session,
presents it through the real tool on replay, and pins the card golden.
TUI is out of scope: the TUI package was removed (ed30088adb), so the Web
surface is the only UI the gap affected; the roadmap proposal's stage 2
is updated accordingly.
The composer seat is one node laid out against two different edges: in Chat it
is a sticky child of the column's scroller and rides its content box, which a
space-consuming scrollbar shortens, while a view declaring a composer overlay
gets an absolutely positioned seat against the padding box, which no bar
reduces. With the transcript scrolling the two tabs disagreed by the bar's
width, so the centred input card moved 4px sideways on every switch — and the
same 4px inside Chat when a growing transcript began to scroll.
The column now reserves its scrollbar gutter unconditionally and states the
overlay branch a scroll container on the same axes, so both states measure
against the same width.
Scope decision: previews for a browser that is not on the Host machine are
not supported. With that settled, host.openPath answers the supported case
completely — a file:// document in a real browser has full page capabilities
and no reach into /api — and the HTTP serving this branch had built answered
only the unsupported one.
Removed: the /f route and its listener, the workspace-file URL shape,
ApiProxy.workspaceRootOf, ConnectionHandle.fileUrl, and the port published
into the index page.
Kept, and finished:
- the produced-files row a turn ends with, derived from mutation locations;
- the path link now reads as a link at rest, not only on hover — the reported
"I can't open what it made" was this, sitting on a working capability;
- the Host opener prefers the default BROWSER for .html/.htm/.xhtml/.svg, so
a developer who binds .html to an editor still gets a rendered page
(macOS via the LaunchServices https handler, Linux via $BROWSER, every
failure falling back to the default application).
The retired designs and their measurements stay in the Agent Note, including
why same-origin serving was unsafe and why the sandbox that fixed it broke
the pages invisibly.
Serve one file at a time out of a Session's workspace under /f on the web
transport, and point the conversation's existing file-open affordance at it.
Clicking a write/edit/read row's path now opens that file in a browser tab —
including from a LAN client, where the Host's system opener is fenced to
loopback and answered nothing.
- /f/<sessionId>/<segments> in client-connection, behind the same
browser-trust fence as /api; realpath confinement, streamed reads,
GET/HEAD only, nosniff + no-store.
- Script-capable documents carry CSP sandbox: model-authored markup must not
be same-origin with /api, where events.mux is a readable GET stream.
- ApiProxy.workspaceRootOf answers where a Session's files live without
resuming an agent; the client program cannot reach the core services.
- The /f URL shape lives in dsh-host-apiproxy/api so both ends share one
encoding (client bundles may not value-import another plugin).
Holds the session.history response at the browser's network boundary so
the auto-selected open is observable, then asserts the visible frame and
the recorded phase timeline. Registered host-plane like the other
scaffold-booting e2e files (host include + client project exclude).
The two shipped surfaces offered different tools for no recorded reason:
session checkpoints, tool-result pruning, the goal tools, and Ralph were in
`tui.cordis.yml`; `tool-todo` and web search were in `web.cordis.yml`. Neither
offered session search, a string-replacement editor, or a repeat-tool guard,
though none of the three is surface-specific.
Move the rows that are not surface-specific into `base.cordis.yml` and add
those three. Web search moves there too — the TUI decision the change that
made it a Web default deferred. Both surfaces now assemble the same 27 tools.
This adds only. No row is removed from either surface and no existing row's
configuration is edited: executors, sandbox composition, access defaults,
`tools.mode`, and the workflow tool are exactly what they were. Two rows stay
surface-specific: `tmux-context` (no terminal multiplexer in a browser) and
`session-reference` (its index has one writer owner).
Ship `dsh-mcp-client` as a dependency without a row: the plugin mounts one
server per instance and `command` is required, so a default would name a
third-party server and spawn it outside `ctx.bash` on every launch. The CLI
README carries the YAML for mounting one from a personal config.