# Changelog — dock-flash Release-by-release history: what changed and, where it matters, why. AGENTS.md holds the rules, contracts and current invariants; this file holds the narrative behind them. git log remains the authoritative record of individual commits — the entries below summarise releases. Recent entries are the most detailed on purpose: several of them record defects whose reasoning is still load-bearing. ## Versions | Version | Change | |---|---| | 0.14.0 | Standalone mode: removed `inject: ['workbench']` hard dependency; `mountStandalonePanel()` for no-dock-base scenarios; dock-base marked optional peer dep | | 0.15.0 | Fixed `dsh.client.inject` (must use `"dock-base"` not `"dock-base/client"` — inject path doesn't strip `/client` suffix); fixed standalone panel rendering (`require('react-dom/client')` for `createRoot`); standalone button draggable + four-corner position preset switch | | 0.15.1 | Fixed sidebar sash drag broken by `style.zoom` on ``: even `zoom:1` distorts browser coordinate system; `_applyZoom(100)` now calls `removeProperty('zoom')` instead of setting `zoom:1` | | 0.15.2 | Removed page zoom feature entirely — CSS `zoom` on `` breaks browser coordinate system at any value; added legacy cleanup (`removeProperty('zoom')` + `localStorage.removeItem`) on startup | | 0.15.3 | Fixed duplicate Claude Style skin entries in dropdown: Phase 1b used raw `data-skin-chrome` value as plugin ID (`claude-style-skin-style`) instead of canonical package name (`claude-style-skin`), so Phase 4 added a second entry from the boot manifest; now cross-references chrome value with boot manifest and strips decorative suffixes (`-style`, `-chrome`, `-css`) to resolve the real package ID; added `claude-style-skin` → `data-dsh-claude-style` body-attr mapping | | 0.16.0 | Optional third-party integration: added `dock-flash:ready` event emission after `ctx.provide('quickControl', registry)` so plugins can discover dock-flash without hard dependency; dual-discovery pattern (event + `ctx.get()`); standalone mode already renders third-party switches in Extensions tab (no code change needed); `dock-flash-qc-types` package for zero-runtime TypeScript definitions; `INTEGRATION.md` / `INTEGRATION.zh-CN.md` integration guides; `dock-flash-qc-demo` updated with both hard-dep and optional patterns | | 0.17.0 | Standalone mode visual consistency with dock-base: trigger button changed from 44px circle to 36px rounded-rect matching `.dsh-wb-activity button` style (transparent bg, hover tint, active blue tint); panel container aligned with `.dsh-wb-floating` style (borderRadius 10px, bg-layer-2, deeper shadow, overflow hidden, flex column); added floating title bar mimicking `.dsh-wb-floating-head` (⠿ grip + ⚡ icon + "Quick" title + × close button); title bar supports drag-to-move; close button closes panel and deactivates trigger | | 0.18.0 | Standalone mode trigger moved from floating `position:fixed` button to `sidebar.footer.action` slot injection (same slot as CordisPanel badge); badge shows ⚡ icon + label when sidebar wide, icon-only when rail; removed four-corner position presets (`POS_PRESETS`, `PRESET_STYLES`, etc.), `dock-flash:standalone-position` buttongroup switch, trigger drag support, and related i18n keys; floating panel positioned above the badge via `getBoundingClientRect()`; legacy `localStorage.removeItem('dock-flash:standalone-position')` cleanup on startup; added trigger-position switch with 4 options: `sidebar.footer.action` (badge style, ⚡+label wide / ⚡ rail), `conversation.input.left` (icon button), `conversation.input.right` (icon button), `conversation.session.header.actions` (small icon button); position persisted in `dock-flash:trigger-position`; switching position disposes old slot injection and injects into new slot | | 0.19.0 | Proxy improvements #4 and #5: (#4) Host-side `GET /plugins/dock-flash/proxy-status` route returns current `NO_PROXY` env var + `useProxy` setting; client fetches on startup and after changes, shows actual `NO_PROXY` value as a subtitle beneath the proxy toggle via new `subtitle` field on toggle switches; (#5) Host-side `POST /plugins/dock-flash/test-connection` route fetches `https://www.google.com/generate_204` and returns success/failure + latency; client registers a `dock-flash:test-connection` action switch that calls this endpoint and records results (✅ success + latency / ❌ failure + error) in the changelog; host half now lazily injects `webServer` service for HTTP routes | | 0.20.0 | Proxy improvement #6 — fine-grained proxy rules: replaced on/off toggle with `select` switch offering 4 modes: `all-proxy` (NO_PROXY cleared, all traffic proxied), `api-bypass` (NO_PROXY=`api.deepseek.com,chat.deepseek.com`), `all-bypass` (NO_PROXY=`*`), `custom` (user-defined NO_PROXY via `prompt()`); host schema adds `proxyMode` (string) + `customNoProxy` (string) alongside legacy `useProxy`; `applyProxyEnv()` now calls `@deepseek-ai/dsh-http-proxy`'s `installProxyFromEnvironment()` to re-install the undici global dispatcher (writing `process.env.NO_PROXY` alone is insufficient — the dispatcher freezes its policy at startup); `/proxy-status` route returns `proxyMode` + `customNoProxy` + `noProxy`; `/test-connection` uses `https://github.com` and treats any HTTP response as success; client migrates legacy `dock-flash:use-proxy` localStorage key → `dock-flash:proxy-mode` on first read; startup sync and `settings/updated` listener handle both `proxyMode` and legacy `useProxy`; `renderSelectSwitch` supports `subtitle` field; selecting "custom" prompts for NO_PROXY value (cancel reverts select to previous mode); proxy scope documented (only affects `fetch()` within DSH Node.js process — `node:http`, self-built transports, OS apps are unaffected); action button (`renderActionSwitch`) bugfix: `actionLabel` is typically a function but was passed as-is to `h('button', ...)` — React silently ignores function children; added `typeof === 'function'` check; button styling enlarged (`S.btnAction` padding/fontSize/minHeight increased); host→local sync race condition fix: `_suppressHostSync` flag prevents `_fetchProxyStatus()` from overwriting a just-set mode before the host confirms it | | 1.0.0 | First stable release. All prior development history squashed into a single commit. `prepare` script removed: with `dist/` tracked, a git install needs no build at all, so declaring `prepare` only forced pnpm ≥10 users to grant an `allowBuilds` permission before their first `dsh plugin add` could succeed. Version bumped from 0.20.0 in both `package.json` and `lib/client.js`. | | 1.0.1 | Settings plugin entry renamed from "Quick" to **"Flash"**. Every other dock-family plugin titles its settings card with the package name minus the `dock-` prefix (Dock, Git, Files, Editor, Images, Markdown), so "Quick" was the sole outlier — and being merely a fragment of the panel's own "Quick Control" name, it gave users nothing to connect to the `dock-flash` package they installed. The floating title bar's hardcoded "Quick" now goes through `t('title')`, closing the only user-visible string that bypassed i18n. The activity-bar and panel name stays "Quick Control", which already matches how siblings name those (dock-git → "Git History", dock-base → "Dock settings"). | | 1.0.2 | Removed the `dock-flash:dock-position` and `dock-flash:auto-hide` switches from workbench mode — both were pure duplicates of dock-base's own settings. dock-base already registers `DOCK_POSITION_SETTING` (a radiogroup over the same `left/right/top/bottom` values) and `DOCK_AUTO_HIDE_SETTING` (`off`/`edge`), and both wrote through the very same call dock-flash used, `wb.updateLayout({ dock })` / `{ autoHide }`, so one store had two entry points. There is nothing to replace them with: dock-base's layout store exposes only `dock` and `autoHide` to users, and its settings registry already covers `reserveSpace`, `hoverScale` and `nearScale`, so any new layout control would duplicate it too. Workbench layout group is now just `close-on-blur`, which dock-flash actually owns. Also dropped the 8 i18n keys the two switches used (`dockPosition`, `dockLeft/Right/Top/Bottom`, `autoHide`, `autoHideOff`, `autoHideEdge`; the latter two were already dead). Docs: removed the stale `Zoom` row (the feature was deleted in 0.15.2 but the READMEs still advertised it) and corrected `Close on Blur`'s Standalone column from ❌ to ✅ — it is registered in both modes and the standalone panel's `handleOutsideClick` reads the shared localStorage key. | | 1.0.3 | Moved `close-on-blur` out of the switch registry and into the panel header: it is now a small icon toggle immediately left of the close (×) button, in **both** the workbench `headerComponent` and the standalone floating title bar. The `off`/`on` buttongroup that used to live in the Layout subgroup is gone, so **workbench mode no longer renders a Layout category at all** — no built-in switch carries `group: 'layout'` there any more, and `groupOrder.filter((g) => builtInGroups.has(g))` drops the empty group with no extra code. `trigger-position` (standalone only) is the sole remaining layout switch. The workbench header button paints its own state imperatively rather than with `useState`, because dock-base may call `headerComponent` as a plain render function, which would make hooks illegal. i18n: `closeOnBlurFloating` renamed to `closeOnBlurOn`, and both state labels reworded (`关闭/开启` → `已关闭/已开启`) since they now read as a tooltip ("失焦关闭: 已开启") instead of as option labels. Docs: the READMEs' mode table, switch table, grouping rules and mode notes were corrected — they still claimed standalone had no Layout switches and workbench had all three groups. | | 1.0.4 | Dropped `sidebar.footer` from `TRIGGER_POSITIONS` — it injected the standalone trigger into `sidebar.footer.action`, a shared slot that CordisPanel and other plugins also occupy, so the badges visually collided. The default in `loadTriggerPosition()` moved from `'sidebar.footer'` to `'input.right'`; since that function validates the stored value against `TRIGGER_POSITIONS`, users who had `sidebar.footer` stored migrate to the new default automatically, with no explicit migration step. Dead code removed with it: the `badge` branch of the panel-positioning code (byte-identical to the `input` branch, so it had always been redundant), the `QuickTriggerBadge` component, the `style === 'badge'` ternary in `injectTrigger()`, and the `triggerSidebarFooter` i18n key. Trade-off accepted: every remaining position lives inside the conversation UI, so with no session open the standalone trigger is not rendered at all — `sidebar.footer.action` was the only always-present slot. | | 1.0.5 | Put the `close-on-blur` switch back into the **standalone** Layout subgroup while keeping the panel-header toggle, and made the two genuinely linked. All three readers/writers now go through one module-level channel next to `LIGHTNING_ICON`: `readCloseOnBlur()`, the single writer `writeCloseOnBlur(on, registry)`, and `subscribeCloseOnBlur(fn)`. `writeCloseOnBlur` repaints every subscriber (that is how the switch reaches the imperatively-painted header toggles) and calls `registry.notifyChange('dock-flash:close-on-blur')` (that is how a header toggle reaches the switch — the registry `version` bump is what re-renders the panel). This replaced four separate copies of the key/reader/writer that had accumulated across the panel component, the standalone title bar and `apply`, so drift between them is no longer possible. Workbench mode still registers no such switch and therefore still renders no Layout category. Docs: the README switch table regained the Close on Blur row (Standalone ✅), gained a missing Trigger Position row, and the mode/grouping notes were corrected. | | 1.0.6 | Fixed the workbench panel being painted over by dock-git. `S.root` had neither `position` nor `z-index`, so the panel was a plain static block and **any** positioned sibling in dock-base's `.dsh-wb-root` won over it — dock-git's `.dg-graph` is only `position: absolute; z-index: 2` and still covered the whole panel. `S.root` now sets `position: relative; z-index: 10`: above in-content escapees like `.dg-graph` (2), below `.dsh-wb-floating` (70) so dock-base's own floating-above-docked precedence is preserved. The value is bounded on purpose — see the "Stacking: why this panel needs its own context" section for why raising it further cannot help against elements that live outside `.dsh-wb-root` and would invert dock-base's precedence above 70. | | 1.0.7 | System-proxy refactor — **the test target became a setting, and the probe became diagnostic**. `TEST_URL` was a hardcoded internal host, which had published a private IP and its path naming in this public repository, in both `src/index.ts` and the tracked `dist/`; it is now `testUrl` in the settings namespace (default `https://www.google.com/generate_204`) and exposed as the `dock-flash:test-url` select (Google 204 / GitHub / DeepSeek API / a `custom` prompt, written through to the host and mirrored in localStorage like `proxyMode`). That address was also purged from history. `POST /test-connection` returns a structured report instead of `{ ok, latencyMs }`: the proxy route decision (`proxied`, `noProxy`, `httpProxy`, `mode`, `routeError`), the redirect chain walked hop-by-hop via `redirect: 'manual'` and bounded by `MAX_REDIRECTS` (following it silently conflated "302 to somewhere unreachable" with "connection refused"), split header/body timings, body size plus a 200-byte snippet of textual bodies (where a proxy's own block page shows up), and the unpacked undici `cause` as `causeCode`/`causeErrno`/`causeMessage` — `fetch()` alone only ever says `TypeError: fetch failed`. The probe accepts an optional `{ url }` body override, so it always targets what the UI is displaying and cannot lag an async `settings.update`. `hasActiveProxy()` now probes the configured target instead of a second hardcoded `https://github.com`, which had let "is a proxy active" and "what did the test do" disagree. New `log` switch type (`getLines` / `getMeta` / `emptyText` / `onClear` + `clearTitle`) renders a read-only multi-line block — the changelog could not serve this, being single-line with a 30s TTL — used by the new `dock-flash:proxy-log` switch, where every field passes through `_oneLine()` so a multi-line error cannot break the one-fact-per-line layout. | | 1.0.8 | System-proxy UI cleanup. The `dock-flash:test-url` switch lost its `tooltip`: it was an `ⓘ` hover carrying the exact same string as its subtitle, so it added a hover target and no information. The subtitle moved out of the label column onto its own full-width line via the new `subtitleBlock` option on `select` switches — the inline variant sets `nowrap` + `text-overflow: ellipsis` because it shares the row with the control, which truncated the URL at precisely the part worth reading (host and path). The effective URL now occupies a whole line directly above the **Test Connection** button, and the row above it is a bare label + select with nothing explanatory in between. Because the URL line is the `test-url` switch's own subtitle rather than a separate element, no new switch type was needed and the value stays next to the control that changes it. `renderSelectSwitch` was restructured around that (also dropping a dead `currentOpt`/`currentLabel` pair that was computed and never read). Client-half only — takes effect on page refresh, no DSH restart. | | 1.0.9 | **The proxy feature never worked, and now it does.** Four independent defects had combined into a silent total failure, all of them invisible because each was swallowed by a `try/catch` or by a "returns direct" code path: (1) `require` does not exist in the ESM host half, so every `require('@deepseek-ai/dsh-http-proxy')` threw `ReferenceError` — `installProxyFromEnvironment` was therefore **never called**, meaning changing the proxy mode never affected actual traffic, only the `NO_PROXY` string; (2) the same throw in `require('@deepseek-ai/schemastery')` happened *inside* the `ctx.inject(['settings'], …)` callback, so `installSection` was never reached and **the `dock-flash` settings namespace was never registered at all** — `proxyMode`, `customNoProxy` and `testUrl` all silently reverted to their composition defaults; (3) `proxyRouteFor(url)` was passed a **string** where it wants a `URL`, and it does not throw on a string, it just answers "direct" — which is why every diagnostics run printed 直连; (4) `installProxyFromEnvironment`'s disposer was dropped, leaking one `ProxyAgent` per mode change. Fixes: `Schema` is now a static default import (schemastery has no named export); the proxy package is loaded once through `loadProxyModule()`, which resolves **DSH's own copy** via `createRequire(process.argv[1]).resolve(...)` so the module-level policy state is shared with the install DSH performed at boot (a second copy would answer "direct" forever — verified that `createRequire` + `pathToFileURL` yields the identical instance); the policy is now based on the `launchEnvironment` ctx service rather than `process.env`, since that snapshot merges the `process`/`project-env`/`user-env` layers DSH actually resolved from; only `NO_PROXY`/`no_proxy` are overridden; `proxyRouteFor(new URL(u))`; and the previous install is released before a new one. `/proxy-status` gained `httpProxy` because `proxyAvailable` alone cannot distinguish "no proxy configured" from "configured, and this URL is deliberately bypassed" — the client's ⓘ tooltip was keyed off the wrong one and called a deliberate bypass a misconfiguration. The invalid-URL branch no longer probes the route, so the log stops printing "Invalid URL" twice. Verified with a host-harness run (17/17) that drives the real routes with `process.argv[1]` pointed at the DSH entry. | | 1.0.10 | Diagnostics-log presentation, from use rather than from theory. `renderLogSwitch` gained `hideWhenEmpty` and `dock-flash:proxy-log` sets it: **before the first test the block is absent entirely** — previously it rendered a titled, empty box that took panel height and pushed the controls below it down. The log also now holds the **latest run only**, replacing its contents on every test, instead of the capped 200-line append-only ring buffer (one report is self-contained at ~15 lines, since the host caps the redirect chain, so the cap and the `_PROXY_LOG_MAX` shift loop are gone). The now-dead `proxyLogEmpty` i18n key was removed from both locales. Rationale: the panel is short, and both an empty box and a wall of history cost height without answering the question just asked. Client-half only — takes effect on page refresh. | | 1.0.11 | System-proxy UI trimmed to what is actually useful, and gated on whether a proxy exists at all. The `dock-flash:test-url` row lost its `🎯` icon and its `subtitleBlock` line — the effective URL no longer prints on its own line directly above **Test Connection** (the line 1.0.8 had added), so the row is now a bare label + select with nothing between it and the button. Both `test-url` and `test-connection` are **hidden while no proxy variable is configured**, gated on `_httpProxyValue` (the host's `httpProxy`, i.e. whether `HTTP(S)_PROXY` exists) rather than on `proxyAvailable`: "nothing configured" and "configured but deliberately bypassed" are different states, and the System switch's tooltip already draws the line in exactly that place — with no proxy to route through, the probe has nothing to exercise. That needed a new generic `visible: () => boolean` field on any switch type: the panel filters **before grouping**, so a group whose every switch is hidden renders no title above nothing, and `renderSwitch` re-checks it as a backstop. A switch driven by `visible` must `notifyChange` when its condition changes or it flips only on the next unrelated render — `_fetchProxyStatus()` already notifies `dock-flash:system-proxy`. **The custom NO_PROXY value is now validated instead of being stored verbatim.** The grammar enforced is the one `dsh-http-proxy`'s `bypassesProxy()` actually matches with: entries separated by commas or whitespace, `*` for everything, an optional leading `.`/`*.` meaning "this host and every subdomain under it", and an optional `:port` that must equal the URL's port exactly. Rejected, with a message naming the offending entry and stating the grammar: a blank or whitespace-only value (the two things it could have meant already have their own options, and an empty list is how `all-proxy` is spelled), anything containing `/ ? # @ \` — which catches both a pasted proxy URL (`http://…`, a dead entry here since this switch owns the bypass list and not the proxy address) and CIDR (`10.0.0.0/8`, which the matcher deliberately does not implement, so it would have bypassed nothing; the message asks for a suffix instead), malformed host names, IPv4 octets above 255, and ports outside 1-65535. Accepted values are normalized — trimmed, empty segments dropped, joined with commas — so `"a.com, b.com c.com"` stores as `a.com,b.com,c.com`; a rejection leaves both the mode and the stored list untouched. Host side, `resolveNoProxy('custom', …)` now returns `undefined` for a blank value, i.e. it **removes** `NO_PROXY` the way `all-proxy` does, instead of publishing `NO_PROXY=''`: routing is identical either way (the parser drops empty entries), but the environment is no longer left carrying a set-but-empty variable, and a blank value arriving from `settings.yaml` — which bypasses the prompt entirely — is normalized the same way. `src/index.ts` changed for that rule, so `dist/` is rebuilt in the same change and the host half needs a DSH restart; the validation itself is client-half only and takes effect on page refresh. | | 1.0.12 | **The peer ranges silently excluded the harness's prerelease builds.** `@deepseek-ai/cordis` was `>=4.0.1-0 <5.0.0-0` and `dock-base` was `>=0.1.2-0 <1.0.0-0`. Both look broad; both fail the one node-semver rule that decides this: a prerelease version satisfies a range only when some comparator in it sits on the **same `major.minor.patch` tuple** *and* itself carries a prerelease tag. Measured with semver 7.8.5: `^4.0.1` and `>=4.0.1-0 <5.0.0-0` both reject `4.0.0-rc.10` — the cordis this machine's dock-base actually runs on — and `>=0.1.2-0 <1.0.0-0` rejects `0.2.0-rc.1`, so every user on a prerelease harness build hit `ERESOLVE` and had to work around it by hand. Both ranges now keep their previous branch and add one for the tuple whose prereleases actually ship: `>=4.0.0-rc.1 <5.0.0-0 \|\| >=4.0.1-0 <5.0.0-0` for cordis and `>=0.1.2-0 <1.0.0-0 \|\| >=0.2.0-0 <1.0.0-0` for dock-base. The union is strictly wider than what it replaces, verified over a probe matrix that checks both directions — the prerelease is accepted *and* no version the old range accepted becomes rejected. That second half earned its place immediately: the first attempt at this fix used `^4.0.1 \|\| >=4.0.0-rc.1 <5.0.0-0`, and the matrix caught it silently dropping `4.0.1-0` (any `4.0.1-rc.*`), because `^4.0.1`'s lower comparator sits on the `4.0.1` tuple *without* a prerelease tag. No code changed, so `dist/` is untouched. Metadata only: the version bump exists so the fix reaches npm and the GitHub Release, not because behaviour changed. | | 1.0.13 | **A placement switch for DSH's built-in turn navigator — and a stand-down rule for when that rail is no longer DSH's.** New Appearance switch `dock-flash:turn-rail-left` moves the rail from the right gutter to the left. DSH exposes no setting, slot or service for it: the rail is hardcoded JSX (`TurnNavigator`) inside `@deepseek-ai/dsh-client-ui-chat`, and the slot tree's `conversation.view` carries only `conversation.chat.node`, `conversation.message.images` and `conversation.trajectory.images`, so the only lever is an injected stylesheet. That sheet is keyed on the class **suffix** plus element type and never on the `eGxaPq_` CSS-module hash, which is a build hash that changes whenever DSH rebuilds that stylesheet; `nav[class$="_frame"]` is unique among DSH's `