# Changelog

Development milestones, newest first. These versions track the repo's own progress; npm releases started at v0.2.27 — see the README's [roadmap](README.md#roadmap) for what is next. 简体中文版见 [CHANGELOG.zh.md](CHANGELOG.zh.md).

- **v0.6.0 — one control plane for humans and agents.** Activity signals are no longer locked to eight built-in buckets: people can create bilingual groups, order them, choose flow or side lanes, assign plugins, search, reset, and keep new plugins visibly unassigned. The same profile-scoped layout can be inspected and changed conversationally through `schematic_activity_layout`. Model automation now covers the rest of the persistent product surface through expanded blueprint management and guarded live composition tools, reusing the same previews, validation, confirmation gates, stale-state checks, backups, and rollback boundaries as the UI. The README and package positioning now make this human/agent symmetry explicit.
- **v0.5.1 — a sharper front door.** The README now leads with the problem Schematic solves, shows the System / Blueprints / Activity mental model before implementation detail, makes the guarded write pipeline legible at a glance, and moves exhaustive capability notes into progressive disclosure. Package metadata and repository discovery terms now describe the product as the visual control plane it has become, rather than the read-only topology viewer it started as. No runtime behavior changed.
- **v0.5.0 — full application redesign.** `/schematic` is now a full-screen System / Blueprints / Activity application with a shared industrial design system, system-following light/dark themes, a contextual topology/inventory workspace, a capability-grouped three-column blueprint editor with isolated local drafts and YAML fallback, explicit drift and unmanaged-entry decisions, and a session-first activity workspace. Every blueprint now carries an explicit Schematic mount switch, including guarded self-disable handling; activity rendering coalesces scoped topology bursts and filters internal service-read noise so host churn cannot flood the page. Schema-1 data migrates idempotently into schema-2 files under `~/.dsh/schematic/blueprints/` without altering its source backup; the public surface is blueprint-only (`/schematic/blueprints/*`, `schematic_blueprint_list/save/switch`). Switches still dry-run through the original protected write pipeline, now delay attribution until HMR is verified and automatically restore the backup on a rejected reload. Below 768 px the application is browse-only.
- **v0.4.3** — the observer keeps its composure under a host-side storm. Chasing "tools/change flickers madly while I've sent no messages" uncovered one: the host itself was storming (on the dev instance, the SPA's connection sets transient agent scopes mounting the whole agent-blueprint tree over and over — each bare `@deepseek-ai/dsh-*` import walks the stale tsconfig path mapping's dozens of candidate dirs, ESM never caches failed probes, so ~thousands of wasted fs probes per second, ~500 tools/change per second plus their system-prompt/change echo, CPU pegged) — and this plugin amplified that storm into the loudest thing on the page: a full graph rebuild on every tools/change (the hottest frame in the profile) and a synchronous SSE broadcast per event, evicting everything else from the action ring. Two disciplines now hold: (1) **the attribution rebuild rides a 500 ms trailing debounce** — re-resolving tool owners is a whole-graph build, and a burst coalesces into one rebuild once it settles (the normal case is unaffected: a late-mounting tool plugin still resolves within half a second, and the /graph.json poll remains the self-healing path); (2) **identical action bursts fold into counting rows** — adjacency no longer matters; actions sharing (module, name) within the window all fold into that action's latest ring row, the row carries an additive `count` field (no protocol bump), SSE re-broadcasts counting rows only on a 200 ms flush cadence, and the client updates the row in place by seq instead of appending duplicates. A storm now shows as two honest "tools/change ×22,200" rows instead of five hundred rows a second scrolling everything away. The storm itself is a harness bug (the agent-blueprint remount loop over stale tsconfig paths — call chain pinned, for upstream); this version only ensures a pure observer never becomes the hot path. Tests 24 → 28 (pure burst folds, interleaved storms fold per action, a different owner or an expired window starts a fresh row, the flush cadence governs re-broadcasts). Verified live on dev :3081: 2 counting rows in the ring, the seq counter still during the storm, wire frame rate down from ~690/s to ~5/s.
- **v0.4.2** — translation gets its notice and its settings. Switching the page to 中文 needs a working model route, and until now the page just silently stayed English when there wasn't one — with v0.4.1's fault marker the only hint. Three surfaces now close that loop. (1) **The notice** — the `.trans` badge (normally the translation progress counter) becomes the messenger in zh mode: when a round fails wholly it reads `translation unavailable: <reason>` with a clickable fail style, the reason being the server's own error verbatim (the 502 text the client used to swallow), or one of two preflight facts. A status precheck runs before any doomed round: switching to 中文 (or booting into it) first fetches the new status endpoint, and when the server already knows translation cannot work — no `llm` service mounted, or no key stored under the effective route's credential ref — the notice appears instantly with **zero** network attempts (before the fix: six 20-second doomed requests pinned per reload). (2) **The settings popover** (the notice's click destination, also behind the badge): pick another model route the harness already knows — providers from the llm registry's configurable list, models fetched lazily per provider — and store its key. The model choice is written as `config.translate: {provider, model}` through the ordinary setConfig pipeline (✎ + edit config + preview drawer + backup + hot reload), reference-only: dsh's rule that config carries the secret's *reference*, never the secret. The key goes through the host's own credentials seam (`ctx.get('credentials').set(ref, value)` — the same 0600 store the Models page writes; describe-only facts ever leave it), with the ref derived exactly as the Models page derives it (`DEEPSEEK_API_KEY` for deepseek-official, `<ROUTE>_API_KEY` uppercased dashes-to-underscores for pi-ai routes). New endpoints: `GET /api/translate/status.json|providers.json|models.json`, `POST /api/translate/key`. (3) **The honest probe** — there is deliberately no key-test endpoint: model discovery answers catalog routes from its built-in registry without touching the endpoint, so a "test" built on it passes a bogus key (verified: a fake key returned `ok:true` plus the full catalog). Instead, saving a key clears the fault marker and fires **one real translate-batch call** through the exact path translation itself takes; its result is the toast, the notice, and the fault state — truth by construction. Also fixed: clearing the override used to splice the config down to an empty string, which the compose pipeline refuses (`config 必须是映射对象` — YAML '' is not a mapping); the empty config is now the pipeline's own canonical `{}`, and the clear's preview honestly carries the `CONFIG_FIELD_DROPPED: translate` warning. Tests 20 → 24 (normalizeTranslateConfig: absent → default, full override trimmed, half-override refused, unknown keys and wrong shapes fail loud). Verified live on the dev :3081 instance against its private empty credential store: the no-key boot shows the notice with zero translate requests; the popover lists 37 providers; a dummy key lands in the store (response carries describe facts only, never the value) and the probe surfaces the route's honest 401 (`令牌已过期或验证不正确`) in toast, notice, and fault marker; the override's setConfig applies → the plugin hot-reloads → status reports the override effective with the credential ref switched to the chosen route → cleared and rolled back to the byte-identical pre-test patch. The same verification surfaced the environment fact behind the original "page won't load" report: the live instance's default route `zai-coding-cn/glm-5.2` was timing out at ~20 s per call — with egress healthy a bogus key 401s in ~1.5 s, so a route switch (exactly what the new settings allow) is the likely cure there.
- **v0.4.1** — a curation surface, a conversation surface, and a page that no longer stalls (four items). (1) **Table-checkbox blueprint curation** — until now save-as could only capture the present (composing a custom set meant hand-disabling/enabling a batch of plugins, saving, then switching back). The table page gains a sticky pick bar: turned on, a checkbox column appears (runtime units and protected rows are disabled with hover explanations — the former are not in the composition tree, the latter blueprints never govern) and 已选 N(含 M 个已停用) counts live; since the table lists only mounted nodes, disabled entries get their own collapsed chip row on the pick bar — a disabled entry can be picked too, and a switch re-enables it with its saved config; 存勾选为蓝图 hands the curated list to the server as save's `memberIds`, where protected or unknown ids land in `skipped` and are toasted by name instead of silently vanishing, and 存当前接线 captures the present in one click (same as the panel's save-as). The picking semantics are fixed in captureBlueprint: **picking means "this should be on after a switch"** — members = pick ∩ (present ∧ unprotected), current disabled state ignored, config taken as the entry stands; overwrite-save keeps its capture-the-present meaning. The pick bar shares the header chip's gate: not gated on ✎, only the server-side edit config. (2) **Opened to dsh's model** — four `schematic_*` tools mount on the host's tool registry, so a user can have a conversation curate and switch blueprints: `schematic_plugins` (the full composition-tree entry list `{id, package, disabled, protected, provides, inject}` — the ground the model picks memberIds from, provides/inject merged from the live graph, empty for unmounted entries), `schematic_blueprint_list` (the list plus the current pointer and its divergence), `schematic_blueprint_save` (`{name, desc?, memberIds?}` — the same implementation as the HTTP save), and `schematic_blueprint_switch` (`{id?|name?}`, exactly one). The switch tool runs precisely the pipeline the browser's Apply button runs: materialize → preview (with a belt-and-braces danger re-check, structurally unreachable for blueprint ops) → base freeze → full-file backup → atomic write → the pointer moves with the tree, then **it watches the reload-failure map for ~3 s** — a conversation has no banner, so the tool owes the model the outcome: when the harness rejects the reload the previous tree keeps running and the result carries the failure reason and the rollback path (one-click in the viewer, or `POST /schematic/compose/rollback`). Deletion is deliberately not offered (a file delete has no backup; that stays a human-UI action). Implementation-wise the shared save/materialize bodies are extracted into `saveBlueprintCore`/`materializeCore` (HTTP and tools are one implementation, not two); the tools are hand-written against dsh-tools' raw registration surface (JSON Schema parameters + manual validation in execute, no defineTool runtime), every dsh-tools import is type-only — the dist "runtime imports are node builtins only" invariant holds; with `edit.enabled=false` none of the four register (the same gate as the routes); and the stance is the `schematic` ctx service's — a capability, not an observation edge, with tool calls attributed honestly through the host's existing activity system. Tests 16 → 20: curated capture (disabled joins, protected/unknown land in skipped, empty pick is legal) and the four tool definitions (parameters and output schemas asserted against the registry's own `assertSupportedJsonSchema`/`validateJsonSchemaValue` to sit inside the enforced subset, save requires name, switch is exactly-one-of, and the render copy names skipped ids and a rejected reload). Verified live on the dev :3081 instance: curated save (68 members including the disabled tool-todo, skipped `['timer','nope']`) → materialize emits exactly one enable → preview with zero warnings → apply (backup taken, entry mounted) → rollback restoring the bytes exactly (hash checked) → cleanup. (3) **The timeline stopped stalling the page** — a report of "the page lags badly" traced to the activity list's time formatting: `toLocaleTimeString` with options constructs a fresh `Intl.DateTimeFormat` per call, and renderActList re-renders all 60 rows on every SSE activity burst, so an otherwise idle page spent ~20% of its CPU inside formatter construction and the main thread took repeating ~500 ms long tasks (typing, hovering, scrolling all stalled half a second at a time). The fix is one shared formatter plus one cached string per distinct second (rows only ever show HH:MM:SS; a 4096-entry cap sheds the cold tail on long uptimes); re-measured, the formatter is gone from the hot list, idle CPU rose 69% → 77%, and the 8 s idle window went from ten ~500 ms long tasks to a single 52 ms one. Also guarded fitJourney against a not-yet-mounted pane — ResizeObserver fires once on observe, and that can land between boot and the first render; the resulting null crash was the one red line on the console at page open. (4) **Translation must never own the connection pool** — a report of "the page won't even load anymore" traced to the zh-mode description translation: with a cold cache the viewer fired every missing batch at once — exactly the six connections a browser allows per origin — and with the model provider timing out each request pinned its connection for ~20 s before failing, so the page's own fetches (including engine.js on the next reload) queued behind translation while every reload re-fired the same failing set: a self-sustaining stall. Three disciplines now hold: at most 2 translate requests in flight, a 25 s client-side deadline per request (freeing the connection even when the server side is the one stuck), and a wholly failed round writes a 10-minute fault marker that suppresses network attempts until it lapses — an explicit language toggle or `?lang=` deep link clears it, because that is a human asking to retry now. Verified on the live instance: cold zh load fires 6 requests at a peak concurrency of 2 and the page renders throughout; a reload within the cooldown fires none; toggling the language re-arms it.
- **v0.4.0** — the blueprint workbench: save the wiring you like, switch the whole optional layer in one previewed batch. A blueprint is a **positive member list** (the enabled, unprotected entries at save time) plus the world it was saved against — not a whole-tree snapshot, so it survives dsh upgrades that add or remove bundle entries. Switching is materialization, not restoration: the engine diffs the blueprint against the live tree and emits a batch of the same ops the workbench already speaks (disable enabled non-members the blueprint knows, enable/insert members, setConfig drifted member configs), which queues into the existing preview drawer — ghost nodes, per-entry diff, YAML diff, warnings — and writes only through the same Apply (backup + staleness check + harness hot reload). Nothing about the write path is new. The rules that keep a switch honest: **protected entries never join a blueprint and are never disabled by one** (the danger/warn tiers are the implicit core, so no hand-written blueprint can switch the editor itself off); **entries that appeared after the save stay exactly as they are** and are named in the switch report shown atop the preview (a blueprint never silently decides a newcomer's fate); members whose ids now bind to a different package are reported and left untouched; and a member whose package is no longer installed refuses the whole batch with the install command (the planner would wave a bare uninstalled insert through and the harness would reject the reload — the refusal mirrors the swap's atomicity). **Divergence is an empty materialization**: `compose.json` gains a `blueprint` field — `{id, name, diverged, pendingOps, blocked}` — computed by the same rule (zero ops ⇔ the tree is on the blueprint), so the 蓝图 chip accents the moment the tree drifts and a one-click 回到蓝图 previews exactly the ops that close the gap. A successful Apply moves the current-blueprint pointer with the tree; any manual op queued after the materialization cancels the attribution (it is no longer a pure switch). Blueprints live as one pretty YAML file each under `~/.dsh/schematic/blueprints/` (member configs stored parsed, `!!js` expressions round-tripped verbatim through the include dialect; a `.current.json` pointer beside them; atomic tmp+rename writes) and never touch the patch file; the panel offers save-as, overwrite-with-current, a two-click delete, and YAML export/import — the shareable-recipe form. Two refinements folded into the same version: the 蓝图 chip lives in the header and is **not gated on ✎** — the server-side edit config is its only gate, `compose.json` is fetched at boot regardless of mode (it powers the divergence accent), and the panel/preview/Apply all work with edit mode off, while the drawer's rollback/clear buttons stay behind ✎ (the one deliberate exception to v0.3.2's "✎ is the only door into editing"); and the filter chips became **isolating** — every category and origin chip rests inactive while the canvas shows everything, clicking one spotlights exactly that group's pills (cards stay alive while any member matches, so a focused pill inside a mixed card never vanishes; clicking again releases), replacing the old cumulative on/off toggles. Nine new tests pin the capture rules, every materialize branch (including newcomer-stays and the uninstalled refusal), the divergence semantics, the planner integration shape, and the store round-trip including path-shape id validation.
- **v0.3.3** — the mini map becomes a live constellation, and the write path gets a test floor (two items). (1) **The composer-side star map rewritten** — the full edge hairball gives way to a sparse gravity skeleton: lanes are module-aggregated consumer → provider pairs (graph edges are injector → provider under the hood), ranked by endpoint degree plus shared keys, and only the top ~20% (floor 8) draw as quietly curved base lanes with deterministic seeded bends; hovering a star reveals its one-hop constellation beside its card. Real service access now lights the exact lane: every mini.json traffic row (reader module + ctx key) resolves the key's provider modules, lights both stars plus the lane between them, sends one photon along consumer → provider (SMIL animateMotion; suppressed under prefers-reduced-motion, and never replayed for the adopting first poll or traffic the previous beat already saw), then leaves a frequency-weighted warm afterglow decaying over the same 4 s TTL; events without a trustworthy second endpoint light only their owning star. Idle stars settle into a genuinely quiet magnitude range (leaves 14%, the largest hubs 42%; runtime activity jumps to full opacity instead of inheriting the dimming), the panel wears a theme-aware glass sky (paired light-dark() tokens, nebula gradients, blur + saturate riding the backdrop dial), it is keyboard-focusable (Enter/Space opens the viewer), and a dropped migration is repaired: the rewrite moved the size store to `sch.mini.size.v2` without carrying the identical-format v1 key over, silently resetting every upgraded browser's panel size. (2) **A test floor under the workbench** — `npm run check` (strict `tsc`, then `node --test` over the compose pure surfaces, then the esbuild build) replaces the build-only CI gate; six tests pin the managed block's byte-level round-trip (every byte outside the markers preserved; a comments-only file stays valid after clear), the 422 refusals (unterminated/malformed blocks, invalid plans — before any writable candidate exists), planner determinism (an enable plus a config edit merge into one managed row, dropped fields named), and the uninstalled swap's atomicity (refuse; never degrade to a bare disable).
- **v0.3.2** — disabled entries stay recoverable. Disabling an entry through the workbench removed its node from the live graph, and the ⏻ toggle lives in the node's detail panel — past that click, the only way back was whole-file rollback. Three recovery surfaces now exist, all funneling into one `queueEnable` gate that **never flips edit mode itself** (every affordance toasts "press ✎ first" while the page is the pure observer; ✎ remains the only door into editing): (1) **timeline topology rows carry the entry id** — an unmounted unit's `topo-node` row and a vacated provider's `topo-provider` row now ship the bare entry id (`TimelineEntry.entry`, additive, no protocol bump) and render a ⏻ affordance when no live node resolves; clicking queues the same `{kind:'enable'}` the workbench always supported server-side, through the same preview → Apply flow (nothing writes without the drawer's Apply). Journal `topo-provider` records gain `fromId`/`toId` for forensics. (2) **a standing "已停用 N" chip** in the filter bar (edit-mode chrome) lists every disabled entry in the composed tree with per-row ⏻ 启用 — the durable path, because timeline rows age out of the action ring (the SSE snapshot carries only 40 host actions) and browser reloads erase them; rows read "in tree · disabled", honest wording inherited from the swap alternatives (a disabled entry's provides/inject are unknowable until mounted). (3) **unresolved-key pills are clickable** — ext-zone pills (scope and overview) and the detail panel's inject chips open a popover naming the key, why it's unresolved, its injectors, and every recovery the data honestly supports: a recent topology row that watched the key fall unresolved (best-effort — the v0.3.1 same-batch suppression keeps the common disable-provider flow from emitting one), curated in-tree seam alternatives (the reliable offer, fs/web/llm), or a pointer to the ✎ disabled list; host-provided keys stay hover-only (a launcher key has no composition remedy).
- **v0.3.1** — structural changes become records (the competitor-analysis absorption round; three items). (1) **Fiber state propagates to the unit** — a FAILED internal child fiber now fails the entry's whole unit on the graph (worst-state-wins over the fiber subtree), with the clipped `_error` reason riding the node and the panel; recovery clears it at the next settle — the unit-root read used to be the ceiling, now it is the floor. (2) **Topology changes land in the journal and the live timeline** — three new record kinds: `topo-node` (an entry-origin unit mounted/unmounted), `topo-provider` (a ctx key's provider unit changed, or the key falling unresolved / resolving to `host`), `topo-state` (a unit flipping into or out of `failed`, with the reason) — each written to the per-day journal and rowed into the host-scope timeline, so "what did the wiring do when I applied that" stays answerable. The diff runs on settled snapshots: three framework events (`internal/plugin`, `internal/status`, `internal/service` — the last registered global to see through isolation scopes) arm one 750 ms trailing debounce, so an HMR reload's active→loading→active burst coalesces to silence; a 10 s boot grace absorbs mid-boot settling that would otherwise journal ~50 phantom arrivals; a provider row is suppressed when the same batch already rowed that unit's own mount/unmount (stopping a ten-key provider must not emit eleven synonymous rows); state rows fire only when they touch `failed`. Host-scope like jobs/agent-status: live rows, journal memory, never merged into a session's replay. (3) **The `schematic` ctx service** — the live graph is provided under the `schematic` key for sibling plugins (`inject: ['schematic']`, then `ctx.schematic.graph()` — same source as `/graph.json`, built fresh per call). A capability, not an observation edge: the footprint is visible only to injectors, the graph shows our own node honestly as providing it, a key collision downgrades to viewer-only with one warn, and the change feed is deliberately not exposed in v1 (a consumer can diff `graph()` itself; a later `changes()` is an addition to the same implementation). Also: the module set tool attribution resolves against is refreshed by the same structural feed, with `/graph.json` polls kept as the self-healing path.
- **v0.3.0** — the composition workbench: the graph becomes the editor (the ✎ header toggle, off by default — off, the page is exactly the read-only viewer). Five operations — disable/enable, per-entry config edit, insert, and per-seam provider swap (disable-the-old + insert-the-new in one batch; an uninstalled target refuses with the `dsh plugin add` command as copyable text rather than degrading to a bare disable). Every change previews before anything is written: ghost nodes on the graph (strikethrough+fade for removals, dashed+`?` for arrivals), a per-entry diff, the exact managed-block YAML before/after as a line diff, and structure-aware warnings — a service key losing its only provider, config fields the edit drops (named), `!!js` expressions a whole-config replace would freeze into literals, boot-critical plugins, and schematic's own disable as the danger tier (the entry id must be typed in; the banner carries manual-recovery steps). Apply re-runs the same offline composition the boot performs (`@deepseek-ai/dsh-app-boot`'s composeEntries, dry-run before write; duplicate entry ids rejected at plan time), then backs up the whole file (timestamped, capped at 20, configurable) and atomically writes only the versioned managed block inside the profile's `cordis.patch.yml` (`# >>> dsh-schematic v1` … `# <<< dsh-schematic v1`; every byte outside preserved verbatim; `[]` injected for comments-only files). Stale bases refuse with 409 (both the GET's baseHash and an on-disk freeze right before the write). The harness hot-reloads the file in ~1–2 s; a reload it rejects keeps the old tree running, locks editing under composition drift, and the banner (failure hashed against the very bytes it failed on, so a later fix or rollback retires it) offers one-click rollback — itself backed up first, and allowed under drift as the rescue path. Clear drops the whole block, markers included, restoring the file byte-for-byte. Zero new npm dependencies: js-yaml / `@deepseek-ai/cordis-plugin-include` (the dialect all YAML round-trips through, `!!js` verbatim) / dsh-app-boot resolve from the profile at runtime, now declared as peerDependencies. Honest boundaries in the UI: an arriving module's provides/inject is unknowable without importing it (hence dashed + `?`), and whether a consumer crashes or degrades is its own logic
- **v0.2.28** — deep observation, four deferred items at once: (1) **workflow runs in the live timeline** — live workflow/* events (start / phase / log / agent fan-out / end) land as progress rows attributed to the workflow engine, a run's strong glow settles on its end row, and durable tool-workflow records replay through the same rows; (2) **the observation journal** — live-only observations no session log ever records (workflow progress, registry changes, background-job lifecycle) are appended to the plugin's own per-day JSONL files under `~/.dsh/schematic/journal/` — the log is the skeleton, the journal is the parts only a live bystander sees (still a pure observer: own files, never session logs); (3) **history replay** — the timeline's replay toggle pages backwards through a session's durable log over the host's own read-only `session.history` RPC, re-attributing every event through the same live fold and merging journal rows for the same window, so the timeline answers "who did what" from before the viewer was ever open (加载更早-style pagination, newest-first, live rows stay below the divider); (4) **per-plugin monitoring panel** — a stats toggle polls a dedicated `/schematic/stats.json` (the graph poll's structural-signature gate would freeze growing counters) for per-plugin counts over this instance's live window: rows, tool calls and failures, tool time (sum + max), LLM completions, plus a live in-flight gauge — replay and boot-adoption folds deliberately never count, so the table shows exactly what the live feed observed; tool attribution re-resolves on every `tools/change`, so late-mounting tool plugins no longer fall into the null bucket. Also: `dev.cordis.yml` isolates the dev instance's session root to `~/.dsh-schematic-dev/` — a dev instance's kill-time shutdown flush can never rewrite the user's live session logs again
- **v0.2.27** — size caps removed + fill fix: the resize grip no longer has a fixed max width/height — the viewport itself is the only ceiling (dragging clamps at the window edges, saved sizes re-clamp into the current window on load); this also fixed the galaxy layout's fill defect at extreme aspect ratios — the force phase has only local repulsion and a centering pull, which left ~15% empty bands at both ends of the long axis (most visible in tall thin panels), so the solver now rescales the solved bounds back onto the padded frame after iterating (each axis only stretches — pairwise gaps grow or hold, so no new overlap is possible): zero overlaps and ceiling-level fill across the full 32–1560 aspect range; follow-up fix: a docked panel whose saved width exceeds the strip beside the composer card now shrinks into that strip instead of hiding entirely (the viewBox follows the shrunk size, the stored size survives and grows back when room returns; free placement clamps into the viewport the same way, and only a window too narrow for even the floor width hides the panel); the settings-nav icon, after five user review rounds, follows the Steam mark's circle arrangement: equal circles laid along Steam's ~45° diagonal (mirrored so the filled node stays the chain's rightmost end) — two outlined rings high, the solid dot low at the rod's end, joined by two round-cap rods sharing one gentle knee (never a 90° corner, never one straight line; a minimal plugin graph whose flow ends on the filled node); the backdrop slider's zh title renamed 星图面板底色; **first npm release**: the package declares dsh.bundle (self-registering layer) and ships built artifacts — host half bundled to dist/index.js (plain JS entry, dist/web/index.html alongside), one-command install `dsh plugin --profile web add dsh-schematic`
- **v0.2.26** — dot hover cards: pausing on any dot in the star map raises that package's card — the star-colored swatch, short name, package id, one-line description, and link count; Chinese descriptions reuse the viewer page's shared translation cache, and a miss is translated on the spot (one batched line into the same sch.zhmap store the viewer reads — both faces warm the same cache); the card follows the cursor with window-edge flipping, hides while dragging or resizing the panel, and the panel's native tooltip never stacks with it
- **v0.2.25** — backdrop dial in settings + atom-bond icon: the backdrop's blur rides the same dial as its color — the backdrop-filter radius scales with --bgA, so 0% leaves the page untouched (a fixed 3px blur used to keep frosting the content behind a "transparent" panel); Settings → Plugin topology gains a backdrop slider (its zh title was 星座面板底色 at the time, renamed in v0.2.27), two-way live-synced with the wheel over the panel through one custom event (wheel moves the open slider too); the section's default gear in the settings nav is replaced by a hand-drawn glyph (the SPA assigns nav icons by section id, so a plugin-side observer dresses the row on each overlay mount without touching host code — the glyph itself was redesigned again in v0.2.27)
- **v0.2.24** — starfield dressing: uncategorized packages no longer render as a field of neutral gray — each picks one of six muted star tints by module hash, and dot brightness layers by edge count (hubs shine, leaves dim); a freshly lit dot ripples one expanding ring before its glow settles; the panel wears a deep-space backdrop whose opacity the mouse wheel adjusts live (0–100%, persisted per browser, 0 restores the fully transparent panel); every panel color now resolves through light-dark() following the page's color-scheme — the dark variants used to key off the OS preference, which mismatched whenever the app theme disagreed with the system
- **v0.2.23** — free placement + galaxy layout: dragging the panel body moves it anywhere on screen (clamped into the viewport, persisted per browser; dropping it back beside the composer card re-docks it to the card-follow), and the star map dropped its even ring packing for a deterministic force-relaxed galaxy — units seed on a golden-angle spiral in edge-count order, then repulsion/springs/centering spread them into clusters with breathing room, hubs drawn as larger rings (pure function of graph + aspect, so re-renders never reshuffle); resize drags stop at the window edge instead of hiding the panel, and stray no-button pointer moves can no longer contaminate a drag
- **v0.2.22** — miniature hardened + resizable: transport is now a ~1.2s poll of a new /schematic/mini.json (sequence-cursored rows + live states + recent traffic) — the per-tab SSE connection held by v0.2.21 ate one of the browser's six per-origin HTTP/1.1 slots and, with a few tabs open, starved the SPA's own boot RPCs until refreshes wedged on the loading page; the panel also rides above the composer seat's sticky stacking context (z 12 > seat 7) so its dots neither vanish nor lose clicks to the seat's frost surface; idle dots are now hollow rings that fill and bloom a double glow when lit, and a bottom-right grip resizes the panel freely (independent width/height within bounds, persisted per browser; the star map re-packs its rings to the new aspect instead of letterboxing)
- **v0.2.21** — composer-side live miniature: the domains expand-all mesh shrunk to a constellation strip beside the composer card — 12px past the card's right edge, vertically centered on the whole card (viewport-fixed, re-anchored to the card's rect every 0.8s so it tracks draft growth; auto-hidden when the window has no room); one dot per package ranked by edge count packing outward (dots wear the viewer's family palette; uncategorized packages stay neutral gray), edges as faint lines; lights with the viewed conversation's real runtime activity (strong breathing glow while a tool runs; soft glow decaying by TTL after model streaming, service reads, host actions), following the SPA's own persisted current session; double-click opens the viewer's domains tab already fully expanded (new ?expand=all deep link in the same viewer version)
- **v0.2.20** — background jobs observable: the jobs registry is live state — it reports its own changes through callbacks (onJobsChanged/onJobDone) and writes nothing any session log carries, so a background bash run was visible only as the tool call that started it; the collector now subscribes as a pure listener (no topology edge, nothing appended or wrapped) and lands start/settlement rows — label, terminal status, run duration — attributed to the jobs provider, lighting its pill; the in-page renderer (client-ui-jobs) stays dark by design: it lives in the browser process and has no host footprint to observe
- **v0.2.19** — edge direction = service flow: every edge is drawn provider → consumer with the arrowhead on the injector — the service flows from its provider to its user (the old arrow rode the depends-on convention and pointed the opposite way, contradicting what the v0.2.14 notes promised); legend, hover title, and direction note reworded to match
- **v0.2.18** — one-click group expansion: every group card in the overview (core spine, family, cluster) gains a hover-revealed corner ⊕ that scatters it into member pills and re-packs the whole mesh — members rank by their own edge count, and edges that lived inside the card become visible; the group's detail panel offers scatter/collapse, a member's detail panel collapses its group, and a footer chip next to Fit expands or collapses all groups (expand-all unions with what is already scattered and only reads "collapse all" once every scatterable group is dissolved)
- **v0.2.17** — domains fill fit: the radial mesh fills the canvas at any window shape too — pills pack along elliptical rows whose axis ratio is bisected for fit balance (the crossing of the fit scale's width/height terms), the widest mesh the canvas shows without letterboxing, and a resize re-solves the layout automatically
- **v0.2.16** — fill fit + Ungrouped auto-send: the journey tab's Fit now solves for a width that fills the canvas instead of scaling to height only (pills re-wrap at the solved width, so no letterboxing at any window shape), and the ask-in-chat hand-off now lands in a fresh Ungrouped session with the question sent for you through the RPC gateway — the send itself unlocks the composer (a workspace-less blank stays inert by harness design), and a failed send falls back to a prefilled draft
- **v0.2.15** — core-spine card: agent-loop, system-prompt, and tools ride only universal ctx keys, so no capability seam claims them — the domains overview groups them into one openable "core" card (double-click or the detail panel's open button) that sits at the mesh center as the most-connected unit; the catch-all "other" family is dissolved, leaving one-off packages as lone pills
- **v0.2.14** — radial mesh overview: the domains overview drops its column arrangement for a radial mesh — units are ranked by unit-level edge count and packed outward from the center, so centrality tracks link count directly (no explicit layers); collision-aware circular-row packing keeps the disc compact and overlap-free, edges become border-anchored straight lines (arrowheads at the consumer), and the layout is fully deterministic. The scope view inside a cluster/family keeps its semantic columns
- **v0.2.13** — service-access observability: pure-wiring packages (provide/inject only, no broadcast listeners) get their first live signal — one internal/get waterfall listener (the framework's own "a service is being read" extension point) counts every ctx.\<service-key\> access as (reader package, key), behavior-neutral (count-and-delegate only, values never wrapped); the timeline gains a toggleable service-access row type, both ends light up (reader and the key's provider), and each package's detail panel lists the keys it actually accessed with counts; one turn traces agent-loop → ctx.llm, tool-bash → ctx.shell ×3 + sandbox/shellEnv/subprocess
- **v0.2.12** — broadcast reception: the live event bus keeps every listener record (ctx.events._hooks, attributed through the registering fiber), so the viewer can answer "who receives this broadcast" — the activity bar names the session/event listeners, each non-action timeline row carries the receiver list on hover, and every package's detail panel lists the events it listens to; a receiver's reaction still shows as its own attributed row
- **v0.2.11** — journey auto-fit: the journey tab scales its stage cards so the whole message path is on screen with zero scrollbars at any window size; the +/−/fit buttons magnify past the fit scale for reading (drag-pan and scrollbars take over), and fit restores the overview
- **v0.2.10** — openable family cards + streaming-highlight fix: domains-view family cards (llm, client — same-prefix packages without a capability seam) now open like clusters, via double-click or the detail panel's open button, into the same scope view; the strong highlight after a model stream used to never decay (the llm card stayed lit forever) and now downgrades when the message completes and expires on the TTL
- **v0.2.9** — all-actions observability: an RPC-gateway observer wraps every apiProxy mutation method as a behavior-neutral pass-through, and live registry events (tools/change, system-prompt/change…) join the same feed — so UI actions that never touch any session log (archiving a session, renaming a workspace, changing settings) light their owning plugin and land in the timeline as action rows with duration; still a pure observer of the host process
- **v0.2.8** — runtime activity: an SSE feed (/schematic/events) observes the session-event firehose in-process and attributes every turn, model reply, and tool call to its owning plugin; the viewer lights the plugin the work is currently flowing through (strong while a tool runs or the model streams, breathing-glow decay after), plus a collapsible activity timeline bar and a session selector that defaults to following the SPA's current chat (pure observer — nothing is appended to session logs)
- **v0.2.5** — readable topology: three tabs — journey (one message's path through the runtime as eight stage cards with the ctx keys exchanged between stages), domains (family cards group same-prefix packages without a capability seam; every edge is an arrow with a hover card naming consumer, provider, and the injected ctx keys), table; live refresh every 5s with a change toast, new plugins pulse, `?tab=` deep links
- **v0.2** — live topology: mounts as a plugin, merges the Cordis runtime × loader streams, serves the viewer at `/schematic` with a one-click EN⇄中文 whole-page switch (descriptions batch-translated by the in-process LLM, identifiers kept in English), a dsh settings section that opens the viewer in a new tab, and an ask-in-chat hand-off that prefills a fresh ungrouped conversation
- **v0.1** — static topology: scan a harness checkout (`tools/scan.mjs`), render the plugin dependency graph from `inject` declarations
- **v0.0.x** — name reserved, positioning docs
