DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Deepseek Harness Tui — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins

deepseek-harness-tui

Deepseek Harness Tui

用于 deepseek-harness 的交互式终端 UI,以 dsh 配置文件包形式安装

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add deepseek-harness-tui@0.3.0
README兼容性版本
dsh TUI demo: launch, ask, watch the agent read a file and answer

兼容性与来源证明

Deepseek Harness Tui 以 deepseek-harness-tui 发布,当前版本为 0.3.0。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
any
发布来源
npm
Registry 更新时间
2026/9/20

版本

0.1.0stable
2026/8/21
0.3.0stable
2026/8/15
0.2.1stable
2026/8/15
查看其余 9 个版本收起版本
0.2.0stable
2026/8/15
0.1.8stable
2026/8/14
0.1.7stable
2026/8/14
0.1.6stable
2026/8/14
0.1.5stable
2026/8/14
0.1.4stable
2026/8/14
0.1.3stable
2026/8/14
0.1.2stable
2026/8/14
0.1.1stable
2026/8/14

相关插件

正在加载相关插件…

最新版
0.3.0
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
1.5 MB
文件数
87
Surface
any
许可证
MIT
发布源
npm
GitHub
★ 2
周下载
219
安全扫描
✓ v0.3.0 扫描通过
最近提交
2026/8/15
查看源码 ↗项目主页 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 developer-tools 分类下经过校验的插件。

Web App@deepseek-ai/dsh-web-appdsh 浏览器界面捆绑包:位于 dsh-base 之上的 Web 补丁层,加上运行时粘合插件(提供前端 dist、Web 界面提示符、bash 运行时变量和 URL 行)Sdk Minimal@deepseek-ai/dsh-sdk-minimal独立的最小 SDK 配置包:JSON-RPC、一个 DeepSeek 适配器、持久化 Shell 和 JSONL 会话Sdk App@deepseek-ai/dsh-sdk-appdsh SDK 配置包:基于 dsh-base 提供 stdio JSON-RPC 服务和进程生命周期管理Subagent Codex@deepseek-ai/dsh-subagent-codex基于官方 app-server 协议的一次性 Codex 子代理提供程序

README

deepseek-harness-tui

English | 简体中文

Interactive terminal UI for deepseek-harness, installed as a dsh profile bundle. It renders into the terminal's main screen — no alternate screen, so the conversation stays in the scrollback after you leave — and runs in the same process as the agent it drives.

dsh TUI demo: launch, ask, watch the agent read a file and answer

Highlights

  • A transcript built for agents — streaming answers, tool cards with live output, and a one-row collapse for runs of read-only calls (Thought for 8s, searched for 3 patterns, read 2 files); Ctrl+O reopens the cards.
  • Steer while it runs — the editor stays live under a running turn: Enter steers, Esc or Ctrl+C cancels, and queued prompts are handed back on cancel. A prompt the agent has not read yet says so (Steering · pending), the prompt row counts what is still queued, and /status lists it. Up on an empty prompt hands the newest queued message back for editing; sending it again puts it at the back of the queue, so pressing Up again walks the whole queue. /config turns Enter into "queue for the next turn" instead, and Ctrl+Enter takes the other branch for one prompt (see Steering and queueing).
  • Modes on one key — Shift+Tab cycles normal → auto-accept → plan; the badges above the prompt and /permission / /plan always agree, because the key writes through the same services.
  • Sessions are durable — /resume picks up any past session, /rewind forks back to an earlier prompt with the original left intact, /search greps everything the session holds.
  • Models and providers, in place — /model picks a route and reasoning effort (per session or as the saved default); /login stores a key checked against the endpoint before it lands in the credential store.
  • A terminal that adapts — light/dark/no-color themes previewed live, English/Chinese chrome (/lang), rebindable keys, @ file references honoring .gitignore via fd.
  • Scriptable — --print runs one task with no UI and answers on stdout, under the same flags, model, and preset as the interactive run.

A turn as it streams: the tool card the agent's read call renders, the thinking that follows it, the answer arriving token by token — and the editor still live underneath, waiting for a steer (Enter) or a cancel (Esc):

A streaming turn: tool card, thinking, partial answer, steer hint

Quick start

The dsh CLI must be installed first — this package is a dsh plugin, not a standalone program:

npm install -g @deepseek-ai/dsh
dsh plugin --profile tui add deepseek-harness-tui
dsh --profile tui

