Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/forge_app/src/agent_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ impl<S: Services + EnvironmentInfra<Config = forge_config::ForgeConfig>> AgentEx
.await?;
conversation
};
// Execute the request through the ForgeApp
// Execute the request through the ForgeApp, propagating silent mode.
let app = crate::ForgeApp::new(self.services.clone());
let mut response_stream = app
.chat(
agent_id.clone(),
ChatRequest::new(Event::new(task.clone()), conversation.id),
ChatRequest::new(Event::new(task.clone()), conversation.id).tool_silent(ctx.silent),
)
.await?;

Expand Down
3 changes: 2 additions & 1 deletion crates/forge_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ impl<S: Services + EnvironmentInfra<Config = forge_config::ForgeConfig>> ForgeAp
.error_tracker(ToolErrorTracker::new(max_tool_failure_per_turn))
.tool_definitions(tool_definitions)
.models(models)
.hook(Arc::new(hook));
.hook(Arc::new(hook))
.tool_silent(chat.tool_silent);

// Create and return the stream
let stream = MpscStream::spawn(
Expand Down
15 changes: 13 additions & 2 deletions crates/forge_app/src/orch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ pub struct Orchestrator<S> {
error_tracker: ToolErrorTracker,
hook: Arc<Hook>,
config: forge_config::ForgeConfig,
/// When `true`, shell tool output is suppressed on stdout to avoid
/// contaminating the ACP JSON-RPC transport.
///
/// Set from [`ChatRequest::tool_silent`] during `ForgeApp::chat()`. This field
/// is consumed in [`Orchestrator::run()`] when constructing the
/// [`ToolCallContext`].
tool_silent: bool,
}

impl<S: AgentService + EnvironmentInfra<Config = forge_config::ForgeConfig>> Orchestrator<S> {
Expand All @@ -45,6 +52,7 @@ impl<S: AgentService + EnvironmentInfra<Config = forge_config::ForgeConfig>> Orc
models: Default::default(),
error_tracker: Default::default(),
hook: Arc::new(Hook::default()),
tool_silent: false,
}
}

Expand Down Expand Up @@ -262,8 +270,11 @@ impl<S: AgentService + EnvironmentInfra<Config = forge_config::ForgeConfig>> Orc

// Retrieve the number of requests allowed per tick.
let max_requests_per_turn = self.agent.max_requests_per_turn;
let tool_context =
ToolCallContext::new(self.conversation.metrics.clone()).sender(self.sender.clone());
// Propagate silent mode to ToolCallContext.
// See designs/acp-silent-mode-propagation.md.
let tool_context = ToolCallContext::new(self.conversation.metrics.clone())
.sender(self.sender.clone())
.silent(self.tool_silent);

while !should_yield {
// Set context for the current loop iteration
Expand Down
2 changes: 1 addition & 1 deletion crates/forge_app/src/tool_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ impl<
input.command.clone(),
PathBuf::from(normalized_cwd),
input.keep_ansi,
false,
context.silent,
input.env.clone(),
input.description.clone(),
)
Expand Down
6 changes: 5 additions & 1 deletion crates/forge_domain/src/chat_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ use crate::{ConversationId, Event};
pub struct ChatRequest {
pub event: Event,
pub conversation_id: ConversationId,
/// When `true`, shell tool output is suppressed on stdout (routed to
/// `io::sink()`) to protect the ACP JSON-RPC transport.
/// See `designs/acp-silent-mode-propagation.md`.
pub tool_silent: bool,
}

impl ChatRequest {
pub fn new(content: Event, conversation_id: ConversationId) -> Self {
Self { event: content, conversation_id }
Self { event: content, conversation_id, tool_silent: false }
}
}
11 changes: 10 additions & 1 deletion crates/forge_domain/src/tools/call/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@ use crate::{ArcSender, ChatResponse, Metrics, TitleFormat, Todo, TodoItem};
pub struct ToolCallContext {
sender: Option<ArcSender>,
metrics: Arc<Mutex<Metrics>>,
/// When set to true, shell tool output is streamed to io::sink() instead
/// of io::stdout() to avoid contaminating the ACP JSON-RPC transport
/// channel. The field is intentionally NOT in ForgeConfig — it is a
/// per-invocation runtime behaviour flag, not persisted state.
pub silent: bool,
}

