From eaaeb8da69d3090bc7d0a658a88c21808880b016 Mon Sep 17 00:00:00 2001 From: Shahmeer Abdullah Date: Thu, 12 Nov 2026 16:27:07 +0500 Subject: [PATCH] [#208] /log and /fetch: give the model their output The /log and /fetch commands returned plain output, so the model never saw it, unlike /diff, /show and /grep. Return it as LLM context too, with the terminal colour codes stripped. --- AUTHORS | 1 + doc/manual/en/97-acknowledgement.md | 1 + src/bin/orangu/dispatch.rs | 69 ++++++++++++++++++++++++++++- src/bin/orangu/export.rs | 2 +- 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 84869658..f3d5ee4b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -10,3 +10,4 @@ Bassam Adnan Saleh Enab Tejas Tyagi abishekcs +Shahmeer Abdullah diff --git a/doc/manual/en/97-acknowledgement.md b/doc/manual/en/97-acknowledgement.md index 5f12b67b..cb9770eb 100644 --- a/doc/manual/en/97-acknowledgement.md +++ b/doc/manual/en/97-acknowledgement.md @@ -16,6 +16,7 @@ Adesh Deshmukh Bassam Adnan Saleh Enab Tejas Tyagi +Shahmeer Abdullah ``` ## Committers diff --git a/src/bin/orangu/dispatch.rs b/src/bin/orangu/dispatch.rs index 8def9036..4a1fee2c 100644 --- a/src/bin/orangu/dispatch.rs +++ b/src/bin/orangu/dispatch.rs @@ -29,6 +29,16 @@ pub(crate) fn local_command_error(err: Error) -> CommandOutcome { } } +/// The context message the model gets for a local command whose output is +/// plain text, such as `/log` and `/fetch`, so it sees what the user saw. +/// Colour codes are dropped: they are for the terminal, not the model. +fn plain_output_llm_context(command: &str, output: &str) -> String { + format!( + "The user executed `{command}`. Output:\n\n```\n{}\n```", + crate::export::strip_ansi(output) + ) +} + /// `/create_file [with ] [containing ]`. /// /// A path that already exists is overwritten — the same on every surface @@ -2240,7 +2250,17 @@ pub(crate) fn handle_command( )) } LocalCommand::Log(count) => match log_output(workspace, count) { - Ok(output) => Ok(CommandOutcome::Output(output)), + Ok(output) => { + let command = match count { + Some(count) => format!("/log {count}"), + None => "/log".to_string(), + }; + let llm_context = plain_output_llm_context(&command, &output); + Ok(CommandOutcome::OutputWithLlmContext { + display: output, + llm_context, + }) + } Err(err) => Ok(local_command_error(err)), }, LocalCommand::Show(commit) => match show_output(workspace, commit.as_deref()) { @@ -2277,7 +2297,17 @@ pub(crate) fn handle_command( Err(err) => Ok(local_command_error(err)), }, LocalCommand::Fetch(remote) => match fetch_output(workspace, remote.as_deref(), forge) { - Ok(output) => Ok(CommandOutcome::Output(output)), + Ok(output) => { + let command = match remote.as_deref() { + Some(remote) => format!("/fetch {remote}"), + None => "/fetch".to_string(), + }; + let llm_context = plain_output_llm_context(&command, &output); + Ok(CommandOutcome::OutputWithLlmContext { + display: output, + llm_context, + }) + } Err(err) => Ok(local_command_error(err)), }, LocalCommand::AddRepository(None) => Ok(CommandOutcome::OutputError( @@ -3022,6 +3052,41 @@ mod tests { use std::fs; use tempfile::tempdir; + #[test] + fn plain_output_llm_context_names_the_command_and_drops_colour() { + let context = plain_output_llm_context( + "/log 2", + "* \x1b[33mabc1234\x1b[m (\x1b[1;36mHEAD\x1b[m) fix it\n* def5678 add it", + ); + assert_eq!( + context, + "The user executed `/log 2`. Output:\n\n```\n* abc1234 (HEAD) fix it\n* def5678 add it\n```" + ); + } + + #[test] + fn log_and_fetch_give_the_model_their_output() { + let workspace = tempdir().expect("workspace"); + let root = workspace.path(); + let git = |args: &[&str]| { + let status = std::process::Command::new("git") + .arg("-C") + .arg(root) + .args(["-c", "user.name=t", "-c", "user.email=t@example.com"]) + .args(args) + .status() + .expect("git runs"); + assert!(status.success(), "git {args:?}"); + }; + git(&["init", "-q"]); + git(&["commit", "-q", "--allow-empty", "-m", "first commit"]); + + let log = log_output(root, Some(1)).expect("log output"); + let context = plain_output_llm_context("/log 1", &log); + assert!(context.contains("first commit"), "{context}"); + assert!(!context.contains('\x1b'), "{context}"); + } + #[test] fn rewind_lists_turns_with_their_age_and_files() { let turns = vec![orangu::checkpoints::TurnSummary { diff --git a/src/bin/orangu/export.rs b/src/bin/orangu/export.rs index 960b1bef..2558e39d 100644 --- a/src/bin/orangu/export.rs +++ b/src/bin/orangu/export.rs @@ -4230,7 +4230,7 @@ fn emit_text( /// Remove ANSI escape sequences (CSI, plus the `ESC O x` cursor keys) so the /// console export shows the plain text the terminal rendered. -fn strip_ansi(line: &str) -> String { +pub(crate) fn strip_ansi(line: &str) -> String { let mut out = String::with_capacity(line.len()); let mut chars = line.chars().peekable(); 'outer: while let Some(ch) = chars.next() {