The plugin command installs the package into a new tui profile (dsh-base + deepseek-harness-tui) and activates it on next launch.

The welcome screen: banner, model route, workspace, and loaded plugins

Usage

dsh --profile tui                                      # start the interactive TUI
dsh --profile tui "fix the failing test"               # start and send an initial prompt
dsh --profile tui --continue                           # resume the most recent session
dsh --profile tui --resume <sessionId>                 # resume a specific session
dsh --profile tui --preset code                        # start on the "code" agent preset
dsh --profile tui -m deepseek-official/deepseek-v4-flash  # override the model
dsh --profile tui --print "run the tests"              # one task, one answer on stdout
FlagAction
-m, --model <provider/model>model selection for this run
--preset <id>agent preset a fresh session is composed from; a resumed session keeps the preset its own log records
-r, --resume <sessionId>resume a session by id
-c, --continuecontinue the most recent session in this workspace
-p, --print <task>run one task with no UI: the answer goes to stdout, the exit code is 0 only for a completed turn, and tool approvals are pinned to never because there is nobody to ask
-h, --helpshow this help
[prompt...]initial prompt, sent once the UI is up

Both stdin and stdout must be TTYs; the bundle refuses to start on a pipe. --print is the exception — it renders nothing, so it runs on a pipe, which is the only place it is useful. Every other flag means the same thing with it: --print runs against a --resumed or --continued session as readily as against a fresh one, under the model and preset the rest of the command line selects.

Keys

Every key below is what the terminal actually binds, at its default. ? at an empty prompt, /hotkeys, and /help print the same list, generated from the keybinding registry, so a deployment that moves a key sees the new one named on all three.

KeyAction
Entersend; while a turn runs it steers or queues, whichever /config says (see Steering and queueing)
Ctrl+Entersend with the opposite busy-Enter behavior for this one prompt: queue when Enter would steer, steer when Enter would queue. Needs a terminal that distinguishes it — under the Kitty keyboard protocol or xterm's modifyOtherKeys it arrives as its own key, and everywhere else it reaches the process as a plain Enter and sends normally
Shift+Enter / Alt+Enter / Ctrl+Jnewline; a \ before Enter does the same, for terminals that cannot send Shift+Enter
Up / Downprompt history from the first row of the prompt, cursor movement below it; on an empty prompt with messages still queued, Up takes the newest one back into the editor to edit
Tabaccept a completion
@reference a file
/run a command; /skill:<name> loads a skill
?shortcut help, on an empty prompt; never typed into the draft
Ctrl+Rsearch the prompt history backwards; it outlives the process in $DSH_HOME/history.jsonl, which DSH_SKIP_PROMPT_HISTORY=1 stops writing (see Prompt history)
Ctrl+Gsearch this session's messages; Ctrl+F stays the editor's forward-char
Shift+Tabcycle mode: normal → auto-accept → plan → normal. normal and auto-accept are the workspace-write and auto-accept permission presets (same sandbox, approval asked or not); plan is plan mode, which the cycle enters on workspace-write. danger-full-access is not a rung — it is reached with /permission, and a session already on it keeps it while the key moves plan mode alone
Ctrl+Nexpand or collapse the plan; Ctrl+Y stays the editor's kill-ring paste
Ctrl+Ocycle tool cards: preview, full, hidden
Ctrl+Tshow or hide thinking blocks — off, thinking streams and goes with the step that wrote it; on, every step keeps it, history included. The model reasons either way; turns the key off with it

While a surface holds the keyboard