impl ToolCallContext {
/// Creates a new ToolCallContext with default values
pub fn new(metrics: Metrics) -> Self {
Self { sender: None, metrics: Arc::new(Mutex::new(metrics)) }
Self {
sender: None,
metrics: Arc::new(Mutex::new(metrics)),
silent: false,
}
}

/// Send a message through the sender if available
Expand Down
3 changes: 2 additions & 1 deletion crates/forge_infra/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ impl ForgeCommandExecutorService {
let mut stdout_pipe = child.stdout.take();
let mut stderr_pipe = child.stderr.take();

// Stream the output of the command to stdout and stderr concurrently
// Suppress stdout in headless mode to avoid contaminating the JSON-RPC
// transport.
let (status, stdout_buffer, stderr_buffer) = if silent {
tokio::try_join!(
child.wait(),
Expand Down
9 changes: 9 additions & 0 deletions crates/forge_main/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ impl Cli {
pub fn is_interactive(&self) -> bool {
self.prompt.is_none() && self.piped_input.is_none() && self.subcommands.is_none()
}

/// Checks if the subcommand owns stdin and should skip the piped-input
/// pre-read.
///
/// Commands like `forge select` interactively read from stdin and would
/// hang if the startup pipeline consumed stdin first.
pub fn uses_stdin(&self) -> bool {
matches!(&self.subcommands, Some(TopLevelCommand::Select(_)))
}
}

#[derive(Subcommand, Debug, Clone)]
Expand Down
2 changes: 1 addition & 1 deletion crates/forge_main/src/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl Section {
/// # Output Format
///
/// ```text
///
///
/// CONFIGURATION
/// model gpt-4
/// provider openai
Expand Down
11 changes: 5 additions & 6 deletions crates/forge_main/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use clap::Parser;
use forge_api::ForgeAPI;
use forge_config::ForgeConfig;
use forge_domain::TitleFormat;
use forge_main::{Cli, Sandbox, TitleDisplayExt, TopLevelCommand, UI, tracker};
use forge_main::{Cli, Sandbox, TitleDisplayExt, UI, tracker};

/// Enables ENABLE_VIRTUAL_TERMINAL_PROCESSING on the stdout console handle.
///
Expand Down Expand Up @@ -82,18 +82,17 @@ async fn run() -> Result<()> {
"Unexpected error occurred".to_string()
};

println!("{}", TitleFormat::error(message.to_string()).display());
eprintln!("{}", TitleFormat::error(message.to_string()).display());
tracker::error_blocking(message);
std::process::exit(1);
}));

// Initialize and run the UI
let mut cli = Cli::parse();

// Check if there's piped input, but skip for `forge select` since that
// command uses stdin for its item list.
let is_select = matches!(cli.subcommands, Some(TopLevelCommand::Select(_)));
if !is_select && !std::io::stdin().is_terminal() {
// Check if there's piped input, but skip for commands that use stdin
// for their own protocol (e.g. `forge select`).
if !cli.uses_stdin() && !std::io::stdin().is_terminal() {
let mut stdin_content = String::new();
std::io::stdin().read_to_string(&mut stdin_content)?;
let trimmed_content = stdin_content.trim();
Expand Down
4 changes: 2 additions & 2 deletions crates/forge_main/src/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl<'a> Sandbox<'a> {
.context("Failed to check if target directory is a git worktree")?;

if worktree_check.status.success() {
println!(
eprintln!(
"{}",
TitleFormat::info("Worktree [Reused]")
.sub_title(worktree_path.display().to_string())
Expand Down Expand Up @@ -125,7 +125,7 @@ impl<'a> Sandbox<'a> {
bail!("Failed to create git worktree: {stderr}");
}

println!(
eprintln!(
"{}",
TitleFormat::info("Worktree [Created]")
.sub_title(worktree_path.display().to_string())
Expand Down