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.
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.
- 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.
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).
Conflict: src/client/sidebar/use-host-feeds.ts
- ours: import { mirrorAgentWaits, ... } from '../state.ts'
- theirs: same line + new import { isNarrowWidth } from '../breakpoints.ts'
Resolution keeps both imports; the two sides' semantic changes are in
disjoint regions (ours: tab-disabled wait mirroring; theirs:
activateTasksPage native-landing) and both survive intact.
The v0.19.0 regression, its root cause in 223b254, the four decisions
(native landing, narrow-viewport park, jump-back always expands, bottom
workbench keeps its own flows), the change list, and the evidence — including
the environment trap that the PATH `dsh` on this machine is 0.1.2-rc.1 and
cannot register the `file:` tarball into a scratch profile.
Baseline moves to DSH 0.1.5-rc.1 — the 0.1.5 line's first release
candidate, and the npm `latest` dist-tag now: peer floor ^0.1.5-rc.1,
devDependency pins, the CI mount-lane pin and dsh.plugin.json engines
all follow. 0.1.5-alpha.2 users stay on v0.19.0-alpha.1.
The upstream delta from alpha.2 is small: 373 changed files, mostly
version bumps, sidebar preview polish and test snapshots. Exactly two
changes touch this plugin, and only one needs work:
- `SidebarRightGuideEntry.description` is back (optional). The native
guide renders it only while it lists at most 4 entries; a longer list
drops every description. The plugin restores `TabDescriptor.description`
and the six built-in `guideDesc*` lines (20 dictionaries) verbatim from
0b6ac5d^, but deliberately does NOT restore the old generic
`nativeGuideDesc` fallback: the host has no fallback of its own, so a
descriptor that declares none now ships no `description` field at all
and renders as a clean title-only capsule.
Consequence worth knowing: the plugin contributes six guide entries, so
the default composition is over the 4-entry limit and shows titles only.
The mount lane proves the restored field end to end by shrinking the
enabled set to three types through the plugin's own settings route
(scratch profile only) and requiring the Files capsule to grow its line.
- The built-in `files` type declares a guide description again and uses a
coloured folder glyph; the plugin owns that kind, so nothing to do.
Verified unchanged from alpha.2 (so nobody re-checks them): the global
main-panel model, the file address grammar (util/workspace-path changed
only its package.json), the native tab-body host contract (.paneBody is
still a block scroller with a definite height), the core/agent/session/
subagent host APIs and the ui-primitives export surface.
Restore caveat fixed in the lane: the settings route merges patches
key-wise, so undoing a temporary `tabsEnabled` change by posting the
original (empty) map leaves the disabled keys behind — the e2e restores
every key it touched explicitly.
Baseline moves to DSH 0.1.5-alpha.2 only (peer floor ^0.1.5-alpha.2,
CI pin, manifest engines); alpha.1 users stay on v0.19.0-alpha.0.
Host-contract changes handled:
- the native guide entry lost its `description` field (entries are
icon+title capsules now): `TabDescriptor.description` and the six
`guideDesc*` keys plus the `nativeGuideDesc` fallback are removed.
- the file address grammar changed: `fileAddressFor` always returns a
session-scoped address (absolute paths keep their leading `/`),
`parseFileAddress` is prefix-based and strips `?`/`#`. The plugin's
purity-gated mirror follows.
- global main panels replaced the `conversation` root slot with the
keyed `main` slot (`main.conversation`): the bottom workbench's
center-column locator now accepts both keys and skips
`display: contents` slot hosts.
Fixes a pre-existing fill bug the alpha.2 probe made obvious: the native
tab body host is a block scroller with a definite height, so tab roots
that only declared `flex: 1` collapsed to content height and the side
chat composer drifted off the pane bottom. The native adapter now wraps
every tab body in a `height: 100%` column flex host.
Also promotes the dependencies `dsh-client-ui-primitives@0.1.5-alpha.2`
dropped from its manifest (anser / shiki / mdast-util-* / micromark-* /
katex / @shikijs/langs) and the newly unmet `dsh-session-persistence`
peer into devDependencies.
The native new-tab list rendered one generic line ("opens in the DSH
sidebar") under every plugin entry, so six plugin rows read identically.
`TabDescriptor.description` now carries the real purpose (string or
locale-following thunk; absent falls back to the generic line), and the
six visible builtins declare theirs across all 20 dictionaries.
- service.ts: `TabDescriptor.description` + docs (guide §4.1, §0 row).
- native/: the guide entry's description follows the descriptor; the
`files` takeover reuses the editor's line.
- builtins: files / changes / tasks / side chat / terminal / browser.
- tests: builtins assert every visible type declares a distinct line;
the lifecycle test asserts descriptor lines (and the takeover reuse).
DSH 0.1.5 owns the right column (its native Sidebar hosts every tab type
this plugin registers), so the plugin's own right panel, its width drag /
new-session seed prefs, and the free-window feature are gone. The bottom
workbench is the only plugin-owned surface left, and its expand/collapse
control is registered into DSH's session-header utilities list slot (the
header's corner seat belongs to the native sidebar's own control).
- state: one `bottomSplits` tree; `makeDefaultState()` takes no arguments;
`toggleBottomPanel` / `openTabInBottomPane` are the only landing
reducers; `setBottomHeight` and `sanitizeState` share the
`CONVERSATION_MIN` cap (a persisted tall height no longer snaps on the
first drag).
- service: every landing in the workbench (creation or focus) expands it;
`SIDEBAR_FEATURES` drops `'floatWindows'`; the open/close/activate tab
pools cover the single tree.
- shell: `Sidebar.tsx` renders only the bottom workbench; the toggle lives
in `sidebar/bottom-toggle.tsx`; layout-push pushes height only (no width,
no `--dsh-sidebar-width`).
- prefs: `openByDefault` / `defaultWidthPercent` / `changesDiffFloat`
removed from the schema, defaults, client parser, settings page, and all
20 dictionaries; the retired keys in stored documents are ignored.
- tests: free-window specs deleted, panel/float assertions dropped,
remaining suites adapted (124 files / 1287 tests); the mount lane asserts
the header toggle and sweeps the native sidebar's guide page.
- docs: README / README_EN / guide §0,§11 / AGENTS test map.
Phase 2 of the 0.1.5 adaptation: the plugin stops drawing its own right
panel and contributes tab TYPES to DSH's native right Sidebar, which owns
the right column. The bottom workbench (terminal dock) stays plugin-owned.
Registrations (src/client/native/):
- every TabDescriptor becomes a native tab type (kind = descriptor.id, the
default `extension` band) plus a keyed body in `sidebar.right.pane.tab`
and a live chip title in `sidebar.right.pane.tab.title`
- the `editor` type also claims `dsh-resource://file/**`, so it outranks
the built-in `text` preview (fallback) and every file the product opens
lands in the plugin's editor
- the built-in `files` page kind is TAKEN OVER by an extension
registration of the same kind: `openTab('files')` draws the plugin's
explorer, and the built-in resumes when the plugin unregisters
- registrations follow the plugin registry + the side-card settings: a
type registered later (external plugin) gets its native type too, a
disabled type is unregistered (absent from the guide, refuses opens)
Adapter (src/client/native/tab-adapter.tsx):
- one synthetic plugin `SidebarTab` per native tab id, minted from the
native record + navigation params (or the descriptor's own createTab,
which a native guide open bypasses), kept live across navigations and
updateTab writes
- the explorer's per-tab expansion/reveal sets and the per-kind instance
counter, which the plugin's own layout used to hold
Service routing (src/client/service.ts):
- `setSurface` installs the native write face; openTab/openFile land
natively (a path becomes a resource address, a path-less editor open
becomes the `files` page, the rest carry the seed as navigation params),
closeTab/updateTab/activateTab recognise native tab ids
- new optional `OpenTabSeed.target` ('right' default, 'bottom' keeps the
bottom workbench); the bottom panel's own + menu and auto-terminal pass
'bottom'
- src/client/native/surface.ts probes the controller's per-session writes
(`openTabIn`/`openResourceIn`/`closeIn`, not in `ISidebarRight`) and
queues opens for a session whose surface is not mounted yet
Right panel retired behaviourally (Phase 3 deletes the code): Sidebar
renders its bottom workbench only, keeps the panel collapsed, and hides
the right-panel toggle. The panel-specific e2e specs are removed; the
mount lane now drives the native guide (tab sweep), the plugin explorer
inside the native pane (editor/mermaid/README chunks) and the sidechat
host routes.
Verified: pnpm typecheck / lint / build / test (126 files, 1349 tests) /
check:consumer-types, plus pnpm test:mount against a real
@deepseek-ai/dsh@0.1.5-alpha.1 host (7/7).
Move the plugin onto the DSH 0.1.5-alpha.1 baseline (peer floor
^0.1.5-alpha.1, CI pinned to the alpha dist-tag) and fix the two host
contract changes that alpha line introduced.
Baseline:
- all @deepseek-ai/dsh-* peer/dev deps -> 0.1.5-alpha.1; optional peer
@deepseek-ai/dsh-client-ui-sidebar-right (declared, unused until the
native-surface phase); devDep @deepseek-ai/dsh-agent-loop (inbox fold)
- dsh.plugin.json engines >=0.1.5-alpha.1; version 0.19.0-alpha.0;
SIDEBAR_SERVICE_VERSION follows
Live assistant stream (assistant/chunk is gone):
- 0.1.5 publishes in-flight model deltas as process-local
agent/assistant-stream frames instead of durable assistant/chunk events
and embeds the settled stream in assistant/message / assistant/attempt.
src/assistant-live.ts folds the frames into a bounded per-session buffer;
sidechat.events serves the current attempt as `live` rows (replaced every
poll, never a delta) and the transcript renders them; the inherited
in-progress snapshot reads message content / attempt stream / live frames.
Cold session reads (persistence.inspect is gone):
- 0.1.5 reads a stored session through an explicit read handle
(open(id,'read') -> handle.read() -> close()). src/session-store.ts owns
that protocol; every cold read (sidechat transcript, cold-resume preset,
session.cwd, changes replay) goes through it.
Dead code and docs:
- drop the remote.session.openWorkspacePath interception: 0.1.5's chat opens
files through ctx.sidebarRight.openResource instead, so the wrapper had no
caller and would hijack the host's "open in app" gesture; the
interceptOpenPath pref, its settings row and its locale keys go with it
- AGENTS.md §3 + external-plugin-guide.md describe the new baseline
- vitest setup bridges Node's localStorage accessor to jsdom's store (the
pre-existing 26 failures on Node >= 22)
- desktop-layout e2e seeds through host RPC and types into the Lexical
composer (0.1.5 replaced the textarea) instead of driving the directory
picker
Verified: pnpm typecheck / lint / build / test (124 files, 1327 tests) /
check:consumer-types, plus pnpm test:mount against a real
@deepseek-ai/dsh@0.1.5-alpha.1 host (18/18).
Escape the .pdf extension in the op-target regex (the unescaped /.pdf$/
matched any path ending in pdf) and fold the copy-pasted toggle button
into PaneToggle; record the sixth deliverable in the design doc.
Records the five features shipped on this PR (md reading mode incl.
mermaid, highlight expansion, secret redaction, html render mode), their
deliberate boundaries (redaction heuristics, saved-file render
semantics), and the 2026-09-06 standards pass.
sidechat.start passed `seed` without `meta.isSeeded: true` +
`inheritedEventCount`, so dsh-session treated the whole seed as the
child's OWN events and the child's Inbox constructor replayed the
parent's `agent/inbox/spliced` history — inheriting whatever input sat
UNCLAIMED in the parent at the click moment:
- next-turn: a queued follow-up behind a still-running turn;
- next-step (the reported long-context case, parent queue empty): a
tool-result context / steering spliced between step boundaries.
The first side prompt then claimed [phantom, boundary, question] — the
stale parent message was sent to the model BEFORE the boundary and
logged as a user/message. The fix passes the exact marker pair the
host's own session.fork uses (api-session-controller); ownEvents() now
stops at the end-seed boundary and the inherited inbox replays empty.
Regression tests run the REAL Session.create + Inbox in both
directions (old shape inherits the phantom, marked shape stays empty);
routes tests pin the two create options. Verified: pnpm typecheck /
test (5 pre-existing unrelated UI-spec failures on main unchanged) /
build.