SurfaceKeys
Panel (/help, /hotkeys, /palette, /status, /mcp, /doctor, /subagents, /jobs)Up/Down scroll · PgUp/PgDn page · g/G or Home/End top or bottom · Esc or Ctrl+C close
QuestionUp/Down move · 1-9 answer straight away · Space toggle (multi-select) · "Type something." row for a custom answer · PgUp/PgDn page long detail · Enter submit · Esc or Ctrl+C cancel
Permission promptUp/Down move · a digit answers the row it numbers · Enter confirm · Esc or Ctrl+C deny. A fifth row appears only when the grant can be remembered (for a shell it opens the command rule to edit)
History search (Ctrl+R)type to match · Ctrl+R steps to an older match · Tab or Esc accepts into the editor · Enter sends it · Ctrl+C or an emptied query restores the draft
Session search (/search, Ctrl+G)type to filter · Up/Down move · PgUp/PgDn page · Enter opens the message · Esc leaves the message, then clears the query, then closes
Model picker (/model)type to filter · Up/Down move · Left/Right or Shift+Tab adjust reasoning effort · Enter save as default · Ctrl+S use for this session only · Esc clears the filter, then closes
Resume picker (/resume)type to search · Up/Down move · PgUp/PgDn page · Tab switches between this workspace and all · Enter resume · Esc clears the search, then closes
Rewind (/rewind)Up/Down move · PgUp/PgDn page · Home/End first or last · Enter go back to that prompt · Esc close
Plugins (/plugins)type to filter · Up/Down move · PgUp/PgDn page · Enter expand one entry · Esc close
Skills (/skills)type to filter · Up/Down move · PgUp/PgDn page · Enter read one skill (Up/Down scroll · g/G or Home/End top or bottom) · Esc leaves the skill, then clears the filter, then closes
Settings (/config)Up/Down move · Enter flips a switch, steps a choice, or opens a submenu · Left/Right step a choice · Esc close
Theme picker (/theme)Up/Down preview each theme on the screen behind it · Enter keep it · Esc restore the one you opened on
Provider sign-in (, )

Ctrl+C is the one key that is never rebindable: it is how a terminal is always left. Every other binding is configurable — see keybindings below.

Commands

CommandAction
/helpkeyboard shortcuts and commands
/hotkeysthe keyboard shortcuts alone
/model [[provider/]model]switch the model and save it as your default; without an argument it opens the picker, which can also pick for this session only
/preset [<preset> | copy <preset> <new-id>]show, switch, or copy this session's agent preset
/configthis terminal's own settings — the Ctrl+T thinking pin, the tool-card phase a session opens on, what Enter does while a turn runs, theme — changed in place and saved for the next session
/theme [auto|light|dark|no-color]the palette this terminal paints with; without an argument it opens the picker
/login [provider]give a provider an API key: pick a configured route or one the adapter offers, paste the key, and it is checked against the endpoint before being stored. The key goes to the credential store; settings record only the variable name
/provider [add]list configured providers and the ones /login can configure; add walks through name, endpoint, protocol, key, and the models the endpoint reports
/copy [N]copy an answer to the system clipboard; without an argument the last one, /copy 2 the one before it
/editoredit the current prompt in $EDITOR; the terminal is released while the editor runs and repainted when it exits
/newstart a blank session in this workspace; the current one keeps its history and stays resumable
/clearclear the transcript view; the session log is unchanged
/rename [name]name this session yourself; the name is pinned and stops the automatic titler. Without an argument the title is regenerated and stays automatic
/compactcompact older conversation history into one summary; the conversation stays on screen and the model keeps the summary. Takes no arguments
/lang [en|zh]show or switch the interface language; the choice is remembered for the next session

Those are this bundle's own commands. Whatever else the profile mounts registers its own on top of them, and /help lists what the running session actually has.

/details is retired. It packed two unrelated switches into one argument syntax — [collapsed|expanded|hidden] [reasoning [on|off]] — that had to be memorized to be used, and it remembered neither past the process. The two things it did belong in different places: the tool-card phase is what Ctrl+O cycles on the spot, and the thinking display is a standing preference. Both are rows in /config now, beside the theme row that opens /theme. Its detailsDialogWidth was renamed settingsDialogWidth, and its argument completions are /theme's four values.

What /config and /theme change applies at once and is written to the tui section of the harness's own settings document ($DSH_HOME/settings.yaml), through the same optional settings service /model saves a default model with. Every /config row reads its value live, so Ctrl+O pressed while the panel is open moves the tool-card row under it. A host that never mounts that service keeps every switch working for the session and simply forgets it at exit.

/lang switches this terminal's own chrome — the command list, the panels (/help, /status, /config, /search, /skills, /subagents, /jobs, /mcp, /doctor, /plugins), the prompt and status rows, the dialogs and their buttons, and the notices those surfaces write — between English (the default) and Chinese; the conversation itself is never translated. A few command answers are still English-only whatever the language is: /model, /preset and /resume print their own report text, and the turn-outcome notices the transcript folds ("Turn cancelled.", "The model reached its output-token limit.") come from the session log rather than from the message table.

The choice is written to the Host's locale settings section when a settings provider owns one, which is the same preference the web client reads, and to $DSH_HOME/tui-locale.json (~/.dsh/tui-locale.json) otherwise.

Steering and queueing

