Escape the .pdf extension in the op-target regex (the unescaped /.pdf$/
matched any path ending in pdf) and fold the copy-pasted toggle button
into PaneToggle; record the sixth deliverable in the design doc.
Records the five features shipped on this PR (md reading mode incl.
mermaid, highlight expansion, secret redaction, html render mode), their
deliberate boundaries (redaction heuristics, saved-file render
semantics), and the 2026-09-06 standards pass.
- redaction useMemo: one redactText pass per field (the outcome carries
both text and hit; was a hit-scan plus a mask pass), and the banner
condition drops the redundant redactionOn check (hit implies on)
- three copy-pasted header toggles -> PaneToggle; the md reading body
-> MdReadingView (absorbing the mermaid branch + codeLabels)
- redaction preference persisted under the repo's dsh-sidebar:v1: prefix
- render toggle limited to .html/.htm (the editor html viewer's ext set;
.xhtml went beyond any claim)
- tsx/jsx share TS_WORDS/JS_WORDS; CSS word list loses its duplicate
entries and its nonexistent true/false constants
- parseReadContent/parseReadLines share the contentLines() envelope core
(content extraction + Showing-lines filter)
Path layer: patterns now match with a right boundary (must not continue
into [a-z0-9]) and list only store-plural forms (credentials/secrets/
passwords, not tokens), so tokenizer.ts / dev.environment.ts /
style.keys.ts / api-tokens.md are no longer masked whole while
.env.local / credentials.json / mytoken.yaml still are.
Content layer: drop the bare key/auth/token/pass/bearer field names —
they label far more ordinary content (keyboard keys, CSS custom
properties, lexical tokens) than secrets; qualified names stay and
Bearer schemes remain covered by BEARER_RE.
Adds the over-masking regression cases; fixes the spec's source import
extension to .ts (repo convention).
.htmlFrame hardcoded #fff -> var(--dsw-alias-bg-base) (mirrors the editor's
.editorHtml); .redactBanner referenced --dsw-alias-state-warning-primary,
which does not exist in the DSW vocabulary (it is state-warn-*), so the
var() always fell through to a hardcoded #b8860b — use the real token with
no literal fallback.
Every .md preview renders through MarkdownDocument (htmlInfo is ungated),
but its markdown segments passed the raw source into MarkdownText, whose
chat-security stance drops non-http(s) image destinations — so local
markdown-syntax images (, reference-style definitions)
always fell back to alt text. Only HTML <img src> tags were rewritten
(postProcessSanitized).
Apply rewriteLocalImageUrls to each markdown run (segment + appended
reference definitions) inside MarkdownDocument's prepared memo, through
the same media trust fence the sanitized HTML leaves already use.
Idempotent for already-absolute media URLs; remote destinations
untouched. Also correct the stale "non-null only for HTML documents"
comment on TextEditor's htmlInfo.
sidechat.start passed `seed` without `meta.isSeeded: true` +
`inheritedEventCount`, so dsh-session treated the whole seed as the
child's OWN events and the child's Inbox constructor replayed the
parent's `agent/inbox/spliced` history — inheriting whatever input sat
UNCLAIMED in the parent at the click moment:
- next-turn: a queued follow-up behind a still-running turn;
- next-step (the reported long-context case, parent queue empty): a
tool-result context / steering spliced between step boundaries.
The first side prompt then claimed [phantom, boundary, question] — the
stale parent message was sent to the model BEFORE the boundary and
logged as a user/message. The fix passes the exact marker pair the
host's own session.fork uses (api-session-controller); ownEvents() now
stops at the end-seed boundary and the inherited inbox replays empty.
Regression tests run the REAL Session.create + Inbox in both
directions (old shape inherits the phantom, marked shape stays empty);
routes tests pin the two create options. Verified: pnpm typecheck /
test (5 pre-existing unrelated UI-spec failures on main unchanged) /
build.
The host-anchor fallback diagnostic strip inlined hardcoded colors
(#f2a1a1 on #1b1b22). Route them through var() chains like the existing
SideChatView.module.css fallback pattern: text/border read
--dsw-alias-state-error-primary (the codebase's canonical error color,
23 existing usages) and the background reads
--dsw-alias-bg-layer-3,var(--dsw-alias-bg-base,...) (elevated surface,
then page base). The old hexes ride as the chain tails, so with no skin
tokens on the page the bar renders byte-identical to before, while any
--dsw-alias-* skin re-themes it (guide §12: no hardcoded colors). The
guide does not cover this strip and tests/theme.spec.ts asserts
effectiveTokenValue semantics only, so neither needs a follow-up.
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.
The viewport-corner toggle cluster sits at top:3 riding the right panel's
tab strip while expanded, and recenters to top:14 over DSH's session
header row when collapsed — an 11px snap on every expand/collapse. Ride
the panel slide's own duration/easing (--ds-transition-duration-slow +
--ds-ease-in-out) so both moves stay in lockstep; prefers-reduced-motion
already clears .toggleCluster transitions.
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.
On the Windows lane the @deepseek-ai/cordis link produced TS2307 even
though the link was visibly created: under pnpm its target,
node_modules/@deepseek-ai/cordis, is itself a junction into .pnpm, and a
native symlink pointing AT that junction does not traverse for tsc.
Resolve both link targets with node's native realpath first
(GetFinalPathNameByHandle also expands junctions, which bash-level
resolution may leave untouched), and fail loudly with the link layer
listed when a created link does not resolve to a package root.
node-pty's WindowsTerminal queues resize calls that arrive before the
ConPTY control socket's first data flush (_deferNoArgs). If the pty exits
before the flush, the deferred resize throws 'Cannot resize a pty that
has already exited' inside the socket's 'data' handler — uncatchable by
any caller and fatal to the host process. The Windows CI lane caught
exactly this as an unhandled error while all 1233 tests passed.
Park pre-first-output resizes in our own gate (armed at every spawn
site) and replay them via setImmediate once output has flushed the
deferred list — by then node-pty executes resizes synchronously, so the
exited throw lands in tryResizePty's catch instead of the host. POSIX
terminals resize synchronously (no deferral), so the gate is armed on
Windows only and the other platforms keep their exact behavior.
Also route AgentPtyRegistry.resize through tryResizePty: it previously
called pty.resize bare, missing the just-exited throw guard the tool and
attach-socket paths already had.
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 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.
- 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.