dsh-custom-theme 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +479 -0
- package/cordis.patch.yml +14 -0
- package/lib/client.js +1512 -0
- package/package.json +50 -0
- package/src/index.mjs +289 -0
- package/src/themes.mjs +121 -0
- package/themes/gov.css +53 -0
- package/themes/monokai-pro.css +39 -0
- package/themes/one-dark.css +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sparrived
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
# dsh-custom-theme
|
|
2
|
+
|
|
3
|
+
User-editable CSS themes for the DeepSeek Harness Web GUI and Desktop app.
|
|
4
|
+
|
|
5
|
+
This ports the one UI-customization capability DSH does not have: a theme
|
|
6
|
+
**directory** a user can edit or extend. DSH ships `light`, `dark` and `system`,
|
|
7
|
+
plus an in-process `ctx.theme.register()` seam for code-authored token
|
|
8
|
+
overrides, but it has no way for a user to supply CSS.
|
|
9
|
+
|
|
10
|
+
## What it does
|
|
11
|
+
|
|
12
|
+
| Half | File | Owns |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| Host | `src/index.mjs` | `$DSH_HOME/themes/` and `$DSH_HOME/backgrounds/` — seeds `gov`, `monokai-pro` and `one-dark`, re-syncs them when the seed generation advances, serves both directories over `/dsh-custom-theme/` |
|
|
15
|
+
| Browser | `lib/client.js` | Its own settings page (**主题与背景**), registered into `settings.section` with theme, colour-scheme, conversation-stream, working-text and background controls. A theme's tokens go to the official theme runtime as an override layer and its base palette to `ctx.theme.setTheme`; the `<style>` element carries only its non-token rules. Backgrounds are written as inline `!important` properties on the painted surface plus one injected rule per picture layer, since a `::before` layer cannot be styled inline |
|
|
16
|
+
|
|
17
|
+
Served routes:
|
|
18
|
+
|
|
19
|
+
| Route | Response |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `GET /dsh-custom-theme/themes` | `{ themes: [{ id, bundled }], dir }` |
|
|
22
|
+
| `GET /dsh-custom-theme/theme/<id>.css` | The stylesheet text |
|
|
23
|
+
| `GET /dsh-custom-theme/backgrounds` | `{ backgrounds: [{ name, url }], dir }` |
|
|
24
|
+
| `GET /dsh-custom-theme/background/<name>` | The image bytes |
|
|
25
|
+
|
|
26
|
+
Adding a theme is dropping a `.css` file into the theme directory and pressing
|
|
27
|
+
**Rescan**; the file only needs to override `--dsw-alias-*` custom properties.
|
|
28
|
+
Seeded ids come first in the picker, extra ids after them in alphabet order.
|
|
29
|
+
|
|
30
|
+
The three seeded files are managed: they are written when the directory is first
|
|
31
|
+
created and rewritten when the bundled palettes change, so an edit inside
|
|
32
|
+
`gov.css` will not survive that. Put a customised palette in a copy under its own
|
|
33
|
+
name — any id other than the three is never touched.
|
|
34
|
+
|
|
35
|
+
```css
|
|
36
|
+
/* $DSH_HOME/themes/my-theme.css */
|
|
37
|
+
:root {
|
|
38
|
+
--dsw-alias-bg-base: #1b1d23;
|
|
39
|
+
--dsw-alias-brand-primary: #7aa2f7;
|
|
40
|
+
--dsw-alias-label-primary: #c0caf5;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A theme states its **palette**, not its `!important` fights: the plugin reads the
|
|
45
|
+
token declarations out of the stylesheet and hands them to the official theme
|
|
46
|
+
runtime as a token override layer, so the presenter applies them the same way it
|
|
47
|
+
applies its own palette. `!important` on a token declaration is therefore
|
|
48
|
+
unnecessary — the plugin de-escalates it on the way in, so a leftover one from an
|
|
49
|
+
older theme is harmless.
|
|
50
|
+
|
|
51
|
+
### The working text
|
|
52
|
+
|
|
53
|
+
While a turn runs, that label is `chat.deepDiving` (「深度求索中」) from the shell's
|
|
54
|
+
`chat` namespace. `ctx.locale.register` throws for a namespace and locale pair that
|
|
55
|
+
already exist — there is no override layer — and no slot carries the label on its
|
|
56
|
+
own, so replacing the row is the only way to reword it.
|
|
57
|
+
|
|
58
|
+
The replacement is **opt-in**. It registers `conversation.chat.node` / key
|
|
59
|
+
`turn-process` at `priority: -1` (the lowest live entry renders), and only when at
|
|
60
|
+
least one phrase is configured; with an empty list the shipped row is left exactly
|
|
61
|
+
in place. Opting in rather than replacing by default is deliberate:
|
|
62
|
+
|
|
63
|
+
- A plugin cannot render the official component — it is not exported — so a default
|
|
64
|
+
replacement would mean reproducing a shell build this plugin cannot read.
|
|
65
|
+
- The installed shell is not the published source. Reading the live row showed its
|
|
66
|
+
finished label carries an elapsed time and reads 「已完成,用时 …」, and its label
|
|
67
|
+
font size follows the primary content size; the vendored sources say otherwise on
|
|
68
|
+
both counts. A copy would have missed both, silently.
|
|
69
|
+
- The translate seat a replacement row receives does not interpolate parameters: a
|
|
70
|
+
template carrying a placeholder comes back with the slot empty. The replacement
|
|
71
|
+
therefore uses parameter-free keys only, and does not re-attach an elapsed time.
|
|
72
|
+
|
|
73
|
+
Fidelity is guarded by `test/browser/working-row.mjs`, which captures the shipped
|
|
74
|
+
row's computed geometry before a phrase is configured and compares the replacement
|
|
75
|
+
against it property by property.
|
|
76
|
+
|
|
77
|
+
### The settings page
|
|
78
|
+
|
|
79
|
+
The controls live on a settings page of their own rather than as a card inside
|
|
80
|
+
General. That is a single `settings.section` registration: it creates both the nav
|
|
81
|
+
row and the panel. The row sits at `order: 30`, after the built-in sections, and
|
|
82
|
+
its label is the `nav` key of this plugin's locale namespace, re-read on every
|
|
83
|
+
projection so it follows a locale change. The shell chooses the nav glyph from the
|
|
84
|
+
entry id and falls back to a generic settings gear for an id it does not know, so
|
|
85
|
+
the page cannot supply its own icon.
|
|
86
|
+
|
|
87
|
+
The shell renders one settings section at a time, which would put the official
|
|
88
|
+
appearance row out of sight while this page is open. The page therefore carries
|
|
89
|
+
its own light/dark/system control for the same preference. Both it and the
|
|
90
|
+
official row write through `ctx.theme.setTheme`, and this control follows
|
|
91
|
+
`theme/change`, so the two cannot disagree — and a theme with a light/dark pair
|
|
92
|
+
stays switchable without leaving the page.
|
|
93
|
+
|
|
94
|
+
### The conversation stream
|
|
95
|
+
|
|
96
|
+
| Control | Route |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| Text size | `ctx.theme.setFontSize(px)` — the official runtime's own preference, 12–17. The shell persists it, so this plugin writes it and reads it back from `ThemeSnapshot.fontSize`. |
|
|
99
|
+
| Line spacing | Adds px to `--dsh-content-font-delta`, the delta the shell derives from the font size and folds into every content line height. At 0 the shell's own value is left untouched. |
|
|
100
|
+
| Text font / code font | `--dsw-font-family` and `--ds-font-family-code`, picked from a preset list rather than typed: **跟随官方默认** (declare nothing), then the system, Microsoft YaHei, Noto Sans SC and Georgia stacks for text, and Cascadia Mono, JetBrains Mono and Sarasa Mono SC for code. A stack that is not one of them still shows up as its own option, so a value written by an earlier version is never silently reset. |
|
|
101
|
+
|
|
102
|
+
Two details worth keeping:
|
|
103
|
+
|
|
104
|
+
Line spacing rides the shell's delta rather than pinning an absolute
|
|
105
|
+
`line-height`, because every content surface computes its own base
|
|
106
|
+
(`calc(24px + delta)` on the assistant flow, `calc(22px + delta)` on the user
|
|
107
|
+
bubble) — one delta moves them together.
|
|
108
|
+
|
|
109
|
+
The declarations are emitted at raised specificity (`html:root` / `html body`)
|
|
110
|
+
rather than relying on source order. The shell installs its palette styles at
|
|
111
|
+
boot and may do so after this plugin runs, so an equal-specificity `:root` or
|
|
112
|
+
`body` rule would lose depending on who ran last.
|
|
113
|
+
|
|
114
|
+
There is no streaming-fade control. Nothing in the client renders streamed text as
|
|
115
|
+
per-chunk elements and no chunk timestamp reaches CSS, so a fade could only be
|
|
116
|
+
faked as a single mask animation per render — visibly wrong, so it is not offered.
|
|
117
|
+
|
|
118
|
+
### One palette, or a light/dark pair
|
|
119
|
+
|
|
120
|
+
A theme may carry a single palette, or one set per colour scheme:
|
|
121
|
+
|
|
122
|
+
```css
|
|
123
|
+
:root { --dsw-alias-bg-base: #f0f7ff; } /* light */
|
|
124
|
+
body[data-ds-dark-theme] { --dsw-alias-bg-base: #0a1520; } /* dark */
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Any dark selector the shell could be expected to write counts: `data-ds-dark-theme`
|
|
128
|
+
is what this shell sets on `body`, `[data-theme="dark"]` is the convention
|
|
129
|
+
Deeptop's own theme files use, and `@media (prefers-color-scheme: dark)` is read
|
|
130
|
+
the same way. All three bundled themes carry both sets, ported from the two-set
|
|
131
|
+
files they came from, so 浅色/深色 moves each of them between its own light and
|
|
132
|
+
dark palette.
|
|
133
|
+
|
|
134
|
+
With a pair the theme adapts: the official choice keeps deciding which set
|
|
135
|
+
applies, and the theme stays selected across the switch. A token declared in only
|
|
136
|
+
one of the two sets reaches both, so a pair may override as few or as many tokens
|
|
137
|
+
as it likes.
|
|
138
|
+
|
|
139
|
+
With a single palette the theme states one look, so the plugin also selects the
|
|
140
|
+
base palette that matches it — otherwise every token the theme does *not* override
|
|
141
|
+
keeps the colour of whichever scheme the user last picked, which is what leaves a
|
|
142
|
+
light theme with dark composer and menu surfaces. The scheme is the luma of
|
|
143
|
+
`--dsw-alias-bg-base`, read by handing the value to the browser, so a hex, a named
|
|
144
|
+
colour, `hsl()`, `oklch()`, `color-mix()` or a `var()` naming a shell token all
|
|
145
|
+
work. A directive overrides the reading when a theme wants to be explicit, and a
|
|
146
|
+
base colour the browser accepts but cannot reduce to a luma leaves the appearance
|
|
147
|
+
preference where the user put it rather than guessing:
|
|
148
|
+
|
|
149
|
+
```css
|
|
150
|
+
/* dsh:color-scheme dark */
|
|
151
|
+
:root {
|
|
152
|
+
--dsw-alias-bg-base: #101418;
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Selecting a single-palette theme therefore also moves the official appearance
|
|
157
|
+
preference, and like any other appearance choice that survives a restart. Moving
|
|
158
|
+
that preference to the other scheme afterwards **unloads the theme** instead of
|
|
159
|
+
keeping it: its layer carries one set of values for both modes, so letting the
|
|
160
|
+
base palette flip underneath it would leave a window split across the two — every
|
|
161
|
+
token the theme declares in one scheme's colours and everything it does not in the
|
|
162
|
+
other's. The page's selector falls back to its built-in entry to match the window.
|
|
163
|
+
|
|
164
|
+
## Background images
|
|
165
|
+
|
|
166
|
+
Drop an image into the background directory (`$DSH_HOME/backgrounds` by default),
|
|
167
|
+
press **Rescan**, and pick it in the **Background image** row. Served extensions
|
|
168
|
+
are `.png .jpg .jpeg .webp .gif .avif .bmp`, up to 16 MiB each.
|
|
169
|
+
|
|
170
|
+
Each of six zones holds its own image, picture opacity, blur, fit and position:
|
|
171
|
+
|
|
172
|
+
| Zone | Painted on |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| Whole app | The frame, and every zone below it |
|
|
175
|
+
| Title bar | The shell's header |
|
|
176
|
+
| Sidebar | The sidebar column |
|
|
177
|
+
| Conversation | The main column |
|
|
178
|
+
| Composer | The composer seat |
|
|
179
|
+
| Tool panel | The tool-panel column, while the dock is open |
|
|
180
|
+
|
|
181
|
+
The zones are the shell's own layout boxes, found by the stable part of their
|
|
182
|
+
CSS-module class names (`_frame`, `_sidebarCol`, `_centerCol`) and by `data-*`
|
|
183
|
+
hooks where the shell provides them (`[data-composer-seat]`, `[data-rightbar-col]`).
|
|
184
|
+
The picture is
|
|
185
|
+
**not** painted on that box directly: the shell paints the visible surface from a
|
|
186
|
+
component root nested a few levels below it, often under a zero-size wrapper, so
|
|
187
|
+
the plugin descends to the deepest opaque element covering the box and paints
|
|
188
|
+
that. Each painted surface is tagged `data-dct-zone`, which makes the target
|
|
189
|
+
visible in the inspector and gives the browser test something to assert on; the
|
|
190
|
+
picture layer itself is addressed by a generated `data-dct-layer`, because two
|
|
191
|
+
zones can resolve to the same surface and an element holds one value per attribute.
|
|
192
|
+
|
|
193
|
+
Three of those anchors — the header, the composer seat and the tool-panel column —
|
|
194
|
+
have a transparent background of their own and take their colour from an ancestor,
|
|
195
|
+
so the panel fill is built from the nearest opaque ancestor instead. Without that the
|
|
196
|
+
picture would show at full strength whatever the fill is set to.
|
|
197
|
+
|
|
198
|
+
**Image opacity** is the picture's own alpha, and it is a channel of its own: the
|
|
199
|
+
picture is painted on a separate layer at that alpha, so it no longer shares a
|
|
200
|
+
gradient with the panel fill. It is bounded to `0.05`–`0.45` with a default of
|
|
201
|
+
`0.18`, the same range and default Deeptop's model uses, so a picture can never
|
|
202
|
+
obscure the shell's own surfaces. Because the panel fill is read from the live
|
|
203
|
+
surface, the composite follows the active theme. A fully covered frame is
|
|
204
|
+
deliberately left unpainted — otherwise the same image would show twice through the
|
|
205
|
+
column fills and read stronger than configured.
|
|
206
|
+
|
|
207
|
+
**Blur** softens the picture alone, `0`–`16` px with a default of `0`, the range and
|
|
208
|
+
default Deeptop's model uses. It is applied to the picture layer, never to an
|
|
209
|
+
element that holds text, so the shell's content stays sharp at every setting. At `0`
|
|
210
|
+
the declaration is dropped entirely rather than written as `blur(0px)`.
|
|
211
|
+
|
|
212
|
+
**Panel fill** follows Deeptop's per-zone defaults: the whole-app frame stays fully
|
|
213
|
+
opaque, while the title bar (94%), sidebar (92%), conversation (91%), composer (91%)
|
|
214
|
+
and tool panel (92%) let the app backdrop show faintly through. The value is bounded
|
|
215
|
+
to 0–100 and only written below 100, so a zone at 100 keeps whatever alpha the
|
|
216
|
+
shell's own colour already had. It applies whether or not that zone has a picture,
|
|
217
|
+
and it is always read from the zone's own entry — the global entry supplies only the
|
|
218
|
+
picture, its alpha and its blur. The fill is built from the nearest **opaque**
|
|
219
|
+
ancestor colour, because a translucent one is either the shell's own panel fill or an
|
|
220
|
+
override this plugin wrote on an earlier pass, and neither is a stable basis.
|
|
221
|
+
|
|
222
|
+
### How the picture is stacked
|
|
223
|
+
|
|
224
|
+
The picture is **not** an element's `background-image`. It is a `::before` layer on
|
|
225
|
+
the painted surface, so the stack from the bottom reads: the shell's surface colour →
|
|
226
|
+
the panel fill → the picture at its own alpha, blurred if asked → the shell's own
|
|
227
|
+
content. Only a separate box can carry the picture's alpha and blur without fading or
|
|
228
|
+
smearing the text that shares the surface element, which is what makes the two
|
|
229
|
+
controls above possible at all.
|
|
230
|
+
|
|
231
|
+
Three declarations arrange that, all written by the plugin rather than assumed of the
|
|
232
|
+
shell:
|
|
233
|
+
|
|
234
|
+
- `::before { position: absolute; inset: 0; z-index: -1 }` sizes the layer to the
|
|
235
|
+
surface and puts it under the shell's content. A layer at `z-index: 0` or `auto`
|
|
236
|
+
would be a positioned element drawn **over** the shell's normal-flow content and
|
|
237
|
+
would cover the text.
|
|
238
|
+
- `isolation: isolate` on the surface creates the stacking context that keeps it
|
|
239
|
+
there. Without it the layer's `z-index: -1` escapes to the nearest ancestor
|
|
240
|
+
stacking context and can be hidden behind a background painted at that level. It
|
|
241
|
+
creates a stacking context without setting a `z-index` and without affecting
|
|
242
|
+
layout, so the shell's own layering is left alone.
|
|
243
|
+
- `position: relative`, written **only when the surface is `static`**, gives the
|
|
244
|
+
absolutely positioned layer something to be laid out against. See the risk note
|
|
245
|
+
below.
|
|
246
|
+
|
|
247
|
+
The plugin writes only inline `!important` declarations and injected layer rules, and
|
|
248
|
+
removes exactly the properties, attributes and rules it added, so switching zones or
|
|
249
|
+
clearing a zone restores the shell's own styling — including the layer stylesheet,
|
|
250
|
+
which is emptied on every pass.
|
|
251
|
+
|
|
252
|
+
## Install
|
|
253
|
+
|
|
254
|
+
This package is a DSH **bundle**: it declares `dsh.bundle.patch`, so installing it
|
|
255
|
+
into a profile contributes the `custom-theme` row. The Host plugin and the browser
|
|
256
|
+
half ride that one row, so one install brings up both halves — there is nothing to
|
|
257
|
+
enable separately.
|
|
258
|
+
|
|
259
|
+
Every path below ends with `dsh plugin --profile <name> remove dsh-custom-theme`,
|
|
260
|
+
which removes both the dependency and the layer.
|
|
261
|
+
|
|
262
|
+
### From a local checkout
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
dsh plugin --profile <name> add /path/to/dsh-custom-theme # links the checkout
|
|
266
|
+
dsh --profile <name> --dump-config # shows a "# == dsh-custom-theme" layer
|
|
267
|
+
dsh --profile <name> --no-open
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### From GitHub
|
|
271
|
+
|
|
272
|
+
```sh
|
|
273
|
+
dsh plugin --profile <name> add github:Sparrived/dsh-custom-theme
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Pin a commit when you want a later push to be unable to change what runs:
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
dsh plugin --profile <name> add github:Sparrived/dsh-custom-theme#<sha>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
A git install fetches **source, not build artifacts**, which is the step where a
|
|
283
|
+
TypeScript plugin arrives without its compiled `lib/` and fails to load — the
|
|
284
|
+
official guide's *"installing from GitHub: the build-script catch"* section is
|
|
285
|
+
about exactly that case, and its fix is a `prepare` script plus an `allowBuilds`
|
|
286
|
+
grant. **This package does not need either.** `src/index.mjs` (Host) and
|
|
287
|
+
`lib/client.js` (browser) are hand-written JavaScript that Node and the browser
|
|
288
|
+
load directly, so there is no build step for `prepare` to run and nothing to
|
|
289
|
+
allowlist: the install completes with no code-execution prompt.
|
|
290
|
+
|
|
291
|
+
### From a tarball, or from npm
|
|
292
|
+
|
|
293
|
+
Both ship the same prebuilt code:
|
|
294
|
+
|
|
295
|
+
```sh
|
|
296
|
+
pnpm pack # -> dsh-custom-theme-0.1.0.tgz
|
|
297
|
+
dsh plugin --profile <name> add ./dsh-custom-theme-0.1.0.tgz
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```sh
|
|
301
|
+
dsh plugin --profile <name> add dsh-custom-theme # after an npm release
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
For the maintainer, publishing a release is:
|
|
305
|
+
|
|
306
|
+
```sh
|
|
307
|
+
git tag v0.1.0 && git push origin v0.1.0
|
|
308
|
+
gh release create v0.1.0 --title v0.1.0 --notes-file CHANGELOG.md
|
|
309
|
+
pnpm publish --access public # optional; needs npm auth
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Configuring the directories
|
|
313
|
+
|
|
314
|
+
`themesDir` and `backgroundsDir` can be overridden on the row; otherwise
|
|
315
|
+
`$DSH_HOME/themes` and `$DSH_HOME/backgrounds` are used:
|
|
316
|
+
|
|
317
|
+
```yaml
|
|
318
|
+
- id: custom-theme
|
|
319
|
+
name: 'dsh-custom-theme'
|
|
320
|
+
config:
|
|
321
|
+
themesDir: D:/themes/dsh
|
|
322
|
+
backgroundsDir: D:/wallpapers/dsh
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Desktop app
|
|
326
|
+
|
|
327
|
+
Install from the **Plugins** page, which uses Desktop's bundled pnpm and is the GUI
|
|
328
|
+
equivalent of `dsh plugin --profile desktop`; point it at the checkout or the
|
|
329
|
+
tarball, then restart the app. Booting the `desktop` profile from the CLI is refused
|
|
330
|
+
by design — *"profile desktop is managed exclusively by the Electron application"* —
|
|
331
|
+
so the Plugins page owns that profile.
|
|
332
|
+
|
|
333
|
+
### Local development without installing
|
|
334
|
+
|
|
335
|
+
A `--patch` overlay row pointing at the source file is enough, because the client
|
|
336
|
+
module system resolves the owning `package.json` by walking up from the entry
|
|
337
|
+
file — the browser half attaches even though the row is a `file://` specifier:
|
|
338
|
+
|
|
339
|
+
```sh
|
|
340
|
+
dsh --profile <name> --patch dev.overlay.yml --no-open
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`dev.overlay.yml` names `./src/index.mjs`, a path the loader resolves against the
|
|
344
|
+
overlay file itself, so a fresh clone works without editing it.
|
|
345
|
+
|
|
346
|
+
The browser test drives a real browser against a booted instance, so it needs the
|
|
347
|
+
token from the printed URL:
|
|
348
|
+
|
|
349
|
+
```sh
|
|
350
|
+
set DCT_TOKEN=<token from the "dsh web: http://…/?token=…" line>
|
|
351
|
+
node test/browser/appearance.mjs
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`DCT_BASE` (default `http://127.0.0.1:3080`), `DCT_CDP_PORT` and `DCT_SHOT_DIR`
|
|
355
|
+
override the target, the debugging port and where screenshots land. The test
|
|
356
|
+
imports `test/browser/driver.mjs`, a small CDP driver over Node's built-in
|
|
357
|
+
`WebSocket`; no browser automation dependency is installed.
|
|
358
|
+
|
|
359
|
+
## Verify
|
|
360
|
+
|
|
361
|
+
Verified against `dsh` 0.2.0-rc.2 on Windows:
|
|
362
|
+
|
|
363
|
+
- `node --test "test/**/*.test.mjs"` — 14 tests, all passing: id and image-name
|
|
364
|
+
whitelists, ordering, directory resolution, seeding, re-sync on a new seed
|
|
365
|
+
generation, both asset routes, both listings, and traversal, extension and
|
|
366
|
+
method rejection.
|
|
367
|
+
- `node test/browser/appearance.mjs` — 33 steps in a real headless Edge, all
|
|
368
|
+
passing. It boots the app, asserts the controls are absent from the chat view and
|
|
369
|
+
still absent once Settings opens, then opens the plugin's own page from the nav
|
|
370
|
+
and drives it. It asserts on rendered state: each bundled theme paints its light
|
|
371
|
+
set and then follows the page's colour-scheme control into its dark set without
|
|
372
|
+
being dropped, the empty option restores a built-in token, a saved theme and
|
|
373
|
+
background both re-apply on boot before Settings is opened. For backgrounds it
|
|
374
|
+
checks that the picture really is on the layer and not on the surface element, that
|
|
375
|
+
the layer's own alpha is the configured picture opacity while the panel fill stays
|
|
376
|
+
at its per-zone percentage, that a blur lands on the picture layer and nowhere a
|
|
377
|
+
text-bearing element could inherit it, that both new controls clamp at Deeptop's
|
|
378
|
+
ceilings, and that clearing a zone removes its inline properties, its attributes and
|
|
379
|
+
every injected layer rule. Finally, with the panel closed, since the panel is
|
|
380
|
+
portalled over the whole window, it asserts that the image is what
|
|
381
|
+
`elementFromPoint` actually finds at a point inside each zone — which is what keeps
|
|
382
|
+
the negative-`z-index` layer from silently disappearing behind a surface.
|
|
383
|
+
- `node test/browser/working-row.mjs` — 5 steps against a session that already has
|
|
384
|
+
turns. It reads the shipped row's computed geometry, asserts the replacement is
|
|
385
|
+
absent while no phrase is configured, configures one, then compares the
|
|
386
|
+
replacement against the captured geometry property by property and asserts no
|
|
387
|
+
placeholder leaked into its label.
|
|
388
|
+
- A theme stating one palette was driven by hand across the official schemes: the
|
|
389
|
+
plugin drops it rather than half-applying it, which no bundled theme exercises
|
|
390
|
+
because all three carry a pair. That path is covered by the host tests only
|
|
391
|
+
through the seeding and serving assertions.
|
|
392
|
+
- Composed into a real profile (`--dump-config`), then booted: the row loads,
|
|
393
|
+
`$DSH_HOME/themes/` is seeded, `GET /dsh-custom-theme/themes` returns `200`
|
|
394
|
+
with `{"themes":[{"id":"gov","bundled":true},…]}`, `GET /dsh-custom-theme/theme/gov.css`
|
|
395
|
+
returns `200 text/css`, an unknown id returns `404`, and
|
|
396
|
+
`GET /dsh-custom-theme/backgrounds` plus the image route serve a dropped-in
|
|
397
|
+
`.png` as `image/png` while `evil.txt` and `missing.png` return `404`.
|
|
398
|
+
- The browser half composes into `window.__DSH_BOOT__` as
|
|
399
|
+
`{"id":"dsh-custom-theme","url":"plugins/??dsh-custom-theme/client.js&rev=…","rev":"…","inject":["@deepseek-ai/dsh-client-ui-settings"]}`,
|
|
400
|
+
and that URL serves `200 text/javascript` containing the
|
|
401
|
+
`settings.section` registration. The `inject` field is
|
|
402
|
+
`dsh.client.inject` reaching the boot graph.
|
|
403
|
+
- Both halves were driven from a `--patch` row as well as a bare-package row,
|
|
404
|
+
and `config.themesDir` was confirmed to redirect the directory: dropping a
|
|
405
|
+
`solarized.css` into it made the listing return
|
|
406
|
+
`{"id":"solarized","bundled":false}` and the stylesheet serve `200 text/css`.
|
|
407
|
+
|
|
408
|
+
Still to confirm by hand, in the running app: the Appearance rows render in
|
|
409
|
+
Settings → General, selecting a theme repaints immediately, and disabling the
|
|
410
|
+
row removes the rows and the palette together.
|
|
411
|
+
|
|
412
|
+
## Known limitations
|
|
413
|
+
|
|
414
|
+
- **A configured phrase drops the elapsed time on finished turns.** The seat a
|
|
415
|
+
replacement row receives does not interpolate parameters, so the shipped
|
|
416
|
+
`{duration}` template would render with an empty slot; the replacement uses only
|
|
417
|
+
parameter-free keys instead. Leave the phrase list empty to keep the shipped row,
|
|
418
|
+
elapsed time included.
|
|
419
|
+
- **The running phrase itself is verified by hand.** It only renders while a turn is
|
|
420
|
+
live, which the browser suite does not start; the suite proves the opt-in swap, the
|
|
421
|
+
geometry match and the finished-turn label. The rotation is driven by a
|
|
422
|
+
`setInterval` on the configured interval.
|
|
423
|
+
|
|
424
|
+
- **The page cannot choose its nav icon.** `settings.section` has no icon field;
|
|
425
|
+
the shell maps the entry id to a glyph and falls back to a generic settings gear
|
|
426
|
+
for an id it does not know, which is what this page gets.
|
|
427
|
+
- **One settings section renders at a time.** The official appearance row is on
|
|
428
|
+
the General section, so it is off-screen while this page is open. The page's own
|
|
429
|
+
colour-scheme control covers that case; it drives the same preference.
|
|
430
|
+
- **Which custom theme is selected lives in `localStorage`.** The tokens and the
|
|
431
|
+
base palette go to the official runtime, but the official preference field
|
|
432
|
+
accepts only the three built-in ids, so `setTheme` does not persist a custom
|
|
433
|
+
selection and the plugin restores it from `localStorage` on boot. Durable
|
|
434
|
+
cross-device persistence would need a Schemastery `Config` on the Host row plus
|
|
435
|
+
`ctx.configForms`.
|
|
436
|
+
- **A theme colours the whole window, buttons and menus included.** There is no
|
|
437
|
+
longer a surface a token theme cannot reach: the tokens ride the official
|
|
438
|
+
runtime, and the base palette follows the theme — for a pair, by the appearance
|
|
439
|
+
preference choosing one of the theme's two sets; for a single palette, by the
|
|
440
|
+
plugin selecting the set that palette belongs to. Reducing the theme's claimed
|
|
441
|
+
coverage is not possible by accident — ask for fewer tokens and you get the
|
|
442
|
+
base palette for the rest.
|
|
443
|
+
- **Background zones bind to the shell's DOM skeleton.** The six zone anchors are
|
|
444
|
+
found by the stable half of the shell's CSS-module class names (`_frame`,
|
|
445
|
+
`_sidebarCol`, `_centerCol`) and, where the shell offers one, by a `data-*` hook
|
|
446
|
+
(`[data-composer-seat]`, `[data-rightbar-col]`, and `header` for the title bar).
|
|
447
|
+
Every class the shell hashes keeps its authored name as a suffix, so a rebuild
|
|
448
|
+
does not move them; only renaming those classes or restructuring the layout
|
|
449
|
+
would. `test/browser/appearance.mjs` asserts that each zone paints with the
|
|
450
|
+
global image, so a moved anchor breaks loudly in the test rather than silently
|
|
451
|
+
for a user.
|
|
452
|
+
- **A painted surface is given `position: relative` when it was `static`.** The
|
|
453
|
+
picture layer is absolutely positioned, so a `static` surface has to become a
|
|
454
|
+
positioned one for the layer to be laid out against it. That changes the
|
|
455
|
+
containing block for any absolutely positioned descendant the shell has inside
|
|
456
|
+
that surface, which is the one way this feature can move shell layout. It is done
|
|
457
|
+
only where `static` was actually computed, so a surface the shell already
|
|
458
|
+
positions is never touched, and both are removed when the zone is cleared. The
|
|
459
|
+
alternative — leaving the surface `static` — lets the picture escape the element
|
|
460
|
+
it is meant to fill, so it is not an option.
|
|
461
|
+
- **A picture layer needs a `::before` free on the surface.** The shell currently
|
|
462
|
+
uses no `::before` content on any of the six zone surfaces (verified in a live
|
|
463
|
+
window: zero elements in the whole shell style one with content), so the layer
|
|
464
|
+
gets that pseudo-element to itself. A future shell build that starts using
|
|
465
|
+
`::before` on one of these surfaces would collide with it; the plugin's rule sets
|
|
466
|
+
`content`, `position`, `inset`, `z-index` and the picture, so the shell's own
|
|
467
|
+
`::before` content would be replaced rather than merged.
|
|
468
|
+
- **No Schemastery `Config` schema**, so `config` is read defensively and never
|
|
469
|
+
validated. That is also why the package carries no peer dependencies at all.
|
|
470
|
+
- **No live file watching.** A new or edited file needs **Rescan**, and an edited
|
|
471
|
+
*active* theme needs re-selecting.
|
|
472
|
+
- **The CSS targets the official DOM**, which is pre-1.0 and changes. Themes
|
|
473
|
+
built on `--dsw-*` alias tokens survive layout changes far better than themes
|
|
474
|
+
that style class names directly.
|
|
475
|
+
- **`body { font-family }` in `gov.css` is a deliberate whole-app restyle**, not
|
|
476
|
+
a token override; drop that block if only the palette is wanted.
|
|
477
|
+
- **Browser halves are not sandboxed.** This plugin runs in the same realm and
|
|
478
|
+
document as the shell; so does every client plugin, including the official
|
|
479
|
+
theme package.
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# dsh-custom-theme — the rows this package contributes to a profile.
|
|
2
|
+
#
|
|
3
|
+
# The row is mounted from the BARE package name on purpose: `client-modules`
|
|
4
|
+
# attaches a package's browser half to the row whose specifier is the bare
|
|
5
|
+
# package name, and a row mounted from a subpath export never carries a half.
|
|
6
|
+
#
|
|
7
|
+
# Host and browser halves ride this one row because they own a single concern —
|
|
8
|
+
# the theme directory and the picker that reads it. Disabling the row therefore
|
|
9
|
+
# removes the Appearance row, the palette, and the route together, which is the
|
|
10
|
+
# supported coarse switch.
|
|
11
|
+
|
|
12
|
+
- insert:
|
|
13
|
+
- id: custom-theme
|
|
14
|
+
name: 'dsh-custom-theme'
|