The editor stays live under a running turn, so Enter always sends. What sending means while the agent is busy is the /config row Enter while running:

  • Steer the current turn (the default) hands the prompt to the running driver, which reads it at its next step boundary — the answer in flight is interrupted by it. The echo above the prompt is badged Steering.
  • Queue for the next turn parks it instead: the turn in flight finishes undisturbed and the prompt opens the turn after it. Its echo is badged Queued until the agent picks it up, and then it is an ordinary prompt row, because that is what it became.

Ctrl+Enter sends one prompt the other way without moving the setting. It needs a terminal that can tell it apart from Enter — the Kitty keyboard protocol or xterm's modifyOtherKeys; elsewhere Ctrl+Enter physically sends a bare \r, which nothing can distinguish, so it simply sends normally.

Either way the prompt is queued rather than answered, and everything the queue offers applies to both: the prompt row counts it (2 queued), /status lists it with the boundary that holds it, Up on an empty prompt takes it back for editing (see Prompt history), and Esc or Ctrl+C hands the text back to the editor when the turn is cancelled. The choice is stored per user in $DSH_HOME/settings.yaml; the harness's web chat keeps its own copy of the same choice and defaults it to queueing, while this terminal defaults to steering, which is what it has always done.

Prompt history

Up and Ctrl+R reach the prompts typed before this process started. Every submitted prompt — and a draft cleared with Esc or Ctrl+C, which is a prompt you may want back, queue handed back by a cancel included — is appended to $DSH_HOME/history.jsonl (~/.dsh/history.jsonl) and read at mount: newest first, this session's own before any other session's, and only the ones typed in this workspace. A prompt over 1,024 characters keeps a 200-character preview on its line and its body moves to history-cache/ beside the file; both are written 0600. Past 1 MB the file is compacted once to its newest 1,000 entries, and a body nothing points at is deleted a week later.

Up has a second job while a turn runs. On an empty prompt, with messages still queued and no completion menu open, it takes the newest queued message out of the agent's inbox and back into the editor — the echo above the prompt goes with it, and the badge counts one fewer. From there it is an ordinary draft: Enter sends it again, which puts it at the back of the queue, and Esc throws it away into this same history. Editing an older one is sending this one back and pressing Up again, which walks the whole queue. Any of the three conditions missing and Up means what it always meant, so a draft in progress is never overwritten and an empty queue navigates history as before.

It is the prompt as sent, in plain text: a key or a customer name pasted at the prompt is on disk until the file is. Overlay fields are not prompts and are never recorded — /login never puts an API key there. Export DSH_SKIP_PROMPT_HISTORY=1 (or true/yes/on) to stop the writing while keeping the reading, and delete history.jsonl and history-cache/ under $DSH_HOME to drop what is already recorded.

Context pressure

${context} on the prompt row reports how much of the model's window this session has used — 78% context, dim — until the window gets tight. From 25% left it reports the other number instead, 22% context left in yellow, and turns red at 10% or less. One reading paints the row, so the two numbers never disagree and only one of them is ever on screen.

The transcript gets one row per band as well, because a user reading a long answer is not watching the prompt row: Context low — 25% of the window left in yellow, and Context nearly full in red under 10%. Each band writes at most one row, and re-arms only after the reading comes back clear of the threshold it crossed (3 points above it), so a token count that revises itself across the edge cannot repeat a warning you already have. The row names /compact when this session's preset composes a compaction service and /new when it composes none — and while a turn is running it says to compact once the turn ends, since /compact needs an idle session.

Both thresholds are constants, not configuration: 25% is deliberately above the 20% at which @deepseek-ai/dsh-compaction-basic compacts on its own under its shipped defaults, so the yellow row is the last moment to choose /compact yourself, and red means the automatic path is absent, disabled, or failing.

Permission grants

Row 2 of the permission prompt ("don't ask again … this session") is remembered in memory and forgotten when the window closes. Row 5 ("don't ask again … in this project") is written to $DSH_HOME/approvals.json (~/.dsh/approvals.json) and still holds after a restart or a /resume:

{
  "version": 1,
  "projects": {
    "/home/you/code/app": {
      "allow": ["edit", "bash(npm run:*)", "bash(git status)", "edit [danger-full-access]"]
    }
  }
}

