Master gained a web-search e2e scenario that calls connectFreshWorkspace
with the pre-change one-argument signature, which fails to compile against
the test-merge commit CI builds. Give it the scaffold's workspace root like
every other scenario.
Master also gained the first-run welcome notice. The shared scaffold
acknowledges it before boot, but the W5 smoke spawns its own server against
a fresh $DSH_HOME, so its overlay owns pointer events and swallows the
first click on the workspace chip; the scenario now dismisses the notice
first. The notice is anchored structurally because this spec belongs to the
client TypeScript program, which does not reference the package owning its
copy.
DetailsDialog is a centered SelectList over the five transcript-detail
states (three tool-card phases, reasoning shown/hidden); it preselects
the current phase, marks both current values, applies on Enter, and
cancels on Esc/Ctrl+C. Width is the new detailsDialogWidth config key.
The argument grammar is unchanged and shares the same setters.
/details reports the transcript detail state bare, jumps tool cards to
collapsed|expanded|hidden, and sets or toggles reasoning blocks, sharing
the closure state behind Ctrl+O/Ctrl+R via setToolsVisibility/setReasoning.
Also fixes a replay defect the reasoning rebuild exposed: rebuildTranscript
reused a settled StreamingAssistantComponent for a later assistant/message
of the same step, overwriting the earlier content; the settled check now
lives in renderEvent for both live and replay paths (untrusted-controls
re-recorded with the previously dropped content present).
The reveal spec wraps setStart to record which layer and index the composer measures; capturing the method directly trips the unbound-method rule, which the pre-commit lint caught after a --no-verify commit slipped it through.
The selector overlay opens as soon as the command dispatches: the
picker renders a loading placeholder over an undefined candidate set,
owns terminal input from its first frame, answers Enter with a
still-loading error, and cancels on Escape exactly like the loaded
list. The finished scan swaps rows in through setCandidates without
replacing the overlay; a scan failure closes it and keeps the existing
notice.
preventScroll stopped the browser from revealing a focus nobody gestured for, and took the caret with it. The composer DOM is reused across sessions, so switching to a longer draft keeps the previous offset while the value swap puts the caret at the new draft's end: measured on all three engines, 940px below a box sitting at 0, where the old geometry reached the caret at 628. The unlock effect now suppresses the browser's walk and reveals the caret in its own scrollport, landing at 625 of 628. The focus-keeping mousedown gets preventScroll too and needs no reveal. revealCaret returns early when the box cannot scroll: the draft fits, so every caret is already in view.
Rewrite the read/search/web/diff card spec suites for the collapsed-by-default
ToolRow composition: each row now needs the conversation `t`, and the card is
absent from the DOM until the row's [data-expandable] toggle is clicked. Refresh
the built-boot and search-card assembled snapshots (they expand the row before
shaping the card) and the code-mode-round golden. Fix a fixture turn-number
collision the 985/986 merge introduced (grep reused turn 67, already the
multi-hunk edit's, so both shared one callId) by renumbering search/web/todo to
68-72. Document the refactor's behavior in the ui-conversation README (both
languages) and add the Agent Note.
- lifecycle-chrome's second scaffold staged its workspace under the OUTER
scaffold's temp root, coupling two supposedly independent worlds and
leaving the aria scrub root wrong; it now uses its own workspaceCwd.
- The direct-open path now carries the same `flowBusy` gate that disables
the equivalent menu entry, so an occupant re-registering mid-adoption
cannot raise a second flow.
- A composition with no directory-picker no longer opens a zero-entry
popover on the hero anchor: with nothing to pick and nothing to add,
the gesture shows nothing. Both behaviors gain a unit test.
- Brought three partially superseded Agent Notes current (the native
picker, the workspace UI product flow, and the sidebar browsing split),
cross-linked to this decision, both languages re-recorded.
- Corrected this Note's own Testing section: the shared e2e helper stages
and adopts its directory, it does not create one in-dialog — only
workspace-management does. Named the client-seam and CLI-README residue
in the follow-up TODO alongside the wire branch.
The user bubble's IconActions row carried an edit button with no click
handler, no client mutation, and no host operation behind it, advertising
an affordance the product cannot honor. The row now renders clock / copy /
branch only for both user and assistant chrome, and MessageIconActions
loses the `edit` prop with the button.
The package README records the missing capability under Known Limitations,
the web aria goldens pin the row without the control, and the Agent Note
states what must exist before it comes back.
dsh --dump-config and dsh web --dump-config compose the shipped base,
the surface overlay, and the --config or personal overlay — exactly the
layers that surface boots — and print the entry list as YAML without
booting; --dump-default-config stops at the surface overlay so the two
outputs diff to precisely the user layer's effect.
The dump shares the mounting code: the vendored include exports its
patch algorithm as applyEntryPatches() and its !!js dialect as
entryListSchema (logged in vendor/README.md), dsh-app-boot's
renderConfigDump() composes and renders through both (and now imports
the dialect instead of duplicating it), and the CLI adds a thin
dump-config mode. !!js expressions print verbatim; unmatched patches
warn on stderr; boot-only flags are rejected alongside the dump flags.
(cherry picked from commit 1fdbebfa8a5dc7df840d53666320064a7e3dae59)
The read, search, web, and file-mutation toolviews each hand-drew their own
summary-row chrome and kept their card (ReadBlock/SearchBlock/WebBlock/
DiffBlock) resident below the summary. Fold all four into the shared ToolRow:
ToolRow now takes read/search/web/diff card material alongside terminal, and
renders it in its collapsed-by-default expanded body, so every tool row shares
one expand interaction and starts collapsed. The GenericToolCard fallback and
the DetailsPanel Output section keep rendering the same cards (the panel at full
height); the per-row .module.css files are deleted as ToolRow owns the chrome.
The Ctrl+O hidden phase keeps one Assistant header per turn: the first
step with visible text/reasoning owns it, later steps render as
headerless continuations, and bodiless (tool-only) steps render
nothing. Leaving hidden restores per-step headers. Pure TUI
presentation; the session log is unchanged.
The selector called readSession per listed session under an unbounded
Promise.all: each call re-listed the whole persistence store (O(N^2)
listings), decompressed and parsed the complete log, replay-validated
every event, and deep-cloned it up to three times, only to derive one
row's title, activity time, turn label, route, and goal phase. On a
real 185-session / 87 MB store the selector took tens of seconds.
Candidate rows now come from one projectSessions batch over borrowed
logs; a rejected projection degrades to the same disabled unreadable
row. Preflight still replay-validates the single chosen session through
readSession, which is already live-preferred, so its redundant live
shortcut is gone.
Public SessionQueryService.projectSessions wraps the existing corpus
projectMany: one persistence listing, bounded persisted-inspect
concurrency, per-id failure isolation, and a synchronous projector over
a borrowed source with no replay validation or cloning.
readTitleSnapshots now routes through it; LogicalSessionSource and
LogicalProjectionResult are exported and documented.
Pasting a long block left the view where it was while the caret sat at the
end of what was pasted. Paste, ctrl/meta-Enter newline and cut all suppress
the native edit — the machine owns the draft and the undo log — and restore
the caret with `setSelectionRange`, which reveals nothing: measured in
chromium and WebKit, before this branch as well as on it. Firefox happened to
reveal it, in the old geometry only.
The three restores now share one helper that measures the caret against the
hidden mirror — same draft, same metrics, same wrap width, so a Range
collapsed at the caret's index reports where the caret is without a caret API
— and scrolls the scrollport the minimum that brings the line inside, which
is what the browser does for typing. One scrollport is what makes this
possible at all: the reveal is finally a single offset to move.
Also from review: the composer's own focus() on unlock and session switch
passes preventScroll, so a session switch cannot move the transcript through
the taller textarea's reveal chain.
SearchRow (this PR) and FileMutationRow (landed on master) independently carry
byte-identical rowStateStatus + rowResultText helpers, which the duplication gate
flags once both are present. Extract both into contract/toolview-status.ts and
consume them from both rows, deleting the clone rather than nudging it under the
threshold.
The read tool now renders through the keyed ReadRow (Read · {path} chrome with a
resident read card) instead of the generic disclosure row, so the seeded read
turns in these built-boot goldens change from an expandable `button "Read …"`
to the read row's icon + Read + path-link shape. Refreshed keyless.
Node >= 21 exposes a global `navigator` reporting the machine's own language,
so gating detection on `navigator` let a non-browser boot of the client tree
resolve to `en` instead of the documented fallback; `window` is the browser
test. `navigator.languages` is spec-required but absent on some embedders and
older WebViews, where spreading it would throw at boot, so the walk tolerates
its absence and `navigator.language` covers that host.
The per-spec pin boilerplate collapses into one suite-level
`usePinnedBrowserLanguages('zh-CN')`, which owns the rationale in
dsh-client-test-runtime, and the English-browser e2e scenario now clears the
console warnings channel too — its page has no closing inventory spec.
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.