Back to directory

dsh-oc-tui

Curated pickMaintenance: Active

rayafriandion/dsh-oc-tui

An opencode-inspired terminal UI for DeepSeek Harness that boots as a dsh profile app plugin, with a session sidebar, streaming chat, markdown tool cards, todo lists, slash-command suggestions, a telemetry footer with a context meter and inline approval prompts.

View on GitHub
$ dsh plugin add dsh-oc-tui

Install

dsh has no central install command — add this plugin’s entry (documented in its README below) to your profile or patch config, then restart.

How installs work

9

stars

1

forks

JavaScript

Language

NOASSERTION

License

2026-08-15

Created

2026-09-20

Last push

README

dsh-oc-tui

A terminal UI for DeepSeek Harness — an opencode-inspired chat client that boots inside the dsh process as a profile app plugin.

npm latest awesome-dsh-plugin License: LGPL-3.0-or-later Node

dsh-oc-tui renders the harness's durable event stream in your terminal — streaming replies, tool cards, todo lists, thinking blocks — and routes what you type back into the agent. Model routing, tool execution, approvals, commands, durable sessions, and credentials stay owned by DSH; this package owns terminal input and presentation.

Published on npm as dsh-oc-tui and listed in the awesome-dsh-plugin marketplace.

中文文档:docs/用户手册.md

Table of contents

Features

Durable sessions Create, resume, list, and delete sessions; the transcript is rebuilt from the persisted event log, so a resumed session looks exactly like the one you left.
Live streaming Assistant text and reasoning stream token by token; thinking renders in its own collapsible box that stays collapsed while streaming.
Tool activity Tool cards with a one-line summary (read src/app.ts, run npm test), flowing spinners while running, and markdown-rendered results.
Interactive questions The model can pause and ask you — option lists, multi-select, free text, and a scrollable plan review — all inline in the terminal. See Interactive prompts.
Inline approvals approval/request prompts are answered with y / n without leaving the UI; the box shows the action and the reason, with room for origin, risk and details when a @dsh-std request carries them.
Session stats One stats strip above the composer — turns/steps, LLM and tool wall time, average TTFT, decode throughput, cache-hit rate, and billed input/output tokens — folded from durable events. See Session stats and the context meter.
Stats window Click the strip or the context meter, or type /stats, for the full session-statistics and token-usage breakdown.
Context meter Live context occupancy (ctx ▓▓░░ 32K/128K 25%), with the system/tools/messages composition in the same window.
Thinking intensity Tab cycles the current model's real reasoning levels; Ctrl+E opens a slider. The level is applied per request and persisted.
Shared settings The same host settings namespaces the Web UI uses — general, sessions, per-provider model configuration, credentials — persisted to $DSH_HOME/settings.yaml.
In-app updates Detect and switch versions of @deepseek-ai/dsh and dsh-oc-tui from inside the TUI, with Windows-safe deferred installs.
Zero-dependency terminal engine Raw mode, alternate screen, a diffing cell buffer, truecolor ANSI, CJK-aware widths, SGR + legacy X10 mouse decoding, and IME caret anchoring.

Requirements

Node.js >= 22
dsh CLI @deepseek-ai/dsh — e.g. npm install -g @deepseek-ai/dsh
pnpm on PATH; dsh plugin forwards to it
Terminal an interactive terminal (Windows Terminal / ConPTY, iTerm2, GNOME Terminal, …)
Model route a usable route in $DSH_HOME/settings.yaml + $DSH_HOME/.credentials.yaml (the same setup the Web GUI uses)
dsh --version
pnpm --version

Compatibility. Verified against dsh 0.1.2-rc.1 (and 0.1.1-rc.2). DSH renamed parts of the session API in 0.1.2 — Session.events became snapshotEvents() — and this plugin reads whichever accessor the host provides, so one build serves both lines.

Install

From npm

The package is published on npm as dsh-oc-tui. Install it into the tui profile:

dsh plugin --profile tui add -w dsh-oc-tui

Or install the launcher globally — that puts the dsh-oc-tui command on PATH, which then boots dsh --profile tui:

