dsh-split-screen
No description
- Stars
- 1
- Language
- JavaScript
- Created
- Sep 5, 2026
- Updated
- Oct 19, 2026
Introduction
dsh-split-screen
Split-screen workspace for the DeepSeek Harness web GUI — chat with several agents side by side, in panes that look and behave exactly like the stock DeepSeek conversation.
What's new in 1.7.0
- Programmatic navigation now follows the split.
Alt+↑/↓/Alt+1‑9from dsh-session-hotkeys, the stock New Session button, agent-team / IM / notification opens and any otheruiWorkspace.openSession()caller move the root session binding and the focused pane with it — exactly like a sidebar click — instead of silently changing the covered stock view behind the overlay. A restored layout is primed, so it is never yanked at boot. - Pane editors can no longer be permanently orphaned. The shared
per-session Lexical editor used to be reclaimed inside
keydown/beforeinputagainst a 6-repoints/10 s budget, and the guard then stood down forever after a few ordinary sidebar switches — the visible pane composer became a plain contenteditable (typing painted, Enter/Ctrl+A dead,/never opened the command menu) whilehealth()stayed green. Repairs are now deferred out of the keystroke, throttled (one per second) rather than stopped, re-armed once stable, and reported throughhealth().editorRoots. Ctrl/Cmd+A and the last-resort Enter path repair an orphaned host first. /compactcompletion is announced. Native DSH only writes a muted transcript-tail row; a new flow observer shows a short toast — "context compacted" / "compaction did not complete" — in stock and split views.
Known remaining gap: the compact/TUI composer (Alt+T, or panes shorter than
~280 px) is the plugin's own single-line textarea and still has no native
slash-suggestion menu; type / in the full composer (Alt+T) for suggestions.
Keyboard shortcuts
| Keys | Action |
|---|---|
| Alt+T | Toggle the focused pane between the full composer and the compact TUI line |
| Alt+Shift+← / → / ↑ / ↓ | Move keyboard focus to the neighboring pane |
Everything else is mouse-driven and works exactly like the stock DeepSeek UI. On macOS the same chords run with ⌥ Option in place of Alt (Option+T, Option+Shift+Arrows).
What it does
- Split-only by design — with a single conversation open you see the stock view, untouched: no overlay, no re-hosted composer, nothing covered, and every stock control (the workspace chooser, the agent-preset hero on a new session) stays clickable. The split workspace appears the moment there are two panes and steps aside again when the second-to-last pane closes.
- Split any pane — drag a conversation from the DeepSeek sidebar onto a pane edge (left / right / top / bottom) to split it there, or drop it in the center to swap that pane's conversation. With the split still off, dragging a conversation onto the conversation column starts the split: a ghost previews the target half, edge drops split in that direction.
- Native look, light & dark — panes render with the stock DeepSeek styles, theme variables and primitives: user bubbles, Think rows, tool rows, the streaming shimmer, the rounded composer card.
- Fully interactive panes — every pane is a live session: streaming responses, Enter to send, Shift+Enter for newline, /commands, Stop while running, queued messages, todos, and image paste & drag-drop attachments.
- Questions and approvals land in the pane — when an agent calls
ask_user_question(or a plan review / approval prompt opens), the pane's composer is taken over by the native question card, exactly like the stock view: options, multi-select, free-text answers, Skip and Cancel all work in place. The pending interaction is read from the ROOT standarduseSessionStatus(...).pendingInteractionsource — the same hook the stock conversation uses — so every pane answers its own session's request. - Every view tab works inside the pane — Chat, Trajectory and any plugin
view (for example the
dsh-contextContext dashboard) switch in place, with the view's own detail slots (conversation.trajectory.images, inspect panes) routed into that pane. The roster is read from the slot ledger, so a view registered after the pane mounted still shows up. On a brand-new session the stock header hides itself until the first message — the view tabs come back with it. - Two composer styles — a full composer, or a compact terminal-style
TUI line with the same width and button positions (Alt+T). The TUI line
carries the composer card's own type scale: its font size and line height
are measured from the live card (
--dsh-content-font-sizeand the matching line height, 14px/24px by default) instead of inheriting the pane's 16px, its left inset matches the card's 14px text inset, and its caret uses the stock token — so Alt+T swaps the input without changing the reader's typography. - Neat by default — queued messages keep the stock dock geometry: the
queued-messages panel is
card − 48pxwide (minus its own 8px inset, asQueueDock.module.cssdefines it) and centered on the input card it merges into, so the queue, the todo strip and the stats line all share the exact width of the input box. The card geometry is measured from the live composer editor (dsh 0.1.5 renders it as a Lexicalcontenteditable). - Persists — safely — the split layout survives page reloads, but the
stored tree is validated on load: duplicate panes de-duped, the pane count
capped at 6 (
MAX_PANES), depth capped at 6, invalid payloads discarded with one console note instead of re-activating a broken workspace. A layout that crashed the workspace before it became healthy is not auto-activated on the next boot (see Safe mode below).
Getting started
- Install the plugin and restart
dsh web— nothing changes until you actually split: one conversation looks and behaves exactly like stock. - Drag a conversation from the sidebar onto the conversation column to start a split (or drop it on a pane edge once you already have panes).
- Click a conversation in the sidebar to load it into the focused pane.
- Use the pane header's split buttons (⬎ / ⬏) for more panes, ✕ to close, and drag the dividers between panes to resize. The ? button (and the one-time tip card) lists the shortcuts and how to disable split view.
- Press Alt+T to try the compact TUI composer.
Safety, recovery and diagnostics (1.7.0)
The plugin is built so that a failing pane can never take the host down with it. Everything below is available without devtools-only tricks; all state is reported by one function that returns plain JSON.
Kill switch — disables the split workspace unconditionally and persists it (the saved layout is kept, not deleted):
window.__dshSplitView.setEnabled(false); // disable
window.__dshSplitView.setEnabled(true); // re-enable
// equivalent persistent switch:
localStorage.setItem("dsh.split-view.enabled.v1", "0");
Safe mode (crash-loop guard) — every deliberate activation is recorded before it can crash; after the workspace has been up and responsive for ~9 s it is marked healthy and the crash counter resets. If a previous boot never reached healthy, or the same guard tripped twice, the next boot does not auto-activate the saved layout:
- the stock view is untouched and the saved layout is kept,
- a small dismissible chip appears bottom-right: "Split screen paused after a crash — Retry / Disable",
- the console gets one actionable warning,
- Retry clears the counters and re-activates; Disable sets the kill switch.
Repeated render failures (3 boundary catches in ~20 s) also switch to safe mode automatically, and a workspace-level crash does so immediately. A failing pane body is isolated with its own Retry card — the other panes keep streaming. A changed plugin release resets the crash guard automatically.
Diagnostics API — window.__dshSplitView:
| Method | Returns |
|---|---|
health() | {pluginVersion, hostRelease, testedHost, testedReleases, enabled, safeMode, active, geometryBlocked, panes, maxPanes, blockedReason, failures, lastError, geometry, persistedTree, crashGuard} |
reset() | clears the saved tree + crash counters, stays in the stock view |
retry() | clears crash counters/safe mode and re-activates the kept tree |
help() | shortcut/help data (the same information as the "?" card) |
setEnabled(flag) | the kill switch |
focusChat(), closeFocused() | the companions bridge (dsh-session-hotkeys) |
All methods return plain serializable data and never throw. health().active
means "the split workspace is rendered and responsive"; health().panes is the
true persisted tree state even while safe mode/geometry keeps it off screen.
Tested host versions — the plugin reads
window.__DSH_BOOT__?.release?.version and compares it against a
TESTED_RELEASES list (currently 0.2.0-rc.2 and 0.2.1-rc.2 — the build is
verified on the user's live 0.2.1-rc.2 host). An untested release logs ONE
info note and shows up in health().testedHost — activation is never blocked
by the check; the crash loop guard is the protection.
Limits — at most 6 panes (MAX_PANES); persisted trees deeper than 6
levels or with duplicate/unknown session ids are repaired or discarded on load.
UI strings are localized (EN/ZH). The split-only contract is unchanged: with
fewer than two panes the plugin renders nothing over the stock UI — its only
chrome is the small fixed chip while safe mode/geometry keeps a saved split
inactive.
Robustness notes (1.4.0)
- The overlay is constrained to the measured conversation column on all four edges (sidebar left, details panel right, desktop chrome above and below). If the column cannot be measured with confidence on a given shell, the workspace stays inactive with one console note rather than covering a guessed area — the stock UI is never blocked.
- While the split is off, the plugin's steady-state cost is zero: no observers, no DOM scraping, no session staging.
- While the split is on, all observation paths are bounded (throttled DOM list adapter, settle-and-exit geometry loops, a rate-limited circuit breaker on the shared Lexical editor root) so a contested resource fails soft instead of freezing the app.
- If the workspace component itself ever throws, it isolates itself: the stock view keeps the viewport, the saved layout is kept, and the plugin enters safe mode with a visible chip (Retry / Disable) instead of disappearing silently — see Safety, recovery and diagnostics above.
Agent Teams & mode selector in panes (1.5.0)
- The session header's action row re-hosts completely inside panes: the
Agent Team trigger, the agent-preset (mode) label, cost-meter statistics
and every other
conversation.session.header.actionsentry render in both the full header (wide panes) and the slim header (narrow panes). The team panel itself is portaled above the split overlay, so it is never clipped. - Clicking a team member in a pane is split-aware: their pane gets focused, or their conversation splits in beside the current one (the stock action's main-view guard would have ignored the click for pane sessions).
- TUI composer mode keeps its header-less minimal design (Alt+T toggles back); with zero or one pane the plugin renders nothing and the stock menu bar is untouched.
Recommended companions
- dsh-session-hotkeys —
keyboard-first control for the DeepSeek Harness:
Escreturns focus to the conversation,Alt+Entersteers or queues while an agent runs, the model picker is keyboard-reachable, and every shortcut is rebindable in a visual panel. Works great inside split-screen panes. - dsh-result-only-view — a compact results-only reading mode for running sessions: less visual noise while several agents work side by side in your panes.
Install
dsh plugin --profile web add dsh-split-screen
then restart dsh web.
How it works
- Client-only plugin: the host half is an empty loader entry; everything
runs in the browser on public service surfaces (
sessions,slots,conversation), plus a small per-session staging bridge (session.open()). - Panes stage real sessions through the sessions service (history window + live event stream), so every pane is a first-class conversation.
- Injects:
@deepseek-ai/dsh-client-ui-layout(owner of theshell.overlayslot the workspace mounts into),@deepseek-ai/dsh-client-locale,@deepseek-ai/dsh-client-ui-primitives. - The stock DeepSeek sidebar is deliberately untouched — it stays the single source of truth for conversations.
- Transcripts reuse the ui-conversation CSS module rules (copied verbatim
under
dsv-class names) so a harness upgrade cannot silently break the look.
Development / tests
No dependencies and no build step: lib/client.js is the shipped browser
bundle, written for window.__ModuleLoader__.load({id, factory}), and is
syntax-checked directly. Node.js 18+ runs the suites (CI uses Node 24).
node --check lib/client.js # bundle syntax
npm test # both sandboxed suites (smoke + safety)
tests/smoke.mjs(16 assertions) evaluates the bundle under a stub DOM and covers the module-loader contract, the double-apply guard, the global-chord / drag-bridge / IME listener lifetimes, full teardown + re-apply, and the split-only activation gate for empty, single-pane and two-pane persisted trees.tests/safety.mjs(117 assertions) covers the 1.6.0–1.7.0 hardening: 0.2.0/0.2.1 frame-geometry fixtures (shell.bottom/handles/overlay excluded, centre anchored, validated grid fallback, fail-closed measurement), persistence validation (corruption, duplicate ids, pane cap, depth), the kill switch, the crash-loop guard → safe mode → retry/reset, thehealth()/help()/setEnabled()diagnostics contract, invisible-active (B3) chord gating, the composer-chain inject-face parity behind in-pane ask/approval cards (useQuestionCard/matched/ store / dispatchers), and the bounded session-retain retry driver with its one-warning diagnostics. The 1.7.0 behaviors themselves (root-follow parity, editor-root ownership, compaction toast) are verified live on an isolated real 0.2.1-rc.2 client — see VERIFICATION-2026-10-20.md.
Both suites are sandboxed (node:vm + stub DOM/localStorage): they never touch
a DSH profile and never start or contact the running app. CI runs the same
commands on every push/PR (.github/workflows/ci.yml).
Documentation
- AUDIT-2026-10-19.md — static audit of the 1.5.1 bundle: architecture map, host-breaking vectors, risk table, must-fix list.
- DIAGNOSIS-2026-10-19.md — live reproduction on the real 0.2.1-rc.2 desktop host (inert geometry, factory-slot drift, invisible-active chords).
- RESEARCH-2026-10-19.md — replacement-candidate research: maturity, parity and risk of the alternatives in the ecosystem.
- SPIKE-2026-10-19.md — empirical test of the top replacement candidate on an isolated 0.2.1 scratch profile.
- VERIFICATION-2026-10-19.md — independent release verification of the hardened build on the isolated real 0.2.1 host.
- VERIFICATION-2026-10-20.md — 1.7.0
follow-up verification: hotkey/sidebar parity, New Session adoption,
editor-root ownership after navigation,
/menu and compaction feedback on an isolated real 0.2.1-rc.2 client. - ROBUSTNESS-REPORT-2026-10-19.md — end-to-end report: runtime failure analysis, alternatives, and the 1.6.0 hardening.
- REVIEW-2026-10-03.md — earlier code review of the pre-1.4 composition.
Compatibility
- Requires DSH 0.1.5 or newer. Verified end-to-end on Windows against
@deepseek-ai/dsh@0.1.5-rc.2and on@deepseek-ai/dsh@0.2.0-rc.2underdsh weband the 0.2.0 desktop composition (same client, desktop profile bundles + patch layer).TESTED_RELEASESin the bundle lists the releases the build is signed off against;health().testedHostreports the live comparison. - 0.2.1 support (1.6.0) — the 0.2.1 shell and renderer differ in two ways
the 1.5.x line could not survive: the frame gained a static
shell.bottomrow (which broke the conversation-column measurement, making split view a silent no-op) and content moved to a new factory-slot render dispatcher (renderFactorySlot) that every user bubble and the model label call. 1.6.0 fixes both: the geometry is now selected structurally (center column + validated grid-track fallback,shell.bottom/handles/overlay excluded), and panes receive a guardedrenderFactorySlotoutlet plus the declared-childrenrenderSlot/renderSlotChainseats the stock renderer attaches. The live0.2.1-rc.2host is verified on the isolated real 0.2.1 desktop host by the release verifier (and reported as tested byhealth()); a changed shell shape fails CLOSED (stock view stays in charge, safe-mode chip appears) rather than covering the wrong area. - 0.2.1 follow-up (1.7.0) — the 1.6.0 click-only adoption contract and its
permanent editor-root breaker were themselves split-view failure modes on
0.2.1: programmatic session navigation (session hotkeys, New Session,
agent-team/IM opens) and ordinary sidebar switching during a split could
leave the panes on the old conversation and orphan a pane composer. 1.7.0
follows the root session binding itself, keeps the editor guard repairable
and visible (
health().editorRoots), and adds a compaction-completion toast. - 0.2.0 support. The 0.2.0 client split the standard source kit in two, and
the plugin assembles both halves the way the renderer does:
- session faces come from a retained
SessionReference(sessions.retain(id, { source })+uiSession.bindingSource(reference)), which the plugin holds for each pane's lifetime; - the global/root faces (
useSessions,useSessionStatus,useSessionRetainInfo,useResource,useWorkspaces,usePanelInfo) are harvested from the plugin's own root-scopedshell.overlayseat, where the renderer materializes them — including anything a third-party plugin contributed throughslots.provideRoot; - the Chat node seat's contextual injected hooks (
useDisclosure,useTurnData) are bound per render from thehookContextthe stock ChatView passes throughrenderSlot. A pane therefore needs nothing private from the renderer, and it is never suppressed: if its session faces are not ready it renders its own built-in transcript and composer for that pane instead.
- session faces come from a retained
- The pane composer re-hosts the Lexical editor slot entries. In-pane question
/ approval cards are read from the ROOT standard
useSessionStatussource the stock conversation uses (with the 0.1.xuiSessionsource as fallback), so the plugin does not run on 0.1.1-rc.2 or earlier cores.
License
MIT