Bundle
@tecfancy/dsh-dock-terminal
In-place terminal popover for a dsh (DeepSeek Harness) web client, launched from the conversation dock. Built as a Feature-Sliced Design static Cordis plugin.
- weekly downloads
- 139 weekly downloads
- License
- MIT
- Updated
- Updated 18 hours ago
Readme
# dsh-dock-terminal
An **in-place terminal popover** for the [dsh](https://github.com/deepseek-ai/deepseek-harness)
web client, built as a [Feature-Sliced Design](https://feature-sliced.design)
static Cordis plugin. It publishes a `terminal:open` button into the
[dsh-dock-host](../../dsh-dock-host) `dockButtons` registry and mounts the
terminal panel into `conversation.composer.dock` - the band under the
composer card. Clicking the dock button expands the popover; clicking again
(or the close button) collapses it while the shells keep running (the tab ×
closes a shell for real). The popover hosts **one terminal per tab**
(capped per conversation; the default cap is 3 tabs, configurable through
`maxPerSession`, 0 for unlimited),
with a tab bar to open, switch and close shells.
The plugin is deliberately **atomic**: it does terminals only. The shell runs
on the dsh host through node-pty; the browser renders it with xterm.js over a
WebSocket bridge.
## Architecture
```
src/
index.ts host root: /dock-terminal/ws upgrade, session/disposed, tools
shared/config/ Config schema (shell, shellArgs, maxPerSession, ...)
features/pty-bridge/ PtyManager + /dock-terminal/ws wire protocol
features/agent-terminal/ optional model terminal tools (official seam)
client/
index.tsx client root: dock button + composer.dock popover
shared/config/context.ts structural contracts (slots, locale, dockButtons)
features/terminal-popover/
TerminalPopover.tsx tabbed panel in conversation.composer.dock
TerminalView.tsx xterm view + WebSocket connection lifecycle
terminal-store.ts module-level store: visibility + terminal tabs
i18n.ts en/zh dictionaries for the popover
```
Host and client are physically isolated: host never uses JSX/React, client
never touches `window`/`document` directly (the popover renders through
slots; the terminal needs only its container element).
## Wire protocol
Client opens `wss://<host>/dock-terminal/ws?sessionId=<id>&tab=<tabId>`:
- keyboard input: any text message that is not valid JSON is written to the
pty stdin;
- control frames: `{"type":"resize","cols":n,"rows":n}` resizes the shell,
`{"type":"park"}` marks the terminal across a session switch, tab switch
or popover collapse (keeps the process alive), `{"type":"close"}` closes
it;
- server -> client: first a `{"type":"meta","shell":...,"cwd":...,"maxPerSession":n}`
frame, then raw pty output. A reconnecting socket replays a bounded
transcript ring before live data.
One pty per `${sessionId}:${tabId}` key survives page refreshes (grace
window `reconnectGraceMs`, default 30 s) and session switches (parked); the
default `maxPerSession` of 3 caps the concurrent terminals per conversation
(0 disables the cap), and a conversation is closed immediately when it is
disposed. The panel is sized by its dock band, so side panels (e.g. a right
sidebar) never cover terminal output.
## Install
```sh
npm install @tecfancy/dsh-dock-terminal
dsh plugin --profile web add @tecfancy/dsh-dock-terminal
```
> **pnpm 11 原生构建批准(首个安装必做)**:`node-pty`(宿主 shell 桥)的
> install script 需要显式批准,否则 `dsh plugin add` 报
> `[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: node-pty@1.1.0`。
> 在 profile 目录(如 `$DSH_HOME/profiles/web`)的 `pnpm-workspace.yaml` 末尾加:
>
> ```yaml
> allowBuilds:
> node-pty: true
> ```
>
> 或在该目录跑一次交互式 `pnpm approve-builds`(选中 node-pty)后再重跑
> `dsh plugin --profile web add @tecfancy/dsh-dock-terminal`。
> Windows 新机器上 profile 由官方模板初始化(模板不含 allowBuilds),必踩此步。
### Default shell and look (Windows)
On Windows the default shell chain is **PowerShell 7** (`pwsh` found on
`PATH` first, then the official/winget install, the preview channel, the
per-user MSI/portable layouts and the Store alias), then Windows PowerShell
5.1, and only when neither exists does it fall back to `cmd.exe`. On POSIX
it is `$SHELL`, then the account login shell from passwd (service managers
often start dsh without `$SHELL`), then `/bin/bash`. That ordering matters
for posh-mocha style setups: oh-my-posh and the profile customizations live
under the pwsh 7 `$PROFILE` path, so spawning `cmd.exe` (or 5.1) starts with
none of the prompt, font or color customization.
The popover terminal renders with a Nerd Font stack
(`Maple Mono NF CN` → `CaskaydiaCove NFM` → JetBrainsMono Nerd Font → system
monospace, dropping whichever family is not installed) and the
**Catppuccin Mocha** 16-color palette, matching the posh-mocha kit so
oh-my-posh prompts, PSReadLine colors and eza icons look identical to
Windows Terminal. To force another shell, configure it explicitly:
```yaml
config:
shell: C:\Program Files\Git\bin\bash.exe
```
No `shellArgs` needed for explicit shells: the plugin only adds `-NoLogo`
when the resolved shell is PowerShell, and `-l` on POSIX.
### Theme, icon and motion
The popover shell follows the dsh theme tokens (`--dsw-alias-bg-layer-1`,
`--dsw-alias-label-primary`, `--dsw-alias-border-l1`, `--dsw-shadow-lv2`), so
it matches the composer card in both light and dark themes. The xterm surface
follows the active theme scheme through the client `theme` service
(`theme/change`): **Catppuccin Mocha** in dark mode (matching the posh-mocha
kit) and **Catppuccin Latte** in light mode, so the terminal is never a dark
box floating in a light UI. The panel fills the whole composer dock band (the
app layout keeps the band inside the visible content column, so right-side
panels never cover it and terminal output gets the full width). Expand rises
from below and collapse sinks downward (grid rows + opacity + a vertical slide
over 200 ms, honoring `prefers-reduced-motion`). The dock button carries an
inline SVG terminal glyph and the label `Terminal`/`终端`; rendering the SVG
glyph requires **dsh-dock-host >= 0.2.1** (older hosts show it as a text
prefix).
Collapse keeps the ptys running: the popover × only hides the panel (each
tab sends a `park` frame), so a build or long command keeps executing and
reopening resumes the same tab with its scrollback. The tab × is the real
teardown (kills that pty).
Requires `@deepseek-ai/cordis` `^4.0.1` as a peer dependency and the
`dsh-dock-host` (>= 0.2.1) client service `dockButtons` (the popover button
is registered through it).
## Model terminal tools (optional)
The host half also registers six model-facing tools — `terminal_create`,
`terminal_send`, `terminal_read`, `terminal_list`, `terminal_signal`,
`terminal_close` — so an agent can open a persistent interactive shell, run
commands, read bounded scrollback pages and close it. They ride the official
`@deepseek-ai/dsh-terminal` seam: the profile must mount `terminal` and
`terminal-bash` rows (an `insert` patch; no npm install needed, the packages
resolve from the dsh global node_modules). Without the seam the plugin keeps
working for the UI popover and simply skips the tool set.
## Config
| Key | Default | Purpose |
| ------------------ | ----------------------------------------------------------------- | ------------------------------------------------------ |
| `shell` | `""` (auto: $SHELL on POSIX, PowerShell 7 > 5.1 > cmd on Windows) | Explicit shell binary |
| `shellArgs` | `[]` (`-l` POSIX, `-NoLogo` for PowerShell) | Shell startup args (replaces default) |
| `maxPerSession` | `3` (a 4th tab shows the cap banner) | Concurrent terminals per conversation; `0` = unlimited |
| `reconnectGraceMs` | `30000` | Grace before a dropped socket kills its pty |
## Release flow (test from the repo before publishing)
Publishing is **tag-triggered** (`.github/workflows/publish.yml` pushes `v*`
tags to npm after the CI gate). Nothing on `main` is published by a push, so
the safe order is:
1. `git push origin main` - the code, not a release;
2. install the branch into a test profile and verify:
`dsh plugin --profile web-test add -w "github:TecFancy/dsh-dock-terminal#main"`,
restart the test instance and run the end-to-end checks;
3. only once the checks pass, tag and publish:
`git tag v0.5.0 && git push origin v0.5.0` (the tag must equal `package.json`'s version);
4. after the publish lands, point the test profile back at the registry version.
This keeps a broken build from ever reaching npm: a npm version exists only
after the tag, so `latest` always refers to a verified release.
## Commands
| Task | Command |
| ---------------- | -------------------------------------------------- |
| Install | `npm install` |
| Type-check | `npm run type-check` |
| Lint | `npm run lint` / `npm run lint:no-emdash` |
| Format | `npm run format:check` (fix with `npm run format`) |
| Test | `npm run test` / `npm run test:coverage` |
| Aliases drift | `npm run aliases:check` |
| Slice boundaries | `npm run slice:check` |
| Lockfile hosts | `npm run lock:check` |
| Build | `npm run build` (host tsc + tsdown client bundle) |
| Bundle verify | `npm run bundle:check` |
| Full gate | `npm run verify` (must stay green) |
## Requirements
- Node >= 22.19.0, npm 10.9+
- `node-pty` prebuilt binaries for the host platform
## License
MIT (c) 2026 TecFancy
> This plugin is scaffolded from the [dsh-plugin-framework](../dsh-plugin-framework)
> template (feature-sliced design + host/client isolation + engineering gates).
Install
dsh plugin --profile web add @tecfancy/dsh-dock-terminal@0.5.3
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 tecfancy-dsh-dock-terminal from the hub