npm install -g dsh-oc-tui

Version channels

The npm package and the awesome-dsh-plugin marketplace entry both ship stable releases only — pre-releases are never published to either. npm install therefore gives you the latest stable version, not a release candidate.

This README describes the current source tree, which can be ahead of the published release — a feature documented here is only guaranteed to exist in a stable build once that version is on npm.

To run a pre-release, or unreleased work from this repository, install it explicitly from source:

npm pack                                   # -> dsh-oc-tui-<version>.tgz
dsh plugin --profile tui add -w ./dsh-oc-tui-<version>.tgz

One-command installers

The repository ships installers that check Node >= 22, make sure pnpm exists, install the plugin into the tui profile, and can also add the dsh-oc-tui launcher globally.

# Linux / macOS
curl -fsSL https://raw.githubusercontent.com/rayafriandion/dsh-oc-tui/main/install.sh | bash
# Windows (PowerShell)
powershell -ExecutionPolicy Bypass -Command "iwr https://raw.githubusercontent.com/rayafriandion/dsh-oc-tui/main/install.ps1 -OutFile install.ps1; & .\install.ps1"

Run ./install.sh / .\install.ps1 from a checkout instead, and add --launcher / -Launcher to also put the dsh-oc-tui command on PATH. Other flags: --local (-Local) installs the current checkout, --source <spec> (-Source <spec>) uses a custom source, --profile <name> (-Profile <name>) targets another profile.

From a checkout or tarball

npm pack                                             # -> dsh-oc-tui-<version>.tgz
dsh plugin --profile tui add -w ./dsh-oc-tui-<version>.tgz

dsh plugin anchors relative paths to the directory you invoke it from before forwarding to pnpm.

Why -w

The profile directory declares itself a pnpm workspace root (pnpm-workspace.yamlpackages: [.]), so pnpm refuses a bare add with ERR_PNPM_ADDING_TO_ROOT. -w makes the dependency land in the profile's own manifest — which is exactly what it is. dsh plugin then reconciles dsh.profile.bundles against what is installed.

What the install does

  1. dsh plugin initializes $DSH_HOME/profiles/tui on first use (@deepseek-ai/dsh-base plus an empty user patch layer).
  2. pnpm installs dsh-oc-tui into the profile's node_modules.
  3. Because the package declares dsh.bundle.patch, dsh appends dsh-oc-tui to dsh.profile.bundles.
  4. dsh --profile tui composes the base layer, this bundle's rows, and your own patch — no manual editing required.

Verify without booting:

dsh --profile tui --dump-config

The dump shows a # == dsh-oc-tui layer containing tui-startup, tui-app, the agent-presets roster row, and the tool-ask-user row.

Quick start

dsh --profile tui                        # title screen; your first message creates a session
dsh --profile tui --resume <sessionId>   # resume a persisted session
dsh --profile tui --model <modelId>      # default model for new sessions
dsh --profile tui --provider <route>     # default provider route
dsh --profile tui --no-sidebar           # start without the session rail
dsh --profile tui --help                 # the TUI's own flags

The stock launcher hardcodes only web and plugin as bare subcommands, so --profile tui is the intended shape. Want the literal dsh tui? Add a shell alias:

function tui { dsh --profile tui @args }   # PowerShell $PROFILE
doskey tui=dsh --profile tui $*            :: CMD

Convenience launcher

The package also ships a dsh-oc-tui binary that is equivalent to dsh --profile tui, but checks first that the profile actually has the plugin installed and prints the one-time install command when it does not.

dsh-oc-tui                 # boot the tui profile
dsh-oc-tui --profile mytui # boot a different profile
dsh-oc-tui --help          # launcher help
dsh-oc-tui --version       # launcher version

It prefers the dsh on PATH and falls back to npx --yes @deepseek-ai/dsh. Install it with npm install -g dsh-oc-tui.

Environment variable Effect
DSH_TUI_PROFILE Default profile when --profile is absent (default tui).
DSH_TUI_SKIP_CHECK Set to 1 to skip the profile preflight (advanced installs).

Usage

Keybindings

