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
main went red on the merge commit (run 34579312780) with a real assertion:
`tests/agent-pty.spec.ts:413` expected `skipWait` to report 1 registered wait
and got 0. The cause is the test's own preparation, not the registry: an
interactive shell ECHOES what is written to it, so a terminal opened with a
command that contains the wait needle already has that needle in its
transcript by the time `waitFor` runs — `waitFor`'s documented fast path then
resolves the wait immediately and `skipWait` finds nothing to skip.
POSIX /bin/sh does not echo a command written to a non-tty, so this had been
latent for as long as the suite existed; PowerShell does echo it, which is why
it only ever failed on Windows, and why it took a Windows run to expose it
rather than a code change.
The three affected cases (skip / waiting-snapshot / wait-notification) now open
a BARE shell and wait for the shell to be up rather than for a printed marker,
via a new `waitForShellReady` helper that watches for output of any kind. That
removes both halves of the hazard: no needle in the command, and no dependency
on which words a given shell prints at startup. Nothing about what the cases
assert changed — they are about the registry's wait bookkeeping, which needs a
live terminal and no particular output.
Verified: tests/agent-pty.spec.ts 33 passed (and the three cases now finish in
milliseconds instead of waiting out a transcript poll), full suite 127 files /
1343 passed / 9 skipped.
The hairline outline I added to the colored tab glyphs reads as a thick stroke
in the live instance, so it goes: the glyphs stay exactly as the icon set draws
them, tinted only through the wrapper's token. What the feedback actually asked
for was the terminal SIZE, and that is all that remains — it renders a step
down (0.85x, floor 10px) because the VSCodicon terminal is a wide filled
rectangle that read heavier than its neighbours.
tests/builtins.spec.ts keeps pinning both readings (tasks = layers, terminal =
the shrunk VscTerminal with no style override), so the assertion now also
guards against an outline creeping back in.
The `--maxWorkers=2` fix from the previous commit was falsified by real CI:
run 34574847774 still ended with `[vitest-pool]: Worker forks emitted error /
Worker exited unexpectedly` while reporting 126 of 127 files passed and zero
FAIL lines. The worker that died was the one running
tests/native-surface.spec.ts — it produced NO results at all, which is the
same shape as the two earlier crashes (125/126 and 123/124: N-1 files
reported plus a silent worker exit at teardown).
So the consistent signature across all four crashes is a worker-recycling
race, not a test failure. `test:windows` now runs the whole suite in ONE fork
(`--maxWorkers=1`), which removes recycling entirely; the cost is wall-clock
(14 s -> 41 s locally, and the Windows step stays well inside its 30-minute
budget) and the signal is unchanged. Retries are still deliberately not used —
they would hide a real regression instead of removing the race.
The CI comment now records both Windows failure classes with their evidence,
so the next person can tell what this flag is for.
Two glyphs read wrong in the live instance:
- Tasks shows subagent sessions AND the host's background jobs, so a checklist
glyph described a page that does not exist. `VscLayers` (sheets stacked, i.e.
work running behind the conversation) says what the page is; the amber token
stays, so it still reads as live activity.
- The terminal was the widest glyph in the set and the only bare fill in a
strip of the plugin's own 1.5px-stroked icons, which made it look like a
heavy foreign block. It now renders a step down (0.85x the strip size, floor
10px) and every colored tab glyph gets a hairline outline of its own ink
(`stroke: currentColor` + `non-scaling-stroke`, so the line stays one device
pixel across the sets' mixed 16- and 24-unit viewBoxes; `paint-order: stroke`
centres it on the fill edge instead of fattening the shape).
tests/builtins.spec.ts pins both readings by glyph identity (tasks = layers,
terminal = the shrunk, outlined VscTerminal), so a future swap has to argue
with a failing assertion rather than with a comment.
Gates: typecheck / lint green, pnpm test 127 files / 1343 passed / 9 skipped.
- docs/plans/2026-09-11-file-icons-and-tab-glyphs.md: the design record —
what the two stacked PRs did, the measured upstream facts that made their
colored implementation redundant (rc.2's `FileTypeIcon`/`CodeFileIcon` with
the host's own palette, the classifier never returning a folder, the native
tab definition having no icon field while its title slot IS the chip), the
three merge conflicts and how each was resolved, what was deleted, the
catch-all semantics change, and the verification evidence.
- docs/plans/2026-09-10-dsh-0.1.5-rc.2-adaptation.md: cross-link it from the
change table.
- docs/external-plugin-guide.md: the file-icon fallback chain now ends at the
host's artwork, with the catch-all caveat spelled out; §12 loses its "one
exemption" paragraph entirely — the plugin draws no color of its own any
more (file icons are host pixels, tab glyphs ride tokens).
- AGENTS.md §5: replaces the icon-theme exemption with the two new rules
(file-icon chain + catch-all hazard, and where the colored tab glyphs live
plus the fact that the native chip glyph is drawn by the plugin inside the
host's title slot).
- README.md / README_EN.md: the v0.19.1 entry gains the icon section
(#611 / #531 / #594) with the dataset deletion and the new public API.
Issue #611 asked for file-type icons like the official explorer's, and #429
built a colored theme for them. DSH 0.1.5-rc.2 makes most of that work
unnecessary: `@deepseek-ai/dsh-client-ui-primitives` now exports
`FileTypeIcon` / `CodeFileIcon` with the host's own 48-type full-color code
artwork plus category-colored sheet glyphs (and its classifier behind them).
So the plugin renders the host's icons instead of shipping a color table, and
color becomes the only behaviour rather than an opt-in theme.
File icons (src/client/file-icons.tsx)
- `builtinFileIcon(path, size)` → `<FileTypeIcon path size />`; folders use the
host's folder category. The plugin's own per-extension VSCodicon map is gone.
- The public `registerFileIcon` API from #531 is unchanged and still outranks
the built-ins, so an external plugin can take over any row. One semantic
consequence, now documented and tested: because the host classifier covers
every path, a registered catch-all (`exts: []`) claims rows it used to lose
to the built-in map. An unregistered catch-all (the old icon theme's shape)
is unaffected.
- Nothing is lazy any more: the artwork lives in a platform module the client
already holds, so `lib/client-file-icons.js` and its 563-entry dataset (255
kB) are deleted outright — the core bundle grew by 10 kB (0.85 → 0.86 MB).
- `src/client/file-icon-theme.ts`, the `fileIconTheme` pref (prefs-shared /
config / client prefs), the settings-page select row and the six
`settingsFileIconTheme*` keys in all 20 dictionaries are removed with it.
Tab glyphs (src/client/builtins/tab-icons.tsx)
- Every built-in tab type now declares a colored glyph: files (the host's
folder artwork), changes/diff (commit), tasks (checklist), side chat
(discussion), terminal (shell), browser (globe).
- The color comes from a `--dsw-alias-*` token through a wrapper class, never
from a literal — the skin contract (§12) holds and `tests/theme.spec.ts`
enforces both halves (no literals in the module, every color declaration in
the stylesheet resolves to a token).
Native right Sidebar chips (src/client/native/tab-adapter.tsx)
- The host's tab definition has no icon field, but the
`sidebar.right.pane.tab.title` slot IS the chip's content, so the plugin
draws the glyph itself: an editor tab with a path shows that file's icon,
every other tab shows its descriptor's icon — the same `descriptor.icon` the
workbench strip and the guide capsules already render, so the three cannot
drift. The glyph is `aria-hidden`, which keeps the chip's accessible name
exactly the title the mount lane matches on.
Gates: typecheck / lint / check:consumer-types green; pnpm test 127 files /
1342 passed / 9 skipped (3 new chip cases, 1 rewritten built-in case).
Brings in the theme plumbing around #531's API — the `fileIconTheme` pref
thread, the settings row, the 20 dictionaries' keys and the theme loader's
test — so the working tree is a complete, green starting point before the
colored implementation is replaced.
Conflicts (three; resolved by taking this line's side where it had moved on):
- src/client/service.ts: the import block keeps `baseName, extOf` (the
`isNarrowWidth` import the theme branch needed belongs to its own
fresh-session seeding, which main has since rewritten) and the feature-list
doc comment keeps main's "v0.19.0 REMOVED 'floatWindows'" note while
adopting the sharper `fileIcons` wording (matched by exts / names /
folderNames).
- tests/plugin-shape.spec.ts and tests/prefs.spec.ts: main DELETED the store
seeding tests these files used to hold and rewrote the schema assertions
(the `SIDEBAR_PREFS_DEFAULTS` destructuring), so this line's side wins —
the theme branch's re-added blocks describe behaviour main no longer has.
Next commit removes `fileIconTheme` again along with the 563-entry dataset and
its lazy chunk: DSH 0.1.5-rc.2 exports the official `FileTypeIcon` artwork, so
the plugin renders the host's icons instead of shipping its own color table.
Brings in the plugin-side file-icon feature (issue #611): the public
`registerFileIcon` API plus the resolver chain the tree, the editor tab and
the external-plugin surface all read.
Conflicts (three, all comment/list unions — resolved by union, verified green):
- src/client/service.ts: main's "v0.19.0 REMOVED 'floatWindows'" note stays
(the capability is gone and must not come back); the feature list gains
'fileIcons'.
- src/client/Sidebar.tsx: one comment above `tabIconOf`; both sides' wording
is kept (the editor-with-path special case and the descriptor fallback).
- docs/external-plugin-guide.md: the `features` list comment keeps main's
"additive only, the one exception being floatWindows" wording plus the new
'fileIcons' entry.
Notes on what this merge does NOT bring in: #594 (feat/file-icon-theme) is
stacked on this branch and lands next, but its colored-theme implementation
(563-entry react-icons dataset in a 255 kB lazy chunk plus a `fileIconTheme`
switch) is being replaced — DSH 0.1.5-rc.2 exports the official
`FileTypeIcon`/`CodeFileIcon` artwork from ui-primitives, so the plugin does
not need to ship its own color dataset.
Gates on the merge result: pnpm typecheck clean, pnpm test 127 files /
1331 passed / 9 skipped.
ci-windows is green twice on 5451f5c — the PR-triggered run 34561189320 and a
follow-up workflow_dispatch run 34561457067, both 126 files / 1318 passed /
3 skipped with zero `Unhandled Errors` and zero `Worker exited unexpectedly`
(the window that motivated this work saw two of the latter). plugin-mount is
green twice as well: `added 521 packages in 24s` with no ERESOLVE lines
against 3062 in the historical OOM run, mount smoke 7 passed, aggregate lane
passed.
Also records the falsification this PR went through — the first cut still
failed on real CI in two places (the third 5000 ms default victim,
tests/git.spec.ts:85 at 9607 ms, and `--legacy-peer-deps` breaking the CLI
install with ERR_MODULE_NOT_FOUND) — and states plainly that two green runs
do not prove the worker-death root cause, only that it stopped reproducing
once concurrency was bounded and the timeout budgets were corrected.
The first run of this PR against real Windows and real npm found two things
the local macOS pass could not:
1. ci-windows lost a THIRD file to vitest's 5000 ms default —
`tests/git.spec.ts:85` ("caps status entries at the truncation limit")
took 9607 ms building a pathological untracked set with real git calls.
Three different files (git / powershell / agent-pty), one root cause, so
per-file timeouts were whack-a-mole: vitest.config.ts now sets a global
`testTimeout: 15_000`. The two heaviest suites keep their own 30 s budgets
and a genuinely hung case still fails.
2. `--legacy-peer-deps` made the mount lane's CLI install broken instead of
robust: it skips exactly the peers a global install has to supply, and
`@deepseek-ai/cordis-plugin-group` — required at boot by
`@deepseek-ai/dsh-app-boot` — is a peer, not a dependency, so the CLI died
with ERR_MODULE_NOT_FOUND (`Cannot find package
'@deepseek-ai/cordis-plugin-group'`) before the smoke could start. The
flag is removed; the pin to a fully-published rc.2 (no ETARGET) plus the
4 GB heap (no resolver OOM) is the whole fix.
Verified locally: a plain `npm install -g @deepseek-ai/dsh@0.1.5-rc.2` into a
scratch prefix (with NODE_OPTIONS=--max-old-space-size=4096) resolves the peer
(`dsh/node_modules/@deepseek-ai/cordis-plugin-group` present), that CLI reports
0.1.5-rc.2, and `DSH_CMD` pointed at it runs the mount lane green — 7 passed.
`pnpm test` stays green with the new timeout: 126 files / 1312 passed / 9 skipped.
Docs (plan §C, AGENTS.md §2, both READMEs' v0.19.1 entries) record the rejected
middle step and why, so nobody re-adds it.
- docs/plans/2026-09-10-dsh-0.1.5-rc.2-adaptation.md: the adaptation plan —
the upstream rc.1 -> rc.2 delta per affected area, why there is no
plugin-side code change, and the CI failure taxonomy (A real Windows
timeouts, B the two silent worker deaths, C the mount lane's npm
ETARGET/OOM) with the evidence and the fix for each.
- AGENTS.md §2/§3: CI pins rc.2, the mount lane's npm flags and their reason
(floating transitives + staged prerelease publishing), the Windows lane's
bounded pool and why, and the primitives-dependencies note extended to rc.2.
- README.md / README_EN.md: support badge (v0.19.1, verified on rc.2), the
install section's stable-release note (rc.1 hosts need no upgrade — the peer
floor is unchanged) and a v0.19.1 changelog entry covering the two CI fixes.
- docs/external-plugin-guide.md: baseline line -> v0.19.1 / DSH 0.1.5-rc.1+,
verified on rc.2.
- Comment-only: `DSH 0.1.5-rc.1` -> `0.1.5-rc.1+` where the note describes a
behaviour that arrived in rc.1 and is unchanged in rc.2 (native guide
description rendering, in src/client/native/index.ts, src/client/service.ts
and the three specs that assert it).
Two independent, evidence-backed defects in the 2026-09-09/10 CI window
(60 completed runs: 26 success / 17 failure; ci-windows|Test red 8 times,
plugin-mount|Install dsh CLI red 4 times).
1. Windows: six of the eight red ci-windows runs were genuine 5000 ms
vitest timeouts in specs that really start a process —
`tests/agent-pty.spec.ts` (`:71` / `:106`; a PowerShell + ConPTY pair per
terminal) and `tests/install-powershell.spec.ts` (`:105`; a cold
`powershell.exe` start measured 12.1 s). vitest.config.ts sets no
testTimeout, so both suites ran on the 5000 ms default, and
`waitForTranscript`'s own poll budget was *also* 5000 ms — the inner wait
consumed the whole outer budget, which is a structurally guaranteed
timeout, not a flake. Both suites now declare a 30 s budget (describe-level
options, verified working on vitest 4) and the inner poll drops to 15 s so
it stays strictly below the outer bound.
2. Windows: `ci-windows`'s Test step runs `pnpm test:windows`
(`vitest run --maxWorkers=2`, the script added with the baseline commit)
instead of `pnpm test`. Five specs spawn real processes, node-pty's ConPTY
helper and a cold powershell.exe are both console-bound, and an unbounded
fork pool on a 2-core runner is the amplifier behind both the timeouts
above and the two silent worker deaths (`Worker exited unexpectedly` with
0 FAIL and every file reported passing — one of them on a commit whose
sibling run was green). Local `pnpm test` semantics are unchanged; only the
Windows lane is bounded.
The worker-death mode itself is NOT proven fixed here: it does not
reproduce on macOS and the two occurrences have no same-commit rerun. This
is the evidence-backed part; the real Windows result decides whether more
is needed, and retries are deliberately not used to paper over it.
3. plugin-mount: `npm install -g @deepseek-ai/dsh@<ver>` never really pinned
anything — the pin's transitives are floating `^` ranges, so while upstream
publishes a prerelease in stages (rc.2 went out package by package between
14:43 and 14:57 UTC on 09-10) npm resolved a mixed rc.1/rc.2 peer graph:
3062 ERESOLVE lines then `Ineffective mark-compacts near heap limit`
(exit 134), or a hard ETARGET on a sibling that appeared 5.5 minutes (resp.
25 seconds) later. The same mechanism hit the alpha line on 09-09. The lane
now pins an already-complete rc.2 and passes `--legacy-peer-deps` plus
`NODE_OPTIONS=--max-old-space-size=4096` to decouple the install from the
upstream publish window; a mismatch would not be silent because the mount
smoke in the same lane boots the real shell.
The stale comment claiming rc.1 is both `latest` and `next` is corrected —
it documented a reality that had already changed.
The pinned baseline moves to DSH 0.1.5-rc.2 (npm `next`; `latest` is still
rc.1) while the plugin's peer floor stays ^0.1.5-rc.1, so rc.1 hosts need no
DSH upgrade to run this version. Nothing in the rc.1 -> rc.2 delta touches
this plugin: of 300 changed upstream files, the overwhelming majority are
single-line `version` bumps in per-package package.json files, and the only
real code edits are the message-feedback dialog, deliverables card CSS and
the CodeFileIcon SVG artwork split (`code-file-icon-artwork.ts`). There are
zero changes under packages/api|host|session|agent and zero type-surface
changes beyond one line in message-feedback/types.ts, so the adaptation is
the pin itself — devDependencies, the CI mount lane and
SIDEBAR_SERVICE_VERSION — with no plugin-side code change and no runtime
compatibility layer.
- package.json: devDependency pins 0.1.5-rc.1 -> 0.1.5-rc.2 (including
dsh-client-locale, which is still published on the DSH tuple); peers keep
the ^0.1.5-rc.1 floor; version -> 0.19.1
- dsh.plugin.json: version -> 0.19.1; engines.dsh keeps >=0.1.5-rc.1 (same
supported range as the peer floor — the two must not fork)
- pnpm-lock.yaml: lockfile realigned to rc.2
- src/client/service.ts: SIDEBAR_SERVICE_VERSION -> 0.19.1 (tests/service.spec.ts
asserts it equals package.json's version)
Verified locally on macOS arm64 against the rc.2 tree: pnpm install clean,
`pnpm peers check` reports no peer dependency issues, typecheck / lint /
check:consumer-types green, `pnpm test` 126 files / 1312 passed / 9 skipped.
The real-mount smoke (scripts/e2e-mount.sh with DSH_CMD pinned to rc.2) and
the aggregate double-mount lane both pass — evidence recorded in
docs/plans/2026-09-10-dsh-0.1.5-rc.2-adaptation.md.