Bundle
@huanghanheng/dsh-ui-turn-rail
Turn progress rail plugin for the DeepSeek Harness Web GUI: a sticky dot strip over the conversation scrollport with per-turn question summaries, one-click deep history loading, and AI-option summaries
- Source
- HenryHwong
- License
- MIT
- Updated
- Updated 13 days ago
Readme
# dsh-ui-turn-rail
> **🌐 Language / 语言:** [**English**](README.md) · [简体中文](README.zh.md)
Turn progress rail plugin for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) Web GUI: a sticky dot strip pinned to the conversation scrollport, one dot per loaded turn, with per-turn question summaries, one-click deep history loading, and summaries for AI-option picks.
**GitHub topics**: `dsh-plugin` · `deepseek-harness`
---
## Features
- **Always-visible rail.** The dot strip is sticky to the conversation scrollport, so it stays in view while the transcript scrolls — even in very long conversations.
- **One dot per turn whose question is loaded**, plus dashed placeholders for turns whose question is still outside the window. AI replies and reasoning never count as a loaded question: a turn whose question has not been paged in yet stays a placeholder.
- **Question summaries.** Hover a dot to see that turn's first question as a one-line preview. Both typed input (`user` nodes) and picked AI options (`steering` nodes) produce summaries.
- **One-click deep-history load.** Click a placeholder and the plugin pages every older batch until the target turn's question is actually in the window (a page boundary can land a turn's first node while its question sits in an older page), then jumps straight to that question row.
- **Click a loaded dot** to scroll the transcript to that turn's question.
- **Scrollable dot window.** The rail shows about ten dots at a time with internal scrolling; the active turn's dot auto-follows into view as the reader moves.
## Security
- **Presentation only.** The rail renders facts already present in the session snapshot. It never produces a model-visible input, never writes the session log, makes no network requests, and touches no credentials.
- **Model Experience**: nothing reaches a model request; token effect none; KV-cache effect none.
- **No new privileges.** The host instrumentation (below) adds one read-only slot to the chat view; it introduces no new RPCs, permissions, or secrets handling.
- **Data lives in the host.** All derivation (turn order, node→turn mapping, summaries) runs against the client-side `ChatSnapshot`; the rail holds no session state of its own.
## Requirements
A DeepSeek Harness checkout (or the published `@deepseek-ai/dsh-*` packages) with the **host instrumentation** applied — the rail registers into the `conversation.chat.rail` seat that ui-conversation's chat view must declare (see below). Without the seat, the plugin loads but renders nothing (the seat renders `fallback: null`).
## Installation
The plugin supports two installation routes. **Neither automates the host instrumentation** — that is a source-level patch to the harness checkout (see [Host instrumentation](#host-instrumentation)) and must be applied and rebuilt regardless of how the package is installed.
### Route 1 — bundle install via `dsh plugin` (recommended)
This repository is a [DSH bundle](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md): its `package.json` declares `dsh.bundle.patch` → `./cordis.patch.yml`, so the profile plugin manager installs it as a patch layer and the patch inserts the plugin into the web profile's browser roster.
```sh
# From the git repository (no npm publish needed):
dsh plugin --profile web add @huanghanheng/dsh-ui-turn-rail
# Or, once published to npm (shorter spec):
dsh plugin --profile web add @huanghanheng/dsh-ui-turn-rail
```
`dsh plugin` runs `pnpm add` in the profile directory and reconciles `dsh.profile.bundles` automatically: a dependency whose manifest declares `dsh.bundle.patch` joins the layer stack.
**Then apply the host instrumentation** to the checkout the profile runs from — the seat must exist for the rail to render. A ready-made patch ships in this repository, applied in one command (then rebuild the web client):
```sh
scripts/apply-instrumentation.sh /path/to/harness-checkout
(cd /path/to/harness-checkout && pnpm run build)
```
### Route 2 — manual npm install
The package publishes to npm with build artifacts committed (`lib/`), so it installs like any package:
```sh
# In the profile directory the app runs from (or the app itself):
npm install @huanghanheng/dsh-ui-turn-rail
# or: pnpm add @huanghanheng/dsh-ui-turn-rail
```
A manual `npm install` does **not** add the bundle layer automatically — declare it in the profile manifest so the patch inserts the plugin row:
```json
// $DSH_HOME/profiles/web/package.json
{
"dependencies": { "@huanghanheng/dsh-ui-turn-rail": "^0.1.0" },
"dsh": { "profile": { "bundles": ["@huanghanheng/dsh-ui-turn-rail"] } }
}
```
Or simply run `dsh plugin --profile web add @huanghanheng/dsh-ui-turn-rail`, which performs the install and the bundle-layer reconciliation for you (equivalent to Route 1).
**Then apply the host instrumentation and rebuild**, exactly as in Route 1.
## Host instrumentation (cannot be packaged)
The seat declaration lives in ui-conversation's slot contract, and the jump/paging gestures must operate ChatView's private scroll anchoring — neither can be delivered as a plugin, so `dsh plugin add` cannot automate it. The repository ships the exact change as `patches/chat-rail-seat.patch` (against harness master `b150a551b8`); apply it with `scripts/apply-instrumentation.sh` or `git apply --3way`, then rebuild. If your checkout has drifted, hand-apply the edits below. These are the exact changes:
### `packages/client/ui-conversation/src/client/contract/slots.ts`
1. SlotMap entry:
```ts
'conversation.chat.rail': { kind: 'single'; scope: 'session'; owner: ChatTurnRailOwnerProps }
```
2. Owner currency type:
```ts
export interface ChatTurnRailOwnerProps {
currentTurn: number | undefined
loadingOlder: boolean
hasMore: boolean
onJump: (key: string) => void
onLoadOlder: () => Promise<void>
}
```
3. Add `'conversation.chat.rail'` to the `PropsRenderSlots` union in `ChatViewSlotProps`.
4. Widen the injected paging contract to return the promise:
```ts
loadOlder: () => Promise<void>
```
### `packages/client/ui-conversation/src/client/apply.ts`
Declare the seat in the chat view entry's children:
```ts
children: {
'conversation.chat.node': { kind: 'keyed', scope: 'session', inject: CHAT_NODE_INJECT },
'conversation.message.images': { kind: 'single', scope: 'session' },
'conversation.chat.rail': { kind: 'single', scope: 'session' },
},
```
### `packages/client/ui-conversation/src/client/chat/ChatView.tsx`
1. Node key → turn map for scroll-position attribution:
```ts
const chat = useSession(s => s.chat)
const turnByKey = useMemo(() => {
const map = new Map<string, number>()
for (const turn of chat.timeline.turnOrder) {
for (const key of chat.locations.getTurn(turn)) map.set(key, turn)
}
return map
}, [chat])
```
2. Track `currentTurn` in the scroll handlers (set it from `turnOf(anchorKey)` or the latest turn when pinned to the bottom).
3. `loadOlderAnchored` returns the paging promise:
```ts
const loadOlderAnchored = (): Promise<void> => {
// ...existing paging-anchor logic...
return loadOlder()
}
```
4. Render the seat with the owner currency:
```tsx
{renderSlot('conversation.chat.rail', {
currentTurn,
loadingOlder,
hasMore,
onJump: jumpTo,
onLoadOlder: loadOlderAnchored,
}, { fallback: null })}
```
## Development
```sh
pnpm install
pnpm vitest run tests/ # unit tests (model + component, jsdom)
pnpm bundle # tsdown client bundle (needs the harness's clientBundle preset)
```
The tests cover the turn model (tick derivation, typed/AI-option summaries, placeholders) and the component (jump, one-click paging past page boundaries, active-dot follow, window scroll).
## License
MIT
Install
dsh plugin --profile web add github:HenryHwong/dsh-ui-turn-rail
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install huanghanheng-dsh-ui-turn-rail from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.