Key Action
Enter Send the message.
Ctrl+Enter / Shift+Enter / Alt+Enter Insert a newline.
Ctrl+C Clear a non-empty prompt, cancel the running turn, or press twice while idle to exit.
Ctrl+P Open Settings.
Ctrl+E Toggle the thinking-intensity slider below the composer.
Tab Session page: cycle the thinking intensity. Settings page: switch the left menu.
Ctrl+N New session.
Ctrl+D In Settings → Manage sessions: delete the focused session (press twice to confirm).
Ctrl+L Clear the transcript view.
Up / Down Move the caret across a multi-line prompt; on the first/last row, step through input history.
Left / Right Move the caret within the input box.
PgUp / PgDn Scroll the transcript.
Esc Close the session stats window, the thinking slider, or help; cancel an approval; cancel a running turn; clear the prompt you are typing.
Esc Esc Idle with an empty prompt: open the rewind picker.
y / n Answer an inline approval prompt.

Mouse. The wheel scrolls the transcript (or the Settings window while it is open). Hold the left button and drag across the transcript to select text, then press the right button to copy the selection.

Slash commands

Built in: /help /settings /new /resume <id> /model <id> /provider <route> /rewind /stats /clear /cancel /quit (/exit also works).

Rewind. Esc Esc (or /rewind) lists the prompts of the live session. Restoring the conversation forks a new session from the events before the chosen prompt — the parent session is left untouched on disk, exactly as the harness's own session/fork does — and the picker lands on the most recent prompt, so Enter twice rewinds the last turn. /rewind <n|last> [conversation|code|both] runs it without the picker. The fork starts with an empty inbox: a cut before a turn also cuts the inbox claim that turn performed, so anything the parent had queued — including the prompt you rewound away from — is not delivered again; it stays in the parent's log, and the result line says dropped N inherited pending input when there was any. Restoring files is best-effort and fenced: it needs a git worktree (anywhere else the rewind reports files not restored (not a git worktree) and changes nothing), it rewrites tracked files from HEAD without touching the index, and it removes an untracked file only when the transcript's first recorded write to that path is at or after the rewind point. Everything it overwrites or deletes is copied to $DSH_HOME/rewind-backups/<sessionId>/<timestamp>/ first, and the result line names that directory. Because the log stores no file contents, a tracked file returns to its last commit, not to its exact state at the rewind point.

Harness commands — /compact, /goal, /plan, … — are forwarded to ctx.commands and run without a model turn. They need a live session: on the title screen the TUI answers /<name>: start a session first instead of dropping the command silently.

/stats is the TUI's own command: it toggles the session stats window and, like the harness commands, needs a live session.

Interactive prompts

Approvals. When a tool needs permission, the composer area is replaced by an approval box. The first row carries the action (Approval · <tool>) with the key hints riding along when they fit; the explanation the request supplies follows as a Summary · row. y allows once, n rejects, Esc cancels. A request from a @dsh-std component can additionally carry Origin, Risk and Details rows — they are drawn when present and omitted when not, a detail marked private shows its label but not its value, and on a narrow terminal the middle yields with a … N more marker while the action and the hints always survive. The plugin also honours the effective permission preset, so an auto-approving preset does not prompt at all.

An approval that outlives the deadline its caller set disappears on its own. That is an expiry, not a cancellation: the caller is told expired rather than cancelled, so a timeout can never be read as a human decision.

Questions. The model can ask you directly through the ask_user_question tool. The tool is declared by this bundle's tool-ask-user row — dsh-base mounts the user-questions service but not the tool, and a TUI session composes from the base rather than from an agent preset — and it is answered by a modal:

Key Action
Up / Down Move between options (wrapping).
Space Toggle the highlighted option (multi-select) or select it (single choice).
Enter Continue: a single choice is selected and advances; on the free-text row it starts editing; in a multi-select it confirms the toggled set.
any printable key Jump into the free-text row and start typing.
PgUp / PgDn, wheel Scroll a long plan or detail pane.
Esc Defer: decline to answer here (Esc while editing returns to the options).
Ctrl+C Still cancels the running turn; the pending question is withdrawn.

