The guide renders `entry.icon`, and the `files` kind takeover registered
without one — so after the editor type stopped contributing its own
duplicate row (b56ceec), the single "Files" row was the only entry in the
new-tab list with a blank icon slot. The takeover now carries the editor
descriptor's icon (the takeover IS that descriptor's page).
Extracted `guideIconOf` so both registration sites build the icon field
the same way; the lifecycle test asserts the takeover's guide entry has
an icon.
The editor type's page identity IS the `files` kind takeover (same
explorer, same title), so the guide/new-tab list showed two identical
"Files" entries — observed on a real profile after the takeover landed.
The editor type now contributes no guide entry of its own; it stays
registered as the file RESOURCE viewer. Covered by the registration
lifecycle test (editor: no guide, files takeover: guide).
The editor type serves both the files PAGE and every file RESOURCE; its
native `title` callback ignored the address, so every opened file showed
up in the strip as "Files" — indistinguishable tabs. Verified on a real
profile: after opening `AGENTS.md` from the tree and a chat file link,
the strip read ["文件", "文件", "文件"].
`title(address)` now returns the file name for a file address and keeps
the descriptor title for a page address (`sidebar://editor`). Covered by
the registration-lifecycle test in tests/native-surface.spec.ts.
The native seat declares `sidebar.right.pane.tab` BEFORE it provides
`sidebarRightTabs`, so driving the registration off the slot declaration
read the service as missing and registered nothing — permanently, since a
declaration never collapses. Observed on a real DSH profile (0.1.5-alpha.1,
`dsh web` on the web profile): the plugin host and its other slot
registrations mounted, the native guide page stayed empty, and a probe
printed `sidebarRightTabs= undefined` at declaration time with the registry
present three seconds later. The scratch mount lane never saw it because
there the plugin activates after the native package finished providing.
`registerNativeSurface` now waits on the SERVICE (`ctx.inject`), which
re-runs whenever the registry appears/reappears, and keeps the per-body
slot registrations on `slots.inject` (they only need the declaration).
Regression test: `tests/native-surface.spec.ts` models the real order
(slot callback fires first, registry arrives later) and fails on the old
implementation. Verified on the real web profile: all guide entries
(files / editor / git / subagent / sidechat / terminal / browser) appear
and the terminal tab renders.
Phase 2 of the 0.1.5 adaptation: the plugin stops drawing its own right
panel and contributes tab TYPES to DSH's native right Sidebar, which owns
the right column. The bottom workbench (terminal dock) stays plugin-owned.
Registrations (src/client/native/):
- every TabDescriptor becomes a native tab type (kind = descriptor.id, the
default `extension` band) plus a keyed body in `sidebar.right.pane.tab`
and a live chip title in `sidebar.right.pane.tab.title`
- the `editor` type also claims `dsh-resource://file/**`, so it outranks
the built-in `text` preview (fallback) and every file the product opens
lands in the plugin's editor
- the built-in `files` page kind is TAKEN OVER by an extension
registration of the same kind: `openTab('files')` draws the plugin's
explorer, and the built-in resumes when the plugin unregisters
- registrations follow the plugin registry + the side-card settings: a
type registered later (external plugin) gets its native type too, a
disabled type is unregistered (absent from the guide, refuses opens)
Adapter (src/client/native/tab-adapter.tsx):
- one synthetic plugin `SidebarTab` per native tab id, minted from the
native record + navigation params (or the descriptor's own createTab,
which a native guide open bypasses), kept live across navigations and
updateTab writes
- the explorer's per-tab expansion/reveal sets and the per-kind instance
counter, which the plugin's own layout used to hold
Service routing (src/client/service.ts):
- `setSurface` installs the native write face; openTab/openFile land
natively (a path becomes a resource address, a path-less editor open
becomes the `files` page, the rest carry the seed as navigation params),
closeTab/updateTab/activateTab recognise native tab ids
- new optional `OpenTabSeed.target` ('right' default, 'bottom' keeps the
bottom workbench); the bottom panel's own + menu and auto-terminal pass
'bottom'
- src/client/native/surface.ts probes the controller's per-session writes
(`openTabIn`/`openResourceIn`/`closeIn`, not in `ISidebarRight`) and
queues opens for a session whose surface is not mounted yet
Right panel retired behaviourally (Phase 3 deletes the code): Sidebar
renders its bottom workbench only, keeps the panel collapsed, and hides
the right-panel toggle. The panel-specific e2e specs are removed; the
mount lane now drives the native guide (tab sweep), the plugin explorer
inside the native pane (editor/mermaid/README chunks) and the sidechat
host routes.
Verified: pnpm typecheck / lint / build / test (126 files, 1349 tests) /
check:consumer-types, plus pnpm test:mount against a real
@deepseek-ai/dsh@0.1.5-alpha.1 host (7/7).
Move the plugin onto the DSH 0.1.5-alpha.1 baseline (peer floor
^0.1.5-alpha.1, CI pinned to the alpha dist-tag) and fix the two host
contract changes that alpha line introduced.
Baseline:
- all @deepseek-ai/dsh-* peer/dev deps -> 0.1.5-alpha.1; optional peer
@deepseek-ai/dsh-client-ui-sidebar-right (declared, unused until the
native-surface phase); devDep @deepseek-ai/dsh-agent-loop (inbox fold)
- dsh.plugin.json engines >=0.1.5-alpha.1; version 0.19.0-alpha.0;
SIDEBAR_SERVICE_VERSION follows
Live assistant stream (assistant/chunk is gone):
- 0.1.5 publishes in-flight model deltas as process-local
agent/assistant-stream frames instead of durable assistant/chunk events
and embeds the settled stream in assistant/message / assistant/attempt.
src/assistant-live.ts folds the frames into a bounded per-session buffer;
sidechat.events serves the current attempt as `live` rows (replaced every
poll, never a delta) and the transcript renders them; the inherited
in-progress snapshot reads message content / attempt stream / live frames.
Cold session reads (persistence.inspect is gone):
- 0.1.5 reads a stored session through an explicit read handle
(open(id,'read') -> handle.read() -> close()). src/session-store.ts owns
that protocol; every cold read (sidechat transcript, cold-resume preset,
session.cwd, changes replay) goes through it.
Dead code and docs:
- drop the remote.session.openWorkspacePath interception: 0.1.5's chat opens
files through ctx.sidebarRight.openResource instead, so the wrapper had no
caller and would hijack the host's "open in app" gesture; the
interceptOpenPath pref, its settings row and its locale keys go with it
- AGENTS.md §3 + external-plugin-guide.md describe the new baseline
- vitest setup bridges Node's localStorage accessor to jsdom's store (the
pre-existing 26 failures on Node >= 22)
- desktop-layout e2e seeds through host RPC and types into the Lexical
composer (0.1.5 replaced the textarea) instead of driving the directory
picker
Verified: pnpm typecheck / lint / build / test (124 files, 1327 tests) /
check:consumer-types, plus pnpm test:mount against a real
@deepseek-ai/dsh@0.1.5-alpha.1 host (18/18).
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.
- 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.
.html/.htm/.xhtml op previews (read / write / edit, non-error) gain a
Render/Raw toggle in the preview pane head: the render view loads the
SAVED file through the same /sidebar/html route the editor's html viewer
uses, so relative assets (./style.css, img/x.png) resolve inside the
route and a segmented read still renders the whole document (the route
serves the file, not the op snapshot — the file-trace-style srcDoc
approach would render a cut-off document whose scripts never run).
The frame is ALWAYS sandboxed (HTML_IFRAME_SANDBOX, moved to the shared
html-preview.ts module — opaque origin, no allow-same-origin, no
top-navigation) plus the route's CSP sandbox header as
defense-in-depth; unlike the editor viewer this surface has no
no-sandbox escape hatch. The contract is pinned by new sandbox-spec
tests on the extracted HtmlRenderPreview component and the pane toggle
(off by default; absent for non-html and error targets).
Locales: zh/en + all 19 shipped third-language overrides (key-set-equal
gate green). Typecheck + eslint clean; sandbox/redact/diff-rows/frontmatter
suites 36/36 green. The full-suite host-side failures (fs-operations /
pty smoke / git / install-powershell) are pre-existing on the base
branch (verified via stash) and unrelated.
- DiffPane.tsx: gate the redacted view on the computed `hit` (the unused-var
lint was the memo returning redactionHit: true unconditionally; now an
unredacted payload short-circuits like the toggle-off path, and only an
actual secret hit builds the masked copy)
- MarkdownHtml.tsx: drop the three dead imports (ComponentType,
lazyChunkComponent, MermaidMarkdownProps) left by the mermaid-lazy merge
Typecheck clean; eslint clean on both files; 39 targeted tests pass.
The changes-tab op previews replay read/write/edit payloads into the DOM
(diff rows, read rows, markdown reading mode, error text) — when the model
reads a credentials file, that plaintext must not land on screen or in
inspectable DOM. New display-only redaction layer, on by default with a
one-click pane toggle (persisted in localStorage):
- path layer: credential-shaped filenames (.env / *secret* / *credential* /
*token* / *api-key* / private keys...) mask whole files line-wise
- content layer: ordinary files mask only secret-shaped spans — secret
field assignments keep the field name (api_key: [REDACTED]), plus bare
token prefixes (sk- / AKIA / ghp_ / xox / ya29.), Bearer headers, and
PEM private-key block headers
- redacted payloads flow through the same viewOp shape into diff rows,
read rows, markdown reading mode, and error text
Known edge (documented in README): an unquoted mermaid node label
containing a redacted secret breaks the diagram and falls back to source;
workaround is quoting the label. Session data is untouched — this is
display-only and not a boundary against same-context malicious plugins.
Locales: zh/en dictionaries + all 19 shipped third-language overrides
(key-set-equal gate green). 9 new unit tests; typecheck clean; targeted
suites 40/40 green. The fs-operations failure is pre-existing on the base
branch (environment-dependent, verified via stash) and unrelated.
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.
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