dsh-style-tweaks 0.1.5 → 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 +13 -5
- package/README.md +13 -5
- package/lib/client.js +549 -2
- package/lib/client.js.map +1 -1
- package/lib/config.js +56 -0
- package/lib/config.js.map +1 -1
- package/lib/types/client/i18n.d.ts +7 -0
- package/lib/types/client/i18n.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/panel.d.ts.map +1 -1
- package/lib/types/client/tweak-config.d.ts +8 -0
- package/lib/types/client/tweak-config.d.ts.map +1 -1
- package/lib/types/client/tweak-types.d.ts +3 -0
- package/lib/types/client/tweak-types.d.ts.map +1 -1
- 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/config.d.ts +65 -0
- package/lib/types/config.d.ts.map +1 -1
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -38,7 +38,9 @@ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) web
|
|
|
38
38
|
|
|
39
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
40
|
|
|
41
|
-
|
|
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.
|
|
42
44
|
|
|
43
45
|
```yaml
|
|
44
46
|
style-tweaks:
|
|
@@ -53,6 +55,10 @@ style-tweaks:
|
|
|
53
55
|
historyPageSizeEnabled: false # default false; true sends the size below on every history request (the panel's master switch)
|
|
54
56
|
historyPageSize: 500 # 50–1000; shown/effective only while historyPageSizeEnabled is on
|
|
55
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)
|
|
56
62
|
# Tweaks
|
|
57
63
|
stableSessionTitle: false # default false; true keeps session titles put with their ellipsis on hover
|
|
58
64
|
hideSessionHoverActions: false # default false; true keeps the Archive / Pin hover buttons off session rows (the "..." menu keeps both)
|
|
@@ -90,8 +96,8 @@ The package spec after `add` is forwarded to pnpm verbatim, so versions can be
|
|
|
90
96
|
pinned — `@version` for the npm package, `#tag` for the GitHub source:
|
|
91
97
|
|
|
92
98
|
```bash
|
|
93
|
-
dsh plugin --profile web add dsh-style-tweaks@0.1.
|
|
94
|
-
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
|
|
95
101
|
```
|
|
96
102
|
|
|
97
103
|
Restart DSH web once after installing (bundle plugins are scanned at process start).
|
|
@@ -249,7 +255,7 @@ source change has to be copied over — exactly the step the `link:` setup spare
|
|
|
249
255
|
```bash
|
|
250
256
|
pnpm build
|
|
251
257
|
pnpm pack # produces dsh-style-tweaks-<version>.tgz (gitignored; do not commit)
|
|
252
|
-
dsh plugin --profile web add ./dsh-style-tweaks-0.1.
|
|
258
|
+
dsh plugin --profile web add ./dsh-style-tweaks-0.1.6.tgz
|
|
253
259
|
```
|
|
254
260
|
|
|
255
261
|
**Restart `dsh web` once** after installing (bundle plugins are scanned at process start).
|
|
@@ -294,7 +300,7 @@ path dependency:
|
|
|
294
300
|
|
|
295
301
|
```bash
|
|
296
302
|
dsh plugin --profile web remove dsh-style-tweaks
|
|
297
|
-
dsh plugin --profile web add ./dsh-style-tweaks-0.1.
|
|
303
|
+
dsh plugin --profile web add ./dsh-style-tweaks-0.1.6.tgz
|
|
298
304
|
```
|
|
299
305
|
|
|
300
306
|
The only difference between the two setups is whether the artifact lands in place automatically: both
|
|
@@ -313,6 +319,8 @@ to verify the published package itself.
|
|
|
313
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`.
|
|
314
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.
|
|
315
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.
|
|
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
|
+
|
|
316
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.
|
|
317
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.
|
|
318
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.
|
package/README.md
CHANGED
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
|
|
39
39
|
- **自定义历史分页大小(默认关闭)**:DSH 原生会自行决定历史请求的条数(当前 0.1.7 的冷打开与「加载更早」为 500,轮次跳转另有至少 200 条的窗口下限)。开启后,每次适用请求都**精确使用**设定条数(50–1000,默认 500),无论比原生值大还是小;改写发生在浏览器发出的请求上——「加载更早」与轮次跳转共用的 unary `session/page`,以及可选的打开会话首屏 `session/follow` 开帧。插件还会把宿主的轮次窗口最小值同步为同一个条数,避免大页数仍因原生 50 条下限只加载两轮。三条边界:**条数精确生效**,不再只是「只升不降」的下限;**首屏由「打开会话时同样生效」(默认开启)单独控制**,关掉它则首屏保持 DSH 原生行为、条数只影响后续分页;**关闭主开关即完全回到 DSH 原生行为**,已保存的条数保留,重新开启时沿用。
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
- **自定义侧边栏会话条数(默认关闭)**:DSH 原生把每个工作区的会话列表折叠成 5 条,点一次「展开其余」再增加 5 条;这两个值都是宿主工作区浏览器组件内部的 `useState`(`COLLAPSED_SESSION_LIMIT`),持久化的视图 store 里没有对应的键,外部设置、插槽与 store 动作都够不到。开启后由插件接管:工作区静止时显示「初始显示条数」条,每次展开增加「每次展开增加条数」条(两者都以宿主的 5 为下限、不设上限,加减号每次点击变化 1)。实现不是替换宿主控件,而是驱动它已经渲染的那个按钮:插件按宿主的溢出按钮把列表撑到需要的条数,把多出来的行用自己的标记属性隐藏,再用自己的按钮顶替宿主按钮的位置——文案沿用宿主自己的 `workspace` 命名空间措辞(`sessions.expand` / `sessions.collapse`),数字是诚实的剩余条数,全部展开后按钮变为宿主的「收起」,点它裁剪回初始条数(宿主的折叠只能表达 5 条,所以收起由插件自己完成)。三条边界:**不隐藏当前选中的会话行**(打开会话——包括通过宿主搜索打开——会把该行标成 `aria-selected="true"`,裁剪一律让位);**展开过程从不隐藏列表**(宿主自己的行滑入动画把 5→10→15 的几步呈现为一次生长,插件不再叠加 `visibility` 之类的隐藏,短到一帧的隐藏同样会被读成闪烁);**静止时零开销**(每组按指纹比对,状态未动就跳过该组的整轮扫描,侧栏因其它原因频繁更新时也不会反复重算)。关闭(默认)即完全回到 DSH 原生的折叠条数与原生按钮;宿主没有这些结构锚点时本项保持惰性。
|
|
42
|
+
|
|
43
|
+
> 字段与默认值参考(设置面板自动维护)。**DSH 0.1.7+ 的实际存储是 `<profile>/.dsh-style-tweaks/store.json` 的 `value` 对象**——字段名与下表一致(JSON),可直接手改、**下次读取(刷新/重开设置面板)即生效**;`cordis.patch.yml` 里本插件那行 `config` 已不再被读取,仅作 store 文件缺失时的播种源(删掉 store 文件即可让它重新播种;本机 profile 里那一行通常已经没有 `config` 块,属正常——store 存在时它不参与任何读写)。更早的宿主存于 settings namespace。对于 partial store,未写入的字段按当前默认值解析;因此旧的 Desktop/Web profile 若没有 `turnProcessCounts` 也会默认显示计数,若要关闭请显式写入 `false`。
|
|
42
44
|
|
|
43
45
|
```yaml
|
|
44
46
|
style-tweaks:
|
|
@@ -53,6 +55,10 @@ style-tweaks:
|
|
|
53
55
|
historyPageSizeEnabled: false # 默认 false;true 则每次历史请求按下方条数携带(面板中的主开关)
|
|
54
56
|
historyPageSize: 500 # 50–1000;仅 historyPageSizeEnabled 开启时显示/生效
|
|
55
57
|
historyPageSizeColdStart: true # 默认 true;打开会话首屏也按该条数
|
|
58
|
+
# 侧边栏会话列表
|
|
59
|
+
sidebarSessionCountEnabled: false # 默认 false;true 则由下方条数接管每个工作区的会话列表折叠
|
|
60
|
+
sidebarSessionInitialCount: 5 # ≥ 5;工作区静止时显示的会话条数(仅 sidebarSessionCountEnabled 开启时显示/生效)
|
|
61
|
+
sidebarSessionExpandStep: 5 # ≥ 5;点一次「展开其余」增加的条数(同上)
|
|
56
62
|
# 调整项
|
|
57
63
|
stableSessionTitle: false # 默认 false;true 则会话标题悬停时不滑动、保留省略号
|
|
58
64
|
hideSessionHoverActions: false # 默认 false;true 则悬停会话行时不浮出「归档」「置顶」按钮(只留「…」菜单)
|
|
@@ -89,8 +95,8 @@ dsh plugin --profile web add github:zhj9709/dsh-style-tweaks
|
|
|
89
95
|
`add` 后面的包说明会**原样转发给 pnpm**,因此可以指定版本——npm 包用 `@版本号`,GitHub 源码用 `#tag`:
|
|
90
96
|
|
|
91
97
|
```bash
|
|
92
|
-
dsh plugin --profile web add dsh-style-tweaks@0.1.
|
|
93
|
-
dsh plugin --profile web add github:zhj9709/dsh-style-tweaks#v0.1.
|
|
98
|
+
dsh plugin --profile web add dsh-style-tweaks@0.1.6 # 锁定 npm 版本
|
|
99
|
+
dsh plugin --profile web add github:zhj9709/dsh-style-tweaks#v0.1.6 # 锁定 git tag
|
|
94
100
|
```
|
|
95
101
|
|
|
96
102
|
安装完成后**重启一次 `dsh web`**(bundle 插件在进程启动时扫描)。
|
|
@@ -234,7 +240,7 @@ dsh plugin --profile web add github:zhj9709/dsh-style-tweaks
|
|
|
234
240
|
```bash
|
|
235
241
|
pnpm build
|
|
236
242
|
pnpm pack # 产出 dsh-style-tweaks-<版本>.tgz(在 .gitignore 里,不要提交)
|
|
237
|
-
dsh plugin --profile web add ./dsh-style-tweaks-0.1.
|
|
243
|
+
dsh plugin --profile web add ./dsh-style-tweaks-0.1.6.tgz
|
|
238
244
|
```
|
|
239
245
|
|
|
240
246
|
装完**重启一次 `dsh web`**(bundle 插件在进程启动时扫描)。
|
|
@@ -275,7 +281,7 @@ dsh plugin --profile web add ./dsh-style-tweaks-0.1.5.tgz
|
|
|
275
281
|
|
|
276
282
|
```bash
|
|
277
283
|
dsh plugin --profile web remove dsh-style-tweaks
|
|
278
|
-
dsh plugin --profile web add ./dsh-style-tweaks-0.1.
|
|
284
|
+
dsh plugin --profile web add ./dsh-style-tweaks-0.1.6.tgz
|
|
279
285
|
```
|
|
280
286
|
|
|
281
287
|
两个方案的差别只有「产物是否自动落到位」这一条:都要 `pnpm build`,都要刷新页面。
|
|
@@ -293,6 +299,8 @@ dsh plugin --profile web add ./dsh-style-tweaks-0.1.5.tgz
|
|
|
293
299
|
- **设置菜单可滚动**(`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`。
|
|
294
300
|
- **中键关闭侧边栏标签页**(`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` 派发到两者的公共祖先,命中不了任何芯片,同样不会关闭。
|
|
295
301
|
- **右侧边栏初始宽度**(`src/client/tweaks/rightbar-initial-width.ts`):右侧边栏的宽度由宿主 `ui-layout` 决定——其 `openRightbar` action 在首次打开时用 `rightbar ??= max(300, round(viewportWidth × 0.45))` 填入 45% 的 px 值(常量 `RIGHTBAR_DEFAULT_RATIO`),拖拽经 `setRightbar` 覆盖(钳制到 `[300, 窗口 × 70%]`),其后关闭 / 再打开都保留该 px;宽度偏好只存在内存 store 里,刷新即回到 `null`。`ctx.layout` 只暴露 `openRightbar(track, fullscreen)` / `closeRightbar()` / `toggleSidebar()` / `selectPanel()`,改宽度的 `setRightbar` 挂在控制器自己的 `panels` 动作集上(TS 层是 `private`,运行时是普通字段)。本项因此包装控制器**实例**的 `openRightbar`:本次页面加载的第一次打开时,先按 `[data-rightbar-col]` 的父元素(宿主测量 `viewportWidth` 的同一个 frame 元素)实测宽度算出 `round(宽 × 百分比)`,经宿主自己的 `panels.setRightbar` 写入,再转发原调用——写入与打开落在同一次 React 提交里,不会先闪一帧 45%;全程不碰 grid 模板与面板 inline 宽度,因此列解算、拖拽手柄位置与面板宽度天然一致。两个页面级标记守住语义:`__cst_rightbar_initial_width_seeded__` 保证每次加载只写一次;`__cst_rightbar_initial_width_dragged__` 由挂在 `[data-side="rightbar"]`(宿主右列拖拽手柄)上的**一次性**探针在首次 `pointerdown` 时置位——用户一旦亲手拖过,本次加载内本项彻底不再写入,否则每次设置变更触发的整体重挂载都会覆盖刚拖出来的宽度。右栏在本项挂载时已打开(用户在设置面板改百分比)则立即按新比例写入,即所见即得的实时预览。宿主未提供 `layout` 服务、或控制器不再携带可调用的 `setRightbar` 时保持惰性;卸载只在当前值仍是自己的包装时才还原宿主方法,不踩其它插件的包装。
|
|
302
|
+
- **自定义侧边栏会话条数**(`src/client/tweaks/sidebar-session-count.ts`):宿主把每个工作区的会话列表折成 5 条(`COLLAPSED_SESSION_LIMIT`),该上限是浏览器组件里的一个 `useState`,持久化视图 store 只存 `groupBy / orderBy / groupExpansion / sessionOrderByAccount / archivedFilter`——没有任何设置、插槽或 store 动作能从外部够到它,因此本项**驱动宿主已经渲染的那个控件**,而不是替换它。宿主的溢出按钮带 `data-row-key="overflow:<groupKey>"` 与 `aria-expanded`(全部显示后为 `true`),按它是让宿主多渲染行的唯一途径,本项于是代用户按它,再决定屏幕上留什么:**撑开**(连按到渲染行数不少于配置条数;宿主每次 +5、剩余 ≤5 时直接跳到全部 `Infinity`,所以循环要么在行数达标、要么在按钮消失时终止)、**裁剪**(隐藏超出配置条数的行——行自身没有内联样式(0.1.7-rc.2 实测),一个标记属性 `data-cst-sid-hidden` 加一条 `display: none !important` 规则即可,宿主重渲染只要元素不变、标记就不丢)、**改标注**(整体隐藏宿主按钮(样式层,不是脚本层——宿主与行在同一次提交里插入按钮,观察器两个 microtask 后才够到它,脚本隐藏会漏出一段时间的错误计数),再用插件自己的 `data-cst-sid-button` 顶替:文案取自宿主 `workspace` 命名空间的 `sessions.expand` / `sessions.collapse`,数字是诚实的剩余条数——宿主标签里的「还有 N 条」加上本次裁剪扣住的行数,即 `总数 − 显示数`)。点插件按钮只改裁剪计数,撑开永远交回宿主,所以长列表背后仍是宿主自己的「全部显示」状态;收起则裁回初始条数,而不是请宿主折回 5 条(宿主的折叠表达不了自定义条数)。**展开过程一概可见**:早期版本在撑开期间给列表加 `visibility: hidden` 想遮住 5→10→15 的阶梯,但那个窗口只有一帧(一次按压就是一次渲染,远小于 100ms),短到仍会被读成闪烁——列表消失又回来,而宿主自己的行滑入动画本就把几步呈现为一次生长,所以现在始终展示生长。**按钮先于行发布**:数字不需要等,总数 = 宿主「N more」标签 + 已渲染行数、显示数 = 本次裁剪数,两者在组展开那一刻就已知,因此按钮在撑开开始前就带着最终数字发布(实测原先要到 t=199ms 才出现,而 11 行早在 ~50ms 就已全部上屏);唯一无法提前发布的是宿主标签里没有整数(已全部显示,或尚未绘制),那一轮按落定后的数字发布。**等待机制**:行滑入约 200ms(`ROW_GLIDE_MS`)且离场的行仍留在 DOM 里,窗口内计数会把正在离开的行算进来,因此另有一次 `GLIDE_SETTLE_MS`(300ms)的纠正 pass;等待以 rAF 为主并回退到定时器(被遮挡的窗口会挂起 rAF),每次落定还带墙钟上限,不会把某个组永久锁在「忙」状态;落在某一轮 pass 期间的按压被排队、由该轮的释放阶段接走,而不是被丢掉。**选中行永不隐藏**:打开会话(包括经宿主搜索打开并滚动到该行)会把它标成 `aria-selected="true"`,裁剪一律让位;宿主自己为"揭示某行"把组的限值抬到全部时,行会先渲染出来,裁剪在下一个 tick 再重新施加。**静止时零开销**:每组对一轮 pass 读写的全部内容做指纹,状态未动就跳过该组,侧栏因其它原因频繁更新时也不会为每个打开的组各买一轮 pass。锚点全部按稳定的语义片段匹配(`groupSection` / `sessionOverflowButton` / `list` 等 CSS Modules 片段会随构建旋转),`data-row-key` 是宿主自己的契约并带着组 key——也就是每组状态的主键。
|
|
303
|
+
|
|
296
304
|
- **经典统计行**(`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 全部沿用原值并带兜底。行皮肤按宿主的 dock 代际自适应:0.1.6-alpha.2 把 dock 槽位的输出移进了 InputBar 内部一个居中 flex 行(与新的上下文占用按钮 ContextMeter 并排,槽位输出不再是输入卡片下的独立一行),行根元素挂载时探测其布局父级(跳过槽位渲染插入的 `display: contents` 包装层——包装层不生成盒子,其 `flexDirection` 恒为初始值 `row`,直接读会把两代 dock 都判成横向)——纵向 flex 容器(alpha.1 及更早宿主,行直接挂在卡片下)走旧皮肤:行根元素带 `data-composer-stats` 标记,宿主 InputBar 会围绕任何挂载的统计行把自己的 8px 底距收紧到 4px(`.root:has([data-composer-stats])`),行自身补 4px 顶距 + 2px 底距使总高与胶囊行(26px 行 + 4px 宿主底距)一致;横向 flex 容器(alpha.2 的 `.dock` 行,自带 4px 顶距、根底距固定 4px,`data-composer-stats` 规则已删除、标记失效)走新皮肤:行按内容自适应大小、不带任何纵向 padding,与胶囊一起按宿主原生 12px 间距居中排布——两种代际下开关切换时对话内容与输入框都不再上下位移。缓存命中率的小数位数跟随"缓存命中两位小数"开关:开启时按两位小数,关闭时按 0.1.2 原样取整;该开关在挂载时捕获,任何设置变化都会重挂载全部调整项,因此始终即时生效。
|
|
297
305
|
- **缓存命中两位小数**(`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` 标记(0.1.6-alpha.2 已删除对应的宿主底距规则,该标记只在更早的宿主上生效),行皮肤同样按 dock 代际自适应(见 `composer-dock.ts` 与上一条的说明)。图标方面,胶囊的 gauge 图标在 0.1.3+ 的 primitives 里才存在,故运行时从宿主 primitives 取 `IconGaugeOutline16`,老版本宿主取不到则回退为时钟图标。与"经典统计行"的层叠关系:两项同写一个槽位格,经典统计行注册在 `priority: -2` 优先渲染,本项在其下被遮蔽(不渲染、零开销);经典统计行关闭后本项若仍为开则自动接管格子,两者都关时恢复原生胶囊。该开关始终显示,不再随经典统计行隐藏:经典统计行同样读取它决定命中率的小数位数。
|
|
298
306
|
- **本轮用时和速度**(`src/client/tweaks/turn-time-pill.tsx` + `src/client/tweaks/assistant-stream-timing.ts`):0.1.6 的 `TurnUsagePanel.tsx` 里并排放着两个组件——`TurnUsagePanel`(数据图标 + 用量)与 `TurnTimePanel`(时钟图标 + 用时,点开"本轮用时和速度"弹窗),`TurnTailNodeView` 把它们作为一个 fragment 传给动作行的 `usageAction`;0.1.7-alpha.1 的性能与用量偏好提交删掉了 `TurnTimePanel`、`TurnTailChatData` 里的 `ttftMs`/`tokensPerSecond`、`formatLatencySeconds` 以及 `message.ranFor` + 整组 `message.turnTime.*`。本项把用时那一半整体装回来:胶囊、弹窗、以及弹窗里 0.1.6 的三行。位置:走的是插件唯一能进到宿主 chrome 里的路——在 `conversation.chat.assistant-actions` 列表插槽(该列表渲染在复制与分叉按钮之间)挂一个不可见锚点,再从锚点把可见胶囊 portal 进行尾自己的尾随簇,即宿主包住 `usageAction` 与时钟的那个 span(按结构取"带 `data-clock="end"` 的行的最后一个元素子节点")。portal 在簇里落在最后,因此两条 CSS 规则把它放回 0.1.6 的座位:时钟(簇内 `_timeEnd` 后缀的那个 span,也就是 portal 在 DOM 里的前一个兄弟)被 `order:1` 排到胶囊之后,与用量胶囊之间 8px 的 flex 间距按 0.1.6 自己的 `.root + .root` 配对规则回退 6px,于是行读作「用量 → 用时 → 时间戳」,与 0.1.6 一致;用 `order` 而不是插队 DOM,位置就不受 React 插入宿主自己子节点的时机影响(簇里没有用量胶囊时不回退间距,没有时钟时胶囊收尾)。数值:0.1.6 读的是 Chat 快照 turn location 的墙钟与节点内存 timing,这两者在 0.1.7 的节点数据里都已不存在,因此三个数字都从会话事件窗口折算——用时走 `turn/start` → `turn/end`(`deriveTurnRunMs`),输出速度与 TTFT 走结算内嵌的模型流(`deriveTurnSpeedMetrics`)。后者正是会话格式 v2(0.1.5)架构笔记预期的"需要精确证据的消费者":v2 把每次模型尝试的精确时序流嵌进持久化结算(`assistant/message` 的 `data.stream`),但 Chat 界面的冷呈现直接从组装后的消息构建、不回放嵌入流(笔记原文:"Cold settled presentation therefore does not reconstruct per-token timing")——轮次尾部的折叠读节点内存 timing,其 `firstTokenTime` 只在 live-chunk 线性折叠中存活,重载后每轮的输出速度(TPS)与首 token 用时(TTFT)全部消失,"本轮用时和速度"弹窗只剩墙钟总用时。插件把 dsh-llm 的紧凑流读取器(`assistantStreamFirstTokenTime`:按 `time0` + `dt` 间隔重建打包 run 内首个 token 的时间)与 0.1.5 `deriveTurnMetrics` 的折叠一并移植——TTFT 取最低 step 的"派发→首 token"延迟,TPS = 携带时序与用量的 step 的 Σ输出 token ÷ Σ解码墙钟;数据全部来自持久化日志,历史会话与刚完成的轮次拿到相同数值。时序:一轮结束后尾节点才存在,数值已经定型;若 `turn/end` 尚未进入窗口(或被窗口上限淘汰)则订阅 `eventSource` 直到墙钟出现再停(结算事件总在 `turn/end` 之前,因此那一刻两个派生量都已就绪);窗口里没有该轮 `turn/start` 时不渲染胶囊,保持 0.1.7 原样。弹窗:宿主 stat-dialog 座位的移植——同样使用宿主自己也在用的公开原语(`useAnchoredPosition` 做触发点上方的视口钳制定位、`useDismissOnOutsidePointer` 做外部点击关闭)加一个 Escape 处理,皮肤照抄 `stat-dialog.module.css`,明细网格带宿主的 `data-turn-time-details` 标记,行序与"只在有值时出行"都沿用 0.1.6 的 `TurnTimePanel`(总用时 → 输出速度 → 首 token 用时);两行标签取自插件自己的 `style-tweaks` 词典(宿主仍存活的 `stats.dialog.speed` / `stats.dialog.ttft` 是给作曲器那份"会话统计"弹窗措辞的——`ttft` 写作"首 token 平均",对单轮并不成立),数值仍用宿主词典的 `message.tokensPerSecond` / `duration.seconds` 键。三行在挂载时一次渲染完,面板不再被追加内容,因此首帧就是最终高度(旧版"先渲染一行、开弹窗时再注入两行"需要把卡片同步上移以避免闪一下,已被整体渲染取代)。选择器安全性:所有钩子都是结构化的(`data-turn-tail` / `data-clock="end"` / 簇内 `_timeEnd` 后缀 / `data-turn-time-details`),胶囊自己的类名带 `cst-ttp-` 前缀;宿主若改名 `timeEnd`,退化为胶囊落在行尾。
|