feat(tui/gh): live-update gh_run_wait progress

This commit is contained in:
James Peter
2026-01-29 23:48:13 +10:00
parent 999fd1d2a1
commit 76d56c6357
8 changed files with 119 additions and 4 deletions
+34 -2
View File
@@ -4878,6 +4878,7 @@ async fn handle_gh_run_wait(
use std::path::Path;
use std::time::Duration;
use chrono::{DateTime, Utc};
use crate::protocol::CustomToolCallUpdateEvent;
#[derive(Deserialize, Clone)]
struct Params {
@@ -4992,14 +4993,14 @@ async fn handle_gh_run_wait(
let cwd = sess.cwd.clone();
#[derive(Clone, Default)]
#[derive(Clone, Default, PartialEq, Eq)]
struct JobFailure {
name: String,
conclusion: String,
step: Option<String>,
}
#[derive(Clone, Default)]
#[derive(Clone, Default, PartialEq, Eq)]
struct JobSummary {
total: usize,
completed: usize,
@@ -5015,6 +5016,12 @@ async fn handle_gh_run_wait(
failed_jobs: Vec<JobFailure>,
}
#[derive(Clone, PartialEq, Eq)]
struct UpdateSnapshot {
jobs: JobSummary,
url: Option<String>,
}
impl JobSummary {
fn to_json(&self) -> Value {
serde_json::json!({
@@ -5459,6 +5466,7 @@ async fn handle_gh_run_wait(
let interval = parsed.interval_seconds.unwrap_or(8).max(1);
let (initial_wait_epoch, _) = sess.wait_interrupt_snapshot();
let mut last_view = prepared_view.clone();
let mut last_update: Option<UpdateSnapshot> = None;
loop {
let view = if let Some(cached) = last_view.take() {
@@ -5547,6 +5555,30 @@ async fn handle_gh_run_wait(
};
}
let update_url = html_url.clone().or_else(|| prepared_url.clone());
if job_summary.total > 0 || update_url.is_some() {
let snapshot = UpdateSnapshot {
jobs: job_summary.clone(),
url: update_url.clone(),
};
if last_update.as_ref() != Some(&snapshot) {
last_update = Some(snapshot.clone());
let mut update_params = serde_json::Map::new();
update_params.insert("jobs".to_string(), snapshot.jobs.to_json());
if let Some(url) = snapshot.url.clone() {
update_params.insert("url".to_string(), Value::String(url));
}
let update_msg = EventMsg::CustomToolCallUpdate(CustomToolCallUpdateEvent {
call_id: call_id.clone(),
tool_name: "gh_run_wait".to_string(),
parameters: Some(Value::Object(update_params)),
});
let order = sess.background_order_for_ctx(ctx, sess.current_request_ordinal());
let event = sess.make_event_with_order(&ctx.sub_id, update_msg, order, ctx.seq_hint);
sess.send_event(event).await;
}
}
let time_budget_message = {
let mut guard = sess.time_budget.lock().unwrap();
guard
+11
View File
@@ -808,6 +808,7 @@ pub enum EventMsg {
/// Custom tool call events for non-MCP tools (browser, agent, etc)
CustomToolCallBegin(CustomToolCallBeginEvent),
CustomToolCallUpdate(CustomToolCallUpdateEvent),
CustomToolCallEnd(CustomToolCallEndEvent),
/// Notification that the server is about to execute a command.
@@ -1165,6 +1166,16 @@ pub struct CustomToolCallBeginEvent {
pub parameters: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CustomToolCallUpdateEvent {
/// Identifier for the corresponding CustomToolCallBegin that is still running.
pub call_id: String,
/// Name of the tool
pub tool_name: String,
/// Parameters passed to the tool as JSON
pub parameters: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CustomToolCallEndEvent {
/// Identifier for the corresponding CustomToolCallBegin that finished.
@@ -638,6 +638,9 @@ impl EventProcessor for EventProcessorWithHumanOutput {
}
}
}
EventMsg::CustomToolCallUpdate(_) => {
// Currently ignored in exec output.
}
EventMsg::CustomToolCallEnd(event) => {
let status = if event.result.is_ok() {
"success".style(self.green)
@@ -340,6 +340,7 @@ async fn run_code_tool_session_inner(
| EventMsg::EnteredReviewMode(_)
| EventMsg::ExitedReviewMode(_)
| EventMsg::CustomToolCallBegin(_)
| EventMsg::CustomToolCallUpdate(_)
| EventMsg::CustomToolCallEnd(_)
| EventMsg::ViewImageToolCall(_) => {
// For now, we do not do anything extra for these
@@ -112,6 +112,7 @@ pub async fn run_conversation_loop(
| EventMsg::AgentStatusUpdate(_)
| EventMsg::ShutdownComplete
| EventMsg::CustomToolCallBegin(_)
| EventMsg::CustomToolCallUpdate(_)
| EventMsg::CustomToolCallEnd(_) => {
// For now, we do not do anything extra for these
// events. Note that
+66
View File
@@ -160,6 +160,7 @@ use code_core::protocol::BrowserScreenshotUpdateEvent;
use code_core::protocol::BrowserSnapshotEvent;
use code_core::protocol::CustomToolCallBeginEvent;
use code_core::protocol::CustomToolCallEndEvent;
use code_core::protocol::CustomToolCallUpdateEvent;
use code_core::protocol::ErrorEvent;
use code_core::protocol::Event;
use code_core::protocol::EventMsg;
@@ -990,6 +991,7 @@ use crate::history::state::{
RateLimitsRecord,
TextTone,
TextEmphasis,
ToolArgument,
ToolStatus,
};
use crate::cloud_tasks_service::CloudEnvironment;
@@ -10020,6 +10022,63 @@ impl ChatWidget<'_> {
}
}
fn merge_tool_arguments(existing: &mut Vec<ToolArgument>, updates: Vec<ToolArgument>) {
for update in updates {
if let Some(existing_arg) = existing.iter_mut().find(|arg| arg.name == update.name) {
*existing_arg = update;
} else {
existing.push(update);
}
}
}
fn apply_custom_tool_update(
&mut self,
call_id: &str,
parameters: Option<serde_json::Value>,
) {
let Some(params) = parameters else {
return;
};
let updates = history_cell::arguments_from_json(&params);
if updates.is_empty() {
return;
}
let running_entry = self
.tools_state
.running_custom_tools
.get(&ToolCallId(call_id.to_string()))
.copied();
let resolved_idx = running_entry
.as_ref()
.and_then(|entry| running_tools::resolve_entry_index(self, entry, call_id))
.or_else(|| running_tools::find_by_call_id(self, call_id));
let Some(idx) = resolved_idx else {
return;
};
if idx >= self.history_cells.len() {
return;
}
let Some(running_cell) = self.history_cells[idx]
.as_any()
.downcast_ref::<history_cell::RunningToolCallCell>()
else {
return;
};
let mut state = running_cell.state().clone();
Self::merge_tool_arguments(&mut state.arguments, updates);
let mut updated_cell = history_cell::RunningToolCallCell::from_state(state.clone());
updated_cell.state_mut().call_id = Some(call_id.to_string());
self.history_replace_with_record(
idx,
Box::new(updated_cell),
HistoryDomainRecord::from(state),
);
}
fn hydrate_cell_from_record(
&self,
cell: &mut Box<dyn HistoryCell>,
@@ -14396,6 +14455,13 @@ impl ChatWidget<'_> {
.update_status_text(format!("using tool: {}", tool_name));
}
}
EventMsg::CustomToolCallUpdate(CustomToolCallUpdateEvent {
call_id,
tool_name: _,
parameters,
}) => {
self.apply_custom_tool_update(&call_id, parameters);
}
EventMsg::CustomToolCallEnd(CustomToolCallEndEvent {
call_id,
tool_name,
+1
View File
@@ -128,6 +128,7 @@ pub(crate) use registry::{cell_from_record, lines_from_record, record_from_cell}
pub(crate) use stream::{new_streaming_content, stream_lines_from_state, StreamingContentCell};
pub(crate) use tool::{RunningToolCallCell, ToolCallCell};
pub(crate) use tool_factory::{
arguments_from_json,
new_completed_custom_tool_call,
new_completed_mcp_tool_call,
new_completed_web_fetch_tool_call,
+2 -2
View File
@@ -114,11 +114,11 @@ fn argument_value_from_json(value: &serde_json::Value) -> ArgumentValue {
}
}
fn arguments_from_json(value: &serde_json::Value) -> Vec<ToolArgument> {
pub(crate) fn arguments_from_json(value: &serde_json::Value) -> Vec<ToolArgument> {
arguments_from_json_excluding(value, &[])
}
fn arguments_from_json_excluding(
pub(crate) fn arguments_from_json_excluding(
value: &serde_json::Value,
exclude: &[&str],
) -> Vec<ToolArgument> {