122 lines
5.3 KiB
Markdown
122 lines
5.3 KiB
Markdown
# DeerFlow Terminal Workbench (TUI)
|
|
|
|
`deerflow` is a terminal-native workbench for the DeerFlow harness. It runs
|
|
**embedded** over `DeerFlowClient` — no Gateway, frontend, nginx, or Docker
|
|
services required — while honoring the same `config.yaml`, checkpointer, skills,
|
|
memory, MCP, and sandbox settings as the rest of DeerFlow.
|
|
|
|

|
|
|
|
## Install & run
|
|
|
|
The TUI ships as an optional extra so the core harness install stays lean:
|
|
|
|
```bash
|
|
uv pip install 'deerflow-harness[tui]' # or: pip install textual
|
|
```
|
|
|
|
Launch modes:
|
|
|
|
| Command | Behavior |
|
|
|---|---|
|
|
| `deerflow` | Launch the TUI when stdin/stdout are TTYs |
|
|
| `deerflow --tui` | Force the TUI (clear diagnostic if `textual` is missing) |
|
|
| `deerflow --cli` | Force headless/classic mode for one invocation |
|
|
| `deerflow chat` | Same TUI conversation surface |
|
|
| `deerflow --continue` | Resume the most recent thread |
|
|
| `deerflow --resume THREAD` | Resume a thread by id |
|
|
| `deerflow --print "question"` | Headless one-shot answer to stdout |
|
|
| `deerflow --json "question"` | Headless newline-delimited `StreamEvent`s |
|
|
| `echo "q" \| deerflow --print` | Read the message from stdin |
|
|
| `DEER_FLOW_TUI=1 deerflow` | Force the TUI via environment |
|
|
|
|
If no TTY is available and no headless flag is given, `deerflow` prints guidance
|
|
instead of hanging.
|
|
|
|
## Surface
|
|
|
|
- **Header** — model, thread, project root, skill/tool counts.
|
|
- **Transcript** — user prompts, assistant answers, and compact tool cards
|
|
(`⚙ Read path ✓`) with dimmed result previews. Finalized assistant messages
|
|
render as Markdown (headings, bold, lists, code, links); the actively-streaming
|
|
message stays plain text to avoid reflow jumpiness and snaps to Markdown when
|
|
it completes. Transcript re-renders are coalesced (~16 fps) so streaming stays
|
|
smooth on long threads.
|
|
- **Status line** — run state + animated spinner, model, thread title, token
|
|
usage, and an `esc interrupt` hint while a run is active.
|
|
- **Composer** — rounded input box. `/` opens the command palette.
|
|
|
|
### Keys
|
|
|
|
| Key | Action |
|
|
|---|---|
|
|
| `Enter` | Send message / accept palette selection |
|
|
| `/` | Open the slash-command palette |
|
|
| `↑` / `↓` | Palette navigation, or input history when the palette is closed |
|
|
| `Tab` | Complete the highlighted command (adds a trailing space) |
|
|
| `Esc` | Close the palette / overlay |
|
|
| `Ctrl+C` | Interrupt the active run, or quit when idle |
|
|
| `Ctrl+L` | Redraw · `Ctrl+U` clear composer |
|
|
|
|
### Slash commands
|
|
|
|
`/help` `/new` `/goal` `/threads` (`/switch`) `/model` `/skills` `/tools`
|
|
`/mcp` `/memory` `/uploads` `/usage` `/config` `/quit`, plus
|
|
`/<skill-name> task` to activate any enabled skill for the current turn (same
|
|
semantics as elsewhere in DeerFlow). `/model` and `/threads` open modal pickers.
|
|
Use `/goal <condition>` to set the active thread goal, `/goal` to show it, and
|
|
`/goal clear` to clear it.
|
|
|
|
## Architecture
|
|
|
|
The TUI is a UI shell over the existing embedded harness — it does **not** fork
|
|
agent behavior.
|
|
|
|
```
|
|
cli.py launch-mode planning (pure) + headless print/json + entry point
|
|
session.py builds DeerFlowClient (+ checkpointer) and the persistence writer
|
|
runtime.py StreamEvent -> reducer actions (pure translate + threaded driver)
|
|
view_state.py ViewState + reduce(state, action) (pure, the testable heart)
|
|
message_format compact tool summaries / truncation (pure)
|
|
command_registry slash-command registry + resolve (pure)
|
|
input_history bounded ↑/↓ history (pure)
|
|
render.py Rich renderers for header / transcript / status / palette (pure)
|
|
theme.py palette + symbols
|
|
app.py Textual App: composes widgets, drives runs on a worker thread,
|
|
marshals actions back to the UI thread, renders ViewState
|
|
persistence.py writes threads_meta so sessions appear in the Web UI (below)
|
|
```
|
|
|
|
`DeerFlowClient.stream()` is a **synchronous** generator, so the app runs it on a
|
|
Textual worker *thread* and marshals each yielded action back to the UI thread
|
|
via `call_from_thread`. The pure layers (everything except `app.py`) have no
|
|
Textual dependency and are unit-tested directly with synthetic `StreamEvent`s.
|
|
|
|
## Web UI visibility (shared persistence)
|
|
|
|
The Web UI lists conversations from the `threads_meta` SQL table (filtered by
|
|
`user_id`), **not** from the checkpointer. An embedded run only writes the
|
|
checkpointer, so a TUI thread would otherwise be invisible in the sidebar.
|
|
|
|
`persistence.py` closes that gap: on the first turn of a thread it writes a
|
|
`threads_meta` row — owned by the local default user (`"default"`) — into the
|
|
**same** database the Gateway reads, and syncs the generated title afterward.
|
|
This requires only the shared `threads_meta` store (built via
|
|
`deerflow.persistence.engine.init_engine_from_config`), **not** the Gateway
|
|
process. When the database backend is `memory` (no SQL store) the writer
|
|
degrades to a silent no-op and the TUI still works.
|
|
|
|
All DB work runs on one long-lived background event loop, because a SQLAlchemy
|
|
async engine is bound to the loop that created it.
|
|
|
|
## Tests
|
|
|
|
Pure layers are TDD'd in `backend/tests/test_tui_*.py`; the Textual app, slash
|
|
palette, and modal overlays are exercised through Textual's pilot harness with a
|
|
fake in-process session (no live model). `test_tui_persistence.py` proves the
|
|
`threads_meta` write/read round-trip.
|
|
|
|
```bash
|
|
cd backend && PYTHONPATH=. uv run pytest tests/ -k tui -q
|
|
```
|