deep-flow
A terminal UI (TUI) for DeepSeek Harness: an interactive Ink REPL shipped as an out-of-tree dsh bundle riding over dsh-base. No Host, HTTP server, or browser — everything runs in-process against the composed Cordis tree.
Published on npm as @jkxie/dsh-deep-flow. 中文说明见 README.zh-CN.md。
░████ ░█████ ░█████ ░████ ░█████ ░█ ░████ ░█ █
░█ █ ░█ ░█ ░█ █ ░█ ░█ ░█ █ ░█ █
░█ █ ░███ ░███ ░████ ░███ ░█ ░█ █ ░█ █
░█ █ ░█ ░█ ░█ ░█ ░█ ░█ █ ░█ █ █
░████ ░█████ ░█████ ░█ ░█ ░█████ ░████ ░█ █
Features
- Default new session — boot drops you straight into a fresh conversation;
/sessions opens the session picker.
- Session management —
/sessions opens a Gemini-style searchable session picker (create / resume / switch); the live session title shows above the composer and can be pinned with /rename.
- Streaming conversation — assistant output streams in, rendered as Markdown with
lowlight (highlight.js) syntax highlighting, in a scrollable transcript with auto-follow.
- Rich tool cards — file edits as inline diffs, reads with line numbers + highlighting, terminal output, search results, and web sources, driven by the tool render-intent contract.
- Inline human collaboration — slash commands (local
/new /rename /init /sessions /models /keys /help /exit plus the harness's own commands), permission prompts (y/n), and ask-user questions, all answered in a focused dialog layer.
- Input UX —
/ command completion and @path completion with inline ghost text (Tab to accept), plus ↑/↓ input history.
- Model switching —
/models opens a picker over ctx.llm's provider/model catalog, persisted through the default-model settings.
- Provider setup —
/provider opens a picker over a curated provider roster (OpenAI / Anthropic / OpenCode), then collects the API key inline (masked) and wires it end to end: the key goes to the harness credential store and the llm-pi-ai provider profile lands in the user settings document, live — no restart.
- Gemini-style theme — semantic color tokens, gradient logo + spinner,
> prompt, and a centralized, documented keymap.
- Status observability — a live working line (driven by the
dsh-working-activity plugin) over a segmented context bar (system / prompt / assistant / thinking / tools / free), a TPS gauge + sparkline, and per-run stats — cache hit rate, reasoning effort, and input → output tokens — on the status line.
- Git branch badge — when the working directory is a git repo, the current branch shows as a
⎇ <branch> badge on the status line (read at boot and refreshed each time a model turn completes).
- Session metrics commands —
/status (model, effort, session id, cwd, tokens, context %, tps), /cost (input / output / cache read / cache write), and /tokens (input → output) report right in the transcript.
- Goals & todos —
/goal and /todo open panels projecting goal/change and todo/write session events into a live goal + todo list.
- Trace timeline —
/trace opens a filterable session timeline (turn / tool / reasoning / token categories), with ↑/↓ switching filters.
- Export to Markdown —
/export writes the current session (user / assistant / tool sections) to a Markdown file in the current working directory (cwd).
- Agent presets —
/preset opens a picker over the harness's agent-preset roster; picking one switches the agent's preset on a blank session (a session with history reports that preset switching needs an empty conversation).
- Session modes —
Shift+Tab cycles Default / Plan / Full access: each mode is a named bundle of optional DSH plane switches — plan mode (dsh-plan-mode /plan), sandbox policy, and approval policy.
- Side questions —
/btw <question> fires an independent llm.stream call against the current model selection, shown in a panel that never blocks or interrupts the main turn.
- Rewind — double-
Esc on an empty input opens a picker of past user messages; picking one forks the conversation back to that point (a fresh agent via sessions.fork + agents.create) and pre-fills the input with that message.
- Startup tips — on an empty session, three randomized usage tips show under the logo/version — command token highlighted, description dimmed — chosen by system locale; the tips live in
src/tips.txt and are editable without touching code.
Requirements
- Node
^22.19.0 or >= 24 (older 22.x lacks node:zlib.createZstdDecompress).
pnpm >= 11 — dsh plugin add shells out to pnpm; pnpm 10.x hits ERR_PNPM_ADDING_TO_ROOT.
DEEPSEEK_API_KEY — only required to send real model requests; boot and the UI work without it.
Install and run
# Prerequisite: the official harness CLI
npm install -g @deepseek-ai/dsh
# Install deep-flow globally (first run auto-initializes the profile)
npm install -g @jkxie/dsh-deep-flow
# Start
deep-flow
The deep-flow launcher keeps the profile in sync with the installed package:
- First run — the uninitialized profile is bootstrapped automatically via
dsh plugin --profile deep-flow add @jkxie/dsh-deep-flow@<version>, then it launches.
- Version drift — if the installed profile version differs from the globally
installed launcher's version, the profile is re-pinned to the launcher's
version automatically, then it launches. So upgrading is just
npm install -g @jkxie/dsh-deep-flow@<new version> followed by deep-flow —
the profile updates itself on the next launch.
- In sync — launches directly.
Manual / advanced equivalent:
dsh plugin --profile deep-flow add @jkxie/dsh-deep-flow@latest
dsh --profile deep-flow
Install a specific version:
dsh plugin --profile deep-flow add @jkxie/dsh-deep-flow@0.2.0
Update to the latest:
dsh plugin --profile deep-flow add @jkxie/dsh-deep-flow@latest
@deepseek-ai/* packages resolve from the dsh installation's flat profile fallback (tsx paths in source launches), so they are not npm dependencies of this package — you never install them yourself.
Keymap
| Surface | Keys | Action |
|---|
| Conversation | ↑ / ↓ | recall previous inputs |
| ← / → | move the cursor |
| PgUp / PgDn / mouse wheel | scroll the transcript |
| Tab | accept a / command or @path suggestion |
| Shift+Tab | cycle session mode (default / plan / full access) |
| Enter | send the message |
| Ctrl-C | clear input → cancel the turn → quit |
| /new | start a new session |
| /rename | rename the current session |
| /init | generate AGENTS.md by analyzing this directory |
| /sessions | pick a session |
| /models | pick a model |
| /provider | set up a model provider (API key) |
| /keys | manage API keys |
| /help | list slash commands |
| /status | show session info |
| /cost | show token usage |
| /tokens | show token details |
| /goal | show the goal panel |
| /todo | show the todo panel |
| /export | export session to Markdown |
| /trace | show session trace timeline |
| Esc Esc | cancel the running turn / rewind to a past message (empty input) |
|
How it works
deep-flow is a Cordis bundle (dsh.bundle.patch → cordis.patch.yml) that disables the shared module-reload hmr row and inserts the deep-flow-runner plugin. The runner injects the core services (agents, sessions, agentDefaultModel, tools, commands, userQuestions, approval), awaits loader settlement, then renders an Ink tree that:
- reads the durable session log through
session/event and projects it through a Channel (src/store/channel.ts) into the immutable snapshot the React layer subscribes to via useSyncExternalStore; TPS, context-bar and token metrics derive from that session/event projection (assistant/message usage, request/header, request/context, user/message, tool/call), while live activity/status frames from dsh-working-activity feed only the working-status line,
- submits user input through
agent.followup(),
- cancels in-flight turns through
agent.cancel(),
- creates / resumes agents through
ctx.agents.create() / ctx.agents.resume() — resume first runs src/compat/sessionLog.ts, which repairs the persisted log in place so the ephemeral activity/status frames are marked ignorable and seed validation accepts the session,
- answers the interaction seams through
ctx.commands, the approval/request waterfall, and ctx.userQuestions.
| Concept | Mechanism |
|---|
| Event stream | session/event |
| Prompt the agent | agent.followup() |
| Interrupt | agent.cancel() |
| Create / resume session | ctx.agents.create() / ctx.agents.resume() |
| Permissions / commands / Q&A | ctx.approval / ctx.commands / ctx.userQuestions |
| Model catalog / selection | ctx.llm / ctx.agentDefaultModel |
| Status observability | activity/status (dsh-working-activity) → working-status line only; TPS / context-bar metrics come from the session/event projection |
The working line comes from the dsh-working-activity plugin, re-exported from src/working-activity.ts under this package's own @jkxie/dsh-deep-flow/working-activity subpath so the dsh loader always resolves it from the profile's direct dependency (pnpm's isolated layout never links transitive deps into the profile root).
Project layout
src/
index.tsx entry — re-exports name/inject/apply from plugin.tsx
plugin.tsx runner plugin boundary (services, controller, boot, render)
app.tsx App surface (view state, single useInput dispatcher, dialogs wiring)
commands.ts deep-flow slash commands + parser (/status /cost /tokens /goal /todo /export /trace, pure)
controller.ts Controller / HomeSession / CommandOutcome / StatusSnapshot types
prompt.ts prompt queue bridging boot ↔ React
working-activity.ts re-export of dsh-working-activity (loader-resolvable subpath)
store/
channel.ts Channel — session/event → transcript lines + live metrics snapshot
metrics.ts context bar, TPS gauge + sparkline, token formatters (pure)
goal-todo.ts goal/change + todo/write event reducers (pure, replay-safe)
rewind.ts rewind candidates + fork-boundary computation (pure, replay-safe)
session-modes.ts configurable session modes (plan/sandbox/approval bundles, pure)
trace.ts bounded, filterable trace timeline projection (pure)
screens/
chat.tsx ChatScreen — transcript + composer + status line
status-line.tsx status line + segmented context bar footer
components/
goal-panel.tsx goal panel (/goal) — live goal from goal/change events
todo-panel.tsx todo panel (/todo) — whole-list todo/write snapshot
trace-view.tsx /trace filterable timeline view
btw-panel.tsx /btw side-question panel (independent llm.stream call)
preset-picker.tsx /preset agent-preset picker
rewind-picker.tsx double-Esc rewind picker (past user messages)
session-picker.tsx /sessions Gemini-style session picker
hooks/
useStore.ts thin useSyncExternalStore wrapper
compat/
sessionLog.ts pre-resume session-log repair for third-party events
transcript-view.tsx ToolLine / LineView
header.tsx gradient logo + version + startup tips (top of the transcript)
tips.ts parse tips.txt, pick randomized subset, locale detection (pure)
tips.txt bilingual startup-tip data (one cmd|desc|cmd|desc line per tip; edit freely)
composer.tsx bordered input box + spinner/footer
file-completion.ts @ path completion (pure)
git-branch.ts readBranch — current git branch of the cwd (pure)
init-prompt.ts buildInitPrompt — /init analysis prompt (pure)
spinner.tsx gradient color-cycling spinner
useTerminalSize.ts terminal size hook
markdown.tsx markdown-it → Ink renderer (streaming-aware)
highlight.ts lowlight/highlight.js syntax highlighter (→ Ink token colors)
tool-cards.tsx read / terminal / search / web result cards
diff.tsx inline file-diff view
dialogs.tsx approval / question / model-picker / help dialogs
keys.tsx API-key management view (masked editor over ctx.credentials)
theme.ts color theme (single dark theme, decoupled)
keymap.ts centralized keymap + help text
logo.tsx the startup ASCII art (gradient; swap this to rebrand)
cordis.patch.yml bundle patch (disables hmr, inserts deep-flow-runner)
Development
Build (produces lib/index.js, ESM):
pnpm run build
Run locally from a deepseek-harness checkout (requires pnpm install and at least pnpm run build:lib:host):
pnpm dsh plugin --profile deep-flow add ../deep-flow
pnpm dsh --profile deep-flow
There is no test, lint, or typecheck script — pnpm run build is the main verification command, plus pnpm run verify:metrics / verify:goal-todo / verify:trace / verify:rewind / verify:session-mode for the pure logic (metrics, goal/todo reducers, trace projection, rewind candidates/boundaries, session-mode folding). Rendering is verified offline by mounting components over a fake stdin/stdout with interactive: false; interactive behavior requires a real terminal. See PLAN.md for the milestone plan (M0–M5, all complete) and the design lessons recorded along the way.
Publishing
npm version patch # or minor / major — npm forbids re-publishing the same version
npm publish --access public --registry https://registry.npmjs.org/
prepublishOnly runs pnpm run build automatically. Use --tag beta to publish a pre-release without moving the latest tag.
License
MIT