dsh-devpanel
A developer toolkit for the DeepSeek Harness (DSH) web console: a real multi-tab PTY terminal plus an AI-output file browser.
dsh-devpanel is a plugin for the DeepSeek Harness that brings three things into the browser console:
- Terminal panel — a multi-tab, real PTY terminal docked under the composer. Start and stop processes, type commands by hand, and watch live output, just like a native terminal.
- AI-output sidebar — a right-hand file viewer that lists every file the AI wrote or edited in the current session, with syntax highlighting, Markdown rendering, and image preview.
- Usage stats panel — a modal dashboard that merges TWO usage dimensions into one "how much did I actually work" view: AI activity (tokens, calls, messages, model share) and terminal activity (sessions, submitted commands, wall time — the dimension only dsh-devpanel can see, because it owns the terminal panel).
The terminal and file surfaces are driven by TerminalPanelService; the usage panel by UsageService. Both wrap harness primitives (ctx.subprocess.spawnTerminal for the PTY, the session/event firehose + terminal events for usage) and are exposed to the browser over the Typert Remote boundary — no harness changes required. The Gateway auto-discovers both services through their typertRemote bindings.
Features
- Real PTY sessions — spawns the system login shell (
$SHELL, falling back to/bin/sh) in the session's working directory, wrapped so it exports a realTERM(the harness forks PTYs withTERM=dumb, which breaksclearand prompt glyphs). Colored output fromgit,ls, and prompts renders like a native terminal. - Multi-tab terminal dialog — docked under the composer, with tab bar, shell badge,
+new tab, per-tab close, and a close button that ends every session. Closing the last tab closes the panel; reopening spawns a fresh default tab. - Native terminal behavior — copy-on-select writes the selection to the clipboard,
Cmd/Ctrl+Shift+Vpastes,clearworks, and multi-byte UTF-8 output survives arbitrary byte-chunk splits without?mojibake. - Process control — deliver
SIGINT/SIGTERM/SIGKILL/SIGTSTP/SIGHUPto the verified foreground process group, or terminate the captured process tree with a 2-second grace period. - Live output deltas — the browser polls
readfor incremental output; the host keeps a per-session scrollback (1 MB tail) so long output stays browsable without unbounded memory. - AI-output file sidebar — collects the files the session's
write/edittool calls produced (from diff views and raw args, de-duplicated in first-seen order), reads them through the remote, and renders them as image, Markdown, highlighted code, or plain text, with a fullscreen mode. - Dual-dimension usage stats — a modal dashboard (session-header bar-chart icon) folding the AI side of the
session/eventfirehose (tokens, calls, messages, per-model share, response duration, reasoning effort) and the terminal side (sessions, submitted commands, wall time) into overview cards, a daily dual-series trend, a yearly activity heatmap, paginated call + terminal detail tables, and CSV/JSON export. Historical chat sessions are replayed best-effort throughctx.sessionQuery; the index is persisted (debounced, atomic) underDSH_HOME/devpanel/usage-v1.json. - Session-scoped cwd — the terminal and relative file reads resolve against the session's project directory (falling back to the user home / host cwd).
- Bilingual UI — Simplified Chinese and English dictionaries, registered in the
toolkitlocale namespace. - Clean lifecycle — every live PTY is terminated when the plugin unloads; the client unregisters its slots, removes the injected stylesheet, and unmounts the Remote namespace.
Architecture
The plugin is a two-face bundle, mirroring the harness client preset:
┌─────────────────────────── browser (web platform) ───────────────────────────┐
│ src/client/ │
│ index.ts plugin body: mounts the Remote, registers slots │
│ remote.ts TYPERT_REMOTE contribution + ctx.remote typing │
│ ConsoleHeaderActions three header icons (terminal + sidebar + usage) │
│ ConsoleSidebar AI-output file viewer (right details column) │
│ TerminateDialog multi-tab xterm dialog (composer dock) │
│ UsagePanel dual-dimension usage dashboard (modal overlay) │
│ console-store.ts shared sidebar/dialog/usage open state │
│ console.css.ts injected <style> tag (no CSS pipeline) │
│ locales.ts zh / en dictionaries (namespace 'toolkit') │
└───────────────▲──────────────────────────────────────────────────────────────┘
│ Typert Remote (JSON wire, zod-strict codecs)
┌───────────────┴──────────────────────────── host (node) ────────────────────┐
│ src/index.ts TerminalPanelService (TypertRemoteService) │
│ src/usage.ts UsageService (TypertRemoteService, usagePanel ns) │
│ src/typert.host.ts TYPERT manifest for the typert-loader │
│ src/types.ts terminal wire vocabulary (pure data) │
│ src/usage-types.ts usage wire vocabulary (pure data) │
│ src/usage-schemas.ts usage zod codecs (pure data) │
└──────────────────────────────────────────────────────────────────────────────┘
Host half —
TerminalPanelService(src/index.ts) extendsTypertRemoteServiceand owns an owner-free session map. Each session wraps aSubprocessTerminalHandlefromctx.subprocess.spawnTerminalwith a streamingTextDecoder(so a multi-byte char straddling two output chunks stays intact), a bounded scrollback buffer, and a read cursor for delta consumption. It emits terminal lifecycle events (devpanel/terminal/spawn|command|exit|dispose) on the shared ctx. Session lifetimes follow the plugin: the constructor registers a fiber effect that terminates every live PTY on dispose.UsageService(src/usage.ts) extendsTypertRemoteServiceunder theusagePanelnamespace: it folds the livesession/eventfirehose into per-chat-session AI activity records, folds the terminal events into per-terminal-session records, persists a debounced atomic index cache underDSH_HOME/devpanel/usage-v1.json, replays historical chat sessions best-effort throughctx.sessionQuery, and serves aggregated snapshots, call/terminal detail pages, and CSV/JSON exports. The plugin'sapplywiresUsageServicefirst (so it subscribes before any terminal can spawn), thenTerminalPanelService.Client half (
src/client/) —applymounts theTYPERT_REMOTEcontribution first (the dialog needs the namespace mounted before it injects it), then contributes three slots over one shared console store:conversation.session.header.utilities(idtoolkit-actions, order10) — the terminal-dialog, sidebar, and usage-stats toggle icons;details(priority-10) — the AI-output file viewer; a lower priority shadows the harnessDetailsPanel(lowest renders, so-10wins) and drives the right column throughctx.layout;conversation.composer.dock(idtoolkit-dialog, order10) — the multi-tab terminal dialog.
The usage-stats toggle renders
UsagePanelas a fixed modal overlay; it calls theusagePanelRemote face for snapshots, detail pages, and exports.Because the plugin itself mounts the
remote.terminalPanelandremote.usagePanelnamespaces, it reads the live instances out of the service store (ctx.get) instead of declaring a static inject entry, avoiding a self-wait deadlock.Wire vocabulary —
src/types.ts(terminal) andsrc/usage-types.ts(usage) hold the JSON-safe shapes crossing the Remote boundary, shared by the host encode and the client descriptors;src/usage-schemas.tsadds the usage zod codecs.src/typert.host.tsships a strict hand-written manifest so the endpoints are known to the gateway regardless of module identity (out-of-tree bundles would otherwise 404 on the runtime reflection fallback).
Installation
dsh-devpanel is developed as a workspace package next to the harness source. Requirements:
- Node
^22.19.0 || >=24.0.0 - pnpm workspace including
../deepseek-harness(seepnpm-workspace.yaml)
# from the workspace root
pnpm install
pnpm --filter dsh-devpanel build
The plugin contributes a bundle row via cordis.patch.yml ({ id: toolkit, name: dsh-devpanel }); enable it in a harness profile that lists this bundle. The published package exposes four entry points:
| Export | Path | Purpose |
|---|---|---|
. |
lib/index.js |
host service entry (default export TerminalPanelService) |
./client |
lib/client.js |
browser CJS closure-factory bundle |
./types |
lib/types/types.js |
shared wire types |
./typert |
lib/typert.host.js |
host-face Typert manifest |
Usage
Terminal panel
Click the terminal icon (>_) in the session header to toggle the terminal dialog docked under the composer. The first open spawns a default tab in the session's working directory (the user home when the session has none). Use the tab bar to switch between sessions, + to open a new tab, and the × on each tab to close it — closing the last tab closes the whole panel, and reopening starts a fresh default tab.
Input is delivered keystroke-by-keystroke into the PTY (Enter included, no newline conversion), so interactive programs (vim, top, REPLs) behave as expected. Select text to copy it; press Cmd/Ctrl+Shift+V to paste. The shell badge next to the tab bar shows the running shell program (e.g. zsh).
AI-output sidebar
Click the panel icon in the session header to toggle the file sidebar in the right details column. It lists every file the current session's write / edit tool calls produced (in first-seen order, de-duplicated). Click a file to read it through the host:
.md/.mdxrenders as Markdown;- common code extensions get syntax-highlighted blocks;
- images (
png,jpg,gif,webp,avif,svg,tiff,heic, …) render inline as data URLs; - PDFs render in a native embedded viewer; audio (
mp3,wav,ogg,flac, …) and video (mp4,webm,mov, …) play inline; - unrecognized binary files show a file-info notice instead of decoded garbage;
- anything else renders as plain text.
Use the ⛶ button to open the current file fullscreen; the × button closes the sidebar.
Usage stats panel
Click the bar-chart icon in the session header to open the usage dashboard as a modal overlay (Esc or × closes it). The toolbar lets you pick a range (last 7 / 30 / 90 days), a lineage scope (all / main tasks only / subtasks only), and a workspace (session cwd). The panel shows:
- Overview cards — AI tokens, calls, messages, sessions, active days + streak, and the terminal dimension: terminal sessions, submitted commands, and terminal wall time.
- Daily trend — a dual-series bar chart of AI tokens and terminal commands per day.
- Activity heatmap — a GitHub-style yearly grid colored by a combined AI + terminal activity score (hover a cell for the exact value).
- Model share — horizontal bars of tokens per model.
- Call details — paginated assistant-call rows (time, model, effort, duration, input/output/cache/reasoning tokens), filterable by the same range/scope/workspace.
- Terminal details — paginated terminal sessions (start, shell, directory, duration, exit, command count; hover the count to see the commands).
- Export — download the current range as CSV or JSON.
Data is collected from the moment the plugin loads: live AI activity comes from the session/event firehose, terminal activity from the terminal panel's own events, and historical chat sessions are replayed on startup (best-effort, via ctx.sessionQuery). The index is persisted under DSH_HOME/devpanel/usage-v1.json.
Remote API
The terminalPanel namespace is available on the client as ctx.remote.terminalPanel.*. All methods take an optional trailing AbortSignal and return a RemoteResult<T> ({ ok: true, value } or { ok: false, error }).
| Method | Parameters | Result | Description |
|---|---|---|---|
spawn |
{ argv, cwd, rows, cols, name? } |
{ id, pid, shell } |
Spawn one PTY session. Empty argv resolves the system login shell with a real TERM; empty or ~ cwd lands in the user home. |
write |
id, text, submit |
{ ok: true } |
Write text to the terminal; submit appends the Enter sequence (\r). |
read |
id |
{ delta, status } |
Consume the output produced since the previous read, plus the current session status. |
signal |
id, sig |
{ delivered: true, targetPgid } |
Deliver SIGINT/SIGTERM/SIGKILL/SIGTSTP/SIGHUP to the verified foreground process group. |
terminate |
id |
{ ok: true } |
Terminate the captured process tree (2 s grace) and await quiescence; the record stays listed as exited. |
list |
— | { sessions } |
List live sessions in creation order. |
dispose |
id |
{ ok: true } |
Remove the session record, terminating it first if still running; unknown ids are idempotently ok. |
readFile |
path, cwd? |
{ path, content, kind, dataUrl?, size? } |
Read one file for the viewer. ~-prefixed and relative paths resolve against cwd (default: host cwd); image/pdf/audio/video return a base64 data URL, unknown binary files return only their byte size (kind: 'binary'). |
Session status is { kind: 'running' } or { kind: 'exited', exitCode, signal }. The full TypeScript vocabulary lives in src/types.ts and is re-exported from the package's ./types entry.
The usagePanel namespace is available on the client as ctx.remote.usagePanel.*, with the same RemoteResult<T> contract.
| Method | Parameters | Result | Description |
|---|---|---|---|
snapshot |
{ from, to, timeZone, scope, workspace? } |
UsageSnapshot |
Aggregated AI + terminal figures for a date range (totals, day rows, model share, workspaces, index health). |
calls |
{ from, to, timeZone, scope, workspace?, model?, provider?, minInputTokens?, minOutputTokens?, page, pageSize } |
UsageCallsPage |
Paginated assistant-call detail rows, newest first. |
terminals |
{ from, to, timeZone, workspace?, page, pageSize } |
UsageTerminalsPage |
Paginated terminal-session detail rows, newest first. |
exportCsv |
{ from, to, timeZone, scope, workspace? } |
{ filename, body } |
CSV export of the merged day rows. |
exportJson |
{ from, to, timeZone, scope, workspace? } |
{ filename, body } |
JSON export of the full snapshot. |
scope is 'all' | 'main' | 'subtasks'; from/to are YYYY-MM-DD in the given IANA timeZone. The usage vocabulary lives in src/usage-types.ts.
Project structure
src/
index.ts host: TerminalPanelService (TypertRemoteService)
usage.ts host: UsageService (TypertRemoteService, usagePanel ns)
typert.host.ts host-face TYPERT manifest for the typert-loader
types.ts terminal wire vocabulary (pure data)
usage-types.ts usage wire vocabulary (pure data)
usage-schemas.ts usage zod codecs (pure data)
client/
index.ts client plugin body (apply/inject)
remote.ts TYPERT_REMOTE contribution + ctx.remote typing
ConsoleHeaderActions.tsx three session-header icon toggles
ConsoleSidebar.tsx AI-output file viewer (details column)
TerminateDialog.tsx multi-tab xterm dialog (composer dock)
UsagePanel.tsx dual-dimension usage dashboard (modal overlay)
console-store.ts shared sidebar/dialog/usage snapshot store
console.css.ts injected stylesheet (style tag)
locales.ts zh / en dictionaries (namespace 'toolkit')
tests/
service.spec.ts TerminalPanelService behavior over a stub subprocess
usage.spec.ts usage collector/aggregator + service integration
apply.client.spec.ts client apply: slots, Remote mount, stylesheet
clear-repro.spec.ts UTF-8 chunk-split + clear escape-sequence regressions
terminate-reopen.spec.ts tab lifecycle: close-last-tab / reopen re-spawns
cordis.patch.yml bundle-row patch contributed to harness profiles
tsdown.config.ts two-face build (host ESM + client CJS closure bundle)
vitest.config.ts resolves @deepseek-ai/* to the harness source
Development
pnpm build # tsc -p tsconfig.json && tsdown (host + client bundles)
pnpm typecheck # tsc --noEmit
pnpm test # vitest run
pnpm test:watch # vitest (watch mode)
Notes for contributors:
- Never bundle the runtime — the client bundle keeps
react,@deepseek-ai/*(and friends) external viaCLIENT_EXTERNALSintsdown.config.ts; they resolve through the module loader's injectedrequireat runtime. - The two Remote contributions must stay in sync —
src/client/remote.ts(browser descriptors) andsrc/typert.host.ts(host manifest) both hand-write what the generator would emit; their zod schemas and wire names must match the@Remotemethods insrc/index.ts. - The client stylesheet is a string — there is no CSS pipeline in the client bundle, so styles ship as one injected
<style>tag (seeconsole.css.ts), colored via the--dsw-*token layer with neutral fallbacks. - Regression tests cover real byte streams —
clear-repro.spec.tssplits real prompt/clear byte sequences at every possible position to guarantee noU+FFFDmojibake.
License
MIT — see LICENSE.
No comments yet. Be the first to write one.