fix(tui): populate conversation from session, add /menu, fix exit
- 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
This commit is contained in:
parent
d104f90c44
commit
a535cb56b4
|
|
@ -7150,6 +7150,89 @@ fn run_repl(
|
||||||
Ok(())
|
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<dyn std::error::Error>> {
|
||||||
|
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,
|
/// Run the REPL inside the split-pane TUI. Unlike the plain REPL,
|
||||||
/// all output goes into the TUI conversation pane instead of stdout.
|
/// all output goes into the TUI conversation pane instead of stdout.
|
||||||
fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
@ -7177,6 +7260,39 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
color: ratatui::style::Color::DarkGray,
|
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 {
|
loop {
|
||||||
match app.read_line()? {
|
match app.read_line()? {
|
||||||
tui::legacy::TuiReadOutcome::Pending => {}
|
tui::legacy::TuiReadOutcome::Pending => {}
|
||||||
|
|
@ -7196,13 +7312,19 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// because they modify TuiApp state directly. Everything else
|
// because they modify TuiApp state directly. Everything else
|
||||||
// goes through the REPL handler with output captured.
|
// goes through the REPL handler with output captured.
|
||||||
if trimmed.starts_with('/') {
|
if trimmed.starts_with('/') {
|
||||||
// TUI-specific: /exit and /quit
|
// TUI-specific: /exit and /quit — leave the TUI and exit
|
||||||
if trimmed == "/exit" || trimmed == "/quit" {
|
if trimmed == "/exit" || trimmed == "/quit" {
|
||||||
app.push_system_message("Bye!");
|
|
||||||
cli.persist_session()?;
|
cli.persist_session()?;
|
||||||
break;
|
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
|
// TUI-specific: /theme — modifies TuiApp theme state
|
||||||
if let Some(rest) = trimmed.strip_prefix("/theme") {
|
if let Some(rest) = trimmed.strip_prefix("/theme") {
|
||||||
let name = rest.trim();
|
let name = rest.trim();
|
||||||
|
|
@ -7324,9 +7446,10 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let _ = app.redraw_after_turn();
|
let _ = app.redraw_after_turn();
|
||||||
}
|
}
|
||||||
tui::legacy::TuiReadOutcome::Cancel => {
|
tui::legacy::TuiReadOutcome::Cancel => {
|
||||||
// Clear input, stay in TUI
|
// Ctrl+C clears input — stay in TUI
|
||||||
}
|
}
|
||||||
tui::legacy::TuiReadOutcome::Exit => {
|
tui::legacy::TuiReadOutcome::Exit => {
|
||||||
|
// Ctrl+D or /exit — exit the application
|
||||||
cli.persist_session()?;
|
cli.persist_session()?;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue