diff --git a/Cargo.toml b/Cargo.toml index 40b615c48..e1be97602 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 ""`, 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 diff --git a/docs/TEST-COVERAGE-MATRIX.md b/docs/TEST-COVERAGE-MATRIX.md index 7079d59a7..281781e5a 100644 --- a/docs/TEST-COVERAGE-MATRIX.md +++ b/docs/TEST-COVERAGE-MATRIX.md @@ -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) diff --git a/docs/plans/tui-chat-plan.md b/docs/plans/tui-chat-plan.md index a863e9fcf..80f4d0554 100644 --- a/docs/plans/tui-chat-plan.md +++ b/docs/plans/tui-chat-plan.md @@ -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 48–50 (a TUI must own the terminal). 4. **In-process core, no HTTP**: build a multi-thread tokio runtime with diff --git a/src/core/cli.rs b/src/core/cli.rs index 9fbb5ac6a..575d62c93 100644 --- a/src/core/cli.rs +++ b/src/core/cli.rs @@ -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. diff --git a/src/core/cli_tests.rs b/src/core/cli_tests.rs index 3d8060b5e..1dd6ae3a3 100644 --- a/src/core/cli_tests.rs +++ b/src/core/cli_tests.rs @@ -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() { diff --git a/src/lib.rs b/src/lib.rs index d1a6d744b..e1e51d65a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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}; diff --git a/src/openhuman/mod.rs b/src/openhuman/mod.rs index a1d153c21..5b30de78b 100644 --- a/src/openhuman/mod.rs +++ b/src/openhuman/mod.rs @@ -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; diff --git a/src/openhuman/tui/mod.rs b/src/openhuman/tui/mod.rs deleted file mode 100644 index c26cc71c5..000000000 --- a/src/openhuman/tui/mod.rs +++ /dev/null @@ -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::*; diff --git a/src/openhuman/tui/stub.rs b/src/openhuman/tui/stub.rs deleted file mode 100644 index 81ff5ee38..000000000 --- a/src/openhuman/tui/stub.rs +++ /dev/null @@ -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`." - ) -} diff --git a/src/openhuman/tui/app.rs b/src/tui/app.rs similarity index 100% rename from src/openhuman/tui/app.rs rename to src/tui/app.rs diff --git a/src/openhuman/tui/controls.rs b/src/tui/controls.rs similarity index 100% rename from src/openhuman/tui/controls.rs rename to src/tui/controls.rs diff --git a/src/tui/mod.rs b/src/tui/mod.rs new file mode 100644 index 000000000..50c6fb599 --- /dev/null +++ b/src/tui/mod.rs @@ -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}; diff --git a/src/openhuman/tui/render.rs b/src/tui/render.rs similarity index 100% rename from src/openhuman/tui/render.rs rename to src/tui/render.rs diff --git a/src/openhuman/tui/runner.rs b/src/tui/runner.rs similarity index 100% rename from src/openhuman/tui/runner.rs rename to src/tui/runner.rs diff --git a/src/openhuman/tui/state.rs b/src/tui/state.rs similarity index 100% rename from src/openhuman/tui/state.rs rename to src/tui/state.rs diff --git a/src/openhuman/tui/terminal.rs b/src/tui/terminal.rs similarity index 100% rename from src/openhuman/tui/terminal.rs rename to src/tui/terminal.rs diff --git a/src/openhuman/tui/ui_state.rs b/src/tui/ui_state.rs similarity index 100% rename from src/openhuman/tui/ui_state.rs rename to src/tui/ui_state.rs