From e9f09d7fedbc3ab55ed10afca59d1ac9294d89b2 Mon Sep 17 00:00:00 2001 From: TheArchitectit Date: Thu, 11 Jun 2026 10:59:33 -0500 Subject: [PATCH] feat(tui): wire up /tui command with split-pane dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'mod tui;' to main.rs (was missing — TUI code was dead) - Add /tui slash command that switches from the plain REPL to the split-pane TUI mode at runtime - Show startup hint: 'Tip: type /tui for the split-pane dashboard view' - Add run_tui_repl() that manages the TuiApp event loop, captures assistant output from the session, and feeds it to the conversation pane - Add update_dashboard() helper that syncs runtime stats (model, session, turns, provider) into the shared dashboard state - ProviderSwap and TeamToggle work inside TUI mode Co-Authored-By: Claude Fable 5 --- rust/crates/rusty-claude-cli/src/main.rs | 144 +++++++++++++++++++++++ rust/crates/rusty-claude-cli/src/tui.rs | 3 +- 2 files changed, 146 insertions(+), 1 deletion(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 66ba043b..b9dd2efc 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -18,6 +18,7 @@ mod init; mod input; mod render; mod setup_wizard; +mod tui; use std::collections::BTreeSet; use std::env; @@ -7062,6 +7063,7 @@ fn run_repl( input::LineEditor::new("> ", cli.repl_completion_candidates().unwrap_or_default()); println!("{}", cli.startup_banner()); println!("{}", format_connected_line(&cli.model)); + println!("\x1b[2mTip: type /tui for the split-pane dashboard view\x1b[0m"); loop { editor.set_completions(cli.repl_completion_candidates().unwrap_or_default()); @@ -7075,6 +7077,12 @@ fn run_repl( cli.persist_session()?; break; } + // /tui — switch to the split-pane TUI mode + if trimmed == "/tui" { + cli.persist_session()?; + drop(editor); + return run_tui_repl(cli); + } match SlashCommand::parse(&trimmed) { Ok(Some(command)) => { if cli.handle_repl_command(command)? { @@ -7133,6 +7141,142 @@ fn run_repl( Ok(()) } +/// Run the REPL inside the split-pane TUI. Unlike the plain REPL, +/// all output goes into the TUI conversation pane instead of stdout. +fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box> { + let dashboard_state = tui::SharedDashboardState::default(); + { + let mut ds = dashboard_state.write().unwrap(); + ds.model = cli.model.clone(); + ds.permission_mode = format!("{:?}", cli.permission_mode); + if let Some(rt) = cli.runtime.runtime.as_ref() { + ds.session_id = Some(rt.session().session_id.clone()); + } + } + + let mut app = tui::TuiApp::init(dashboard_state.clone())?; + + // Push startup info into conversation pane + app.push_banner(&[tui::BannerLine { text: "🦀 Claw Code".to_string(), color: ratatui::style::Color::Cyan }]); + app.push_banner(&[tui::BannerLine { text: format_connected_line(&cli.model), color: ratatui::style::Color::DarkGray }]); + + loop { + match app.read_line()? { + tui::TuiReadOutcome::Pending => {} + tui::TuiReadOutcome::Submit(input) => { + let trimmed = input.trim().to_string(); + if trimmed.is_empty() { + continue; + } + if matches!(trimmed.as_str(), "/exit" | "/quit") { + app.push_system_message("Bye!"); + cli.persist_session()?; + break; + } + + app.push_user_input(&input); + cli.record_prompt_history(&trimmed); + update_dashboard(&dashboard_state, &cli); + app.set_status("Thinking..."); + + // Run the turn. Output goes to the alternate screen buffer (which + // ratatui owns) — it will be overwritten on next redraw. + // We capture the final assistant text from the session. + let result = cli.run_turn(&trimmed); + + // Read the last assistant message from the session for the conversation pane + { + let messages = &cli.runtime.session().messages; + if let Some(msg) = messages.last() { + if msg.role == runtime::MessageRole::Assistant { + for block in &msg.blocks { + if let runtime::ContentBlock::Text { text } = block { + app.push_output(text, false); + } + } + } + } + } + + match result { + Ok(()) => { + app.set_status("Done"); + if let Ok(mut ds) = dashboard_state.write() { + ds.status_message.clear(); + } + } + Err(e) => { + app.push_system_message(&format!("Error: {e}")); + app.set_status(""); + } + } + update_dashboard(&dashboard_state, &cli); + } + tui::TuiReadOutcome::Cancel => { + // Clear input, stay in TUI + } + tui::TuiReadOutcome::Exit => { + cli.persist_session()?; + break; + } + tui::TuiReadOutcome::ProviderSwap => { + let _ = app.restore_terminal(); + setup_wizard::run_setup_wizard()?; + let cwd = std::env::current_dir().unwrap_or_default(); + let config = runtime::ConfigLoader::default_for(&cwd).load().ok(); + if let Some(new_model) = + config.as_ref().and_then(|c| c.provider().model().map(str::to_string)) + { + let _ = cli.set_model(Some(new_model)); + } + app.push_system_message("Provider updated — restart for full effect"); + // Re-enter TUI + let _ = app.suspend(); + } + tui::TuiReadOutcome::TeamToggle => { + let current = std::env::var("CLAWD_AGENT_TEAMS").unwrap_or_default(); + if current == "1" { + std::env::set_var("CLAWD_AGENT_TEAMS", "0"); + app.push_system_message("[team] Agent teams disabled"); + } else { + std::env::set_var("CLAWD_AGENT_TEAMS", "1"); + app.push_system_message("[team] Agent teams enabled"); + } + } + } + } + + app.restore_terminal()?; + Ok(()) +} + +/// Push current CLI/runtime stats into the shared dashboard state. +fn update_dashboard(state: &tui::SharedDashboardState, cli: &LiveCli) { + if let Ok(mut ds) = state.write() { + ds.model = cli.model.clone(); + ds.permission_mode = format!("{:?}", cli.permission_mode); + if let Some(rt) = cli.runtime.runtime.as_ref() { + let session = rt.session(); + ds.session_id = Some(session.session_id.clone()); + ds.turn_count = session.messages.len() as u32; + // Estimate token count from message text lengths (rough: ~4 chars per token) + let total_chars: usize = session.messages.iter().map(|m| { + m.blocks.iter().map(|b| match b { + runtime::ContentBlock::Text { text } => text.len(), + runtime::ContentBlock::ToolUse { input, .. } => input.to_string().len(), + runtime::ContentBlock::ToolResult { output, .. } => output.len(), + _ => 0, + }).sum::() + }).sum(); + ds.input_tokens = (total_chars / 4) as u32; + } + ds.provider = std::env::var("CLAWD_PROVIDER").unwrap_or_else(|_| "anthropic".to_string()); + ds.provider_url = std::env::var("ANTHROPIC_BASE_URL") + .or_else(|_| std::env::var("OPENAI_BASE_URL")) + .unwrap_or_default(); + } +} + #[derive(Debug, Clone)] struct SessionHandle { id: String, diff --git a/rust/crates/rusty-claude-cli/src/tui.rs b/rust/crates/rusty-claude-cli/src/tui.rs index 76ed9c36..57a77b68 100644 --- a/rust/crates/rusty-claude-cli/src/tui.rs +++ b/rust/crates/rusty-claude-cli/src/tui.rs @@ -714,7 +714,8 @@ fn draw_right_pane( ) { let state = dashboard.read().unwrap_or_else(|e| e.into_inner()); let mut lines: Vec = Vec::new(); - let mut gauge_row: Option = None; // track the gauge's logical row + #[allow(unused_assignments)] + let mut gauge_row: Option = None; lines.push(section("Connection")); lines.push(kv("Model", &state.model, Color::White));