refactor(tui): move terminal UI to crate root (#5236)

This commit is contained in:
Steven Enamakel
2026-07-28 09:09:31 +03:00
committed by GitHub
parent 46ee9a0fa4
commit 4ed193b060
17 changed files with 68 additions and 122 deletions
+6 -7
View File
@@ -290,11 +290,11 @@ ppt-rs = { version = "0.2.14", optional = true }
# drops both `ratatui` and `crossterm`. Kept in lockstep — ratatui 0.30
# re-exports crossterm 0.29, so declaring crossterm directly at the same major
# unifies to a single crossterm in the dep graph. Only compiled into
# `src/openhuman/tui/` behind `#[cfg(feature = "tui")]`.
# `src/tui/` behind `#[cfg(feature = "tui")]`.
ratatui = { version = "0.30", optional = true }
crossterm = { version = "0.29", optional = true }
# Terminal column-width measurement for the `tui` chat renderer
# (`src/openhuman/tui/render.rs`); only compiled behind `#[cfg(feature = "tui")]`.
# (`src/tui/render.rs`); only compiled behind `#[cfg(feature = "tui")]`.
unicode-width = { version = "0.2", optional = true }
# Native-Rust `.docx` writer for the `generate_document` tool (GH #4847).
# Pure Rust (no subprocess / managed runtime), MIT-licensed, actively
@@ -564,11 +564,10 @@ crash-reporting = ["dep:sentry"]
# Tauri UI and never needs a terminal one) — see the allowlist entry in
# `scripts/ci/check-feature-forwarding.mjs`. Slim / headless builds opt out via
# `--no-default-features --features "<explicit list without tui>"`, which drops
# the exclusive `ratatui` + `crossterm` dependencies. The module
# `src/openhuman/tui/` is a facade (always compiled) whose behavioural submodules
# are `#[cfg(feature = "tui")]`; when off, `tui::stub::run_from_cli` returns a
# build-fact "tui feature disabled at compile time" error from the untouched
# `"tui" | "chat"` CLI arm (mirrors the `mcp` stub pattern).
# the exclusive `ratatui` + `crossterm` dependencies. The crate-root
# `src/tui/` module is declared only under `#[cfg(feature = "tui")]`; when off,
# the always-compiled CLI dispatcher returns a build-fact "tui feature disabled
# at compile time" error for the `"tui" | "chat"` commands.
tui = ["dep:ratatui", "dep:crossterm", "dep:unicode-width"]
# Medulla integration: the HTTP/SSE client for the Medulla orchestration backend
+9 -9
View File
@@ -587,15 +587,15 @@ End-to-end coverage of the agent harness via the web-chat RPC surface against an
### 14.1 TUI Subcommand (feature-gated `tui`)
| ID | Feature | Layer | Test path(s) | Status | Notes |
| ------ | -------------------------------------------------- | ----- | ------------------------------------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------- |
| 14.1.1 | Transcript reducer (deltas, done, error, tools) | RU | `src/openhuman/tui/state.rs` | ✅ | Pure `TranscriptState::apply_event` — client-id filtering, text/thinking split, `chat_done` finalization |
| 14.1.2 | Runner flags + thread resolution + RPC name pins | RU | `src/openhuman/tui/runner.rs`, `src/openhuman/tui/app.rs` | ✅ | `--thread`/`--new` parsing; canonical `openhuman.*` method names resolve via registry |
| 14.1.3 | Tab order + render layout + persistent footer | RU | `src/openhuman/tui/ui_state.rs`, `src/openhuman/tui/render.rs` | ✅ | Logs-first tab state and ratatui TestBackend |
| 14.1.4 | Disabled-build stub (`--no-default-features`) | RU | `src/core/cli_tests.rs` (`tui`/`chat` `*_reports_disabled_build_when_gate_off`) | ✅ | Build-fact error, not `unknown namespace` |
| 14.1.5 | Bare-command launch policy | RU | `src/core/cli_tests.rs` | ✅ | TTY + CLI auto-launch; Docker, pipes, feature-off, and explicit commands stay headless |
| 14.1.6 | Bounded live core-log buffer | RU | `src/core/logging.rs` | ✅ | Preserves ordering; bounds both line count and individual line length |
| 14.1.7 | Interactive terminal session (raw mode, streaming) | MS | manual PTY smoke | 🚫 | Requires a real PTY to verify key input and terminal restoration |
| ID | Feature | Layer | Test path(s) | Status | Notes |
| ------ | ------------------------------------------------- | ----- | ----------------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------- |
| 14.1.1 | Transcript reducer (deltas, done, error, tools) | RU | `src/tui/state.rs` | ✅ | Pure `TranscriptState::apply_event` — client-id filtering, text/thinking split, `chat_done` finalization |
| 14.1.2 | Runner flags + thread resolution + RPC name pins | RU | `src/tui/runner.rs`, `src/tui/app.rs` | ✅ | `--thread`/`--new` parsing; canonical `openhuman.*` method names resolve via registry |
| 14.1.3 | Tab order + render layout + persistent footer | RU | `src/tui/ui_state.rs`, `src/tui/render.rs` | ✅ | Logs-first tab state and ratatui TestBackend |
| 14.1.4 | Disabled-build diagnostic (`--no-default-features`) | RU | `src/core/cli_tests.rs` (`tui`/`chat` `*_reports_disabled_build_when_gate_off`) | ✅ | Build-fact error, not `unknown namespace` |
| 14.1.5 | Bare-command launch policy | RU | `src/core/cli_tests.rs` | ✅ | TTY + CLI auto-launch; Docker, pipes, feature-off, and explicit commands stay headless |
| 14.1.6 | Bounded live core-log buffer | RU | `src/core/logging.rs` | ✅ | Preserves ordering; bounds both line count and individual line length |
| 14.1.7 | Interactive terminal session (raw mode, streaming) | MS | manual PTY smoke | 🚫 | Requires a real PTY to verify key input and terminal restoration |
## 15. Agent Profile Homes (multi-agent profiles)
+2 -2
View File
@@ -17,7 +17,7 @@ gated behind a Cargo feature `tui`.
(repo convention: gates default-ON). It must NOT ship in the desktop app —
add `'tui': 'Terminal UI subcommand; the desktop app ships its own Tauri UI.'`
to `INTENTIONALLY_NOT_FORWARDED` in `scripts/ci/check-feature-forwarding.mjs`.
2. **Module**: new domain dir `src/openhuman/tui/` using the **mcp/voice facade pattern**:
2. **Module**: crate-level `src/tui/`, gated at its declaration in `src/lib.rs`:
- `mod.rs` always compiled; real submodules `#[cfg(feature = "tui")]`;
`#[cfg(not(feature = "tui"))] mod stub;` exposing the same `run_from_cli`.
- `stub.rs` `run_from_cli` bails with
@@ -27,7 +27,7 @@ gated behind a Cargo feature `tui`.
philosophy: absence, not degraded registration — but here the only outside
touch-point is the CLI arm, which uses the stub for a build-fact error).
3. **CLI arm**: in `src/core/cli.rs` match (~line 63), add
`"tui" | "chat" => crate::openhuman::tui::run_from_cli(&args[1..])`.
`"tui" | "chat" => run_tui_from_cli(&args[1..])`.
Arm stays **un-cfg'd** (mcp precedent). Add `"tui" | "chat"` to the banner-suppression
`matches!` at lines 4850 (a TUI must own the terminal).
4. **In-process core, no HTTP**: build a multi-thread tokio runtime with
+17 -6
View File
@@ -56,7 +56,7 @@ pub fn run_from_cli_args(args: &[String]) -> Result<()> {
cfg!(feature = "tui"),
)
{
return crate::openhuman::tui::run_from_cli(&[]);
return run_tui_from_cli(&[]);
}
// `--no-tui` is a global opt-out, not a synthetic subcommand. Strip it
@@ -85,11 +85,9 @@ pub fn run_from_cli_args(args: &[String]) -> Result<()> {
match args[0].as_str() {
"run" | "serve" => run_server_command(&args[1..]),
"mcp" | "mcp-server" => crate::openhuman::mcp_server::run_stdio_from_cli(&args[1..]),
// Terminal chat UI. Un-`#[cfg]`'d on purpose: in a slim build this
// resolves to `tui::stub::run_from_cli`, which bails with a build-fact
// error (see `src/openhuman/tui/stub.rs`) rather than falling through to
// `unknown namespace: tui`.
"tui" | "chat" => crate::openhuman::tui::run_from_cli(&args[1..]),
// Keep the command present in slim builds so users get a build-fact
// diagnostic rather than a misleading "unknown namespace" error.
"tui" | "chat" => run_tui_from_cli(&args[1..]),
"call" => run_call_command(&args[1..]),
"tree-summarizer" => {
crate::openhuman::memory_tree::tree_runtime::cli::run_tree_summarizer_command(
@@ -113,6 +111,19 @@ pub fn run_from_cli_args(args: &[String]) -> Result<()> {
}
}
#[cfg(feature = "tui")]
fn run_tui_from_cli(args: &[String]) -> Result<()> {
crate::tui::run_from_cli(args)
}
#[cfg(not(feature = "tui"))]
fn run_tui_from_cli(_args: &[String]) -> Result<()> {
anyhow::bail!(
"tui feature disabled at compile time; rebuild with `--features tui` \
(or use a default-feature build) to enable `openhuman tui`"
)
}
/// Pure launch policy for the bare `openhuman` command. Explicit subcommands
/// are never rewritten. Docker and redirected/CI sessions keep the headless
/// CLI behavior; `openhuman tui` remains an explicit override everywhere.
+4 -4
View File
@@ -279,8 +279,8 @@ fn mcp_server_alias_reports_disabled_build_when_gate_off() {
/// is to delete the `"tui" | "chat"` match arm, which is WRONG — `tui` would
/// fall through to generic namespace resolution and die with `unknown
/// namespace: tui`, reading like a user typo. Instead `cli.rs` is untouched and
/// the arm resolves to `tui::stub::run_from_cli`, which bails with the message
/// asserted below.
/// the arm resolves to the CLI-local disabled-feature dispatcher, which bails
/// with the message asserted below.
#[test]
#[cfg(not(feature = "tui"))]
fn tui_subcommand_reports_disabled_build_when_gate_off() {
@@ -305,8 +305,8 @@ fn tui_subcommand_reports_disabled_build_when_gate_off() {
);
}
/// The `chat` alias must behave identically to `tui` — both arms route to the
/// same stub, so neither can silently regress into the fall-through.
/// The `chat` alias must behave identically to `tui` — both names route to the
/// same dispatcher, so neither can silently regress into the fall-through.
#[test]
#[cfg(not(feature = "tui"))]
fn chat_alias_reports_disabled_build_when_gate_off() {
+2
View File
@@ -18,6 +18,8 @@ pub mod core;
pub mod embed;
pub mod openhuman;
pub mod rpc;
#[cfg(feature = "tui")]
pub mod tui;
pub use openhuman::config::DaemonConfig;
pub use openhuman::memory_store::{MemoryClient, MemoryState};
-1
View File
@@ -143,7 +143,6 @@ pub mod tool_registry;
pub mod tool_status;
pub mod tool_timeout;
pub mod tools;
pub mod tui;
pub mod update;
pub mod util;
pub mod voice;
-56
View File
@@ -1,56 +0,0 @@
//! Tabbed terminal UI — bare `openhuman` or the explicit `tui` / `chat` subcommand.
//!
//! A [ratatui]-based terminal front-end with Logs, Chat, Config, and Settings.
//! Chat uses the **same `web_chat` surface** the desktop app drives (`openhuman.channel_web_chat` /
//! `openhuman.channel_web_cancel` +
//! [`web_chat::subscribe_web_channel_events`](crate::openhuman::web_chat::subscribe_web_channel_events)).
//! It boots the core in-process — no HTTP, no sockets — via
//! `CoreBuilder::new(HostKind::Cli).domains(DomainSet::full()).services(ServiceSet::none())`
//! and streams a live transcript in the terminal.
//!
//! ## Compile-time gate (`tui` feature)
//!
//! `pub mod tui;` is ALWAYS compiled — it is a facade (mirrors
//! [`mcp_server`](crate::openhuman::mcp_server)). The terminal driver, the
//! renderer, the reducer, and the event loop are gated behind the default-ON
//! `tui` Cargo feature; when it is off, [`stub`] mirrors the one surface an
//! always-compiled caller reaches — [`run_from_cli`] — with a build-fact
//! disabled-error body.
//!
//! The `"tui" | "chat"` arm in `src/core/cli.rs` is deliberately left
//! **un-`#[cfg]`'d**: in a slim build it resolves to [`stub::run_from_cli`],
//! which bails with a message naming the compile-time gate as the cause (so the
//! error reads like a build fact, not `unknown namespace: tui`). This is the
//! same reasoning documented on [`mcp_server::stub::run_stdio_from_cli`].
#[cfg(feature = "tui")]
mod app;
#[cfg(feature = "tui")]
mod controls;
#[cfg(feature = "tui")]
mod render;
#[cfg(feature = "tui")]
mod runner;
#[cfg(feature = "tui")]
mod state;
#[cfg(feature = "tui")]
mod terminal;
#[cfg(feature = "tui")]
mod ui_state;
#[cfg(feature = "tui")]
pub use runner::run_from_cli;
// State reducer is behaviour-only but has no terminal deps, so its tests run in
// the default (feature-on) build. Exported for the sibling submodules + tests.
#[cfg(feature = "tui")]
pub use state::{Entry, EntryKind, TranscriptState};
// ---------------------------------------------------------------------------
// Disabled facade — compiled only when the `tui` feature is OFF.
// ---------------------------------------------------------------------------
#[cfg(not(feature = "tui"))]
mod stub;
#[cfg(not(feature = "tui"))]
pub use stub::*;
-37
View File
@@ -1,37 +0,0 @@
//! Disabled-`tui` facade for [`super`] (the tabbed terminal UI).
//!
//! Compiled only when the `tui` Cargo feature is OFF (see the gate in
//! [`super`]). It mirrors the one public symbol always-compiled callers reach —
//! [`run_from_cli`] — with a disabled-error body.
//!
//! The signature MUST match the real one exactly (`&[String] -> anyhow::Result<()>`).
//! The disabled build
//! (`cargo check --no-default-features --features tokenjuice-treesitter`) is the
//! only thing that catches drift.
/// Error text returned by the disabled path. Shared so callers / log-greps see
/// one stable string, and asserted by the disabled-build CLI tests.
const DISABLED_MSG: &str = "tui feature disabled at compile time";
/// Fails with a build-fact diagnostic instead of opening the terminal UI.
///
/// This is deliberately a stub rather than a `#[cfg]` on the `"tui" | "chat"`
/// match arm in `src/core/cli.rs`. Deleting the arm is the naive move and is
/// WRONG: the `tui` / `chat` token would fall through to generic namespace
/// resolution and die with `unknown namespace: tui`, which reads like the user
/// typo'd a command rather than like a deliberate property of this build.
/// Keeping the arm and failing here means the user gets a non-zero exit and a
/// one-line stderr diagnostic naming the fix, and `cli.rs` needs no `#[cfg]` at
/// all — the gate stays invisible to the transport layer.
///
/// Banner suppression in `cli.rs` is a `matches!` on the raw string, so it
/// keeps working here without touching a gated symbol.
pub fn run_from_cli(_args: &[String]) -> anyhow::Result<()> {
log::warn!(
"[tui] {DISABLED_MSG} — `openhuman tui`/`chat` rejected; rebuild with `--features tui`"
);
anyhow::bail!(
"{DISABLED_MSG}: this build was compiled without the `tui` feature, so the terminal \
chat UI is unavailable. Rebuild with `--features tui`."
)
}
+28
View File
@@ -0,0 +1,28 @@
//! Tabbed terminal UI — bare `openhuman` or the explicit `tui` / `chat` subcommand.
//!
//! A [ratatui]-based terminal front-end with Logs, Chat, Config, and Settings.
//! Chat uses the **same `web_chat` surface** the desktop app drives (`openhuman.channel_web_chat` /
//! `openhuman.channel_web_cancel` +
//! [`web_chat::subscribe_web_channel_events`](crate::openhuman::web_chat::subscribe_web_channel_events)).
//! It boots the core in-process — no HTTP, no sockets — via
//! `CoreBuilder::new(HostKind::Cli).domains(DomainSet::full()).services(ServiceSet::none())`
//! and streams a live transcript in the terminal.
//!
//! The entire module is gated at its crate-root declaration in `src/lib.rs`.
//! Slim builds therefore compile none of the terminal driver, renderer,
//! reducer, or event loop. The always-compiled CLI owns the disabled-feature
//! diagnostic for the `tui` and `chat` commands.
mod app;
mod controls;
mod render;
mod runner;
mod state;
mod terminal;
mod ui_state;
pub use runner::run_from_cli;
// State reducer is behaviour-only but has no terminal deps, so its tests run in
// feature-on builds. Exported for the sibling submodules + tests.
pub use state::{Entry, EntryKind, TranscriptState};