Interaction and Commands

September 20, 2026 · View on GitHub

Documentation index · 简体中文

Input and global shortcuts

KeyBehavior
EnterSend while idle; steer text into the running turn at its next step boundary; confirm an open menu
TabComplete a / command or @ file; while the model is working, queue non-empty input as a post-turn follow-up
Ctrl+EnterInterrupt the running turn and process the input immediately
Shift+Enter / Ctrl+JInsert a newline at the caret; Ctrl+J (LF) is the fallback when the terminal cannot report the Shift modifier; macOS Terminal.app uses Option+Enter
Shift+TabCycle the configured session modes (default: default → plan → full-access)
Alt/Option+UpPull the latest undelivered message back into the editor
Up/DownSelect menu items; in ordinary input, browse history or move through multiline text
Ctrl+V / Alt+VInsert clipboard text or files; images are sent as durable attachments. Use Alt+V when the terminal intercepts Ctrl+V
Ctrl+GEdit the current input in an external editor ($VISUAL$EDITOR); saving and quitting fills it back, :cq/non-zero exit keeps the draft; with neither variable set the TUI asks you to configure one (no vi fallback)
Ctrl+Shift+EExpand the fullscreen draft editor (or click the affordance at the end of the input row): line numbers + current-line highlight + live line/char stats, Enter inserts a newline, Ctrl+Enter or the Send button sends, Esc or the Collapse button keeps the draft and returns; wheel-scrolls freely, click/drag/double-click selection work as in the inline prompt; remappable via /settings
EscLadder: close help → close the image preview → close the command menu → close the file menu (only the current @ token) → with a selection in the prompt input: only clear it (text untouched) → interrupt the turn and redeliver pending messages → clear non-empty input → double-tap on empty input = rewind; in fullscreen, an active mouse selection is cleared first (not copied)
Esc / Ctrl+C / Enter while an image preview is openClose the preview and restore the surface underneath; other keys are not passed through
Left / Right in the image modalPrevious / next image, no wrapping; caret peeks keep arrows with the prompt
Ctrl+CInterrupt while working; press again while the interrupt is still settling to force-exit; clear non-empty idle input; while idle with a selection in the prompt input, copy it to the clipboard (selection kept for editing); press twice on empty input to exit
Ctrl+DSame ladder as Ctrl+C: interrupt while working (press again to force-exit if the interrupt stalls); press twice while idle to exit
Ctrl+OToggle transcript/verbose detail, including full reasoning and tool arguments/output; also the escape hatch for the long-line fold (a single line over 1000 chars is clipped to 1000 with a … N chars folded marker — see the user guide §5). Clicking the folded row (or the tool card face) toggles it too
Ctrl+PToggle the loaded-context panel shown at startup (while it is on screen)
Ctrl+TOpen the trajectory scene (same as /trace); q/Esc returns to the conversation
Ctrl+ROpen input-history search; repeat or press Down for the next result
Ctrl+LClear and force a physical terminal redraw
?Open shortcut and command help when the input is empty
In Help: ↑/↓, PgUp/PgDn, Home/EndScroll by line, page, or jump to either end; Esc closes
Transcript: PgUp / PgDnPage the fullscreen transcript (one viewport minus one row per press); yielded to Help and open overlays, which page their own lists; inline mode does not claim them — history lives in the terminal's native scrollback there, and paging belongs to the terminal
Shift+UpEnter message selection; arrows move, Enter expands one row, Esc exits

The action shortcuts (paste, history search, external editor, Ctrl+O/T/P/R/L, subagent dashboard, show-all, todo fold) are remappable in /settingsdsh-tuiShortcuts: enter combos such as alt+v, comma-separate several, leave blank to restore defaults — saves apply live. Combos clashing with the fixed editing keys or another action are rejected. Deployments can also pin them via shortcuts.<action> in cordis.yml.

/ has two meanings. In normal input it opens slash-command completion. In the Ctrl+O transcript view it opens full-session search; use n and N to move forward and backward through matches.

