The pure-logic code carries real win32 branches (shell parsing, WSL path
projection, the PowerShell installer) that mock-injection tests alone
cannot validate. Mirror the ci job on windows-latest (Git Bash as the
step shell) for typecheck / test / build / consumer-type-surface; the
plugin-mount lane (real DSH + Playwright) stays ubuntu-only.
check-consumer-types.sh: force MSYS winsymlinks:nativestrict so Git
Bash's ln -s creates real symlinks instead of deep-copying the repo
(node_modules + full .git history) — inert on Linux/macOS.
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.
The markdown preview container re-mounts on every mode flip (its scrollTop
lives on that DOM element), and the save-then-switch-to-preview reload
(#215) rebuilds the whole TextEditor instance - so after editing and
saving, the preview returned to the top, and entering edit mode opened the
editor at the top instead of where the reader was.
- Module-level per-file (session + path) preview scroll memory, seeded on
component mount and re-applied in a before-paint layout effect on
[mode, mdText]; a restoring guard keeps the collapsed-reload clamp from
polluting the remembered position.
- Preview -> edit handoff: throttle-capture the block text at the preview
viewport top, reverse-map it to a source line (linesOfSelection) and
scroll the editor there, with a proportional fallback when the text match
is ambiguous (ratio === 1, i.e. the reader is exactly at the bottom, also
lands the editor at the bottom).
- The editor jump writes scrollDOM.scrollTop directly instead of using
CodeMirror's scrollIntoView: that API walks every scrollable ancestor -
and even the window when the browser is zoomed (visualViewport <
innerHeight) - to reveal the target in the sidebar, which dragged the
whole sidebar/page up when the reader was at the very end of the
document. A plain scrollTop write on the editor scroller can never
scroll anything outside the editor.
Closes#466
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 comparison table (3-run medians on the same machine against a real
0.1.2-alpha.2 host): mount latency 377→273ms (-28%), the host's
aggregated client bundle ships 203KB less over the wire (-14%,
matching the 19 dictionaries' gzip weight) and 640KB less decoded
(their source weight), boot settings.get 4→2, the bottom-drag width
leak 504px→0. Frame pacing was already at 8/10ms p50/p95 — the drag
fix removed the host-layout snap, not a frame drop. Structural wins
that don't show in a single sweep (git spawns per poll tick, fold
passes per poll, transcript row re-renders, per-keystroke doc
stringification) are listed with their per-event arithmetic.
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).