# Changelog All notable changes to dsh-tui-pi are documented here, grouped by release. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Fixed - **`SESSION_LOG_FILE_NAMES` now carries the v4 artifact names** (`session.v4.jsonl[.zstd]`, dsh 0.1.7-rc.1's current Session format generation). Every store walk that stats the log — the retention janitor's mtime walk, the /resume size-floor and last-update map, `locateSessionLog` — was blind to every v4 session on an rc.1 host: the janitor never deleted aged sessions, and the resume byte floor judged every row as unknown-size. Surfaced by the podman e2e retention scenario (four "survived age rule" failures); a v4-name fixture test pins the list. ### Changed - **e2e scenario suite rides the 0.1.7-rc.1 storage layout.** The settings assertions read the active profile's `cordis.patch.yml` (rc.1 removed the `~/.dsh/settings.yaml` sections; theme/login/limits persist through the per-entry profile patch now) with a structured upsert helper (`e2e/lib/patch-setting.mjs`); the retention seeder stamps Session format v4 (`encodeCurrent` refuses v3) and lays down `session.v4.jsonl.zstd` artifacts; the hard-stop ladder's registry pin moves to 0.12.1 (the leaf-deny fix — 0.12.0's stock deny list named the unregistered `ralph` and rc.1's fail-fast `tools.restrict()` aborted every dispatch); scenarios 69/71/75 run for real under the mock route instead of skipping on a stale settings.yaml probe. The e2e Containerfile no longer swallows a failed `npm install -g` behind `|| true` — a poisoned-but-cached layer made pnpm vanish for every later rebuild. - **Sibling bundle ranges ride the 0.1.7-rc.1 wave.** All nine `@aiwayds/*` dependencies move to the newly published minors (`ask-router ^0.5.0`, `dcp ^0.12.0`, `llm-proxy ^0.6.0`, `llm-stats ^0.7.0`, `mcp-adapter ^0.5.0`, `model-sync ^0.6.0`, `profile-switch ^0.2.0`, `subagent-registry ^0.12.0`, `web-search-anysearch ^0.5.0`) — 0.x carets cannot cross a minor, so the floor bump is required for the wave's settings/message-model adaptations to install together. ### Added - **Renderer support for the 0.1.7 message model.** `developer/message` events (the new tool-addition / tool-removal blocks) render as one system-class `ⓘ` line (`+name` / `-name`) in the transcript and join the remote-view durable rows; `system/message` stays non-conversational and renders nothing. Tool results follow the new role `'tool'` message shape — `toolCallId` / `isError` read at message level, content is the flat block list — everywhere (ToolPanel settle, subagent viewer, occupancy pricing). - **The `/agents → l` limits panel mirrors the official subagent caps.** Two read-only rows show the dsh-subagent plugin's live `maxActiveSubagents` (default 8) and `maxDepth` (default 1) — read through the settings forms when the entry is loaded, annotated `n/a` when it is not — alongside this TUI's own `maxAgents` / `maxRounds` / `maxRoundsGrace` / fence toggles, which stay editable here. `AgentView.mode` accepts the catalog v1 `'unknown'` arm so catalog-derived children render tolerantly. - **Session log V4 regression tests** (test/projcache.test.mjs): the projection-cache wrapper keeps V4-era records byte-identical, backfilled pre-V4 records stay schema-valid (fold-ineligible cache miss, never a boot failure), and the official `dsh-session-format-v3-to-v4` header migration edge runs and passes its target validator. - **Plugin Manager metadata.** Added `icon.svg` and `locale/{en,zh-CN,ja,ko}.json` (`meta.title`/`meta.description` per the official `readPluginMeta` contract, which reads only the singular `locale/` dir — the bundled `locales/` i18n is untouched); `package.json` now declares the `icon`, files, and exports entries. ### Changed - **Producer sources declare their own kind.** dsh 0.1.7 removed the shared `'plugin'` MessageSource kind; this plugin now injects its btw side-questions, maxRounds wrap-ups and Ctrl+G steers as `source: { kind: 'dsh-tui-pi', surface: … }` (src/source-kind.ts). The injection markers (`⚡`) recognize BOTH spellings, so pre-0.1.7 session logs replay with their markers intact on resume. - `snapshotEvents()` call sites (replay/resume/fork/export/history viewer — 15 hits) are annotated and kept on purpose: they re-consume raw events, which the 0.1.7 projection / `handle.read()` successor does not serve (soft-deprecated upstream, runtime unaffected). - **Agent presets read the 0.1.7 registry; Cordis picker semantics updated; directory presets retired.** The `/preset` roster now comes from the host's `agentPresets` registry (`@deepseek-ai/dsh-agent-preset-registry`, `remoteExportList()` — the same face as the remote `agentPresets.list` RPC), so the roster follows the deployment's declared bundle-patch presets: `isDefault` is registry-provided (no longer guessed), the shipped order is `standard` / `ptc` / `minimal` / `cordis`, and a declaration that fails to activate stays listed with a ⚠ badge and its reason. `cordis` renders under its new upstream meaning — the plugin/preset **authoring** slot ("Creator"; the dynamic define/run tools were removed upstream) — and `ptc` rides the new PTC runtime (`workflow-ptc`/`ptc-runtime`). The `trust` field is gone (removed upstream with the old presets package). The 0.1.5 directory scan (`agent.cordis.yml` + `preset.yml` roots) is dead upstream and survives only as a deprecated fallback for pre-0.1.7 hosts; on a 0.1.7 host it finds nothing and `/preset` disables gracefully. **Users of directory presets:** re-declare yours as a bundle-patch row with the SAME id (see docs/features/preset-switch.md) — old directory presets are not read anymore, and sessions recorded under an id the registry no longer declares fail to resume. Switch / fork-at-turn / rememberPreset (workspace-presets.json) semantics are unchanged. The `/settings → Agent Presets` category now targets the registry's profile entry (`agent-preset-registry`: new-task default + picker toggle) instead of the removed `agent-presets` namespace. - **dsh 0.1.7 settings migration — the configuration surface moved to the plugin's declared Config schema.** dsh-settings 0.1.7 removed the runtime `register` / watch API and the `settings.yaml` document. The plugin now declares its whole user-facing configuration as a static `Config` schema (every field `.volatile()`), the loader projects it into the settings surface, and `apply(ctx, config)` receives the live volatile references — all readers (`readThemePreference`, `readSubagentLimits`, `readModelPrefs`, …) go through `.get()` and no longer touch the settings service. The `dsh-tui` keys, defaults and semantics are unchanged. - **Hot-reload rides `settings/document-updated`.** The 0.1.5 watch hook is replaced by the document-updated subscription: a committed volatile-only write (the `/theme` picker, `/settings`, `/language`, `/model` favorites, `/agents → l` limits) swaps the references in place and hot-applies with no restart — the old `'live'`/`'restart'` split is gone, everything is live. The `/settings` browser keeps its revision-guarded `mutate` write path and now lists settings-bearing plugin entries by their profile entry ids. - **Entry id `dsh-tui` (was `tui-pi`)** in the bundle patch: the settings namespace IS the profile entry id under 0.1.7, so mounting as `dsh-tui` lets the one-time legacy import pick up every existing value. On the first 0.1.7 boot the removed `~/.dsh/settings.yaml` is renamed `settings.yaml.imported` and its `dsh-tui:` section merges into the profile's `dsh-tui` entry config automatically — no user action needed. - Dependency floor: `@deepseek-ai/dsh-settings` / `dsh-skill` / `dsh-user-questions` peers raised to `>=0.1.7-rc.1` (devDeps pinned `0.1.7-rc.1`). ## [2.21.0] - 2026-09-22 ### Added - **Scrollable answer window in the `/btw` overlay.** A long answer used to render as a fixed tail window (only the newest rows visible, `… N lines above`); the window now scrolls: `↑`/`↓` line-scroll, PgUp/PgDn page. The view stays pinned to the tail while streaming (newest rows always on screen); an explicit scroll-up detaches so you can read back through the answer while it keeps growing, and reaching the bottom re-attaches. Both ends of the overflow are named (`… N lines above` / `… N lines below`), and the footer carries an `↑↓ scroll` hint only when the answer actually overflows the window. Esc semantics unchanged — closing never stops the run. ### Notes - Compatibility note (README): resuming under dsh 0.1.6 a session saved under 0.1.5-rc.2 with reasoning content in subagent completion notices fails to serialize the first model request (upstream B-21, host-side data issue) — start a new session instead. - CI hardening (no shipped-code change): the dsh CLI the CI installs is now the publish wave's own closure (npm overrides pin every `@deepseek-ai/*` range to its base version) with a dist-tag fallback, so an incomplete upstream publish wave can no longer redden CI. ## [2.20.0] - 2026-09-17 ### Added - **UI languages — one JSON file per language, Chinese first.** Every user-visible TUI string now resolves through an i18n catalog: flat `locales/.json` files (`en.json` is the canonical byte-identical template; `zh-CN.json`/`ja.json`/`ko.json` ship fully translated — 638 keys each across the footer, panels, dialogs, pickers, settings browser, hotkeys manager, ask-user panel, subagent viewer, history/resume and every echo). `/language` **asks through the native ask-user panel** — the installed languages become one question (the current language first and tagged, so the panel's no-input timeout keeps it instead of switching); answering works on any registered surface (the TUI, or a phone through dsh-feishu) with first answer wins. `/language ` switches directly. The `/settings` browser's `dsh-tui.language` row opens an in-place picker instead of a raw text field. Switching applies live — the next repaint speaks the new language; the choice persists to the new `dsh-tui.language` key (18th key of the namespace, `applies: 'live'` via the settings watch hook, unknown ids fall back to `en`). Adding a language or partially overriding a shipped one needs zero code: drop `.json` into `~/.dsh/locales/` — a same-id file merges per key over the bundled one, a new id registers as a new language (non-empty `name` + flat string entries; invalid files are skipped fail-open with a warning). Enforcement rides the test suite: every `t('…')` literal must exist in `en.json` (and vice versa), every bundled locale must carry exactly en's key set and per-key `{param}` placeholders. Known edges: `/settings` schema descriptions resolve at boot (restart to re-translate those), already-rendered transcript rows keep the old language until a rebuild, and command descriptions in the autocomplete list apply on restart. - **Settings browser rebuilt — everything one level closer.** The `/settings` first level is now alphabetical: Agent Presets · General · Models · Plugins · TUI. The TUI namespace gets its own first-class category that drills straight into its fields (no intermediate namespace list), with the subagent knobs (`maxAgents` / `maxRounds` / `maxRoundsGrace` / `disableSubagent` / `registeredOnly`) collapsed under a **Subagent** group row (display-only grouping — settings.yaml paths never move). Settings namespaces registered by dsh plugins group under **Plugins**, so the catch-all *Other* category no longer appears. The Models category lists each configured provider with its **configured model ids as the value column** (empty when none are configured) and the API-key state in the description. ### Fixed - **Quitting can no longer hang the terminal.** Ctrl+C×2 (and every other self-initiated exit) now goes through a guarded hard-exit: the exit is deferred one event-loop turn off the callback stack, and a Worker-thread watchdog SIGKILLs the process if the graceful exit has not landed within 3 s. Root cause (caught live with `sample`): `process.exit` drained the libuv threadpool forever when a file-close completion raced the quit — the process hung after printing the resume hint, holding the terminal foreground with cooked mode restored but no shell to come back to. ## [2.19.0] - 2026-09-16 ### Added - **Ask User Question auto-answer timeouts — the panel never waits forever.** Every focused question carries a dual rule: an **idle window** (default 5 min — any keypress restarts it, so an actively interacting user is never interrupted) and an **absolute cap** (default 10 min — fires even under continuous input). A firing timer auto-answers the focused question with the **recommended option** (first in the list, per the `dsh-tool-ask-user` contract), hops to the next unanswered question with fresh budgets, and — once nothing is left unanswered — settles the envelope directly (the review page is a human double-check an absent human cannot do); a review-phase timeout submits the answers already given. Safety and honesty guarantees: `plan-review` questions are **never auto-approved** (a non-approve option is picked instead), a half-typed sentinel buffer commits as the custom answer (the panel's existing commit-on-exit semantics), and every automatic pick is declared to the model in-band via a note in the answer's `custom` field (`Auto-answered after the no-input timeout: …`) — never silently passed off as a human choice. The panel footer shows a live `auto in m:ss` countdown while a timer is armed (riding the footer clock's 1s render, no extra timer), and a transient notice reports the auto-answer once it happens. Knobs follow the repo's standard precedence chain — settings.yaml `dsh-tui.askUser.{idleMinutes,absoluteMinutes}` > `DSH_TUI_ASK_USER_*` env > defaults; invalid settings emit one notice each, `<= 0` disables a rule, and both rules off reproduces the legacy wait-forever panel. Timers are re-read per ask (a committed settings change applies to the next question without a reload) and are fully inert when disabled — including never reading the injectable clock, so queue-clock tests stay aligned. ## [2.18.2] - 2026-09-14 ### Changed - **The `@aiwayds/dsh-profile-switch` floor is raised to `^0.1.2`** — the companion plugin's docs-only 0.1.2 (TUI pairing guidance, no-wiring install note) is the version this package is validated against; the previous caret range already resolved to it on fresh installs. ### Fixed - **e2e scenario 22-search matches pi-tui 0.85.1's reworded placeholder** — the search overlay title changed from "Find transcript" to "Find in transcript" with the pi-tui 0.85.1 bump; the scenario needles still waited for the old wording and failed two assertions even though the overlay opened and worked. Test-infra only, no runtime change. ## [2.18.1] - 2026-09-14 ### Fixed - **`/profile-switch` + `/profile-cfg` no longer abort with "aborted due to timeout"** — the 2.16.0 extraction of the profile panels into the external `@aiwayds/dsh-profile-switch` plugin also dropped their names from the TUI's `MODAL_COMMANDS` set, but the TUI's submit dispatcher wraps *every* plugin command in the generic 90s guard, so the externally-registered commands fired it mid-configuration. Both names are re-whitelisted with never-aborting dispatch, plus a comment anchoring why externally-owned names must stay in the set. ### Changed - **Theme picker layout** — the theme list renders as a single flex column sized to the widest theme label (floor 20, cap 28 columns) instead of a fixed ~40% share, so the live preview grows into the freed width (~98 of 120 columns at the dual-pane threshold vs ~71 before). Row descriptions still exist and surface in the preview's title bar. ## [2.18.0] - 2026-09-12 ### Added - **Themes are now files** — the palette set moved from two hard-coded GitHub palettes to a `themes/` directory of 20 built-in JSON themes (10 light + 10 dark: github, one, solarized, gruvbox, catppuccin, tokyo-night, ayu, material, nord, dracula, monokai, synthwave) with automatic discovery. A new theme registry (`src/theme/registry.ts`) parses the JSON contract (15 required fields + 8 derived blend fields), scans the builtin `themes/` directory and the user theme directory, and merges user themes over built-ins by name. `/theme` now lists every registered theme, and `DSH_TUI_THEME` / `dsh-tui.theme` accept any registered theme name. Drop a `.json` file into `~/.dsh/themes/` (or `$DSH_HOME/themes/`) to register your own — same-name user themes override the built-ins. Invalid theme files are skipped with a warning, never a crash. - **`/theme` live preview** — the picker is now a dual-pane overlay: the theme list on the left (flat two-level: `auto`, then every registered theme), and a mock chat page on the right that repaints with the SELECTED theme's real surface colors on every cursor move — user bubble, think panel, tool card, code block, assistant reply, input dock — so a theme is previewed before it is committed. Stacks vertically below 100 columns. Repeated `resolveTheme` calls now return the same bundle per palette (`WeakMap` cache), restoring `applyTheme`'s identity no-op guard. ### Changed - **Unified slash-command registration** (`registerLocalCommand`): local dispatch and agent discovery registration (`ctx.commands.register`) share one helper — agentless bodies go through both, so autocomplete matches the web client. ## [2.17.0] - 2026-09-12 ### Changed - **`/profile-switch` + `/profile-cfg` now auto-mount with the TUI.** The bundle patch inserts `@aiwayds/dsh-profile-switch` (the ninth default dependency) directly, so upgrading profiles get the commands back with zero user action — no `bundles` edit needed. Profiles that also list the plugin explicitly are unaffected: its apply is idempotent across mounts (0.1.1). ## [2.16.0] - 2026-09-12 ### Changed - **Model profiles moved to the new [dsh-profile-switch](https://github.com/fan56/dsh-profile-switch) plugin.** `/profile-switch` + `/profile-cfg` are gone from this suite (panels, local stubs and MODAL_COMMANDS entries removed — the commands now come from dsh-profile-switch and work on the TUI *and* the web surface through the host's ask-user flow, instead of TUI-only panels). This plugin keeps the full read side: new sessions still seed from the `.dsh-profile` tree pin, `/agents` edits stay scope-aware of the pin, and the shared `$DSH_HOME/model-profiles.json` store is untouched. It now provides the `dshTuiModelSelection` bridge (its live selection ref) so dsh-profile-switch's switch applies to the current conversation live; without the bridge present that leg degrades to "next session", everything else works. `@aiwayds/dsh-profile-switch` now ships as a default dependency of this package — add it to your profile's `bundles` list to get the commands back. ## [2.15.0] - 2026-09-11 ### Changed - **The `/resume` display window widens from 7 days / 20KB to 30 days / 1KB, and the retention janitor's age window follows (7 → 30 days).** A real session idle for a fortnight (a handful of commands, a few KB on disk) used to vanish from the picker and could be deleted at startup before it was ever resumable. The 30d window covers a month's working set; the 1KB floor still hides e2e/demo stubs (~200B) while letting genuine short conversations through. Data and format are untouched — stored v0 logs still auto-migrate to the current generation on open. Both knobs stay configurable via `dsh-tui.resume.*` / `dsh-tui.retention.*` (settings > env > default). ## [2.14.0] - 2026-09-11 ### Changed - **dsh support floor raised to 0.1.5-rc.2.** `HOST_FLOOR` follows the 0.1.5 migration line: hosts below it get the startup warning and exit 1 (the `DSH_TUI_SKIP_HOST_CHECK=1` escape hatch still applies); rc.2 sits exactly at the floor and rc.1 now sorts below it. The dev closure and the e2e image ride rc.2 — a code-zero-diff re-pin of rc.1. READMEs and CONTEXT.md updated. ## [2.13.0] - 2026-09-10 ### Changed - **The Todos panel windows to 5 rows and auto-pages with the run.** The table used to render EVERY todo unbounded, so a 20-item plan boxed 20+ rows above the editor and squeezed the transcript. The visible slice is now re-derived per render by status priority — `in_progress` → `pending` → completed (done newest-first) — so each `todo/write` snapshot alone slides the window onto the newly active items: no timers, no keybindings, the status churn IS the paging. Rows keep their global plan numbers and a `· showing 4-8` header hint (range-compressed for scattered windows) accounts for hidden rows; lists of ≤5 render exactly as before. In the tail phase (fewer live rows than slots) the freshest ☑ rows fill in beside the frontier, keeping the window a pinned 5-row viewport that rides the active front to completion. - **dsh closure moved to 0.1.5-rc.1** (dev pins, overrides, peer floors). - **Live streaming rides `agent/assistant-stream` frames.** The 0.1.5 firehose delivers settlements only (`assistant/chunk` is gone). Start frames pin the attempt's turn/step; chunk frames drive the transcript typewriter, the think/tool phase machine, the occupancy estimate, and the child rows' live tail. A `streamDelta` replay op keeps the in-flight typewriter across theme/relayout rebuilds. - **V3 session artifacts**: log discovery (`SESSION_LOG_FILE_NAMES`, remote-watch candidates) knows `session.v3.jsonl[.zstd]` and prefers the current generation over preserved legacy files. - **Persistence seam normalization**: `list()` now returns `{header, revision, sizeBytes}` snapshots (`headerOf()` accepts both shapes); the removed one-shot `inspect` is replaced by `readPersistedSession` draining `open(id, 'read')` handles — /resume previews, /session info and the resume pre-check work on 0.1.5. ### Fixed - e2e: default-model marker renamed `deepseek-flash`; the 66 seeder writes V3 headers via the new `encodeMaterialization` signature with audited-clean filler rows; `wait_pane` dumps the pane on timeout; the version assertion is generic semver. ## [2.12.0] - 2026-09-10 ### Added - **Synchronous spawn admission ledger — the maxAgents burst hole is closed.** The guard used to read the bridge's *async discovery* count, so every spawn call inside ONE assistant message read the same stale `live` value: a burst of six background `use_agent` calls defeated a cap of two, all six answering "Started background agent" (found live, session 9c777e88). Every allowance now records an in-flight ledger entry at grant time, and every newly discovered child consumes exactly one, so same-step bursts see each other and deny at the cap exactly. Credited-id reconciliation handles settle/discovery interleaving without phantom slots (a live-count delta would double-hold on a net-zero swap); a 30s TTL releases the slot of a spawn that failed after admission; children the guard never admitted (workflow/ralph provider spawns, foreign surfaces) can only consume a phantom entry — the estimate errs conservative, never above the cap. - **The `subagent_status` model-facing board tool** — a read-only live board served straight from the bridge's real-time views and the policy's admission counters: cap headroom, per-child progress (rounds, tokens, context %, last tool, elapsed, retries), recent settles with outcomes, and the admitted/denied/pruned/in-flight counters. One call replaces probing spawn tools or re-deriving state from transcripts; registered host-level, so it is visible on every preset. - **The `dsh-tui.registeredOnly` fence (the 15th settings key, default off)** — denies EVERY spawn tool except `use_agent` for TUI-scoped callers, so a child can only ever be backed by a registered agent definition (`~/.dsh/agents/*.md`). This closes the ad-hoc "implement \" children that rode the native tools (found live the same day). Foreign surfaces stay fail-open; the fence reason outranks the cap reason; the knob is read live like every other. The denial teaches the equivalents — `background:true` for durable children, `resume`/`fresh` for continuation, parallel calls for fan-out — so the model redirects instead of retrying. `/agents → l` gains a fifth field row and an `r` shortcut; the bundled config skill documents the new key. - **Runtime counters in the /agents limits panel** — the panel header shows `live · admitted · denied · pruned · in-flight (since TUI start)` when the policy handle is available. ### Changed - The maxAgents denial message now directs the model what to do instead of only naming the cap: call `subagent_status` for the board, record the task with `todo_write` (pending), and dispatch it when a running agent finishes and frees a slot. ## [2.11.4] - 2026-09-10 ### Changed - The `@aiwayds/dsh-dcp` dependency floor moves `^0.7.0` → `^0.10.0` to pick up dsh-dcp 0.10.0 (the model-switch token-size gate for compaction notices) — same 0.x caret-boundary trap as the registry floor in 2.11.3: `^0.7.0` could never resolve past 0.9, so freshly installed profiles kept running dcp 0.9.x. The profile-side pin (`tui` profile) tracks the release per the rolling-latest policy. ## [2.11.3] - 2026-09-09 ### Fixed - **The round ladder was never wired** — THE "maxRounds is ignored" root cause: `bridgeCallbacks.onRoundCount` was assigned after `bridgeCallbacksWithTakeover` (a spread copy) had already been handed to the bridge, so the bridge held a callback object whose `onRoundCount` stayed `undefined` and the policy never saw a single round — no wrap-up injection ever fired, in any surface, since the knob shipped. The assignment now targets the object the bridge actually holds. Found by the new container e2e (scenario 75), not by unit tests (they call the policy directly). - **`⏻` hard-stop records no longer race event folds** — the record moved from the view object (whose snapshot-spread updates raced the cancel's own `turn/end` and observably lost the field) to an authoritative per-bridge map, composed onto `getAgentViews()` output at read time. ### Added - **Scenario 75-hard-stop** (container-only, offline via the mock LLM): dispatches a `deep: 0` agent whose frontmatter `maxRounds: 5` overrides the global `3 + grace 2`, whose mock persona never complies with the wrap-up, and asserts the full ladder end to end — wrap-up injected at 5 (⚡), force-stopped at 7 (⏻ stopped on the picker row, marker row `round 7 + grace exhausted, cap 5` in the viewer transcript, `⏻ hard-stopped @7` on the header), parent turn completing with the cancelled child's result. Also exercises the per-agent cap tier through the registry contract and the `/subagents` picker path. ## [2.11.1] - 2026-09-09 ### Fixed - The `@aiwayds/dsh-subagent-registry` dependency floor moves `^0.8.0` → `^0.10.0`: the per-agent round-cap tier needs the registry's `readAgentMaxRounds` contract export (introduced in registry 0.10.0), and a `^0.8.0` range can never resolve a 0.x minor beyond 0.9 — under it the probe silently degraded every dispatch to the global cap. The degradation path itself stays: an older registry (or a failed probe) still means the global cap, never a crash. ## [2.11.0] - 2026-09-09 ### Added - **The maxRounds hard-stop ladder** — the wrap-up injection is now stage 1 of a two-stage ladder, not a plea that can be ignored forever: a child that keeps burning rounds past `maxRoundsGrace` further rounds (default **7**, `dsh-tui.maxRoundsGrace`, `0` = warn-only, the historical behavior) is force-stopped by code — `cancel({kind:'user'}, {keepInbox:true})`, so a one-shot run settles `aborted` with its partial output in the parent's tool result while a continuable child's session and inbox survive for resume. Not an execution: a hard-stopped child is inspectable (Ctrl+G) and resumable, and its stop is reported through a new `⏻` marker on the compact line, the picker row, and a synthetic in-transcript marker row (`⏻ hard-stopped by maxRounds policy (round N + grace exhausted, cap M)`). The wrap-up message now warns the child about the grace window. Per-child caps resolve once and freeze (a mid-run settings edit never moves the goalposts between warn and stop). - **Per-agent round caps** — an agent `.md` frontmatter `maxRounds` key (positive integer, fail-loud like `thinking`/`background`) overrides the global round cap for children dispatched under that agent, via a new registry contract export (`readAgentMaxRounds(label, dir)`: exact agent name first, then a unique display name; any ambiguity/absence/older-registry falls back to the global cap — a lookup failure can only narrow the resolution, never widen the limit). The lookup is lazy (at the child's first cap crossing, label→roster) and consumed through the existing dynamic probe seam. - **Model route in the subagent surfaces** — the Ctrl+G viewer header is now two-to-three lines: identity+status, a route line (`provider/model · think level · mode`, the subagent transport demoted to the tail and only shown when it is not `spawn` — the old single line conflated the LLM route with the transport under one `provider` field), and the activity/budget line. Sources: the child's `request/header` call config (every child, one-shot included) or the continuable descriptor v3 route fields. The Ctrl+G picker row gains the model short name (`· hy3`). - **Viewer/picker management keys** — `k ×2` in the transcript viewer stops that child (double-press confirm, same arm as `x ×2`); `s` in the Ctrl+G picker toggles rounds-descending sort ("which child burns hottest"); `K` in the picker stop-alls (`cancelAllChildren`); the picker footer documents both. ### Fixed - **The maxAgents guard now covers TUI-descended children** — the guard (and the `subagent/start` prune) previously failed open for any caller without the surface marker, but the marker is only provided on the session setups the TUI itself runs: host-materialized children carry none, so a `deep ≥ 1` agent's spawn calls and any workflow fan-out under a TUI root were invisible to the cap. The enforcement scope is now "belongs to this TUI": a marked caller OR a live ancestor chain that roots at one (`session.header.parentSession` walk through `ctx.agents`, bounded at 4 hops with cycle protection; a broken chain or a foreign root — feishu, web — stays fail-open). The prune's ownership check rides the child's parent ancestry, so a foreign-rooted child is never pruned even when the cap is full. - **Live counts reconcile against the authoritative listing** — the 600ms reconcile now also corrects the firehose-folded live view with `SubagentRuntime.listDescendants` (projection-backed, no event dependency): burst-parallel spawns are counted within one tick instead of overshooting the cap until events arrive, and a child whose events never bubble is registered (its rounds then come from the log scan) instead of staying invisible; one-shot children the listing reports inactive settle off the live board instead of holding a maxAgents slot forever. Merge only adds/settles — never removes, never resurrects a settled continuable. - The maxAgents denial no longer points at a `list_agents` tool that does not exist in the distribution. ## [2.10.0] - 2026-09-09 ### Added - **Preset memory, per workspace**: the last `/preset` selection committed in a directory is remembered and becomes the next launch's selection in that directory — including the session-composition leg (`bridge.setAgentPreset` is seeded at startup, so the FIRST session of the day records `meta.agentPreset` and composes under the remembered preset instead of silently falling back to the server-side default). The toggle is `dsh-tui.rememberPreset` (default **on**; `false` in settings.yaml restores always-server-default); the toggle is read live, so a `/settings` change applies without a restart, and the store itself lives plugin-side at `$DSH_HOME/workspace-presets.json` (keyed by the session backend's project key — the model-profiles.json precedent: atomic tmp+rename writes, self-healing normalize, never fatal; a stale id whose preset was renamed/removed degrades to the stock default-selection behavior). Committed switches record (fresh, fork, and degraded-fork paths alike); a failed commit never records. - **Stop-everything confirmation dialog**: the double-Esc stop no longer auto-fires after a 200ms window — that confirmed the press *timing*, never the *intent*, and the gesture's reach had grown to every running subagent. The second Esc now opens a dialog that states the blast radius (main turn generating? N subagents running?) with `Stop everything` (preselected; `1`/`Enter`) and `Cancel — keep everything running` (`2`/`Esc`). While the overlay is up the keymap yields it every app key, so a mashing user cannot double-commit. Replaces the old STOP_CONFIRM_MS timer, the held-key abort notice and the third-press fast path. ### Fixed - **The Esc stop went blind exactly when only subagents were running**: the keymap's running gate read only the parent agent's mid-turn status, so with background/continuable children keeping the LLM busy after the parent turn ended, double-Esc resolved to a no-op and Ctrl+C cleared the editor — nothing could stop the burn. The gate now reads `bridge.hasRunningWork()` (parent status OR any live child), and the stop itself enumerates: `cancelAllChildren()` cancels every live child through the in-process registry handle (`ctx.agents.get(id)?.cancel({kind:'user'}, {keepInbox:true})` — the same idiom the maxAgents prune uses, since the wire `session.cancel` refuses subagent-owned sessions and a plain parent cancel only kills foreground children riding the tool-call abort signal). The Ctrl+C stop rides the same everything-stop. - **The subagent viewer's `x ×2` never stopped anything** — it closed the panel (the footer honestly said `x ×2 close`, but with the stop paths above dead it read as a broken stop). It is now the per-subagent stop: on a RUNNING child the double press cancels that child (registry handle, `keepInbox`), shows a `⏹ canceling this subagent…` notice and keeps the panel open so the status flip is visible; on a settled child it keeps the close shortcut; when the cancel finds nothing live (a raced/vanished handle) it closes rather than dead-keying. The footer is now state-dependent (`viewerFooter`): `x ×2 stop` while running, `x ×2 close` when settled. ### Changed - The bundled `dsh-tui-pi-config` skill's key table grows the `rememberPreset` row (13 keys) and gains troubleshooting entries for the stop dialog and preset memory; README (en/zh-CN) keyboard and configuration sections document the new stop semantics and the preset memory. ## [2.9.1] - 2026-09-09 ### Changed - The bundled skill is renamed `dsh-tui-pi` → `dsh-tui-pi-config` (ecosystem-wide convention: config/usage-guide skills end with `-config`). Bundled skills are registered in-process with zero on-disk footprint — updating the package and restarting dsh migrates the name automatically; the old `/dsh-tui-pi` slash invocation stops resolving. README skill mentions updated. ## [2.9.0] - 2026-09-08 ### Added - **内置技能(bundled skill)**:插件随包注册 `dsh-tui-pi` skill(`skills/dsh-tui-pi/SKILL.md`,经 `ctx.skills.registerProvider`,与 dsh-vault / dsh-llm-proxy 同款机制;顶层 `inject` 相应加入 `'skills'`)。用户在会话里问 TUI 主题/面板/footer、子代理并发与轮数、模型收藏与隐藏、会话保留清理或 `/resume` 过滤时,agent 自动加载指南;指南核心是一段 ask_user_question 交互式配置向导——只问三题(主题 auto/light/dark、面板高度 `'1'`/`'5'`/`'7'`/`'10'`/`'all'`、并发 `maxAgents` 4/8/0),代写 settings.yaml 顶层 `dsh-tui:` 段(只写问过的键),细调项指向全键表(12 键 / 15 叶子值 + 全部 `DSH_TUI_*` 环境变量,retention/resume 组优先级 settings.yaml 显式值 > env > 默认)与排障条目。包随 `files` 新增 `skills/` 发布;`@deepseek-ai/dsh-skill` 以 peer(`>=0.1.2-rc.1`,optional)+ dev(`0.1.2-rc.1`)声明。 - **README(en/zh-CN)「配置 / Configuration」一节补全**:此前沿用段落只覆盖 retention/resume 加 panelHeight/iconSet/cacheHitMode 一笔带过——`theme`、`maxAgents`、`maxRounds`、`disableSubagent`、`footerHints`、`favoriteModels`、`hiddenModels` 七个键从未文档化,现以全键表补齐(含默认值与作用),并指向内置 skill。 - 新增 `test/skill.test.mjs`:provider 注册、候选元数据(500 字符路由预算、resourceBase 指向打包的 skills/ 目录)、get() 去除 frontmatter、硬编码 description 与 SKILL.md frontmatter 反漂移断言、`stripFrontmatter` 边界。 ## [2.8.3] - 2026-09-08 ### Fixed - **Background subagent runs now show their registered nickname instead of a permanent `subagent `** — a background continuable child (dsh-subagent-registry's `use_agent` background leg, any native continuable spawn) gets its identity `subagent/descriptor` written as part of the child's **constructor seed**, and constructor seeds do not emit on the `session/event` firehose (dsh-session's `firstLiveSeq` contract). The bridge's label refinement lives on the firehose path, so it never fired for exactly the runs users need named: the live Agents board and Ctrl+G kept the anonymous discovery label forever, making a registered agent's background run indistinguishable from an anonymous spawn. Foreground one-shot children were unaffected — their descriptor is appended at runtime and streams fine. The fix routes the refinement through `reconcileChildRounds`, whose incremental scan of the child's authoritative in-process log already runs every 600ms and sees seed events: it now folds the descriptor with the same rules as the event path (provider/mode always; label only when present, so anonymous native spawns keep their honest `subagent `), gated on an actual change (already-refined views are a no-op, no extra `onLive`), and re-emits the live board once per tick when something changed. Children recovered after a TUI restart refine the same way. ## [2.8.2] - 2026-09-07 ### Fixed - **The footer hint no longer advertises the removed `Tab` preset binding** — the hint bar's preset segment still read `Tab: preset` after 2.7.0 unbound the key (a deliberate no-op since the confirmed three-way switch landed), so the footer told every user to press a dead key. The segment now reads `/preset: switch`, pointing at the only switching path; the width guards and the stale 103-column comments were brought along. ## [2.8.1] - 2026-09-07 ### Fixed - **Shipped presets are discovered from the running dsh install, not from hard-coded global prefixes** — `resolvePresetRoots()` probed exactly four absolute paths (homebrew / `/usr/local`), so on any other install layout (nvm-managed node, a custom npm prefix, pnpm global) the `/preset` roster went silent: only user presets from `~/.dsh/.agent-presets/` showed and every dsh-shipped preset (standard / minimal / cordis / ptc) vanished (#3). The `--profile tui` in the report was context, not cause — a profile changes which plugins load, never where the host's presets package lives. The shipped root now resolves dynamically: walk up from the running dsh entry script (real path'd past the bin shim) probing `/node_modules/@deepseek-ai/dsh-agent-presets/presets` — the same physical copy the host itself composes sessions from, so the roster always matches the ids a `/preset` switch can apply; then a module-closure resolve from the plugin (monorepo dev runs); then the old four-path probe as a last-resort fallback. Reconstruction of the reporter's nvm-style off-prefix install is covered end to end in `test/preset.test.mjs`. ## [2.8.0] - 2026-09-07 ### Added - **`dsh-tui.cacheHitMode` — choose what the footer CH segment reports** — `lastMessage` (the new default): the latest assistant message's own cache-hit rate (cacheRead ÷ (input + cacheRead + cacheWrite) of that single request), matching the pi-tui / pi-powerline-footer footer semantics; `session`: the previous session-cumulative rate over the whole session's input traffic. The mode is read live on every footer render, so a `/settings` change applies on the next repaint; the CH segment hides until the selected mode has a sample. Bridge stats gain `lastMessageCacheHitRate` (per-message rate; reset on `/new`, rebuilt by replay, untouched by route changes — a usage-less message leaves the previous sample standing). - **e2e `71-cache-hit` scenario** — the footer CH mode verified end-to-end in the container: the mock LLM gains `E2E_CH_TRIGGER` phases whose usage carries OpenAI `prompt_tokens_details.cached_tokens` (mapped to cacheRead by pi-ai's adapter); two scripted turns bill 90% / 20% per-message (55% cumulative over the fresh session), and the scenario asserts no CH before usage → `CH90.0%` after turn one → `CH20.0%` in the default lastMessage mode (stale 90% and cumulative 55% both absent) → an external `cacheHitMode: session` settings.yaml edit flips the same segment to `CH55.0%` live, no restart. ## [2.7.1] - 2026-09-05 ### Changed - **/history: oversized turns truncate instead of stalling** — a turn whose assistant content exceeds `MAX_DETAIL_LINES` (4000 source lines) builds only the budgeted prefix and ends with a dim `… more lines truncated — /resume for the full turn` marker; selection of pathological turns no longer freezes the panel on a full Markdown parse. - **a resize no longer kicks the /history list out of filter input** — `rebuildListPanel` re-engages the engaged filter on the rebuilt TablePanel (query, cursor and input mode all survive; new public `TablePanel.beginFilterInput()`). - **Clean-uninstall documentation and coverage** — README/README.zh-CN gain an Uninstall section (removal steps + the full on-disk artifact inventory), and the boot smoke gains an uninstall leg asserting `dsh plugin remove` reconciles the profile tree back to stock. ### Fixed - `turnSeedSlice` now slices at the SELECTED fold's own end seq — a damaged log with duplicate turn numbers no longer cuts a fork short at the first match. - Dead references to the retired HANDOFF.md removed from AGENTS.md / README / README.zh-CN.md; dead `else` branch in the history browser's picker cancel path removed. ## [2.7.0] - 2026-09-05 ### Changed - **BREAKING: `Tab` no longer cycles agent presets** — the `preset-cycle` app binding is removed (Tab is unbound, with no replacement); an entry referencing `presetCycle` in `~/.dsh/keybindings.json` is ignored with a warning notice now. Preset switching is `/preset`'s explicit flow only. - **/preset switching is a confirmed action that takes effect immediately** — with a live session, every switch path (picker `Enter`, `/preset `, `/preset next`) opens a confirmation dialog (*Switch preset to \?*; *Restart session on \?* when the target is already selected) with exactly three options: **Fork & switch** — the new session on the preset is SEEDED with this conversation (the log through the last completed turn, compaction included; the transcript replays it and the old session stays resumable via `/resume`); **Fresh start** — a new empty session on the preset (the same detach path as `/new`); **Cancel** — nothing changes. Without a live session the selection applies directly (the first submit already creates the session on it). This removes the old footgun where the footer's preset silently disagreed with the live session until the next session happened to be created. New `src/preset-dialog.ts` (pure reducer + panel, the fork seed slicer, and the injectable `performPresetSwitch`); bridge gains `forkCurrentSession` (seeded `agents.create` + rebind; the teardown runs first — the TUI surface service cannot host two live agents — so a failed fork leaves the old session detached but resumable via `/resume`); `cyclePreset` → pure `peekNextPreset`. - **/history: fork at turn (`f`)** — the browser can now branch a NEW session at the selected turn and switch to it: the new session is seeded with the browsed session's events through that turn's `turn/end` (inclusive — the state as the turn finished; the in-flight turn stays out), under the CURRENT preset selection (no preset change), and the transcript replays the carried history. Works on live sessions and cold-read (`/history `) sessions alike — fork any stored session at any turn without resuming it. A confirmation dialog (*Fork at turn N?*) carries the semantics (turns through N of M; later turns stay in the current session, resumable via `/resume`); a failed fork keeps the browser open with the error and never touches the original session. New `src/history-fork.ts` (pure reducer + panel) and `turnSeedSlice`; rides the bridge's seeded `forkCurrentSession`. ## [2.6.0] - 2026-09-05 ### Added - **`/history` — the read-only two-pane history browser** (ADR 0003, CONTEXT.md "History browser"): the left pane lists the browsed session's completed turns (turn number + user-message preview, seq order — a list, not a tree), the right pane shows the selected turn's user prompts, step replies (Markdown) and per-tool call summary; `Enter`/`c` refills the editor with the turn's prompt (plain `setText`, never submitted), `s` swaps the browsed session through a `/resume`-style picker, `[`/`]` page the detail. Bare `/history` snapshots the current live session (no live updates while open); `/history ` cold-reads any stored log via `sessionPersistence.inspect` — no writer lock, no resume, no agent activation. New `src/history.ts` (viewer + session picker) and `src/history-turns.ts` (pure `SessionEvent[] → completed Turn[]` fold), 33 tests. ### Changed - **/history: fixed-window geometry + two-pane focus model** — the browser mounts at 90% × 85% of the terminal and renders exactly its line budget (short turns pad blank rows, long ones scroll; a selection change can no longer resize the window — only a terminal resize re-derives it), and the keyboard gains a focus model: `→` moves it to the detail pane (↑/↓ line-scroll, PgUp/PgDn or `[`/`]` page; `/`, `c`, `s`, Enter inert there), `←`/Esc steps back, Esc grades detail → list → applied-filter-clear → close, and focus is visible — the list's ▸ cursor demotes to `›`, the detail title turns accent bold with a `← list · ↑↓ scroll` footer hint. ## [2.5.0] - 2026-09-03 ### Changed - **companion plugins ride their rc/stable-line releases** — all eight bundled dependency floors move to the versions published alongside the dsh rc/stable policy change: `@aiwayds/dsh-ask-router ^0.4.0`, `@aiwayds/dsh-dcp ^0.7.0`, `@aiwayds/dsh-llm-proxy ^0.4.0`, `@aiwayds/dsh-llm-stats ^0.5.0`, `@aiwayds/dsh-mcp-adapter ^0.4.0`, `@aiwayds/dsh-model-sync ^0.3.0`, `@aiwayds/dsh-subagent-registry ^0.8.0`, `@aiwayds/dsh-web-search-anysearch ^0.3.0` (pnpm-lock converged in the same change) - **dsh host floor moves to the rc/stable line: `>= 0.1.2-rc.1`** — the peerDependencies range (`@deepseek-ai/dsh-user-questions`), the devDependencies pin, and the startup host-version guard's `HOST_FLOOR` all leave the alpha line (the guard's unsupported-host upgrade hint now reads `npm install -g @deepseek-ai/dsh@next`) - **CI, release and e2e retire the `@alpha` dist-tag** (policy 2026-09-03) — ci.yml, release.yml and e2e/run-e2e.sh resolve the newest of the `latest` (stable) and `next` (rc) dist-tags at runtime, never hand-pinned; when a stable 0.1.2+ lands on `latest` it wins over the rc by plain semver compare. The e2e Containerfile drops the `DSH_VERSION=alpha` default (empty default + an explicit required-build-arg guard; `run-e2e.sh` forwards the resolved version as `--build-arg`) - **README declares rc/stable-only support** — `Requires dsh >= 0.1.2-rc.1`; the alpha line is no longer supported (ADR 0002 marked superseded) ## [2.4.0] - 2026-09-03 ### Added - **startup host-version guard** — `apply()` resolves the `@deepseek-ai` closure version the plugin actually loads against and compares it against the peer floor (`0.1.2-alpha.4`, kept in lockstep with the peerDependencies range); an older host (the shipped 0.1.1-rc.2 stable line included) gets a one-line warning with the found version and the `npm install -g @deepseek-ai/dsh@alpha` upgrade path, then a clean exit (code 1, no stack trace) instead of raw TypeErrors deeper in boot. An unresolvable or unparsable closure version fails open with a warning (the guard is a human-readable exit, not install validation); `DSH_TUI_SKIP_HOST_CHECK=1` opts out. New `src/host-version.ts` (parse/compare on the semver subset dsh ships) + `test/host-version.test.mjs` 7 cases. ## [2.3.0] - 2026-09-03 ### Added - **e2e: two scenarios close the biggest default-feature gaps in the podman suite** — `69-btw` drives `/btw` end-to-end offline over the 68 mock route (idle paths: bare `/btw` usage text, the idle-rejection notice, the `--model` argument error; busy paths under a ~42s mock main turn: the framed overlay with the side model label, `Thinking…` → streamed answer, `btw queued (position 1)` + the overlay's `· 1 queued` status, the `Not kept in the session` badge, the promote-under-overlay notice, the live reopen via bare `/btw`, the screen-level not-persisted proof after Esc, and the Last-btw review); `74-profiles` covers the 2.2.0 model-profiles surface with zero model traffic (`/profile-switch` seeded roster → apply + `.dsh-profile` binding → the `● work` marker → `p` unpin; `/agents` scope-aware edits land in `model-profiles.json` under the pin with the frontmatter untouched, and in the agent file itself without it). `lib/mock-llm.mjs` gains two body-keyed phases — `E2E_BTW_Q` (checked first: the side call's snapshot embeds the main prompt) and `E2E_BTW_MAIN`'s slow tick stream. ### Changed - **dependency floor: `@aiwayds/dsh-subagent-registry ^0.7.0`** — 0.7.0 adds continuable background dispatch (`use_agent` gains `background: true`, returning a durable subagent id immediately) and the `ask_agent` follow-up tool (steer a running child or get the reply from a finished one; `askToolName` config, default `ask_agent`); pnpm-lock converged to the published 0.7.0 and the dev type closure moved to dsh 0.1.2-alpha.5 (the peer floor stays `>=0.1.2-alpha.4`) ## [2.2.0] - 2026-09-02 ### Added - **per-workspace agent model/thinking composition** — an agent's runtime model/think values compose at spawn time from the frontmatter baseline plus the pinned profile's per-agent overrides, via the `@aiwayds/dsh-subagent-registry@^0.6.0` contract (`composeAgentRuntime` / `workspaceProfileName` / `readModelProfilesDoc`; a built-in equivalent covers the registry's absence); non-empty entries override, empty/missing/null values fall back to the baseline ### Changed - **profile apply no longer writes global agent frontmatter** — `/profile-switch` binds the tree (`.dsh-profile`) and composes at spawn instead of rewriting `~/.dsh/agents/*.md`, so a profile applied in one workspace no longer leaks into every other workspace (the cross-workspace crosstalk fix) - **`/agents` edits are scope-aware** — with a workspace profile pin, model/think edits land in the profile's per-agent overrides (`model-profiles.json`) with the frontmatter bytes untouched; unpinned trees keep writing the frontmatter baseline; the dead `planAgentApply` write-plan path is removed and agentsDir resolution aligns to `$DSH_HOME` - **dependency floor: `@aiwayds/dsh-subagent-registry ^0.6.0`** — pnpm-lock converged to the published 0.6.0 in the same change - **docs** — CONTEXT.md floor claims updated to the actual `>=0.1.2-alpha.4` peer floor; README's dangling ADR 0002 link repaired ### Fixed - **e2e: the 66-retention-resume seeder resolves the host persistence backend dynamically** — probes `npm root -g` for the nested and npm-flat global layouts instead of hard-coding one, and fail-fasts with both candidates before touching fixtures (a seeder miss used to make the janitor "removed" checks pass vacuously) ## [2.1.1] - 2026-09-02 ### Changed - **e2e docs** — scenario table restored for `22-search` / `60-preset`; the container host now resolves from the rolling `@alpha` dist-tag (was the stale `0.1.2-alpha.3` pin), overridable via `--build-arg DSH_VERSION=` ### Fixed - **the podman e2e suite is green again on the current host line** — the `66-retention-resume` seeder passes the `dsh-session-persistence-jsonl@0.1.2-alpha.4` materialization contract (`encodeMaterialization({ meta }, events)` storage descriptor; the bare-meta call shape was the alpha.3 contract) ## [2.1.0] - 2026-09-02 ### Added - **default dependencies now bundle `@aiwayds/dsh-mcp-adapter`, `@aiwayds/dsh-model-sync`, `@aiwayds/dsh-llm-proxy` and `@aiwayds/dsh-web-search-anysearch`** — installing the package puts them into the profile's `node_modules`; activation still follows the profile's `bundles` list. The READMEs now recommend installing `@aiwayds/dsh-llmwiki-memory` (OKF topic memory for dsh) ### Changed - **dependency floors bumped to the current npm latest** — ask-router `0.3.0`, dcp `0.6.0`, llm-stats `0.4.0`, subagent-registry `0.5.0`; pnpm-lock regenerated in the same change ## [2.0.2] - 2026-09-02 ### Fixed - **the projection-cache boot-crash fix now covers plain `dsh --profile tui` boots, launcher or not** — 2.0.1 shipped the migration only behind the `dsh-tui-pi` launcher's preflight, but product users start dsh directly and never ran it. The bundle patch now disables the stock `session-projection-cache` row (guarded by `name`, so a renamed future row is skipped with a warning, never disabled blindly) and inserts a wrapper entry (`tui-pi-projcache` → `@aiwayds/dsh-tui-pi/projcache`) that re-exports the stock module unchanged but runs the record migration at module-evaluation time — the one hook the loader reaches strictly before the stock plugin's `Service.init`, so the legacy records are backfilled on disk before the domain opens and the first post-upgrade boot survives instead of crashing. The wrapper entry mirrors the stock row's `writeEveryEvents`/`writeIntervalMs` config (the schema requires them). The migration core moved to `src/preflight-projcache.ts`, shared by the wrapper and the launcher's CLI preflight; the boot smoke now seeds a legacy record into its scratch home and asserts the rescue (boot survives, record backfilled, backup written) ## [2.0.1] - 2026-09-02 ### Fixed - **Boot crash on dsh 0.1.2-alpha.4 with legacy session_projcache records**: the `dsh-tui-pi` launcher now runs a preflight migration (`bin/preflight-projcache.mjs`) before `exec dsh`, backfilling `identity.isSeeded: false` and `identity.inheritedEventCount: 0` on every legacy record under `/storages/session_projcache/sessions/` — alpha.4 fail-fasts on those fields at storage-open time and old records (0.1.1-rc.2 era) crashed the whole boot. Each rewrite is backed up next to the original and applied atomically; already-migrated records are left byte-identical; unparsable records are skipped with a warning; any preflight failure is swallowed so startup is never blocked. ## [2.0.0] - 2026-09-02 ### Changed - **BREAKING — dsh host floor `>= 0.1.2-alpha.4`, rc-line support dropped** (ADR 0002; adapted against `0.1.2-alpha.3`, the closure re-tracked to `0.1.2-alpha.4` before shipping): all rc/alpha dual paths and feature-detection are gone, single-target alpha only - ask-user answers on the Agent-scoped `'user-questions/request'` cordis waterfall only — the rc-era `ctx.userQuestions.registerProvider` slot (and its `DUPLICATE_PROVIDER` yield + `isDuplicateProviderError` classifier) is deleted; the dsh-ask-router surface path is unaffected - dsh-settings removed the `settingsNamespace()` runtime helper: the `dsh-tui` / `llm-pi-ai` / `llm-deepseek` / `agent-default-model` namespaces are plain literals now (type-level brand check via `SettingsNamespaceInput` + host-side runtime validation) - `permissionPresets.current()`/`set()` take the `Session` (alpha.3 folds knob state through the session projection) instead of the raw event array - the ask-user guidance system-prompt section rides `systemPrompt.getSectionOrder('PTC_ONLY')` instead of the hardcoded numeric order 112 (alpha.3 replaced the numeric constants with `getSectionOrder`/`getContextOrder`) - the `'todo/write'` event type comes from `@deepseek-ai/dsh-tool-todo` (moved out of dsh-session in alpha.3) via its SessionEventMap augmentation - preset discovery probes only the `@deepseek-ai/dsh-agent-presets` package layouts (the pre-alpha `/config/agent-presets` probe and the `metadata.yml` legacy fallback are gone; alpha ships `preset.yml` only), and the English mapping drops the `code` id — the shipped alpha roster is standard/minimal/cordis/ptc - peers: `@deepseek-ai/dsh-user-questions` `0.1.1-rc.2` (exact) → `>=0.1.2-alpha.4` (floor); devDependencies pin `0.1.2-alpha.4`; pnpm-lock regenerated in the same commit - **CI/resolution moves to the rolling `@alpha` dist-tag** (ci.yml + release.yml) — `latest` still points at the rc line the plugin dropped; the daily schedule stays so alpha.4+ drift surfaces within a day; new `scripts/smoke-boot.mjs` boots the packed plugin in a scratch dsh profile (mount + loader-clean boot proof) as a CI gate; e2e container pins `DSH_VERSION=0.1.2-alpha.3` ## [1.3.1] - 2026-09-01 ### Added - **`@aiwayds/dsh-llm-stats@^0.3.0` ships as a default dependency** so fresh installs of the TUI profile carry the stats plugin out of the box; activation still follows the profile's `bundles` list ## [1.3.0] - 2026-09-01 ### Added - **slash-command output containing a GFM table renders as a real table** — `renderCommandEcho`'s success branch (extracted as `appendCommandText`) routes captured output through the pi-tui Markdown component when it contains a GitHub-flavored-markdown table, so plugin reports like `/llm-stats` draw framed, styled tables instead of raw pipe rows. A new `hasGfmTable` gate keys on a strict multi-column separator row, so ASCII dashed rules (`|------|`) and plain-text command output keep the unchanged fallback; session replay shares the same path. +4 messages tests (16 → 20, full suite 1112) ### Changed - the generic slash-command guard (wedge protection) raised from 30s to 90s — slow but legitimate commands (model-backed handlers, network round trips) no longer get clipped; whitelisted modal commands keep the never-aborting signal - `/wiki` and `/vault backup|restore` are whitelisted from the generic command guard — both outlasted the 30s `AbortSignal.timeout` and died with a spurious "aborted due to timeout" while the panel stayed open ## [1.2.4] - 2026-08-31 ### Added - per-feature docs gain the second demo batch (real config, real model) embedded via GitHub user-attachments — footer, think & tool panels, subagents, DCP and themes - a preset-switching guide (`docs/features/preset-switch.md`, linked from both READMEs) covering the entry points (Tab / `/preset`), the shipped roster, and the two mechanics that routinely surprise: a switch is read only at session create (existing sessions need `/new`; `/resume` ignores the selection), and a preset gates only the agent-plane composition — profile-plugin tools like `use_agent` stay visible on every preset ## [1.2.3] - 2026-08-31 ### Fixed - `/profile-switch` is workspace-scoped — switching used to persist the profile's default model through the global agent-default-model (`settings.yaml`) and drive every panel's ● current marker from the machine-wide pointer, leaking into every other workspace; Enter now writes/rebinds a clean `.dsh-profile` for the current directory tree (hand-decorated files are refused, never clobbered), other trees keep their own binding or the global default, and the global default model remains `/model`'s write. +3 tests (bind/bound), 1108 green ### Changed - README feature sections split into per-feature docs (`docs/features/.md`); the in-repo demo-video embed experiment was reverted — `*.mp4` stays gitignored, videos move to GitHub user-attachments ## [1.2.2] - 2026-08-31 ### Fixed - the startup plugin tree resolves each user plugin's installed version from the profile root — the resolver anchored at this plugin's own module URL, so `link:`-mounted checkouts read the repo's dev `node_modules` (stale labels, e.g. subagent-registry 0.3.0 vs the installed 0.4.0) and plugins outside the dev closure rendered without a version at all - `@aiwayds` runtime dep floors relaxed from exact `0.3.0` pins to carets — subagent-registry `^0.4.0`, dcp `^0.5.1`; the exact pin made fresh installs pull a stale nested copy whose version the plugin tree then reported ### Removed - the deprecated `dsh-model-sync` mount — upstream archived it on 2026-08-28, and the stale bundle-patch insert crashed the whole plugin tree at boot (`ERR_MODULE_NOT_FOUND`) once a profile dropped the package ## [1.2.1] - 2026-08-30 ### Added - the startup plugin tree shows each user plugin's installed version (`├─ @aiwayds/dsh-feishu@0.5.0`) — resolved from the profile's own install tree by walking up from the specifier's resolved entry to the nearest package.json (bare specifiers and dev `file:`/path entries both work; an `exports` field that hides package.json cannot break it). Best-effort: an unresolvable plugin renders its plain name as before; base rows stay collapsed and unversioned ## [1.2.0] - 2026-08-30 ### Added - **Fullscreen transcript search** (`Ctrl+Shift+F`) — pi-tui ships the search wiring itself (input overlay anchored top-right, `enter`/`ctrl+g` and `shift+enter`/`ctrl+shift+g` navigation, escape to close) over the layout root's primary ScrollView, which is our transcript. Dispatch order keeps the app key chain safe: TuiAltScreen's viewport listener registers in its constructor, before the app-level Esc/Ctrl+C chains, so while the search is open enter/escape/ctrl+g are consumed modally. The footer hint bar gains a toggleable `Ctrl+Shift+F: search` segment (default on, `dsh-tui.footerHints.search`; the default hint grows 117 → 140 columns). e2e: new `22-search` scenario drives open/query/navigate/close/reopen over a real PTY - **Fullscreen selection copy lands in the system clipboard** — releasing a drag selection copies through our clipboard ladder (native `pbcopy`/`wl-copy`/`xclip`/`xsel`/`clip` + OSC 52 with a verified success path) via TuiAltScreen's `copySelection` option; pi-tui's bare-OSC-52 fallback shows "Copied!" while leaving the system clipboard untouched on macOS Terminal.app and tmux without passthrough. `DSH_TUI_COPY_ON_SELECT=0|false|off` keeps selection visual-only (default on, matching the always-copy behavior pi-tui 0.84.2 always had; any other value stays on so an invalid setting can never silently disable the feature). Mouse-mode ownership is unchanged (dsh writes the button-motion set; `DSH_TUI_MOUSE` still governs it) - **User-message image attachments render in the transcript** (`src/attachments.ts`) — dsh records attached images in user content as `ImageBlock`s whose attachment is a content-addressed reference (attachmentId embeds the sha256; bytes live under `$DSH_HOME/attachments/v1/objects/`). The transcript renders a muted placeholder the moment the event arrives and swaps it for a pi-tui `Image` component once `readImageFile` delivers the verified bytes — a real bitmap on kitty/iterm2 terminals (Kitty, Ghostty, WezTerm), the component's text fallback (shortened filename + mime) elsewhere. A failed load (missing object, corrupt digest, metadata mismatch) degrades to a muted `unavailable ()` note; the transcript never blocks or throws. Session replay covers images automatically (resume feeds historical `user/message` events through the same path); the session echo of a locally submitted prompt still dedupes the text bubble but renders attachments that arrived from other surfaces (web / feishu); at most 8 bitmaps per message, extra blocks collapse into a `+N more` line - **LaTeX → Unicode math rendering** in assistant messages — pi-tui's Markdown component (since 0.84.0) renders `$...$` inline and `$$...$$` display LaTeX as terminal-friendly Unicode (`e^{iπ}+1=0`, `∫₀¹ x dx`); assistant text already renders through that component, so this arrives with the dependency bump. Pinned by tests against the plugin theme; known upstream quirk (pi shares it): a literal `$$` pair in prose is consumed as display-math delimiters ### Changed - **`@earendil-works/pi-tui` `0.84.2` → `0.84.4`** — brings the image-heavy-output V8 string-limit crash fix (prerequisite for image rendering), terminal capability overrides (`PI_HYPERLINKS` / `PI_IMAGE_PROTOCOL` / `PI_TRUE_COLOR` env), `copyOnSelect` selection handling, terminal word-selection joiners for double-click (paths and kebab-case tokens stay whole), and the markdown table-cell style-prefix fix. Mouse parsers and StdinBuffer are byte-identical between the two versions (verified by diff and a coalesced-input probe) - **CI closure pin `@deepseek-ai/dsh` `0.1.0-rc.8` → `0.1.1-rc.2`** (ci.yml + release.yml) — CI now typechecks against the same closure as dev machines. The rc.8 closure predates the dsh attachment subsystem (`dsh-attachment` / `dsh-attachment-local`), so the new image-rendering import would fail the gate; rc.2 ships it (196-package closure, matches the link-dsh-closure source) ### Fixed - the mouse-garbage diagnosis corrected (no behavior change): the upstream input path splits coalesced SGR mouse chunks per sequence (StdinBuffer, verified empirically), so the 1.0.x "escapes pi-tui's single-sequence parsers" theory was a plausible mis-attribution — the shipped button-motion default remains the right mitigation under cmux regardless, since idle-motion bursts simply stop existing ## [1.1.0] - 2026-08-30 ### Added - `/btw` — by-the-way side questions while the main task runs: one tool-less one-shot model call (same model as the session unless `--model provider/model` overrides the route) over a read-only recent-conversation snapshot (`DSH_TUI_BTW_CONTEXT_MESSAGES`, default 6, clamped 0–50), streamed into a temporary framed overlay. Nothing enters the session log, the inbox, or any main-line model request; Esc closes the overlay without stopping the call — the finished exchange lands in an in-process last-btw slot and bare `/btw` reopens it (the live run, or the last exchange once settled). One side call at a time with a bounded queue (5); `/new`, `/resume`, a remote takeover and the stop gesture cancel the running call and drop the queue; the idle main line refuses `/btw` (a normal prompt is strictly better there — tools, history, full context). Snapshot filtering keeps real user prompts only, so `agent.inject()` synthetic context (file notices, skill content) never crowds the dialog window. Placement rationale: `docs/adr/0001-btw-tui-owned-command.md` (TUI-owned command, not an independent plugin) - 51 unit tests for the btw decision layer (arg parsing, snapshot assembly, single-flight queue, stream consumption, controller cancel/queue races); e2e: the node dist tarball pulls through the npmmirror binary mirror (nodejs.org is unreachable from the e2e container's network; `ARG NODE_DIST_BASE` switches back) ## [1.0.8] - 2026-08-29 ### Changed - the `cordis` preset label is `Cordis` (the preset id / framework name) instead of 1.0.7's `Create` — the official 创造模式 reads as a rendering, and the id is what the ecosystem knows ## [1.0.7] - 2026-08-29 ### Fixed - the footer brand segment showed upstream's Chinese-only preset display names (`dsh(标准模式)`) to every user once 1.0.6 unlocked `preset.yml` metadata on rc-era hosts. The roster now maps the shipped preset ids to English names — `standard`→Standard, `minimal`→Minimal, `cordis`→Create, `code` (rc) / `ptc` (alpha)→PTC — and falls back to the official string for anything unmapped (a renamed or newly shipped preset keeps its official display name until mapped). `/preset ` accepts the official name too (new `officialName` entry field) - corrected the metadata file precedence: `preset.yml` is the canonical display-metadata file on current rc and alpha hosts alike (`METADATA_FILE` in `@deepseek-ai/dsh-agent-presets`); `metadata.yml` is legacy and is only read when `preset.yml` is absent. The previous "metadata.yml wins" order was based on a wrong assumption and never matched shipped layouts ### Added - unit-test coverage for the English-mapping hit and official-string fallback, preset.yml-first metadata precedence with the legacy `metadata.yml` fallback, and official-name matching via `/preset` ## [1.0.6] - 2026-08-29 ### Fixed - alpha-era (v0.1.2-alpha+) host support for the standalone ask path: when `ctx.userQuestions.registerProvider` is absent, register a claim-scoped `'user-questions/request'` cordis waterfall answerer instead of silently degrading to NO_PROVIDER (asks for other sessions delegate via `next()`; rc-era behavior unchanged) - preset roster on alpha-era hosts: probe the shipped presets inside the `@deepseek-ai/dsh-agent-presets` package (nested and flat install layouts) in addition to the rc-era `dsh/config/agent-presets` root, and read `preset.yml` metadata (rc `metadata.yml` still wins when both exist) ### Changed - raised the `@aiwayds/dsh-ask-router` dependency floor to `^0.2.0` — older ask-router releases crash on load on alpha-era hosts ### Added - 5 unit tests (waterfall answerer claim/delegate/dispose, preset.yml metadata, dual-layout root probing) via a new `__setPresetRootOverride` test seam ## [1.0.5] - 2026-08-29 ### Changed - **`@aiwayds/dsh-model-sync` dependency floor `^0.1.1` → `^0.1.4`.** model-sync 0.1.3 crashes every dsh boot (cordis 4 rejects its bare `ctx.commands` read without a declared inject; fixed in 0.1.4, which registers `/model-sync` through a `ctx.inject` sub-fiber). The floor bump guarantees a fresh install of dsh-tui-pi can never resolve a boot-crashing model-sync. Existing trees that already pinned 0.1.3 in a lockfile need `npm update @aiwayds/dsh-model-sync` (or pnpm equivalent). ### Added - **System-clipboard bridge for the ask-user sentinel editor** (`src/clipboard.ts`, `src/ask-user.ts`, `src/tui.ts`). The `Type something.` free-text input now round-trips with the OS clipboard (documented here for the first time — this code has shipped unannounced in the 1.0.2–1.0.4 tarballs): - `Ctrl+Shift+C` (kitty-CSI-u `\x1b[;u`, modifier 6) writes the sentinel buffer through OSC 52 in parallel with the platform's `pbcopy` / `wl-copy` / `xclip` / `xsel` / `clip`, and shows a short `Copied` hint under the panel; an empty buffer is a silent no-op. - **Right-click while editing** short-circuits the SGR right-click press *before* it reaches pi-tui (which would otherwise start a selection drag) and pipes `wl-paste` / `xclip` / `xsel` / `pbpaste` / PowerShell into the same buffer; a missing binary is cached per-platform for the rest of the run so the right-click path stays snappy. - **Bracketed paste** (`?2004`) is sanitized through the same rules the right-click path uses: every `CR`/`LF`/`CRLF` run folds to one space, C0/C1 control bytes (including `ESC`) are dropped, tabs expand to four spaces, and the buffer is capped at 16 KiB. A pasted paragraph lands as one space-separated run, never a stack of newlines. - OSC 52 writes larger than 64 KiB are skipped (the local command alone is relied on) so a pathological paste can't spam the terminal with a multi-MB escape run. Every I/O seam is injectable through `ClipboardImpl`, so `test/clipboard.test.mjs` covers the full ladder without spawning a single real process. ## [1.0.4] - 2026-08-29 ### Fixed - **Registry recovery release.** npm 1.0.3 was published from an un-versioned working tree on 2026-08-28 and its git history was removed; it also bundled a TUI-side `/model-sync` registration whose final home is the `@aiwayds/dsh-model-sync` plugin (0.1.3+ registers the command itself through the shared dsh command registry, which every interactive UI discovers automatically). 1.0.4 re-publishes the v1.0.2 tree — zero code changes — so `^1.0.x` ranges resolve to a clean release. npm 1.0.3 is considered superseded; pin `1.0.2`/`1.0.4` if a range still picks it up. ## [1.0.2] - 2026-08-28 ### Added - **`@aiwayds/dsh-model-sync` ships as a default dependency and mounts itself by default** (`package.json` `^0.1.1`; `cordis.patch.yml` bundle entry `id: dsh-model-sync`). The standalone plugin owns the automatic sync of the built-in routes' model lists — keeping the configured `llm-pi-ai` provider routes in step with each gateway's `/models` endpoint through add-only merges written via the official settings seam — with no TUI command involved. It carries its own bundle patch (`@aiwayds/dsh-model-sync/cordis.patch.yml`); the dependency only puts the package into the profile's `node_modules` so that patch has something to mount, and `cordis.patch.yml` documents the rule: do NOT also list `@aiwayds/dsh-model-sync` in a profile's `bundles` — the duplicate mount would crash the loader — remove that entry from the profile instead. ### Changed - Package and suite bookkeeping follow the removal: version 1.0.2, and the suite stands at 1037 → 1020 tests across 56 → 55 files — the deleted `test/model-sync.test.mjs` carried the 18 `/model-sync` cases. - Docs follow the removal: both READMEs drop the `/model-sync` row from the command table, add a paragraph pointing at the companion plugin, and re-count their `pnpm test` reference to 1020 / 55 files; `AGENTS.md`'s quality gates re-count 1020 / 55 files and drop the model-sync line from the per-file tally; `e2e/README.md`'s autocomplete caveat cites `usage` instead of the retired `models-sync` command; the dev-upgrade examples move to 1.0.2. ### Removed - The built-in `/model-sync` command (`src/model-sync.ts`, `test/model-sync.test.mjs`). Model-list syncing is now owned by the standalone `@aiwayds/dsh-model-sync` plugin, which this package depends on and mounts by default via its own bundle patch — no TUI command needed. If a profile's `bundles` also list `@aiwayds/dsh-model-sync` explicitly, remove that entry: the duplicate mount would crash the loader. ## [1.0.1] - 2026-08-28 ### Changed - **README rewrite around feature highlights** (`README.md`, `README.zh-CN.md`). Both READMEs now lead with a feature TOC and impact-first structure (README.md 552→207 lines, README.zh-CN.md mirrored 222-line shape): - **Cache-hit definition corrected to session-cumulative rate** in both languages, matching the v0.27.0 implementation in `src/session.ts`: `ΣcacheRead ÷ (Σinput + ΣcacheRead + ΣcacheWrite)`, session-wide, with `output` never in the denominator and route (`request/header` provider/model) changes no longer resetting the counter. - **Demo videos restored as bare user-attachments MP4 URLs** (ask-user and Feishu panels) so GitHub renders them inline as `