MsgRail(dsh-msgrail)
August 22, 2026 · View on GitHub
MsgRail is a narrow message rail for DeepSeek Harness: one thin bar on the left of the chat for each message you sent. Hover to preview, click to jump — older messages load automatically. Full history, live, bilingual.
MsgRail(消息轨道)——DeepSeek Harness 的窄消息轨道插件:在对话左侧,为每条你发送的消息显示一根细横线。悬停看预览、点击即跳转,更早消息自动加载;全量历史、实时更新、中英双语。
中文说明见 README.zh.md
Repository / 仓库:
- GitHub: https://github.com/chou109/dsh-msgrail.git
- Gitee (mainland-China mirror — faster there): https://gitee.com/chill109/dsh-msgrail.git
Screenshots
Demo captures from
dsh web(light theme), stored indocs/screenshots/.
| The message rail | Message example |
|---|---|
![]() | ![]() |
If you are a human, read this
What it does
- A very narrow rail (44px) on the left of the chat — between the sidebar and the messages, no divider; the rail background joins the conversation area.
- One thin horizontal bar per message YOU sent in the current conversation (AI replies, auto-injected context, interrupts and commands are all excluded), densely stacked and vertically centered in the rail. No text in the rail — a pure minimap of your prompts.
- The whole history is indexed — the rail fetches the complete conversation history itself, so older messages appear even before they are loaded into the chat window.
- Hover a bar → it grows longer and bolder, and an attached preview card shows your exact message text + time, auto-positioned inside the viewport (auto-injected hint blocks, such as the paste-image instructions, are hidden). Click a bar → the conversation scrolls to that message; older messages are loaded automatically first if they are not yet in the chat window.
- Scrolls when overflowing — scroll up for older messages; the view stays pinned to the latest as new messages arrive. Switching sessions reloads the rail for the new chat.
- Deliberately minimal: no collapse, no search, no session list, no separate workspace.
Manual deployment
Requires: DeepSeek Harness web profile (dsh web), rc.7/rc.8 line. No build toolchain needed.
-
Patch the host layout (one-time; re-run after every harness update):
node scripts/patch-layout.mjsThe script adds a
shell.historygrid column to@deepseek-ai/dsh-client-ui-layout(sized byvar(--dsh-history-width), default0px, so an uninstalled plugin costs no space). It is idempotent and reports loudly if an anchor no longer matches. The layout bundle is resolved from$DSH_HOME(falling back to~/.dsh); pass the path explicitly (node scripts/patch-layout.mjs <path>) for non-standard layouts. -
Copy the plugin into the profile's node_modules:
cp -r dsh-msgrail ~/.dsh/profiles/node_modules/ # or on Windows PowerShell: Copy-Item -Recurse dsh-msgrail "$env:USERPROFILE\.dsh\profiles\node_modules\" -
Register the plugin in
~/.dsh/profiles/web/cordis.patch.yml(append):- insert: - id: msgrail name: 'dsh-msgrail' -
Restart the harness (
dsh web). The rail appears on the left of the chat.
How to let an AI deploy it
Tell your AI agent:
Please deploy the MsgRail plugin from https://github.com/chou109/dsh-msgrail.git (or the Gitee mirror, faster in mainland China: https://gitee.com/chill109/dsh-msgrail.git) to my DeepSeek Harness web profile. Follow the "If you are an AI, read this" section of the README exactly.
The AI section below contains the full deployment, verification and debugging instructions.
Uninstall
- Remove the
msgrailentry from~/.dsh/profiles/web/cordis.patch.yml. - Delete
~/.dsh/profiles/node_modules/dsh-msgrail/. - (Optional) revert the layout patch by re-installing/upgrading
@deepseek-ai/dsh-client-ui-layout(the column disappears automatically once the plugin's stylesheet is gone, so this is optional). - Restart
dsh web.
If you are an AI, read this
Mission
Deploy the dsh-msgrail plugin for a DeepSeek Harness web profile (dsh web,
rc.7/rc.8 line). The deliverable is a shell.history grid column in the host layout plus a
client bundle that renders a narrow message rail — one bar per user-sent message of the
current conversation, with hover previews and click-to-jump.
Repository layout
dsh-msgrail/
├── package.json # plugin manifest: dsh.client declaration + exports["./client"]
├── lib/
│ ├── index.js # minimal cordis host half (keeps the Loader entry valid)
│ └── client.js # the browser bundle (hand-written, follows the client-bundle contract)
├── scripts/
│ └── patch-layout.mjs # idempotent patch for @deepseek-ai/dsh-client-ui-layout
├── README.md / README.zh.md
└── LICENSE (MIT)
How the pieces fit
- Client discovery: the harness's client-modules scans enabled Loader entries for
packages declaring
dsh.client, resolvesexports["./client"], hashes the bundle, serves it at/plugins/<id>/client.jsand injects it into the browser boot graph. The bundle registers viawindow.__ModuleLoader__.load({ id, factory }); the factory receives the shell'srequire(react, react/jsx-runtime, @deepseek-ai/dsh-client-ui-primitives). - Layout column:
scripts/patch-layout.mjsedits@deepseek-ai/dsh-client-ui-layout/lib/client.js(three edits): the grid becomessidebar | var(--dsh-history-width, 0px) | center | details; a<div class="dsh-layout-historyCol">renders theshell.historyslot between the sidebar and the conversation;"shell.history": { kind: "single", scope: "root" }is declared. The idempotency marker isdsh-session-history (patched)(predates the rename — do NOT change it; the already-patched host layout carries it). - Rail width: the bundle's stylesheet declares
:root { --dsh-history-width: 44px }(removed automatically on unload via the loader's style-tag tracking). The hovered bar's preview is aposition: fixedcard measured at runtime and clamped inside the viewport (flip left on right-edge overflow, shift up on bottom overflow). - Message data — full history, your messages only: the rail pages the
whole session log itself via the paginated history RPC
(
session.history({ beforeSeq, maxMessages: 200 })throughctx.sessions.binding(id)?.session) untilhasMoreis false, and renders one bar peruser/messageevent whosesurfaceOp === "append"anddata.source.kind === "user"(assistant replies, steering, context injections and commands are skipped — the conversation UI's own discriminators), ordered byevent.seq. Because it pages the log RPC directly, the index is independent of the conversation's loaded window: older messages appear even before the chat has loaded them. Live updates come fromsession.subscribe→ a debounced (250ms) tail-page refetch merged byseq. If a future release drops the (type-private)session.historyRPC, the bundle falls back to reading the conversation snapshot (session.getSnapshot()→ the loaded window) and extending it withsession.loadOlder()while the snapshot reportshasMore— waiting for the window to open first and re-running the walk on live updates, so the fallback never silently shows only the loaded messages. - Preview text: extracted from the content blocks'
textfield; blocks that are auto-injected hints (starting with[图片附件,[系统提示:当前模型无法直接查看图片or[用户粘贴的图片) are skipped. - Jump: the conversation anchors rows with
data-chat-anchor-key={conversationContextKey(definitionKind, id)}— the user definition isinput-message(id = message id), so the key is{13}:input-message{id}. If the anchor is absent (message not yet loaded), the rail callssession.loadOlder()repeatedly (cap 60 pages) until the anchor appears, thenscrollIntoView({behavior:"smooth", block:"center"}).
Deployment steps (exact)
-
Patch the layout (from the repo root):
node scripts/patch-layout.mjs # the default target is resolved from $DSH_HOME (~/.dsh); or pass an explicit path: node scripts/patch-layout.mjs "<path>/node_modules/@deepseek-ai/dsh-client-ui-layout/lib/client.js" -
Install the package:
echo ${DSH_HOME:-$HOME/.dsh} # e.g. C:\Users\<you>\.dsh on Windows cp -r dsh-msgrail "$DSH_HOME/profiles/node_modules/" # the package folder itself -
Register the Loader entry in
"$DSH_HOME/profiles/web/cordis.patch.yml":- insert: - id: msgrail name: 'dsh-msgrail' -
Restart
dsh web.
Verification
node --checkpasses onlib/client.js,lib/index.js,scripts/patch-layout.mjs;node scripts/smoke-test-client.mjsloads the real bundle in a shim and exercises the injected session actions (history RPC pagination/envelope, guards, snapshot read).- Installed copy:
node -e "console.log(require('<installed>/package.json').exports['./client'])"prints./lib/client.js. - JSON/BOM trap:
package.jsonmust be UTF-8 without BOM (the harness parses it withJSON.parse; a BOM aborts startup withUnexpected token ''). Check:grep -rl $'\xef\xbb\xbf' "$DSH_HOME" --exclude-dir=node_modules; strip:sed -i '1s/^\xEF\xBB\xBF//' package.json. - After restart: the rail appears left of the chat; hovering shows a preview (clamped to the viewport), clicking jumps (auto-loading older pages when needed), and switching sessions reloads the rail.
Debugging
- Column missing: restart needed?
msgrailincordis.patch.yml? installedpackage.jsondeclaresdsh.client+exports["./client"]? layout patch marker present? browser console errors on/plugins/dsh-msgrail/client.js? - Startup crash
Unexpected token '': BOM inpackage.json(strip it). - Bars ≠ your messages: the filters are
surfaceOp === "append"anddata.source.kind === "user"; injected/assistant/steer rows are skipped by design. - Jump does nothing: the target may be outside the loaded window; the rail calls
loadOlder()(max 60 pages); if the session has no more history the anchor cannot exist. - Harness upgrade drift: slot names (
shell.history),ctx.sessions.binding, the history RPC and the chat-node key formula are internal APIs — re-runscripts/patch-layout.mjsand re-check after upgrades.
Operations
- Version bump: edit
versioninpackage.json, re-copy the package folder to the profile's node_modules (both copies byte-identical, BOM-free). Never rewrite JSON via PowerShellSet-Content -Encoding UTF8(writes a BOM on Windows PowerShell). - Harness update: re-run
scripts/patch-layout.mjs, restart.
Compatibility and limitations
- Verified against the rc.7/rc.8 web profile (the current
dsh webline). The paginated history RPC (session.history({ beforeSeq, maxMessages })) is still present on the Session class at runtime — it is type-privateon the session face, not removed — so the rail pages the full log directly (the same approach that originally made "unloaded messages also appear" work). Should it disappear in a later release, the bundle's snapshot +loadOlder()fallback takes over (bounded walk, waits for the window to open, re-runs on live updates). Slot names, the session service API and layout internals remain private and may change in later versions. - Requires the host-layout patch (idempotent re-apply script included).
- Fixed 44px rail (no collapse, no resizing); the rail indexes every user-sent message of the current conversation (RPC walk capped at 300 pages of 200 events; the fallback walk at ~250 user bars / 50 pages), and clicking a bar loads older pages on demand when the message is outside the loaded window.
- Only the current conversation's user-sent messages are shown.

