dsh-chat-locator
Turn-rail settings for DSH Web: tick thickness, rail side, and a curved length gradient anchored on the hovered tick — plus a plain-text hover preview whose line count, font size, and width you control.
Language: English | 简体中文
📋 Table of Contents
DSH Web draws a turn rail down the edge of every conversation: one tick per turn, so you can see where you are in a long session and jump between turns. It is a good rail with three opinions baked into it. Ticks are always 2px. The rail always sits on the right. And pointing at a tick tells you nothing about what that turn contains, so finding a specific turn in a long conversation means hovering ticks one at a time.
None of that is configurable. The rail is rendered directly by ChatView inside @deepseek-ai/dsh-client-ui-chat; the Slot system gives it no seat, and no setting for thickness or side is exposed anywhere.
dsh-chat-locator is a persistent DSH plugin that turns those fixed opinions into settings. It does not replace the rail — it claims the rail's styles at runtime, so the built-in jump-to-turn, unloaded-turn paging, and active-turn following all keep working exactly as before.
✨ Features
- Tick thickness (横线粗细): 1–8px instead of a fixed 2px. Only the line width changes; the 10px tick spacing stays put.
- Rail side (轨道位置): Left or right. The hover preview automatically opens on the opposite side, so it never covers the text you are reading.
- Hover preview (悬停预览): A plain-text card showing that turn's prompt and response. Thinking content can never appear in it, whitespace is collapsed so it cannot contain a blank line, and long text is truncated with an ellipsis.
- Preview line count (预览正文行数): 1–6 lines. The card really grows — only the overflow is clipped.
- Preview font size and width (预览字号 / 预览框宽度): 10–18px and 200–420px. Font size and line height scale as a pair, so enlarging the text never crowds or clips it.
- A curved length gradient: With the preview on, the tick under the pointer grows to 32px and its neighbours taper back along a curve —
21 / 14 / 12 — so the rail reads as a hook pointing at where you are, not as a straight diagonal.
- Restore defaults (恢复默认): All seven settings back to factory values in one click, via per-field
unset rather than rewriting the defaults.
- A dedicated settings page: Settings → 对话定位条, with a live sample rail and sample preview card that redraw as you change each value.
- Zero dependencies, no build step. Both halves are plain ESM loaded directly by Node and the browser.
🚀 Quick Start
What you need: a working DSH installation (the dsh command) with a profile, plus Node.js 20 or newer. The plugin itself has no dependencies to install.
Step 1 — Open a terminal
- macOS / Linux: open Terminal.
- Windows: press
Win + R, type powershell, and press Enter.
Step 2 — Install the bundle into your profile
dsh plugin forwards its arguments to pnpm inside the profile directory, so this registers the bundle as a dependency and DSH picks up its patch automatically. Replace web with your own profile name if it differs.
dsh plugin --profile web add github:hawkongz/dsh-chat-locator
That is the whole install. It fetches the four files a DSH plugin bundle needs — package.json (which declares the bundle and the browser half), index.js (the host half), client.js (the browser half), and cordis.patch.yml (the patch that adds the plugin row) — and there is nothing to compile and no dependency to install.
Step 3 — Restart the host
dsh web
The host process caches imported modules, so a newly added bundle is not mounted until the host restarts once.
Step 4 — Done. With DSH Web open at http://127.0.0.1:3080, you are finished if:
- Settings → 对话定位条 appears in the settings sidebar, and
- in the browser console, this returns
railFound: true on a conversation with at least two turns:
__dshChatLocator.state()
Open a conversation and slide the pointer up and down the rail: the tick under the pointer grows and its neighbours taper.
To update: run dsh plugin --profile web update dsh-chat-locator, then restart the host. To uninstall: dsh plugin --profile web remove dsh-chat-locator, then restart the host.
Installing offline, or want a local copy you can edit? Download the four files instead and install the folder — see Install without git.
📦 Installation
Requirements
| Requirement | Version | Notes |
|---|
| DSH | 11.x | Provides the dsh command and the profile you install into. |
| Node.js | 20 or newer | Used by the host half and the test suite. |
A browser with :has() support | Chromium 105+ | Without it the length gradient does not apply; everything else still works. |
Install methods
| Method | Best for |
|---|
dsh plugin --profile web add github:hawkongz/dsh-chat-locator | Most users. One command, and it is what the Quick Start uses. |
| From a git clone | Pinning a revision, or working on the plugin itself. |
| From four downloaded files | No git available, or an offline machine. |
Install from a git clone
Use this if you would rather update with git pull than by re-downloading files.
git clone https://github.com/hawkongz/dsh-chat-locator.git
cd dsh-chat-locator
dsh plugin --profile web add "$(pwd)"
dsh web
Install without git
A bundle is four files. Download them into a folder, install the folder, and restart the host — nothing else is needed.
macOS / Linux:
mkdir -p ~/.dsh/plugin-src/dsh-chat-locator
cd ~/.dsh/plugin-src/dsh-chat-locator
curl -fsSLO https://raw.githubusercontent.com/hawkongz/dsh-chat-locator/main/package.json
curl -fsSLO https://raw.githubusercontent.com/hawkongz/dsh-chat-locator/main/index.js
curl -fsSLO https://raw.githubusercontent.com/hawkongz/dsh-chat-locator/main/client.js
curl -fsSLO https://raw.githubusercontent.com/hawkongz/dsh-chat-locator/main/cordis.patch.yml
dsh plugin --profile web add ~/.dsh/plugin-src/dsh-chat-locator
Windows (PowerShell):
$dir = "$env:USERPROFILE\.dsh\plugin-src\dsh-chat-locator"
$base = "https://raw.githubusercontent.com/hawkongz/dsh-chat-locator/main"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
foreach ($file in "package.json", "index.js", "client.js", "cordis.patch.yml") {
Invoke-WebRequest -Uri "$base/$file" -OutFile "$dir\$file"
}
dsh plugin --profile web add "$dir"
Then restart the host:
dsh web
Notes on installation
- DSH registers the bundle for you. A package that declares
dsh.bundle.patch is added to your profile's dsh.profile.bundles list automatically, so dsh plugin add is the only step.
- There is no build step. pnpm may print a note about blocked build scripts for git-hosted packages; this plugin has no
prepare script and no dependencies, so there is nothing to allow.
- Restart the host after installing or removing. See How It Works for why.
- Installation is per profile.
--profile web is the profile this plugin was developed against; substitute your own.
📖 Usage
Everything lives on one settings page: Settings → 对话定位条. The table below lists each control with its default and range.
| Setting | Default | Range | What it does |
|---|
Show the locator rail
显示对话定位条 | On | On / Off | Hides the whole rail. Turn jumping and unloaded-turn paging are unaffected. |
Tick thickness
横线粗细 | 2px | 1–8px | The line width of each turn's tick. Tick spacing stays at 10px, so 8px is the practical ceiling. |
Rail side
轨道位置 | Right | Left / Right | Which edge of the conversation area the rail hugs. The hover preview opens on the opposite side automatically. |
Hover preview
悬停预览 | On | On / Off | Shows the prompt and response for the hovered turn. Also gates the length gradient — with the preview off, no gradient is applied. |
Preview line count
预览正文行数 | 3 | 1–6 | How many lines of the response to show. The card grows with the content; only the overflow is clipped. |
Preview font size
预览字号 | 12px | 10–18px | The card's font size. Line height scales with it at 1.5x, and the card height is computed from the same line height, so larger text is never clipped. |
Preview card width
预览框宽度 | 300px | 200–420px | The card's width. It keeps the built-in container clamp, so it shrinks automatically in a narrow window. |
Restore defaults
恢复默认 | — | — | Resets all seven settings to their factory values. Disabled when everything is already at its default. |
The length gradient
With the preview enabled, the tick you point at is the longest, and the ticks around it taper away along a curve:
| Distance from the hovered tick | 0 | ±1 | ±2 | ≥3 |
|---|
| Width | 32px | 21px | 14px | 12px |
| Drop per step | — | 11px | 7px | 2px |
The adjacent tick gives up the most width, then progressively less, flattening out as it rejoins the rail. The width table is generated from 12 + 20 * (1 - d/3)^2, and docs/design-notes.md records how the exponent was tuned (2.5 was a cliff, 1.5 left the neighbours too long, 2.0 is the compromise in use). The gradient anchors on the hovered tick — the active turn's tick is never resized — and it disappears the moment the pointer leaves.
Troubleshooting
The plugin ships one debug hook. Run it in the browser console:
__dshChatLocator.state()
// {
// railPrefix: 'eGxaPq', railFound: true, rules: '…',
// config: { … },
// settingsStatus: { status: 'ready', writable: true, unsupported: [] }
// }
| What you see | What it means |
|---|
railPrefix: null | The rail is not rendered in this view — fewer than two turns, a container narrower than 900px, or a non-conversation view. Nothing is broken. |
settingsStatus.status !== 'ready' | The host settings namespace was not read. Check that the host half loaded. |
settingsStatus.unsupported is non-empty | The running host process has not registered those fields yet, so they apply to this session only. Restart dsh web; the plugin back-fills them automatically. |
🧠 How It Works
Three constraints shaped the implementation, and each one is documented in full — with the experiments behind it — in docs/design-notes.md.
- The rail is claimed, not redrawn. It sits in no Slot, so a replacement would have to re-implement turn jumping, unloaded-turn paging, and active-turn following. Instead the plugin discovers the CSS Module prefix at runtime (looking for a
<prefix>_frame that contains a <prefix>_mark) and injects one owned stylesheet of !important overrides. If upstream renames those classes, the plugin goes quiet rather than breaking the rail.
- Configuration needs a host half, and that half has a module cache. The host registers the
chat-locator settings namespace; the browser half binds to it. Changes to index.js take effect only after restarting dsh web, so the plugin keeps unknown-to-the-host settings in the session, says so on the settings page, and back-fills them once the host catches up.
- The preview stays plain text. It is built from the built-in turn outline and only from text blocks, so thinking content cannot reach it, and whitespace runs are collapsed so it cannot contain a blank line.
Known boundaries — the upstream class-name contract, the 900px container cutoff, the 8px interaction strip the widened clip box adds — are listed in the same document.
📌 Topics
dsh dsh-plugin deepseek-harness cordis cordis-plugin web-ui conversation-navigation css-injection
🤝 Contributing
See CONTRIBUTING.md. There is nothing to install: clone the repository and run node test/verify-client.mjs to get all 121 assertions.
Bug reports and feature requests are welcome — the templates ask for the __dshChatLocator.state() output, which answers most triage questions in one paste.
📄 License
MIT