Plugins may register additional combos through the tuiShortcuts seam (they must carry Ctrl or Alt); built-in bindings always win and conflicting combos are refused at registration. A managed plugin dialog (select/confirm/input) owns the keyboard while open: / to move, Enter to confirm, Esc to cancel. Plugins may also contribute one text line above the prompt, or a compact rich status view of up to three rows when the host exposes tuiStatus.registerView. Rich views receive only host Box, Text, Image, and terminal size; click/hover/drag work in fullscreen, and the view never owns the keyboard. Image takes decoded RGBA pixels plus a same-size cell fallback: after a successful Kitty probe, the host centers the image at its natural aspect ratio using terminal-reported cell geometry (or a conservative default), adding transparent letterboxing and downsampling when needed. Otherwise it renders the fallback (including inline, accessibility, and multiplexer sessions). Plugins provide the keyboard path for the same action through a slash command or tuiShortcuts. A refused rich registration returns undefined; an admitted registration returns a disposer that removes both the view and its Cordis effect.

Editing keys

KeyBehavior
Left/RightMove by character; with a selection, collapse to the corresponding edge
Ctrl+Left/RightMove by word
Home/EndMove to the start/end of the current logical line
Ctrl+A / Ctrl+ECtrl+A opens the subagent dashboard (Mod+A in the editor still moves to line start); Ctrl+E moves to line end and also expands or folds hidden older rows in long transcripts
Ctrl+UDelete before the caret
Ctrl+KDelete after the caret
Ctrl+WDelete the preceding word
Backspace / DeleteDelete the character before / after the caret; with a selection, delete the whole selection
TypingReplaces an active selection (standard editor semantics), caret after the inserted text

vim editing mode (/vim)

/vim toggles vim editing for the prompt (session-scoped, not persisted): the prompt shows an INSERT badge and typing works as usual; Esc switches to the NORMAL badge, where bare keys follow vim semantics.

NORMAL keyBehavior
h / lMove left / right one character
j / kMove down / up one line (multi-line input)
0 / ^ / $Line start / first non-blank / line end
w / bNext / previous word start (whitespace-split)
x / XDelete the character at / before the caret (x deletes the last char at line end)
d + second keydd delete whole line (newline included) · d$ delete to line end · d0/d^ delete to line start · dw delete to word end
uUndo the last vim edit (stack capped at 100)
i / I / a / AINSERT at caret / first non-blank of the line / after caret / line end
o / ONew line below / above, then INSERT
/Inserts / and returns to INSERT (opens the command menu)
EscCancel a pending d; otherwise no-op (the usual clear / double-Esc rewind semantics stay off)
Enter / Tab / arrows / Ctrl+* combosPass through unchanged (submit, completion, history, edit shortcuts…)
Other bare keysIgnored, never inserted

While vim mode is on, Esc belongs to vim — use /rewind or exit vim mode then double-Esc for time rewind; during a running turn, Esc in INSERT just returns to NORMAL (interrupt with Ctrl+C / Ctrl+Enter). Clear the draft in NORMAL with Ctrl+C or dd.

Bracketed paste from right-click or the terminal's native paste command keeps ordinary text and newlines and is never mistaken for an Enter key. To keep rendering, click mapping, and selection geometry consistent, terminal ANSI controls are stripped and tabs are expanded to spaces on entry.

Fullscreen draft editor (Ctrl+Shift+E / )

