dsh-style-tweaks 0.1.4 → 0.1.6
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 +57 -22
- package/README.md +57 -22
- package/lib/client.js +3413 -3190
- package/lib/client.js.map +1 -1
- package/lib/config.js +216 -38
- package/lib/config.js.map +1 -1
- package/lib/index.js +73 -15
- package/lib/index.js.map +1 -1
- package/lib/store.js +142 -0
- package/lib/store.js.map +1 -0
- package/lib/types/client/conversation-width.d.ts +0 -75
- package/lib/types/client/conversation-width.d.ts.map +1 -1
- package/lib/types/client/i18n.d.ts +144 -0
- package/lib/types/client/i18n.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +0 -191
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/mutation-scope.d.ts +2 -0
- package/lib/types/client/mutation-scope.d.ts.map +1 -0
- package/lib/types/client/panel.d.ts +10 -0
- package/lib/types/client/panel.d.ts.map +1 -0
- package/lib/types/client/settings-client.d.ts +39 -0
- package/lib/types/client/settings-client.d.ts.map +1 -0
- package/lib/types/client/settings-nav-icon.d.ts +0 -85
- package/lib/types/client/settings-nav-icon.d.ts.map +1 -1
- package/lib/types/client/settings-value.d.ts +12 -0
- package/lib/types/client/settings-value.d.ts.map +1 -0
- package/lib/types/client/style-node.d.ts +5 -0
- package/lib/types/client/style-node.d.ts.map +1 -0
- package/lib/types/client/tweak-config.d.ts +25 -72
- package/lib/types/client/tweak-config.d.ts.map +1 -1
- package/lib/types/client/tweak-types.d.ts +15 -36
- package/lib/types/client/tweak-types.d.ts.map +1 -1
- package/lib/types/client/tweaks/assistant-stream-timing.d.ts +1 -68
- package/lib/types/client/tweaks/assistant-stream-timing.d.ts.map +1 -1
- package/lib/types/client/tweaks/chat-row-gap.d.ts +40 -0
- package/lib/types/client/tweaks/chat-row-gap.d.ts.map +1 -0
- package/lib/types/client/tweaks/code-block-flush-top.d.ts +0 -88
- package/lib/types/client/tweaks/code-block-flush-top.d.ts.map +1 -1
- package/lib/types/client/tweaks/composer-dock.d.ts +3 -0
- package/lib/types/client/tweaks/composer-dock.d.ts.map +1 -0
- package/lib/types/client/tweaks/context-pill-no-tooltip.d.ts +3 -0
- package/lib/types/client/tweaks/context-pill-no-tooltip.d.ts.map +1 -0
- package/lib/types/client/tweaks/desktop-settings-launcher.d.ts +4 -0
- package/lib/types/client/tweaks/desktop-settings-launcher.d.ts.map +1 -0
- package/lib/types/client/tweaks/fold-per-turn.d.ts +54 -0
- package/lib/types/client/tweaks/fold-per-turn.d.ts.map +1 -0
- package/lib/types/client/tweaks/hide-session-hover-actions.d.ts +3 -0
- package/lib/types/client/tweaks/hide-session-hover-actions.d.ts.map +1 -0
- package/lib/types/client/tweaks/history-page-size.d.ts +4 -0
- package/lib/types/client/tweaks/history-page-size.d.ts.map +1 -0
- package/lib/types/client/tweaks/keep-turn-rail.d.ts +0 -79
- package/lib/types/client/tweaks/keep-turn-rail.d.ts.map +1 -1
- package/lib/types/client/tweaks/legacy-context-meter.d.ts +7 -0
- package/lib/types/client/tweaks/legacy-context-meter.d.ts.map +1 -0
- package/lib/types/client/tweaks/legacy-stats-line.d.ts +0 -66
- package/lib/types/client/tweaks/legacy-stats-line.d.ts.map +1 -1
- package/lib/types/client/tweaks/locate-current-session.d.ts +0 -62
- package/lib/types/client/tweaks/locate-current-session.d.ts.map +1 -1
- package/lib/types/client/tweaks/opaque-cards.d.ts +64 -0
- package/lib/types/client/tweaks/opaque-cards.d.ts.map +1 -0
- package/lib/types/client/tweaks/opaque-pill-dialogs.d.ts +79 -0
- package/lib/types/client/tweaks/opaque-pill-dialogs.d.ts.map +1 -0
- package/lib/types/client/tweaks/opaque-stat-dialogs.d.ts +3 -0
- package/lib/types/client/tweaks/opaque-stat-dialogs.d.ts.map +1 -0
- package/lib/types/client/tweaks/pills-cache-hit-decimals.d.ts +0 -51
- package/lib/types/client/tweaks/pills-cache-hit-decimals.d.ts.map +1 -1
- package/lib/types/client/tweaks/project-running-indicator.d.ts +0 -100
- package/lib/types/client/tweaks/project-running-indicator.d.ts.map +1 -1
- package/lib/types/client/tweaks/registry.d.ts +4 -13
- package/lib/types/client/tweaks/registry.d.ts.map +1 -1
- package/lib/types/client/tweaks/rightbar-initial-width.d.ts +0 -96
- package/lib/types/client/tweaks/rightbar-initial-width.d.ts.map +1 -1
- package/lib/types/client/tweaks/running-status.d.ts +3 -0
- package/lib/types/client/tweaks/running-status.d.ts.map +1 -0
- package/lib/types/client/tweaks/secondary-row-gap.d.ts +37 -0
- package/lib/types/client/tweaks/secondary-row-gap.d.ts.map +1 -0
- package/lib/types/client/tweaks/settings-nav-scroll.d.ts +1 -169
- package/lib/types/client/tweaks/settings-nav-scroll.d.ts.map +1 -1
- package/lib/types/client/tweaks/sidebar-middle-click-close.d.ts +0 -102
- package/lib/types/client/tweaks/sidebar-middle-click-close.d.ts.map +1 -1
- package/lib/types/client/tweaks/sidebar-plugins-above-settings.d.ts +91 -0
- package/lib/types/client/tweaks/sidebar-plugins-above-settings.d.ts.map +1 -0
- package/lib/types/client/tweaks/sidebar-session-count.d.ts +7 -0
- package/lib/types/client/tweaks/sidebar-session-count.d.ts.map +1 -0
- package/lib/types/client/tweaks/stable-session-title.d.ts +3 -0
- package/lib/types/client/tweaks/stable-session-title.d.ts.map +1 -0
- package/lib/types/client/tweaks/stats-cache-hit.d.ts +0 -27
- package/lib/types/client/tweaks/stats-cache-hit.d.ts.map +1 -1
- package/lib/types/client/tweaks/turn-process-counts.d.ts +4 -0
- package/lib/types/client/tweaks/turn-process-counts.d.ts.map +1 -0
- package/lib/types/client/tweaks/turn-speed-metrics.d.ts +16 -4
- package/lib/types/client/tweaks/turn-speed-metrics.d.ts.map +1 -1
- package/lib/types/client/tweaks/turn-time-pill.d.ts +3 -0
- package/lib/types/client/tweaks/turn-time-pill.d.ts.map +1 -0
- package/lib/types/client/tweaks/workspace-close.d.ts +0 -78
- package/lib/types/client/tweaks/workspace-close.d.ts.map +1 -1
- package/lib/types/config.d.ts +315 -59
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/index.d.ts +29 -9
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/store.d.ts +92 -0
- package/lib/types/store.d.ts.map +1 -0
- package/lib/types/web.d.ts +86 -6
- package/lib/types/web.d.ts.map +1 -1
- package/lib/web.js +506 -24
- package/lib/web.js.map +1 -1
- package/package.json +30 -28
- package/lib/types/client/tweaks/stable-table.d.ts +0 -3
- package/lib/types/client/tweaks/stable-table.d.ts.map +0 -1
package/README.en.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.md)
|
|
4
4
|
|
|
5
|
-
> Dependency version: deepseek-harness v0.1.
|
|
5
|
+
> Dependency version: deepseek-harness v0.1.7-rc.2
|
|
6
6
|
|
|
7
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.
|
|
8
8
|
|
|
@@ -13,37 +13,55 @@ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) web
|
|
|
13
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
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
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
16
|
- **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.
|
|
18
17
|
|
|
18
|
+
- **Desktop Settings launcher (default off)** — the preference is shown only in the Desktop Style tweaks settings. When enabled, the sidebar reads `[More] [⚙ Settings]`; clicking the gear activates the host's native Settings action through a short-lived portalled-menu bridge, so the original full Settings window is reused and the duplicate Settings row is removed from More. Feedback, sign-in, sign-out, and the other account actions remain. When disabled, the native More → Settings path is restored; Web does not show this preference or the external gear. The bridge depends on the current account-menu semantics, menu placement, and CSS Modules local-name fragments; if those change, it falls back to the untouched native menu.
|
|
19
|
+
|
|
19
20
|
### Tweaks
|
|
20
21
|
|
|
21
|
-
- **Stable
|
|
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).
|
|
22
|
+
- **Stable session titles (default off)** — since 0.1.6-alpha.2, hovering a session row smoothly slides an overlong title to its end to reveal the clipped tail, and it snaps back on leaving. This restores the pre-0.1.6-alpha.2 behaviour: the title stays put with its ellipsis at all times; the row's hover card (pause a moment) still shows the full title.
|
|
23
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.
|
|
24
|
+
- **Hide session hover buttons (default off)** — hovering a session row floats the Archive and Pin icon buttons into its trailing cell, with the archive button right under the pointer (one click archives). This keeps them off the row: while hovered it shows only the host's own `...` menu button, where both actions still live.
|
|
24
25
|
- **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.
|
|
25
|
-
- **Project running indicator (default on)** — puts the
|
|
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.
|
|
26
|
+
- **Project running indicator (default on)** — puts an animated running dot into the sidebar. While it is on, every running dot in the app is redrawn as ZCode's spinner: eight evenly spaced spokes turned at a constant 1s per revolution, independent of the system's reduced-motion setting — DSH 0.1.7 freezes all of them into still rings there, so the tweak draws its own (the two injected dots and the title's own alike; see below). 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.
|
|
27
|
+
- **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. The button does not appear while the sidebar is collapsed to its icon rail — there is no section header (and so no search box) in that state.
|
|
27
28
|
- **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
29
|
- **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.
|
|
29
30
|
- **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.
|
|
30
31
|
- **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
|
|
32
|
+
- **Turn time and speed (default off)** — through 0.1.6 a settled turn ended with two click-open readings in its action row: usage and time, each opening its own dialog. 0.1.7-alpha.1's performance-and-usage preference dropped the time one (`TurnTimePanel`, `message.ranFor` and the whole `message.turnTime.*` vocabulary went with it). This puts it back right of the usage reading (the row reads usage → time → clock again, as in 0.1.6), with its Turn time and speed dialog: the turn's total run time plus output speed (TPS) and time to first token (TTFT), the last two rebuilt from the model stream embedded in the session log (cold sessions have not replayed that stream since 0.1.5, so this is the step that brings those two numbers back). Both figures come from the durable log, so history loads show them too; a turn whose start/end the loaded window does not hold shows no reading, leaving the 0.1.7 row untouched.
|
|
33
|
+
- **Call counts on process groups (default on)** — a turn's reasoning and tool calls merge into one folded process group. Through 0.1.6 its header was a tally — tool calls and messages, plus subagents when the turn spawned any; 0.1.7-alpha.1 replaced it with the elapsed time (or the state wording while running / failed / stopped). This restores the 0.1.6 tally after the time, so both halves show at once: the counts grow while the turn runs (a new tool call or message lands immediately), the failed / stopped / running headers read the same way, and a turn with nothing to count is left as shipped. Set the switch off to keep the stock 0.1.7 header.
|
|
34
|
+
- **Blue running status at the tail (default on)** — through 0.1.6 an open turn always ended with a separate brand-blue shimmer line, "深度求索中 / Deep diving", immediately above the composer; after fifteen seconds its elapsed clock appeared beside it. 0.1.7's grouped-work-details refactor removed that React component and merged the running wording into the process-group header. The running group stays open, so its header remains above the whole reasoning/tool run and moves away from the composer as calls accumulate. This restores a visual-only tail row using the host's current locale and live clock. The process header and accessibility announcement stay host-owned, so nothing is announced twice; pre-0.1.7 hosts already render their own line and remain unchanged.
|
|
35
|
+
- **Opaque stat dialogs (default off)** — 0.1.7-alpha.1 swapped the card material for a translucent, frosted one (the shared `--dsw-specific-menu` fill went from 0.1.6's opaque layer-3 to a half-transparent color paired with a 40px backdrop blur), which is harder to read a wall of figures through. This switches **only** the click-open stat dialogs back to 0.1.6's opaque fill, five in all: the composer's session-statistics and token-usage cards, the turn-tail per-turn usage and time pills, and the context-occupancy panel. Every other surface — the session and workspace "..." menus, the model selector, the composer's / and @ menus, the todo / queue / dock panels — deliberately keeps 0.1.7's frosted material; the dark theme uses the theme's own layer-3, so no separate value is needed.
|
|
36
|
+
- **No context meter hover info (default off)** — the context meter button under the input box (a ring gauge plus percentage, new in 0.1.6-alpha.2) floats a "N% of context used" tooltip when hovered. With this on, the hover tooltip no longer appears; the button's own hover highlight and its click-open breakdown dialog are untouched, so the full figures stay one click away. Hosts without that button leave this inert.
|
|
32
37
|
- **Closable workspaces (default off)** — hide a Workspace from the sidebar list and the New Session picker instead of deleting it: the host registry row, its session account, the files on disk and every session log are all kept, the Workspace and every session keep their account position, and **its sessions are hidden with it** (they never fall into Ungrouped, and search stops listing them); restoring the Workspace brings the whole group back. Restore it by re-adding the same folder (the host resolves it by canonical path and returns the same Workspace) or from the "Closed workspaces" list in the Style tweaks settings panel (each entry reveals its folder on hover). The entry sits in each Workspace row's `...` menu, above "Delete workspace", behind a confirmation that spells out what is kept. Three boundaries: a closed Workspace's manual session order is not preserved (sessions show in host account order); closing the Workspace a session is currently open in hands you straight to a new session (in the most recent visible Workspace); and "New session" triggered from a session inside a closed Workspace falls back to the most recent **visible** Workspace (the host's own fallback).
|
|
33
38
|
|
|
39
|
+
- **Custom history page size (default off)** — DSH chooses its own request size (the current 0.1.7 line uses 500 for a cold open and "Load earlier", with a separate 200-message lower bound for turn jumps). When ON, every applicable request uses the configured size **exactly** (50–1000, default 500), whether that is larger or smaller than DSH's native value; the rewrite happens on the unary `session/page` used by "Load earlier" and turn jumps, plus the optional `session/follow` open frame for a session's first screen. The host's turn-window minimum is synchronized to the same value, so a large page is not cut short after only two turns by the native 50-message boundary. Three boundaries: **the configured value is exact**, rather than only a lower bound; **the first screen has its own "Apply to session open too" toggle (default ON)** — turning it off leaves DSH's native first screen untouched while the size still applies to later pagination; **the master switch off fully restores DSH's own behaviour**, keeping the saved size for the next enable.
|
|
40
|
+
|
|
41
|
+
- **Custom sidebar session count (default off)** — DSH folds every Workspace's session list to five rows and grows it five at a time; both numbers are a `useState` inside the host's workspace-browser component (`COLLAPSED_SESSION_LIMIT`), and the persisted view store keeps no key for either, so no setting, slot or store action reaches them from outside. When ON the plugin owns the list: a Workspace shows the configured "rows shown initially" at rest, and each press of "show more" adds the configured step (both floored at the host's own 5 and uncapped above it; the steppers move one row per click). It does not replace the host's control — it drives the one the host already renders: the plugin presses the host's overflow button until enough rows are rendered, hides the surplus behind a marker attribute of its own, and puts its own button where the host's was, carrying the host's own `workspace` wording (`sessions.expand` / `sessions.collapse`) and the honest remainder as the figure; once everything is shown the button reads the host's "collapse" and trims back to the initial count (the host's own fold can only express five, so collapsing is the plugin's job). Three boundaries: **the selected session row is never hidden** (opening a session — including one found through the host's own search — marks the row `aria-selected="true"`, and the trim always yields); **growing is never hidden** (the host's own row glide already presents the 5 → 10 → 15 steps as one growth, and a hide short enough to be one frame still reads as a flicker, so the plugin never adds a `visibility` toggle); and **steady state costs nothing** (a per-group fingerprint lets a sweep skip groups whose settled state has not moved, so a sidebar whose rows keep updating for other reasons is not recomputed again and again). OFF (default) restores DSH's own folding and its own button outright; hosts without these structural anchors leave the tweak inert.
|
|
42
|
+
|
|
43
|
+
> Field and default reference (maintained by the Settings panel). **On DSH 0.1.7+ the live copy is the `value` object of `<profile>/.dsh-style-tweaks/store.json`** — same field names as below, plain JSON, hand-editable and **picked up on the next read** (reopen/refresh the Settings panel); this plugin's `config` row in `cordis.patch.yml` is no longer read and exists only as the seed for a missing store file (delete that file to re-seed from it; the row in this machine's profile normally carries no `config` block at all, which is expected — while the store exists that block takes no part in reads or writes). Older hosts keep the values in the settings namespace. Because the store is partial, an omitted field uses its current default; existing Desktop/Web profiles without `turnProcessCounts` therefore show the counts by default, and an explicit `false` keeps the stock header.
|
|
44
|
+
|
|
34
45
|
```yaml
|
|
35
46
|
style-tweaks:
|
|
36
47
|
# Layout
|
|
37
48
|
usePluginWidth: false # default false; true lets the plugin input/presets own the column
|
|
38
49
|
dialogWidth: 748 # 600–1600 px; shown/effective only while usePluginWidth is on
|
|
39
50
|
sideMargin: 50 # ≥ 32 px; shown/effective only while usePluginWidth is on
|
|
40
|
-
thinkFixedHeight: false # default false; true caps the think body at a fixed height and scrolls it
|
|
41
|
-
thinkHeight: 300 # 120–1200 px; shown/effective only while thinkFixedHeight is on
|
|
42
51
|
rightbarInitialWidth: false # default false; true lets the plugin own the right sidebar's first-open width
|
|
43
52
|
rightbarWidthPercent: 45 # 15–70; right sidebar first-open width as a percentage of the session frame
|
|
53
|
+
desktopSettingsLauncher: false # default false; Desktop-only; true shows [More] [Settings gear]
|
|
54
|
+
# History loading
|
|
55
|
+
historyPageSizeEnabled: false # default false; true sends the size below on every history request (the panel's master switch)
|
|
56
|
+
historyPageSize: 500 # 50–1000; shown/effective only while historyPageSizeEnabled is on
|
|
57
|
+
historyPageSizeColdStart: true # default true; a session's first screen uses that size too
|
|
58
|
+
# Sidebar session list
|
|
59
|
+
sidebarSessionCountEnabled: false # default false; true lets the counts below own each Workspace's session-list folding
|
|
60
|
+
sidebarSessionInitialCount: 5 # ≥ 5; session rows a Workspace shows at rest (shown/effective only while sidebarSessionCountEnabled is on)
|
|
61
|
+
sidebarSessionExpandStep: 5 # ≥ 5; rows one "show more" press adds (same gate)
|
|
44
62
|
# Tweaks
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
stableSessionTitle: false # default false; true keeps session titles put with their ellipsis on hover
|
|
64
|
+
hideSessionHoverActions: false # default false; true keeps the Archive / Pin hover buttons off session rows (the "..." menu keeps both)
|
|
47
65
|
keepTurnRail: false # default false; true keeps the turn-navigation rail at every chat width
|
|
48
66
|
codeBlockFlushTop: true # default true; false disables the tweak
|
|
49
67
|
projectRunningIndicator: true # default true; false disables the tweak
|
|
@@ -52,7 +70,12 @@ style-tweaks:
|
|
|
52
70
|
sidebarMiddleClickClose: true # default true; false disables middle-click tab closing
|
|
53
71
|
legacyStatsLine: false # default false; true swaps the pills for the 0.1.2 text line
|
|
54
72
|
pillsCacheHitDecimals: false # default false; true shows the cache hit with two decimals (pills and legacy line)
|
|
55
|
-
|
|
73
|
+
turnTimePill: false # default false; true restores the turn-time reading 0.1.7 removed (next to the usage reading, with its "Turn time and speed" dialog)
|
|
74
|
+
turnProcessCounts: true # default true; false keeps the folded process group's stock 0.1.7 header
|
|
75
|
+
runningStatus: true # default true; restores 0.1.6's blue live status above the composer on 0.1.7
|
|
76
|
+
opaqueStatDialogs: false # default false; true gives the five readout stat dialogs 0.1.6's opaque fill (menus and panels keep 0.1.7's frosted material)
|
|
77
|
+
legacyContextMeter: false # default false; true puts the context ring back inside the input card (0.1.6-alpha.2+)
|
|
78
|
+
contextPillNoTooltip: false # default false; true suppresses the context meter button's hover tooltip (0.1.6-alpha.2+; the row hides while the previous toggle is on — the stored value still applies)
|
|
56
79
|
workspaceClose: false # default false; true enables closing (hiding) a Workspace from its row menu
|
|
57
80
|
closedWorkspaces: [] # ids of closed Workspaces (internal data; managed by the panel's list)
|
|
58
81
|
```
|
|
@@ -73,8 +96,8 @@ The package spec after `add` is forwarded to pnpm verbatim, so versions can be
|
|
|
73
96
|
pinned — `@version` for the npm package, `#tag` for the GitHub source:
|
|
74
97
|
|
|
75
98
|
```bash
|
|
76
|
-
dsh plugin --profile web add dsh-style-tweaks@0.1.
|
|
77
|
-
dsh plugin --profile web add github:zhj9709/dsh-style-tweaks#v0.1.
|
|
99
|
+
dsh plugin --profile web add dsh-style-tweaks@0.1.6 # pin the npm version
|
|
100
|
+
dsh plugin --profile web add github:zhj9709/dsh-style-tweaks#v0.1.6 # pin a git tag
|
|
78
101
|
```
|
|
79
102
|
|
|
80
103
|
Restart DSH web once after installing (bundle plugins are scanned at process start).
|
|
@@ -232,7 +255,7 @@ source change has to be copied over — exactly the step the `link:` setup spare
|
|
|
232
255
|
```bash
|
|
233
256
|
pnpm build
|
|
234
257
|
pnpm pack # produces dsh-style-tweaks-<version>.tgz (gitignored; do not commit)
|
|
235
|
-
dsh plugin --profile web add ./dsh-style-tweaks-0.1.
|
|
258
|
+
dsh plugin --profile web add ./dsh-style-tweaks-0.1.6.tgz
|
|
236
259
|
```
|
|
237
260
|
|
|
238
261
|
**Restart `dsh web` once** after installing (bundle plugins are scanned at process start).
|
|
@@ -277,7 +300,7 @@ path dependency:
|
|
|
277
300
|
|
|
278
301
|
```bash
|
|
279
302
|
dsh plugin --profile web remove dsh-style-tweaks
|
|
280
|
-
dsh plugin --profile web add ./dsh-style-tweaks-0.1.
|
|
303
|
+
dsh plugin --profile web add ./dsh-style-tweaks-0.1.6.tgz
|
|
281
304
|
```
|
|
282
305
|
|
|
283
306
|
The only difference between the two setups is whether the artifact lands in place automatically: both
|
|
@@ -286,18 +309,30 @@ to verify the published package itself.
|
|
|
286
309
|
|
|
287
310
|
## How it works
|
|
288
311
|
|
|
289
|
-
- **Server** (`src/index.ts`)
|
|
312
|
+
- **Server** (`src/index.ts` / `src/store.ts`) declares the `style-tweaks` settings namespace and mounts a same-origin route (`/_dsh/style-tweaks/settings`). Since DSH 0.1.7 that namespace IS this plugin's own entry Config (the entry id is the namespace, its fields carry the `.volatile()` marker), but **values are read and written through the plugin's own store, `<profile>/.dsh-style-tweaks/store.json`** — the route reads one file per request and writes it back with a revision CAS, skipping the host's settings pipeline (measured 905–1114ms per write vs ~10ms for the store; see `.docs/PLAN-DIAGNOSIS-index.md`), seeded once from the entry Config's explicit fields when the file is missing; older hosts go through `ctx.settings.register` and the original settings path, with the same namespace.
|
|
290
313
|
- **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).
|
|
314
|
+
- **Desktop Settings launcher** (`src/client/tweaks/desktop-settings-launcher.ts`) observes the Desktop account menu only when the Desktop-only `desktopSettingsLauncher` preference is enabled, inserts a gear immediately after the More trigger's row, and produces `[More] [⚙ Settings]`. The gear is not hard-coded to a pixel size: it measures the live More trigger and matches it exactly (44px in the wide sidebar), keeping the host's own centring with no added height and no negative margin, so the two hover backgrounds come out the same height. The gear uses a short-lived portalled-menu bridge to activate the host's native Settings action, so the original full Settings window is reused; the duplicate Settings row is hidden while feedback, sign-in, sign-out, and other account actions remain. The preference defaults off, restores native More → Settings when disabled, and is not shown or mounted in Web. The module falls back to the native menu if the account-menu semantics, portal placement, or CSS Modules fragments no longer match.
|
|
291
315
|
- **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.
|
|
292
316
|
- **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/`.
|
|
293
|
-
- **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` (
|
|
294
|
-
- **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
|
|
317
|
+
- **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` (mounted from `@deepseek-ai/dsh-client-ui-primitives`, but the ongoing state's look is replaced wholesale by this tweak. The host's own ongoing dot is a breathing arc on a ring, and since 0.1.7 it is frozen into a still ring under `prefers-reduced-motion: reduce` — with that preference set, **every** running dot in the app, the title's own included, simply stops. A still ring cannot say "work in flight", so every ongoing dot in the app is redrawn as ZCode's spinner: lucide's `loader` icon — eight evenly spaced spokes, radius 6..10 of a 24 viewBox, ~2 units thick, round caps, all equal, with **no** opacity trail — turned at `animate-spin`'s constant 1s per revolution, regardless of the system setting. CSS can only reach the host's `svg`, `g` and two circles, so the spokes are an SVG mask on the svg element: lucide's own `loader` path data turned into a `data:` URI (`mask-size: 100% 100%`, so any StateDot size works), with the svg itself filled `currentColor` so the colour still comes from the host's CSS module (tertiary grey); everything the host draws inside the svg is switched off (`> *`, not just today's two circles, so a host that redraws its spinner with other elements stays covered), and `:has([class*="spinnerMotion"])` keeps it to real StateDots. A mask rather than a `conic-gradient` because gradient wedges rasterise differently per angle and end up looking uneven in weight, while an SVG stroke (round caps) rasterises exactly like the icon ZCode itself draws. The override stops at that component: the host's other reduced-motion answers — tool-row sweeps, text shimmers, progress bars — keep honouring the setting) 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 match workspaces by group key (`ProjectRowItem`'s `props.group.key`, i.e. `GroupNode.key`) rather than by title text: `GroupNode.label` is the directory basename, so two same-named directories under different parents collapse to one shared label (the Host only rejects a colliding *rename*, not a duplicate directory name) and title matching would light both — which is exactly why two `pi-web` directories once showed the indicator together. 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.
|
|
318
|
+
- **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 first reads the current session's **id** off the conversation header — `ConversationSessionHeader` and the header components above it all memoize the `sessionId` they render, so the nearest one walking up the fiber chain from the breadcrumb nav is the session on screen; a sidebar row keeps that same value as `props.node.id` (`SessionNodeItem`), so "the current session" and "some sidebar row" are keyed alike. `ctx.get('workspaces')` then maps the session id to its workspace **id** (works even when the workspace's sidebar group is collapsed), and the workspace row is found by that id (`ProjectRowItem`'s `props.group.key`, read off the fiber chain) rather than by exact title match — two Workspaces whose directories share a basename share a label, and a title match then opens whichever one comes first. It 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' })`, matching rows by id at every step rather than by the text they render. **As of 0.1.7-alpha.1 this no longer goes through the breadcrumb title**: that build renders the current crumb as a plain `<span class="…crumbCurrent">` instead of a `button[disabled]` (plain text is what lets the title join the desktop window's drag band, harness `92101e1a5b`), and a title was never a reliable identity anyway — two sessions may share one, and a session with no recorded ancestry is painted as its raw id. The title survives only as the fallback path used when no id can be read (`readCurrentSessionTitle` accepts both the span and the older disabled button, so 0.1.6 keeps working), and that fallback is the only thing `ctx.get('sessions')` is still needed for. 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 **The mount point is verified, not inferred from "walk up two levels"**: DSH paints a **second** search button for the collapsed 56px rail — the same `searchButton` class and the same `search.sessions.aria` aria-label, only the icon is 18px instead of 14px — but it hangs straight off the workspace browser's root (rail: `root > search > searchButton`; wide: `root > sectionHeader > searchSlot > search > searchButton`). Counting two parents up from the rail's button therefore lands on `root`, not `searchSlot`, and the button gets injected into a container React owns: a stray locate icon shows up in the rail, and since React only unmounts fibers it created, that orphan survives the rail's own teardown as a child of `root` that no re-render clears — it stays over the expanded sidebar until the page is reloaded. So mounting first asserts that two levels up is a `div[class*="searchSlot"]` and bails out otherwise (the rail has no section header at all, so the button rightly does not exist there), and every sync additionally prunes any `cst-locate-btn` not sitting in a `searchSlot`, which makes the invariant self-repairing. MutationObserver watches `aria-selected` / `aria-expanded` attribute changes to keep the button's enabled state and mount in sync.
|
|
295
319
|
- **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`.
|
|
296
320
|
- **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.
|
|
297
321
|
- **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.
|
|
298
|
-
- **
|
|
299
|
-
|
|
300
|
-
- **
|
|
322
|
+
- **Custom sidebar session count** (`src/client/tweaks/sidebar-session-count.ts`): the host folds each Workspace's session list to five rows (`COLLAPSED_SESSION_LIMIT`), and that limit is a `useState` inside its browser component — the persisted view store keeps only `groupBy / orderBy / groupExpansion / sessionOrderByAccount / archivedFilter`, so no setting, slot or store action reaches it from outside the host. The tweak therefore **drives the control the host already renders** instead of replacing it. The host's overflow button carries `data-row-key="overflow:<groupKey>"` and `aria-expanded` (true once nothing is hidden), and pressing it is the only way to make the host render more rows, so the tweak presses it on the user's behalf and then decides what stays on screen: **grow** (press until at least the configured number of rows is rendered; the host advances five at a time and finally jumps to "all" — `Infinity` — when the remainder is five or fewer, so the loop terminates on either the row count or the button going away), **trim** (hide the rows past the configured count — the rows carry no inline style of their own, verified on 0.1.7-rc.2, so one marker attribute `data-cst-sid-hidden` plus a `display: none !important` rule is enough, and a host re-render that keeps the same elements leaves the markers alone), and **re-label** (hide the host's button wholesale — at the style layer rather than from script, since the host inserts the button in the same commit as the rows and an observer only reaches it two microtasks later, which would leave its recomputed-on-every-render count on screen meanwhile — and put the plugin's own `data-cst-sid-button` in its place, carrying the host's own `workspace` wording (`sessions.expand` / `sessions.collapse`) and the honest remainder as the figure: the "N more" in the host's label plus the rows this trim is holding, i.e. `total − shown`). Pressing the plugin's button changes only the trim count; growing is always delegated back to the host, so the host's own "show everything" state backs a long list, while collapsing trims back to the initial count rather than asking the host to fold to five (the host's own fold cannot express a custom count). **The grow is never hidden**: an earlier revision marked the list `visibility: hidden` during a grow to conceal the 5 → 10 → 15 ladder, but that window is one frame (a press is a single render, well under 100ms) and a hide that short still reads as a flicker — the list vanished and came back — while the host's own row glide already presents the steps as one growth, so growth is now always shown. **The button is published before the rows, not after**: the figure never needed the wait — the total is the host's own "N more" label plus the rows it has rendered, and `shown` is the trim the pass is about to apply, both known the moment the group opens — so the button is published before the grow starts, carrying the figure the grow was going to produce (measured: the button used to appear at t=199ms with all eleven rows already on screen by ~50ms); the one case that cannot publish early is a host label with no integer in it (everything already shown, or not painted yet), which is read the settled way. **The wait machinery**: the host's row glide runs ~200ms (`ROW_GLIDE_MS`) and a departing row stays in the DOM for that window, where it still matches the row selector, so a corrective pass follows after `GLIDE_SETTLE_MS` (300ms); waits prefer rAF and fall back to a timer (an occluded window suspends rAF), each settle carries a wall-clock ceiling so a suspended frame cannot freeze a group behind its busy mark, and a press landing while a pass holds the group is queued and served by that pass's release rather than dropped. **A selected row is never hidden**: opening a session — including one found through the host's own search, which reveals the row and scrolls to it — marks it `aria-selected="true"` and the trim yields; when the host itself lifts a group's limit to reveal a row, that row renders first and the trim is re-applied on the next tick. **Steady state costs nothing**: a per-group fingerprint of everything a pass reads or writes lets a sweep skip groups whose settled state has not moved, so a sidebar whose rows keep updating for other reasons no longer buys every open group a pass per mutation. Every anchor matches on the stable semantic fragment (`groupSection` / `sessionOverflowButton` / `list` are CSS-Modules hashes that rotate per build), and `data-row-key` is the host's own contract carrying the group key — the key every per-group state is held by.
|
|
323
|
+
|
|
324
|
+
- **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 skin adapts to the host's dock generation: 0.1.6-alpha.2 moved the dock slot's output into a centered flex row inside the InputBar (beside the new ContextMeter capsule, the context pill — the slot output is no longer a free row under the input card), and the row root probes its layout parent at mount (walking up past the `display: contents` wrapper the slot machinery inserts — the wrapper generates no box, so its `flexDirection` computes to the initial `row` and a direct read would classify both dock generations as row-flex) — a column flex container (alpha.1 and earlier hosts, where the row hangs directly under the card) takes the old skin: 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 4px of top + 2px of bottom padding so its total matches the pills row (26px row + 4px host clearance on both presentations); a row flex container (alpha.2's `.dock` wrapper, which owns the 4px top clearance, with a fixed 4px root bottom pad and the `data-composer-stats` rule removed — the marker is inert there) takes the new skin: the row sizes to its content with no vertical padding of its own, centered beside the capsule at the host's native 12px gap — toggling the tweak shifts neither the conversation nor the input box on either generation. 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.
|
|
325
|
+
- **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 (0.1.6-alpha.2 removed the host's bottom-clearance rule behind it; the marker only acts on earlier hosts), with the row skin following the dock generation the same way the legacy line's does (see `composer-dock.ts` and the previous entry). 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.
|
|
326
|
+
- **Turn time and speed** (`src/client/tweaks/turn-time-pill.tsx` + `src/client/tweaks/assistant-stream-timing.ts`): 0.1.6's `TurnUsagePanel.tsx` held two components side by side — `TurnUsagePanel` (data glyph + usage) and `TurnTimePanel` (clock glyph + time, opening the 本轮用时和速度 dialog) — and `TurnTailNodeView` passed both to the actions row as one `usageAction` fragment; 0.1.7-alpha.1's performance-and-usage preference deleted `TurnTimePanel`, `TurnTailChatData`'s `ttftMs`/`tokensPerSecond`, `formatLatencySeconds`, and `message.ranFor` with the whole `message.turnTime.*` group. This tweak puts the time half back whole: the pill, the dialog, and 0.1.6's three rows inside it. Placement: it takes the only route a plugin has into host chrome — an invisible anchor on the `conversation.chat.assistant-actions` list slot (whose list renders between the copy and branch controls), from which the visible pill is portaled into the tail's own trailing cluster, the span wrapping `usageAction` and the clock (found structurally as the last element child of the row carrying `data-clock="end"`). The portal lands last in that cluster, so two CSS rules restore 0.1.6's seating: the clock (the cluster's `_timeEnd` span, the portal's previous sibling in the DOM) is ordered after the pill, and the 8px flex gap against the usage pill is rebated by 6px — 0.1.6's own `.root + .root` pairing — so the row reads usage → time → clock again, exactly the cluster 0.1.6 printed; CSS `order` rather than DOM insertion keeps that placement stable no matter when React inserts the host's own children (a cluster without the usage pill keeps the plain 8px rhythm, one without a clock ends with the pill). Figures: 0.1.6 read them off the Chat snapshot's turn location (wall time) and the node's in-memory timing, neither of which survives in 0.1.7's node data, so all three are folded out of the session event window instead — the wall time over `turn/start` → `turn/end` (`deriveTurnRunMs`), the output speed and TTFT over the settlements' embedded model streams (`deriveTurnSpeedMetrics`). The latter is exactly the consumer the session-format-v2 (0.1.5) architecture note anticipates: v2 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 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. The compact-stream readers are ported verbatim from dsh-llm (`assistantStreamFirstTokenTime`: reconstructs the first token's time inside a packed run from `time0` plus the `dt` gap list) together with 0.1.5's `deriveTurnMetrics` fold — 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 — and everything rides the durable log, so history loads and freshly settled turns show identical figures. Timing: the tail only exists once the turn settled, so the values are final; while `turn/end` has not reached the window (or was evicted by its cap) the tweak watches the `eventSource` subscription until the wall time appears and then stops (settlements always precede `turn/end`, so both derived figures are ready at that moment), and a turn with no `turn/start` in the window renders no pill, leaving the 0.1.7 row untouched. Dialog: a port of the host's stat-dialog seat over the same public primitives the host itself uses (`useAnchoredPosition` for the viewport-clamped placement above the trigger, `useDismissOnOutsidePointer` for outside dismissal) plus an Escape handler, with the surface skin copied from `stat-dialog.module.css`; its details grid carries the host's `data-turn-time-details` marker and keeps 0.1.6's `TurnTimePanel` row order and "a row only when its figure is known" rule (total run time → output speed → TTFT). The two figure labels come from this plugin's own `style-tweaks` dictionary (the host's surviving `stats.dialog.speed` / `stats.dialog.ttft` pair is worded for the composer's whole-session dialog, where `ttft` reads "首 token 平均", an average that does not hold for one turn), while the values keep the host vocabulary's `message.tokensPerSecond` / `duration.seconds` keys. All three rows render in the mount commit, so nothing is ever appended to the panel after it is measured and the first painted frame is already final (the earlier "render one row, inject two on open" approach needed a synchronised card lift to avoid a one-frame flash; rendering the rows up front replaced it). Selector safety: every hook is structural (`data-turn-tail` / `data-clock="end"` / the `_timeEnd` class suffix inside the cluster / `data-turn-time-details`), the pill's own classes are namespaced `cst-ttp-`, and a host that renames `timeEnd` degrades to the pill simply ending the row.
|
|
327
|
+
- **Call counts on process groups** (`src/client/tweaks/turn-process-counts.ts`): 0.1.6's `TurnProcessNodeView` built its header label out of `node.data.toolCallCount` / `messageCount` / `subagentCount`, joining the non-zero parts with `message.turnProcess.separator` (falling back to "已思考" when all three were zero); 0.1.7-alpha.1's performance-and-usage preference replaced that label with the elapsed-time wording (`message.turnProcess.took` / `deepDivingFor` / `failed` / `stopped` / `worked`). The counts themselves did not go away — the node data still carries all three, the host still writes them onto the disclosure button as `data-turn-process-tool-calls` / `-messages` / `-subagents` (`button[data-turn-process]`), and the whole `message.turnProcess.*` tally vocabulary is still in the shipped `chat` dictionary. The process group is a transcript node view, not a slot, so a plugin has no render seat inside it and the tweak patches the DOM instead: it appends one `span.cst-tp-counts` child to the host's button and never touches the host's own children. The text is rebuilt by 0.1.6's own rule — a zero (or missing) count is left out, a single count takes the `one` key, the parts are joined with the host's own separator, and the separator leads the string so the tally reads as a continuation of the host's label (which also keeps it off the truncated tail of a narrow label, since the label is the part that ellipsizes). Order is fixed in CSS rather than by insertion position: the tally must read after the host's label and before the chevron, and the chevron is conditional (`canCollapse`) — when React adds it later it appends to the end of the button, which would land it after the tally. So the tally carries `order:1` and the chevron `order:2` on the button's own flex row (the host's `.root` is `display:flex`), and the reading order holds however React re-inserts its children; the tally also takes the host's font-size / line-height tokens and `flex:none`, so the label still shrinks and ellipsizes exactly as before. Syncing rides one MutationObserver on `document.body` whose `attributeFilter` names only the three count attributes (`data-open` / `aria-expanded` / class churn never schedules a sweep) and whose childList half walks only added subtrees for `button[data-turn-process]`, so a sweep costs what the batch touched rather than what the document holds, with the batch coalesced into one pass per microtask. A host that renders process groups some other way (no such attributes) leaves the tweak inert.
|
|
328
|
+
- **Blue running status at the tail** (`src/client/tweaks/running-status.ts`): through 0.1.6, `ChatView` rendered a dedicated `TurnStatus` after the node list. While `running`, it stayed at the live transcript tail—immediately above the composer—as a 26 px one-line brand-blue shimmer, with an ordinary caption-coloured clock added after fifteen seconds. 0.1.7's grouped-work-details refactor removed that React component. `TurnProcessNodeView` still emits a visually-hidden `role="status"` announcement («深度求索中 / Deep diving») before its process button, and the visible header still carries the live clock, but an open process group is forced open: its header stays above the entire reasoning/tool run, so accumulating calls push it away from the composer and the old blue line appears to vanish. This tweak neither moves a host node nor edits the process group. It recognizes an open turn when the direct accessibility announcement beside the current `button[data-turn-process]` is a **non-settled state**, then appends one `div.cst-running-status` to that control's `[data-chat-flow]`. It uses the announcement text as-is rather than forcing a fixed “Deep diving” label, so a companion plugin that changes it to “Deep sleep” / “Deep Sleeping” is followed too. The visible row is a visual clone of the host's current label and is `aria-hidden`; the host announcement remains the sole live region, so one-second clock ticks are not read aloud. Duration extraction is not a zh/en string table: a private marker is substituted into the same `message.turnProcess.deepDivingFor` template the host used to build the label, and the text between that template's literal prefix and suffix is read back, preserving placeholder order in any locale. If a companion plugin changes the wording or template, the complete customized label is kept as one blue line. The row also takes the flex column's last `order`, and is moved back to the DOM tail if React later appends an admitted local echo. Its shimmer, typography, clock placement, and static `prefers-reduced-motion` branch are ported item-for-item from 0.1.6. Hosts through 0.1.6 already render their own `TurnStatus` and do not put the 0.1.7 announcement beside a process button, so the selector stays inert; the same is true if a future host changes either structural anchor.
|
|
329
|
+
- **Opaque stat dialogs** (`src/client/tweaks/opaque-stat-dialogs.ts`): 0.1.7-alpha.1's "Compact translucent menu surfaces" decision (harness note, 2026-09-17) swapped the menu / card material from opaque to translucent-plus-blur — `--dsw-specific-menu` went from 0.1.6's `var(--dsw-alias-bg-layer-3)` to `rgba(248, 249, 250, 0.58)` light / `rgba(48, 49, 54, 0.5)` dark, with a new `--dsw-menu-backdrop-filter: blur(40px) saturate(150%)` that every surface painting that fill must pair with (the host's own stylesheet gate rejects a material layer that omits the filter). This tweak restores the opaque fill on **only** the five click-open stat dialogs (ui-chat's README calls the composer pair 统计交互卡片 / "interactive statistic dialogs", all of them rendering through the host's one `stat-dialog` seat plus the ContextMeter panel): the composer's session-statistics and token-usage cards, the turn-tail per-turn usage and time pills, and the context-occupancy panel. **Why not the token**: `--dsw-specific-menu` *is* the whole elevated-surface family's single definition point, so re-pointing it repaints every menu and panel in the client — far wider than these five dialogs (that was this tweak's first cut, dropped when the scope narrowed). **How the five are targeted**: three of them (the turn-tail usage dialog and both composer stat dialogs) are the host's one `stat-dialog` panel (`ui-chat/src/client/chat/stat-dialog.module.css`, whose direct children are `.title` / `.titleRule` / `.details`), one is the ContextMeter panel (direct children `.header` / `.bar` / `.rows`), and two are this plugin's own ported panels (`.cst-ttp-panel` / `.cst-pilldlg-panel` — plain namespaced classes, nothing to guess). The host marks neither panel kind with any `data-*` attribute, and their class shape — `*_panel` with `role="dialog"` — is shared by a set of UNRELATED dialogs (the settings panel's `SettingsRoot.panel`, the document preview's `FontNotice.panel`, the agent team's `TeamAction.panel`, the memory-save modal), so the two are separated by their own direct children instead: `:has(> [class$="_details"])` and `:has(> [class$="_rows"])`, the same structural-hook discipline as `[class$="_timeEnd"]` / `[data-turn-tail]` elsewhere in this plugin — and that is what keeps the settings panel and the rest out (measured: with the settings dialog open, the selector matches nothing). **Specificity**: the host's rule is `.panel` (0,1,0) and the plugin's own two ports are also (0,1,0), so each rule here carries a `body` element prefix — one extra element selector settles both ties, with no `!important` and no dependence on which of the plugin's stylesheets is injected last. **The `backdrop-filter` is deliberately left alone**: under an opaque fill the blur is fully covered and therefore invisible, while keeping the property preserves the host's own isolation / containing-block semantics (a few of those surfaces paint their material on an isolated background pseudo-element or rely on the filter giving their fixed overlays a containing block). The dark theme needs no separate value because layer-3 is the theme's own dark surface color, and hosts before 0.1.7-alpha.1 already paint the opaque value, making this a same-value no-op there. Side fix: two of this plugin's own ported panels (`pills-cache-hit-decimals`' dialog, `legacy-context-meter`'s panel) had missed the `backdrop-filter` line 0.1.7 added (the `turn-time-pill` port already carried it) — that is now added, so with this switch off the plugin's dialogs match the host's material exactly.
|
|
330
|
+
- **No context meter hover info** (`src/client/tweaks/context-pill-no-tooltip.ts`): the 0.1.6-alpha.2 ContextMeter capsule is wrapped in ui-primitives' `Tooltip`, whose bubble (`span[role="tooltip"]`) is the **adjacent sibling** of the `cloneElement`'d anchor — so one plain CSS rule hides it exactly: `button:has(> svg[viewBox="0 0 14 14"] > circle) + span[role="tooltip"] { display: none }`. The anchor's structural identification is unique across the client UI: every circle-bearing icon in the icon set (gauge, database, …) is a 16×16 viewBox, and the only 14×14 ring with `circle` children is the ContextMeter's gauge (TodoPanel's 14×14 glyphs are path/rect, circle-free), while the `+` adjacent-sibling combinator binds the bubble to its own anchor only — every other Tooltip in the composer (the + attach circle, etc.) and the plugin rows' own hover tooltips carry no such svg and are untouched. The rule covers both the hover and the keyboard-focus trigger (the same bubble element; the click-open breakdown dialog remains the entry to the full figures); the bubble is driven by React state and the hiding happens at the style layer after it renders, so nothing of the host's behaviour changes and no JS listeners are involved. Hosts up to 0.1.6-alpha.1 ship no ContextMeter, the selector matches nothing, and the tweak is inert.
|
|
331
|
+
- **Context ring in the input card** (`src/client/tweaks/legacy-context-meter.tsx`): 0.1.6-alpha.2 moved the ContextMeter out of the input card's toolbar row into a new `.dock` wrapper below the card (a ring-and-percent capsule next to the stats row). This tweak restores the pre-alpha.2 presentation: a 28px ring-only button back inside the toolbar row, left of the send button. It deliberately avoids a DOM move — the capsule is a React-owned node, and when React commits an insertion it finds its `insertBefore` reference by walking the fiber tree (`getHostSibling`), not the DOM, without verifying that the reference is still a child of the container — so once the capsule were moved out, a stats row mounting at that moment would throw NotFoundError inside the commit and take the whole app down. Instead it works the way "cache hit with two decimals" does: it registers a cell on the `conversation.input.right` list slot (the toolbar's right group, inside `.trailing` in normal flex flow) with a style-for-style port of the alpha.1 ContextMeter — 28px grid ring, hover reading, the click-open 264px occupancy breakdown, outside-click + Escape close, and the `contextOccupancy` fold ported verbatim — riding the same `contextPressure` / `contextBreakdown` projections, reusing the host primitives' own `Tooltip` for the hover reading, and keeping alpha.1's absolutely-positioned panel skin (anchored above the ring, right-aligned). Host generation is probed by the cell itself (layout effect, settled before the first paint): from its own position it walks past the `display: contents` anchor to its composer card and checks the InputBar root's own children for a `div` whose hashed class carries the `_dock` local name — present means the alpha.2+ generation and the ported ring renders; absent (0.1.6-alpha.1 and earlier) the cell renders only a hidden (`display: none`) ref-holder (the probe needs a mount point; the hidden shell takes no space and adds no row gap), the native ring already holds that seat, and the tweak is fully inert. Three companion CSS rules: `[class*="_dock"] > span:has(> button > svg[viewBox="0 0 14 14"] > circle) { visibility: hidden; width: 0; margin-left: -12px }` hides the shipped capsule — through `visibility` rather than `display`, and collapsing only its width, because the box must stay as a 22px height floor: the two stats presentations are 20px (legacy line) and 22px (pills) tall, so with the capsule's box fully gone every toggle of the stats rows — or a session with no stats at all — changes the composer's height and pushes the whole input area (the shipped capsule always occupied those 22px, which is exactly why stock 0.1.6-alpha.2 does not jump when the stats rows toggle). Keeping the real element means the floor follows a font-size preference for free, with no hard-coded height, while `visibility: hidden` also takes the capsule out of hit-testing and the accessibility tree; the `margin-left: -12px` cancels the dock's 12px gap so the stats row keeps sitting exactly on the card's centre line (the pre-alpha.2 line's position). Two `order` rules place the ported ring after the model chip and before the stop/send cluster (`conversation.input.right` cells render before the model chip; through the `display: contents` anchors the order declarations act on the visible boxes directly), reproducing alpha.1's in-row order exactly; the two are additionally gated on an ancestor that owns a dock child (`div:has(> div[class*="_dock"])`), so they never enter the cascade on earlier hosts rather than merely restating the native order. Layering with "no context pill hover info": that rule's anchor test matches the ported ring as well, so the one switch covers both presentations; that switch's row is a **dependent row** — while this tweak is on the panel no longer renders it (hidden, never greyed out, the rule the dependent numeric fields follow), because the capsule it names has been replaced and hidden and leaving the row would only mislead. The stored value is untouched: turning this tweak off brings the row back with its value.
|
|
332
|
+
- **Stable session titles** (`src/client/tweaks/stable-session-title.ts`): 0.1.6-alpha.2 gave session rows a "hover reveal" — `Rows.tsx`'s `revealClippedTitle` scrolls the clipping title element to its far edge while the pointer rests on the row (`.sessionRow .title` gliding there through `scroll-behavior: smooth`) and `@media (hover: hover)`'s `.sessionRow:hover .title { text-overflow: clip }` drops the ellipsis for the reveal, with a one-step snap back on leaving; that glide is the "title moves on hover" motion (pre-alpha.2 builds have no such code — the title always rests at the start). This tweak turns the title from a scroll container into a **non-scrollable** clipped box with one CSS rule: `[role="treeitem"]:not([aria-expanded]) [class$="_title"] { overflow: clip !important; text-overflow: ellipsis !important }` — an `overflow: clip` box is not programmatically scrollable, so the host's `scrollLeft` / `scrollTo` assignments become no-ops (the host JS keeps running and simply has nothing to move), and the `!important` on `text-overflow: ellipsis` wins the importance tie against `.sessionRow:hover .title` (both 0,3,0), keeping the ellipsis on hover. The selector is structurally scoped: session rows are the workspace browser's `role="treeitem"` elements WITHOUT `aria-expanded` (workspace rows carry it), and the title is the span whose CSS-Modules class ends in `_title` (hashed as `<hash>_title`; the row hover card's `hoverTitle` local name differs and never matches). `overflow: clip` lays out identically to the shipped `overflow: hidden` (both clip in place), so rows whose titles never scroll are visually untouched; on 0.1.6-alpha.1 and earlier hosts the titles already rest at scroll position 0, and the same declarations change nothing.
|
|
333
|
+
|
|
334
|
+
- **Hide session hover buttons** (`src/client/tweaks/hide-session-hover-actions.ts`): the Archive and Pin buttons that float into a session row's trailing cell are not host inline markup — they are two injected entries of the workspace browser's `sidebar.workspaces.session.row.action` list (`session-actions/ArchiveSession.tsx`, `PinSession.tsx`), which `Rows.tsx` renders INSIDE the row's `.rowActions` strip; the `...` menu button in that same strip is the host's own and is not part of the list. This tweak hides only the slot's wrapper: the host tags every injected entry with `data-slot="…"` and `style="display: contents"`, so one `[data-slot="sidebar.workspaces.session.row.action"] { display: none !important }` beats that inline style (an important author declaration outranks a non-important inline one). The rule is not hover-scoped, so the buttons never appear — not on hover, and not while the row's menu is open either (a state in which the host otherwise pins the strip visible; that menu is where those two actions moved to, with the archive / pin rows registered on `sidebar.workspaces.session.menu.item`). The selector matches the slot name alone, so it cannot touch the `...` menu button (a sibling outside the wrapper), and no other trailing cell mounts that slot — workspace rows and search results included. Rows keep their hover fill, hover card, timestamp and menu button untouched; on hosts that do not tag injected entries with `data-slot` the tweak stays inert.
|
|
335
|
+
- **History page size** (`src/client/tweaks/history-page-size.ts`): the per-request history size is a client parameter, not a host setting: the current 0.1.7 line requests `maxMessages: 500` for a cold open and "Load earlier", while a turn jump carries `turnWindow.minMessages: 200`; the host requires `maxMessages` to be a positive safe integer and the turn-window minimum not to exceed it. The tweak therefore rewrites the two outgoing requests in the browser: the unary `session/page` POST body (`payload.args.request.maxMessages`) and the Gateway WebSocket `session/follow` open frame (a live 0.1.6-alpha.2 frame nests it at `payload.args.request.maxMessages`; the flat spelling is only a fallback — do not "clean up" the nested lookup). It installs once and then only moves targets: `installHistoryPageSizeTransport()` idempotently wraps `globalThis.fetch` and `WebSocket.prototype.send` (**never unwound** — undoing it inside other plugins' wrappers is not worth the risk), and every settings change just updates the shared target, so a save applies to the next request without re-patching. When enabled, `maxMessages` is set to the **exact** configured value rather than only being raised; the host's `turnWindow.minMessages` is set to the same value, so a large page is not cut short after only two turns by the native 50-message boundary. Because the settings read can land after the first follow frame of a load, the targets are seeded from localStorage (`dsh-style-tweaks.history-page-size`) at install time and only overwritten once the settings snapshot arrives (pushing defaults before the first read would clobber the seed back to disabled); both the seed and the pushed value are clamped to [50, 1000]. The targets and a monotonic ownership counter (the newest instance holds them) live in one object parked on `globalThis`, not in module state: the wrappers are therefore installed once per page, and every layer a re-evaluated bundle adds reads the same target (re-wrapping writes the same exact value and is therefore idempotent). Plugin teardown (disable, or a bundle reload in the same page) releases ownership and zeroes the targets, and only the instance still holding ownership may do so: a teardown arriving after the next instance took over is a no-op instead of a wipe of its settings. Two boundaries: if the settings read never lands (route unavailable, invalid document) the seed drives the whole page lifetime, so "master switch off restores DSH's own behaviour" does not hold within that page (the configured value stays clamped to [50, 1000]); and the rewrite only knows the unary `session/page` body and the `session/follow` open frame, so a host that moved pagination to another transport would silently stop being affected. In the panel, the two rows below hide while the master switch is off (the stored size survives), "Apply to session open too" remains visible whenever the master switch is on, and neither emptying the field nor typing a value below the floor silently commits the minimum.
|
|
301
336
|
|
|
302
337
|
## Acknowledgements
|
|
303
338
|
|