dsh-style-tweaks 0.1.0 → 0.1.2

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.
Files changed (30) hide show
  1. package/README.en.md +22 -7
  2. package/README.md +22 -7
  3. package/lib/client.js +1602 -16
  4. package/lib/client.js.map +1 -1
  5. package/lib/config.js +41 -4
  6. package/lib/config.js.map +1 -1
  7. package/lib/types/client/index.d.ts +58 -3
  8. package/lib/types/client/index.d.ts.map +1 -1
  9. package/lib/types/client/tweak-config.d.ts +22 -1
  10. package/lib/types/client/tweak-config.d.ts.map +1 -1
  11. package/lib/types/client/tweak-types.d.ts +12 -0
  12. package/lib/types/client/tweak-types.d.ts.map +1 -1
  13. package/lib/types/client/tweaks/assistant-stream-timing.d.ts +105 -0
  14. package/lib/types/client/tweaks/assistant-stream-timing.d.ts.map +1 -0
  15. package/lib/types/client/tweaks/legacy-stats-line.d.ts +75 -0
  16. package/lib/types/client/tweaks/legacy-stats-line.d.ts.map +1 -0
  17. package/lib/types/client/tweaks/pills-cache-hit-decimals.d.ts +58 -0
  18. package/lib/types/client/tweaks/pills-cache-hit-decimals.d.ts.map +1 -0
  19. package/lib/types/client/tweaks/registry.d.ts.map +1 -1
  20. package/lib/types/client/tweaks/sidebar-middle-click-close.d.ts +105 -0
  21. package/lib/types/client/tweaks/sidebar-middle-click-close.d.ts.map +1 -0
  22. package/lib/types/client/tweaks/stats-cache-hit.d.ts +31 -0
  23. package/lib/types/client/tweaks/stats-cache-hit.d.ts.map +1 -0
  24. package/lib/types/client/tweaks/thinking-scroll.d.ts +4 -0
  25. package/lib/types/client/tweaks/thinking-scroll.d.ts.map +1 -0
  26. package/lib/types/client/tweaks/turn-speed-metrics.d.ts +67 -0
  27. package/lib/types/client/tweaks/turn-speed-metrics.d.ts.map +1 -0
  28. package/lib/types/config.d.ts +100 -13
  29. package/lib/types/config.d.ts.map +1 -1
  30. package/package.json +23 -18
package/README.en.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # dsh-style-tweaks
2
2
 
3
- > Dependency version: deepseek-harness v0.1.2-rc.1
3
+ > Dependency version: deepseek-harness v0.1.5-rc.1
4
4
 
5
5
  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
6
 
@@ -8,9 +8,10 @@ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) web
8
8
 
9
9
  ### Layout
10
10
 
11
- - **Plugin width control (default on)** — when ON, the plugin's width input / presets drive the conversation column and DSH's native drag handles are hidden; when OFF, DSH's native handles own the column and the width input mirrors their value.
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.
11
+ - **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.
12
+ - **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.
13
+ - **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.
14
+ - **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.
14
15
 
15
16
  ### Tweaks
16
17
 
@@ -20,13 +21,19 @@ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) web
20
21
  - **Project running indicator (default on)** — shows the conversation title's animated running dot on the right side of each project directory in the sidebar, so a running conversation stays visible even when its group is collapsed.
21
22
  - **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
23
  - **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.
24
+ - **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.
25
+ - **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.
26
+ - **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.
27
+ - **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.
23
28
 
24
29
  ```yaml
25
30
  style-tweaks:
26
31
  # Layout
27
- usePluginWidth: true # default true; false hands the column back to native handles
28
- dialogWidth: 748 # 600–1600 px
29
- sideMargin: 50 # ≥ 32 px
32
+ usePluginWidth: false # default false; true lets the plugin input/presets own the column
33
+ dialogWidth: 748 # 600–1600 px; shown/effective only while usePluginWidth is on
34
+ sideMargin: 50 # ≥ 32 px; shown/effective only while usePluginWidth is on
35
+ thinkFixedHeight: false # default false; true caps the think body at a fixed height and scrolls it
36
+ thinkHeight: 300 # 120–1200 px; shown/effective only while thinkFixedHeight is on
30
37
  # Tweaks
31
38
  stableTable: true # default true; false disables the tweak
32
39
  stableTurnRail: true # default true; false disables the tweak
@@ -34,6 +41,10 @@ style-tweaks:
34
41
  projectRunningIndicator: true # default true; false disables the tweak
35
42
  locateCurrentSession: true # default true; false hides the sidebar locate button
36
43
  settingsNavScroll: true # default true; false disables the settings-nav scrolling
44
+ sidebarMiddleClickClose: true # default true; false disables middle-click tab closing
45
+ legacyStatsLine: false # default false; true swaps the pills for the 0.1.2 text line
46
+ pillsCacheHitDecimals: false # default false; true shows the cache hit with two decimals (pills and legacy line)
47
+ turnSpeedMetrics: false # default false; true refills the turn-time dialog with output speed and TTFT
37
48
  ```
