Wire the PatchUpdated events through app_server (#18289)

Wires patch_updated events through app_server. These events are parsed
and streamed while apply_patch is being written by the model. Also adds 500ms of buffering to the patch_updated events in the diff_consumer.

The eventual goal is to use this to display better progress indicators in
the codex app.
This commit is contained in:
Akshay Nathan
2026-04-20 10:44:03 -07:00
committed by GitHub
parent 316cf0e90b
commit 34a3e85fcd
18 changed files with 613 additions and 6 deletions

View File

@@ -3,6 +3,8 @@ use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use crate::apply_patch;
use crate::apply_patch::InternalApplyPatchInvocation;
@@ -44,12 +46,16 @@ use codex_sandboxing::policy_transforms::normalize_additional_permissions;
use codex_tools::ApplyPatchToolArgs;
use codex_utils_absolute_path::AbsolutePathBuf;
const APPLY_PATCH_ARGUMENT_DIFF_BUFFER_INTERVAL: Duration = Duration::from_millis(500);
pub struct ApplyPatchHandler;
#[derive(Default)]
struct ApplyPatchArgumentDiffConsumer {
input: String,
last_progress: Option<Vec<Hunk>>,
last_sent_at: Option<Instant>,
pending: Option<PatchApplyUpdatedEvent>,
}
impl ToolArgumentDiffConsumer for ApplyPatchArgumentDiffConsumer {
@@ -66,6 +72,11 @@ impl ToolArgumentDiffConsumer for ApplyPatchArgumentDiffConsumer {
self.push_delta(call_id, diff)
.map(EventMsg::PatchApplyUpdated)
}
fn flush_on_complete(&mut self) -> Option<EventMsg> {
self.flush_update_on_complete()
.map(EventMsg::PatchApplyUpdated)
}
}
impl ApplyPatchArgumentDiffConsumer {
@@ -82,7 +93,29 @@ impl ApplyPatchArgumentDiffConsumer {
let changes = convert_apply_patch_hunks_to_protocol(&hunks);
self.last_progress = Some(hunks);
Some(PatchApplyUpdatedEvent { call_id, changes })
let event = PatchApplyUpdatedEvent { call_id, changes };
let now = Instant::now();
match self.last_sent_at {
Some(last_sent_at)
if now.duration_since(last_sent_at) < APPLY_PATCH_ARGUMENT_DIFF_BUFFER_INTERVAL =>
{
self.pending = Some(event);
None
}
Some(_) | None => {
self.pending = None;
self.last_sent_at = Some(now);
Some(event)
}
}
}
fn flush_update_on_complete(&mut self) -> Option<PatchApplyUpdatedEvent> {
let event = self.pending.take();
if event.is_some() {
self.last_sent_at = Some(Instant::now());
}
event
}
}

View File

@@ -54,9 +54,13 @@ fn diff_consumer_streams_apply_patch_changes() {
)
);
let event = consumer
.push_delta("call-1".to_string(), "\n+world")
.expect("progress event");
assert!(
consumer
.push_delta("call-1".to_string(), "\n+world")
.is_none()
);
let event = consumer.flush_update_on_complete().expect("progress event");
assert_eq!(
(event.call_id, event.changes),
(
@@ -71,6 +75,39 @@ fn diff_consumer_streams_apply_patch_changes() {
);
}
#[test]
fn diff_consumer_sends_next_update_after_buffer_interval() {
let mut consumer = ApplyPatchArgumentDiffConsumer::default();
consumer.push_delta("call-1".to_string(), "*** Begin Patch\n");
let first = consumer
.push_delta("call-1".to_string(), "*** Add File: hello.txt\n+hello")
.expect("first progress event");
assert_eq!(
first.changes,
HashMap::from([(
PathBuf::from("hello.txt"),
FileChange::Add {
content: "hello\n".to_string(),
},
)])
);
consumer.last_sent_at =
Some(std::time::Instant::now() - APPLY_PATCH_ARGUMENT_DIFF_BUFFER_INTERVAL);
let second = consumer
.push_delta("call-1".to_string(), "\n+world")
.expect("second progress event");
assert_eq!(
second.changes,
HashMap::from([(
PathBuf::from("hello.txt"),
FileChange::Add {
content: "hello\nworld\n".to_string(),
},
)])
);
}
#[tokio::test]
async fn approval_keys_include_move_destination() {
let tmp = TempDir::new().expect("tmp");

View File

@@ -95,6 +95,11 @@ pub(crate) trait ToolArgumentDiffConsumer: Send {
/// Consume the next argument diff for a tool call.
fn consume_diff(&mut self, turn: &TurnContext, call_id: String, diff: &str)
-> Option<EventMsg>;
/// Flush any buffered event before the tool call completes.
fn flush_on_complete(&mut self) -> Option<EventMsg> {
None
}
}
pub(crate) struct AnyToolResult {