dsh-multi-chat
Manifest valid★ 1Side-by-side session grid for the DeepSeek Harness web GUI: click sessions in the sidebar, get an equal-cell grid of live conversations (max 3 per row), drag the top-right handle to reorder, each pane keeps its own composer.
dsh-multi-chat
Side-by-side sessions for the DeepSeek Harness web GUI, as an equal-cell grid. Click a session in the left sidebar and it joins the conversation area as its own pane: one click → one session, three clicks → three sessions. At most three panes per row; the grid degrades to two or one column when the window is narrow. The session you clicked (the shell's current session) stays highlighted, every pane keeps its own bottom-pinned input box, and panes can be reordered by dragging the ⠿ handle in the top-right corner of a pane header.
中文说明:README.zh.md · 设计依据:docs/design.md · Design notes (EN): docs/design.en.md
What it does
| Follow the sidebar | Every session you click in the sidebar becomes a pane. The pane order survives a page reload. |
| Equal-cell grid | At most three equal cells per row (3 × 3 = 9 panes max, six by default). Rows share the height but never below a comfortable pane height (~420 px); beyond that the grid scrolls. Narrow windows degrade 3 → 2 → 1 columns. |
| Highlight | The current session's pane is ringed, its title emphasized, and its sequence badge inverts. |
| Focus by click | Clicking inside a non-active pane asks the shell to make that session current (the official sidebar highlight follows). |
| Reorder by drag | Grab the ⠿ handle at the top-right of a pane header (immediate drag) or press the header itself (4 px of travel, or a 300 ms hold on touch). Drop on another pane to swap the two, drop on empty grid space to move the pane to the end, Esc cancels. The dragged pane follows the pointer as a ghost card and the drop target is outlined. |
| Reorder by keyboard | The handle is focusable: Ctrl/⌘ + arrows swaps with the neighbouring cell (grid-aware for up/down), otherwise nothing happens. |
| Close | Each pane has a close control. The active pane can only be closed by handing the shell over to a sibling — otherwise the control is disabled with a reason. The toolbar's Keep only active collapses the grid. |
| Sidebar toggle | Each session row gets an extra action button (next to archive/pin) that adds/removes that session as a pane without navigating. |
| Degrade, never break | A single pane → the official single-session UI is used untouched. Unknown/closed sessions render a note card. A crashed pane is contained by a pane-level error boundary. No slots/sessions service → the plugin stays inert instead of failing the web boot. |
Install
The package is a DSH bundle plugin: the host half (. → lib/index.js) only exists so the
package becomes a loader entry; the browser half (./client → lib/client.js) is what the web
shell loads through the dsh.client declaration.
dsh plugin --profile desktop add link:<path-to-this-package>
# then restart the desktop app / profile (bundle layers apply at boot)
Read-only check that the row composes (writes nothing):
dsh --profile desktop --dump-config | Select-String dsh-multi-chat
Uninstall: dsh plugin --profile desktop remove dsh-multi-chat.
Development
npm install # dev-only: react, react-dom, jsdom, yaml, @deepseek-ai/dsh-client-ui-slots
npm test # 7 suites, 90 tests
npm run typecheck
v0.3.0 feature — the grid can be switched off: pick Settings → Plugins → dsh-multi-chat and flip Enable the side-by-side grid (or press Exit grid in the grid toolbar). Off returns the conversation area to the official single-session view; the panes you had opened are remembered, so switching back on brings the grid straight back. The preference is stored separately from the pane order, so toggling can never disturb it.
v0.2.6 fix: the official turn navigator (the tick rail with turn previews on the right of a conversation) hid itself inside every pane, because the official stylesheet hides it whenever its conversation container is narrower than 900px. One pane-scoped rule with higher specificity restores it (the official class prefix is read from the official stylesheet at runtime, and nothing is injected when that stylesheet is missing). Verified live: the rail renders 28×102px with its marks inside a 456px pane.
v0.2.5 fix: the column count was hard-coded to three, so two sessions left a third of the row empty.
The grid now fills: columns follow the available width (auto-fit collapses the tracks no pane lands in),
two panes share the row, and three across appears only when the width allows — panes stay at least ~400px
wide. Measured at a 1680px window: two panes = 2×688px (100% of the row), three = 3×456px, four = 3+1
equal cells.
v0.2.4 change: the grid is a wide-window feature, so a narrow window keeps the official single session view. Below 720px of window width the plugin is completely inert — no session is retained, no slot is replaced, and nothing is written to the shared storage (which the floating session window would otherwise clobber with its own pane set). Widening the window brings the grid back; narrowing hands the conversation area back. Verified live: 490×640 cold start shows the normal conversation, 1680×1050 shows the grid, resizing switches both ways.
v0.2.3 fix: in a narrow window (the ~490 px floating session window) the grid toolbar wrapped into
three lines and squeezed the conversation. It now never wraps and degrades by width — dropping the drag
hint, then the usage counter, swapping the title for a compact "N panes" label — and the row height
yields to short windows (min(420px, 62vh)). Measured in a 490×640 window: one 37 px line, single
column, composer fully visible.
v0.2.2 fixes (reported from a live session, located in a real browser): the grid used to be
registered in a session-scoped slot, which the renderer remounts whenever the shell switches
session — so every switch rebuilt the grid and all pane conversations (the lag that grew with the
number of panes) and replaced the composer input mid-click (the "needs a second click to focus"
bug). The grid now lives in the root-scope main / conversation panel cell (same replacement
semantics, no remount), and pane activation runs on focusin/click instead of pointerdown.
Measured live: one click focuses the composer, session switching ≈ one frame, 5 panes ≈ 2.6k DOM
nodes with off-screen panes virtualised. Details: docs/design.md §4.9.
v0.2.1 fixes: switching a session no longer walks a retain → publish → retain recursion (it
used to overflow the stack, slow every switch down and leave the pane ring and the sidebar highlight
disagreeing); dragging can no longer strand a ghost or a drop ring when the pointer is released
outside the grid; the selection ring is a single crisp outer ring instead of a double-drawn edge.
Details and the reproduction evidence: docs/design.md §4.8.
| Suite | Covers |
|---|---|
tests/manifest.test.mjs | package/bundle/client-half/patch/locale contract, plus "the composer fix stays in place" |
tests/host.test.mjs | the inert host half never throws |
tests/store.test.mjs | pane ledger: follow adds panes, ceiling 6/9, eviction, highlight, swap/moveToEnd, persistence + the v1→v2 migration, untrusted payloads, drop targeting, keyboard steps |
tests/registration.test.mjs | activation contract: no registration for one pane, shadow registration for 2+, retention/release, sidebar button, unknown sessions, disposal, missing services |
tests/real-slot-core.test.mjs | the same wiring driven through the real SlotCore shipped by @deepseek-ai/dsh-client-ui-slots |
tests/render.test.mjs | structural rendering: equal-cell grid, single highlight, handle + close per header, variant: 'embedded' for the conversation body, hero/settling phases, token-only stylesheet |
tests/interaction.test.mjs | React DOM in jsdom: pointer drag → swap, drop on empty → move to end, Esc cancel, header 4 px threshold, Ctrl+Arrow keyboard swap, close/focus, sidebar toggle, toolbar, pane-level crash containment |
End-to-end install verification (optional, needs a running profile):
# isolated profile that never touches your desktop profile
$env:DSH_HOME="$PWD\..\.dsh-verify"
dsh --profile multichat-verify --from-default-profile web --dump-config
dsh plugin --profile multichat-verify add link:$PWD
dsh --profile multichat-verify --port 4399 --no-open
# then, in another shell:
npm run e2e:served -- 4399 <token-from-the-printed-url> # boot graph + served bundle
npm run e2e:browser -- --port 4399 --token <token> # real drag + screenshot
e2e:browser needs permission to launch Chrome/Edge normally; some sandboxes deny Chrome's
crashpad child process and the script then reports that instead of hanging.
lib/client.js is the shipped browser bundle: a single readable file in the official
module-loader wrapper (window.__ModuleLoader__.load({ id, factory })) with require('react')
as its only import. No build step — edit the file and the client HMR driver picks it up while
pnpm run dev:web watches the checkout.
How it works (short version)
ctx.sessions.listis observed; the session withretainedBy.mainView > 0is the shell's current session. Each new current session joins an ordered pane set, which is the grid order.- Every pane holds its own
sessions.retain(id, { source: 'multichat' })reference, so the client keeps that session's scope alive even after the user navigates away. - While 2+ panes are open the plugin registers into the documented
main.conversationreplacement point (singleslot, lowerpriority) and renders one<SessionProvider session={reference}>subtree per pane around the officialconversation.contentFactory — rendered asvariant: 'embedded', which is what keeps the composer's phase-gatedposition: sticky; bottom: 0alive without the official conversation root (see docs/design.md §4.2). With one pane the registration is disposed, restoring the official view. - Styles use one tagged
<style>element and only--dsw-alias-*/--dsw-radius-*theme tokens, so light and dark themes both work.
Known limitations
- The official conversation header (title breadcrumbs, view tabs, header actions) belongs to the entry the plugin replaces and cannot be rendered by a third party, so grid mode draws its own compact pane headers instead.
- Panes keep their sessions open, so N panes mean N live session streams; the default ceiling is six, and the hard cap is nine.
- Drag reordering is pointer-based (mouse/touch/pen) plus the keyboard shortcut; there is no auto-scroll while dragging near the grid edge.
- The client boot path does not pass cordis config to browser plugins, so all knobs live in the
grid toolbar /
localStoragerather than incordis.patch.yml.
License
MIT — see LICENSE.
Comments
Loading…
From the same category
by awesome-dsh-plugin
A curated list of plugins for DeepSeek Harness (dsh) · DeepSeek Harness 插件精选列表
★ 18.5k
CC0-1.0
Python
Oct 12, 2026
by 0xsline
DeepSeek Harness (DSH) ecosystem: curated plugins, tools, and infrastructure from dsh-external/hub and the public dsh-plugin topic.
★ 1.2k
CC0-1.0
Python
Oct 10, 2026
by pax-beehive
Open-source CLI, schemas, resolver, and DSH agent tools for DSH Plugin Hub
★ 400
MIT
TypeScript
Oct 6, 2026
by xiajiajun516
DeepSeek Harness (DSH) backup & restore plugin — export, import, migrate and sync your complete DSH configuration, plugins, MCP servers, skills and workspace. One-click migration to another machine.
★ 185
MIT
TypeScript
Oct 13, 2026
dsh plugin --profile web add dsh-config-managerby yjh051108
推荐组件(非必须):DeepSeek Harness 运行时注入器;已随 dsh-routing-suite 单仓库化保留,本仓库继续维护/发布。
★ 166
TypeScript
Sep 18, 2026
dsh plugin --profile web add @dsh-external/dsh-super-injectorby jigjoy-ai
A CLI that turns a goal into a pull request - and a sandbox for testing concurrent AI coding agents on the Mozaik runtime.
★ 124
MIT
TypeScript
Oct 12, 2026