Add TUI tools for managing Codex tasks (#40308)

## Why

The TUI rejected app-server dynamic tool calls as unsupported, so agents could
not inspect or manage other Codex tasks through a TUI session.

## What changed

- Add the `codex_tui` tool namespace for listing, reading, waiting on, creating,
  forking, messaging, renaming, archiving, and restoring tasks.
- Handle tool calls through existing app-server thread operations, with bounded
  inputs and responses and cleanup when calls complete or the TUI disconnects.
- Route delegation tools through an authenticated local MCP server with explicit
  approval prompts, while respecting configured and managed MCP policies.
- Fall back to starting threads without dynamic tools when connected to an older
  app server that does not support them.

## Testing

Add coverage for tool schemas and response bounds, task lifecycle operations,
approval-gated MCP transport, namespace dispatch, cancellation, and legacy
app-server fallback behavior.

GitOrigin-RevId: c782c11f0a3edceb3fa6909a4b92d6ce01a6eed3
This commit is contained in:
Eric Traut authored and copyberry committed 2026-08-24 01:50:53 +00:00
1 parent fb0781b9ee
commit a8468330bb
18 files changed
+3722 -62

No files matched your search

+48
View File
@@ -100,6 +100,35 @@ impl App {
self.handle_startup_thread_started(app_server, result)
.await?;
}
AppEvent::DynamicToolThreadStarted {
thread_id,
registered,
} => {
self.agents_overview
.dispatched_requests
.entry(thread_id)
.or_default();
let _ = registered.send(());
}
AppEvent::DynamicToolCallCompleted {
request_id,
response,
} => {
self.dynamic_tool_tasks.remove(&request_id);
match serde_json::to_value(response) {
Ok(result) => {
if let Err(error) = app_server
.resolve_server_request(request_id.clone(), result)
.await
{
tracing::warn!(?request_id, %error, "failed to resolve dynamic tool call");
}
}
Err(error) => {
tracing::warn!(?request_id, %error, "failed to serialize dynamic tool response");
}
}
}
AppEvent::RequestOlderScrollbackHistory { thread_id } => {
if self.chat_widget.thread_id() == Some(thread_id)
&& self.overlay.is_none()
@@ -2903,6 +2932,25 @@ impl App {
app_server: &mut AppServerSession,
mode: ExitMode,
) -> AppRunControl {
for (request_id, (_, task)) in self.dynamic_tool_tasks.drain() {
task.abort();
let response = crate::dynamic_tools::failure_response(
"TUI disconnected while handling a dynamic tool call",
);
match serde_json::to_value(response) {
Ok(result) => {
if let Err(error) = app_server
.resolve_server_request(request_id.clone(), result)
.await
{
tracing::warn!(?request_id, %error, "failed to cancel dynamic tool call");
}
}
Err(error) => {
tracing::warn!(?request_id, %error, "failed to serialize dynamic tool response")
}
}
}
match mode {
ExitMode::ShutdownFirst => {
// Mark the thread we are explicitly shutting down for exit so