Rules are keyed by the workspace the session was opened in, so a grant given in one repository is never spent in another, and nothing is written into the repository itself. A rule is either a bare tool name (edit — every ask about that tool is granted), a command prefix (bash(npm run:*) — npm run build and npm run test, but never npm run-evil), or one exact command (bash(git status)). Commands are compared word by word, so the spacing a model happens to use never decides whether a rule fires. A rule never covers a line that runs more than one command: anything carrying ;, &&, |, a redirect, a backquote, a parenthesis or a newline is asked about even when its first command is allowed. A command that names a working directory outside the project is asked about too, and the prompt says which directory that is. To revoke a grant, delete its entry from the file — everything else under projects, including keys this build does not write, is left alone.

A trailing [mode] is the sandbox access the grant was given at. Hosts widen a refused call by asking again for more permission ("escalate sandbox to danger-full-access: …"), and a rule only ever answers asks of its own kind: one saved while widening to workspace-write leaves a later danger-full-access request to you, and a rule saved for an ordinary call answers no escalation at all. The answer row says the access it would stop asking about.

A shell asks about the command it is about to run, so row 5 offers a rule instead of a blanket grant: the prompt pre-fills it (npm run build becomes npm run:*, git status becomes git status:*), Enter saves it, and editing it first is the point — trim it to npm:* to cover more, or to npm run build alone to cover exactly this one. Emptying the box allows this call and stores nothing. There is deliberately no "allow every shell command in this project" row, and a command no rule could ever match — a compound line, or a bare sudo/env/bash -c wrapper — leaves the prompt with its first four answers. So does a request the terminal could not look behind, such as a background command or a call whose arguments will not parse: a permanent grant is offered on what the prompt could show, never on a tool name alone.

A tool that writes files shows the change it would make inside the prompt: the file's path and the old→new hunks, taken from the pending call's own arguments (nothing is read from disk). Long changes are clipped to keep the answers on screen at any terminal width — the preview is budgeted in the rows it will actually occupy, wrapped lines and extra files included — and the marker says how many rows are not shown, with the tool's card in the transcript carrying the whole diff once the call runs. A change past maxDiffEditLength is shown as a whole-file replacement and labelled as approximate; a call the terminal cannot present at all asks the plain question.

@ file references

@ lists the workspace through fd when the host has it (fd, or fdfind on Debian and Ubuntu), so completion honors .gitignore, .ignore, and .fdignore. Without it a built-in walker takes over and skips build output by name — .git, node_modules, dist, build, out, coverage, .cache, .next, .nuxt, .turbo, .venv, __pycache__, target — and withholds *.log and *.tsbuildinfo from a query that named no extension. Set fileSearchCommand to pin the binary's path, or to "" to always use the walker, and fileSearchExcludedDirectories to change what the walker skips.

Commands complete their arguments too: /model offers every advertised provider/model, /preset the roster's presets and the copy verb, /theme its four values, and /resume this workspace's recent sessions.

Surfaces

  • Chat — the main view: streaming messages, tool cards, the plan, the status row, and the prompt with its context line. A run of consecutive read-only calls — reads, greps, globs, ls/cat-shaped shell commands, MCP queries — reports as one row (Thought for 8s, searched for 3 patterns, read 2 files) instead of one card each; Ctrl+O opens the run back into its cards. A call that writes never joins one — cat a > b writes b, whatever its verb says — while a call that fails stays in the run and turns its bullet red, because a failure the reader cannot see is worse than a row that admits one. Each fragment of that row is a whole phrase per language rather than a verb and a noun joined at render time, so Chinese picks its own word order, measure words, and comma.
  • Thinking on that row — the run reports the thinking next to it as its first clause (Thinking for 12s, read 2 files…), counting up against the clock while the model is still thinking. It is the only thinking duration a default transcript states: the thinking block itself keeps its own rule and goes with the step that wrote it (Ctrl+T pins it, Ctrl+O expanded brings it back). Every clause carries its own tense — the files are read while the thought is still running — and the row a duration appeared on is the row that keeps it, so a thought that ends in an answer rather than in another tool call settles in place instead of vanishing off the screen. Until the run's first call names a file, a pattern or a command, the ⎿ line under the row shows the newest line of the thinking; showReasoning: false keeps that line off the row like everywhere else, and the duration — which quotes nothing — stays.
  • Workflow runs — one workflow tool call folds into a run / phase / member block: the run's name and member count, one header per phase, and one row per member with its status and elapsed time. Which levels are on screen follows the run's state rather than a toggle — a phase holding anything that is not completed keeps its member rows, and a run whose members all completed recedes to a single row Ctrl+O opens again. A member with no phase and a member with an empty phase name are two different groups, because they are two different things in the log. A run whose turn ended without a result reads as interrupted, together with the members that never settled: nothing is coming for them, and a row still claiming to be running would say the opposite.
  • Rewind — /rewind, or a double Esc at an empty prompt: go back to an earlier prompt. With a host that can fork the session the conversation moves with it and the original stays resumable; otherwise the prompt comes back to the editor alone. Files are never restored — dsh keeps no file checkpoints.

