The hover affordance framed only the strip right of the crumbs. The bar itself
now carries the outline and the padding in both modes, so hovering previews
exactly the field the click produces and nothing resizes when the two swap.
One keystroke moved the view twice: deleting a separator first narrowed the
pane the draft had just walked away from, then replaced it with its landing.
The tail now filters only the LAST pane — the one whose level the path names —
so a pane on its way out holds still until its landing arrives.
Also from the review round: the walk waits both legs out instead of taking the
submitted-navigation bound (a speculative scan has nothing waiting on it, and
a tail keystroke aborting a slow parent leg would otherwise strand the
two-pane view); a level keeps answering the directory text that produced it,
so `..` segments and Windows forward slashes filter and stop rescanning; the
release-on-miss rule counts displayable rows, so it survives `hidden` ever
meaning more than dot-prefixed; and the editor's 250ms rest joins the other
two constants on the remote-recalibration list.
Skipping the draft-following scan whenever ANY pane happened to list the
directory was the cheaper rule and the wrong one: erasing a segment left the
level being typed on the LEFT, with its own child pane still standing to its
right, so the two panes stopped reading as "where I am, and where I came
from".
The pane arity is now the invariant the editor maintains — the last pane lists
the level the path names, its parent sits beside it, and only a display root
lists alone. Only that last pane's own tail costs no scan; every other
directory part re-lands.
The draft-following scan replaced the panes with one wide level, so typing a
path collapsed the dialog's Miller view — the thing the dialog is. It now
lands through the same selection-anchored landing every navigation uses:
target and parent legs as one frame, the target re-selected in its parent
level, its children on the right. Typing a path moves the Miller view exactly
as a crumb jump does.
One landing shape, two callers: `land(path, {closeEditor, announce})` is what
`navigate` and the draft-following scan share. A submitted path closes the
editor and announces failures; the speculative scan keeps both to itself and
re-parks the focus its swap dropped.
A level a pane already lists still needs no scan at all — the filter alone
answers the draft — so erasing back into the parent's own path keeps both
panes and only moves the filter.
ds-review-bot round one. Keying the debounce on the directory part the draft
named left two states with no recovery until the operator crossed a separator:
a keystroke that superseded an in-flight scan never re-armed one, and an edit
after a rejected submission released the hold with no timer left to release.
The wait is now keyed on the draft itself and decides its target when it
fires, reading the panes through a ref so a landing cannot re-arm it (a host
answering with a differently spelled path would otherwise scan forever).
A landed scan that unmounts the row a keyboard operator Tabbed onto re-parks
focus on the still-open editor; the Modal has no focus trap. That a walked-to
level survives closing the editor is now stated in the README, the Agent Note,
and the module contract. The new e2e stages its own beta directory so running
it alone sees the tree its assertions describe.
The Select Workspace Directory dialog hid its one route into typing a path
behind an invisible click target, and once the editor opened the panes stayed
on whatever level was listed when it opened — so the typed text and the list
under it disagreed for the whole edit.
The edit zone now carries a pencil glyph at the bar's right edge and lights in
the editor's own footprint on hover/focus (the bar keeps one height across the
swap). While editing, the panes follow the draft: a directory part no pane
lists is scanned after a 250ms rest and lands in place, so typing deeper
descends and erasing segments steps back up without leaving the editor, and a
final segment nobody matches releases the prefix filter instead of emptying
the pane it is being spelled into. The draft-following scan is speculative and
silent on failure; Enter still owns the view from submission until landing and
remains the only path that surfaces an error.
The live fast-path fence and the raced-collision catch duplicated the same
subagent-ownership classification, tripping the duplication gate. Extract
`fencedLiveAgent` so both paths resolve one live identity through the
fence identically.
The raced-collision catch mirrored only the subagent-owned half of
ensureSession's `.catch`: a concurrent plain-agent publish winning the
identity still fell through to `internal`, where ensureSession returns
the winner. Mirror in full — classify a subagent-owned winner as
`agent-busy`, return a clean plain-agent winner directly.
The commands entry's inline comment described the old routing shape
("clients only send a sessionId for a published session") without the
ownership fence that agentFor now applies on every path — the fence's
contract home is the api/commands.ts module JSDoc, so trim the duplicate
and point at the routing shape only, keeping one home per fact.
Explicit-id adoption of a cold session-backed subagent under a *different*
cwd answered `session-conflict` because the cwd check ran before the
persistence inspection classified the identity. The api/commands.ts
contract states explicit-id `session.create` adoption rejects
session-backed subagents with `agent-busy` — ownership is an identity
property, so it must win regardless of the requested workspace.
Reorder the stored-session branch to inspect and classify ownership
first, then enforce the cwd match, making the response match the
documented contract.
When a generic `agentFor` cold resume loses the identity to a parent's
concurrent `enter()` — the collision rejection arrives from
`ctx.agents.resume` publication after the pre-resume re-check — the error
fell through to the `internal` mapping. Clients retrying then see a
transient-looking internal failure instead of the stable ownership error
that `ensureSession`'s `.catch` already produces for the exact same
published-winner case.
Mirror that re-classification in `agentFor`'s resume error path: after the
typed errors, re-check the registry and attached store and answer
`agent-busy` when the raced winner is subagent-owned. Adds a regression
test whose resume mock publishes the subagent winner before throwing the
ID-collision error.
`agentFor` fenced subagent ownership through the attached session store
(`ctx.sessions.get`) and only then returned a live registered agent. A
registered agent whose session is ever absent from the attached store —
an invariant nothing in this package guarantees — would therefore be
handed out through generic Host routing unfenced, bypassing subagent
delivery entirely.
Fence `live.session` directly whenever a live agent exists, and keep the
attached-store check only for the not-live durable classification.
`ensureSession`'s race `.catch` already fences `live.session`; this makes
the fast path the same check instead of an asymmetric weaker one.
`hasSubagentDescriptor` sliced the whole own-suffix events array on every
Agent-bound RPC — including each `session.prompt` and `sessions.models`
call on long transcripts — and `ensureSession` rescans the same suffix
after creation. Replace the slice-then-some with an indexed loop from the
seed boundary, so the classification is a plain O(suffix) read with no
allocation.
The fork button on a stopped assistant message was inert. Frozen
interrupted nodes carry a flow-ordering seq of turnEnd.seq - 0.9, and
session.fork takes a non-negative integer on the wire, so every such
request was rejected as invalid-params before reaching the host — where
an aborted turn's logged turn/end has always made it forkable.
SessionsService.fork floors atSeq at the wire boundary. Flooring stays
inside the anchor's own turn (every turn opens with turn/start), so the
host's first-turn/end-at-or-after cut still closes on that turn.
- 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.
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.
A first visit resolved to Chinese regardless of the browser: LocaleService
read `dsh.locale` and fell straight back to `zh` when nothing was stored,
ignoring the languages the browser already states it reads.
The initial locale now resolves through three ordered sources — the persisted
preference, then `navigator` (first entry of the ordered language list whose
primary subtag names a shipped locale, so `zh-Hans-CN` -> zh and `en-GB` ->
en), then `FALLBACK_LOCALE`. An explicit choice still wins and nothing writes
the detected locale back to storage, so "has the user chosen?" stays a
question only the stored value answers.
Specs asserting the shipped Chinese copy now state the browser they assume:
the web e2e scenarios open their page with `locale: ZH_BROWSER_LOCALE`, and
package specs pin it through the new `pinBrowserLanguages` test helper.
`settings-chrome.e2e.ts` gains an English-browser scenario as the
assembled-app proof.
workspace.archiveSession answers the full updated archive set;
workspace.list carries the set as the reconnect baseline; the host
stream pushes host/archived-sessions-changed full snapshots from the
domain/changed global-put branch (same posture as workspace-changed).
Unknown sessions map to the existing session-not-found code.