Questions are staged one at a time, exactly as the Web UI composer stages them, and the answer encoding is identical: a free-text answer replaces the selection for a single-select question and accompanies it for a multi-select one.

A question carrying the plan-review intent — what exit_plan_mode sends — renders the plan markdown in a scrollable pane above Approve / Keep planning. Answering Approve exits plan mode and the model continues; anything else keeps planning.

Deferring is deliberate, not a cancel: with no other answerer the tool reports no user-questions answerer accepted the request, which cannot be mistaken for a human choice.

Thinking intensity

The effective level sits on the composer's top-right border as the bare level name, diagonally opposite the provider · model label.

  • Tab on the session page cycles the levels of the current model (wrapping strongest → weakest); Shift+Tab steps backwards.
  • Ctrl+E opens a slider below the composer: Tab or / adjust and persist, Esc or Ctrl+E close it.
  • Levels come from the provider adapter (ctx.llm.resolveModelInfo), so a boolean-thinking model shows exactly its two ends, DeepSeek's Off/High/Max shows those three, and a full-range model shows every advertised level — never a blanket none → max scale.

The choice is applied to the session's requests through the agent/request waterfall and stored in agent-default-model.reasoningEffort.

Session stats and the context meter

The row above the composer is the session stats strip, the TUI's counterpart of the web chat's stats line: the same figures in the same order, separated by :

▤ 1 turn · 2 steps│LLM 1.3s · tools 1.2s│TTFT avg 400ms · 20.0 tok/s│cache 55%│in 110 · out 30
  • turns / steps count closed steps (step/end), so failed, cancelled, and max-tokens steps count too. LLM is step/start → assembled reply; tools pairs tool/calltool/result.
  • TTFT avg is the per-step average time to first token; tok/s is decode throughput (first token → assembled reply over the reported output tokens).
  • cache is the prompt-side cache-hit share (cache reads over all billed input); in / out are the session's billed input and output tokens.
  • A narrow terminal drops trailing groups whole and marks the elision with │… rather than cutting a figure in half; the window always carries the complete set.
  • A session with no closed step and no billed tokens hides the strip entirely and gives the row back to the transcript.

The whole strip is a click target. Clicking it — or the context meter at the right end of the status row (ctx ▓▓░░ 32K/128K 25%), or typing /stats — opens the session stats window; click again, click elsewhere, or press Esc to close. The window breaks the same line into labelled rows (usage / duration / speed / tokens / cache) and adds the context-occupancy reading with its heuristic composition — system prompt, tools, and messages — matching the Web UI's ContextMeter dialog.

Figures come from the same sources as the Web UI, projection-first with the plugin's own fold as the fallback: tokenUsage, contextPressure, and contextBreakdown are mounted by dsh-base's token-meter row, while sessionStats is mounted only by the web app bundle — so the TUI folds the durable step / chunk / message / tool events by the same rules. A missing projection falls back for that figure alone, and a figure nobody can supply stays hidden instead of printing a zero.

Settings

Ctrl+P opens a settings menu over the same host settings namespaces as the Web UI, persisted through ctx.settings to $DSH_HOME/settings.yaml. A left menu splits it into three tabs (Tab or click to switch):

  • Main — General (busy-Enter behaviour, default agent preset, permission preset), Sessions (new session, manage sessions), System (provider API hints, update-manager shortcut, settings file path).
  • Model — the default provider/model/reasoning choice, then one group per provider holding its URL, API key, and model list. Pressing Enter on Models fetches the provider's advertised catalog (ctx.llm.discoverModels) and opens a checkbox window; pressing Enter on a listed model makes it the default route.
  • Update — see In-app updates.

Only providers you actually added (present in your user settings layer) are listed; a provider that was never added stays hidden. The default agent preset comes from the roster the profile mounts (the shipped presets plus any you authored under $DSH_HOME/.agent-presets) — note that a TUI session composes process-wide from the base, so the stored default applies where a session is created from a preset. Web-UI-only options (ui-theme appearance, locale) are not shown because they have no effect in the TUI.

In-app updates

