Catalog entry names (15 tab + 4 viewer) and the dsh-desktop shell preset
desc were raw hardcoded zh strings rendered straight into the add-plugin
modals and the title-bar scheme dropdown. Mirror the existing desc
pattern exactly: PluginEntry.name / ShellPreset.desc widen to the
i18n-friendly string-or-() =>-string union, the catalogs carry lazy
t('pluginXxxName') / t('presetDshDesktopDesc') lookups, and the
add-plugin modal (render, search filter, aria-labels) plus the scheme
SelectMenu resolve them like every other copy. The plain-string arm of
the union stays as the raw-name fallback, and t() itself never yields
empty text (missing keys render the key), so a dict gap can never blank
a name.
20 new dictionary keys added to zh/en (locales.ts), ja (locales-ja.ts),
and all 18 third-language dictionaries — tests/locales.spec.ts enforces
key-set equality against zh for every shipped dict (precedent: 9cc642b),
so the 'leave other locales-* untouched' shortcut would fail the hard
parity gate; real translations are provided instead.
Spec follow-ups (behavior change: names/descs now render through t()):
- plugin-list.spec.ts: name non-empty assertion resolves the union.
- add-plugin-modal.spec.tsx: sentinel/office name assertions resolve
from the catalog (en locale in tests) instead of pinning zh literals.
- shell-presets.spec.ts: desc non-empty assertion resolves the union.
Graduate from the alpha track to the stable channel:
- Baseline: CI mount pin, dsh.plugin.json engines floor, and every
@deepseek-ai/* peer floor / devDependencies pin move to 0.1.2-rc.1
(published on the `next` dist-tag). alpha.5 -> rc.1 is a pure version
bump in the harness (all package.json "version" lines, zero source
delta), so no code adaptation; every host contract was re-verified
unchanged and the real-host mount smoke passes.
- dsh-client-locale resumed publishing; its trailing-pin exception is
gone — peers and devDeps align at 0.1.2-rc.1 (pnpm peers check clean).
- Version: fold the unpublished v0.18.1-alpha.0 / v0.19.0-alpha.0 bumps
into the v0.18.0 stable release; SIDEBAR_SERVICE_VERSION follows
package.json (asserted by tests/service.spec.ts).
- Docs: README/README_EN badges + channel notes + a full "since v0.17.1"
changelog entry (the two folded alpha numbers are annotated), AGENTS.md
§2/§3 baseline wording, external-plugin-guide version line, and the
e2e-host-protocol header's re-verified list gains 0.1.2-rc.1.
First ci-windows run exposed four platform assumptions in the fixtures:
- smoke: the scratch repo inherited the runner's system-wide
core.autocrlf=true (Git for Windows default), so every index restore
smudged LF to CRLF and broke the byte-exact assertions — pin
core.autocrlf=false in the fixture (eol policy is not what these
destructive-op tests are about).
- git / git-worktree: Windows TEMP is the 8.3 short form
(C:\Users\RUNNER~1\...) while git reports long paths, and
realpathSync does not expand 8.3 aliases — switch the canonicalizer
to realpathSync.native (libuv resolves via GetFinalPathNameByHandle,
which does); same trick as the existing macOS /var workaround.
- bundle-route: the ETag memo revalidates on stat (mtime+size) and
Windows updates file timestamps lazily, so the equal-size rewrite
still looked unchanged — change the length too.
- market-manifest: GNU tar (Git Bash) reads the drive-letter colon of
absolute Windows paths as a remote-host spec — run it from the pack
dir with the bare tarball name instead.
The explorer @ button appended plain text (@folder/file.ts), which DSH's
composer only decorates as a folder ref (@folder/) — the file name stayed
outside the link. Insert FILE references through DSH's own structured
insert-reference event instead, minting one chip for the whole reference
(displayed as @basename, serialized as the full @path). Directories keep the
plain @dir/ mention so folder completion still works.
Three preview (render) bugs where DSH's chat-optimized markdown primitives
leak into the file-preview scroll world:
- CodeBlock banner: DSH's bannerWrap is position:sticky (top:0, z-index:6) so a
block keeps its copy action reachable inside a per-message scroll container.
In the preview every fence shares one .editorMd scroll container, so the
banner pins to the viewport top until the whole block scrolls past and reads
as a floating header. Neutralize stickiness for non-mermaid fences in the
preview (mermaid chrome replaces the banner and is excluded via
data-mermaid-processed).
- TOC popover z-index: .tocBar was z-index:3, below the code banner's 6, so the
outline panel painted beneath a sticky fence header. Raise the bar to 7.
- TOC dismissal: only the toggle re-click closed the popover. Add a document
pointerdown listener that closes on an outside press (blank preview space),
exempting the button and panel so the toggle still flips.
Adds md-toc unit coverage for outside-click dismiss and toggle-preserving
behavior; records the deviations in the TOC design doc.
shift fix) — but as a BARE declaration. Engines without clip support
(Safari < 16, older Chromium) drop an unsupported declaration at parse
time, which leaves the host `overflow: visible`: no clipping at all,
and the historical "collapsed panels extend the document's scrollable
area, whole page draggable" regression returns on those engines.
Declare `overflow: hidden` BEFORE `overflow: clip`: clip-aware engines
apply the later clip (identical behavior to #453), while old engines
fall back to hidden and keep clipping (with its pre-clip
scrollability — the same state they had before #453, strictly better
than visible). The cascade order is the contract.
Also extends the rule comment (the fix guards the whole
scroll-into-view class — focus moving into off-viewport regions,
nested iframes/workbenches claiming focus while loading — not just the
reveal case) and adds tests/panel-host-css.spec.ts guarding the
cascade (hidden first, clip last), the fixed viewport layer, and that
the degraded rule never reintroduces overflow. Contract documented in
the integration guide §12 (root anchor bullet).
- pins: CI mount-gate CLI, dsh.plugin.json engines floor, and the
@deepseek-ai/* peer floor / devDependencies baseline move to
0.1.2-alpha.5 (dsh-client-locale trails at 0.1.2-alpha.3 — no such
release upstream; ^0.1.2-alpha.3 already accepts the alpha.5 runtime)
- code: migrate the alpha.4 Session.events removal to snapshotEvents()
(sidechat transcript live reads, fork inheritance, jobs.output replay,
subagent activity) and drop the seedLength meta field the host removed
from CreateAgentOptions.meta
- gates: pnpm peers check clean, typecheck + 1137 unit tests green,
real-host mount smoke 14/14 on npm-published 0.1.2-alpha.5
getEntriesByType widens to PerformanceEntry; transferSize/
decodedBodySize live on PerformanceResourceTiming (the e2e specs sit
outside tsconfig.build.json, so the bundler never saw this).
The bottom-push rule anchored on #root :has(> [data-slot=conversation]):
a :has() match cache invalidates on every #root subtree mutation, and
the streaming chat mutates that subtree constantly — every layout-push
frame (the bottom drag writes --dsh-sidebar-height per frame) paid a
whole-document selector re-evaluation. The shell's locator (which
already holds the measured column node for the ResizeObserver) now
tags it with [data-dsh-center-col]; the rule evaluates once per node.
The tag retargets with the ref on boot swap / HMR node replacement and
is removed on unmount; the drag lane pins exactly-one-tagged-node and
its identity as the conversation slot wrapper's parent. The dead
[data-pane] branch (renamed away in alpha hosts) is gone.
TextEditor: the CodeMirror updateListener re-stringified the WHOLE
document into React state on every keystroke (O(docLength) per key,
re-rendering the shell per key). The listener now only flips the dirty
flag; the preview's draft is snapshotted from the live view when
entering preview (or on a content swap), and save() already reads the
live document.
The core client bundle statically imported every third-language
dictionary (ja/de/fr/…/zh-HK/MO/TW, ~640KB of source — roughly half
the bundle's weight) for a code path most sessions never enter: the
sidebar's own t() only consults zh/en plus the better-locale override
store, and the dicts' ONLY consumer is that store's register() call.
They now ride the existing lazy-chunk machinery as lib/client-locale.js
(fetched from /sidebar/bundle only when better-locale is actually
installed; a generation counter keeps the async registration moot when
the integration effect re-runs or disposes mid-flight). Until the chunk
lands, t() keeps the zh/en chain; the store's own revision bump on
register re-renders the chrome once the dicts arrive.
Core client.js: 1.46MB → 822KB (-44%). The chunk-boundary key-set
check moves to the existing runtime test (every shipped dict stays
key-set-equal to zh).
SideChat transcript: every 2s poll rebuilt ALL row objects, so every
row (markdown parse + DOMPurify included) re-rendered even when the
log had not changed. transcriptRows now takes the previous poll's
rows and re-adopts the position-aligned rows whose content is
identical (reuseRows) — only the changed tail rebuilds. The view
feeds the memoized array back through a ref; unit tests pin the
identity contract (unchanged rows keep object identity, changed rows
and everything past the change rebuild, tool-result landings never
re-adopt).
FileTree: expanded/revealed membership was an includes() scan per
rendered row (O(rows × expanded) per render on deep trees); one Set
per render keeps it O(1) per row.
Shell: buildNewTabOptions was inlined in the two Workbench JSX
sites — two fresh arrays per render (drag state, resizes) re-rendered
every LeafView's + affordance. One shared memo now.
DiffPane resize / TerminalView ResizeObserver: pointer and resize
streams fired a setState / glyph-measuring fit() per event; both ride
per-frame batching now (frame-batcher in the core, a local rAF gate
in the lazy terminal chunk). SessionLens sizes each row's content
with a Blob (a UTF-8 encode of the whole edit) — that moved from
per-row-per-render into one memoized pass per changed fold.
Boot: loadPrefs and loadExternalDisable each fetched the settings
document, serially, before the first paint — and the second had no
timeout, so a stalled wire kept the sidebar unmounted forever. Both
decisions now ride ONE fetch (loadBootDecision) under the existing
2s race.
GitLens: the 2s silent poll re-listed worktrees on every tick (a
second git process per tick for a list that almost never changes, and
a fresh worktrees array re-render each time). Silent ticks now fetch
only the selected checkout's status; every 15th tick (~30s) re-lists
and re-runs auto-selection, so an agent-created linked checkout is
still picked up mid-session.
ChangesTab: extractFileOps parses every accumulated tool/call (up to
4000 events); the poll ran it once for the badge and the render ran
it AGAIN per tick, and the fresh array defeated the downstream memo.
The fold now runs once per poll and its result rides a ref shared by
badge and render (reset on session switch).
A measurement harness, not a pass/fail gate (beyond the mount sanity
it shares with the other lanes): an init-script probe samples the
navigationStart → host-attach latency, a longtask collector runs
through the full built-in tab sweep, sidebar-shaped resource entries
report transfer/decoded/duration, and a frame sampler records the
bottom-strip drag's pacing (p50/p95/max) plus the width-leak guard.
Results print as single-line PERF_JSON records so a shell run can tee
and grep them; before/after comparisons land in
docs/plans/2026-08-31-perf-optimization.md. Baseline (main @ d70db8f,
3 runs): mount ≈ 377ms, 2 longtasks ≈ 161ms, drag p95 = 10ms,
width leak = 504px.
The bottom drag applied the panel's persisted width preference to
--dsh-sidebar-width regardless of panelOpen (only the idle push effect
gated it through layoutPushSize). With the right panel closed, the
first drag frame jumped the variable 0 → ~400px: #root lost 400px
instantly, the host frame's viewport dropped across its 1024px
auto-collapse breakpoint, and the NATIVE left sidebar snapped to its
56px rail — then snapped back on release. The bottom strip is the only
drag reachable with the panel closed, which is why only it showed.
applyDrag now rides the same gate as the push effect for the PUSHED
width; the panel-side DOM writes (panelRef/bottomRef/lastDragSize)
keep the raw width — the bottom panel's right edge derives from it.
The e2e regression pins the pose: panel closed + bottom open, drag the
strip, and every sampled frame must keep the pushed width at 0, the
host grid template, the left column width and #root's right edge
unchanged. Verified red on the old code (leaked 504px).