The `subagentchat` aside open failed silently: the host's `parseSubagentChatAddress`
(the tab type's `canOpen`) rejects a MISSING mode (`get("mode")` → `null`), so a mode
we could not stand behind — omitted to dodge the controller's 401 — made `claim` refuse
the address (`sidebarRight: tab type "subagentchat" refuses ...`), and the right column
never revealed.
Fix: the codec now normalizes an unknown/undefined mode to an EXPLICIT `mode=unknown`
instead of dropping it. That value passes both gates:
- client: `parseSubagentChatAddress` accepts `unknown` → `canOpen` true → claim accepts;
- server: `validateAddress` skips the compare when `mode === "unknown"` → no 401.
A real (one-shot/continuable) mode is still emitted when the host catalog is `ready` and
has the child; only an honest `unknown` is sent when it is not.
Drops the TEMP `diag.append` probe route + the client before/after diag fetches (their
mission is done) and rewrites the two spec cases to assert the explicit `mode=unknown`.
Verified with the host's real `parseSubagentChatAddress` pulled verbatim from the app
bundle: the old no-mode address → canOpen false (reproduces the refuse), the new
`mode=unknown` address → canOpen true. typecheck / lint / 29 subagent+sidechat specs green.
The aside button could hit `subagent/unauthorized: subagent mode does not
match the supplied address`. The host's session-controller (dsh-api-session-
controller validateAddress) compares the `mode` in the resource address
against the child's durable descriptor mode and rejects any mismatch. The
address `mode` came from the row, and on the degraded path the row's mode is
a best-effort guess from the summary `running` flag (running => continuable)
— so a child that is genuinely one-shot but still running was guessed as
continuable and the very open it was meant to trigger 401'd.
Two layers, so it holds on both the real-catalog and the degraded path:
- subagentChatAddress: only pass a mode through when it is one of the two
durable values (`one-shot` / `continuable`). Anything else is omitted,
which the controller reads as `mode: "unknown"` and therefore skips the
comparison — the safest form when the caller does not actually know.
- openChildAside: prefer the host's real descriptor mode for the child
(the `subagentsByParent` entry carries the durable mode) over the address
mode, falling back to the address only when the real catalog never arrived.
Gates: typecheck / lint / build / test 132 files 1392 passed.
The host's own catalog puts an "open beside" action on every non-current
child row; this port had never wired it. Add it: the row grows a hover
button that opens the child's chat as a `subagentchat` resource in the
native right Sidebar, so a user can watch a child work without giving up
the pane he is in (the in-pane jump stays the plain row click).
- src/client/native/subagent-chat.ts: the resource-address codec, copied
from @deepseek-ai/dsh-client-ui-subagent rather than imported (the
package exports no helpers and the plugin must not reach into it).
- native/surface.ts: carry `kind` / `preferNewPane` on an open.
- SubagentView: `openChildAside` via `ctx.get?.('sidebarRight')` — an
optional probe, since a fake ctx without `get` otherwise crashes the
whole render (9 specs caught it) — threaded through all three
CatalogRows call sites.
- locales: `subagentOpenAside` in zh/en plus all 20 third-language
dictionaries (tests/locales.spec.ts enforces key-set parity).
- docs: the conventions page now documents the resource-open recipe.
Gates: typecheck / lint / build / test 132 files 1390 passed /
check:consumer-types, plus the repo's runtime-contract symbol check.