Long drafts stop squeezing into the 5-row window: Ctrl+Shift+E (remappable) or the affordance at the end of the input row expands the current draft into a whole-screen editor sharing the exact editing state (caret/selection/fold/vim mode) with the inline prompt. Turn it off in /settingsdsh-tui (expandEditor, on by default) — both entry points (the affordance and the shortcut) then disappear.

  • Chrome: a round border following the session accent / plan color; a line-number gutter on the left (width scales with the row count, the caret row's number is highlighted); the caret row carries a soft background; the title row shows live line/char stats and the status row shows Ln L, Col C plus the vim badge (when /vim is on).
  • Keys: Enter inserts a newline (never mis-sends), Ctrl+Enter or the ⏎ Send button sends and collapses; Esc is layered (clear the selection first, then collapse keeping the draft); Tab inserts a 4-space indent; every other editing key (arrows, word jumps, Home/End, Ctrl+U/K/W, vim NORMAL keys) matches the inline prompt. Fold blocks are mutually exclusive with it: a big paste still folds into a chip while collapsed, but expanding the editor unfolds it (same semantics as clicking the chip), pastes inside the editor are plain, directly editable text, and collapsing never re-folds them.
  • Mouse: click positions the caret, drag builds a selection, double-click selects a word, Ctrl+C copies the selection — geometry is corrected for the gutter width; the wheel scrolls the viewport freely (browsing does not snap back to the caret; any caret move re-engages following); the Send/Collapse buttons are clickable with hover feedback.
  • Complements Ctrl+G: no $VISUAL/$EDITOR needed, never leaves the terminal.

@ file references

Typing @ at any position of the message opens file completion: keep typing to filter, Tab/Enter to pick, and directories can be entered further (plain fragments match fuzzily@ment matches src/utils/mentions.ts, with prefix/boundary and short-path boosts; path-shaped queries — @src/, @./, @../, @~/, @D:\, or anything containing a separator — read only that directory for local completion. Esc closes only the current @ token's menu; async refreshes keep your selected candidate). Text files and directory listings are attached as text; PNG, JPEG, WebP, and GIF files are sent as durable Harness image blocks. Reads use the active workspace filesystem, including provider-owned workspaces.

On Ctrl+V, files copied from a file manager (Finder, Windows Explorer, GNOME Files, KDE Dolphin, …) insert as paths, while copied image files are staged into the attachment store exactly like clipboard bitmaps and appear as [Image #N] (falling back to an @ reference when staging fails); submitting the prompt sends a real image block. The prompt never contains base64. When a terminal forwards a drop as pasted text (Ghostty sends a shell-escaped path through the PTY), the paste stages only when it is exactly one existing local image path — anything ambiguous stays verbatim text. A staged [Image #N] is one unit in the composer: the caret never rests inside it, ←/→ step over it, Backspace at its end, Delete at its start and Ctrl+W remove it whole, and a selection edge inside it grows to cover the token. It renders in the theme accent and inverts whole while the caret sits at its start; clicking it places the caret at its start and opens the preview. The preview opens by itself while the token is selected (the caret at its start) and closes when the caret leaves; the cell just after the token does not count. The keyboard stays with the prompt meanwhile, so ←/→ walk from image to image with the card following. Esc or a click outside the card dismisses only that token's preview until the caret leaves and returns; a click on the token always shows it. Look-alike text typed by hand or restored from history without an attachment capability stays ordinary text. In fullscreen, clicking a staged [Image #N] token or a transcript thumbnail opens one shared preview centered over the transcript area; the card targets about 95% of its width and height while preserving the image aspect ratio. The prompt, status rows and sticky header stay visible (Esc or a click outside the card closes it; narrow terminals get a metadata-only card). While the preview is open, the conversation outside the card fades: explicit foreground and background colours (pixel art, tool cards, syntax highlighting) blend halfway toward the terminal background (from OSC 11; black or white by theme lightness when unknown), and uncoloured text takes the terminal's faint attribute. The card, the prompt and the status rows are not touched, and closing restores everything. The card's title sits centered in its top border as Image #N — format · width×height · size · file name; the card is at least as wide as the title, so a small image never squeezes the file name, and a title wider than the transcript area shortens the file name in its middle first. Images staged from a file or the clipboard in this session show their source path on the card's bottom row (Open original: …, head and tail kept, middle elided); historical images use their file name. This link opens the unchanged attachment bytes in the system image viewer, independent of deleted or modified source paths. Only an explicit click exports a file to a private dsh-tui-original-* system temporary directory; it survives TUI exit for the external viewer and can later be removed with system temporary-file cleanup. A stale [Image #N] placeholder (evicted past 128 staged images or cleared by a session switch) warns on click, on submit, and when it appears as a slash-command argument. After submission, user images are re-projected from durable session events into the transcript. Assistant messages and tool results use the same preview path whenever their content contains image blocks. Fullscreen sessions with a successful Kitty graphics or Sixel probe show bounded, aspect-preserving thumbnails; inline, accessibility, multiplexer, and read-failure paths reserve the same layout with a text fallback. Visible attachments are read and decoded only when graphics are available; other paths use metadata without loading the decoder. Resumed sessions do not depend on the original local path.

Sixel thumbnails are cropped to the visible transcript while scrolling, without squeezing the whole image into the remaining rows or painting over the prompt. Opening a full preview withdraws background thumbnails; closing it restores them from cache. Unrelated text updates do not retransmit unchanged images. Decode and transport concurrency, queues and byte budgets are bounded, with text fallback on overflow or failure.

Large previews allow a 2048-pixel edge with at most 2,097,152 pixels (8 MiB RGBA), so square previews are smaller than 2048 by 2048. Thumbnails still decode at 384; ordinary plugin images remain at 1024 / 4 MiB, frames at 16 MiB, and Sixel output at 4 MiB. Fit returns to the whole image. 100% displays original pixels; +/- selects 100%, 200%, 400%, or 800%, shown in the title. Drag, wheel, or arrow buttons pan the image. Keyboard ownership and Escape remain unchanged. Actual pixels requires measured terminal cell dimensions. Cropping precedes nearest-neighbor zoom; budget limits reduce the viewport instead of scaling original pixels. Pan bursts coalesce and stale results are discarded. Each inspection retains one encoded original (up to 64 MiB), with a 64-megapixel input limit and no full-size JS RGBA cache. Some formats still require scanning the original file. Sixel retains its 256-color quantization: 100% describes spatial pixels, not lossless color. Open original preserves colors and animation in the system viewer.

Modal previews use Left/Right or the bottom ‹/› controls for previous/next image, with a current/total counter and no end-to-start wrapping. Transcript galleries follow conversation message order and never prefetch unvisited images. Draft galleries follow capability-backed token order; clicking gallery navigation promotes the caret peek to a modal without editing the draft. A plain caret peek still leaves arrows with the input. Each image starts in Fit, stale image jobs cancel, and session changes or blocking dialogs close the modal.

Interface language

/lang toggles the UI between Simplified Chinese and English (affects all UI strings); the choice persists across restarts (0.3.7+). The dsh-tui → Language select in /settings switches it too (applies immediately and saves to dsh-tui.lang in ~/.dsh/settings.yaml; the DSH_TUI_LANG env var always wins).

Message delivery semantics

While the model is working, three paths have different placement:

ActionPlacement
EnterSteer: deliver to the running turn at its next step boundary
TabFollow-up: wait until the current turn finishes
Ctrl+EnterInterrupt: stop the turn and deliver immediately

Undelivered messages appear above the editor. Alt/Option+Up retrieves the latest one. Pressing Esc while pending messages exist interrupts and redelivers them immediately.

Session workflows

Session management (/resume, /home, /agentview, /bg, prompt )

The four commands and the entry at the head of the prompt row all open the same screen: the workspace rail (the durable registry) on the left, the sessions of the selected workspace on the right, every row carrying that session's live state. They used to be three implementations that grew apart (a session browser, an agent view, a workspace home), each listing the same session with its own selection model and its own idea of what "open" meant — so there is one screen and one selection model now.

The screen describes the runtime as it actually is: one TUI process hosts several sessions at once. Switching away PARKS a session, it does not end it — a turn in flight keeps running and is still there when you come back. Leaving a session therefore needs no confirmation, and there is no "stop" meaning to it.

Opening this screen does not lose an unsent draft: the composer unmounts with the screen, and Chat keeps the draft — text, caret position, the staged image references, and the edit state around them (fold block, fullscreen draft editor, vim mode) — until Esc brings it back. A draft belongs to its session, so it never follows you into another conversation; the message /rewind hands back belongs to the session the rewind created, so it does stay in the input.

KeyAction
Choose the column that owns the keyboard (rail / sessions); exactly one is on screen
/ PgUp PgDn / wheelMove in the rail; move in the session list (row 0 is the new-session card)
TypeFilter the selected workspace's sessions live (title, directory, branch, model)
EnterSessions: enter the row under the cursor (row 0 = start a session in this workspace). Rail: open that workspace's action menu
Ctrl+EnterStart a session in the workspace under the cursor (either column)
Ctrl+NStart a session in the workspace under the cursor
Ctrl+XStop the background session under the cursor (the attached one cannot be stopped)
Ctrl+LRe-read the workspace registry and the session listing
Shift+TabRail: open the action menu of the workspace under the cursor
EscDismiss a notice → clear the filter → leave the screen

The rail's menu has four entries: edit (select that workspace, listing its sessions on the right), new session, rename, and remove from list (the registration only — the directory and every session log survive). A workspace joins by starting dsh-tui in that directory (the startup attach); there is no "add workspace" picker on this screen.

A session row's live state comes from the channel's own agent-view projection — the same source as the status bar's "needs input" hint: working / needs input / idle / completed / failed / stopped, and the session this terminal is attached to is marked current. Clicking a row enters that session (same as Enter); clicking the in-row / toggles the pin only (persisted in ~/.dsh-tui/session-pins.json) and never enters the session.

A session held by another TUI terminal is still listed, but the row turns red, ends with held by pid <pid>, and cannot be clicked: two processes driving one append-only log would interleave its events and corrupt the transcript. Occupancy is re-read from the ledger on the screen's own 2-second tick, so the row becomes enterable again on its own once that process exits. A session this process parked is not "occupied". See session-mount-runtime.en.md.

When the workspace service is absent (a bare composition) or a session's directory is not registered, the rail carries an "Unregistered" group that lists and resumes those sessions as usual — "no registration" is not "no history". The group lives only inside this screen and is never written back to the registry.

Behaviour changes versus the old screens (deliberately removed, and no longer covered by regressions): the session-level right-click menu (rename/delete one session), the Ctrl+P pin shortcut, Ctrl+S to reveal delegated runs, Ctrl+A this-project/all-projects, Ctrl+B branch filter, Tab preview, Ctrl+R rename, Ctrl+D delete, and Ctrl+X clearing empty sessions. Pinning is still reachable by clicking the row's . Dispatching a background session and replying to one, the Space peek panel, and Shift+Enter dispatch-and-attach have not returned to the new screen: after /bg, reach a background session by pressing Enter on it in the workspace list.

On Windows, dsh-tui.cmd --resume uses the session ID last written to ~/.dsh-tui/resume.txt.

Rewind

Double-tap Esc on an empty editor to open the user-message list. After a selection is confirmed, the TUI:

  1. Finds the beginning of the turn containing that message.
  2. Creates a branch session through DSH session fork.
  3. Replays history before the boundary.
  4. Restores the original message to the editor for revision and resubmission.
  • The boundary is taken before the turn that contained the message; you cannot rewind past the first message.
  • If the model is working, the TUI cancels the turn first and waits for it to settle (up to 30s).
  • The rewound branch is not a sub-agent (it records parentSession without origin) and keeps using the current model route plus the session's own preset.

Plugins can intervene (tui/rewind-prompt decision event): veto the rewind (with a reason), or offer extra rewind modes in the confirm pane — e.g. "rewind the conversation AND restore the files changed since". The first option is always "Conversation only"; when a plugin mode is picked, the plugin receives tui/rewind-done (with the chosen mode id and both session ids) once the rewind completes, and may reply with a summary toast.

Side question /btw

/btw <question> asks a quick side question without disturbing the main task: it reuses the current session context (system prompt + existing history) for a single tool-less, one-turn model call, and shows the answer in a scrollable panel. Notes:

  • Never enters conversation history: the exchange is not written to the session log and never reaches the main context or token counts (closing the panel discards it).
  • Never interrupts the running turn: it can be triggered while the model is streaming; the main task keeps going.
  • Inside the panel: / scroll, Space/Enter/Esc dismiss, c copies the answer; Esc cancels while the answer is still pending.
  • Triggering /btw again aborts the previous side question.

Trajectory scene (/trace / Ctrl+T)

A full-screen scene (no scrollback pollution) over the whole session timeline:

KeyAction
/ (or h)Switch timeline / hotspot view
/ PgUp PgDnMove, page
[ / ]Jump to previous / next failed point
{ / }Jump to previous / next turn
/Query line: tool: kind: turn: err: run: >10s tok>1k prefixes, ANDed together; hits highlight in place
mCycle projection modes (equal / wall-clock / collapsed idle)
g / GJump to top / bottom
EnterExpand details; j/k page inside the details
t (hotspot view)Cycle sorting (time / count / tokens)
q / EscExit; Esc is layered: fold details → clear query → close

/settings editor

/settings opens the plugin settings editor, read/edit by namespace. Edits auto-save: / to move, Enter to expand/toggle/edit, booleans/selects write on the spot, text drafts confirm on Enter, Esc just exits. Fields under the dsh-tui namespace are written to the user layer of settings.yaml and take effect immediately (lang, statusBar.*, …); namespaces without a declared TUI section are listed read-only and need manual edits to ~/.dsh/settings.yaml.

Model and preset

/model switches through a session fork at the end of current history because DSH has no in-place model-switch API. The old session remains in /resume.

/preset switches in place only for a blank session. In a started session, the choice becomes the default for the next /new or launch. See Configuration.

Workspaces

/workspace resume opens the workspace picker. /workspace rename <name> renames the current workspace, while /workspace open <target> opens a workspace and starts a fresh session. /resume and /rename continue to switch sessions within the current workspace and rename the current session. A local target may be an absolute path, a path relative to the current local workspace, or a standard file:// URL. Other URI schemes and /workspace subcommands are registered by optional plugins; the TUI has no built-in knowledge of any external protocol. When a plugin owns the current workspace, it also resolves relative paths in its own path space.

After /workspace , the completion menu includes both built-in and plugin-contributed subcommands. Type a prefix and press Tab, for example /workspace rem; plugin aliases participate in matching as well.

The launcher accepts the same target, for example dsh-tui ., dsh-tui ../project, or dsh-tui file:///path/to/project. Without any workspace plugin installed, local paths, !command, and all normal TUI session flows remain available.

Fullscreen and mouse

fullscreen: false restores inline mode (fullscreen is the factory default since 0.9.0), where the terminal emulator owns native scrollback and selection.

fullscreen: true uses the alternate screen and enables in-app mouse handling:

ActionBehavior
WheelRouted by position: moves the selected row in the completion/command menu under the pointer; scrolls the topmost scroll container (transcript / help / subagent panel); elsewhere scrolls the message list; never scrolls the transcript behind an open overlay; moves the cursor in the trajectory scene (±3 rows per notch on the timeline, ±1 in hotspot, scrolls the detail while expanded); walks the focused row in /settings
DragSelect text, copy on release, then clear the selection; with dsh-tui.scrollGutter: scrollbar the right-edge scrollbar is a drag target — an unmodified left drag scrubs the transcript to the track position (the same mapping as a track click: drag to point), while Shift/Alt/Ctrl+drag still selects text (the drag protocol opens only for unmodified left presses)
Double/triple clickSelect and copy a word/line (exception: the scrollGutter: scrollbar track is a drag target, so multi-clicks there no longer select a line)
EscCancel an active drag (or an existing selection) without copying
Single-click a message rowPlain text rows (user/assistant) do nothing — the transcript is a reading surface, selection is the mouse's job there
Single-click a tool card / thinking / compact summaryExpand / collapse (header brightens on hover; trailing blank cells do not trigger)
Single-click a subagent cardOpen that subagent's detail scene (status glyph brightens on hover)
Single-click the input boxPlace the text caret at the click (multi-line, wrapped rows and CJK all width-aligned)
Click a [Image #N] token in the input box / a transcript thumbnailOpen the centered image preview (image metadata when Kitty/Sixel graphics is unavailable); clicking outside the preview closes it
Drag inside the prompt inputBuild an in-input selection (rendered highlight, caret rides the drag end): Backspace/Delete delete it, typing replaces it, ←/→ collapse it to the corresponding edge, Esc only clears it; drags map only visible rows (no edge auto-scroll yet); a folded paste block keeps the selection on the clicked side (never across the chip row)
Shift+click in the prompt inputExtend the selection from its start edge (or the caret) to the clicked position
Double-click a word in the prompt inputSelect the whole word (detected in the component, 500 ms / 1 cell; paths and punctuation runs select as one)
Ctrl+C with a prompt selectionCopy the selection to the clipboard (OSC 52 + native fallback) and keep it for editing
Fullscreen editor (expanded via Ctrl+Shift+E)Click/drag/double-click follow the prompt-input paths (geometry corrected for the gutter); the wheel scrolls the editor viewport (position-routed; caret moves re-engage following); the Send/Collapse buttons execute on click and brighten on hover; the input row's expands
Single-click “load earlier messages” / “ctrl+e show previous N”Load earlier messages / expand all
Single-click the sticky header / “↓ N new messages”Jump back to the pinned message / scroll to bottom
Single-click a hyperlinkOpen it in the browser
Single-click a picker / menu rowSelect and apply immediately (model / skills / activity frames / preset / permissions / plan / language / theme / effort / command & file completion / history / session rows / thinking mode / workspace targets, submenu & flow choices) — the keyboard Enter path; rows are inert while a picker is busy or mid-input
Single-click a rewind candidate / confirm rowList page: click selects only (stepping into the confirm state stays an explicit keyboard Enter); confirm page: clicking the message / mode row executes the rewind directly — the confirm pane is itself the confirmation layer
Single-click an approval / questionnaire / plan-review / plugin dialog rowSubmit that decision directly (unblock a waiting agent with the mouse)
Single-click in the trajectory sceneTimeline/hotspot rows jump the cursor (a hotspot row jumps back to the timeline at that group — same as Enter; hover shows a dim ▸ pointer); tabs switch views; the sort/projection label cycles; the query line and the tab gap open the / search; a wave-band column (ruler included) jumps to its nearest event
Single-click a /settings field / group rowFocus it and run that row's Enter action (boolean/select cycles, text enters edit, groups open); hover moves the focus (lazygit-style); the edit mode ignores the mouse entirely
Single-click a session-browser confirm rowConfirm the delete/clean (same as Enter); cancelling stays on keyboard Esc
Single-click a help-menu command rowFill /name into the prompt and close the help (the Tab completion's mouse equivalent)
Keyboard selection extensionWith a selection, Shift+←/→/↑/↓/Home/End extends / shrinks it (wraps across lines)

These mouse behaviors apply only under fullscreen: true (alternate screen); inline mode enables no mouse reporting, so scrollbar dragging does not apply there — the terminal's native scrollback and selection stay in charge.

Copy prefers OSC 52. Local fallbacks include wl-copy, xclip, and xsel; tmux uses load-buffer -w. Set DSH_TUI_DISABLE_MOUSE=1 to temporarily disable fullscreen mouse handling.

ask_user_question questionnaires

When the model invokes the questionnaire tool, its panel temporarily owns the keyboard:

KeyBehavior
Up/DownMove through options
SpaceToggle a multi-select option
TabSwitch to a custom text answer
EnterSubmit the current question
Esc (from question 2 onward)Return to the previous question and keep the current draft
Esc (from question 1)Cancel the whole batch; the model receives ASK_CANCELLED
Ctrl+CCancel the whole batch from any question; the model receives ASK_CANCELLED (a harness-side abort still reports ASK_ABORTED)
Ctrl+KFold/unfold the ask_user_question questionnaire panel

The last row is a free-form input line: typing directly on an option row submits that option's label plus your custom text together (no need to Tab first); Tab jumps straight to the input line.

Batched questions and concurrent subagent questions are shown one at a time in FIFO order. A compact Q&A summary is added to the local transcript afterward.

Plan review

When the model calls exit_plan_mode in plan mode, the full plan is rendered as markdown in the review panel (the dedicated decision layout for intent: plan-review):

KeyBehavior
Up/DownMove between the options and the feedback input line at the bottom
Mouse wheelScroll the plan body; Approve / Keep planning / feedback stay pinned in view
1/2Submit the corresponding option directly (when the feedback buffer is empty; otherwise digits are treated as feedback characters)
TypingEnters the feedback input line
Enter (option row)Submit that option; an approval row with feedback errors out — approval must carry no feedback, or the protocol treats it as “continue planning”
Enter (input line)Submit “continue planning” with the feedback text
EscInterrupt the review to talk (ASK_CANCELLED); the model stays in plan mode

Approving a plan or running /plan off restores the actual sandbox and approval policy from before plan entry. Shift+Tab keeps the selected target mode, including switches deferred while a turn is running. Resumed sessions recover pre-plan permissions from event history; unknown historical permissions stay unchanged instead of falling back to full access when no configured mode matches.

Tool approval

When the permission layer issues an approval/request, the approval panel shows the tool name, the full command extracted from the paired tool call, and the reason, and temporarily owns the keyboard (when a questionnaire is also pending, approval takes priority):

KeyBehavior
Up/DownMove through options
1 / 2Allow (this time only) / deny
EnterSubmit the focused item
Esc / Ctrl+CDeny (fail closed)

The protocol offers only "allow once / deny" — there is no "always allow".

Slash commands

The command menu merges local commands with the DSH command registry. Type / to inspect the complete surface available in the current composition. Command descriptions follow the UI language (/lang): built-in commands and mapped registry commands (/plan, /goal, /feedback) show Chinese translations in zh; unmapped registry commands fall back to the registry's own text.

GroupCommands
Sessions/new, /resume, /home, /agentview (all three open the same session-management screen), /bg (alias /background, backgrounds the session and opens that screen), /rename, /recap (recent-activity summary + one-key suggested title), `/workspace resume
Status/context, /status, /cost, /balance (official DeepSeek balance: summary row + hover details, click to refresh), /config, /doctor, /init, /agents, /jobs (background jobs panel: status/elapsed/exit code, k kills), /settings
Model and display/model, /effort, /thinking, /tokens, /activity, /preset, /theme, /color (session accent color: bare opens the palette picker, <name> sets directly, status/reset; input border + session-name chip at the top-right, per-session; chip off by default, enable in /settings), /lang
Account and policy/provider, /login, /logout, /permission, /add-dir, /hooks, /mcp, /plugins (check <path> validates a plugin manifest)
Skills/skills lists skills DSH discovers from the active profile, user, and project; user-invocable skills join the menu as /name
Other/update, /vim (vim editing mode toggle — see “Editing keys”), /terminal-setup, /connect, /help, /exit (aliases /quit, /q)
Registry/plan, /goal, and any other command registered by the DSH composition

dsh-TUI does not preinstall general-purpose skills; DSH and the active composition own skill content and discovery.

Additional forms:

  • /activity opens the animation picker; /activity frames <name> selects directly (current names: random, star2, sand, triangle, box, box2, corners, point, layer, flip, aesthetic, hamburger, moon, moon8, whale-spout, whale-spin, whale-bubbles, clock, traffic_lights, comet, breathe, dots, arrow, spark, bar, braille, arc, circle, grow, noise, bounce, rainbow, bar2, dqpb, toggle; default moon8). A legacy local claude setting is read as moon8, and the picker does not show that legacy preset; /activity status reports the current choice.
  • /preset <id> and /preset status are described in the configuration guide.
  • /effort opens the reasoning-effort slider (←/→ adjusts live); /effort <id> sets a level directly; /effort status reports the current one.
  • /theme <name> and /theme status are described in the theme guide.
  • /permission reads the DSH permissionPresets registry, preserving registry order for the picker, completion and the Shift+Tab cycle. Third-party presets need no TUI hard-coding. While the service snapshot is usable the TUI owns /permission as a local command: bare run opens the picker, an argument switches directly, status prints the current preset and policy explainer. Switches prefer the official /permission <preset> command; when the command row never reaches this agent's registry the TUI falls back to the service's own official write path (the same handler, real events) and confirms via event/readback; when neither is available it fails loudly instead of sending the input to the model. Exiting plan mode restores the pre-plan atoms first, then the durable preset you were on before plan mode (while the registry still offers it). When the registry service is absent, TUI uses its legacy three-row compatibility roster; a mounted but broken service is unavailable and fails closed.
  • /lang toggles the interface language (see “Interface language”).
  • /compact compresses the session history; unavailable under the minimal preset (bash + editor only).
  • /thinking toggles extended reasoning display; UI state only — not persisted.
  • After startup, the TUI checks npm for a newer version in the background and shows a notification when one is available. The check follows the npm registry configuration (NPM_CONFIG_REGISTRY or ~/.npmrc), so mirror users see the versions their package manager actually installs. /update updates the installed @deepseek-harness-tui/dsh-tui, then restarts and resumes the current session automatically; wait for an active turn to finish first. It is only available under a dsh --profile <name> launch (source checkouts get an unavailable notice), and an already-latest install is reported as such without restarting.
  • /plan [off|message] and /goal ... are handled by DSH command plugins and recorded as session events.
  • Skill commands are executed by the host injecting the corresponding SKILL.md body, with arguments passed through unchanged; DSH and the active composition own their content and discovery.

/connect and /hooks are currently compatibility placeholders. When the DSH composition has no matching capability, each command explains that explicitly rather than silently doing nothing.