vecnode/vncode--packages-dsh-themes ↗★ 3

dsh-themes

The vncode Web GUI's conversation-header package. It owns four controls on that header. The Page-zoom control (order -40, the leftmost of the four) drops a menu holding the level in force plus the two steps, Zoom in and Zoom out: it writes `zoom` on the document element along Chrome's own ladder cut at 50% and 200%, which is the browser's own Ctrl+ / Ctrl- PAGE zoom - the only way to get it in the native window the desktop launcher opens (a Tauri shell over the same dsh web, which has no such gesture at all) - and remembers the level per origin in localStorage, re-applying it before the first render. The Screenshot control (order -30, left of the Themes control) captures the whole window - the app is a fixed-viewport shell, so its 100% width and 100% height are exactly the tab's box - and saves the PNG to the Desktop of the machine running the app: the browser takes the pixels (getDisplayMedia on the current tab, one frame into a canvas) and this package's own host route writes the file, falling back to an ordinary browser download when that route is absent. The Themes control switches the app between the light, dark and system appearance, the same preference Settings > General > Appearance owns, and its menu also lists every theme REGISTERED into that same shipped registry - through ctx.theme.register, the documented third-party surface - which is where this package's own Nord, Monokai and Hacker come from (Nord's Polar Night surfaces, Snow Storm text, Frost accents and Aurora states; Monokai's near-black warm page, off-white body and pink/cyan/yellow/green syntax palette; Hacker's near-black green-cast page, phosphor body and signal-green/amber/cyan syntax palette - all three as alias-token overrides on the dark base; in-process by the shipped design, since the durable preference schema accepts the built-in three only). The button wears one static appearance mark rather than the active preference's sun/moon, and the menu is built from the registry's own list, so a theme is added by being registered. And the Session-log download seat replaces the shipped three-dot 'more actions' button (its only menu item was Download session log) with one plain download icon button that starts the export on the first click while the shipped sessionLogDownload controller keeps doing the work, its preparing/success/error dialog included. It also carries the pack's appearance overrides: the always-light paper the rendered Markdown view is drawn on, the chrome rule that hides the preview header's viewer menu on Markdown tabs so the page has exactly one renderer, the left column's top bar (the branding row becomes the same 76px band, ending in the same hairline, that the middle and right columns open with), the pack's VN branding on that row - the shipped mark and wordmark are replaced by a 24px black disc and the text vncode - and the header ring, so the right bar's own collapse/expand toggle in the header corner wears the same round hairline outline as this package's own header buttons. Alpha. 适合需要调整界面缩放比例、更换主题或导出聊天记录的用户。

パッケージ
dsh-themes
互換性
未検証
バージョン
0.1.0-alpha.19
ライセンス
MIT
最終更新
2026/09/30

同名パッケージの別リポジトリ

インストール

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:vecnode/vncode#b3a6a0d53b96ef29d452af3962e8d726a122db3a&path:packages/dsh-themes

ドキュメント

README 全文を読む ↗

dsh-themes (alpha.19)

The pack's conversation-header package. It owns four controls on that header and the appearance overrides that dress it.

  • Themes (order -20) — the size and dress of the header's other icon buttons, immediately left of the shipped "Open In…" control (-10). Its menu holds Light, Dark and System plus every theme registered into the shipped theme registry, which is where this package's own Nord, Monokai and Hacker come from; the button wears one static appearance mark rather than the active preference's sun/moon. See Registered themes.
  • The Session-log download seat (order 0) — one plain download icon button where the shipped three-dot "more actions" button sits, which starts the session-log export behind a one-item menu, so one click starts the export. See The Session-log download seat.
  • The Screenshot control (order -30) — captures the whole window (the app is a fixed-viewport shell, so the page's 100% width and 100% height are exactly the tab's box) and saves the PNG to the Desktop of the machine running the app through this package's own host route. See The screenshot control.
  • The Page-zoom control (order -40, the leftmost) — a menu holding the level in force plus Zoom in and Zoom out, doing what the browser's own Ctrl+ / Ctrl- page zoom does: zoom on the document element along Chrome's own ladder. It exists because that gesture belongs to the browser — a Chrome tab has it, and the native window the desktop launcher opens (a Tauri shell over the very same dsh web) does not. See The page-zoom control.

It also carries the pack's appearance overrides — rules that hold one surface on a fixed palette or a fixed shape whatever the app theme is: the Markdown paper, the Markdown chrome, the left column's top bar with the pack's VN branding, the header ring, and the right bar's resize seam. All of them are plain engine-neutral CSS, so they hold in whichever browser the Web GUI is opened in. It is a thin control, not a second theme system, and the preference stays owned by the shipped @deepseek-ai/dsh-client-ui-theme: its theme client service persists the choice in the ui-theme settings namespace, resolves system through prefers-color-scheme, and ui-layout applies every snapshot to the document (body[data-ds-dark-theme] + the --dsw-* tokens). This bundle only reads the published snapshot and calls setTheme(id), exactly like the Settings → General → Appearance row — one preference, one persistence path, one palette. The service is resolved lazily (ctx.get('theme'), never in inject), so a profile that never mounts ui-theme keeps its header, with the button disabled and reading "The theme service is unavailable". It can also register themes through that same service (ctx.theme.register, ui-theme's documented third-party surface).

Registered themes

@deepseek-ai/dsh-client-ui-theme exposes register({ id, colorScheme, tokens }); ui-layout's presenter writes a registered theme's alias tokens as inline CSS variables on body, over whichever base palette colorScheme selects, and getTheme() publishes every registered theme in snapshot.themes. This bundle:

  • registers each theme in THEME_EXTENSIONS inside a ctx.effect, so the theme leaves the registry with the row. A profile without ui-theme simply has nothing to register into, and the control reports that the way it always did;
  • builds the control's menu from the registry (snapshot.themes, with the system preference appended last), so a theme becomes selectable by being registered — there is no second list to keep in step;
  • draws the header button with one static appearance mark (a half-filled disc), because a registered palette has no shipped glyph to wear.

All three sit on the dark base palette in the same 93-token shape — 73 alias tokens, 11 --dsw-specific-* and the nine shiki syntax tokens — so one theme cannot quietly become a subset of another. THEME_EXTENSIONS order is the menu order: Light / Dark / Nord / Monokai / Hacker / System.

Nord

Nord (nordtheme.com) recolors the alias layer with the palette's own colours:

NordValuesWhat they paint here
Polar Night#2e3440 #3b4252 #434c5e #4c566athe page, the surface ladder, menus, code blocks, scrollbars
Snow Storm#d8dee9 #e5e9f0 #eceff4primary and secondary text, inverted foreground
Frost#8fbcbb #88c0d0 #81a1c1 #5e81acbrand, links, primary button, switches, focus rings, muted text
Aurora#bf616a #d08770 #ebcb8b #a3be8c #b48eadthe error / warn / success states and the syntax tokens

nord0 is the page for the conversation and the sidebar (the way vscode-nord draws editor and sidebar alike — the columns are separated by the frame's own hairline, not by a lighter rail), a code fence sits one step above it, and the syntax colours follow the rules nord-vim follows: keywords and comments in Frost #81a1c1, functions and links in #88c0d0, strings in Aurora green, numbers in #b48ead.

Monokai

Monokai (monokai.nl) is the classic TextMate palette Wimer Hazenberg wrote for Coda:

MonokaiValuesWhat they paint here
Surfaces#272822 #2f3029 #3e3d32 #49483ethe page, the raised step, and the theme's own line-highlight and selection steps — menus, code blocks, scrollbars
Text#f8f8f2 #dadad5 #b6b4a8 #75715ethe off-white body, its 12% darker step, the comment grey lifted halfway back to the body, and the comment grey itself as the quietest tier
Warm accents#f92672 #fd971f #e6db74the pink brand — switches, focus rings, the primary button, errors — plus orange parameters and the warn step, and yellow strings and warning
Cool accents#66d9ef #ae81ff #a6e22ecyan links, the info state and the ghost-active border, purple constants, green functions and success

#272822 is the page for the conversation and the sidebar, the theme's line highlight (#3e3d32) is one step above it and its selection (#49483e) the step above that, so a code fence, a menu and a selected row are all steps of the same near-black. The syntax tokens carry the classic Monokai roles unchanged — keywords and tags pink, constants purple, strings yellow, comments the olive grey, parameters orange, functions green, punctuation the off-white body. Steps the classic palette does not define are derived from its own colours rather than invented: the primary button's hover is Monokai Pro's pink #ff6188, its dimmed fill is the pink darkened 28% (#b31b52), the cyan's hover is the cyan lifted a fifth toward white (#85e1f2), and the two text tiers between the body and the comment grey are the body darkened 12% (#dadad5) and the comment grey lifted halfway back to the body (#b6b4a8).

Hacker

Hacker is the phosphor terminal — the palette a monochrome CRT and a hacker film are both drawn with:

HackerValuesWhat they paint here
Surfaces#0a0e0a #101710 #162116 #1c2c1cthe page and the sidebar alike, the raised step, menus, bubbles and code blocks, then the selection and the toolbar one step above
Text#d8ffe0 #bee0c5 #8bb595 #3f6b4athe phosphor body, its 12% darker step, the quiet tier halfway back to the body, and the dim green as the quietest tier
Signal green#00ff41 #33ff67 #00b82fbrand, primary button, focus rings, switches, success, and the keyword syntax token (the function token is the lighter step #5cff85; hover and dimmed are derived from the green)
Amber#ffb000 #ffd166the warn state and its label, and the string tokens
Cyan#00d9ff #33e1ff #7dd3fclinks, the info/business state, the ghost-active border, constants, parameters
Error red#ff4d4dthe error state — Monokai's pink would be off-palette here

#0a0e0a is the page for the conversation and the sidebar, #162116 is the step a menu, a bubble and a code fence sit on, and #1c2c1c is the selection and the inline chip, so a menu, a fence and a selected row are all steps of one near-black with a green cast. The syntax tokens are one terminal's worth of colour — keywords and functions green, strings amber, parameters and constants cyan, comments the dim green, punctuation the phosphor body. Steps the palette does not define are derived from its own colours by the same arithmetic the Monokai section documents: the signal green lifts a fifth toward white for its hover (#33ff67) and darkens 28% when dimmed (#00b82f), the cyan lifts the same fifth (#33e1ff), and the two text tiers between the body and the dim green are the body darkened 12% (#bee0c5) and the dim green lifted halfway back to the body (#8bb595).

Why the alias layer, and how the choice is kept

A static is shared by roles that are not the same role — in the dark palette neutral-bluish-50 is both the primary label and the brand fill — so recoloring the --dsw-static-* ramp drags unrelated surfaces along with it. The alias layer is the semantic one, and the layer ui-theme documents as the third-party surface. Aliases a theme does not name keep their shipped dark value: the scrims (bg-mask-*), the elevation strokes and the shadow scale are scheme-neutral black/white alphas and read correctly on any registered dark theme unchanged.

ui-theme's durable preference schema accepts light / dark / system only, so an extension theme id is written to the pack's own vncode section instead (through the uiState service dsh-ui-state publishes) and put back on the next load; picking a built-in clears that field so it reads as inherited again, and the shipped preference is never faked. Keeping it in force is the other half: ui-theme re-adopts its durable preference whenever the settings DOCUMENT changes, and an extension theme is never in that section, so this control treats it as a DESIRED STATE it keeps applied (desiredTheme + reconcileTheme on every theme/change) rather than a one-shot choice. ui-theme's own namespace REVISION is the tie-break: a re-adopt (revision unmoved) puts the theme back, while a built-in chosen in the shipped Settings → Appearance row (revision moved) wins and the remembered id is cleared. Re-picking the built-in that was ALREADY durable is the one case the revision cannot see, so the extension is re-applied there and this control's own menu is the escape.

Adding another theme is one entry in THEME_EXTENSIONS (id, label key, colorScheme, token map, menu glyph) plus its theme. copy in both dictionaries. The menu picks it up from the registry; nothing else changes.

The Markdown paper

The shipped document preview draws rendered Markdown into a container marked data-document-markdown and paints it from the --dsw-* tokens. Those tokens are declared on body (light) and overridden on body[data-ds-dark-theme] (dark), so a subtree cannot un-dark itself by referencing them — it just inherits the dark values, which is why the rendered document would otherwise go dark with the app. This package injects one rule that re-declares ui-theme's own light declarations on that container (the static palette, the ~80 alias tokens, and the shiki token colours), then paints background:#fff on it:

body [data-document-markdown]{ /* the theme's light layer, verbatim */ background:#fff; … }
  • Read, not hardcoded. The light layer is copied out of ui-theme's own stylesheets at boot (every top-level :root / body rule that is not the dark one), so a palette change on a harness bump carries over by itself instead of freezing today's hex values here. The read uses the CSSOM and falls back to the rule's text where an engine does not enumerate custom properties.
  • All or nothing. If the stylesheets cannot be read, nothing is injected: forcing white without the light tokens would paint light text on a white page, which is worse than leaving the view on the app theme.
  • Scoped. Only the rendered Markdown document is pinned — chat Markdown, code previews and every other surface keep following the app theme. A second selector paints the preview's scrollport ([data-textpreview-body], matched with :has([data-document-markdown])) white as well, so a short document does not sit on the app's dark canvas underneath; where :has() is unsupported that one rule is dropped and the document itself is still white. Because --dsl-code-block-* and --shiki-* resolve inside the document, the copied tokens also give the code blocks, inline code, links and lists their light styling for free.
  • Re-installed on every theme/change, and once more on the tick after boot, in case ui-theme's stylesheets land after this row.

The editor's Preview button is what reaches this view for a Markdown file; the white page is this package's doing.

The Markdown chrome

A rendered Markdown page in this pack has exactly one viewer, but the shipped preview header builds its viewer menu out of every candidate implementation it resolved for the file — the Markdown body plus the shipped plain-text fallback — so a Markdown tab offers "Markdown" / "Plain text". The second entry is never what the pack wants: plain text is what the editor's own text surface is for, and the deliberate way there is the Edit button the editor's document body draws on the page. So the menu is hidden on Markdown tabs:

body [data-document-preview="@deepseek-ai/dsh-client-ui-sidebar-documentpreview/markdown"]
  [data-document-viewer-menu]{display:none}

The preview stamps the selected implementation's id into data-document-preview on the document root, so the rule is scoped by renderer, not by guesswork: it matches the shipped Markdown id alone and a code or plain-text preview keeps its menu, where the choice is real and the pack has no opinion about it. It is static CSS, installed once (there is no palette to read) in its own tag (dsh-themes/markdown-chrome.css), so it does not depend on the paper's read succeeding. Only the header control goes: the path, the reload tool, the document itself and the page's Edit button are untouched.

The left column's top bar

The frame opens with one band per column, and every column's band ends in the same hairline at y=76:

columnits bandits line
left.hHd-Xa_logoRow — the sidebar branding rownone at all
middlethe conversation header (min-height:76px, padding:10px 28px 0 20px).5px solid var(--dsw-alias-border-l3) at 76
rightthe docking kit's 38px tab strip, then the open tab's own 38px header (the shipped Files tab)the same hairline at 38+38 = 76

The left column is the odd one out twice over: no rule under its branding row, and a collapsed rail that changes both the root's top padding (6px → 18px) and the row's height (60px → 36px), so anything drawn under that row moves with the toggle. The override gives the branding row the other two columns' band, in both rail states:

html .hHd-Xa_root.hHd-Xa_collapsed{padding-top:6px}
html .hHd-Xa_root .hHd-Xa_logoRow{
  height:70px;                       /* 6px root padding + 70 = the 76px line */
  margin:0 -12px 8px;                /* rule to both edges; 8px under it       */
  padding:4px 12px 35.5px 16px;      /* leaves a 30px content strip at the top */
  align-items:center;
  border-bottom:.5px solid var(--dsw-alias-border-l3,rgba(127,127,127,.18));
}
html .hHd-Xa_root.hHd-Xa_collapse