38
49
 
39
50
  Settings entry: **Settings → Style tweaks**.
@@ -104,6 +115,10 @@ Only client plugins (`client.js`) support hot reloading. Changes to the `apps/we
104
115
  - **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) inside each project directory header row (`role="treeitem"[aria-expanded]`, a stable hand-written attribute).
105
116
  - **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.
106
117
  - **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`.
118
+ - **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.
119
+ - **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.
120
+ - **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.
121
+ - **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.
107
122
 
108
123
  ## Acknowledgements
109
124
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # dsh-style-tweaks
2
2
 
3
- > 依赖版本:deepseek-harness v0.1.2-rc.1
3
+ > 依赖版本:deepseek-harness v0.1.5-rc.1
4
4
 
5
5
  [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)Web UI 插件:为 DSH 界面提供一套可选的样式调整——精确的对话列宽控制,以及侧边栏与设置面板的一系列小幅度修复。
6
6
 
@@ -8,9 +8,10 @@
8
8
 
9
9
  ### 布局
10
10
 
11
- - **插件宽度控制**(默认开启):开启时插件自带的列宽输入 / 预设接管列宽,并隐藏 DSH 原生的拖拽手柄;关闭时原生手柄接管列宽,插件同步显示当前值。
12
- - **对话框宽度**:600–1600 px 之间任意值;含 748(默认)/ 880(稍宽)/ 1024(更宽)三个预设按钮。
13
- - **两侧边距**:对话区域两侧保留的空白(px),列宽被钳制为对话框宽度,侧边栏打开或窗口缩小时内容会收窄,不会贴住边缘。最低 32 px。
11
+ - **插件宽度控制**(默认关闭):开启时插件自带的列宽输入 / 预设接管列宽,并隐藏 DSH 原生的拖拽手柄;关闭(默认)时原生手柄接管列宽,"对话框宽度"设置随之隐藏,已设置的宽度值保留,重新开启即恢复。
12
+ - **对话框宽度**:600–1600 px 之间任意值;含 748(默认)/ 880(稍宽)/ 1024(更宽)三个预设按钮。仅在"插件宽度控制"开启时显示。
13
+ - **两侧边距**:插件宽度控制开启时,对话区域两侧保留的空白(px),列宽被钳制为对话框宽度,侧边栏打开或窗口缩小时内容会收窄,不会贴住边缘。最低 32 px。仅在"插件宽度控制"开启时显示;关闭后边距不生效,保持 DSH 原生行为。
14
+ - **思考内容固定高度**(默认关闭):展开"深度思考"正文时限制为固定高度(默认 300 px,120–1200 可调),超出部分在窗口内滚动,很长的思考不再把后面的回复顶出视野;思考仍在生成时窗口显示开头,可滚动查看后续。滚轮自然穿透:块内滚到边界或内容不满一块时,滚轮直接滚动会话正文。收起后照旧恢复为一行摘要。右侧配 4px 常显细滚动条(与设置菜单一致),正文与滚动条间留有间距。
14
15
 
15
16
  ### 调整项
16
17
 
@@ -20,13 +21,19 @@
20
21
  - **项目目录运行指示(默认开启)**:在侧边栏项目目录右侧显示与对话标题一致的运行动画圆点,目录收起时也能一眼看出里面有对话正在进行。
21
22
  - **定位当前会话(默认开启)**:在侧边栏"工作区"段头部的搜索按钮左边新增一个定位按钮;点击后自动展开当前会话所属的工作区目录(包括"展开其余 x 个会话"的折叠层),将该会话滚动到侧边栏视口中央。无当前会话时按钮禁用并提示"请先打开一个会话"。
