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.
0.2.0-rc.2 withdrew every catalog/subagent seam this fork was written
against, and all three calls degraded to silent no-ops:
openSubagent(address) -> gone
setSubagentCatalogOpen(id, open) -> gone
refreshSubagents(id) -> gone
The host's own subagent UI (dsh-client-ui-subagent) rebuilds a parent's
child catalog with `sessions.refreshProjections(parentSessionId)`, so
that is the only kick that still exists. Wire it into `observe()` and the
catalog retry button, keeping the legacy names as fallbacks for older
hosts. Without it the root catalog never hydrated: the page sat on
"loading" or degraded to synthesized summary rows.
Also document what the investigation settled:
- docs/subagent-state-enumeration.md: the full state model (catalog
child / diagnostic / session / job / sidechat) and proof that this
fork already renders it 1:1 with upstream.
- docs/plugin-conventions.md: the hard rules another sidebar plugin
must follow (host schemastery, volatile prefs, ns = profile entry id,
no colons in tab ids, never read a volatile field directly) plus the
withdrawn-API replacement table and a pre-release checklist.
- docs/feedback-0.2.0-rc2-contract-gaps.md: gaps 2/3 reframed. The ask
is documenting the official paths (open(childSessionId) /
refreshProjections(id)), not adding new APIs.
Gates: typecheck 0, lint 0, build 0, test 131 files / 1385 passed.
Every side-card switch was dead: settings.update returned OK and
cordis.patch.yml was written, yet the running plugin never saw the new value
and nothing reported an error.
Root cause: the plugin declared Config with the plain npm `schemastery`
(3.18.0), which has no volatile support. The host resolves plugin config via
`runtime.Config['~standard'].validate()` — i.e. the plugin's OWN schemastery —
so `meta.volatile` nodes were never wrapped in cosmokit volatile references.
`cordis-plugin-loader`'s `_commitVolatile()` bails out as soon as
`volatileEntries(fiber.config)` is empty, so every volatile-only edit was
silently dropped: no in-place update, no plugin restart.
Fix: import z from '@deepseek-ai/schemastery' (peer provided by the host at
mount; pinned 3.18.4 as a devDependency for the dev tree), matching the
repo's existing pnpm-workspace convention for @deepseek-ai/* peers.
Verified: probe reports tabsIsVolatile=true and revision climbing 0->6 across
six writes, each read-back matching the written value (previously the value
was frozen and revision stuck at 1).
Also in this change:
- apes tab ids lose their colon (apes:tasks -> apesTasks, ...): the host's
settings projection dropped map keys containing a colon.
- sidebar prefs ns follows the profile entry id (`better-sidebar`).
- subagent rows degrade to summary rows when the root catalog never hydrates.
- docs/feedback-0.2.0-rc2-contract-gaps.md: four host contract gaps, gap 4
documenting the silent volatile-write drop above.
Gates: typecheck 0, lint 0, build 0, test 131 files / 1385 passed.
The Subagent/任务管理 page pinned on a permanent '加载中…' spinner
whenever the host's per-parent subagent catalog (subagentsByParent)
never hydrated for the root session — rootCatalog stayed undefined
while the session mirror already listed the children, so
summaryBackedLoading was true forever (SubagentView.tsx summaryBacked
gate). Root cause: a host-side hydrate gap (e.g. the
agent/disposed listener TypeError breaking the refresh chain) leaves
setSubagentCatalogOpen unanswered, and the UI had no escape.
Add a 5s hydrate timeout: once setSubagentCatalogOpen has had a chance
to land and the root catalog is still un-hydrated, render the known
children from the session mirror as CLICKABLE summary rows
(SummaryFallbackRows) instead of the infinite spinner. The timer resets
on root switch / catalog hydrate / page hidden. mode is inferred from
the summary's running flag (best-effort transport hint).
Verified: lint 0 / typecheck 0 / test 131 files·1385 passed / build 0.
Rollback: git revert <this-sha> or reset SubagentView.tsx to 99bef0f.
eslint was red at HEAD (5b715ac) in files untouched by this branch: unused statSync/dirname (check-plugin-api.mjs), unused PrefsSchema (src/index.ts, tests/smoke.spec.ts) and two Function types (tests/smoke.spec.ts). No behaviour change.
C6 removed apes:tasks as a duplicate of the host session panel; in practice the two answer different questions (host panel = switch workspaces, plugin tab = who is running right now), so the tab is restored and the info layer goes back to five types.
tasks: live session list (current / running / subagent markers) + showInactive toggle via pluginSettings['apes:tasks']; messages: placeholder that states the host has no message feed yet; both get badge counters, and the whole layer now honours the visible gate (auto-collapse) so hidden tabs stop waking on session events.
zh/en/ja dictionaries in sync (AGENTS 5); wiki 02/03/07 reverted from three to five tabs; decision record in docs/plans/2026-10-11-apes-tab-revival.md.
A/B fix per WorkBuddy review:
- browser-globals.ts: add addEventListener/removeEventListener/dispatchEvent
to window stub, add compatMode to document stub
- Remove // @vitest-environment jsdom from 5 specs that only use
renderToString/createElement (SSR, no DOM needed)
Test Files 131 passed (131), Tests 1367 passed, EXIT=0
PeerDependencies changed from ^0.1.5-rc.1 to ^0.2.0-rc.2 to match
DSH Desktop 2.0.17's built-in dsh packages (Harness v0.2.0-rc.2).
Rollback anchor: git revert this commit to restore 0.1.5-rc.1 peerDeps.
Context: DSH 2.0.17 release notes state '新版内核会检查插件声明的
兼容版本,不兼容的插件可能被跳过'. This is Plan D (force-install
via peerDeps bump) — if runtime API breakage occurs, fall back to
Plan A (rebase onto upstream dsh-better-sidebar 0.25.0).
On DSH Desktop the plugin's panel layer covered host modals — the community
market dialog (dsh-community-market, which renders into the shell's own
`shell.overlay` slot) had its lower-left corner clipped by our workbench.
The fence is not a z-index mistake on either side. The shell's framed modes do:
const contentViewport = document.getElementById('root')
document.body.dataset.dshDesktopMode = environment.mode
/* …and tags #root with data-dsh-desktop-content-viewport */
with
body:is([data-dsh-desktop-mode="compatibility"], […"extended"]) [data-dsh-desktop-content-viewport] {
isolation: isolate;
}
`isolation: isolate` makes #root a stacking context (z-index: auto) wrapping the
whole app INCLUDING the shell's overlay layer (`.dshDesktopOverlay`,
z-index: 1000) that hosts plugin overlays. Our host was appended to
document.body — OUTSIDE that context — and because it is a positioned element
with z-index 25 it painted above the entire context, so the market's 1000 never
competed. The shell's overlay layer is the boundary plugins are meant to sit
under, so the fix is to join the layer, not to renumber ours: panelHostParent()
mounts the host into #root when the shell framed one (body otherwise, i.e. the
plain web deployment keeps today's behaviour), where the documented 25 keeps
its meaning — above the app frame (z-index auto), below the overlay layer.
The re-anchor guard now re-appends into the resolved parent instead of always
`document.body`, and also watches `data-dsh-desktop-mode` on <body> (the same
shell effect that stamps the viewport) so a shell that frames #root after our
mount still gets the host moved.
Verified: typecheck clean; build clean; 1367 passed / 9 skipped (131 files,
+3 parenting specs). Revert is one line per call site if the shell's overlay
layer turns out to sit below our panels for a reason not visible in the CSS.
The preview showed nothing but the bare word `forbidden` in DSH Desktop. The
cause is not this plugin's routes but the host, one layer above them:
`DesktopWebServer` wraps EVERY route with `permits()`, and in the default
isolated/扩展 mode `ordinaryBrowserEnabled` is false, so a request is served
only when it carries the Electron renderer capability
(`x-dsh-desktop-renderer`). That header is attached solely to MAIN-frame and
SAME-ORIGIN-subframe traffic (`requestComesFromRendererOrigin`), and a frame
sandboxed WITHOUT allow-same-origin is an opaque origin at request time — so
the preview document request could never be served, while every other sidebar
route (all main-frame) worked fine.
The fix keeps the sandbox boundary exactly where it was and moves the delivery:
the document is fetched by the MAIN frame — which the host does trust — and
handed to the frame through `srcdoc`. New shared component HtmlPreviewFrame
owns that path plus the sandbox tokens for BOTH surfaces (the editor viewer and
the changes tab's op render preview, which had no escape hatch at all), so the
two can no longer drift; a failed fetch falls back to handing the frame the
route URL, leaving the route's own answer visible instead of a blank pane.
`withPreviewBase` injects `<base href="<the file's route directory>">`: a
srcdoc document has no URL of its own, so without it `./style.css` would
resolve against the GUI page. A document that declares its own base is left
alone.
Two consequences, both deliberate:
- sandboxed: the frame stays opaque, so a self-contained document previews
completely while a multi-file one shows its markup with the relative assets
missing (those requests are opaque-origin too — inlining them is a separate
job);
- unlocked (the warned per-surface unlock, or the setting): the srcdoc frame
inherits the GUI origin — which is what that escape hatch already meant — and
there relative references resolve and load like any other same-origin
request, so multi-file previews work in that mode.
Verified: typecheck clean; build clean; 1364 passed / 9 skipped (130 files,
+13: the pure base-rewrite spec, the delivery spec, and the updated
sandbox-contract assertions, which now pin srcdoc + no route src instead of
the old route-src shape).
Brings 5a7e213 (the v0.19.1 re-base) onto main. The merge result is deliberately
the sync branch's tree verbatim: every file both sides touched is upstream
v0.19.1 + our rename, and the v0.18.0-era content main still carries for those
files (including the two package.json.bak.* scratch backups, FreeWindow, the
hand-written openpath interception and the v0.18.0 service surface) is
superseded rather than textually merged — a hunk-level merge of a 135-commit
upstream jump would be a guess, not an integration.
Net effect on main: the fork now runs on the base that registers its content
into DSH's OWN right sidebar, so the duplicate second sidebar and its duplicate
toggle cluster are gone. The fork's own contribution is unchanged and intact:
the apes:* tabs (agents / apps / profile) in src/client/apes/, the switch.sh
two-line switcher, and the fork identity (1.2.0).
Tree verified identical to sync/v0.19.1 (git diff --quiet) before this commit.
The fork was pinned at upstream v0.18.0. That is the entire reason the window
ran TWO sidebars and TWO near-identical panel toggles: DSH 2.0.10 ships its own
right sidebar (@deepseek-ai/dsh-client-ui-sidebar-right), and upstream only
adopted it in v0.19.0-alpha.1 — which arrived after our fork point. v0.19.1 is
also the one upstream line whose support range actually matches our host
(engines.dsh >=0.1.5-rc.1, 14 DSH peers ^0.1.5-rc.1, host = 0.1.5-rc.2), so it
is the only tag the host's startup precheck accepts.
What upstream v0.18.0 → v0.19.1 brings
- src/client/native/{index,surface,tab-adapter}.tsx: every tab the plugin can
draw becomes an OFFICIAL native tab type + body, and an external plugin's
descriptor registered later gets its native type too. This is what retires
the fork's own right panel — and with it the duplicate toggle cluster
(`toggleCluster` is gone from v0.19.1).
- native tab-chip icons via the `sidebar.right.pane.tab.title` seat, coloured
built-in tab icons, file/folder glyphs from DSH's own `FileTypeIcon`.
- guide descriptions (DSH renders them while the guide lists <= 4 entries),
tab bodies that fill the panel, and the 0.1.5-rc.2 adaptation work we were
carrying by hand (peer/devDep pinning, primitives' transitive devDeps).
How the sync was done
Rebuilt the tree from the v0.19.1 tag rather than resolving 53 conflicted
files: our v0.18.0 delta was measured first and is only (a) one mechanical
rename `dsh-better-sidebar` -> `dsh-apes-sidebar` and (b) ten real edits. So:
tag tree + rename (scoped: code/config/scripts/tests/README, while the 46
historical docs/plans records and switch.sh's upstream package name stay
untouched) + re-applied deltas + our apes module.
Fork deltas re-applied on the new base
- src/client/apes/ (icons / locales / tabs): the three apes:* information-layer
tabs (agents / apps / profile), registered through the same BetterSidebarService
as the builtins — which is exactly why they now ride upstream's native adapter
into the official sidebar for free.
- identity: package.json (name/version 1.2.0/description/forkedFrom), dsh.plugin.json
(id/version), SIDEBAR_SERVICE_VERSION 1.2.0, README fork notice,
docs/external-plugin-guide.md.
- scripts/switch.sh + the pnpm switch* scripts (apes <-> upstream two-line switch).
- tests/apes-tabs.spec.ts (unescaped: it starts from the v0.19.1 base).
Dropped
- the Phase A native-registration spike (`apes/native-tabs.tsx` + its spec): it
re-derived by hand exactly what upstream's native layer already does — the
hand-written bridge is dead weight now, and v0.19.1's own e2e/jsdom lanes
cover the same seat contract far better.
- the unused apesProfileGuideDesc key and the two package.json.bak.* backups.
Verified on the new base: typecheck clean; build clean; 1351 passed / 9 skipped
(128 files), up from 1255 on the old base.
Phase A spike of the C8 root fix: instead of only rendering in the fork's
own shell, apes:profile now also registers with DSH's OFFICIAL right
sidebar through its documented two-stage extension protocol:
1. ctx.sidebarRightTabs.register({ id, kind, priority:'extension',
title, guide }) — the type declaration
2. ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register(
{ name:'sidebar.right.pane.tab', key: id }, Body)) — the keyed body
The registry is reached through ctx.inject(['sidebarRightTabs'], ...) (the
same pattern intercept.tsx uses for the async remote.session namespace), so
activation order cannot strand the bridge and a deployment without the
official sidebar stays a silent no-op — the fork's own shell is untouched.
The body reuses the existing ApesProfileView (content is shared, never
duplicated); only the shell-scope binding differs. Guide entry order 70
keeps the apes trio's position in the official entry list.
Verified: typecheck clean; build clean; 1255 passed / 9 skipped (120 files,
6 new bridge specs pinning the two-stage contract, both no-op paths, and the
HMR-safe unwind).
Local commit only — not pushed until the in-app spike is confirmed.
The apes tab glyphs (智能体/应用/画像) were fill-only paths spanning only
10-12 of the 16 viewBox units, so they rendered visibly smaller and lighter
than the DSH native glyphs (which fill ~92-96% of the box) in the + new-tab
menu and the right-panel tile grid.
Rewrite them in this repo's own outline recipe (src/client/icons.tsx): 16x16
viewBox, artwork on 1.5-14.5, 1.5px currentColor stroke, round caps/joins.
Renamed IconAgentFill16/IconGaugeFill16/IconUserFill16 ->
IconAgentOutline16/IconAppsOutline16/IconUserOutline16.
Root cause: DSH's @deepseek-ai/dsh-client-ui-primitives only ships
outline variants (mask+stroke) for agent/gauge/user icons, while native
tabs use fill:currentColor paths (solid, heavy visual weight). This made
the apes tabs look 'lighter' than the official ones in the + menu.
Fix: custom fill-style SVG icons in src/client/apes/icons.tsx
(IconAgentFill16, IconGaugeFill16, IconUserFill16) — matching the
fill:currentColor path style of DSH native tabs (IconCodeOutline16,
IconFolderOpen16, etc.).
Also: order 40/45/50 → 60/65/70 (avoid clash with terminal=40,
browser=50 in the native tab order).
C1-C5 env repair:
- align all @deepseek-ai/dsh-* peer+devDeps to 0.1.5-rc.2 (DSH 2.0.10 ships it)
- fix 0.1.5-rc.2 npm packaging defect: 15 primitives runtime deps (anser,
shiki, katex, marked, ... ) were mis-declared as devDeps in the published
package -> add to local devDependencies so vitest can resolve them
- fix sidechat-seed contract: dsh-session@0.1.5 assertAssistantSettlementShape
now requires a top-level data.stream: Array on assistant messages -> seed
helper emits stream: []
C6 slim tabs:
- remove apes:tasks (overlaps the official DSH session tree in the native
sidebar); keep apes:agents / apes:apps / apes:profile
- clean dead code: ApesTasksView + job helpers import + apes i18n dead keys
(tasks/status/duration) and the unused menu-title keys
C7 style align:
- apes view inline colors -> DSW alias tokens (--dsw-alias-bg/border/label/
state-success/interactive) with literal fallbacks; geometry (font/spacing/
radius) stays inline (DSH exposes no --ds-* geometry tokens to plugin panels)
gate: typecheck ok / build ok / test 119 files · 1249 passed