From a535cb56b4f8c4656ac71e6548c6bc10ffc432c7 Mon Sep 17 00:00:00 2001 From: TheArchitectit Date: Mon, 15 Jun 2026 12:44:56 -0500 Subject: [PATCH] fix(tui): populate conversation from session, add /menu, fix exit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Populate the TUI conversation pane from existing session messages when entering via /tui — no more empty TUI - Add /menu command to return from TUI to the plain REPL - Add run_repl_from_cli() to resume the REPL with the same LiveCli - Ctrl+D and /exit both break the loop and exit cleanly - Ctrl+C clears input and stays in TUI - Handle MessageRole::Tool in the populate loop --- rust/crates/rusty-claude-cli/src/main.rs | 129 ++++++++++++++++++++++- 1 file changed, 126 insertions(+), 3 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 86c4ec34..1d7eac45 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -7150,6 +7150,89 @@ fn run_repl( Ok(()) } +/// Run the plain REPL starting from an existing LiveCli (e.g. after /menu +/// exits the TUI). This is the same loop as `run_repl` but takes ownership +/// of a pre-existing `LiveCli` instead of creating one from CLI args. +fn run_repl_from_cli(mut cli: LiveCli) -> Result<(), Box> { + let mut editor = + input::LineEditor::new("> ", cli.repl_completion_candidates().unwrap_or_default()); + println!(); + 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()); + match editor.read_line()? { + input::ReadOutcome::Submit(input) => { + let trimmed = input.trim().to_string(); + if trimmed.is_empty() { + continue; + } + if matches!(trimmed.as_str(), "/exit" | "/quit") { + cli.persist_session()?; + break; + } + 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)? { + cli.persist_session()?; + } + continue; + } + Ok(None) => {} + Err(error) => { + eprintln!("{error}"); + continue; + } + } + let cwd = std::env::current_dir().unwrap_or_default(); + if let Some(prompt) = try_resolve_bare_skill_prompt(&cwd, &trimmed) { + editor.push_history(input); + cli.record_prompt_history(&trimmed); + cli.run_turn(&prompt)?; + continue; + } + editor.push_history(input); + cli.record_prompt_history(&trimmed); + cli.run_turn(&trimmed)?; + } + input::ReadOutcome::Cancel => {} + input::ReadOutcome::Exit => { + cli.persist_session()?; + break; + } + input::ReadOutcome::ProviderSwap => { + let _ = 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)); + } + } + input::ReadOutcome::TeamToggle => { + let current = std::env::var("CLAWD_AGENT_TEAMS").unwrap_or_default(); + if current == "1" { + std::env::set_var("CLAWD_AGENT_TEAMS", "0"); + eprintln!("[team] Agent teams disabled"); + } else { + std::env::set_var("CLAWD_AGENT_TEAMS", "1"); + eprintln!("[team] Agent teams enabled"); + } + } + } + } + + 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> { @@ -7177,6 +7260,39 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box> { color: ratatui::style::Color::DarkGray, }]); + // Populate conversation from existing session history so the + // TUI shows messages from the REPL session that preceded /tui. + { + let session = cli.runtime.session(); + for msg in &session.messages { + match msg.role { + runtime::MessageRole::User => { + for block in &msg.blocks { + if let runtime::ContentBlock::Text { text } = block { + app.push_user_input(text); + } + } + } + runtime::MessageRole::Assistant => { + for block in &msg.blocks { + if let runtime::ContentBlock::Text { text } = block { + app.push_output(text, false); + } + } + } + runtime::MessageRole::System => { + for block in &msg.blocks { + if let runtime::ContentBlock::Text { text } = block { + app.push_system_message(text); + } + } + } + // Skip tool messages — they're part of the assistant turn + runtime::MessageRole::Tool => {} + } + } + } + loop { match app.read_line()? { tui::legacy::TuiReadOutcome::Pending => {} @@ -7196,13 +7312,19 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box> { // because they modify TuiApp state directly. Everything else // goes through the REPL handler with output captured. if trimmed.starts_with('/') { - // TUI-specific: /exit and /quit + // TUI-specific: /exit and /quit — leave the TUI and exit if trimmed == "/exit" || trimmed == "/quit" { - app.push_system_message("Bye!"); cli.persist_session()?; break; } + // TUI-specific: /menu — go back to the plain REPL + if trimmed == "/menu" { + cli.persist_session()?; + let _ = app.restore_terminal(); + return run_repl_from_cli(cli); + } + // TUI-specific: /theme — modifies TuiApp theme state if let Some(rest) = trimmed.strip_prefix("/theme") { let name = rest.trim(); @@ -7324,9 +7446,10 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box> { let _ = app.redraw_after_turn(); } tui::legacy::TuiReadOutcome::Cancel => { - // Clear input, stay in TUI + // Ctrl+C clears input — stay in TUI } tui::legacy::TuiReadOutcome::Exit => { + // Ctrl+D or /exit — exit the application cli.persist_session()?; break; }