22
23
  - **设置菜单可滚动(默认开启)**:设置项较多时,让设置面板左侧菜单可以上下滚动,而不是把放不下的项直接裁掉(面板高度固定且 `overflow: hidden`,原生样式只给右侧内容列加了滚动)。滚动条为悬浮式细条:停靠在菜单右侧的留白里、不挤压菜单宽度,只在列表实际滚动时出现,停止滚动约 0.8 秒后淡出。
24
+ - **中键关闭侧边栏标签页(默认开启)**:0.1.5 新增的右侧边栏是标签页面板。开启后,鼠标中键点击任一标签页即关闭它(浮动面板的标签页同样适用),与浏览器标签页的习惯一致。关闭走宿主公开的 `ctx.sidebarRight` 关闭接口,原生规则照常生效:唯一驻留的引导页不可关、关闭最后一个停靠标签时侧边栏一并收起,与 ✕ 按钮的行为完全一致;条上的中键自动滚动被抑制,中键按下也不再启动标签拖拽,按住中键移动不会误把标签拖走或浮出。0.1.5 之前的宿主上本项保持惰性。
25
+ - **经典统计行(默认关闭)**:DSH 0.1.5 起,输入框下方的统计信息从一行居中文本(轮数/步数 · 耗时 · 速度 · 缓存命中 · Token 用量)改成了两个图标胶囊(点开弹窗查看)。开启本项可恢复 0.1.2 的文本行样式:数据仍读同一批持久化投影(`sessionStats` / `tokenUsage`),数值与胶囊一致,超宽时省略号截断、悬停显示完整内容;关闭后立即恢复新版胶囊。
26
+ - **缓存命中两位小数(默认关闭)**:统计信息的缓存命中率按两位小数显示(如 87.35%),不再取整;点开的 Token 用量弹窗同步生效。无论统计信息以新版图标胶囊还是经典文本行展示,均适用;两个开关同时为开时由经典统计行生效。
27
+ - **轮次速度与首 token 用时(默认关闭)**:0.1.5 起冷会话不再重建逐 token 时序,每轮"本轮用时和速度"弹窗只剩总用时。开启本项后点击每轮的用时胶囊,弹窗会回填输出速度与首 token 用时两行(由会话日志内嵌的模型流重建,历史会话同样生效)。
23
28
 
24
29
  ```yaml
25
30
  style-tweaks:
26
31
  # 布局
27
- usePluginWidth: true # 默认 true;false 时由原生拖拽手柄接管
28
- dialogWidth: 748 # 600–1600 px
29
- sideMargin: 50 # ≥ 32 px
32
+ usePluginWidth: false # 默认 false;true 时插件输入/预设接管列宽
33
+ dialogWidth: 748 # 600–1600 px;仅 usePluginWidth 开启时显示/生效
34
+ sideMargin: 50 # ≥ 32 px;仅 usePluginWidth 开启时显示/生效
35
+ thinkFixedHeight: false # 默认 false;true 则"深度思考"正文限高滚动
36
+ thinkHeight: 300 # 120–1200 px;仅 thinkFixedHeight 开启时显示/生效
30
37
  # 调整项
31
38
  stableTable: true # 默认 true;false 则关闭
32
39
  stableTurnRail: true # 默认 true;false 则关闭
@@ -34,6 +41,10 @@ style-tweaks:
34
41
  projectRunningIndicator: true # 默认 true;false 则关闭
35
42
  locateCurrentSession: true # 默认 true;false 则隐藏侧边栏定位按钮
36
43
  settingsNavScroll: true # 默认 true;false 则关闭设置左侧菜单滚动
44
+ sidebarMiddleClickClose: true # 默认 true;false 则关闭侧边栏中键关闭标签页
45
+ legacyStatsLine: false # 默认 false;true 则用 0.1.2 的文本统计行替换新版胶囊
46
+ pillsCacheHitDecimals: false # 默认 false;true 则缓存命中率显示两位小数(胶囊与经典行均适用)
47
+ turnSpeedMetrics: false # 默认 false;true 则用时弹窗回填输出速度与首 token 用时
37
48
  ```
38
49
 
39
50
  设置入口:**设置 → 样式调整**。
@@ -103,6 +114,10 @@ npx -y @deepseek-ai/dsh plugin --profile web add . # 从本目录作为 b
103
114
  - **项目目录运行指示**(`src/client/tweaks/project-running-indicator.ts`):从 `ctx.get('sessions')` / `ctx.get('workspaces')` 读取会话运行状态与目录归属,用 MutationObserver 在项目目录头行(`role="treeitem"[aria-expanded]`,稳定手写属性)内挂载应用自身的 `StateDot`(复用 `@deepseek-ai/dsh-client-ui-primitives` 的同一份模块,动画 keyframes 与样式 token 与对话标题处完全一致)。
