riesbri/dsh-tui--packages-renderer0

@riesbri/dsh-tui-renderer

Append-and-live-region terminal renderer: display width, key decoding, and differential redraw. Knows nothing about agents.

包名
@riesbri/dsh-tui-renderer
版本
0.1.0
许可证
MIT
最近更新
2026年8月17日

安装

此插件尚未提供可验证的 bundle,或兼容性检查未通过。请先阅读仓库说明。 阅读完整 README ↗

dsh-tui

An interactive terminal surface for DeepSeek Harness.

The harness ships a browser UI and a one-shot CLI; its terminal frontend was removed and never replaced. This is one, built the way the removal note asks for: an in-process Cordis bundle over the real interaction seams, with its own renderer.

git clone https://github.com/riesbri/dsh-tui && cd dsh-tui
pnpm install && pnpm build
dsh plugin --profile tui add ./packages/tui
dsh --profile tui

A github: spec cannot be used yet: it installs a repository root, and this one is a workspace whose root is not the bundle. Publishing @riesbri/dsh-tui to npm is what turns installation into one line.

What it does today

A session loop: type a prompt, watch the reply stream, see tool calls and results, answer approvals and ask_user_question prompts, switch models with /model, interrupt a turn with ctrl-c, quit with ctrl-d. Slash commands registered by the harness (/compact, /plan, /goal, /permission, /feedback) dispatch through ctx.commands.

Editing keys: arrows, home/end, ctrl-a/ctrl-e, backspace/delete, ctrl-u/ctrl-k/ctrl-w, ctrl-l to clear the display.

How it is put together

Two packages, split so the drawing half never learns about agents:

PackageOwns
@riesbri/dsh-tui-rendererDisplay width, key decoding, the input buffer, and the screen. Imports nothing from the harness.
@riesbri/dsh-tuiThe bundle: the session loop, the transcript projection, the interaction seams, and the slot registry.

The screen is append-plus-live-region, not a viewport

A chat transcript only ever grows, so the renderer owns no full-screen buffer. Finished output is written straight into the terminal's own scroll buffer and never touched again; only a bottom live region — the streaming reply, a prompt, the composer — is redrawn in place. Native scrollback, mouse selection, and copy keep working, and scroll position never has to be modelled or reflowed on resize. The rule that makes it correct: the live region is always the last thing on screen, so every write goes through Screen.

Widths follow Unicode East Asian Width

The harness is bilingual — shipped agent presets are named in Chinese and half its documentation is — so a CJK ideograph measured as one column corrupts every row in the buffer, not only the row holding it. displayWidth treats W and F as two columns and combining marks as none, and the redraw arithmetic counts rendered rows, so a wrapped or CJK line is climbed correctly.

Untrusted text is escaped before it can reach the terminal

Everything a model, a tool, or a session log produces is untrusted for terminal purposes: an escape sequence in tool output would repaint the live region out from under the renderer, and a carriage return would reposition the cursor. Such text goes through escapeControls and is shown in caret notation. Styling is a separate function applied only to strings this frontend writes itself.

Its own parts are plugins too

The status line, the composer, the approval prompt, the question prompt, and the model picker do not know about each other. Each registers into ctx.tuiSlots, and the runner composes the live region from whatever is registered — the terminal's equivalent of the web client's ctx.slots. Adding a footer widget or replacing the approval prompt is a registration, not an edit to the runner.

ctx.tuiSlots.register('status', { render: () => ['my widget'] })
ctx.tuiSlots.pushOverlay(myPrompt)   // takes the whole region and every key

It restores the ask_user_question seam

ctx.userQuestions accepts exactly one provider per context and throws DUPLICATE_PROVIDER on a second registration, and the web host's API proxy already claims that slot. A terminal frontend is therefore its own profile, never a layer over the web profile — and it re-fills the gap left when the harness removed its last non-browser questions provider.

Known limitations and deferred work

  • No session resume. Each launch creates a new session. Resuming needs process handoff, because a resumed session must re-enter its own workspace directory.
  • No markdown rendering. Replies are shown as plain text. The harness already depends on mdast-util-from-markdown, so parsing is available and only an ANSI pass is missing.
  • Tool cards are generic. presentCall/presentResult render intent is not consulted yet; calls show name and arguments, results show a truncated preview. Every render-intent variant is documented to degrade to raw content, so this is the sanctioned fallback rather than a gap in correctness.
  • A streaming reply shows only its last 8 lines while it streams. The live region is redrawn by climbing rows, so it has to stay shorter than the screen; the full text commits to scrollback when the assembled message lands.
  • Multi-line paste submits each line separately. The decoder reports an embedded newline as enter, and the composer submits on enter.
  • No @ file mentions, autocomplete, or command menu. A submitted /name is dispatched, but nothing lists what is available.
  • Running from a source checkout needs the bin directlynode --import tsx/esm apps/cli/src/bin.ts --profile tui. Under pnpm dsh the script wrapper does not hand the child a terminal, and the frontend refuses to start without one.

Development

pnpm install
pnpm build          # tsc project references; .ts source imports emit as .js
pnpm test           # renderer unit tests: no terminal, no model

The bundle's devDependencies link the harness packages from a sibling checkout so it typechecks against the real service types. Adjust those paths for your layout.

License

MIT