* feat(channels): select custom agents per conversation Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> * fix(channels): reserve agent slash command across clients Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> * fix(tui): hide reserved slash commands from skills Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> * fix(channels): preserve selected agent across clients --------- Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
3.3 KiB
Terminal Workbench / TUI (packages/harness/deerflow/tui/)
A terminal-native UI over the embedded harness, exposed as the deerflow console script ([project.scripts] in packages/harness/pyproject.toml). It is a UI shell over DeerFlowClient and does not fork agent behavior. textual is an optional dependency (deerflow-harness[tui]; also in the backend dev group); the console script degrades to headless help when it is absent. Full guide: docs/TUI.md.
Module layout (all layers except app.py are pure / Textual-free and unit-tested directly):
cli.py—plan_launch()(pure launch-mode decision) + headless--print/--json+main()entry point. TTY → TUI, else headless help.--tui-transparent/DEER_FLOW_TUI_TRANSPARENTopt into terminal-default backgrounds without changing the solid-theme default. Uses an absolutefrom deerflow.tui.app import run_tuiso theapp.pymodule name doesn't triptest_harness_boundary.py(which records relative import module names verbatim).view_state.py—ViewState+reduce(state, action), the testable heart. Rows: user / assistant / tool / system. Title captured fromvaluesevents.runtime.py—translate(StreamEvent) -> [Action](pure) +stream_actions()which brackets a run withRunStarted/RunEndedand turns model errors into anAssistantErrorrow.message_format.py/command_registry.py/input_history.py/render.py/theme.py— pure helpers (tool summaries, slash registry +resolve(), ↑/↓ history, Rich renderers). The command registry must exclude the sharedRESERVED_SLASH_SKILL_NAMESfrom both its picker and resolver so the TUI cannot advertise a skill activation that the agent runtime rejects.app.py— TextualApp. RunsDeerFlowClient.stream()(sync) on a worker thread and marshals actions to the UI thread viacall_from_thread. Slash palette with/goalmanagement + model/thread modal pickers; routes idle display-only/clearthroughClearRowswithout replacing the active thread, and blocks state-resetting local commands like/newand/clearwith the standard "Still working" message during an active run; priority key bindings gated bycheck_actionso they never steal keys from overlays or the composer. Application-level PageUp/PageDown bindings scroll the transcript while preserving composer focus; streaming follows output only while the transcript remains at the bottom.session.py/persistence.py— builds the client + checkpointer and theThreadMetaWriter.
Web UI visibility: the Web UI lists threads from the threads_meta SQL table (user-scoped), not the checkpointer. persistence.py writes a threads_meta row under the default user ("default") into the same DB the Gateway reads — via the harness-only deerflow.persistence.engine.init_engine_from_config() — so TUI sessions appear in the Web UI sidebar without running the Gateway. Best-effort: a no-op on the memory backend. All DB work runs on one long-lived background event loop (a SQLAlchemy async engine is bound to its creating loop).
Tests: tests/test_tui_*.py — pure layers via plain pytest, the app/palette/overlays via Textual's pilot harness with a fake in-process session, and test_tui_persistence.py for the threads_meta round-trip.