Ctrl+P → Update shows the installed versions of @deepseek-ai/dsh and dsh-oc-tui, the latest npm dist-tag, and a status line that only ever targets stable releases:

  • Update available → x.y.z — a newer stable release exists.
  • Up to date — nothing to do.
  • No stable release — pick from Versions — the registry has no stable release yet; pick one manually.
  • Install damaged — reinstall below — the global dsh tree is in a mixed old/new state; reinstall it.

Enter on a package's Versions row opens the full registry list (newest first, [latest]/[next]/other tags and (installed) colour-coded) where you can pick any version — including pre-releases — for a y/n-confirmed install through npm/dsh plugin. Check now re-reads the registry; Startup check toggles the silent boot-time stable-release check. Installs run in the background, never block the UI, and need a restart to apply.

Windows: why dsh installs are deferred to exit

On Windows, updating dsh while any dsh process runs can silently corrupt the global install: npm replaces the directory while the running process holds memory-mapped native DLLs, still exits 0, and the resulting old/new hybrid tree fails to boot. The updater guards this in three layers:

  1. dsh installs are deferred to TUI exit — a detached helper waits for the TUI to close, runs the install, and records the outcome in $DSH_HOME/tui-dsh-install.json, which the Update page verifies on the next visit.
  2. The on-disk version is compared against the requested target after every direct install, so a silent corruption surfaces as an install corrupt toast with repair instructions.
  3. An already-damaged install is flagged in the Status row rather than reported as a bogus success.

macOS/Linux have no DLL lock, but an install is refused while other dsh processes are running.

How it works

  • The plugin is a Cordis function plugin loaded by the tui profile. lib/startup.js parses the app's flags and provides the tuiStartup service; lib/index.js owns the UI loop.
  • lib/term.js is a zero-dependency terminal engine: raw mode, alternate screen, a diffing cell buffer, and a key decoder (truecolor ANSI, CJK-aware widths). It parks the hidden terminal cursor at the input caret so the OS IME anchors its composition window inside the composer, and it understands both SGR and legacy X10 mouse encodings so wheel and click bytes can never leak into the input text. The cell buffer is also the single chokepoint where every painted string is sanitized: C0, DEL and C1 characters are replaced with a visible \uFFFD, so a tool name, a path, a title or a @dsh-std request's action can never smuggle an escape sequence into the output stream. The width-preserving placeholder is deliberate — dropping the character instead would silently change layout, whereas a visible one makes the injection obvious.
  • lib/ui.js is the responsive view model and renderer (DeepSeek blue-white theme, session rail, transcript, multiline composer, command suggestions, telemetry footer). Transcript lines are cached per block, only the visible window is materialised each frame, streaming paints are coalesced, and the live block re-renders on a short throttle — so render cost stays bounded as history grows. Thinking collapses to keep the transcript readable, and running tools and thinking blocks animate with flowing spinners.
  • lib/metrics.js folds durable step/chunk/message events into token, TTFT, throughput, and cache-hit metrics.
  • lib/interrupt.js owns the clear/cancel/double-exit state machine used by stdin and SIGINT.
  • lib/markdown.js renders model output (headings, lists, quotes, code, inline spans) to styled lines.
  • lib/updates.js isolates every npm/pnpm interaction for the Update tab — registry queries, dependency-free semver comparison, dsh install detection, and installs — all through child_process.spawn, never spawnSync.
  • Agents are created and resumed through ctx.agents, the transcript is rebuilt from the session's durable log and fed live by session/event (including assistant/chunk), model defaults come from ctx.agentDefaultModel, and approvals answer the approval/request waterfall inline.
  • ask_user_question is answered over the user-questions/request waterfall: a scoped Cordis waterfall where the modal either returns an answer or delegates with next(). An aborted request rejects so the service reports its own ASK_ABORTED; requests addressed to another agent are delegated untouched.

@dsh-std interop

View on GitHub

DSH Plugins is an independent community directory of DeepSeek Harness plugins. Not affiliated with or endorsed by DeepSeek. Third-party plugins are not security-audited — review the source before installing.

New DeepSeek Harness plugins, weekly. No spam.