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
1 change: 1 addition & 0 deletions codex-rs/app-server/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ load("//:defs.bzl", "codex_rust_crate")

codex_rust_crate(
name = "app-server",
compile_data = ["src/spine_ui/tree.html"],
crate_name = "codex_app_server",
extra_binaries = [
"//codex-rs/bwrap:bwrap",
Expand Down
74 changes: 73 additions & 1 deletion codex-rs/app-server/src/bespoke_event_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,24 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
EventMsg::SpineTreeUpdate(spine_tree_event) => {
if crate::spine_ui::is_enabled() {
let live_spine_ui = {
let mut state = thread_state.lock().await;
state.record_spine_ui_snapshot(spine_tree_event.clone());
state.live_spine_ui(&event_turn_id).cloned()
};
if let Some(notification) = live_spine_ui.as_ref().and_then(|spine_ui| {
crate::spine_ui::snapshot_started_notification(
&conversation_id.to_string(),
&event_turn_id,
spine_ui,
)
}) {
outgoing
.send_server_notification(ServerNotification::ItemStarted(notification))
.await;
}
}
let notification = item_event_to_server_notification(
EventMsg::SpineTreeUpdate(spine_tree_event),
&conversation_id.to_string(),
Expand All @@ -1161,6 +1179,24 @@ pub(crate) async fn apply_bespoke_event_handling(
outgoing.send_server_notification(notification).await;
}
EventMsg::SpineSpawnProgress(progress) => {
if crate::spine_ui::is_enabled() {
let live_spine_ui = {
let mut state = thread_state.lock().await;
state.record_spine_ui_spawn_progress(progress.clone());
state.live_spine_ui(&event_turn_id).cloned()
};
if let Some(notification) = live_spine_ui.as_ref().and_then(|spine_ui| {
crate::spine_ui::snapshot_started_notification(
&conversation_id.to_string(),
&event_turn_id,
spine_ui,
)
}) {
outgoing
.send_server_notification(ServerNotification::ItemStarted(notification))
.await;
}
}
let notification = item_event_to_server_notification(
EventMsg::SpineSpawnProgress(progress),
&conversation_id.to_string(),
Expand Down Expand Up @@ -1406,7 +1442,7 @@ async fn find_and_remove_turn_summary(
thread_state: &Arc<Mutex<ThreadState>>,
) -> TurnSummary {
let mut state = thread_state.lock().await;
std::mem::take(&mut state.turn_summary)
state.take_turn_summary()
}

async fn handle_turn_complete(
Expand All @@ -1417,6 +1453,12 @@ async fn handle_turn_complete(
thread_state: &Arc<Mutex<ThreadState>>,
) {
let turn_summary = find_and_remove_turn_summary(conversation_id, thread_state).await;
thread_state
.lock()
.await
.set_spine_ui_terminal_connection_ids(&event_turn_id, outgoing.connection_ids());

emit_spine_ui_item_completed(conversation_id, &event_turn_id, &turn_summary, outgoing).await;

let (status, error) = match turn_summary.last_error {
Some(error) => (TurnStatus::Failed, Some(error)),
Expand Down Expand Up @@ -1446,6 +1488,12 @@ async fn handle_turn_interrupted(
thread_state: &Arc<Mutex<ThreadState>>,
) {
let turn_summary = find_and_remove_turn_summary(conversation_id, thread_state).await;
thread_state
.lock()
.await
.set_spine_ui_terminal_connection_ids(&event_turn_id, outgoing.connection_ids());

emit_spine_ui_item_completed(conversation_id, &event_turn_id, &turn_summary, outgoing).await;

emit_turn_completed_with_status(
conversation_id,
Expand All @@ -1462,6 +1510,30 @@ async fn handle_turn_interrupted(
.await;
}

async fn emit_spine_ui_item_completed(
conversation_id: ThreadId,
turn_id: &str,
turn_summary: &TurnSummary,
outgoing: &ThreadScopedOutgoingMessageSender,
) {
if !crate::spine_ui::is_enabled() {
return;
}
let Some(spine_ui) = turn_summary.active_spine_ui(turn_id) else {
return;
};
let Some(notification) = crate::spine_ui::snapshot_completed_notification(
&conversation_id.to_string(),
turn_id,
spine_ui,
) else {
return;
};
outgoing
.send_server_notification(ServerNotification::ItemCompleted(notification))
.await;
}

async fn handle_thread_rollback_failed(
_conversation_id: ThreadId,
message: String,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ mod request_processors;
mod request_serialization;
mod server_request_error;
mod skills_watcher;
mod spine_ui;
mod thread_state;
mod thread_status;
mod transport;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ impl MessageProcessor {
let mcp_processor = McpRequestProcessor::new(
auth_manager.clone(),
Arc::clone(&thread_manager),
thread_state_manager.clone(),
outgoing.clone(),
config_manager.clone(),
);
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/app-server/src/outgoing_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ impl ThreadScopedOutgoingMessageSender {
}
}

pub(crate) fn connection_ids(&self) -> &[ConnectionId] {
self.connection_ids.as_slice()
}

pub(crate) async fn send_request(
&self,
payload: ServerRequestPayload,
Expand Down
64 changes: 52 additions & 12 deletions codex-rs/app-server/src/request_processors/mcp_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId";
pub(crate) struct McpRequestProcessor {
auth_manager: Arc<AuthManager>,
thread_manager: Arc<ThreadManager>,
thread_state_manager: ThreadStateManager,
outgoing: Arc<OutgoingMessageSender>,
config_manager: ConfigManager,
}
Expand All @@ -14,12 +15,14 @@ impl McpRequestProcessor {
pub(crate) fn new(
auth_manager: Arc<AuthManager>,
thread_manager: Arc<ThreadManager>,
thread_state_manager: ThreadStateManager,
outgoing: Arc<OutgoingMessageSender>,
config_manager: ConfigManager,
) -> Self {
Self {
auth_manager,
thread_manager,
thread_state_manager,
outgoing,
config_manager,
}
Expand Down Expand Up @@ -338,6 +341,12 @@ impl McpRequestProcessor {
);
server_names.sort();
server_names.dedup();
let inject_spine_ui = crate::spine_ui::is_enabled();
if inject_spine_ui {
server_names.push(crate::spine_ui::SERVER_NAME.to_string());
server_names.sort();
server_names.dedup();
}

let total = server_names.len();
let limit = params.limit.unwrap_or(total as u32).max(1) as usize;
Expand All @@ -360,17 +369,26 @@ impl McpRequestProcessor {

let data: Vec<McpServerStatus> = server_names[start..end]
.iter()
.map(|name| McpServerStatus {
name: name.clone(),
server_info: server_infos.get(name).cloned(),
tools: tools_by_server.get(name).cloned().unwrap_or_default(),
resources: resources.get(name).cloned().unwrap_or_default(),
resource_templates: resource_templates.get(name).cloned().unwrap_or_default(),
auth_status: auth_statuses
.get(name)
.cloned()
.unwrap_or(CoreMcpAuthStatus::Unsupported)
.into(),
.map(|name| {
if inject_spine_ui && name == crate::spine_ui::SERVER_NAME {
crate::spine_ui::server_status(matches!(detail, McpSnapshotDetail::Full))
} else {
McpServerStatus {
name: name.clone(),
server_info: server_infos.get(name).cloned(),
tools: tools_by_server.get(name).cloned().unwrap_or_default(),
resources: resources.get(name).cloned().unwrap_or_default(),
resource_templates: resource_templates
.get(name)
.cloned()
.unwrap_or_default(),
auth_status: auth_statuses
.get(name)
.cloned()
.unwrap_or(CoreMcpAuthStatus::Unsupported)
.into(),
}
}
})
.collect();

Expand All @@ -388,6 +406,15 @@ impl McpRequestProcessor {
request_id: &ConnectionRequestId,
params: McpResourceReadParams,
) -> Result<(), JSONRPCErrorError> {
if crate::spine_ui::is_enabled()
&& let Some(response) = crate::spine_ui::read_resource(&params.server, &params.uri)
{
self.outgoing
.send_response(request_id.clone(), response)
.await;
return Ok(());
}

let outgoing = Arc::clone(&self.outgoing);
let McpResourceReadParams {
thread_id,
Expand Down Expand Up @@ -457,9 +484,22 @@ impl McpRequestProcessor {
request_id: &ConnectionRequestId,
params: McpServerToolCallParams,
) -> Result<(), JSONRPCErrorError> {
let (thread_id, thread) = self.load_thread(&params.thread_id).await?;
if crate::spine_ui::is_enabled()
&& crate::spine_ui::is_internal_tool(&params.server, &params.tool)
{
let state = self
.thread_state_manager
.spine_ui_state_for_thread(thread_id)
.await;
let response = crate::spine_ui::tool_call_response(&params.thread_id, state.as_ref());
self.outgoing
.send_response(request_id.clone(), response)
.await;
return Ok(());
}
let outgoing = Arc::clone(&self.outgoing);
let thread_id = params.thread_id.clone();
let (_, thread) = self.load_thread(&thread_id).await?;
let meta = with_mcp_tool_call_thread_id_meta(params.meta, &thread_id);
let request_id = request_id.clone();

Expand Down
Loading