104
115
  - **定位当前会话**(`src/client/tweaks/locate-current-session.ts`):在 DSH 侧边栏"工作区"段头部的搜索按钮左边注入一个新按钮。锚点全部走 i18n 与语义片段:通过 `ctx.locale.bind()` 解析当前语言的搜索按钮 aria-label(`workspace` 命名空间的 `search.sessions.aria` 键)与顶部面包屑的 aria-label(`conversation` 命名空间的 `session.hierarchy` 键),locale 服务不可用时退回 `[class*="searchButton"]` / `[class*="crumbs"]` 语义片段兜底。点击后从面包屑(`button[disabled]` 的当前项)读出会话标题,用 `ctx.get('sessions')` 与 `ctx.get('workspaces')` 两个应用级 store 反查所在工作区标题(即使其工作区目录收起也能定位),再按标题匹配工作区行;依次穿透两级折叠——工作区收起时 click 展开、"展开其余 x 个会话"溢出按钮(`[class*="sessionOverflowButton"][aria-expanded="false"]`)挡住目标行时自动点开——然后 `scrollIntoView({ block: 'center' })` 滚动居中。悬停提示复刻了 DSH 原生 `<Tooltip>` 的自研气泡(fixed 定位 + 主题 token + 500ms 延迟 + 视口翻转),而非浏览器原生 `title`。MutationObserver 监听 `aria-selected` / `aria-expanded` 属性变化同步按钮可用性与挂载状态。
105
116
  - **设置菜单可滚动**(`src/client/tweaks/settings-nav-scroll.ts`):DSH 设置面板(`SettingsRoot`)高度固定且 `overflow: hidden`,原生样式只让右侧内容列(`.options`)滚动;左侧菜单列表 `.navList` 没有 `min-height: 0` 与 overflow 处理,条目多时被面板直接裁掉。本调整项用"两条 CSS 规则 + 一个小型 JS 驱动"实现:基础规则 `flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding-bottom: 12px; margin-inline-end: -12px` 让列表成为滚动容器,并把滚动条"停靠"进导航栏右侧的 12px 留白里(8px 间距 + 4px 条位,恰好占满留白),滑块悬浮在留白上、与菜单项保持 8px 间距;菜单项上限 `max-width: 164px`(DSH 原生 188 导航 − 2×12 padding)钉住原生宽度,无论滚动条出现与否、悬停还是滚动,菜单项都与原生完全同宽;`::-webkit-scrollbar { width: 4px }` 比全局皮肤细两档。显示时机由 JS 驱动:第一版用 `:not(:hover)` 隐藏滑块,但 Chromium 在宿主 `:hover` 变化时不会可靠重绘自定义滚动条伪元素(实测表现为"点击菜单才出现"),因此改为监听列表 `scroll` 事件——滚动时给列表加 `cst-nav-scroll-show` 类显示滑块(颜色沿用面板继承的 l2 主题 token),停止滚动约 0.8 秒后移除类淡出;MutationObserver 在设置弹窗卸载/重开时保持监听器挂在当前列表上。选择器用"弹窗结构 + CSS Modules 语义片段"双保险(DSH 类名按 `[hash]_[local]` 哈希,`navList` 局部名全 DSH 唯一),无需 `!important`。