Configuration

Values on the bundle row (tui-runner), all optional.

KeyDefaultMeaning
welcome—extra dim line under the startup banner; with no key at all the wordmark sweeps in instead
sessionIdmainshared agent/session identity this terminal drives
initialSkill—skill auto-invoked as the session's first turn, as if /skill:<name> were typed; set by a launcher, not by a person
initialDraft—text the editor opens with, unsent; set by a rewind handoff
experimentalCommandsfalseregister the developer commands (/reload today)
showReasoningtruemay this transcript render reasoning text at all; false hides the blocks in every phase, keeps the model's words off a collapsed row's ⎿ hint (the duration stays), and turns both Ctrl+T and the /config Thinking display row off
markdownRendererclaudeclaude (this bundle's renderer) or pi (pi-tui's Markdown); a claude render that throws falls back to pi for the rest of the process
maxToolOutputLines6body lines kept in a collapsed tool card's head/tail preview
maxDiffEditLength1000added and removed lines explored while deriving an exact line diff
maxPromptChars10000characters one submitted prompt may carry; the middle of a longer one is dropped with a ... [N characters truncated] ... marker and a notice, 0 sends every prompt whole
maxQuestionOptions8options visible at once in a question panel
maxModelOptions8models visible at once in the model selector
maxResumeOptions

Prompt templates interpolate ${name} against the values this bundle registers — cwd, git/worktree, model, context, token_meter/cache_hit_rate, goal, queued, jobs, symbol, indicator — and a separator next to a value that is currently unavailable is dropped with it. context reports used or remaining depending on how full the window is; see Context pressure.

Bindings other than Ctrl+C are configurable: set keybindings on the bundle row ({ "app.history.search": "alt+r" }), keyed by action id and valued with one pi-tui key id or several. This bundle's ids are app.mode.cycle, app.tools.cycle, app.history.search, app.transcript.search, app.todos.toggle, app.thinking.toggle, app.message.copy, app.screen.redraw, app.cancel, and app.exit; pi-tui's own editor bindings can be moved the same way. Shift+Ctrl+D reports what each of those ids resolved to, any key two actions both claim, and any key an app.* action takes off pi-tui's editor — which is the first thing to suspect when a key "does nothing".

Two keys are deliberately not what a reader might expect, because an app.* binding is answered before the editor sees the key: search is Ctrl+G rather than Ctrl+F (pi-tui's tui.editor.cursorRight), and the plan toggle is Ctrl+N rather than Ctrl+Y (pi-tui's tui.editor.yank). Rebinding either onto the editor's key takes the editor's habit away for good.

Shift+Tab is safe to take for the mode cycle for the same reason: pi-tui binds tab and nothing else in that family, and its editor recognises no Shift+Tab of its own. The /model picker's Shift+Tab (step the reasoning effort) is a different scope and keeps working — while any overlay owns the screen the app's listener returns before its first branch, so the dialog sees the key first.

