This commit is contained in:
jimmyfraiture
2025-09-24 13:03:14 +01:00
parent d3653f0c54
commit 9ad7e72808
10 changed files with 270 additions and 151 deletions

View File

@@ -109,6 +109,7 @@ use codex_git_tooling::GhostCommit;
use codex_git_tooling::GitToolingError;
use codex_git_tooling::create_ghost_commit;
use codex_git_tooling::restore_ghost_commit;
use codex_utils_readiness::ReadinessFlag;
const MAX_TRACKED_GHOST_COMMITS: usize = 20;
@@ -182,6 +183,7 @@ pub(crate) struct ChatWidgetInit {
pub(crate) struct ChatWidget {
app_event_tx: AppEventSender,
codex_op_tx: UnboundedSender<Op>,
turn_readiness: UnboundedSender<Arc<ReadinessFlag>>,
bottom_pane: BottomPane,
active_exec_cell: Option<ExecCell>,
config: Config,
@@ -772,12 +774,14 @@ impl ChatWidget {
} = common;
let mut rng = rand::rng();
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), conversation_manager);
let agent_channels =
spawn_agent(config.clone(), app_event_tx.clone(), conversation_manager);
Self {
app_event_tx: app_event_tx.clone(),
frame_requester: frame_requester.clone(),
codex_op_tx,
codex_op_tx: agent_channels.op_tx,
turn_readiness: agent_channels.turn_readiness,
bottom_pane: BottomPane::new(BottomPaneParams {
frame_requester,
app_event_tx,
@@ -832,13 +836,14 @@ impl ChatWidget {
let mut rng = rand::rng();
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
let codex_op_tx =
let agent_channels =
spawn_agent_from_existing(conversation, session_configured, app_event_tx.clone());
Self {
app_event_tx: app_event_tx.clone(),
frame_requester: frame_requester.clone(),
codex_op_tx,
codex_op_tx: agent_channels.op_tx,
turn_readiness: agent_channels.turn_readiness,
bottom_pane: BottomPane::new(BottomPaneParams {
frame_requester,
app_event_tx,
@@ -1121,6 +1126,9 @@ impl ChatWidget {
return;
}
let readiness_flag = Arc::new(ReadinessFlag::new());
agent::send_turn_readiness(&self.turn_readiness, Arc::clone(&readiness_flag));
self.capture_ghost_snapshot();
let mut items: Vec<InputItem> = Vec::new();

View File

@@ -5,20 +5,58 @@ use codex_core::ConversationManager;
use codex_core::NewConversation;
use codex_core::config::Config;
use codex_core::protocol::Op;
use codex_utils_readiness::Readiness;
use codex_utils_readiness::ReadinessFlag;
use tokio::sync::mpsc::UnboundedReceiver;
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::mpsc::unbounded_channel;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
pub(crate) struct AgentChannels {
pub(crate) op_tx: UnboundedSender<Op>,
pub(crate) turn_readiness: UnboundedSender<Arc<ReadinessFlag>>,
}
fn mark_ready(flag: Arc<ReadinessFlag>) {
tokio::spawn(async move {
if let Ok(token) = flag.subscribe().await {
let _ = flag.mark_ready(token).await;
}
});
}
fn spawn_readiness_forwarder(
mut rx: UnboundedReceiver<Arc<ReadinessFlag>>,
sender: UnboundedSender<Arc<ReadinessFlag>>,
) {
tokio::spawn(async move {
while let Some(flag) = rx.recv().await {
if sender.send(Arc::clone(&flag)).is_err() {
mark_ready(flag);
}
}
});
}
pub(crate) fn send_turn_readiness(
sender: &UnboundedSender<Arc<ReadinessFlag>>,
flag: Arc<ReadinessFlag>,
) {
if sender.send(Arc::clone(&flag)).is_err() {
mark_ready(flag);
}
}
/// Spawn the agent bootstrapper and op forwarding loop, returning the
/// `UnboundedSender<Op>` used by the UI to submit operations.
/// channels used by the UI to submit operations and register turn readiness.
pub(crate) fn spawn_agent(
config: Config,
app_event_tx: AppEventSender,
server: Arc<ConversationManager>,
) -> UnboundedSender<Op> {
) -> AgentChannels {
let (codex_op_tx, mut codex_op_rx) = unbounded_channel::<Op>();
let (turn_readiness_tx, turn_readiness_rx) = unbounded_channel::<Arc<ReadinessFlag>>();
let app_event_tx_clone = app_event_tx;
tokio::spawn(async move {
@@ -35,6 +73,9 @@ pub(crate) fn spawn_agent(
}
};
let readiness_sender = conversation.turn_readiness_sender();
spawn_readiness_forwarder(turn_readiness_rx, readiness_sender);
// Forward the captured `SessionConfigured` event so it can be rendered in the UI.
let ev = codex_core::protocol::Event {
// The `id` does not matter for rendering, so we can use a fake value.
@@ -58,7 +99,10 @@ pub(crate) fn spawn_agent(
}
});
codex_op_tx
AgentChannels {
op_tx: codex_op_tx,
turn_readiness: turn_readiness_tx,
}
}
/// Spawn agent loops for an existing conversation (e.g., a forked conversation).
@@ -68,8 +112,10 @@ pub(crate) fn spawn_agent_from_existing(
conversation: std::sync::Arc<CodexConversation>,
session_configured: codex_core::protocol::SessionConfiguredEvent,
app_event_tx: AppEventSender,
) -> UnboundedSender<Op> {
) -> AgentChannels {
let (codex_op_tx, mut codex_op_rx) = unbounded_channel::<Op>();
let (turn_readiness_tx, turn_readiness_rx) = unbounded_channel::<Arc<ReadinessFlag>>();
spawn_readiness_forwarder(turn_readiness_rx, conversation.turn_readiness_sender());
let app_event_tx_clone = app_event_tx;
tokio::spawn(async move {
@@ -95,5 +141,8 @@ pub(crate) fn spawn_agent_from_existing(
}
});
codex_op_tx
AgentChannels {
op_tx: codex_op_tx,
turn_readiness: turn_readiness_tx,
}
}

View File

@@ -34,6 +34,7 @@ use codex_core::protocol::StreamErrorEvent;
use codex_core::protocol::TaskCompleteEvent;
use codex_core::protocol::TaskStartedEvent;
use codex_protocol::mcp_protocol::ConversationId;
use codex_utils_readiness::ReadinessFlag;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
@@ -43,6 +44,7 @@ use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::NamedTempFile;
use tokio::sync::mpsc::unbounded_channel;
@@ -310,9 +312,11 @@ fn make_chatwidget_manual() -> (
disable_paste_burst: false,
});
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test"));
let (turn_readiness, _rx) = unbounded_channel::<Arc<ReadinessFlag>>();
let widget = ChatWidget {
app_event_tx,
codex_op_tx: op_tx,
turn_readiness,
bottom_pane: bottom,
active_exec_cell: None,
config: cfg.clone(),