The catalog action (diagnostics, relative times, loading/error/retry,
mode and activity labels, branch toggles, descendant counts, tree aria)
and the read-only composer were hardcoded to Simplified Chinese, so an
English-locale session rendered mixed-language UI. Register a `subagent`
locale namespace (zh source of truth + en dictionary), declare it on both
slot registrations, thread the locale `t` seat through the components, and
mount the locale service in the plugin specs.
The UI spec's zh assertions now run against the real dictionary through a
`t` stub that interpolates `{name}` params exactly like the locale
service.
A removed session can no longer be the delivery owner of its continuable
children, but the `host/session-removed` handler only reconciled the
removed row's own activity. `parentAvailable` was updated exclusively from
`refreshSubagents` success, and removal schedules no catalog refresh — so
after the parent's Activation detaches, an addressed child kept a writable
editor against a dead continuation owner until an unrelated refresh (or
forever, for a closed menu).
Flip `parentAvailable` to false on the owned catalog and push
`handleSubagentParentAvailable(false)` to every addressed child Session at
removal time, matching the refresh path's notification. New Session
instances already read `parentAvailable` from the catalog, so they inherit
the invalidated state.
Adds a regression test: removing the catalog's owning parent flips the
snapshot's `parentAvailable` and notifies the addressed child instance.
`refreshSubagents` single-flights per catalog owner: a request arriving
while a pull is in flight returns the in-flight promise and is silently
coalesced into it. The in-flight response was requested before the
triggering change, so it can never contain that change — a debounced
membership refresh (50ms after `host/session-added`) firing during a slow
pull therefore lost the new child, and the catalog stayed stale until an
unrelated trigger (reselection, menu reopen, reconnect).
Mark the owner stale on coalescing and re-arm one trailing pull in the
settlement `finally`, so every membership change observed during a pull is
carried by a follow-up refresh exactly once. Bounded: the trailing pull
only runs when a refresh request was actually coalesced, and a new
coalescing during the trailing pull re-marks the same set.
Adds a fake-timer regression test: a `host/session-added` debounce firing
mid-pull yields exactly two `subagent.list` calls and the catalog
eventually contains the new child.
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.
A sandbox header bought isolation by taking the document's origin away, and
measuring that cost decided against it: the reported artifact throws
SecurityError on load, and because an uncaught exception aborts the rest of
its <script>, every listener declared after that line — theme toggle, mobile
menu, model tabs — never binds. Two of the four artifacts in the reporting
user's workspace were dead pages under it, and they still looked right.
A second listener on the API's host, answering /f and nothing else, is the
same boundary without the amputation: cross-origin to /api (refused by the
Origin fence and by CORS), same-origin with itself (localStorage, cookies and
fetch all work). Its port is published into the index page; the browser half
reads it to address previews, and its absence — the keyless fixture lane — is
what makes a file row fall back to the Host opener instead of a dead tab.
fileUrl moves from IWorkspaces to ConnectionHandle: the transport owns both
the listener that serves the bytes and the port that addresses it.
Isolation is restored on the premise the review corrected: a workspace file
need not be agent-authored — a read row makes every file in a cloned
repository openable — and a same-origin active document was measured driving
/api/settings.describe to a 200 with full data. Script-capable documents go
back into an opaque origin; the preview's lost localStorage is the known cost,
and a separate serving origin is the way to retire it.
- confine(): a workspace rooted at a filesystem root has a realpath already
ending in the separator, and the doubled prefix 403'd every child.
- turnDeliverables(): reset on the turn boundary, not only at a closing
assistant, so an interrupted turn cannot spill into the next turn's row;
and recognize a mutation by render intent (diff card, or generic with
kind 'edit') so str_replace_editor's insert counts.
- 405 answers name the methods it allows.
- The e2e now cold-seeds a recorded WRITE turn, so the assembled application
covers the Produced row, its chip's served URL, and the isolation header.
- Agent Note matched to what shipped (the row is in this PR, not deferred);
ui-conversation README documents the new destination and the row; the
fixture lane's dead-tab quirk and the cold-path listing cost are recorded.
A preview lost localStorage and cookies under CSP sandbox — measurably, the
reported artifact throws SecurityError on load and its theme toggle goes
dead. The capability the sandbox denied is one the file's author, an agent
already holding this user's shell, never needed the browser for, so the
header sat behind a trust boundary it had already crossed.
Isolating a preview becomes a real question when workspace content stops
being the viewer's own; the answer then is a separate origin, not a header.
The paths come from the mutation tools' follow-along locations, not from the
closing prose, so a turn's output is listed whether or not the model named
it. Each chip opens through the same openFile the tool rows use.
Reads contribute nothing (looking at a file does not produce it), a failed
mutation contributes nothing, a file touched twice is one entry, and the row
shows six with an explicit remainder rather than burying the answer.
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).