From cd92bc3d7fd76143745fcc67a0d73d4cb1c8e039 Mon Sep 17 00:00:00 2001 From: Bobby Date: Mon, 23 Mar 2026 22:28:14 -0700 Subject: [PATCH] feat: pass shell context (last command + exit code) to forge CLI When using the ZSH shell plugin, forge now receives context about the last command the user ran and its exit code. This lets the AI understand what the user was doing before asking for help (e.g., "fix it" after a failed command). Changes: - Add preexec/precmd ZSH hooks to capture last command and exit code - Add --shell-context CLI flag to receive terminal context - Pass shell context as additional_context on the user's prompt - Add CLI parsing tests for the new flag Fixes #2639 --- crates/forge_main/src/cli.rs | 9 ++++++++ crates/forge_main/src/main.rs | 19 +++++++++++++++++ crates/forge_main/src/ui.rs | 33 ++++++++++++++++++++++++++++++ shell-plugin/forge.plugin.zsh | 3 +++ shell-plugin/lib/helpers.zsh | 4 ++++ shell-plugin/lib/shell_context.zsh | 31 ++++++++++++++++++++++++++++ 6 files changed, 99 insertions(+) create mode 100644 shell-plugin/lib/shell_context.zsh diff --git a/crates/forge_main/src/cli.rs b/crates/forge_main/src/cli.rs index c3f93a1db0..6b28e1d35e 100644 --- a/crates/forge_main/src/cli.rs +++ b/crates/forge_main/src/cli.rs @@ -90,6 +90,15 @@ pub struct Cli { /// Event to dispatch to the workflow in JSON format. #[arg(long, short = 'e')] pub event: Option, + + /// Shell context from the terminal environment. + /// + /// When provided by the shell plugin, this contains information about + /// the last command executed in the terminal and its exit code. This + /// helps the AI understand what the user was doing before asking for + /// help. Format: "command\nexit_code" + #[arg(long)] + pub shell_context: Option, } impl Cli { diff --git a/crates/forge_main/src/main.rs b/crates/forge_main/src/main.rs index c9b14c9d1c..b17a7ad9e0 100644 --- a/crates/forge_main/src/main.rs +++ b/crates/forge_main/src/main.rs @@ -120,6 +120,25 @@ mod tests { assert_eq!(cli_with_flags.restricted, true); } + #[test] + fn test_cli_parsing_shell_context() { + let cli = Cli::parse_from([ + "forge", + "-p", + "fix it", + "--shell-context", + "rm test\n1", + ]); + assert_eq!(cli.prompt, Some("fix it".to_string())); + assert_eq!(cli.shell_context, Some("rm test\n1".to_string())); + } + + #[test] + fn test_cli_shell_context_not_required() { + let cli = Cli::parse_from(["forge", "-p", "hello"]); + assert_eq!(cli.shell_context, None); + } + #[test] fn test_commit_command_diff_field_initially_none() { // Test that the diff field in CommitCommandGroup starts as None diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index ad3231cdf1..7113f73038 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -2955,6 +2955,39 @@ impl A + Send + Sync> UI { event = event.additional_context(piped); } + // Add shell context from the terminal environment if provided. + // The shell plugin captures the last command and its exit code, + // passing them via --shell-context so the AI knows what happened + // before the user's request. + if let Some(ref shell_ctx) = self.cli.shell_context { + let mut parts = shell_ctx.splitn(2, '\n'); + let last_command = parts.next().unwrap_or_default(); + let exit_code = parts.next().unwrap_or_default(); + + if !last_command.is_empty() { + let context_text = if exit_code == "0" { + format!( + "Terminal context — the user just ran this command successfully:\n$ {}", + last_command, + ) + } else { + format!( + "Terminal context — the user just ran this command and it failed (exit code {}):\n$ {}", + exit_code, + last_command, + ) + }; + + // If additional_context is already set (from piped input), + // prepend the shell context to it + if let Some(existing) = event.additional_context.take() { + event = event.additional_context(format!("{}\n\n{}", context_text, existing)); + } else { + event = event.additional_context(context_text); + } + } + } + // Create the chat request with the event let chat = ChatRequest::new(event, conversation_id); diff --git a/shell-plugin/forge.plugin.zsh b/shell-plugin/forge.plugin.zsh index e877afdff7..427c90fde6 100755 --- a/shell-plugin/forge.plugin.zsh +++ b/shell-plugin/forge.plugin.zsh @@ -13,6 +13,9 @@ source "${0:A:h}/lib/highlight.zsh" # Core utilities (includes logging) source "${0:A:h}/lib/helpers.zsh" +# Shell context tracking (preexec/precmd hooks) +source "${0:A:h}/lib/shell_context.zsh" + # Completion widget source "${0:A:h}/lib/completion.zsh" diff --git a/shell-plugin/lib/helpers.zsh b/shell-plugin/lib/helpers.zsh index e0a017282e..a135bf744a 100644 --- a/shell-plugin/lib/helpers.zsh +++ b/shell-plugin/lib/helpers.zsh @@ -40,6 +40,10 @@ function _forge_exec_interactive() { cmd=($_FORGE_BIN --agent "$agent_id") [[ -n "$_FORGE_SESSION_MODEL" ]] && cmd+=(--model "$_FORGE_SESSION_MODEL") [[ -n "$_FORGE_SESSION_PROVIDER" ]] && cmd+=(--provider "$_FORGE_SESSION_PROVIDER") + # Pass shell context (last command + exit code) when available + if [[ -n "$_FORGE_LAST_COMMAND" ]]; then + cmd+=(--shell-context "${_FORGE_LAST_COMMAND}"$'\n'"${_FORGE_LAST_EXIT_CODE}") + fi cmd+=("$@") "${cmd[@]}" /dev/tty } diff --git a/shell-plugin/lib/shell_context.zsh b/shell-plugin/lib/shell_context.zsh new file mode 100644 index 0000000000..50fdb945ec --- /dev/null +++ b/shell-plugin/lib/shell_context.zsh @@ -0,0 +1,31 @@ +#!/usr/bin/env zsh + +# Shell context tracking for forge plugin +# Captures the last command and its exit code so that forge can understand +# what the user was doing before asking for help. +# +# Uses ZSH's preexec/precmd hook arrays so that existing user hooks are +# not overwritten. + +# Variables to store shell context +typeset -g _FORGE_LAST_COMMAND="" +typeset -g _FORGE_LAST_EXIT_CODE="" + +# preexec runs just before a command is executed. +# We capture the command string here. +function _forge_preexec() { + # $1 is the command string as typed by the user + _FORGE_LAST_COMMAND="$1" +} + +# precmd runs just before the prompt is drawn (i.e., after the previous +# command finishes). We capture the exit code here. +function _forge_precmd() { + # $? is the exit code of the command that just finished + _FORGE_LAST_EXIT_CODE="$?" +} + +# Register hooks using ZSH hook arrays (non-destructive) +autoload -Uz add-zsh-hook +add-zsh-hook preexec _forge_preexec +add-zsh-hook precmd _forge_precmd