dsh-style-tweaks 0.1.1 → 0.1.3
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/README.en.md +212 -27
- package/README.md +193 -27
- package/lib/client.js +2034 -156
- package/lib/client.js.map +1 -1
- package/lib/config.js +62 -4
- package/lib/config.js.map +1 -1
- package/lib/types/client/index.d.ts +35 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/settings-nav-icon.d.ts +87 -0
- package/lib/types/client/settings-nav-icon.d.ts.map +1 -0
- package/lib/types/client/tweak-config.d.ts +33 -1
- package/lib/types/client/tweak-config.d.ts.map +1 -1
- package/lib/types/client/tweak-types.d.ts +14 -0
- package/lib/types/client/tweak-types.d.ts.map +1 -1
- package/lib/types/client/tweaks/assistant-stream-timing.d.ts +105 -0
- package/lib/types/client/tweaks/assistant-stream-timing.d.ts.map +1 -0
- package/lib/types/client/tweaks/idle-send-flash-guard.d.ts +81 -0
- package/lib/types/client/tweaks/idle-send-flash-guard.d.ts.map +1 -0
- package/lib/types/client/tweaks/keep-turn-rail.d.ts +82 -0
- package/lib/types/client/tweaks/keep-turn-rail.d.ts.map +1 -0
- package/lib/types/client/tweaks/legacy-stats-line.d.ts +22 -6
- package/lib/types/client/tweaks/legacy-stats-line.d.ts.map +1 -1
- package/lib/types/client/tweaks/locate-current-session.d.ts +6 -5
- package/lib/types/client/tweaks/locate-current-session.d.ts.map +1 -1
- package/lib/types/client/tweaks/pills-cache-hit-decimals.d.ts +3 -2
- package/lib/types/client/tweaks/pills-cache-hit-decimals.d.ts.map +1 -1
- package/lib/types/client/tweaks/project-running-indicator.d.ts +50 -14
- package/lib/types/client/tweaks/project-running-indicator.d.ts.map +1 -1
- package/lib/types/client/tweaks/registry.d.ts.map +1 -1
- package/lib/types/client/tweaks/rightbar-initial-width.d.ts +99 -0
- package/lib/types/client/tweaks/rightbar-initial-width.d.ts.map +1 -0
- package/lib/types/client/tweaks/settings-nav-scroll.d.ts +6 -0
- package/lib/types/client/tweaks/settings-nav-scroll.d.ts.map +1 -1
- package/lib/types/client/tweaks/sidebar-middle-click-close.d.ts +105 -0
- package/lib/types/client/tweaks/sidebar-middle-click-close.d.ts.map +1 -0
- package/lib/types/client/tweaks/stable-turn-rail.d.ts +11 -5
- package/lib/types/client/tweaks/stable-turn-rail.d.ts.map +1 -1
- package/lib/types/client/tweaks/stats-cache-hit.d.ts +3 -2
- package/lib/types/client/tweaks/stats-cache-hit.d.ts.map +1 -1
- package/lib/types/client/tweaks/thinking-scroll.d.ts +4 -0
- package/lib/types/client/tweaks/thinking-scroll.d.ts.map +1 -0
- package/lib/types/client/tweaks/turn-speed-metrics.d.ts +67 -0
- package/lib/types/client/tweaks/turn-speed-metrics.d.ts.map +1 -0
- package/lib/types/config.d.ts +132 -20
- package/lib/types/config.d.ts.map +1 -1
- package/package.json +24 -23
package/README.en.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# dsh-style-tweaks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
English | [简体中文](README.md)
|
|
4
|
+
|
|
5
|
+
> Dependency version: deepseek-harness v0.1.5-rc.1
|
|
4
6
|
|
|
5
7
|
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) web plugin that bundles a precise conversation column-width control with a collection of opt-in style tweaks for DSH's UI — including small fixes for the sidebar and the settings panel.
|
|
6
8
|
|
|
@@ -8,36 +10,48 @@ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) web
|
|
|
8
10
|
|
|
9
11
|
### Layout
|
|
10
12
|
|
|
11
|
-
- **Plugin width control (default
|
|
12
|
-
- **Dialog width** — any value between 600 and 1600 px; includes presets 748 (default) / 880 (Wide) / 1024 (Extra wide).
|
|
13
|
-
- **Side margin** — whitespace in px kept on each side of the conversation area. The column is clamped to the dialog width and narrows when the sidebar opens or the window shrinks, never hugging the edges. Minimum 32 px.
|
|
13
|
+
- **Plugin width control (default off)** — when ON, the plugin's width input / presets drive the conversation column and DSH's native drag handles are hidden; when OFF (default), DSH's native handles own the column and the dialog-width setting is hidden. The last width is kept, so switching back on restores it.
|
|
14
|
+
- **Dialog width** — any value between 600 and 1600 px; includes presets 748 (default) / 880 (Wide) / 1024 (Extra wide). Shown only while plugin width control is on.
|
|
15
|
+
- **Side margin** — whitespace in px kept on each side of the conversation area while plugin width control is on. The column is clamped to the dialog width and narrows when the sidebar opens or the window shrinks, never hugging the edges. Minimum 32 px. Shown only while plugin width control is on; when it is off, the margin does not apply and DSH's native behavior stands.
|
|
16
|
+
- **Fixed think height (default off)** — caps the expanded think (reasoning) body at a fixed height (default 300 px, adjustable 120–1200) and scrolls the overflow, so a long thinking trace stops pushing the rest of the conversation out of view; while a trace is still streaming, the window shows its top and you scroll for the tail. The wheel chains naturally: when the body hits its top/bottom edge or its content fits the window, the wheel scrolls the conversation instead. Folding the row back to one line keeps working as usual. The window gets an always-visible 4px thin scrollbar (same as the settings nav), with a breathing gap between text and bar.
|
|
17
|
+
- **Right sidebar initial width (default off, 45%)** — when ON, owns the right sidebar's first-open width: it is written once, the first time the sidebar opens in a page load, as the session frame's width × a percentage (15–70, with 30 / 40 / 45 / 55 presets). A manual drag, and every later open, keeps your own width; a reload applies the percentage again. OFF by default, which leaves DSH's own 45% completely untouched; once ON, the default percentage is that same 45, so nothing changes until the number is edited. DSH still clamps the result into its own range (at least 300 px, at most 70% of the frame), so a conversion below 300 px renders 300 px wide. On pre-0.1.5 hosts the tweak stays inert.
|
|
14
18
|
|
|
15
19
|
### Tweaks
|
|
16
20
|
|
|
17
21
|
- **Stable table layout (default on)** — locks table layout on hover so surrounding content does not reflow ("text jumps when I hover a table").
|
|
18
22
|
- **Stable turn-navigation rail (default on)** — keeps the turn-navigation rail at a stable position when scrolling up past the first message into the system prompt area (no longer drops by ~16 px).
|
|
23
|
+
- **Always show turn navigation (default off)** — DSH hides the turn-navigation rail once the chat column's content box reaches 900 px (a container query in `TurnNavigator.module.css` measured against the chat scrollport). Widening the right sidebar is exactly what triggers it: the center column may be squeezed down to 400 px, so the rail disappears over almost the panel's whole usable range. With this ON the rail stays at every chat width; at narrow widths it sits in the scrollport's right gutter and its hover preview covers part of the transcript.
|
|
19
24
|
- **Flush code-block top (default on)** — removes the 16 px gap above highlighted code blocks so the code sits flush with the preceding paragraph, list item, or heading.
|
|
20
|
-
- **Project running indicator (default on)** —
|
|
25
|
+
- **Project running indicator (default on)** — puts the conversation title's animated running dot into the sidebar. One sits on the right side of each project directory (so a busy conversation stays visible even when its group is collapsed), and one fills a session row's status slot whenever that session holds a background job the app's own dot does not cover (when the app does render its own dot there, the tweak steps aside rather than doubling up). A session counts as busy when it is executing a turn, when a background job is still open under it (a `run_in_background` shell command, lit from launch until the job settles), or when a running subagent sits below it in the lineage.
|
|
21
26
|
- **Locate current session (default on)** — adds a "locate" button to the left of the native search button in the sidebar's "Workspaces" section header. Clicking it expands the current session's workspace directory (if collapsed) and its "Show {n} more sessions" overflow, then scrolls the session into the sidebar's visible area. Disabled with the tooltip "Open a session first" when no session is open.
|
|
22
27
|
- **Scrollable settings nav (default on)** — lets the settings dialog's left menu scroll when its entries outgrow the panel, instead of silently clipping the ones at the bottom (the panel has a fixed height and `overflow: hidden`; stock CSS only gave the right content column a scroll treatment). The scrollbar is an overlay-style thin strip: parked in the rail's spare right padding so it never squeezes the menu, and visible only while the list is actually scrolling, fading out ~0.8 s after scrolling stops.
|
|
28
|
+
- **Middle-click closes sidebar tabs (default on)** — since 0.1.5 the right sidebar is a tabbed panel. A middle mouse click on any of its tabs (docked or floating) closes it, the way browser tabs behave. The close rides the host's public `ctx.sidebarRight` close face, so the native rules stay in force: the guide standing as the sole docked tab cannot be closed, and closing the last docked tab collapses the column exactly as its ✕ does. Middle-click autoscroll is suppressed over the strips, and a held middle button can no longer drag or float a tab. On pre-0.1.5 hosts the tweak stays inert.
|
|
23
29
|
- **Legacy stats line (default off)** — since DSH 0.1.5 the stats under the input box are two icon pills that open dialogs; this tweak brings back the pre-0.1.5 centered text line (turns/steps · timings · speed · cache hit · tokens). It reads the same durable projections (`sessionStats` / `tokenUsage`) so every figure matches the pills; an overlong line truncates with an ellipsis and reveals itself on hover, and turning the tweak off hands the row straight back to the pills.
|
|
24
|
-
- **Cache hit with two decimals (
|
|
30
|
+
- **Cache hit with two decimals (default off)** — shows the composer stats' cache-hit share with two decimal places (e.g. `87.35%`) instead of integer rounding; the token-usage dialog follows. Applies to both presentations — the new icon pills and the legacy text line alike; with both toggles on, the legacy line renders and takes the flag.
|
|
31
|
+
- **Turn speed & TTFT (default off)** — since 0.1.5, cold sessions no longer rebuild per-token timing, so each turn's time dialog keeps only the wall-clock duration. Clicking a turn's time pill refills that dialog with the output speed and TTFT rows, rebuilt from the model stream embedded in the session log; history loads get the same figures a live session did.
|
|
25
32
|
|
|
26
33
|
```yaml
|
|
27
34
|
style-tweaks:
|
|
28
35
|
# Layout
|
|
29
|
-
usePluginWidth:
|
|
30
|
-
dialogWidth: 748 # 600–1600 px
|
|
31
|
-
sideMargin: 50 # ≥ 32 px
|
|
36
|
+
usePluginWidth: false # default false; true lets the plugin input/presets own the column
|
|
37
|
+
dialogWidth: 748 # 600–1600 px; shown/effective only while usePluginWidth is on
|
|
38
|
+
sideMargin: 50 # ≥ 32 px; shown/effective only while usePluginWidth is on
|
|
39
|
+
thinkFixedHeight: false # default false; true caps the think body at a fixed height and scrolls it
|
|
40
|
+
thinkHeight: 300 # 120–1200 px; shown/effective only while thinkFixedHeight is on
|
|
41
|
+
rightbarInitialWidth: false # default false; true lets the plugin own the right sidebar's first-open width
|
|
42
|
+
rightbarWidthPercent: 45 # 15–70; right sidebar first-open width as a percentage of the session frame
|
|
32
43
|
# Tweaks
|
|
33
44
|
stableTable: true # default true; false disables the tweak
|
|
34
45
|
stableTurnRail: true # default true; false disables the tweak
|
|
46
|
+
keepTurnRail: false # default false; true keeps the turn-navigation rail at every chat width
|
|
35
47
|
codeBlockFlushTop: true # default true; false disables the tweak
|
|
36
48
|
projectRunningIndicator: true # default true; false disables the tweak
|
|
37
49
|
locateCurrentSession: true # default true; false hides the sidebar locate button
|
|
38
50
|
settingsNavScroll: true # default true; false disables the settings-nav scrolling
|
|
51
|
+
sidebarMiddleClickClose: true # default true; false disables middle-click tab closing
|
|
39
52
|
legacyStatsLine: false # default false; true swaps the pills for the 0.1.2 text line
|
|
40
|
-
pillsCacheHitDecimals: false # default false; true shows the
|
|
53
|
+
pillsCacheHitDecimals: false # default false; true shows the cache hit with two decimals (pills and legacy line)
|
|
54
|
+
turnSpeedMetrics: false # default false; true refills the turn-time dialog with output speed and TTFT
|
|
41
55
|
```
|
|
42
56
|
|
|
43
57
|
Settings entry: **Settings → Style tweaks**.
|
|
@@ -46,22 +60,26 @@ Settings entry: **Settings → Style tweaks**.
|
|
|
46
60
|
|
|
47
61
|
```bash
|
|
48
62
|
# from npm (recommended, prebuilt)
|
|
49
|
-
|
|
63
|
+
dsh plugin --profile web add dsh-style-tweaks
|
|
50
64
|
|
|
51
65
|
# from GitHub (source; runs the self-contained prepare build)
|
|
52
|
-
|
|
66
|
+
dsh plugin --profile web add github:zhj9709/dsh-style-tweaks
|
|
53
67
|
```
|
|
54
68
|
|
|
55
69
|
The package spec after `add` is forwarded to pnpm verbatim, so versions can be
|
|
56
70
|
pinned — `@version` for the npm package, `#tag` for the GitHub source:
|
|
57
71
|
|
|
58
72
|
```bash
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
dsh plugin --profile web add dsh-style-tweaks@0.1.3 # pin the npm version
|
|
74
|
+
dsh plugin --profile web add github:zhj9709/dsh-style-tweaks#v0.1.3 # pin a git tag
|
|
61
75
|
```
|
|
62
76
|
|
|
63
77
|
Restart DSH web once after installing (bundle plugins are scanned at process start).
|
|
64
78
|
|
|
79
|
+
> The commands above assume `dsh` is on your PATH. Without a global install, replace the leading
|
|
80
|
+
> `dsh` with `npx -y @deepseek-ai/dsh` — e.g.
|
|
81
|
+
> `npx -y @deepseek-ai/dsh plugin --profile web add dsh-style-tweaks`.
|
|
82
|
+
|
|
65
83
|
## Development
|
|
66
84
|
|
|
67
85
|
### Build
|
|
@@ -72,32 +90,196 @@ pnpm build # tsc (server) + tsc (client) + bundle lib/client.js
|
|
|
72
90
|
pnpm typecheck
|
|
73
91
|
```
|
|
74
92
|
|
|
75
|
-
Load against a running DSH with an overlay, or install as a
|
|
93
|
+
Load against a running DSH with an overlay, or install as a symlink:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
dsh web --patch ./cordis.patch.yml # dev overlay
|
|
97
|
+
dsh plugin --profile web add "link:./" # symlink install (local dev, see below)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Local development (recommended): `link:` symlink + hot reload
|
|
101
|
+
|
|
102
|
+
While developing, **do not pack the repo into a `.tgz` and install that**, and do not copy files into
|
|
103
|
+
the profile by hand after each build. Point the profile's `node_modules/dsh-style-tweaks` at this
|
|
104
|
+
checkout with pnpm's `link:` protocol instead — build output then lands on the very path DSH loads,
|
|
105
|
+
so a code change needs no copy, no repack, and no reinstall.
|
|
106
|
+
|
|
107
|
+
`<profile>` below stands for the profile directory:
|
|
108
|
+
|
|
109
|
+
| Platform | Profile directory |
|
|
110
|
+
|---|---|
|
|
111
|
+
| macOS / Linux | `~/.dsh/profiles/web` |
|
|
112
|
+
| Windows | `%USERPROFILE%\.dsh\profiles\web` |
|
|
113
|
+
|
|
114
|
+
**One-time install** (run from the repository root; the spec after `add` is forwarded to pnpm verbatim):
|
|
76
115
|
|
|
77
116
|
```bash
|
|
78
|
-
|
|
79
|
-
npx -y @deepseek-ai/dsh plugin --profile web add . # bundle install from this checkout
|
|
117
|
+
dsh plugin --profile web add "link:./"
|
|
80
118
|
```
|
|
81
119
|
|
|
82
|
-
|
|
120
|
+
This writes both the profile's `dependencies` and its `dsh.profile.bundles`, and has pnpm create the
|
|
121
|
+
symlink; whether `link:./` ends up recorded as a relative or an absolute path depends on the CLI, so
|
|
122
|
+
verify it after installing with the commands below.
|
|
123
|
+
|
|
124
|
+
> **Creating the symlink requires**: on Windows, **Developer Mode** (Settings → System → For
|
|
125
|
+
> developers) or an elevated shell, otherwise pnpm may fail to create a real symlink (some versions
|
|
126
|
+
> fall back to a junction, which is functionally equivalent); macOS / Linux have no such restriction.
|
|
83
127
|
|
|
84
|
-
Only
|
|
128
|
+
> **Only needed if this plugin was ever disabled by hand**: DSH composes its tree as
|
|
129
|
+
> `dsh.profile.bundles` → `cordis.patch.yml` → `--patch` overlays, so the patch layer is applied last,
|
|
130
|
+
> while `dsh plugin add/remove` only maintains `dependencies` and `dsh.profile.bundles` and never
|
|
131
|
+
> touches that hand-written file. A leftover `disabled: true` therefore survives a remove → add and
|
|
132
|
+
> shows up as "installed, built — and nothing mounts in the page".
|
|
133
|
+
>
|
|
134
|
+
> The file to edit is the **profile's** `<profile>/cordis.patch.yml`, **not** the same-named file in
|
|
135
|
+
> the repository root (that one is this bundle's own patch — a single `insert`, with no `disabled`).
|
|
136
|
+
> Check first with:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
grep -n "style-tweaks" ~/.dsh/profiles/web/cordis.patch.yml # macOS / Linux
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```powershell
|
|
143
|
+
Select-String "$env:USERPROFILE\.dsh\profiles\web\cordis.patch.yml" -Pattern style-tweaks
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
> Delete only that entry; keep the other disables (they are deliberate).
|
|
147
|
+
|
|
148
|
+
Verify the symlink points at this repo:
|
|
149
|
+
|
|
150
|
+
**macOS / Linux (including WSL and Git Bash)**
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
link=~/.dsh/profiles/web/node_modules/dsh-style-tweaks
|
|
154
|
+
ls -ld "$link" # expect an l... entry whose -> points at this repo
|
|
155
|
+
readlink "$link" # expect the repository root
|
|
156
|
+
test -e "$link/src" && echo "src is reachable"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Windows (PowerShell)**
|
|
160
|
+
|
|
161
|
+
```powershell
|
|
162
|
+
$link = "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-style-tweaks"
|
|
163
|
+
(Get-Item $link).LinkType # expect SymbolicLink (or Junction)
|
|
164
|
+
(Get-Item $link).Target # expect the repository root
|
|
165
|
+
Test-Path "$link\src" # expect True, proving it really points at the sources
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**The hot-reload loop after every code change:**
|
|
85
169
|
|
|
86
170
|
1. **Build**:
|
|
171
|
+
|
|
87
172
|
```bash
|
|
88
173
|
pnpm build
|
|
89
174
|
```
|
|
90
175
|
|
|
91
|
-
|
|
176
|
+
This step is not optional. The repo ships **no** `dev` / `watch` script — nothing watches `src/`
|
|
177
|
+
and recompiles for you, so a source change stays invisible until you build by hand (the symptom is
|
|
178
|
+
"I changed it and nothing happened"). Under the symlink, `pnpm build` rewrites `lib/client.js` in
|
|
179
|
+
place — the very file DSH loads — so **no copy into the profile is needed any more**.
|
|
180
|
+
|
|
181
|
+
2. **Make the page load the bundle again**:
|
|
182
|
+
|
|
183
|
+
- **Hard-refresh the GUI page first** — `Ctrl+Shift+R` on Windows / Linux, `Cmd+Shift+R` on macOS
|
|
184
|
+
(or close the tab and reopen it). In practice this is the most common and most reliable step;
|
|
185
|
+
a refresh is enough in the vast majority of cases.
|
|
186
|
+
- Only if a refresh still does not take effect should you restart the `dsh web` process (stop it
|
|
187
|
+
and run it again).
|
|
188
|
+
|
|
189
|
+
The claim in older versions of this README — that "DSH's client-plugin HMR receiver detects the
|
|
190
|
+
change and reloads automatically" — **often does not fire on a local setup**: `dsh web` usually runs
|
|
191
|
+
from an installed package rather than a source checkout running `dev:web`, so the page keeps using
|
|
192
|
+
the bundle it already fetched. "No change until I refresh" is therefore normal and does not mean
|
|
193
|
+
your code failed. A symlink only guarantees the file **on disk** is new; it does not guarantee the
|
|
194
|
+
page **fetches it again**.
|
|
195
|
+
|
|
196
|
+
**How to confirm the new code is really loaded**:
|
|
197
|
+
|
|
198
|
+
- The page should carry injected styles such as `style[data-tweak-css="cst-*"]`, plus the
|
|
199
|
+
`__cst_*_cleanup__` guards each tweak leaves on `window` (e.g.
|
|
200
|
+
`__cst_project_running_indicator_cleanup__`). A guard that is `undefined` means that tweak never
|
|
201
|
+
mounted.
|
|
202
|
+
- To see which build the page actually got: take the full URL of the
|
|
203
|
+
`/plugins/??…dsh-style-tweaks…` entry from `performance.getEntriesByType('resource')`, then
|
|
204
|
+
`fetch(url, { cache: 'no-store' })` and look for an identifier you just added. The page does
|
|
205
|
+
**not** re-request that URL after a build — which is itself a direct way to tell whether HMR fired.
|
|
206
|
+
- Overwriting `lib/client.js` exactly while DSH is reading the bundle can produce one bogus
|
|
207
|
+
"the whole plugin is gone" pass (no guards, no styles). A single refresh restores it; don't blame
|
|
208
|
+
your code yet.
|
|
209
|
+
|
|
210
|
+
**Boundaries**: only the client artifact `lib/client.js` hot-loads this way. Server code
|
|
211
|
+
(`src/index.ts` / `src/web.ts` / `src/config.ts` → `lib/index.js`) needs a `dsh web` restart on top of
|
|
212
|
+
`pnpm build`; changes to the `apps/web` shell or to a plain package also require rebuilding the
|
|
213
|
+
corresponding web artifacts and refreshing the page.
|
|
214
|
+
|
|
215
|
+
**Back to the published version** (package name, no local path):
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
dsh plugin --profile web remove dsh-style-tweaks
|
|
219
|
+
dsh plugin --profile web add github:zhj9709/dsh-style-tweaks
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Alternative: install from a packed `.tgz` (artifact copy required)
|
|
223
|
+
|
|
224
|
+
When you would rather not have the profile depend on this repo's path — or want to exercise a real
|
|
225
|
+
install end to end — pack the repo into a `.tgz` and install that. The trade-off: the profile's
|
|
226
|
+
`node_modules/dsh-style-tweaks` becomes a **real copy** (no longer pointing at the sources), so every
|
|
227
|
+
source change has to be copied over — exactly the step the `link:` setup spares you.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
pnpm build
|
|
231
|
+
pnpm pack # produces dsh-style-tweaks-<version>.tgz (gitignored; do not commit)
|
|
232
|
+
dsh plugin --profile web add ./dsh-style-tweaks-0.1.3.tgz
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Restart `dsh web` once** after installing (bundle plugins are scanned at process start).
|
|
236
|
+
|
|
237
|
+
Then, after each code change:
|
|
238
|
+
|
|
239
|
+
1. **Build**:
|
|
240
|
+
|
|
92
241
|
```bash
|
|
93
|
-
|
|
242
|
+
pnpm build
|
|
94
243
|
```
|
|
95
244
|
|
|
96
|
-
|
|
245
|
+
2. **Copy the artifact into the profile** (not needed under `link:`). Delete first rather than
|
|
246
|
+
overwriting in place:
|
|
247
|
+
|
|
248
|
+
**macOS / Linux**
|
|
97
249
|
|
|
98
|
-
|
|
250
|
+
```bash
|
|
251
|
+
dest=~/.dsh/profiles/web/node_modules/dsh-style-tweaks/lib/client.js
|
|
252
|
+
rm -f "$dest" && cp lib/client.js "$dest"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Windows (PowerShell)**
|
|
256
|
+
|
|
257
|
+
```powershell
|
|
258
|
+
$dest = "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-style-tweaks\lib\client.js"
|
|
259
|
+
Remove-Item $dest -Force
|
|
260
|
+
Copy-Item lib/client.js $dest
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
3. **Hard-refresh the GUI page**; if that still does not take effect, restart `dsh web`.
|
|
264
|
+
|
|
265
|
+
> **Why not simply overwrite with `cp -f`**: pnpm links files in `node_modules` to its
|
|
266
|
+
> content-addressable store — by **hard link** on Linux and Windows — so an in-place overwrite writes
|
|
267
|
+
> through to the store and can corrupt it for other projects sharing those files. macOS defaults to
|
|
268
|
+
> APFS clonefile (copy-on-write) and is usually unaffected, but "delete then copy" is safe everywhere.
|
|
269
|
+
> On GNU coreutils you can also use `cp --remove-destination`; note that **BSD `cp` on macOS has no
|
|
270
|
+
> such option**.
|
|
271
|
+
|
|
272
|
+
Switching from `link:` back to `.tgz` needs a `remove` first, since the profile already holds a local
|
|
273
|
+
path dependency:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
dsh plugin --profile web remove dsh-style-tweaks
|
|
277
|
+
dsh plugin --profile web add ./dsh-style-tweaks-0.1.3.tgz
|
|
278
|
+
```
|
|
99
279
|
|
|
100
|
-
|
|
280
|
+
The only difference between the two setups is whether the artifact lands in place automatically: both
|
|
281
|
+
need `pnpm build` and a page refresh. Use `link:` for day-to-day code changes, and `.tgz` when you want
|
|
282
|
+
to verify the published package itself.
|
|
101
283
|
|
|
102
284
|
## How it works
|
|
103
285
|
|
|
@@ -105,11 +287,14 @@ Only client plugins (`client.js`) support hot reloading. Changes to the `apps/we
|
|
|
105
287
|
- **Browser** (`src/client/index.tsx`) reads/writes that route, renders the Settings section, and mounts / unmounts each tweak live based on its toggle (pure-CSS tweaks inject runtime `<style>` elements; JS-level tweaks also read the app's own state stores and patch the DOM).
|
|
106
288
|
- **Column-width engine** (`src/client/conversation-width.ts`) writes the `--dsh-chat-user-width` CSS variable and hides DSH's native `[data-width-handle]` drag strips while plugin width control is on; the chosen px is also mirrored into the localStorage slot the native handles read, so flipping the switch round-trips cleanly.
|
|
107
289
|
- **Tweak registry** (`src/client/tweaks/registry.ts`) centralises each tweak's metadata (id, settings field name, default value, i18n keys); adding a new tweak means appending one entry here and dropping a new injector file under `src/client/tweaks/`.
|
|
108
|
-
- **Project running indicator** (`src/client/tweaks/project-running-indicator.ts`) reads session running state and directory membership from `ctx.get('sessions')` / `ctx.get('workspaces')`, and uses a MutationObserver to mount the app's own `StateDot` (re-using `@deepseek-ai/dsh-client-ui-primitives` so the animation keyframes and style tokens are byte-identical to the conversation title's dot)
|
|
290
|
+
- **Project running indicator** (`src/client/tweaks/project-running-indicator.ts`) reads session running state and directory membership from `ctx.get('sessions')` / `ctx.get('workspaces')`, and uses a MutationObserver to mount the app's own `StateDot` (re-using `@deepseek-ai/dsh-client-ui-primitives` so the animation keyframes and style tokens are byte-identical to the conversation title's dot) on two kinds of sidebar row: the right side of each project directory header row (`role="treeitem"[aria-expanded]`), and the status slot of a session row (`role="treeitem"` plus the CSS-Modules local name `sessionRow`) that the app left empty. "Busy" merges three sources: the session's own `SessionSummary.running` (a turn in flight); the background jobs under it — `jobsBySession` in the `sessions` store snapshot, mirrored by Session Controller's control stream, where a status of `running` or `stopping` counts (the same `isLive` predicate DSH's own `ui-jobs` session-header action uses, so a `run_in_background` command stays lit from launch to settlement instead of going dark once the turn that started it answers); and a running subagent anywhere below it in the lineage (the same upward walk as `indexSubagentDescendants`). Header rows keep matching workspaces by title text, exactly as before. A session row is only decorated while its slot is empty — the moment the app renders its own dot there (turn running, subagent running, approval or question pending, unviewed completion) the injected dot steps aside instead of sitting next to it. Session rows expose no plugin slot and carry no session id in the DOM, so the id is read off React's fiber chain (`SessionNodeItem`'s `props.node.id`): the walk is defensive (an unreadable id just leaves that row alone) and re-resolved every pass, because the sidebar recycles its row elements. On a host without the `jobs` service `jobsBySession` is absent or empty and the tweak quietly falls back to the two cases the app already covers.
|
|
109
291
|
- **Locate current session** (`src/client/tweaks/locate-current-session.ts`) injects a new button into the sidebar's workspaces section header, to the left of the native search button. All anchors are i18n-safe: the search button's aria-label and the breadcrumb nav's aria-label are resolved through `ctx.locale.bind()` (the `workspace` / `conversation` namespace keys DSH itself uses), with structural class-fragment fallbacks (`searchButton`, `crumbs`) when the locale service is unavailable. Clicking it reads the current session title from the breadcrumb's disabled crumb, resolves the parent workspace via `ctx.get('sessions')` / `ctx.get('workspaces')` app stores (works even when the workspace's sidebar group is collapsed), finds the workspace row by exact title match, then pierces both collapse layers — clicking the collapsed workspace row to expand it, and auto-clicking the "Show {n} more sessions" overflow button (`[class*="sessionOverflowButton"][aria-expanded="false"]`) when the target row hides behind it — before `scrollIntoView({ block: 'center' })`. The hover tooltip replicates DSH's native `<Tooltip>` primitive (fixed-position bubble, theme tokens, 500 ms delay, viewport flip) instead of the browser-native `title`. A MutationObserver watches `aria-selected` / `aria-expanded` attribute changes to keep the button's enabled state and mount in sync.
|
|
110
292
|
- **Scrollable settings nav** (`src/client/tweaks/settings-nav-scroll.ts`) turns the settings dialog's left nav list into the scroll container of the nav rail: DSH's settings panel (`SettingsRoot`) has one fixed height with `overflow: hidden` and only gave the right content column (`.options`) an `overflow-y: auto`; the left `.navList` has neither `min-height: 0` nor an overflow treatment, so extra entries spill out and get clipped. Two CSS rules plus a small JS driver do it: the base rule `flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding-bottom: 12px; margin-inline-end: -12px` makes the list scrollable and parks the classic scrollbar in the nav rail's full 12px right padding gutter (8px gap + 4px strip), so the thumb floats over the spare padding 8px clear of the entries; a cell cap `max-width: 164px` (DSH's stock 188 nav − 2×12 padding) pins the entries to stock width in every state — bar present or not, hovering or scrolling — and `::-webkit-scrollbar { width: 4px }` is two notches thinner than DSH's global skin. The show timing is JS-driven: the first cut used `:not(:hover)` to blank the thumb, but Chromium does not reliably repaint custom scrollbar pseudo-elements when only the host's `:hover` state changes (in practice the bar appeared when clicking a nav cell — the React re-render forced the repaint), so the driver listens for `scroll` events on the list instead: scrolling adds a `cst-nav-scroll-show` class that reveals the thumb (coloured from the panel's inherited l2 tokens), and the class is removed ~0.8 s after scrolling stops; a MutationObserver re-attaches the listener whenever the dialog unmounts and reopens. The selector pairs the modal structure with a CSS Modules class fragment (DSH hashes classes as `[hash]_[local]`; the `navList` local name is unique across DSH), and needs no `!important`.
|
|
111
|
-
- **
|
|
112
|
-
- **
|
|
293
|
+
- **Middle-click closes sidebar tabs** (`src/client/tweaks/sidebar-middle-click-close.ts`): a right-Sidebar tab chip is `div[role="tab"][data-dockkit-tab="<tabId>"]`, rendered by `ui-dockkit`'s `TabPanel`, and the docking kit is adopted only by the right Sidebar (docked panel plus floating layer), so "chip" is exactly "Sidebar tab". The tweak hangs three listeners on `document` in the capture phase (React 17+ listens at the root container, so document capture precedes the whole React tree and the interception is total): `pointerdown` (middle) stops propagation so the chip's own press→drag gesture never begins — holding the middle button and drifting ≥4px can no longer place, split, or float the tab; `mousedown` (middle) calls `preventDefault()`, suppressing the browser's middle-click autoscroll (the `auxclick` that follows still fires); `auxclick` (middle — browsers deliver non-primary clicks as `auxclick`, never `click`) reads the chip's `TabId` and hands it to `ctx.sidebarRight.close()`, so every closing rule is the host store's own planner's (missing tab → no-op, the sole docked guide is unclosable, closing the last docked tab collapses the column) and the plugin duplicates none of them. `ctx.sidebarRight` only exists on 0.1.5+ hosts, and it is provided by `ui-sidebar-right` in that package's own plugin fiber — a plain property read resolves services only along the ancestor-fiber chain, so from a sibling plugin it throws ("cannot get property … without inject") even on hosts that have the face, while declaring the service in this plugin's `inject` would make it required and dead-lock the boot on pre-0.1.5 hosts; so the face is resolved at every middle close through `ctx.reflect.get('sidebarRight')` — the reflect instance and its store are shared per root and `provide` allocates the isolation key at the root, so `get` reads sibling-provided services across fibers with no inject requirement, and an unprovided name answers `undefined` instead of throwing (pre-0.1.5 hosts → `undefined`, where the tweak keeps its inert form — side-effect suppression only); a `close()` throw with no mounted seat is swallowed. Floating panels render no chip: their middle close forwards to the header's own ✕ control (absent — a no-op — when the embedder's `canCloseTab` withholds it), while the header's dock button is explicitly excluded; a press and a release on different elements hand the `auxclick` to their common ancestor, which answers neither helper and closes nothing.
|
|
294
|
+
- **Right sidebar initial width** (`src/client/tweaks/rightbar-initial-width.ts`): the right sidebar's width belongs to the host's `ui-layout` — its `openRightbar` action fills an unset preference with `rightbar ??= max(300, round(viewportWidth × 0.45))` (the `RIGHTBAR_DEFAULT_RATIO` constant), a drag overwrites it through `setRightbar` (clamped to `[300, window × 70%]`), and every later open/close keeps that px; the preference lives only in the in-memory store, so a reload drops it back to `null`. `ctx.layout` exposes only `openRightbar(track, fullscreen)` / `closeRightbar()` / `toggleSidebar()` / `selectPanel()` — the width action lives on the controller's own `panels` set (declared `private` in TypeScript, a plain runtime property). The tweak therefore wraps the controller **instance**'s `openRightbar`: on the FIRST open of a page load it measures the parent of `[data-rightbar-col]` (the very frame `ui-layout` measures for `viewportWidth`), writes `round(width × percent)` through the host's own `panels.setRightbar`, and only then forwards the original call — the write and the open land in the same React commit (never a first frame at 45%); neither the grid template nor the panel's inline width is touched, so the column solve, the drag handle's position and the panel width stay in agreement by construction. Two page-level markers hold the semantics: `__cst_rightbar_initial_width_seeded__` allows one write per load, and `__cst_rightbar_initial_width_dragged__` is set by a **one-shot** probe on the first `pointerdown` over `[data-side="rightbar"]` (the host's right-column drag handle) — once the user has dragged by hand, this tweak writes nothing for the rest of the load, since every settings change re-mounts every tweak and would otherwise overwrite the width just dragged. With the sidebar already open as the tweak mounts (the user editing the percentage in Settings) the new ratio is written immediately — the live preview. It stays inert when the host provides no `layout` service or its controller no longer carries a callable `setRightbar`, and disposal restores the host method only while the installed wrapper is still the current value, so another plugin's wrapper is never clobbered.
|
|
295
|
+
- **Legacy stats line** (`src/client/tweaks/legacy-stats-line.tsx`): 0.1.5 replaced the composer stats line (the old `StatsLine`) with icon pills (the new `StatsPills`); both mount the same way — a `list` entry with id `stats` on the `conversation.composer.dock` slot. A list cell renders its lowest-priority live entry (see `SlotCore.register`), so while the tweak is on, the plugin shadows the shipped entry with `id: 'stats'` + `priority: -2` (one step below the pills-decimals row, which it outranks when both are on) — the slot system's own shadowing mechanism, no CSS hiding or DOM patching — and disposing the registration hands the cell straight back; a crashed entry retires itself, restoring the pills. The data plane matches the pills: the durable `sessionStats` (whole-log counts and wall times) and `tokenUsage` (billing buckets) projections, without the old whole-window fallback fold (no projection → no row, matching the old line's "no data, no row" rule). Copy lives in the plugin's own `style-tweaks` namespace (`legacyStats.*` keys) — DSH removed the old `stats.llm` family from its dictionaries in 0.1.5, so the plugin carries its own en/zh strings; the row skin (centered, tertiary text, ellipsis truncation) is ported from 0.1.2's `StatsLine.module.css` with the original tokens plus fallbacks. The row root carries the `data-composer-stats` marker: the host InputBar tightens its own 8px bottom clearance to 4px around any mounted stats row (`.root:has([data-composer-stats])`), and the row adds 2px of bottom padding so its total matches the pills row (26px row + 4px host clearance on both presentations) — toggling the tweak no longer shifts the conversation. The cache-hit share's precision follows the `pillsCacheHitDecimals` flag — two decimals while it is on, 0.1.2's integer rounding while off; the flag is captured at mount, and any settings change remounts every tweak, so the line always renders with the current value.
|
|
296
|
+
- **Cache hit with two decimals** (`src/client/tweaks/pills-cache-hit-decimals.tsx`): the pills' percent rounding happens inside a module-internal formatter in dsh-client-ui-chat that a plugin cannot reach, so while the tweak is on, it shadows the same `stats` slot cell (`priority: -1`) with its own render of the whole row — both pills and their click-open dialogs ported style-for-style from 0.1.5's `StatsPills` / `stat-dialog` (placement and outside-close ride the host primitives' own `useAnchoredPosition` / `useDismissOnOutsidePointer`), with the cache hit routed through the plugin's shared `formatCacheHitPercent(…, 2)` (two-decimal rounding; a hit that would read `100%` keeps its honest extra-precision tail, e.g. `99.97`). The dialog rows and pill labels follow. The data plane is the same `sessionStats` / `tokenUsage` projections (no window fold), and the root carries `data-composer-stats` as well. One icon note: the pills' gauge icon only exists in primitives 0.1.3+, so the host's `IconGaugeOutline16` is looked up at runtime and a clock icon stands in on hosts that predate it. Layering with the legacy line: both tweaks write the same slot cell; the legacy line registers at `priority: -2` and renders instead, leaving this row shadowed (unrendered, zero cost) until the legacy line is turned off, and the shipped pills return only when both tweaks are off. The toggle stays visible either way: the legacy line reads the same flag for its own decimals.
|
|
297
|
+
- **Turn speed & TTFT** (`src/client/tweaks/turn-speed-metrics.tsx` + `src/client/tweaks/assistant-stream-timing.ts`): session format v2 (0.1.5) embeds each model attempt's exact timed stream inside its durable settlement (`assistant/message`'s `data.stream`), but the Chat UI's cold presentation builds settled output straight from the assembled message and never replays it (the v2 architecture note, verbatim: "Cold settled presentation therefore does not reconstruct per-token timing") — the turn footer's own fold reads the node's in-memory timing, whose `firstTokenTime` only survives live-chunk folding, so every turn's output speed (TPS) and first-token time (TTFT) vanish across a reload and the "turn time & speed" dialog keeps only the wall-clock duration. This tweak is the consumer that note anticipates: it reads the settlements out of the session binding's documented event window (`ctx.sessions.binding(sessionId).eventSource`, the same feed the Conversation assembly consumes), with the compact-stream readers ported verbatim from dsh-llm (`assistantStreamFirstTokenTime`: reconstructs the first token's time inside a packed run from `time0` plus the `dt` gap list) and the fold from 0.1.5's `deriveTurnMetrics` — TTFT is the lowest step's dispatch→first-token delta, TPS is Σ output tokens ÷ Σ decode wall time over the steps carrying both timing and usage. Everything rides the durable log, so history loads and freshly settled turns show identical figures. Display choice: the dialog belongs to `TurnTimePanel`, which offers no slot, so the tweak mounts an invisible controller on the `conversation.chat.assistant-actions` list slot (inside each turn's action row; list entries render additively, no election race with other plugins' entries). A capture click listener on the footer only hands that footer to one shared body-wide `MutationObserver` (the dialog is portaled to `<body>`, so the panel node cannot reach its footer through `closest()`); the observer callback is a microtask that always runs before the next frame's rendering steps, claims the freshly committed panel by its stable `data-turn-time-details` marker, and appends `<dt>/<dd>` pairs after the duration row — labels straight from DSH's surviving `chat` vocabulary (`message.turnTime.speed` / `message.turnTime.ttft` / `message.tokensPerSecond` / `duration.seconds`), values inheriting the dialog's own `.details dt/.details dd` grid rules, so the refilled panel reads exactly like the 0.1.2 one. Appending rows grows the panel, and `useAnchoredPosition` repositions through `ResizeObserver` → `setState`, whose render lands after the next paint — the panel would otherwise paint one frame at its stale `top` (48 px too low) before snapping up, the very flash this tweak shipped with — so the same microtask lifts the fixed-positioned card by the added height: the dialog hangs from the trigger's top edge (`side: 'top'`), so keeping the card's bottom edge fixed is exactly the value the host's own `place()` computes for the taller panel, the first painted frame is already final, and the host's later write becomes a same-value no-op; the lift is skipped only when it would push the card above the viewport (a clamped placement). Hiding the card until the host repainted was tried and rejected — it turns a correct instant open into a visible late pop, making turns that never flashed start flashing. Do not derive the shift from footer geometry either: `data-turn-tail` is the whole turn container, not the action row. Turns whose live timing already survived render their own rows and are skipped by label. The rows are plain DOM inside the React-owned panel: they die with the dialog and re-inject on every open; with no derivable figure (no token in the stream, or the settlement outside the loaded window) nothing is injected, and turning the tweak off restores the stock behaviour.
|
|
113
298
|
|
|
114
299
|
## Acknowledgements
|
|
115
300
|
|