Rebases the whole branch onto main's new 0.1.7-rc.1 baseline and lands the
adaptation the new host contract requires: the ui-primitives icon family rename
(Icon<Name><14|16> -> Icon<Name>Regular/Medium, with the 14px defaults pinned
back explicitly) and the StateDot redraw, the rewritten dsh-settings surface
(entry-id namespace, DSH schemastery with collected volatile returns,
configure auto:false), session format v4 (producer-owned source kind plus the
rewritten tool-result shape, parsers accepting both), the client snapshot move
to projectionsBySession.subagentCatalog with the jobs.list route behind it, the
^0.1.7-rc.1 peer floor with vendor and bare-import devDeps, the reduced chunk
set mirrored in all eight places, and the capability hand-backs (read-only
previews, terminal, browser).
Verified: typecheck and lint clean; 119 files / 1200 tests; build with no old
contract residue in the bundles; mount e2e 7/7; the tasks page still passes its
70-assertion real-browser contract.
The native port is finished off with the readability work the earlier rounds
left open, plus the cleanup of everything the shadcn era or the older canvas
left behind.
- Geometry: node height is now composed from named parts (chrome, two title
lines, meta row) and per-node extras, so a teammate card that carries a task
line no longer gets clipped by the old fixed 68px budget; the layout spec
follows the new model and GRAPH_ROW_STRIDE stays exported for it.
- Readability: row rhythm (28-32px), three ink levels for title/meta/owner,
truncation with title hints, current-row and hover expressed as an accent bar
plus a hover fill (no shadows), phase frames and edges back on the l1/l2
hairlines, control cluster uniformly 28px, drawer durations on tabular
figures, kill action revealed on hover/focus without shifting the row, no
horizontal overflow at 360px.
- Task window: the markdown body is pulled onto the page scale (12px body,
12/13px headings, tightened list indent and paragraph spacing) while the
shared MarkdownText/markdownTextProps contract is untouched.
- Cleanup: dead stylesheet rules, unused TSX exports and helpers, stale
comments that still described the retired Variant-D canvas, zero-reference
i18n keys across the dictionaries, and the Tailwind/shadcn/Variant-D
narrative in the design doc, AGENTS.md and the plugin guide (kept as one
short "why it was rolled back" section).
Verified here: typecheck and lint clean; 133 files / 1413 tests; build with no
radix-ui or --tw- strings in lib/client.js (988.9 KiB); the 70-assertion
real-browser contract at 70/70 with the 20 reference screenshots regenerated;
mount e2e 7/7.
shadcn's defaults are built for a standalone app: 14px body, 36px controls,
solid primary fills and elevation read as a foreign surface next to this
plugin's own tabs (files, changes, side chat, settings), which are 12/11/10px
type, 28px controls, hairline borders and flat panels. Retuning the layer did
not converge, so the layer goes.
- Removed: src/client/ui/** (21 vendored components), components.json,
scripts/ui-css.mjs, the Tailwind/PostCSS compile step in tsdown, and the deps
tailwindcss / @tailwindcss/postcss / postcss / radix-ui /
class-variance-authority / tailwind-merge. (diff and simple-icons stay: they
patch undeclared bare imports of the host primitives, unrelated to shadcn.)
- The seven page surfaces are back on the plugin's own base: *.module.css with
--dsw-* tokens plus the host primitives (Button/Menu/Modal/Pill/Tag/Switch/
Input/Tooltip/StateDot/MarkdownText). Behaviour, data-* hooks, aria labels,
i18n keys and the AnchoredPopover geometry/drag/dismiss contract are
unchanged; the non-styling fixes landed during the shadcn era (model id
dedupe, task-window CAS, drawer arming, popover dragging, the chunk packaging
glob) were re-applied and are covered by the existing specs.
- The tasks page returns to the core bundle: `tasks` left CHUNK_NAMES /
CHUNKS / ChunkName, src/client/chunks/tasks.tsx is gone and tabs.tsx imports
SubagentView directly.
- The reverted stylesheet was retuned to the plugin's measured vocabulary
(12/11/10px, 28px controls, border-l2 with border-l1 hairlines, hover fills,
no static shadows, --dsw-shadow-lv2/3 on floating layers only) and verified
against a 70-assertion real-browser contract; the harness's own two
measurement defects were fixed with a falsification test proving the
assertions still catch injected regressions (56/70, 14 FAIL).
- Tests: ui-foundation / ui-bundle / ui-shadows (the shadcn guards) deleted,
theme.spec scoped back to the plugin's own stylesheets, bundle-route and
manifest-consistency chunk lists back to four. Docs: AGENTS.md and the plugin
guide drop the Tailwind/shadcn guidance; the design doc marks those sections
superseded and records why the layer was removed.
Verified: typecheck/lint clean; 133 files / 1410 tests; core bundle 987.3 KiB
with no radix-ui/--tw-/tailwind strings and no client-tasks.js; mount e2e 7/7;
20 real-browser screenshots at 360/720 for graph, tree, board, task window,
drawer (including armed), both popovers and the 77-node fixture.
Stock shadcn defaults are designed for a standalone app; next to this plugin's
other tabs (files, changes, side chat, settings) they read as foreign: 14px body
against 12px, 36px controls against 28px, #00000029 borders against the plugin's
10%/4% hairlines, solid primary badges against flat state tints, and elevation
where every other panel is flat.
Measured the plugin's own vocabulary from its CSS modules (12px body / 11px meta
/ 10px micro; 16-20px line heights; 28px controls; 6x8 and 6x10 paddings; 8/6/4
gaps; 6/8px radii; `border-l2` for 1px borders and `border-l1` for hairlines;
`--dsw-shadow-lv2/3` only on floating layers) and mapped the vendored set onto
it instead of editing every page:
- theme.css bridge: `--border`/`--input` -> `border-l2` (l4 was visibly darker
than every other tab), new `--border-subtle` -> `border-l1`, the type scale
remapped (text-xs 11px, text-sm 12px, text-base 13px, text-lg 14px), elevation
mapped to the plugin's tokens (static shadows none, md/lg -> --dsw-shadow-lv2
/lv3), and the state tints bridged.
- component defaults retuned to the same scale: button sizes 28/24/20px with
text-xs, card padding 12px with gap-3 and no shadow, input 28px, badge 18px
with 11px ink, empty at p-4, separators on the hairline token.
- badges: the default variant is the plugin's flat state tint
(bg-state-business-tertiary + state ink, the same recipe sidebar.module.css
uses for revealed rows) rather than a solid primary fill.
Guards updated with the reasoning inline: the border bridge expectations, a
`none` value being the plugin's way of saying "flat", and the skin-contract
check now derives local token names from the file instead of a hand-kept list.
Verified: typecheck/lint clean; 136 files / 1515 tests; build with the bundle
guards; mount e2e 7/7; mirror-page screenshots (real host stylesheet + real
components) re-shot at 360/720 for graph, tree, board and the 77-node fixture.
The previous round kept a hand-trimmed, shadow-less, 11-13px variant of the
component set. This replaces it with the registry's own defaults and the
registry's own composition primitives, keeping only the adaptations this host
actually requires.
Registry refresh (npx shadcn@latest add --overwrite): the whole vendored set is
back to upstream sources, plus label, field, input-group, item and button-group.
Re-applied adaptations (documented in the design doc):
- cn from ./utils and explicit ./x.tsx specifiers (the registry writes virtual
aliases this repo has no resolver for);
- host IconXxx instead of lucide (skin contract);
- no dark: variants (the DSH tokens already flip) and no palette literals;
- React.forwardRef on Button (the host is React 18, so radix Slot needs it);
- tw-animate-css animation utilities stripped: installing that dependency would
inject generic keyframes into a page we do not own.
Restored to stock: the full Button variant/size set, shadow-xs on outline,
transition-all, badge ghost/link.
Recomposition on the newest primitives (per-file, verified here):
- board: full Card composition, Item rows, ToggleGroup filter, dropdown items
grouped, Empty for the empty state;
- task window: Card composition, FieldGroup/Field with data-invalid validation,
ToggleGroup owner (default variant: the outline variant's hover and pressed
states measured identically, hiding the selection), destructive-armed delete,
FieldError for conflicts;
- popovers: both portal cards use the same Card composition, rows are Item;
- drawer/header: Item rows with ItemActions, Separator bands, Spinner, Empty;
- graph/tree: stock node recipe, outline buttons, cn() for conditional classes.
Two no-preflight traps found by rendering, not by types:
- stock Empty ships border-dashed without a width, so the UA's 3px medium dashed
frame wrapped the whole page: border-0 on that Empty;
- <Spinner size={12}/> stayed type-clean after the refresh (size is a valid span
attribute) while the glyph silently rendered at size-4: className="size-3".
Verified: typecheck/lint clean; 136 files / 1515 tests; build with the core and
chunk bundle guards (core 836.3 KiB, chunk 734.7 KiB); mount e2e 7/7; and the
real-host-stylesheet mirror page re-shot for graph, tree, board, task window and
the 77-node stress fixture at 360 and 720.
Real-device report: the Tasks page "looks like it has no CSS at all, with
blurry borders". Two hypotheses were falsified before the real cause surfaced:
the sheet IS delivered (the chunk carries both the CSS and its injector, and
the bundle route serves it) and the cascade is NOT the problem (with the real
host stylesheet plus the real compiled sheet, a controlled sample keeps our
padding, radius, border, background and font; host CSS-module selectors only
match host nodes, and its global element rules are element-level specificity).
The cause is what we deliberately excluded: PREFLIGHT is also what normalizes
form controls. Measured on a plugin icon button that sets only an icon size,
the UA chrome survived as padding 1px 6px, border 2px outset, background
ButtonFace (#efefef) and a 13.33px system font - gray beveled buttons with a
soft double border, i.e. "no CSS".
- ui/theme.css: a .dsw-tasks-scoped preflight subset (zero margins/padding,
border width+style, background, appearance none, inherit color/spacing,
cursor rules) inside @layer base, written with :where() so it stays at
(0,1,0) and utilities in the later layer still win; plus an explicit
"@layer theme, base, components, utilities;" statement - layer order follows
first appearance, and without it the utilities import registered utilities
BEFORE base, so the reset beat rounded-md (measured: square buttons). The
built chunk's order was re-checked: properties, theme, base, components,
utilities.
- Stock shadcn restyle: static cards carry stock shadow-xs/shadow-sm again
(floating layers keep shadow-lg), body/meta text is text-sm/text-xs instead
of 11-13px, status uses stock Badge variants, controls use stock Button
variants and sizes, row actions appear on hover/focus while staying keyboard
reachable, node cards are 176x68 with shadow-xs.
- bandColsFor budgets columns at the readability floor (width / 0.78) instead
of 1:1, so a 360px panel keeps two columns; FIT_MIN_SCALE moved into the
layout module as GRAPH_FIT_MIN_SCALE so the fit and the column budget share
one constant.
- Guards: tests/ui-foundation.spec.ts pins the scoped reset declarations and
the layer-order statement; tests/ui-shadows.spec.ts now allows shadow-xs and
shadow-sm on static panels and bans md and above; the theme guard's inert
paint rule forced the reset to spell border-width/border-style instead of the
"border: 0 solid" shorthand.
Verified: typecheck and lint clean; 136 files / 1505 tests; build with the
core/chunk bundle guards; mount e2e 7/7 (mount 444ms, longtasks 2 of 8); the
visual mirror page (real host stylesheet + real components) re-shot at 360 and
720 for graph, tree and the 77-node stress fixture.
package.json#files enumerated chunks by hand, so the new lib/client-tasks.js
never entered the tarball — a published install would have failed to open the
Tasks tab. The same list was also missing lib/client-locale.js since that
chunk landed. Replace the enumeration with the lib/client-*.js glob, derive the
test's chunk list from CHUNK_NAMES instead of a hand-mirrored copy, and add an
assertion that every chunk is covered by the publish list (verified against the
packed tarball and the live /sidebar/bundle route).
Visual base replacement for the Tasks page after the 'hard shadows make a
dense graph unreadable' review. Behaviour, hooks, aria labels and i18n are
unchanged; the base layer is not.
- FOUNDATION: Tailwind v4 wired into tsdown's css-inline plugin (theme +
utilities ONLY — preflight is excluded because the plugin's stylesheet is a
global <style data-plugin> tag and would repaint the host page), shadcn
tokens bridged to --dsw-* so the DSH theme keeps flipping light/dark with no
dark: overrides, cn() util, components.json for the CLI, src/client/ui/.
- COMPONENTS: shadcn's component source vendored into src/client/ui (button,
card, badge, separator, input, textarea, tooltip, popover, dropdown-menu,
scroll-area, collapsible, toggle-group, skeleton, spinner, empty) with a
documented local diff: lucide-react replaced by host IconXxx (no new icon
dep), shadow-* dropped from non-floating variants, unused variants pruned,
and React.forwardRef restored on Button — the host runs React 18, where
radix's Slot cannot hand a ref to a plain function component (dev warns,
production silently mis-anchors the floating layer).
- SHADOWS: static panels are now hairline + surface tiers with hover:bg-muted;
shadows exist only on floating layers. Settled nodes recede by ink
(text-muted-foreground/foreground-3) instead of opacity.
- DENSITY: node cards widened 132 -> 150px with a two-line clamped title
(13px text in a 132px card left ~7 CJK characters), reserved height 46 -> 60.
- CHUNK: the whole page (graph, tree, task window, board, drawer, shadcn/radix
layer and its stylesheet) moved into the lazy 'tasks' chunk. Core bundle
988.5 -> 835.6 KiB (-153 KiB); the 709.8 KiB UI payload loads only when the
Tasks tab opens.
- MODEL: workflow members whose childId is already a real node elsewhere are
no longer synthesized into a duplicate id (React keys/edges), with a
last-resort per-id dedupe.
- GUARDS: tests/ui-foundation.spec.ts, tests/ui-bundle.spec.ts (core/chunk
split + size budget + no preflight/oklch/lucide), tests/ui-shadows.spec.ts,
extended tests/theme.spec.ts (no colour literal / palette class in
src/client/ui/** and the migrated files).
- REVIEW FIXES: portal popovers re-scoped under .dsw-tasks, graph loading
state, 11px floor, error nodes no longer tint a whole border, TeamBoard's
delete item is neutral (the window owns the two-step confirm).
Verified: typecheck / lint / 136 files 1502 tests / build bundle assertions /
mount e2e 7-7; browser harness 65-65 checks with 0 console errors.
Round-three review feedback:
- NO PER-CARD DETAIL BUTTON: the ellipsis control is gone from graph cards
and tree rows — the card IS the affordance, and clicking it opens the
detail window (the transcript jump moved inside as the primary button).
Fold aggregates keep click-to-expand.
- ONE TASK WINDOW: every task surface (board row, agent node task line, the
node detail list, and creating a task) now opens the same non-fullscreen
DRAGGABLE window (TaskWindow.tsx + its TaskPopover shell): markdown
description by default, 编辑 switches the same card to multi-line editing,
plus owner reassignment, complete/reopen and a two-step delete. The board
lost its overflow menu and its Modal entirely; create uses the same window
in create mode.
- REUSE: TaskPopover (4 call sites), MultilineField (the plugin's own
multi-line input — the host primitive set has none), OwnerPicker,
TaskCreateButton, and the shared glyph/TaskLine helpers across both modes.
- 3 new copy keys across zh/en/ja and the 18 third-language dictionaries.
Tests: the node-click spec now asserts the detail window (and that the jump
button inside it navigates), a new spec pins 'no nested control inside a
card', and two more cover the shared window: markdown-first view then
multi-line edit posting an 'edit' with the current revision, and immediate
owner reassignment. Full suite 1406 passed / 133 files.
Round-two review feedback, all three points:
- TASKS ON NODES: the model maps each shared task (ownerName -> member name
-> member session id) onto its owner's node; graph cards and tree rows
render a fourth line (icon + subject + status + '+N'), and the node detail
popover lists every owned task.
- TEAM BOARD: member Pills double as the owner filter; each task row is a
status dot + subject + owner + status Tag + ONE overflow Menu (complete /
reopen / edit / reassign submenu / delete with a two-step arm). Creating
and editing moved into a real Modal with labeled Inputs and owner Pills —
the narrow strip is no longer where you type.
- HOST PRIMITIVES ONLY: the board and the jobs drawer now use Menu / Modal /
Input / Button / Pill / Tag / Switch / StateDot; no native select or input
remains (guarded by a spec).
- ICONS, NOT HAND-DRAWN GLYPHS: every text glyph (circle/diamond/triangle/
check/chevrons/i/house) is replaced by a host icon — agent preset, user
(teammate), branch (workflow run), checklist (fold + tasks), tree corner,
fullscreen (fit), ellipsis (node details), copy/stop (jobs). The live line
now shows the tool's own icon + name + args without the drawn box.
- JOBS UX: the output popover is DRAGGABLE (offset clamped to the viewport,
double click re-anchors, controls do not start a drag), defaults to 380px,
and gains copy-output, a follow-latest switch, kind/status Tags and a drag
hint; the drawer highlights the open row.
- 19 new copy keys across zh/en/ja and the 18 third-language dictionaries.
Tests: 4 new specs (task rendered on its node, no native select/input +
menu-driven board, create-through-dialog posts the CAS-free create, job
popover drag moves the card). Full suite 1404 passed / 133 files.
Review feedback from the live 3384 install, fixed at the root:
- COLORS: the new stylesheet used var(--dsw-alias-accent) in 17 places — a
token the DSH theme does NOT define, so every declaration was dropped
(older files only survived because they carry fallbacks); container edges
used border-l1 (4% black, invisible). Accent now rides
--dsw-alias-state-business-primary, container edges --dsw-alias-border-l4.
- CLICKS: the canvas called setPointerCapture on pointerdown, which
retargets the derived click to the container and killed every node (and
ⓘ) interaction; jsdom cannot see this, a real browser can. Capture is
gone and panning only starts from the background.
- LAYOUT: fit gave up silently at zero container size and never retried,
centered only horizontally, and capped at 100% while a five-child level
scaled to 35%. Now: ResizeObserver + first non-zero fit, both axes
centered, a 0.78 readability floor, and layoutTasksGraphForWidth picks
the flattest sibling arrangement that still meets that floor.
- CARDS: title (one line) + mono meta + live line only; display title,
team role, full model id and latest text moved into the ⓘ popover.
- PANEL: metrics follow the approved 360x660 mockup (132x46 cards, 112 row
stride), explicit line-heights, user-select:none, horizontal control
cluster with a labelled mode button, always-visible team board strip.
- Tokens for two new popover headings across all 20 dictionaries.
Tests: 3 new page-interaction specs (click after background pointerdown,
node gestures never pan, team board visible without a click) and a
rewritten 9-case layout suite (band wrap, row reservation, width solver,
reserved live row). Full suite 1400 passed / 133 files.
The Subagent page becomes the Tasks page per the approved Variant-D design:
- tasks-model.ts feeds BOTH modes from one pre-order node list: graph
canvas (TasksGraph: layered nodes over bezier edges, drag-pan,
wheel-zoom-to-cursor, phase frames, bottom-right control cluster) and
the classic tree (TasksTree: keyboard navigable) — the view toggle and
fold toggle stay reachable in both modes.
- Workflow runs (workflows.list) hang under their origin agent; member
agents re-parent under the run node, missing ones synthesize from run
data; completed leaf agents fold into per-parent aggregate nodes
(click or cluster toggle to expand/re-collapse).
- Agent Teams (experimental): roster enriches nodes; a header chip opens
the shared task board popover with basic CAS operations (complete /
reopen / delete / reassign / create / edit); no experimental layer →
the whole block hides (structural degradation).
- Background jobs move into a bottom drawer that auto-collapses at 8+
agents (manual toggle wins); job output opens as an anchored popover
(event replay, never the model's cursor); two-click kill preserved.
- Live activity lines render as glyph + tool + args (tool-icons map onto
host primitive icons).
- Node click jumps to the transcript; ⓘ opens the detail popover;
popovers follow the selection-popup dismissal contract (Escape /
outside click / anchor off-screen via IntersectionObserver).
- tasksViewMode pref (graph default) with a select row in settings;
58 new copy keys across zh/en/ja + 18 third-language dictionaries;
dead CSS from the old tree/dock pruned.
- Tests: 6 new page-interaction specs (auto-collapse, both-mode toggle,
jump, fold, popover); jobs-view suite re-pointed at the popover;
prefs/builtins/smoke lock tests updated for the new pref.
- tasks-model.ts: one pre-order node list for graph AND tree modes —
catalog walking, workflow-run attachment with member re-parenting and
synthesis, team enrichment, per-parent settled-leaf fold aggregates.
- tasks-graph-layout.ts: dependency-free tidy-tree layered layout
(bundle-purity: no graph library in the core bundle).
- api.ts: workflowsList/teamsView/teamsTaskCreate/teamsTaskUpdate with
the wire result types mirrored from the host routes.
- 11 unit tests across model derivation, edge classification, fold, and
layout geometry.
- workflows.list folds tool-workflow/* session events (the same four types
the official workflow-run panel folds) across the whole session tree,
merging the stored log with a live session/event mirror deduped by seq;
absence of events yields an empty list, never an error.
- teams.view / teams.taskCreate / teams.taskUpdate ride the experimental
agentTeams service's Remote vocabulary with structural degradation:
layer absent -> {available:false}; root leads no team -> {team:null};
CAS conflicts pass through the service's own result union.
- wire gains the team-error code; context-types gains structural mirrors
of the team view/request vocabulary (no experimental package import).
- 29 unit tests across fold, route merging/degradation, and CAS shaping.
A plugin reload (or a disable/enable) tears the client fiber down while this
module's own service/store subscriptions are still live: unregistering each
built-in descriptor notifies the plugin's service, and every notification ran
`sync()` again. The `files` takeover is never a descriptor — its key is
`FILES_KIND` — so the drop loop released it every single time, and the block
below re-created it. Re-registering on the now-inactive context throws inside
`registerSlots` (`ctx.slots.inject` cannot create an effect on an inactive
context) *after* `tabs.register` already put the type into the host registry,
so that registration's disposer was lost with the exception.
The id then stayed taken for the rest of the page's life: every later attempt
was refused with
sidebarRight: tab type id "dsh-better-sidebar:files" is already registered
and the kind rendered the host's "nothing here can view this kind of content
yet" face until a page refresh.
- `sync()` skips the `files` takeover in its drop loop: the takeover is owned
by the editor-type switch, so an unrelated notification no longer puts its
host-side type through a tear-down/re-register window;
- a registration that fails after its type was registered releases the type
again (and whatever slot disposers it collected), so the same id stays
registrable and a retry can succeed;
- one descriptor that cannot register no longer aborts the whole sync, so the
remaining types are still brought up;
- teardown releases every registration even when one release throws.
Verified: tests/native-registration.spec.ts (new, 4 cases) is red 4/4 against
the unfixed code and green with the fix; tsc/eslint clean and the full unit
suite passes.
The shell's base stylesheet makes every direct body child no-drag
(`html[data-platform=darwin] body > :not(#root)`). The viewport-sized
`[data-dsh-panel-host]` layer inherited it, and since app-region ignores
pointer-events it cancelled every window-drag strip beneath it once the
shell recomputed regions.
Take the host and layer out of the app-region computation (`!important`,
the shell selector carries an id's specificity) and keep the panels
themselves no-drag so their controls still receive clicks.
Fixes#772
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`tests/primitives-exports.spec.ts` collected its file list with
`execFileSync('rg', …)`. ripgrep is installed on a developer machine but not
on the CI runners, so the whole spec failed to COLLECT there
(`spawnSync rg ENOENT`, which is why `ci` and `ci-windows` were red on this
branch while `pnpm test` was green locally). Replaced with a plain readdir
walk over `src/` and `tests/` — no external binary, and the scan is a few
hundred small files.
Verified by running the spec with a failing `rg` shim first on PATH: it
passes, which it cannot do while still calling ripgrep.
The alpha.1 handover refused 27 extensions so DSH's own document preview
could own them, but nine of those have NO host renderer at all: `avif` and
`ods` sit in `document/unviewable.ts`'s "known binary, no renderer" list
and get the unsupported empty state directly, while `xlsb` / `xlt` /
`xltx` / `xltm` / `ots` / `dot` / `dotx` fall through to the plain-text
body, fail the binary check and land on the same dead end.
Before that handover these files reached the plugin's `code` catch-all and
rendered the binary-download pane, so refusing them was a user-visible
regression we introduced ourselves — and rc.1 did not restore it. They are
claimed again (`fods` stays handed over: the host shows that flat XML as
plain text, which beats a download pane).
`canOpen` had zero test coverage, which is exactly why the regression went
unnoticed, so `tests/native-surface.spec.ts` now pins both directions:
every format the host renders must be refused, every format it cannot
render must be claimed. Reverting to the old list makes it fail with
`macro.xlsb: expected false to be true`.
Move the support line from 0.1.7-alpha.1 to 0.1.7-rc.1 (peer floor
`^0.1.7-rc.1`). 0.21.0-alpha.1 never reached npm, and once the code-card
labels below are fixed the same build crashes on alpha.1 (its DiffBlock
still reads the `labels.files` upstream deleted), so declaring only the
host we verify against is the honest floor — and from rc.1 on the host
enforces peers at boot anyway: a row whose `@deepseek-ai/dsh-*` peers do
not satisfy the runtime is silently disabled.
Dependencies: 28 devDependency pins and 14 peers move to 0.1.7-rc.1.
rc.1 also raised the vendored runtime (cordis 4.0.4, schemastery 3.18.4,
cordis-plugin-group 1.0.4, cordis-plugin-include 1.0.9) and publishes DSH
cross-references as exact versions, so bumping schemastery/cordis alone
left four unmet transitive peers — the three vendor packages are hoisted
into devDependencies, which is what makes `pnpm peers check` clean again.
One compile break: rc.1 added `CodeToolbarLabels` (codeLabel / wrapLabel /
unwrapLabel, all required), made DiffBlockLabels / ReadBlockLabels extend
it and deleted `DiffBlockLabels.files`. Fixing SideChatView then leaving
markdown on the legacy banner would render two different code-card styles
inside one side-chat view, so the plugin adopts the card everywhere:
`markdownTextProps()` now passes `labels.code.toolbarLabels` with the
host's own wording (codeBlock.title|wrap|unwrap, added to all 20
dictionaries; `sideChatBlockFiles` retires with the deleted footer).
Adopting the card surfaced a silent regression the suite caught: the new
toolbar shows `codeLabel` for languages the highlighter does not know, and
mermaid has no shiki grammar, so the info string no longer reaches the DOM
and the banner-text probe in mermaid.tsx matched nothing — mermaid fences
stopped being swapped for diagrams. Detection now matches the FENCE BODY
from the source that produced the render.
`tests/market-manifest.spec.ts` pins every DSH peer as a floating caret
range: an exact pin (the devDependency style) or a comparator without a
prerelease would fail rc.1's preflight and silently stop the plugin from
loading, and `pnpm peers check` cannot see that — it validates the
resolved dev tree, not the range shape the host reads.
Two things the Plugins list made obvious once the icon rendered beside the
built-ins.
The artwork was flat: one blue panel, one darker blue rail. The built-ins
each carry their own two-hue palette — the file header says so outright
("these glyphs carry their own brand colors and gradients instead of riding
currentColor") — and at the 30px a row renders at, a single-hue mark reads as
a plain block. Three candidates were rendered next to the real built-in
artwork at the real sizes (30 / 36 / 72, light and dark) before picking one: a
violet rail sat too close to the panel's deep blue to read as a second colour,
and a pale teal panel washed out. The shipped pair is blue for the panel
(gradient) and teal-green for the rail (gradient), which stays legible at row
size and matches the Subagent artwork's proven blue/green combination without
sharing its silhouette.
The description sold the plugin as someone else's sidebar. It now leads with
what it is — better — in one sentence rather than a colon and a feature list,
in both locales, and both `package.json` (the npm blurb and the list's
fallback label) and `dsh.plugin.json` say the same thing. The README never
framed it that way, so nothing there changed.
The artwork guard gained the two properties this decision rests on: the mark
must be colourised (two gradients, no currentColor) and must not use a
CSS-only paint, since the icon ships as an <img> and the search artwork's
foreignObject technique would stay empty there. Red/green verified — a flat
single-hue redraw fails it.
Verified on the live 3080 instance: the row renders the new artwork and
"更好用的右侧边栏——文件树、编辑器、文件变动、任务和侧边对话都收在这一栏里,每个会话互不干扰。"
with no VSCode framing anywhere in the copy.
0.1.7 reads `package.json#icon` plus `locale/<lang>.json` to illustrate and
label a plugin in the Plugins page and the `plugin_manager` list. The plugin
had neither, so it rendered as a bare row (and every other third-party entry
in this profile still does — ours is now the only one with a real mark).
The icon follows the two icons DSH ships for this feature
(packages/experimental/*/icon.svg): a 36x36, fill-only geometric mark with a
blue gradient and no stroked outlines or text. It reads as a panel with a
docked right rail — the sidebar this plugin is. The first cut used two blocks
separated by a gap, but the rail shared the panel's light tone at the top, so
the split leaned on a 3px gap that shrinks away at list size; a darker rail
flush inside one panel survives any downscale.
`locale/en.json` is the discovery baseline English needs to be present for the
whole directory to be read — and `locale/zh.json` carries the Chinese title
and blurb. The manifest's own `description` still advertised the terminal and
the browser, which this plugin handed to DSH in 0.20.0; it is now user-visible
as the fallback label, so it was corrected too.
Every failure in this feature is silent: bad metadata only ever degrades to a
plain text row. `tests/plugin-meta.spec.ts` therefore pins both ends — the
manifest wiring and the packed tarball — plus the house style of the artwork,
and was red/green verified (removing `icon.svg` from `files` fails it twice,
once against the real tarball).
Verified on the live 3080 instance: `readPluginMeta` resolves the icon to a
`data:image/svg+xml` URI and the page renders it (`ourIconExact: 1`, no page
errors).
The one-time import of a pre-0.1.7 `settings.yaml` section never ran. It
started from the `ctx.inject(['settings'], …)` callback, which fires the
moment the settings SERVICE appears — while the loader is still mounting
rows, so `describe()` does not list this plugin's own entry yet, the lookup
returns undefined, and the import bailed on "no form" without a trace.
Upstream's own migration waits (`ctx.root.loader.await()`); this does the
same.
That made every early return of the import invisible, which is how it went
unnoticed: the callback simply produced nothing. It now returns a named
outcome ('imported' / 'no-profile-home' / 'no-form' / 'already-configured' /
'no-legacy-section' / 'rejected') and the caller logs it — anomalies at warn,
the rest at info. The `loader` mirror's `await` is optional so a composition
without the cordis loader still reaches the import instead of aborting the
whole settings face inside the inject callback; the smoke fakes model it.
Verified on the live 3080 instance: after the upgrade the settings form
reports the user's real values back (autoOpenJobs false, workspaceFence
false, the nine-entry tabsEnabled map), where before it reported pure
defaults. The mount lane stays 7/7.
DSH 0.1.7-alpha.1 is 1299 commits past 0.1.6-alpha.2 and rewrites four
contracts this plugin was built on. The peer floor moves to
^0.1.7-alpha.1 (semver's prerelease rule means ^0.1.6-alpha.2 can never
match a 0.1.7 prerelease); 0.1.6-alpha.2 and earlier stay on v0.19.1.
Released as 0.21.0-alpha.1 (dist-tag alpha).
Hard breaks:
- dsh-settings was rewritten: register(ns, schema) is gone, a form is
addressed by the plugin row's Loader entry id and read from the
module's exported Config. The preferences are merged into Config and
marked volatile — which requires DSH's own schemastery, because only
that build wraps a volatile field in the cosmokit reference the
loader's volatile commit walks. With the public package a settings
write reports success and revision 1 while the effective value never
changes.
- ui-primitives renamed its whole icon family (IconXxx14/16 ->
IconXxxRegular/Medium): 26 imported symbols, 17 files. The failing
mode is render-time, not load-time. Five call sites that relied on the
old 14px default now pass size={14} explicitly.
- Session format v4 rejects source.kind === 'plugin'; the side-chat
boundary injection now writes plugin:dsh-better-sidebar, the same kind
DSH's own v3->v4 migration produces, so historical threads survive.
Tool results moved to role 'tool' with top-level content/isError; all
four parsers accept both shapes.
- The client session snapshot dropped subagentsByParent/jobsBySession
and the on-demand subagent-catalog observation API. The Tasks page
reads projectionsBySession and a new jobs.list route.
Yields and convergence:
- The read-only previews go to DSH's documentpreview (Office->PDF, a
spreadsheet worker, zoom, per-directory refresh); editor.canOpen
refuses those 26 extensions. Markdown, HTML (with its sandbox
switches) and the editable code buffer stay.
- The host browser kind is desktop-profile-only now, so the web e2e
lanes expect no browser entry and assert its absence.
- Link takeover claims only urlTarget links and passes the rest to the
host, fixing a regression from the 0.1.6 round: the fallback pointed
at a 'browser' type the plugin no longer registers, so http links in
its own markdown did nothing.
Also fixed, found along the way:
- The file tree refreshes live (new directory watch over the plugin's
own socket), since the plugin owns the `files` kind.
- ctx.sessions.list has no `current` field at either tag; the three
readers were dead. They now use ctx.sidebarRight.mounted.
- package.json.files was missing lib/client-locale.js.
- Two genuinely dead call sites: sessions.openSubagent/open/
refreshSubagents do not exist on the current baseline, so the
topology click, the jump-back and the retry were all no-ops.
- The test harness window shim lacked addEventListener, which the 0.1.7
primitives call at module-evaluation time; and the e2e token exchange
needs Playwright's request stack, not Node's fetch.
Adds the per-version adaptation record (docs/plans/2026-09-21-…-adaptation.md),
rewrites AGENTS.md §3 for the new baseline (the two silent contract breaks, the
terminal/browser handoff, the new official plugin-manager surface, the three
ui-primitives breaks, the widened primitives bare-import set), and updates both
READMEs and the integration guide for the v0.20.0 feature set.
Also retires the support text the terminal deletion made false: the install
flow is one step again (no build-script approval), the FAQ's two node-pty rows
collapse into one pointing at DSH's own terminal, the platform note no longer
promises prebuilt native binaries (there are no native deps left), and the
installers drop the -Repair mode and their allowBuilds writes along with
pnpm-workspace.yaml's now-empty allowBuilds block. Removes the dead
browserNoSandboxWarning locale key (its setting went with the plugin's browser)
and tightens theme.spec's capsule guard so a literal colour fails it.
The e2e resolver now refuses a PATH that is not on the baseline line:
this machine's PATH is 0.1.6-alpha.1, and running the mount lane against
it fails at 'the bundle was not registered' with a zero exit from the CLI —
a misleading dead end worth one explicit error instead.
DSH 0.1.6-alpha.2 ships a right-Sidebar terminal and a right-Sidebar browser
of its own. Both were things this plugin had built from scratch, so keeping
them meant two implementations of one surface plus a plugin that shadows a
host type the product is actively developing.
Terminal: the plugin's whole PTY stack goes — the UI-tab manager, the
agent-owned registry, the lazy node-pty loader, the eight model-facing
terminal_* tools, the xterm view and its font/link/scrollback helpers, and
the cross-session 'pinned terminal' rail that existed only to keep a terminal
alive across a session switch. The host's terminal kind becomes the only
provider, which also retires the node-pty dependency, its pnpm allowBuilds
entry, and the installer's -Repair mode (issue #140's whole class of
'node-pty failed to load' support).
Model-facing capability note: the plugin used to inject terminal_* tools
(off by default). Those are the model's only cross-call persistent terminal,
and DSH's own equivalent (@deepseek-ai/dsh-tool-terminal) is not mounted by
any shipped bundle, so removing them leaves the model with the one-shot
bash/pwsh tools only. That is the accepted cost of not maintaining a second
PTY surface; the README documents how to enable the upstream toolset.
Browser: the plugin's browser tab type is dropped, so the host's own browser
kind owns embedded pages. The DOM-level link takeover STAYS (it is the only
thing that can route an https link to the system browser by protocol, which
the host does not do) but now delegates to the host's browser tab via
ctx.sidebarRight.openTab('browser', …) with a window.open fallback, and the
two settings that only ever configured the plugin's own iframe (sandbox
escape hatch, loopback allowlist) go with it — along with the host-side
browser.probe route they fed.
Cards: the empty-pane welcome cards adopt the host's guide-capsule recipe
(0.5px l4 hairline, 24px radius, layer-1 fill, 56px floor, 14px/20px padding)
so the plugin's 'pick what opens here' surface reads as the same design as
DSH's own. A theme.spec guard pins the geometry and the token-only paint —
including that the upstream-only --dsw-alias-bg-l1/-l2 spellings (defined
nowhere, so their declarations are silently dropped upstream) never appear
here.
Also drops the dead state the removed features left behind (SidebarTab.pin,
nextTerminal, agentWaits, bottomOpenedOnce, the agent-tab helpers, the
pinned-tab memo key), re-points every test fixture that used them at
surviving equivalents, and prunes the specs of the deleted features.
DSH 0.1.6-alpha.2 made two plugin-facing contracts stricter, and both broke
this plugin silently:
- `SidebarRightGuideEntry.id` is now required and unique per provider. The
native tab registrations sent guide entries without one, so the second kind
collided on `undefined` and `SidebarRightTabRegistry.register` threw
'duplicate guide entry id'. The throw happens inside `ctx.inject`'s callback
body, which cordis swallows, so the whole native surface stayed empty with no
symptom beyond a missing guide row.
- `conversation.chat.turnTail` changed from a `chain` to an additive `list`.
The chain's `select` made one entry render and let this plugin REPLACE the
host's produced-files row; a list cannot express that, and the registration
now throws 'list slot ... requires options.id'. Upstream chose the list
deliberately ('An additive list lets both plugins contribute without either
knowing the other's data or rendering'), so the plugin's row can only ever
duplicate the host's deliverables row -- which itself opens files through the
host's openFile, i.e. into this plugin's editor. The contribution is removed
and the row is the host's alone.
The file-opening half survives as `sidebar-file.ts` (the changes tab and the
editor host still use it), and `resolveSidebarPath` moves to the `paths.ts`
path-helper module beside its siblings. `registerNativeSurface` grows an
optional `reportFailure` so a future contract break reaches the visible
diagnostic strip instead of a silently empty guide, and one bad descriptor can
no longer take the remaining registrations down with it.
The native openTab branch rerouted EVERY path seed into openResource (a
dsh-resource://file/... open) without looking at seed.type, so any
component tab opened with a path landed in the editor (the only
dsh-resource://file/** claimant) and its registered component never
mounted. This also contradicted the guide, whose TabDescriptor/openTab
contract and WebDocsView example document path-seed component tabs.
Route by type instead: only the editor kind (the only registration with
dsh-resource://file/** patterns) turns its path seeds into resource
opens; every other kind keeps the page open and carries the path in the
navigation params, which NativeTabRecords.ensure already merges back
onto the synthetic record's tab.path for the component. The bottom
workbench path is untouched, editor opens behave exactly as before, and
onOpen still receives the synthetic tab with the path seed.
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.
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.
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.