citisen/dsh-font ↗★ 0

@citisen/dsh-font

DeepSeek Harness plugin: customize the Web GUI fonts (interface family, code family, and three font-size axes) from Settings 适合需要个性化调整Web GUI界面及代码字体样式的用户。

パッケージ
@citisen/dsh-font
互換性
未検証
Cordis ピア範囲
^4.0.2
バージョン
0.1.1
ライセンス
MIT
最終更新
2026/09/15

インストール

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:citisen/dsh-font

ドキュメント

README 全文を読む ↗

dsh-font

English | 中文

Customize the DeepSeek Harness Web GUI fonts from Settings: the interface font family, the code font family, and three independent size axes.

This is a third-party dsh profile bundle. It ships as one dual-face package: a Node half that owns a durable settings namespace and a pre-paint style row, and a browser half that paints the result and registers the Settings row.

Requires dsh 0.1.5-rc.1 or a later 0.1.5-rc.x; it uses the settings.general.item slot, the settingsScope service, and ctx.theme.overrideTokens, all of which are present in the latest and next release channels.

What it adds

A Fonts row in Settings → General, with five controls:

ControlEffectRange
Interface font--dsw-font-family — all non-code UI textany CSS font-family list
Code font--ds-font-family-code — code blocks, inline code, monospaceany CSS font-family list
Interface text sizeScales every hard-coded UI text size75% – 150%, step 5%
Conversation text sizeMessage bodies, headings, and tables12 – 20 px
Code text sizeCode blocks and inline code10 – 20 px

Both family fields accept free text and offer one-click presets. Every value is saved through the host settings document ($DSH_HOME/settings.yaml, namespace ui-font), so settings survive a restart and are shared by every browser pointed at the same host.

The Interface text size and Conversation text size axes are independent on purpose: bumping the interface makes the surrounding chrome easier to read without changing how much message text fits on screen, and vice versa.

Install

dsh plugin --profile web add @citisen/dsh-font

Straight from GitHub (identical package, no registry involved):

dsh plugin --profile web add github:citisen/dsh-font

Then restart the Web surface:

dsh --profile web

dsh plugin forwards to pnpm inside the profile directory and then reconciles dsh.profile.bundles: because this package declares dsh.bundle, the install appends it as a profile layer automatically. Nothing has to be hand-edited in cordis.patch.yml.

Both paths are verified end to end on a fresh profile — the row reaches the composed entry list and the browser roster resolves the client bundle. Confirm it yourself with node scripts/verify-profile.mjs after installing.

Installing from a local checkout

dsh plugin --profile web add is unreliable on Windows when the profile and the checkout are on different drives — pnpm resolves the cross-drive directory link to a nonexistent path, and the reconciliation then concludes the package declares no dsh.bundle and leaves it out of bundles. Link it yourself instead:

cd "$DSH_HOME/profiles/web"
pnpm add "D:/path/to/dsh-font"          # writes the dependency
# then repair the link pnpm created, which points at
#   
/D:/path/to/dsh-font  -- a path that does not exist
rm -rf node_modules/dsh-font
cmd /c mklink /J node_modules\dsh-font D:\path\to\dsh-font   # Windows
# and add "dsh-font" to dsh.profile.bundles in package.json by hand

Verify the result with node scripts/verify-profile.mjs; it fails loudly if the row never made it into the composed entry list.

How it works

Read this section before changing the code; the two halves solve different problems and the split is deliberate.

Why the families go through the theme service

The design system declares both families once, on :root:

:root{--dsw-font-family:…;--ds-font-family-code:…}

Every typographic token in the UI resolves through var(--dsw-font-family) (--dsw-font-base-16, --dsw-font-xs-13, the whole Markdown ladder, …), and the shell's own body rule reads it too, so overriding those two variables retargets the entire interface.

The obvious way to set them — a custom property on documentElement — does not survive. ui-layout's ThemePresenter owns document.body.style and, on every theme change, deletes every custom property it did not write before re-writing the active token set. A property set from outside disappears the first time the user switches between light and dark.

So the browser half stacks the families onto the theme itself:

ctx.theme.overrideTokens('dsh-font', {
  '--dsw-font-family': { light: uiFontFamily, dark: uiFontFamily },
  '--ds-font-family-code': { light: codeFontFamily, dark: codeFontFamily },
})

The override layer folds into the active snapshot, and the presenter then republishes it — and keeps republishing it — on every palette change. The { light, dark } pair is mandatory: overrideTokens throws a teaching error on a bare string, because a single value would go illegible on the other palette.

Why the sizes are a stylesheet

There is no interface font-size variable in the design system. Each component hard-codes one of 11, 12, 13, 14, 16, 20, or 24 px, and the --dsw-font-* scale is a fixed ladder (--dsw-font-xs-13 is 13px, full stop). Code sizes are likewise baked into composite tokens.

So the plugin emits one utility class per shipped size:

