mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
refactor(tui): move terminal UI to crate root (#5236)
This commit is contained in:
+6
-7
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+17
-6
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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::*;
|
||||
@@ -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`."
|
||||
)
|
||||
}
|
||||
@@ -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};
|
||||
Reference in New Issue
Block a user