The cycle itself writes nothing of its own: normal and auto-accept are selected through ctx.permissionPresets (the auto-accept entry is added to the table by this bundle's cordis.patch.yml) and plan mode through ctx.planMode, so /permission, /plan, a resumed log, and the key all report the same state. A mode that is on says so above the prompt — ⏸ plan mode on, ⏵⏵ auto-accept on — with the key that cycles it named beside the badge. Both badges can be up at once, reached with /permission auto-accept and /plan rather than with the key; the hint then rides the last one alone, because one key repeated on two stacked rows reads as two keys to press. A deployment that composes no preset table, or no plan mode, keeps the rungs it has: the key cycles what is mounted, and says so once there is nothing left to cycle.

Development

pnpm install
pnpm typecheck
pnpm test
pnpm build
dsh plugin --profile tui add link:./path/to/dsh-tui  # live-link for development
dsh --profile tui

Set experimentalCommands: true on the tui-runner row to get /reload while editing config files.

The plugin is two Cordis plugins in one bundle:

  • dsh-tui/startup — parses the TUI's own command line and provides the tuiStartup service.
  • dsh-tui — the runner: owns the pi-tui render loop, the in-process agent session, the approval answerer, and the user-question provider.

Data flows one way. Events from the dsh bus (session/event, agent/status) are folded by a per-session read model into an immutable node list; a keyed reconciler turns that list into pi-tui components, reusing every node whose version it has already applied, so a burst of stream chunks repaints one assistant step rather than the transcript. The TUI runs in-process and calls ctx.agents / ctx.approval / ctx.commands directly — no SDK, no ACP transport, and no React.

License

MIT

showReasoning: false
Ctrl+Xcopy the last answer
Alt+Eedit the draft in $EDITOR and take back what you save; $VISUAL first, then $EDITOR, then nano/vim/vi on PATH. /editor does the same for terminals that cannot send Alt
Ctrl+Lredraw
Esccancel the turn (and hand back what was queued behind it); again on a draft clears it; again on an empty prompt opens Rewind
Ctrl+Ccancel while running (queued prompts are handed back, exactly as Esc does), clear the draft while typing, twice to exit while idle; a third press leaves a turn that will not cancel
Ctrl+Dexit on an empty prompt
Shift+Ctrl+Dsession debug panel — identity, lifecycle, screen, resolved keys
/login
/provider add
Up/Down move · Space tick a model · Enter continue · Ctrl+U clear the field · Esc cancel the whole flow
/paletteevery color and attribute role this terminal renders
/export [path | clipboard]write this session's log to a file and report the path (an existing file is replaced only after you confirm); clipboard puts the session on the system clipboard as Markdown instead (over SSH, where the copy is one OSC 52 write, a document past 100,000 characters is truncated and the confirmation says so)
/pluginssearch and inspect the Loader's plugin entries
/search [query]search this session's messages; an argument fills the panel's query box
/rewindgo back to an earlier prompt in this session
/resume [session]list this workspace's resumable sessions; an argument fills the picker's search box
/skillssearch this session's skills and read one in full
/subagentsthe subagent tree below this session: label, one-shot or continuable, running or inactive, and the child session id /resume takes. Refreshes itself while it is open; says so when the profile mounts no subagent registry
/jobsbackground jobs: kind, label, state, the producer's own detail, and how long each has been running. Follows the registry while it is open; says so when the profile mounts no job registry
/statussession diagnostics, queued messages, system prompt, registered tools
/mcpthe MCP servers this agent's tools come from, with each server's tool list; says how to mount one when the profile has none
/doctorcheck the Node version, the terminal, the model route, and the services this terminal degrades without
/exit, /quitexit after the active turn reaches idle
/skill:<name> [instructions]load a skill into the conversation
/reloadEXPERIMENTAL (dev): re-read the Loader's config files and apply the diff, idle only. Registered only when experimentalCommands is on
  • Resume — /resume [session]: pick and resume a past session, in this workspace or (Tab) in all of them. A row is its title over how long ago it was touched and how big its log is (2 hours ago · 354.1KB); the session you are in is not listed, because resuming into it is not a destination. The id is matched by the search box but printed on no row. With nothing typed and nothing to list the panel says there is no other session to resume, rather than reporting a search that missed — the empty list is the answer, not a failed query. Leaving the terminal prints the command that brings the session you just left back, so the way in is on screen at the moment you walk out.
  • Session search — /search [query], or Ctrl+G: every message this session holds, filtered as you type, with the hit shown in place and the whole message one Enter away. It is a panel rather than a jump because the transcript above the prompt belongs to the terminal's scrollback, which no program can scroll for you.
  • Plugins — /plugins: search and inspect the Loader's entries.
  • Skills — /skills: search what this session composes and read one skill's body; /skill:<name> is how you then load it into the conversation.
  • Settings — /config: the preferences this terminal decides for itself — the thinking pin, the tool-card phase a session opens on, the theme — plus the language and model rows, which are read-only and name the command that changes them.
  • Theme — /theme, or the /config row: auto (follow the terminal's report), light, dark, no-color, previewed on the screen behind the picker as you move and restored if you leave by Esc.
  • Provider sign-in — /login [provider]: give a route an API key. The list holds what settings already configure and what the adapter's own directory offers, which is what lets a machine with an empty settings file reach DeepSeek's official endpoint at all. The key is never echoed — the field draws dots — and it is checked against the endpoint before it is stored where there is an endpoint to check: a 401 or a 403 stores nothing; a key an endpoint could not answer for is stored only after an explicit yes; and a catalog route, whose endpoint lives inside the adapter where this terminal cannot see it, is stored straight away because there was never anything here to ask. A key the endpoint actually answered for is the only one the receipt calls checked; every other stored key is reported as unchecked rather than as working. The secret goes to the credential store's own file; settings record the variable name and nothing else. /provider lists the same two groups, and /provider add walks a route the adapter has never heard of through name, endpoint, protocol, credential variable, key, and the models the endpoint reports.
  • Subagents — /subagents: the delegation tree below this session, one row per child — its label, whether it is a one-shot delegation or a continuable conversation, whether its record is live or only in persistence, and the child session id /resume takes. The rows come from the subagent directory rather than from the transcript, so children this session delegated in an earlier process are in the tree too; the subagent/start and subagent/end events only tell the open panel to read the directory again. A child that was just spawned can be one refresh late: the directory lists a child from the moment it has written its own descriptor. /status states the same tree in one row.
  • Status — /status: session diagnostics, queued messages, system prompt, registered tools. The queue section lists what the agent has been handed and has not read yet, in the order it will read it, one elided line each, saying which boundary holds it — [steering] interrupts the answer being written, [next turn] waits for a turn of its own. It is the same queue the prompt row counts (2 queued), read from the agent's inbox, so a prompt another host steered into this session is listed here too.
  • Background jobs — /jobs: what this session left running in the background. A bash call with run_in_background, or a delegation that was sent off rather than waited for, returns to the model at once and keeps going; the only place its current state exists is the job registry, so that is where these rows come from. Each one names the producer kind, the label the producer gave it (the command, the delegation description), its lifecycle state with whatever detail came with it (failed · exit code: 1), and its elapsed time — running first and oldest first, then finished work, most recently finished first. A live row's clock ticks while the panel is open and stops when nothing is running. The prompt row carries the count alone (2 jobs running) so background work is visible without a second stopwatch on screen, and /status states the same two numbers in one row.
  • MCP — /mcp: which MCP server each of this session's tools came from, read back out of the mcp__<server>__<tool> names the tools are registered under, because the harness keeps no registry to ask. It is read-only by construction — the terminal holds no handle to connect, restart, or authenticate a server — and a profile with no MCP row is told how to mount one instead of shown an empty list.
  • Doctor — /doctor: what the session is running on, where /status describes the session itself — the Node version, the terminal, the model route, and the services this terminal degrades quietly without. Each check is one line: a verdict, what was observed, and the one thing to do about it.
  • Help — /help: keys and slash commands.
  • 8
    sessions visible at once in the resume selector
    resumeScanConcurrency4concurrent cold projection reads in one resume scan
    questionDialogWidth200question panel width in columns, clamped to the terminal
    questionDialogMaxHeight20question panel maximum height in rows
    modelDialogWidth76model selector width in columns
    modelDialogMaxHeight20model selector maximum height in rows
    settingsDialogWidth72/theme selector width in columns
    fileSearchMaxResults20fuzzy file candidates displayed for one @ query
    fileSearchMaxEntries10000paths retained in one @ workspace index
    fileSearchExcludedDirectoriessee abovedirectory basenames the walker skips
    fileSearchCommand—fd path or name; unset discovers it on PATH, "" disables it
    externalEditor—editor Alt+E and /editor hand the draft to; unset reads $VISUAL/$EDITOR then discovers one on PATH, "" disables it. A GUI editor needs its wait flag (code -w); the known ones get it added
    showHardwareCursorfalseshow the terminal's hardware cursor at the editor's IME marker
    updateChecktrueask the npm registry once a day whether a newer release is published and say so in one notice; nothing is ever installed, and every failure is silent
    titleDeepSeek Harnessterminal window title while the UI is mounted
    theme.colortrueapply the built-in ANSI palette
    theme.truecolordetected24-bit brand gradient on the banner; unset reads COLORTERM
    theme.leftPrompt${cwd}${git/worktree}${model}${token_meter/cache_hit_rate}${context}left-aligned template above the editor
    theme.rightPrompt${queued}${jobs}right-aligned template above the editor
    theme.inputPrompt❯ the editor's first-line prefix
    theme.inputPlaceholderpress enter to steer and esc to cancelplaceholder in an empty editor while the agent runs
    keybindings—key overrides, keyed by action id