# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] Reading a document and asking about a paragraph, without leaving the panel. ### Added - Selecting text in the preview offers **加入对话** / **Add to conversation**: the excerpt becomes a blockquote with its source path in this Session's composer, inserted at the caret through `ctx.sessions.scope(id).conversation.input`, and the input takes focus. The excerpt is captured when the pill appears, so the click cannot race a collapsing selection; the draft editor applies the edit in one undo step, which keeps the draft's `@` references and anything already typed. The caret path probes `insertText`/`captureInsertion` (the facade has them; the public `SessionInput` type does not) and falls back to `setDraft` only when the draft holds no reference chips, so a chip cannot be dropped silently. A draft that changed between capture and click is reported with the excerpt kept for one more click, and a Session that is not in the foreground is refused rather than written into another Session's composer. Both services are read through `ctx.get`, which carries no inject requirement and returns the value traced to the reading context — property access on a service the plugin did not declare throws `cannot get property … without inject`, and a service only reachable through a Session scope is exactly that case. - **Code splitting**: KaTeX (372 KB) and 27 highlight.js language definitions (227 KB) are now loaded as separate chunks via `require.async`, only when the document carries math or code blocks. The entry bundle drops from about 918 KB to about 313 KB (66% reduction). Chunks are registered with `__ModuleLoader__.load({ id, chunk, factory })` and materialized on demand. `engines.dsh` is now `>=0.2.1-rc.2`, which is the first release to support chunk loading. - **Draft persistence**: the editor saves drafts to `localStorage` with a 300ms debounce, keyed by file path. A file with a saved draft shows a restore prompt on open; the reader can restore the draft or discard it. The draft is cleared on successful save. - **IntersectionObserver for TOC**: the outline now tracks which heading is in view through IntersectionObserver instead of scroll events, with a scroll-based fallback for environments without observer support. The observer watches all heading elements and updates the TOC highlight as they enter and leave the viewport. - **EOL and encoding preservation**: `readText` now detects whether a file uses CRLF or LF line endings and whether it is valid UTF-8. The editor normalizes to LF internally and converts back to the original EOL on save, so a CRLF file stays CRLF. Non-UTF-8 files show an encoding notice banner. ### Changed - **Preview no longer unmounts on background refresh.** `load()` used to call `setStatus('loading')` on every reload, which unmounted the preview DOM and lost scroll position, TOC highlight, and editor focus. Background reloads now set a `refreshing` boolean instead, shown as a thin progress bar under the header. The watch handler debounces frames by 120 ms (non-resetting, so frequent writes do not keep postponing the reload), skips reloads when the version matches, guards against stale responses with an incrementing token, and sets a `diskChanged` flag instead of auto-applying when the editor is dirty — the editor shows a "磁盘已更新" / "Disk updated" banner with a reload button. - **TOC anchors are now extracted from the rendered DOM**, not from the source Markdown. The previous implementation used a line regex on the source for `extractTOC` and positional HTML replacement for `withHeadingIds`, which broke the 1:1 assumption for setext headings, `# comments` inside code fences, headings inside blockquotes, and raw HTML headings. A single DOMParser pass after sanitization now walks `h1`–`h6`, generates slugs, deduplicates, and builds the TOC from the same source of truth that the preview renders. - **Highlight and math caching**: code block highlighting and KaTeX rendering now use bounded LRU caches (256 entries for highlight, 128 for math). The highlight pass skips blocks already marked and blocks over 8KB, so large documents do not stall on syntax coloring. The math cache keys on display mode and TeX source, so repeated expressions render instantly. - **Scan deduplication**: the file picker caches scan results for 30 seconds, keyed by session id, workspace root, and skip-dirs rules. Rapid tab switches or settings changes no longer trigger redundant workspace walks. - **Follow-writes default**: the "follow Agent writes" checkbox now defaults to off instead of on. The rationale is that automatic file opens are surprising behaviour; a reader who wants them can opt in explicitly. The checkbox still takes effect immediately and is remembered per browser. - **Host localization**: the Host-side tool messages are now in English (`"Opened ${path} in the md-live panel"`, `"preview_md requires a file path"`, etc.) instead of Chinese. The client-side UI still reads the locale dictionaries. - **Sanitization fallback**: when DOMPurify cannot be constructed, the sanitizer now escapes HTML entities (`&`, `<`, `>`, `"`, `'`) instead of stripping tags with a regex. The fail-closed path renders the document as plain text rather than risking malformed HTML. - **Bundle minification**: the client bundles are now minified with legal comments preserved at EOF. The entry bundle dropped from 322 KB to 176 KB (45%), the KaTeX chunk from 373 KB to 265 KB (29%), and the highlight.js chunk from 227 KB to 115 KB (49%). Total bundle size is 556 KB instead of 922 KB (40% reduction). - **Accessibility**: the zoom overlay now handles Escape to close, carries `aria-modal="true"` and `tabIndex={-1}`, and its label is translatable. The editor textarea has an `aria-label`. Scroll-to-heading respects `prefers-reduced-motion`. - **Package metadata**: `package.json` now carries `repository`, `homepage`, and `bugs` fields pointing to the GitHub repository. The `files` list includes `CHANGELOG.md`. - `verify:client`'s locale stub now substitutes `{name}` placeholders the way the real service does, so parameterised copy is exercised rather than merely looked up. ### Removed - **TypertRemoteService inlining**: the `MdLiveRemote` class no longer extends `TypertRemoteService` from `@deepseek-ai/dsh-typert-protocol`. The base class is inlined (a constructor that calls `ctx.reflect.provide(namespace, this)`), and the Remote method descriptor is placed on the prototype manually. The `@deepseek-ai/dsh-typert-protocol` dependency is removed from `package.json`. ## [0.9.1] - 2026-10-27 An npm install that cannot collide with the Host's own copy of `@deepseek-ai/dsh-tools`. ### Fixed - Installing from npm no longer breaks every tool call in the running Host. The Host half imported `@deepseek-ai/dsh-tools` for `defineTool()`, so pnpm wrote a second copy of that package into the profile; the profile uses a hoisted `node_modules` layout, and `TOOL_RUNTIME_SCHEDULER` is a module-local `Symbol`, so a tool call prepared through the other copy could not find the tool runtime — every call in the Session ended with "Cannot read properties of undefined (reading 'prepare')" and the turn with `UNKNOWN`. `preview_md` is now a plain registry definition, its argument checks live in `execute`, and the package no longer depends on `@deepseek-ai/dsh-tools`. ### Changed - `marked`, `dompurify`, `highlight.js` and `katex` moved from `dependencies` to `devDependencies`: the Client bundle already inlines them, so an npm install no longer copies them into the profile's `node_modules` either. - The unused `@deepseek-ai/dsh-fs` peer is gone, and the packages DSH itself supplies (`@deepseek-ai/cordis`, `react`, `react-dom`) are optional peers, so `npm install` no longer tries to resolve them from a registry. ## [0.9.0] - 2026-10-27 An editor, and every write fenced by the right policy, in the reader's language. ### Added - The panel can edit. **编辑** turns the body into a textarea over the very file it is showing, **保存** (or `Ctrl/Cmd+S`) writes it back, and the save carries the version the editor started from. A file that changed underneath raises **覆盖 / 重新加载** instead of letting one side win in silence, and a live update never overwrites a dirty draft. - The panel follows DSH's locale. Its copy is one `md-live` namespace with `zh` and `en` dictionaries, registered through `ctx.locale`; the body and the tab chip declare `locale: NS`, so the slot renderer re-renders them with a freshly bound `t` when the reader switches language in Settings — no reload. The guide entry, the scan errors and the Remote helpers read the same translator. - Refusals travel as values: a denied write now carries a `code` and the values behind it (`mode`, `root`, `path`, byte limits), and the panel phrases the refusal in the current language. `reason` stays in the union as the Host's own sentence, used for logs and as the fallback for a code this build does not know. ### Fixed - Saving a file in the Session workspace no longer fails with "文件不在工作区内, 无法写入". `mdLive/writeText` takes the Session id as its first wire parameter (a `session` lookup, resolved by the Host to the live Session), asks `ctx.sandboxPolicy` for that Session's mode and workspace root, and passes the result to `fs.writeText` as the per-call fence. Without it the backend fell back to the deployment default — `workspace-write` rooted at the Host process's cwd — so every file outside the directory `dsh web` was launched from was refused, the Session's own workspace included. Starting the server from the workspace directory only ever hid that by accident. - A refusal no longer claims the file is outside the workspace. The message now names the sandbox mode and the root it allows, and a read-only mode reads as a read-only refusal instead of a containment failure — the two used to be indistinguishable because both arrive as "access denied". - Navigating away from a dirty editor no longer drops the draft in silence. The plugin now asks the mounted tab before it points the tab elsewhere, so the back arrow, another file from the picker and an Agent-follow switch all confirm first; declining leaves the editor and the tab exactly where they were. Unmounting clears the question, so a closed tab cannot veto navigation. ### Changed - `writeText` takes four positional arguments: `sessionId`, `path`, `content`, `version`. The panel passes the tab's own Session id and reports "没有会话 上下文,无法保存" rather than writing with no policy at all. - The client no longer rewrites path separators before the call: the Host normalizes the path, and the tab already holds the absolute spelling the workspace listing used. - The file picker names the workspace before the absolute path (`工作区` / `Workspace`), so the path in a file list is not left to be guessed. The path ellipsizes; the label never does. - The picker's group headings (`最近打开` / `Recently opened` and `全部文件` / `All files`) no longer look like a line of the list. They are sticky bars with the app's ground colour, a hairline underneath, a smaller heavier face and uppercase in Latin locales, so a heading cannot be mistaken for a directory in a path — and it stays on screen while the list scrolls. - `verify:host` gained the policy-forwarding check and the denial-message check; the client suite now asserts the Session id rides along with a save, and grew to 87 checks with the navigation guard (`back arrow`, declined prompt, confirmed prompt, unmount) and the locale seats (both dictionaries, the `locale` on both registrations, an English render, a live switch). ### Removed - The `spike/` manifests (`check-manifest.mjs`, `md-live-manifest.mjs`): two hand-written sketches of the Remote descriptor from before the codegen shims, imported by nothing. ### Documentation - Both READMEs describe the editor and document the write path: the four wire parameters, the `session` lookup, how the policy is resolved and applied, and what the fallbacks deliberately refuse to do. The Roadmap's write-API and editor items are ticked, the stale "KaTeX is still missing" and "renders as plain text" notes are gone, and the manual check (`test.md` + `test-image.png`) is documented next to the automated suites. ## [0.8.0] - 2026-10-26 Math, at a price the next reader should know about. ### Added - `$inline$` and `$$display$$` render through KaTeX. They are marked extensions rather than a text pass, so an expression inside a code span or a fence is left alone — those are tokenised first. The extensions emit a placeholder carrying the TeX, and the expression is drawn on the live DOM after sanitising: the sanitiser's job stays exactly what it was, and KaTeX's markup never passes through the HTML string at all. - `verify:client` grew from 65 to 69 checks: inline math, display math, math inside a fence staying literal, and an unparseable expression neither throwing nor disappearing. ### Changed - Output is MathML rather than KaTeX's HTML. The HTML rendering depends on KaTeX's own web fonts — several hundred kilobytes the plugin has no way to serve — and without them the metrics are wrong and glyphs collide. The browser draws MathML itself, so it needs neither the fonts nor the stylesheet. - KaTeX's minified build is imported directly. The client bundle is deliberately not minified, so importing the package entry expanded to 620 KB; the minified file costs about 380 KB instead. ### Notes - The bundle is now about 877 KB, most of the growth being KaTeX. That is what math costs without code splitting, and this module format — one file per id, loaded by `__ModuleLoader__` — has no way to split. ## [0.7.0] - 2026-10-26 The Agent can now ask for a document instead of only producing one. ### Added - `preview_md`, an Agent tool registered by the Host half: "show me the notes" now puts that file in the panel, with nothing written first. The Client reacts to the `tool/call` frame, so the tool's own body only carries the request. It is honoured even when automatic following is switched off — that checkbox governs what happens on its own, not what was asked for. - `npm run verify:host`, nine checks that run the built Host half against a fake cordis context: what it injects, the tool's schema, the arguments it refuses, and that a registry which throws costs the tool rather than the plugin. ### Changed - The Host half is no longer an empty `apply()`. It registers the tool through `ctx.inject(['tools'], …)`, so a composition without a tool registry still loads the plugin, and the `@deepseek-ai/dsh-tools` import is dynamic and guarded: a missing or mismatched package warns and skips. - `npm run verify` now runs both halves. ### Notes - The tool resolves no paths. The panel already answers that question, and a second answer here could disagree with the one the panel then uses. ## [0.6.0] - 2026-10-26 The tab stops waiting to be opened and starts following the Agent. ### Added - The panel follows the Agent's writes — and opens itself to do it. When a `write`, `edit` or `str_replace_editor` call finishes on a `.md` inside the workspace, the right Sidebar is revealed and the tab lands on that file. The subscription lives in the plugin rather than in the tab body, fed by `remote.session.follow` for whichever Session `sidebarRight.mounted` names, so a write is followed whether or not a panel was open. The switch is armed by `tool/call` and fired by the matching `tool/result`, so the file is read after the write landed — opening on the call would show the previous contents and make a working feature look broken. `str_replace_editor`'s `view` command is a read and is ignored, as is any call naming a non-Markdown path. - A "follow the Agent's writes" checkbox in the panel settings, on by default and remembered per browser. It is read at the moment of each write, so flipping it takes effect without a reload. ### Changed - Both Remote streams — the file watch and the Session follow — now share one wrapper that unwraps `item.value` and acknowledges every frame. The two call sites had been carrying the same twenty lines twice. - The header's settings button is "panel settings" rather than "reading settings", because following the Agent is not a reading preference. - Placing a tab in a Session whose store was never adopted does nothing at all, and a closed pane is exactly that case. So the pane is revealed first and the tab is placed again a frame later, twice; repeating is safe because the type is single-instance. - `verify:client` grew from 56 to 64 checks. The jsdom `$stream` double now owns a lifetime and cancels it on `dispose()`; without that, an unmounted component's generator kept draining the fake queue and eating the frames meant for the live one — which is exactly how the first run of these tests failed. ## [0.5.0] - 2026-10-26 This release was planned as "swap in the official `MarkdownText` renderer". Reading its implementation killed that plan: it renders from an mdast tree that emits raw HTML verbatim with no sanitiser, gives headings no `id`, and does no syntax highlighting — the three things this tab cannot give up on arbitrary files from a workspace. The parts worth taking from it were rebuilt on the existing renderer instead. ### Added - A code-block toolbar: the language the fence declared, and a copy control, revealed on hover. The copy strips the trailing newline `marked` leaves before ``, so a pasted shell command does not run itself. - Front matter is separated from the prose and shown in a collapsed box, so a document opens with its content instead of a stray horizontal rule and a `title: …` paragraph. ### Changed - `verify:client` grew from 53 to 56 checks. ## [0.4.0] - 2026-10-26 The document became something you read rather than something you look at: its own images resolve, its links navigate, and the panel can hand the file back to the desktop. ### Added - Relative images resolve against the document's own directory and are served through the Host's `api/file` endpoint — the same address the official document preview uses. Remote and absolute URLs are left untouched. - Links inside the document are handled by the tab: another `.md` opens in this tab, `#anchor` scrolls in place, `https://` and `mailto:` go to the browser, and a relative link to anything else is handed to the Host's default application. - Header actions that hand the file to the OS: open with the default application, or reveal it in the file manager (`remote.session.openWorkspacePath`). Both hide themselves when `canOpenWorkspacePath` reports that the Host has no desktop. - Search over the workspace list by path fragment, and the last eight documents opened as a shortcut section above the full list. - The outline marks the heading currently in view while scrolling, and folds to a 28px rail; the fold state is remembered per browser. - Reading settings — body size, leading and measure — in a header popover, remembered per browser. Every default is "follow the app", so the Markdown tokens stay in charge until the reader chooses otherwise. ### Changed - The picker counts matches while a search is active (`3 / 12 个文件`) instead of showing only the total. - `verify:client` grew from 35 to 53 checks, covering the new paths. ## [0.3.0] - 2026-10-24 Reworked into a native right-Sidebar tab. The 0.1.0 shape — a `main` panel opened by a sidebar icon, driven by Host-side events — never worked: DSH has no Host → Client event bridge, and the panel had no entry point. ### Added - Right-Sidebar tab type (`sidebarRightTabs` + `sidebar.right.pane.tab`) with a guide entry, so the preview is reachable from the pane's add control. - Workspace file list: one full scan from the Session's cwd, parallel by level, rendered progressively as each level lands. - Editable filter rules for build/dependency directories, persisted in `localStorage`, with "restore defaults" and "show everything". - Live refresh through `remote.workspaceFiles.changes`. - Working TOC anchors: heading ids are injected at render time and de-duplicated. - Header shows `directory/filename` relative to the workspace, with the absolute path in the tooltip. - `npm run verify:client`: 35 end-to-end checks in jsdom against the built bundle. ### Changed - Styling reads the app's `--dsw-*` design tokens, so the panel follows the appearance setting, the text/code font sizes, the font families, and any registered third-party theme. - Sanitising moved from a hand-written regex to DOMPurify (HTML profile). - Code highlighting moved to the rendered DOM with 27 registered languages; the bundle dropped from 1.95 MB to 0.46 MB. ### Fixed - Client bundle lost its `window.__ModuleLoader__.load({ id, factory })` wrapper and called `require` at script scope: `ReferenceError: require is not defined` left the entry permanently inactive. - Remote calls were read as bare values, but they resolve to `{ ok, value } | { ok, error }` envelopes — every listing came back empty without an error. - `list`/`readBytes` were called with an empty path, which the Host rejects with `path is required`; the scan now starts from the Session's absolute cwd. - The file watch was iterated raw instead of through `remote.$stream`, so frames were never acknowledged and live updates could stall. - marked v9 no longer honours the `highlight` option, so code blocks had never actually been highlighted. - `ctx.layout.selectPanel` was not a real API, so auto-open could never switch panels. ### Removed - The Host-side chokidar watcher and the `md-live:*` events it emitted: with no Host → Client bridge they reached nothing and only cost I/O. `chokidar` is no longer a dependency, and the Host half is now an empty `apply()`. - The `preview_md` / `export_md` Agent tools documented in 0.1.0. They were never implemented. ## [0.1.0] - 2026-10-23 ### Added - Initial plugin scaffold: a `main`-slot Markdown preview panel with edit / preview modes, TOC, theme overrides and a Host-side file watcher. [0.9.1]: https://github.com/lg114/dsh-md-live/releases/tag/v0.9.1 [0.9.0]: https://github.com/lg114/dsh-md-live/releases/tag/v0.9.0 [0.8.0]: https://github.com/lg114/dsh-md-live/releases/tag/v0.8.0 [0.7.0]: https://github.com/lg114/dsh-md-live/releases/tag/v0.7.0 [0.6.0]: https://github.com/lg114/dsh-md-live/releases/tag/v0.6.0 [0.5.0]: https://github.com/lg114/dsh-md-live/releases/tag/v0.5.0 [0.4.0]: https://github.com/lg114/dsh-md-live/releases/tag/v0.4.0 [0.3.0]: https://github.com/lg114/dsh-md-live/releases/tag/v0.3.0 [0.1.0]: https://github.com/lg114/dsh-md-live/releases/tag/v0.1.0