dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DeepSeek Harness plugin: customize the Web GUI fonts (interface font, code font, and font sizes) from Settings
English | 中文
The three lines you are looking at are these —
Failed to load plugins
web boot: 3 entries did not activate
@citisen/dsh-font: pending (waiting for service: settingsScope)
An enabled plugin row that never activates is a failed boot, not a warning: dsh refuses
to finish starting rather than loading without the plugin. Three ways out, fastest
first, and all three work while dsh cannot start. This plugin's row in the profile is
id: ui-font, for name: '@citisen/dsh-font'.
1. Disable it — one entry in the profile's own patch layer
($DSH_HOME/profiles/web/cordis.patch.yml, applied after every bundle layer):
- id: ui-font
disabled: true
No command, no network, nothing to install, and deleting those two lines brings the row
back. dsh --profile web --dump-config prints the composed tree — every row's id and
package name, whoever it belongs to — and marks this row disabled: true once the patch
takes effect; it loads no plugin, so it works while dsh cannot start.
2. One boot only, changing nothing — put the same two lines in a file of your own and pass it as an overlay:
dsh --profile web --patch ./no-font.yml web
3. Remove it — one command drops the dependency and the bundle layer
(dsh.profile.bundles is reconciled against what is installed). It forwards to pnpm in
the profile directory and never composes the profile, so it runs while dsh cannot start:
dsh plugin --profile web remove @citisen/dsh-font
It needs pnpm on PATH; without it, delete the package name from
dsh.profile.bundles (and the matching dependencies entry) in
$DSH_HOME/profiles/web/package.json by hand.
Or just get a working dsh now: boot a clean profile from the shipped template, which carries none of your bundles:
dsh --profile rescue --from-default-profile web
This build runs on both dsh lines: the 0.1.5-rc.x line (today's latest and
next) and 0.1.7-alpha.1, whose settings model it also speaks. One string names
this plugin's section on both — ui-font — because the 0.1.7 line keys settings by
Loader entry id and this bundle's patch inserts the entry under that name, while the
0.1.5 line registers the same name as a settings namespace.
| what it reads | 0.1.5-rc.x | 0.1.7-alpha.1 |
|---|---|---|
| the durable section | settingsScope.bind({ namespace: 'ui-font' }) | configForms.get('ui-font'), over the entry's own Config |
| the host contract | settings.register('ui-font', schema) | the exported Config, its fields marked .volatile() |
Both are bound optionally, so a dsh that provides neither still activates: the plugin
never sits pending — which blocks the boot outright — and never throws on activation.
It runs on the shipped defaults, and the first control you touch says why nothing is
saved. Up to 0.2.2 the plugin required the 0.1.5 service, so on 0.1.7-alpha.1 it was
reported as an entry that "did not activate"; 0.2.3 speaks both lines' settings, and
0.2.4 also stops it borrowing the design system's icons.
That last one is worth stating plainly, because it is the failure mode a settings port
hides. The design system's icon exports are named for their stroke weight and were
renamed between dsh lines (IconChevronDownOutline14 on 0.1.5-rc.x,
IconChevronDownOutlineRegular on 0.1.7). An icon whose name is gone does not render as
a missing picture: it arrives as undefined in place of a component type, so React
throws "Element type is invalid" while rendering the row and the whole Fonts row
disappears from the settings page, leaving one console error to explain it. The row
borrows no icon now, and draws none of its own either — it is two text boxes, two number
boxes, and a button — which is why its client bundle asks the shell for nothing but
react and @deepseek-ai/dsh-client-store.
0.3.1: the conversation text size is goneMessage bodies, headings, and tables are sized by dsh's own Appearance → Font size row
again (ui-theme writes the inline --dsh-content-font-size the shipped ladder reads).
0.3.1 stopped writing that property and stopped re-deriving the Markdown ladder, so the
Fonts row offers no conversation-size control at all and contentFontSize has left the
schema — if an older version of this plugin stored it, delete that key from
$DSH_HOME/settings.yaml or the profile's cordis.patch.yml: dsh validates the section
against this entry's own schema and refuses the whole section over a single unknown key.
dsh 0.1.7 imports a legacy $DSH_HOME/settings.yaml once — each section into the entry
of the same id — and renames the file to settings.yaml.imported. Before
0.2.4 this plugin's entry was named font, so a ui-font section had
nowhere to go and stayed only in the renamed file. The names match now, so dsh's own
import can put those values back:
$DSH_HOME/settings.yaml.imported to $DSH_HOME/settings.yaml (a copy, not a
move — the file is the only record until the import runs), and keep only the keys the
entry still declares: dsh validates the section against that entry's schema and refuses
the whole section over a single unknown key, so drop every key the table above does not
list.ui-font among them — is written into that profile's Cordis patch.settings.yaml.imported, so keep that file until you have what you need out of
it.Customize the DeepSeek Harness Web GUI fonts from Settings: the interface and code font stacks (each with its weight) and two 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.
A Fonts row in Settings → General: two font-query fields, each with its own size box sitting after the query input.
| Field | Effect | Value |
|---|---|---|
| Interface font | --dsw-font-family — all non-code UI text | a font query: families, optionally with a weight |
| Interface text size | the box beside it: scales every hard-coded UI text size | 75 – 150 (percent) |
| Code font | --ds-font-family-code — code blocks, inline code, monospace | a font query |
| Code text size | the box beside it: code blocks and inline code | 10 – 20 px |
The size is a native number input rather than a slider: type it, step it with the arrow
keys or the browser's own spinner, and read the unit (% or px) inside the box. Only a
complete, in-range number is written — a half-typed 1 on the way to 125 is not an
instruction, so it neither writes a bad value nor gets re-rendered away — and leaving the
box puts it back to what is stored, so a number that was refused cannot stay on screen
looking as if it had been taken.
The conversation text size is not here. Message bodies, headings, and tables have
always been sized by dsh's own Settings → Appearance → Font size row (the ui-theme
namespace); 0.3.1 removed the second control this row used to offer for it, and the
plugin writes no --dsh-content-font-size of its own.
Both font fields are editors for a small font-query language, not plain text inputs: the whole stack is one piece of text you edit directly, with syntax colouring, soft wrapping, and a completion list built from the fonts actually on your machine. The weight is written in the query, next to the family it belongs to, so changing a font and its weight is one edit in one place:
Geist Mono medium, "Zhuque Fangsong (technical preview)", monospace
└─────┬────┘ └──┬─┘
family weight
Inter, typed by hand, or a pick from the completion list at the
start of an entry.-> in Fira Code)
would draw one glyph in the layer and two in the field, and a synthesized
weight would differ between them. The readout line under the field is rendered
in the axis's own font, so the family you picked is still visible there.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 Code text size axes are independent on purpose: bumping the interface makes the surrounding chrome easier to read without moving a line of message text, and the code size touches neither. Conversation text follows dsh's own setting alone.
Geist Mono mediumCSS cannot put a weight in a font-family — font-family: "Geist Mono" 500, monospace is simply an invalid declaration, and the whole stack would be
dropped. The query language therefore writes the weight as a word beside the
family, and the plugin reads the pair back into the two declarations that
actually apply:
Geist Mono medium, monospace → font-family: "Geist Mono", monospace
→ font-weight: 500
Thin, ExtraLight, Light, Book, Regular, Medium, SemiBold, Bold,
ExtraBold, Black and their usual aliases (Hairline, DemiBold,
UltraBold, …) are all recognized, case-insensitively. The weight belongs to the
axis rather than to one family, so it is honoured wherever it is written and
counted once; the canonical form puts it after the first family — the one in
effect.
The shipped weight is implicit: an axis at its factory 400 carries no word at
all, because a regular that appears after every family is noise nobody asked
for — and for the interface axis 400 literally means "no override". A word
appears exactly when a weight is chosen, and deleting it returns the axis to 400.
What the axis currently stands at is always stated in the line under the field
(Interface weight: Regular 400 (shipped, not overridden)), so nothing is hidden
by leaving the word out.
The weight is a closed list rather than a free number, because a font-weight
the chosen family does not have is synthesized by the browser, and offering
faux-bold as a normal choice is worse than not offering it. When the faces could
be read — the Local Font Access API reports them — the popup offers that
family's weights, and a weight the family lacks is called out below the field:
Geist Mono has no 700 face on this machine (bold), so the browser will synthesize it
Deleting the word does not quietly reset anything: the axis keeps the weight it
was set to, the line under the field states it, and the word comes back the next
time the row is built from the stored values. To go back to the shipped weight,
pick its row from the list (Regular 400) — a pick is an instruction, so it sets
the value even though its text is just the family name.
One deliberate escape hatch: if what you type is itself a family on your machine,
the trailing word is read as part of the name. Book Antiqua and
Franklin Gothic Medium are real families, and applying a weight to them instead
of picking them would be silent misbehaviour. The price is that a name which is
genuinely both — a family literally called Geist Mono Medium — is also read as
a name. The catalogue decides, so the judgement uses the same font list the
completion list shows you. Quoting is the way out: "Book Antiqua" always means
the name, verbatim.
The code surface has no weight hierarchy to preserve, so its weight is simply the one written in the query. The interface does have one, which is why the interface weight is a single inherited base rather than a rewrite of every step:
/* --dsh-font-ui-weight: 500 */
html body { font-weight: 500 }
Text that inherits its weight moves; anything whose weight the design system fixes
— headings, table heads, buttons — keeps it, because its own rule still wins. The
conversation's font: shorthands fix theirs too, and this plugin no longer restates
that ladder, so message text is not rewritten either. And the shipped 400 emits no
rule at all, so an untouched install paints exactly what the design system paints.
query := entry ("," entry)*
entry := family | weight
family := '"' … '"' | "'" … "'" | word (space word)*
weight := thin | extralight | light | book | regular | roman | medium |
demibold | semibold | bold | extrabold | black | heavy | …
The grammar is the CSS font-family list plus the one thing the list cannot
carry. Only the LAST word of an unquoted entry may be a weight, and only when the
whole entry is not itself a catalogued family. A bare weight word (medium) sets
the axis weight without naming a family. Commas inside quotes are not separators,
so "Foo, Bar" is one family.
The reader is forgiving on purpose — this text is typed by hand, not generated — so anything it cannot place is kept as written and reported under the field instead of being dropped: an unclosed quote, a stray word after a quoted name, a weight written twice, a family the machine does not list, a weight the family has no face for. The field also names the family that is in effect, which is the first one the browser can actually resolve.
Order matters in a CSS font stack — the first installed family wins — so the editor is built around the text rather than around a picker.
Reading the real installed-font list needs the
Local Font Access API
(queryLocalFonts), which is Chromium-only, experimental, and
permission-gated; MDN also notes browsers are not obliged to return the
complete list. So the catalogue is layered:
queryLocalFonts() when the browser offers it — the machine's own list,
which needs no badge to explain itself. Its per-face style names (Regular,
SemiBold, Bold Italic) are kept alongside the families, which is what lets
the completion list offer a family's real weights and warn about one it lacks.
The first use shows a permission prompt; declining falls back silently rather
than erroring.monospace baseline; a different width or
height means it is installed. No permission needed, works in every browser.
It only sees the curated probe list, so it under-reports by design and can say
nothing about weights — which is the one case worth calling out, so the row
then says so once, above the fields, and adds that any family can still be
typed.The result is cached for the session, and discovery only runs when the row is actually rendered — never at startup.
The editor also warns when a query has no generic family at the end (such as
sans-serif), because a missing font then falls back unpredictably. Quoting is
automatic: Fira Code is stored as "Fira Code", and generic keywords are
deliberately left unquoted, so "sans-serif" can never be written by accident.
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 <profile> after installing.
dsh plugin --profile web add <path> 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
# <profile>/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.
Read this section before changing the code; the two halves solve different problems and the split is deliberate.
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.
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 size dsh gives message text, 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.
--dsh-content-font-size is an inline custom property on body, written by
ui-layout from the sibling ui-theme namespace — the Font size row in
Appearance. An inline declaration outranks a stylesheet regardless of !important,
so taking that axis over meant writing to exactly the same place, and re-deriving
the whole Markdown ladder on top of it:
document.body.style.setProperty('--dsh-content-font-size', `${contentSize}px`)
The plugin no longer writes either, and no longer declares any --dsh-font-markdown-*
token. Conversation text is sized, and its line-height derived, entirely by the
design system from the user's setting in Appearance; this plugin keeps two axes, the
interface scale and the code size, and each one stays inside its own subtree.
The design system has no weight token at all — every font: declaration in
the interface is a literal, and the code ladder is literally 400. So the
weight cannot ride the family token the way a family does, and it is applied on
two levels:
/* the three code tokens carry it in their font: shorthand … */
--dsw-font-markdown-code: var(--dsh-font-code-weight,400) var(--dsh-font-code-size,12px) / … ;
/* … and everything styled directly with the code family is matched structurally */
html body pre, html body code, html body [class*="code" i] {
font-family: var(--ds-font-family-code) !important;
font-weight: var(--dsh-font-code-weight,400) !important;
}
The second rule exists because most code in the interface never reads a token:
the tool I/O cards and the terminal output are styled with font-family: var(--ds-font-family-code) and their own literal weight inside a component
stylesheet, so there is nothing to override but the element. The !important is
what outranks font: 500 12px/18px …. It is deliberately not a universal
rule: the surrounding labels keep the shipped hierarchy.
The interface weight is opt-in, so the shipped 400 emits no rule at all and an
untouched install paints exactly what the design system paints. Anything else is on