117
+ - **中键关闭侧边栏标签页**(`src/client/tweaks/sidebar-middle-click-close.ts`):右侧边栏的标签芯片是 `div[role="tab"][data-dockkit-tab="<tabId>"]`,由 `ui-dockkit` 的 `TabPanel` 渲染,而 dockkit 只有右侧边栏(停靠面板与浮动层)采用,因此"芯片"即"侧边栏标签页"。本项在 `document` 捕获阶段挂三个监听(React 17+ 把监听挂在根容器上,document 捕获先于整棵 React 树,拦截因此彻底):`pointerdown`(中键)拦截传播,芯片自带的按压→拖拽手势无从启动,按住中键移动 ≥4px 也不会再把标签拖走、分屏或浮出;`mousedown`(中键)`preventDefault`,抑制浏览器在标签条上的自动滚动(不影响其后仍会派发的 `auxclick`);`auxclick`(中键,浏览器对非主键点击只派发 `auxclick`、不派发 `click`)读出芯片上的 `TabId` 交给 `ctx.sidebarRight.close()`——关闭完全走宿主 store 自己的规划规则(缺失标签无事发生、唯一驻留引导页不可关、关闭最后一个停靠标签时侧边栏收起),插件不复制任何一条。`ctx.sidebarRight` 是 0.1.5+ 宿主才提供的服务,且由 `ui-sidebar-right` 在**它自己的插件 fiber** 里 provide——cordis 的属性访问只沿祖先 fiber 链解析,从兄弟插件读会直接抛错("cannot get property … without inject"),而把它加进本插件的 `inject` 会让服务变成必需、在 0.1.2 宿主上卡死启动;因此该 face 在每次中键关闭时经 `ctx.reflect.get('sidebarRight')` 解析——reflect 实例与 store 为根共享、provide 的隔离键也登记在根,`get` 无需 inject 声明即可跨 fiber 读到兄弟提供的服务,未提供时返回 `undefined` 而非抛错(0.1.2 宿主 → `undefined`,本项保持惰性,仅保留副作用抑制);`close()` 在无挂载 seat 时抛错,统一吞掉。浮动面板的头部不渲染芯片,其中键关闭转发给头部自带的 ✕ 按钮(`canCloseTab` 不允许时按钮不存在,无事发生),头部的"停靠回面板"按钮被显式排除;按压与释放不在同一芯片上时,浏览器把 `auxclick` 派发到两者的公共祖先,命中不了任何芯片,同样不会关闭。
118
+ - **经典统计行**(`src/client/tweaks/legacy-stats-line.tsx`):0.1.5 起 DSH 把输入框下方的统计行(旧 `StatsLine`)换成了图标胶囊(新 `StatsPills`),两者挂载方式相同——`conversation.composer.dock` 列表槽位上 id 为 `stats` 的条目。列表槽位的规则是同一 id(同一"格")里 priority 最低的存活条目渲染,因此本调整项在开关打开时用 `id: 'stats'` + `priority: -2` 影子覆盖原生条目(走槽位系统自己的遮蔽机制,无需 CSS 隐藏或 DOM 修补;它比"缓存命中两位小数"的 `priority: -1` 更优先,见下条),销毁注册即把格子还给原生胶囊;若条目渲染崩溃会自动退位,胶囊原样回来。数据面与新胶囊一致:读 `sessionStats`(整段日志的轮数/步数/耗时)与 `tokenUsage`(计费桶)两个持久化投影,不再做旧版"可见窗口折叠"的兜底(无投影时整行不渲染,符合旧行"无数据不出行"的规则)。文案走插件自己的 `style-tweaks` 命名空间(`legacyStats.*` 键)——旧版 `stats.llm` 系列键在 0.1.5 的词典里已被删除,插件自带中英文案;行样式(居中、三级文字色、省略号截断)按 0.1.2 的 `StatsLine.module.css` 移植,token 全部沿用原值并带兜底。行根元素带 `data-composer-stats` 标记:宿主 InputBar 会围绕任何挂载的统计行把自己的 8px 底距收紧到 4px(`.root:has([data-composer-stats])`),行本身底部补 2px 使总高与胶囊行(26px 行 + 4px 宿主底距)一致——开关切换时对话内容不再上下位移。缓存命中率的小数位数跟随"缓存命中两位小数"开关:开启时按两位小数,关闭时按 0.1.2 原样取整;该开关在挂载时捕获,任何设置变化都会重挂载全部调整项,因此始终即时生效。
119
+ - **缓存命中两位小数**(`src/client/tweaks/pills-cache-hit-decimals.tsx`):新胶囊的命中率取整发生在 dsh-client-ui-chat 模块内部的格式化函数里,插件无法触及,因此本调整项在开关打开时对同一个 `stats` 槽位格做影子替换(`priority: -1`),由插件自绘整行——两个胶囊与点开弹窗按 0.1.5 的 `StatsPills` / `stat-dialog` 逐样式移植(定位与外点关闭直接复用宿主 primitives 的 `useAnchoredPosition` / `useDismissOnOutsidePointer`),缓存命中率改走插件共享的 `formatCacheHitPercent(…, 2)`(按两位小数取整,靠近 100% 时仍按"诚实尾数"多显示一位,如 `99.97`),弹窗各行与胶囊标签同步生效。数据面同样是 `sessionStats` / `tokenUsage` 两个投影(不做窗口折叠兜底);根元素同样带 `data-composer-stats`。图标方面,胶囊的 gauge 图标在 0.1.3+ 的 primitives 里才存在,故运行时从宿主 primitives 取 `IconGaugeOutline16`,老版本宿主取不到则回退为时钟图标。与"经典统计行"的层叠关系:两项同写一个槽位格,经典统计行注册在 `priority: -2` 优先渲染,本项在其下被遮蔽(不渲染、零开销);经典统计行关闭后本项若仍为开则自动接管格子,两者都关时恢复原生胶囊。该开关始终显示,不再随经典统计行隐藏:经典统计行同样读取它决定命中率的小数位数。
120
+ - **轮次速度与首 token 用时**(`src/client/tweaks/turn-speed-metrics.tsx` + `src/client/tweaks/assistant-stream-timing.ts`):会话格式 v2(0.1.5)把每次模型尝试的精确时序流嵌进持久化结算(`assistant/message` 的 `data.stream`),但 Chat 界面的冷呈现直接从组装后的消息构建、不回放嵌入流(v2 架构笔记原文:"Cold settled presentation therefore does not reconstruct per-token timing")——轮次尾部的折叠读节点内存 timing,其 `firstTokenTime` 只在 live-chunk 线性折叠中存活,重载后每轮的输出速度(TPS)与首 token 用时(TTFT)全部消失,"本轮用时和速度"弹窗只剩墙钟总用时。本调整项就是该笔记预期的"需要精确证据的消费者":从会话绑定的公开事件窗口(`ctx.sessions.binding(sessionId).eventSource`,Conversation 组装消费的同一份 feed)读出结算,把 dsh-llm 的紧凑流读取器(`assistantStreamFirstTokenTime`:按 `time0` + `dt` 间隔重建打包 run 内首个 token 的时间)与 0.1.5 `deriveTurnMetrics` 的折叠一并移植——TTFT 取最低 step 的"派发→首 token"延迟,TPS = 携带时序与用量的 step 的 Σ输出 token ÷ Σ解码墙钟。数据全部来自持久化日志,历史会话与刚完成的轮次拿到相同数值。展示选择:弹窗属于 `TurnTimePanel`,无插槽可用,因此本项在 `conversation.chat.assistant-actions` 列表槽(每轮操作行内;list 槽可加性渲染,与其它插件条目无选举冲突)挂一个不可见的控制器——页脚捕获监听只把被点击的页脚转交给一个共享的 body 级 `MutationObserver`(弹窗 portal 到 `<body>`,面板节点无法经 `closest` 找回页脚);观察器回调是微任务,必然先于下一帧渲染,按 `data-turn-time-details` 稳定标记认领刚提交的弹窗,在“本轮总用时”行后追加 `<dt>/<dd>` 两行:标签直接用宿主词典里仍存活的 `message.turnTime.speed` / `message.turnTime.ttft` / `message.tokensPerSecond` / `duration.seconds` 键,数值行自动继承弹窗自己的 `.details dt/.details dd` 网格样式,与 0.1.2 弹窗逐字一致。追加行会让面板变高,而 `useAnchoredPosition` 的重定位经 `ResizeObserver`→`setState` 延迟到下一帧之后,首帧会先画在偏低 48 px 的旧 `top` 再跳上去(正是“闪一下”)——回填在同一个微任务里按新增高度把固定定位卡片同步上移:弹窗是 `side: 'top'` 挂在胶囊上沿,保持卡片底边不动正是宿主 `place()` 对更高面板算出的同一个值,因此首个绘制帧即最终位置,宿主随后的写入变成同值空操作;只有位移会越过视口顶边(被钳制的极端摆放)才跳过。曾试过“先隐藏卡片、等宿主重定位写入再显示”,它把原本正确的即时打开变成可见的延迟弹出(原本不闪的会话反而开始闪),已弃用;也不要按页脚几何反推位移方向,因为 `data-turn-tail` 是整个轮次的大容器而非操作行;若该轮的线性时序本就存活(原生已渲染速度/TTFT 行),按标签去重跳过。行是注入 React 弹窗内的普通 DOM,随弹窗开合生灭,重开即重注;无数据(流内无 token 或结算不在窗口内)不注入任何行,开关关闭即恢复原生行为。
106
121
 
107
122
  ## 致谢
108
123