.dsh-font-size-14{font-size:calc(14px * var(--dsh-font-ui-scale,1)) !important}

and stamps the matching class onto each element, measuring its shipped size with getComputedStyle while it carries no stamp. A MutationObserver re-stamps as the interface mounts new nodes.

Stamping per element rather than using a universal rule is what keeps the two size axes independent: a html body * override would inherit into the conversation subtree and compound with the content size, and it would also rewrite decorative sizes (the SVG labels inside file-type icons) that nobody meant to scale. Measuring per element also means the plugin needs no knowledge of any component's class names, so it survives a UI refactor.

Where the conversation size is written

--dsh-content-font-size is an inline custom property on body, written by ui-layout from the sibling ui-theme namespace (whose own Settings row offers 12–17 px). An inline declaration outranks a stylesheet regardless of !important, so the plugin writes its value to exactly the same place:

document.body.style.setProperty('--dsh-content-font-size', `${contentSize}px`)

The stylesheet then re-derives the whole Markdown ladder in absolute px from that value. ui-theme still owns the variable and still writes it on every theme change; the plugin re-asserts its own on every settings change. The row's help text tells the user this control wins over the Font size row in Appearance.

Pre-paint

The Node half answers webserver/index-inject with an inline `` that installs the stylesheet and sets the two family variables before the shell mounts, so the first frame is already in the user's fonts. It reads the same ui-font settings section at render time, and falls back to the schema defaults when the settings provider is absent.

Development

npm run build     # src/client.js -> lib/client.js
npm run check     # the release gate: bundle in sync, host and client verified
npm run check:all # adds the profile-composition check (needs a local dsh)
npm run verify    # just the host and client verifiers
npm run watch     # rebuild on save, for dsh-client-hmr

src/client.js is the single source of truth for the browser half. It is written as an ES module for readability, but DSH client bundles are classic scripts that may only register a lazy CommonJS factory through window.__ModuleLoader__ — so scripts/build-client.mjs applies that envelope and rewrites the static imports into require calls. It is deliberately narrow and fails the build on anything it cannot rewrite, because a hand-rolled client bundle has no bundler to catch a mistake.

The only modules a browser half may request are the nine the shell seeds into its module table (react, react/jsx-runtime, react-dom, react-dom/client, @deepseek-ai/cordis, @deepseek-ai/dsh-client-store, @deepseek-ai/dsh-client-ui-slots, @deepseek-ai/dsh-client-ui-primitives, @deepseek-ai/dsh-client-ui-dockkit); anything else must be declared in dsh.client.external and shipped as its own graph row. The build enforces this.

verify-profile.mjs needs a dsh installation and an initialized profile, so it skips (exit 0) when neither is present — a clean CI runner has no dsh. Set DSH_REQUIRE=1 to turn that skip into a failure.

To iterate on the browser half against a running host, run the watcher and let dsh-client-hmr swap the plugin in — it stat-polls client bundles every 500 ms, so a saved rebuild reaches the open page without a refresh:

# terminal 1
npm run watch

# terminal 2
dsh --profile web

A page refresh always picks up a newly composed graph, so the watcher is a convenience rather than a requirement.

Releasing

Releases are staged by GitHub Actions over npm trusted publishing and then approved by a maintainer, so no NPM_TOKEN exists in this repository and none should ever be added. RELEASING.md is the step-by-step runbook; PUBLISHING.md covers the one-time npm setup and what the arrangement does and does not protect against.

Package layout

PathRole
lib/index.jsHost half: settings namespace, pre-paint injection. Loaded by the loader.
lib/client.jsBrowser half, generated from src/client.js. Served at /plugins/@citisen/dsh-font/client.js.
src/client.jsBrowser-half source.
cordis.patch.ymlThe profile layer this bundle contributes.
scripts/Build and verification scripts.
.github/workflows/stage.ymlThe CI half of the only publishing path.
PUBLISHING.mdTrusted-publishing setup, and what it does not protect against.
RELEASING.mdThe runbook for shipping a change.
package.jsonDeclares dsh.bundle (profile layer) and dsh.client (browser roster entry).

Known limitations

  • Hard-coded sizes only. The interface scale only moves text that a shipped stylesheet fixes in px. It deliberately leaves display-size text and decorative glyph sizes alone, so a scale of 150% is not a uniform 1.5× of the whole UI.
  • The conversation size overrides ui-theme's font-size row. Both write --dsh-content-font-size; last write wins, and this plugin always writes on a settings change. Use one or the other.
  • Fonts are not installed. A family name is only used if the browser or OS can resolve it; the plugin does not bundle or download webfonts, so a typo falls back silently to the generic family at the end of the list.
  • Per-element stamping is proportional to the DOM. The scale pass measures each element once and caches the result, and re-stamps on mutation, so it is bounded — but it is not free on a very large transcript.
  • The settings row is English/Chinese only, matching the shipped locale pair.

License

MIT