Merge branch 'main' into remove-reasoning-effort

This commit is contained in:
Ahmed Ibrahim
2025-08-18 11:15:53 -07:00
committed by GitHub
47 changed files with 609 additions and 2126 deletions
+4 -2
View File
@@ -9,7 +9,7 @@
},
"devDependencies": {
"@types/bun": "^1.2.20",
"@types/node": "^24.2.1",
"@types/node": "^24.3.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2",
},
@@ -50,7 +50,7 @@
"@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
"@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="],
"@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
@@ -82,6 +82,8 @@
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
"bun-types/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="],
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
+1 -1
View File
@@ -14,7 +14,7 @@
},
"devDependencies": {
"@types/bun": "^1.2.20",
"@types/node": "^24.2.1",
"@types/node": "^24.3.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2"
}
+4
View File
@@ -12,6 +12,10 @@ Before finalizing a change to `codex-rs`, run `just fmt` (in `codex-rs` director
1. Run the test for the specific project that was changed. For example, if changes were made in `codex-rs/tui`, run `cargo test -p codex-tui`.
2. Once those pass, if any changes were made in common, core, or protocol, run the complete test suite with `cargo test --all-features`.
## TUI style conventions
See `codex-rs/tui/styles.md`.
## TUI code conventions
- Use concise styling helpers from ratatui’s Stylize trait.
+40
View File
@@ -665,6 +665,7 @@ dependencies = [
"codex-exec",
"codex-login",
"codex-mcp-server",
"codex-protocol",
"codex-tui",
"serde_json",
"tokio",
@@ -678,6 +679,7 @@ version = "0.0.0"
dependencies = [
"clap",
"codex-core",
"codex-protocol",
"serde",
"toml 0.9.5",
]
@@ -752,6 +754,7 @@ dependencies = [
"codex-common",
"codex-core",
"codex-ollama",
"codex-protocol",
"core_test_support",
"libc",
"owo-colors",
@@ -856,6 +859,7 @@ dependencies = [
"codex-arg0",
"codex-core",
"codex-login",
"codex-protocol",
"mcp-types",
"mcp_test_support",
"pretty_assertions",
@@ -896,6 +900,7 @@ name = "codex-protocol"
version = "0.0.0"
dependencies = [
"mcp-types",
"pretty_assertions",
"serde",
"serde_bytes",
"serde_json",
@@ -2771,6 +2776,7 @@ version = "0.0.0"
dependencies = [
"serde",
"serde_json",
"ts-rs",
]
[[package]]
@@ -2781,8 +2787,10 @@ dependencies = [
"assert_cmd",
"codex-core",
"codex-mcp-server",
"codex-protocol",
"mcp-types",
"pretty_assertions",
"serde",
"serde_json",
"shlex",
"tempfile",
@@ -4700,6 +4708,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.4.2"
@@ -5199,6 +5216,29 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ts-rs"
version = "11.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be"
dependencies = [
"serde_json",
"thiserror 2.0.12",
"ts-rs-macros",
]
[[package]]
name = "ts-rs-macros"
version = "11.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"termcolor",
]
[[package]]
name = "tui-input"
version = "0.14.0"
+1
View File
@@ -25,6 +25,7 @@ codex-core = { path = "../core" }
codex-exec = { path = "../exec" }
codex-login = { path = "../login" }
codex-mcp-server = { path = "../mcp-server" }
codex-protocol = { path = "../protocol" }
codex-tui = { path = "../tui" }
serde_json = "1"
tokio = { version = "1", features = [
+1 -1
View File
@@ -3,11 +3,11 @@ use std::path::PathBuf;
use codex_common::CliConfigOverrides;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config_types::SandboxMode;
use codex_core::exec_env::create_env;
use codex_core::landlock::spawn_command_under_linux_sandbox;
use codex_core::seatbelt::spawn_command_under_seatbelt;
use codex_core::spawn::StdioPolicy;
use codex_protocol::config_types::SandboxMode;
use crate::LandlockCommand;
use crate::SeatbeltCommand;
+8 -1
View File
@@ -1,2 +1,9 @@
allow-expect-in-tests = true
allow-unwrap-in-tests = true
allow-unwrap-in-tests = true
disallowed-methods = [
{ path = "ratatui::style::Color::Rgb", reason = "Use ANSI colors, which work better in various terminal themes." },
{ path = "ratatui::style::Color::Indexed", reason = "Use ANSI colors, which work better in various terminal themes." },
{ path = "ratatui::style::Stylize::white", reason = "Avoid hardcoding white; prefer default fg or dim/bold. Exception: Disable this rule if rendering over a hardcoded ANSI background." },
{ path = "ratatui::style::Stylize::black", reason = "Avoid hardcoding black; prefer default fg or dim/bold. Exception: Disable this rule if rendering over a hardcoded ANSI background." },
{ path = "ratatui::style::Stylize::yellow", reason = "Avoid yellow; prefer other colors in `tui/styles.md`." },
]
+1
View File
@@ -9,6 +9,7 @@ workspace = true
[dependencies]
clap = { version = "4", features = ["derive", "wrap_help"], optional = true }
codex-core = { path = "../core" }
codex-protocol = { path = "../protocol" }
serde = { version = "1", optional = true }
toml = { version = "0.9", optional = true }
+1 -1
View File
@@ -7,7 +7,7 @@
//! `config.toml`.
use clap::ValueEnum;
use codex_core::config_types::SandboxMode;
use codex_protocol::config_types::SandboxMode;
#[derive(Clone, Copy, Debug, ValueEnum)]
#[value(rename_all = "kebab-case")]
-15
View File
@@ -78,21 +78,6 @@ pub enum HistoryPersistence {
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
pub struct Tui {}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Default, Serialize, Display)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum SandboxMode {
#[serde(rename = "read-only")]
#[default]
ReadOnly,
#[serde(rename = "workspace-write")]
WorkspaceWrite,
#[serde(rename = "danger-full-access")]
DangerFullAccess,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
pub struct SandboxWorkspaceWrite {
#[serde(default)]
+1 -1
View File
@@ -2,11 +2,11 @@ use serde::Deserialize;
use serde::Serialize;
use strum_macros::Display as DeriveDisplay;
use crate::config_types::SandboxMode;
use crate::models::ContentItem;
use crate::models::ResponseItem;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use codex_protocol::config_types::SandboxMode;
use std::fmt::Display;
use std::path::PathBuf;
+2 -1
View File
@@ -26,6 +26,7 @@ codex-common = { path = "../common", features = [
] }
codex-core = { path = "../core" }
codex-ollama = { path = "../ollama" }
codex-protocol = { path = "../protocol" }
owo-colors = "4.2.0"
serde_json = "1"
shlex = "1.3.0"
@@ -41,8 +42,8 @@ tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
[dev-dependencies]
assert_cmd = "2"
core_test_support = { path = "../core/tests/common" }
libc = "0.2"
predicates = "3"
tempfile = "3.13.0"
wiremock = "0.6"
core_test_support = { path = "../core/tests/common" }
+1 -1
View File
@@ -13,7 +13,6 @@ use codex_core::ConversationManager;
use codex_core::NewConversation;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config_types::SandboxMode;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
@@ -22,6 +21,7 @@ use codex_core::protocol::Op;
use codex_core::protocol::TaskCompleteEvent;
use codex_core::util::is_inside_git_repo;
use codex_ollama::DEFAULT_OSS_MODEL;
use codex_protocol::config_types::SandboxMode;
use event_processor_with_human_output::EventProcessorWithHumanOutput;
use event_processor_with_json_output::EventProcessorWithJsonOutput;
use tracing::debug;
+1
View File
@@ -19,6 +19,7 @@ anyhow = "1"
codex-arg0 = { path = "../arg0" }
codex-core = { path = "../core" }
codex-login = { path = "../login" }
codex-protocol = { path = "../protocol" }
mcp-types = { path = "../mcp-types" }
schemars = "0.8.22"
serde = { version = "1", features = ["derive"] }
@@ -25,36 +25,36 @@ use crate::error_code::INVALID_REQUEST_ERROR_CODE;
use crate::json_to_toml::json_to_toml;
use crate::outgoing_message::OutgoingMessageSender;
use crate::outgoing_message::OutgoingNotification;
use crate::wire_format::APPLY_PATCH_APPROVAL_METHOD;
use crate::wire_format::AddConversationListenerParams;
use crate::wire_format::AddConversationSubscriptionResponse;
use crate::wire_format::ApplyPatchApprovalParams;
use crate::wire_format::ApplyPatchApprovalResponse;
use crate::wire_format::ClientRequest;
use crate::wire_format::ConversationId;
use crate::wire_format::EXEC_COMMAND_APPROVAL_METHOD;
use crate::wire_format::ExecCommandApprovalParams;
use crate::wire_format::ExecCommandApprovalResponse;
use crate::wire_format::InputItem as WireInputItem;
use crate::wire_format::InterruptConversationParams;
use crate::wire_format::InterruptConversationResponse;
use crate::wire_format::LOGIN_CHATGPT_COMPLETE_EVENT;
use crate::wire_format::LoginChatGptCompleteNotification;
use crate::wire_format::LoginChatGptResponse;
use crate::wire_format::NewConversationParams;
use crate::wire_format::NewConversationResponse;
use crate::wire_format::RemoveConversationListenerParams;
use crate::wire_format::RemoveConversationSubscriptionResponse;
use crate::wire_format::SendUserMessageParams;
use crate::wire_format::SendUserMessageResponse;
use crate::wire_format::SendUserTurnParams;
use crate::wire_format::SendUserTurnResponse;
use codex_core::protocol::InputItem as CoreInputItem;
use codex_core::protocol::Op;
use codex_login::CLIENT_ID;
use codex_login::ServerOptions as LoginServerOptions;
use codex_login::ShutdownHandle;
use codex_login::run_login_server;
use codex_protocol::mcp_protocol::APPLY_PATCH_APPROVAL_METHOD;
use codex_protocol::mcp_protocol::AddConversationListenerParams;
use codex_protocol::mcp_protocol::AddConversationSubscriptionResponse;
use codex_protocol::mcp_protocol::ApplyPatchApprovalParams;
use codex_protocol::mcp_protocol::ApplyPatchApprovalResponse;
use codex_protocol::mcp_protocol::ClientRequest;
use codex_protocol::mcp_protocol::ConversationId;
use codex_protocol::mcp_protocol::EXEC_COMMAND_APPROVAL_METHOD;
use codex_protocol::mcp_protocol::ExecCommandApprovalParams;
use codex_protocol::mcp_protocol::ExecCommandApprovalResponse;
use codex_protocol::mcp_protocol::InputItem as WireInputItem;
use codex_protocol::mcp_protocol::InterruptConversationParams;
use codex_protocol::mcp_protocol::InterruptConversationResponse;
use codex_protocol::mcp_protocol::LOGIN_CHATGPT_COMPLETE_EVENT;
use codex_protocol::mcp_protocol::LoginChatGptCompleteNotification;
use codex_protocol::mcp_protocol::LoginChatGptResponse;
use codex_protocol::mcp_protocol::NewConversationParams;
use codex_protocol::mcp_protocol::NewConversationResponse;
use codex_protocol::mcp_protocol::RemoveConversationListenerParams;
use codex_protocol::mcp_protocol::RemoveConversationSubscriptionResponse;
use codex_protocol::mcp_protocol::SendUserMessageParams;
use codex_protocol::mcp_protocol::SendUserMessageResponse;
use codex_protocol::mcp_protocol::SendUserTurnParams;
use codex_protocol::mcp_protocol::SendUserTurnResponse;
// Duration before a ChatGPT login attempt is abandoned.
const LOGIN_CHATGPT_TIMEOUT: Duration = Duration::from_secs(10 * 60);
@@ -237,7 +237,7 @@ impl CodexMessageProcessor {
self.outgoing
.send_response(
request_id,
crate::wire_format::CancelLoginChatGptResponse {},
codex_protocol::mcp_protocol::CancelLoginChatGptResponse {},
)
.await;
} else {
@@ -595,7 +595,7 @@ fn derive_config_from_params(
profile,
cwd,
approval_policy,
sandbox,
sandbox: sandbox_mode,
config: cli_overrides,
base_instructions,
include_plan_tool,
@@ -605,8 +605,8 @@ fn derive_config_from_params(
model,
config_profile: profile,
cwd: cwd.map(PathBuf::from),
approval_policy: approval_policy.map(Into::into),
sandbox_mode: sandbox.map(Into::into),
approval_policy,
sandbox_mode,
model_provider: None,
codex_linux_sandbox_exe,
base_instructions,
+1 -1
View File
@@ -1,7 +1,7 @@
//! Configuration object accepted by the `codex` MCP tool-call.
use codex_core::config_types::SandboxMode;
use codex_core::protocol::AskForApproval;
use codex_protocol::config_types::SandboxMode;
use mcp_types::Tool;
use mcp_types::ToolInputSchema;
use schemars::JsonSchema;
@@ -1,124 +0,0 @@
use std::sync::Arc;
use crate::exec_approval::handle_exec_approval_request;
use crate::outgoing_message::OutgoingMessageSender;
use crate::outgoing_message::OutgoingNotificationMeta;
use crate::patch_approval::handle_patch_approval_request;
use codex_core::CodexConversation;
use codex_core::protocol::AgentMessageEvent;
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
use codex_core::protocol::EventMsg;
use codex_core::protocol::ExecApprovalRequestEvent;
use mcp_types::RequestId;
use tracing::error;
pub async fn run_conversation_loop(
codex: Arc<CodexConversation>,
outgoing: Arc<OutgoingMessageSender>,
request_id: RequestId,
) {
let request_id_str = match &request_id {
RequestId::String(s) => s.clone(),
RequestId::Integer(n) => n.to_string(),
};
// Stream events until the task needs to pause for user interaction or
// completes.
loop {
match codex.next_event().await {
Ok(event) => {
outgoing
.send_event_as_notification(
&event,
Some(OutgoingNotificationMeta::new(Some(request_id.clone()))),
)
.await;
match event.msg {
EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
command,
cwd,
call_id,
reason: _,
}) => {
handle_exec_approval_request(
command,
cwd,
outgoing.clone(),
codex.clone(),
request_id.clone(),
request_id_str.clone(),
event.id.clone(),
call_id,
)
.await;
}
EventMsg::Error(_) => {
error!("Codex runtime error");
}
EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
call_id,
reason,
grant_root,
changes,
}) => {
handle_patch_approval_request(
call_id,
reason,
grant_root,
changes,
outgoing.clone(),
codex.clone(),
request_id.clone(),
request_id_str.clone(),
event.id.clone(),
)
.await;
}
EventMsg::TaskComplete(_) => {}
EventMsg::SessionConfigured(_) => {
tracing::error!("unexpected SessionConfigured event");
}
EventMsg::AgentMessageDelta(_) => {
// TODO: think how we want to support this in the MCP
}
EventMsg::AgentReasoningDelta(_) => {
// TODO: think how we want to support this in the MCP
}
EventMsg::AgentMessage(AgentMessageEvent { .. }) => {
// TODO: think how we want to support this in the MCP
}
EventMsg::AgentReasoningRawContent(_)
| EventMsg::AgentReasoningRawContentDelta(_)
| EventMsg::TaskStarted
| EventMsg::TokenCount(_)
| EventMsg::AgentReasoning(_)
| EventMsg::AgentReasoningSectionBreak(_)
| EventMsg::McpToolCallBegin(_)
| EventMsg::McpToolCallEnd(_)
| EventMsg::ExecCommandBegin(_)
| EventMsg::ExecCommandEnd(_)
| EventMsg::TurnDiff(_)
| EventMsg::BackgroundEvent(_)
| EventMsg::ExecCommandOutputDelta(_)
| EventMsg::PatchApplyBegin(_)
| EventMsg::PatchApplyEnd(_)
| EventMsg::GetHistoryEntryResponse(_)
| EventMsg::PlanUpdate(_)
| EventMsg::TurnAborted(_)
| EventMsg::ShutdownComplete => {
// For now, we do not do anything extra for these
// events. Note that
// send(codex_event_to_notification(&event)) above has
// already dispatched these events as notifications,
// though we may want to do give different treatment to
// individual events in the future.
}
}
}
Err(e) => {
error!("Codex runtime error: {e}");
}
}
}
}
-4
View File
@@ -18,16 +18,12 @@ use tracing_subscriber::EnvFilter;
mod codex_message_processor;
mod codex_tool_config;
mod codex_tool_runner;
mod conversation_loop;
mod error_code;
mod exec_approval;
mod json_to_toml;
pub mod mcp_protocol;
pub(crate) mod message_processor;
mod outgoing_message;
mod patch_approval;
pub(crate) mod tool_handlers;
pub mod wire_format;
use crate::message_processor::MessageProcessor;
use crate::outgoing_message::OutgoingMessage;
File diff suppressed because it is too large Load Diff
+1 -70
View File
@@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
@@ -9,18 +8,12 @@ use crate::codex_tool_config::CodexToolCallReplyParam;
use crate::codex_tool_config::create_tool_for_codex_tool_call_param;
use crate::codex_tool_config::create_tool_for_codex_tool_call_reply_param;
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
use crate::mcp_protocol::ToolCallRequestParams;
use crate::mcp_protocol::ToolCallResponse;
use crate::mcp_protocol::ToolCallResponseResult;
use crate::outgoing_message::OutgoingMessageSender;
use crate::tool_handlers::create_conversation::handle_create_conversation;
use crate::tool_handlers::send_message::handle_send_message;
use crate::wire_format::ClientRequest;
use codex_protocol::mcp_protocol::ClientRequest;
use codex_core::ConversationManager;
use codex_core::config::Config as CodexConfig;
use codex_core::protocol::Submission;
use mcp_types::CallToolRequest;
use mcp_types::CallToolRequestParams;
use mcp_types::CallToolResult;
use mcp_types::ClientRequest as McpClientRequest;
@@ -48,7 +41,6 @@ pub(crate) struct MessageProcessor {
codex_linux_sandbox_exe: Option<PathBuf>,
conversation_manager: Arc<ConversationManager>,
running_requests_id_to_codex_uuid: Arc<Mutex<HashMap<RequestId, Uuid>>>,
running_session_ids: Arc<Mutex<HashSet<Uuid>>>,
}
impl MessageProcessor {
@@ -72,22 +64,9 @@ impl MessageProcessor {
codex_linux_sandbox_exe,
conversation_manager,
running_requests_id_to_codex_uuid: Arc::new(Mutex::new(HashMap::new())),
running_session_ids: Arc::new(Mutex::new(HashSet::new())),
}
}
pub(crate) fn get_conversation_manager(&self) -> &ConversationManager {
&self.conversation_manager
}
pub(crate) fn outgoing(&self) -> Arc<OutgoingMessageSender> {
self.outgoing.clone()
}
pub(crate) fn running_session_ids(&self) -> Arc<Mutex<HashSet<Uuid>>> {
self.running_session_ids.clone()
}
pub(crate) async fn process_request(&mut self, request: JSONRPCRequest) {
if let Ok(request_json) = serde_json::to_value(request.clone())
&& let Ok(codex_request) = serde_json::from_value::<ClientRequest>(request_json)
@@ -341,14 +320,6 @@ impl MessageProcessor {
params: <mcp_types::CallToolRequest as mcp_types::ModelContextProtocolRequest>::Params,
) {
tracing::info!("tools/call -> params: {:?}", params);
// Serialize params into JSON and try to parse as new type
if let Ok(new_params) =
serde_json::to_value(&params).and_then(serde_json::from_value::<ToolCallRequestParams>)
{
// New tool call matched → forward
self.handle_new_tool_calls(id, new_params).await;
return;
}
let CallToolRequestParams { name, arguments } = params;
match name.as_str() {
@@ -372,30 +343,6 @@ impl MessageProcessor {
}
}
}
async fn handle_new_tool_calls(&self, request_id: RequestId, params: ToolCallRequestParams) {
match params {
ToolCallRequestParams::ConversationCreate(args) => {
handle_create_conversation(self, request_id, args).await;
}
ToolCallRequestParams::ConversationSendMessage(args) => {
handle_send_message(self, request_id, args).await;
}
_ => {
let result = CallToolResult {
content: vec![ContentBlock::TextContent(TextContent {
r#type: "text".to_string(),
text: "Unknown tool".to_string(),
annotations: None,
})],
is_error: Some(true),
structured_content: None,
};
self.send_response::<CallToolRequest>(request_id, result)
.await;
}
}
}
async fn handle_tool_call_codex(&self, id: RequestId, arguments: Option<serde_json::Value>) {
let (initial_prompt, config): (String, CodexConfig) = match arguments {
Some(json_val) => match serde_json::from_value::<CodexToolCallParam>(json_val) {
@@ -692,20 +639,4 @@ impl MessageProcessor {
) {
tracing::info!("notifications/message -> params: {:?}", params);
}
pub(crate) async fn send_response_with_optional_error(
&self,
id: RequestId,
message: Option<ToolCallResponseResult>,
error: Option<bool>,
) {
let response = ToolCallResponse {
request_id: id.clone(),
is_error: error,
result: message,
};
let result: CallToolResult = response.into();
self.send_response::<mcp_types::CallToolRequest>(id.clone(), result)
.await;
}
}
@@ -119,9 +119,6 @@ impl OutgoingMessageSender {
params: Some(params.clone()),
})
.await;
self.send_event_as_notification_new_schema(event, Some(params.clone()))
.await;
}
pub(crate) async fn send_notification(&self, notification: OutgoingNotification) {
@@ -129,19 +126,6 @@ impl OutgoingMessageSender {
let _ = self.sender.send(outgoing_message).await;
}
// should be backwards compatible.
// it will replace send_event_as_notification eventually.
async fn send_event_as_notification_new_schema(
&self,
event: &Event,
params: Option<serde_json::Value>,
) {
let outgoing_message = OutgoingMessage::Notification(OutgoingNotification {
method: event.msg.to_string(),
params,
});
let _ = self.sender.send(outgoing_message).await;
}
pub(crate) async fn send_error(&self, id: RequestId, error: JSONRPCErrorError) {
let outgoing_message = OutgoingMessage::Error(OutgoingError { id, error });
let _ = self.sender.send(outgoing_message).await;
@@ -281,17 +265,6 @@ mod tests {
panic!("Event must serialize");
};
assert_eq!(params, Some(expected_params.clone()));
let result2 = outgoing_rx.recv().await.unwrap();
let OutgoingMessage::Notification(OutgoingNotification {
method: method2,
params: params2,
}) = result2
else {
panic!("expected Notification for second message");
};
assert_eq!(method2, event.msg.to_string());
assert_eq!(params2, Some(expected_params));
}
#[tokio::test]
@@ -336,16 +309,5 @@ mod tests {
}
});
assert_eq!(params.unwrap(), expected_params);
let result2 = outgoing_rx.recv().await.unwrap();
let OutgoingMessage::Notification(OutgoingNotification {
method: method2,
params: params2,
}) = result2
else {
panic!("expected Notification for second message");
};
assert_eq!(method2, event.msg.to_string());
assert_eq!(params2.unwrap(), expected_params);
}
}
@@ -1,127 +0,0 @@
use std::path::PathBuf;
use codex_core::NewConversation;
use codex_core::config::Config as CodexConfig;
use codex_core::config::ConfigOverrides;
use mcp_types::RequestId;
use crate::conversation_loop::run_conversation_loop;
use crate::json_to_toml::json_to_toml;
use crate::mcp_protocol::ConversationCreateArgs;
use crate::mcp_protocol::ConversationCreateResult;
use crate::mcp_protocol::ConversationId;
use crate::mcp_protocol::ToolCallResponseResult;
use crate::message_processor::MessageProcessor;
pub(crate) async fn handle_create_conversation(
message_processor: &MessageProcessor,
id: RequestId,
args: ConversationCreateArgs,
) {
// Build ConfigOverrides from args
let ConversationCreateArgs {
prompt: _, // not used here; creation only establishes the session
model,
cwd,
approval_policy,
sandbox,
config,
profile,
base_instructions,
} = args;
// Convert config overrides JSON into CLI-style TOML overrides
let cli_overrides: Vec<(String, toml::Value)> = match config {
Some(v) => match v.as_object() {
Some(map) => map
.into_iter()
.map(|(k, v)| (k.clone(), json_to_toml(v.clone())))
.collect(),
None => Vec::new(),
},
None => Vec::new(),
};
let overrides = ConfigOverrides {
model: Some(model.clone()),
cwd: Some(PathBuf::from(cwd)),
approval_policy,
sandbox_mode: sandbox,
model_provider: None,
config_profile: profile,
codex_linux_sandbox_exe: None,
base_instructions,
include_plan_tool: None,
include_apply_patch_tool: None,
disable_response_storage: None,
show_raw_agent_reasoning: None,
};
let cfg: CodexConfig = match CodexConfig::load_with_cli_overrides(cli_overrides, overrides) {
Ok(cfg) => cfg,
Err(e) => {
message_processor
.send_response_with_optional_error(
id,
Some(ToolCallResponseResult::ConversationCreate(
ConversationCreateResult::Error {
message: format!("Failed to load config: {e}"),
},
)),
Some(true),
)
.await;
return;
}
};
// Initialize Codex session via server API
let NewConversation {
conversation_id: session_id,
conversation,
session_configured,
} = match message_processor
.get_conversation_manager()
.new_conversation(cfg)
.await
{
Ok(conv) => conv,
Err(e) => {
message_processor
.send_response_with_optional_error(
id,
Some(ToolCallResponseResult::ConversationCreate(
ConversationCreateResult::Error {
message: format!("Failed to initialize session: {e}"),
},
)),
Some(true),
)
.await;
return;
}
};
let effective_model = session_configured.model.clone();
// Run the conversation loop in the background so this request can return immediately.
let outgoing = message_processor.outgoing();
let spawn_id = id.clone();
tokio::spawn(async move {
run_conversation_loop(conversation.clone(), outgoing, spawn_id).await;
});
// Reply with the new conversation id and effective model
message_processor
.send_response_with_optional_error(
id,
Some(ToolCallResponseResult::ConversationCreate(
ConversationCreateResult::Ok {
conversation_id: ConversationId(session_id),
model: effective_model,
},
)),
Some(false),
)
.await;
}
@@ -1,114 +0,0 @@
use codex_core::protocol::Op;
use codex_core::protocol::Submission;
use mcp_types::RequestId;
use crate::mcp_protocol::ConversationSendMessageArgs;
use crate::mcp_protocol::ConversationSendMessageResult;
use crate::mcp_protocol::ToolCallResponseResult;
use crate::message_processor::MessageProcessor;
pub(crate) async fn handle_send_message(
message_processor: &MessageProcessor,
id: RequestId,
arguments: ConversationSendMessageArgs,
) {
let ConversationSendMessageArgs {
conversation_id,
content: items,
parent_message_id: _,
conversation_overrides: _,
} = arguments;
if items.is_empty() {
message_processor
.send_response_with_optional_error(
id,
Some(ToolCallResponseResult::ConversationSendMessage(
ConversationSendMessageResult::Error {
message: "No content items provided".to_string(),
},
)),
Some(true),
)
.await;
return;
}
let session_id = conversation_id.0;
let Ok(codex) = message_processor
.get_conversation_manager()
.get_conversation(session_id)
.await
else {
message_processor
.send_response_with_optional_error(
id,
Some(ToolCallResponseResult::ConversationSendMessage(
ConversationSendMessageResult::Error {
message: "Session does not exist".to_string(),
},
)),
Some(true),
)
.await;
return;
};
let running = {
let running_sessions = message_processor.running_session_ids();
let mut running_sessions = running_sessions.lock().await;
!running_sessions.insert(session_id)
};
if running {
message_processor
.send_response_with_optional_error(
id,
Some(ToolCallResponseResult::ConversationSendMessage(
ConversationSendMessageResult::Error {
message: "Session is already running".to_string(),
},
)),
Some(true),
)
.await;
return;
}
let request_id_string = match &id {
RequestId::String(s) => s.clone(),
RequestId::Integer(i) => i.to_string(),
};
let submit_res = codex
.submit_with_id(Submission {
id: request_id_string,
op: Op::UserInput { items },
})
.await;
if let Err(e) = submit_res {
message_processor
.send_response_with_optional_error(
id,
Some(ToolCallResponseResult::ConversationSendMessage(
ConversationSendMessageResult::Error {
message: format!("Failed to submit user input: {e}"),
},
)),
Some(true),
)
.await;
return;
}
message_processor
.send_response_with_optional_error(
id,
Some(ToolCallResponseResult::ConversationSendMessage(
ConversationSendMessageResult::Ok,
)),
Some(false),
)
.await;
}
@@ -5,26 +5,26 @@ use codex_core::protocol::SandboxPolicy;
use codex_core::protocol_config_types::ReasoningEffort;
use codex_core::protocol_config_types::ReasoningSummary;
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_mcp_server::wire_format::AddConversationListenerParams;
use codex_mcp_server::wire_format::AddConversationSubscriptionResponse;
use codex_mcp_server::wire_format::EXEC_COMMAND_APPROVAL_METHOD;
use codex_mcp_server::wire_format::NewConversationParams;
use codex_mcp_server::wire_format::NewConversationResponse;
use codex_mcp_server::wire_format::RemoveConversationListenerParams;
use codex_mcp_server::wire_format::RemoveConversationSubscriptionResponse;
use codex_mcp_server::wire_format::SendUserMessageParams;
use codex_mcp_server::wire_format::SendUserMessageResponse;
use codex_mcp_server::wire_format::SendUserTurnParams;
use codex_mcp_server::wire_format::SendUserTurnResponse;
use codex_protocol::mcp_protocol::AddConversationListenerParams;
use codex_protocol::mcp_protocol::AddConversationSubscriptionResponse;
use codex_protocol::mcp_protocol::EXEC_COMMAND_APPROVAL_METHOD;
use codex_protocol::mcp_protocol::NewConversationParams;
use codex_protocol::mcp_protocol::NewConversationResponse;
use codex_protocol::mcp_protocol::RemoveConversationListenerParams;
use codex_protocol::mcp_protocol::RemoveConversationSubscriptionResponse;
use codex_protocol::mcp_protocol::SendUserMessageParams;
use codex_protocol::mcp_protocol::SendUserMessageResponse;
use codex_protocol::mcp_protocol::SendUserTurnParams;
use codex_protocol::mcp_protocol::SendUserTurnResponse;
use mcp_test_support::McpProcess;
use mcp_test_support::create_final_assistant_message_sse_response;
use mcp_test_support::create_mock_chat_completions_server;
use mcp_test_support::create_shell_sse_response;
use mcp_test_support::to_response;
use mcp_types::JSONRPCNotification;
use mcp_types::JSONRPCResponse;
use mcp_types::RequestId;
use pretty_assertions::assert_eq;
use serde::de::DeserializeOwned;
use std::env;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -113,7 +113,7 @@ async fn test_codex_jsonrpc_conversation_flow() {
let send_user_id = mcp
.send_send_user_message_request(SendUserMessageParams {
conversation_id,
items: vec![codex_mcp_server::wire_format::InputItem::Text {
items: vec![codex_protocol::mcp_protocol::InputItem::Text {
text: "text".to_string(),
}],
})
@@ -168,12 +168,6 @@ async fn test_codex_jsonrpc_conversation_flow() {
to_response(remove_listener_resp).expect("deserialize removeConversationListener response");
}
fn to_response<T: DeserializeOwned>(response: JSONRPCResponse) -> anyhow::Result<T> {
let value = serde_json::to_value(response.result)?;
let codex_response = serde_json::from_value(value)?;
Ok(codex_response)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_send_user_turn_changes_approval_policy_behavior() {
if env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
@@ -269,7 +263,7 @@ async fn test_send_user_turn_changes_approval_policy_behavior() {
let send_user_id = mcp
.send_send_user_message_request(SendUserMessageParams {
conversation_id,
items: vec![codex_mcp_server::wire_format::InputItem::Text {
items: vec![codex_protocol::mcp_protocol::InputItem::Text {
text: "run python".to_string(),
}],
})
@@ -317,7 +311,7 @@ async fn test_send_user_turn_changes_approval_policy_behavior() {
let send_turn_id = mcp
.send_send_user_turn_request(SendUserTurnParams {
conversation_id,
items: vec![codex_mcp_server::wire_format::InputItem::Text {
items: vec![codex_protocol::mcp_protocol::InputItem::Text {
text: "run python again".to_string(),
}],
cwd: working_directory.clone(),
+5 -3
View File
@@ -1,7 +1,7 @@
[package]
edition = "2024"
name = "mcp_test_support"
version = { workspace = true }
edition = "2024"
[lib]
path = "lib.rs"
@@ -9,10 +9,12 @@ path = "lib.rs"
[dependencies]
anyhow = "1"
assert_cmd = "2"
codex-mcp-server = { path = "../.." }
codex-core = { path = "../../../core" }
codex-mcp-server = { path = "../.." }
codex-protocol = { path = "../../../protocol" }
mcp-types = { path = "../../../mcp-types" }
pretty_assertions = "1.4.1"
serde = { version = "1" }
serde_json = "1"
shlex = "1.3.0"
tempfile = "3"
@@ -22,5 +24,5 @@ tokio = { version = "1", features = [
"process",
"rt-multi-thread",
] }
uuid = { version = "1", features = ["serde", "v4"] }
wiremock = "0.6"
uuid = { version = "1", features = ["serde", "v4"] }
+8
View File
@@ -3,7 +3,15 @@ mod mock_model_server;
mod responses;
pub use mcp_process::McpProcess;
use mcp_types::JSONRPCResponse;
pub use mock_model_server::create_mock_chat_completions_server;
pub use responses::create_apply_patch_sse_response;
pub use responses::create_final_assistant_message_sse_response;
pub use responses::create_shell_sse_response;
use serde::de::DeserializeOwned;
pub fn to_response<T: DeserializeOwned>(response: JSONRPCResponse) -> anyhow::Result<T> {
let value = serde_json::to_value(response.result)?;
let codex_response = serde_json::from_value(value)?;
Ok(codex_response)
}
+43 -163
View File
@@ -11,18 +11,13 @@ use tokio::process::ChildStdout;
use anyhow::Context;
use assert_cmd::prelude::*;
use codex_core::protocol::InputItem;
use codex_mcp_server::CodexToolCallParam;
use codex_mcp_server::CodexToolCallReplyParam;
use codex_mcp_server::mcp_protocol::ConversationCreateArgs;
use codex_mcp_server::mcp_protocol::ConversationId;
use codex_mcp_server::mcp_protocol::ConversationSendMessageArgs;
use codex_mcp_server::mcp_protocol::ToolCallRequestParams;
use codex_mcp_server::wire_format::AddConversationListenerParams;
use codex_mcp_server::wire_format::NewConversationParams;
use codex_mcp_server::wire_format::RemoveConversationListenerParams;
use codex_mcp_server::wire_format::SendUserMessageParams;
use codex_mcp_server::wire_format::SendUserTurnParams;
use codex_protocol::mcp_protocol::AddConversationListenerParams;
use codex_protocol::mcp_protocol::InterruptConversationParams;
use codex_protocol::mcp_protocol::NewConversationParams;
use codex_protocol::mcp_protocol::RemoveConversationListenerParams;
use codex_protocol::mcp_protocol::SendUserMessageParams;
use codex_protocol::mcp_protocol::SendUserTurnParams;
use mcp_types::CallToolRequestParams;
use mcp_types::ClientCapabilities;
@@ -40,7 +35,6 @@ use pretty_assertions::assert_eq;
use serde_json::json;
use std::process::Command as StdCommand;
use tokio::process::Command;
use uuid::Uuid;
pub struct McpProcess {
next_request_id: AtomicI64,
@@ -167,83 +161,6 @@ impl McpProcess {
.await
}
pub async fn send_codex_reply_tool_call(
&mut self,
session_id: &str,
prompt: &str,
) -> anyhow::Result<i64> {
let codex_tool_call_params = CallToolRequestParams {
name: "codex-reply".to_string(),
arguments: Some(serde_json::to_value(CodexToolCallReplyParam {
prompt: prompt.to_string(),
session_id: session_id.to_string(),
})?),
};
self.send_request(
mcp_types::CallToolRequest::METHOD,
Some(serde_json::to_value(codex_tool_call_params)?),
)
.await
}
pub async fn send_user_message_tool_call(
&mut self,
message: &str,
session_id: &str,
) -> anyhow::Result<i64> {
let params = ToolCallRequestParams::ConversationSendMessage(ConversationSendMessageArgs {
conversation_id: ConversationId(Uuid::parse_str(session_id)?),
content: vec![InputItem::Text {
text: message.to_string(),
}],
parent_message_id: None,
conversation_overrides: None,
});
self.send_request(
mcp_types::CallToolRequest::METHOD,
Some(serde_json::to_value(params)?),
)
.await
}
pub async fn send_conversation_create_tool_call(
&mut self,
prompt: &str,
model: &str,
cwd: &str,
) -> anyhow::Result<i64> {
let params = ToolCallRequestParams::ConversationCreate(ConversationCreateArgs {
prompt: prompt.to_string(),
model: model.to_string(),
cwd: cwd.to_string(),
approval_policy: None,
sandbox: None,
config: None,
profile: None,
base_instructions: None,
});
self.send_request(
mcp_types::CallToolRequest::METHOD,
Some(serde_json::to_value(params)?),
)
.await
}
pub async fn send_conversation_create_with_args(
&mut self,
args: ConversationCreateArgs,
) -> anyhow::Result<i64> {
let params = ToolCallRequestParams::ConversationCreate(args);
self.send_request(
mcp_types::CallToolRequest::METHOD,
Some(serde_json::to_value(params)?),
)
.await
}
// ---------------------------------------------------------------------
// Codex JSON-RPC (non-tool) helpers
// ---------------------------------------------------------------------
/// Send a `newConversation` JSON-RPC request.
pub async fn send_new_conversation_request(
&mut self,
@@ -291,6 +208,15 @@ impl McpProcess {
self.send_request("sendUserTurn", params).await
}
/// Send a `interruptConversation` JSON-RPC request.
pub async fn send_interrupt_conversation_request(
&mut self,
params: InterruptConversationParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("interruptConversation", params).await
}
async fn send_request(
&mut self,
method: &str,
@@ -335,6 +261,7 @@ impl McpProcess {
let message = serde_json::from_str::<JSONRPCMessage>(&line)?;
Ok(message)
}
pub async fn read_stream_until_request_message(&mut self) -> anyhow::Result<JSONRPCRequest> {
loop {
let message = self.read_jsonrpc_message().await?;
@@ -384,6 +311,33 @@ impl McpProcess {
}
}
pub async fn read_stream_until_error_message(
&mut self,
request_id: RequestId,
) -> anyhow::Result<mcp_types::JSONRPCError> {
loop {
let message = self.read_jsonrpc_message().await?;
eprint!("message: {message:?}");
match message {
JSONRPCMessage::Notification(_) => {
eprintln!("notification: {message:?}");
}
JSONRPCMessage::Request(_) => {
anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}");
}
JSONRPCMessage::Response(_) => {
// Keep scanning; we're waiting for an error with matching id.
}
JSONRPCMessage::Error(err) => {
if err.id == request_id {
return Ok(err);
}
}
}
}
}
pub async fn read_stream_until_notification_message(
&mut self,
method: &str,
@@ -411,80 +365,6 @@ impl McpProcess {
}
}
pub async fn read_stream_until_configured_response_message(
&mut self,
) -> anyhow::Result<String> {
let mut sid_old: Option<String> = None;
let mut sid_new: Option<String> = None;
loop {
let message = self.read_jsonrpc_message().await?;
eprint!("message: {message:?}");
match message {
JSONRPCMessage::Notification(notification) => {
if let Some(params) = notification.params {
// Back-compat schema: method == "codex/event" and msg.type == "session_configured"
if notification.method == "codex/event" {
if let Some(msg) = params.get("msg") {
if msg.get("type").and_then(|v| v.as_str())
== Some("session_configured")
{
if let Some(session_id) =
msg.get("session_id").and_then(|v| v.as_str())
{
sid_old = Some(session_id.to_string());
}
}
}
}
// New schema: method is the Display of EventMsg::SessionConfigured => "SessionConfigured"
if notification.method == "session_configured" {
if let Some(msg) = params.get("msg") {
if let Some(session_id) =
msg.get("session_id").and_then(|v| v.as_str())
{
sid_new = Some(session_id.to_string());
}
}
}
}
if sid_old.is_some() && sid_new.is_some() {
// Both seen, they must match
assert_eq!(
sid_old.as_ref().unwrap(),
sid_new.as_ref().unwrap(),
"session_id mismatch between old and new schema"
);
return Ok(sid_old.unwrap());
}
}
JSONRPCMessage::Request(_) => {
anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}");
}
JSONRPCMessage::Error(_) => {
anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}");
}
JSONRPCMessage::Response(_) => {
anyhow::bail!("unexpected JSONRPCMessage::Response: {message:?}");
}
}
}
}
pub async fn send_notification(
&mut self,
method: &str,
params: Option<serde_json::Value>,
) -> anyhow::Result<()> {
self.send_jsonrpc_message(JSONRPCMessage::Notification(JSONRPCNotification {
jsonrpc: JSONRPC_VERSION.into(),
method: method.to_string(),
params,
}))
.await
}
/// Reads notifications until a legacy TaskComplete event is observed:
/// Method "codex/event" with params.msg.type == "task_complete".
pub async fn read_stream_until_legacy_task_complete_notification(
@@ -1,8 +1,16 @@
use std::path::Path;
use codex_protocol::mcp_protocol::AddConversationListenerParams;
use codex_protocol::mcp_protocol::AddConversationSubscriptionResponse;
use codex_protocol::mcp_protocol::InputItem;
use codex_protocol::mcp_protocol::NewConversationParams;
use codex_protocol::mcp_protocol::NewConversationResponse;
use codex_protocol::mcp_protocol::SendUserMessageParams;
use codex_protocol::mcp_protocol::SendUserMessageResponse;
use mcp_test_support::McpProcess;
use mcp_test_support::create_final_assistant_message_sse_response;
use mcp_test_support::create_mock_chat_completions_server;
use mcp_test_support::to_response;
use mcp_types::JSONRPCResponse;
use mcp_types::RequestId;
use pretty_assertions::assert_eq;
@@ -33,43 +41,64 @@ async fn test_conversation_create_and_send_message_ok() {
.expect("init timeout")
.expect("init failed");
// Create a conversation via the new tool.
let req_id = mcp
.send_conversation_create_tool_call("", "o3", "/repo")
// Create a conversation via the new JSON-RPC API.
let new_conv_id = mcp
.send_new_conversation_request(NewConversationParams {
model: Some("o3".to_string()),
..Default::default()
})
.await
.expect("send conversationCreate");
let resp: JSONRPCResponse = timeout(
.expect("send newConversation");
let new_conv_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
mcp.read_stream_until_response_message(RequestId::Integer(new_conv_id)),
)
.await
.expect("create response timeout")
.expect("create response error");
.expect("newConversation timeout")
.expect("newConversation resp");
let NewConversationResponse {
conversation_id,
model,
} = to_response::<NewConversationResponse>(new_conv_resp)
.expect("deserialize newConversation response");
assert_eq!(model, "o3");
// Structured content must include status=ok, a UUID conversation_id and the model we passed.
let sc = &resp.result["structuredContent"];
let conv_id = sc["conversation_id"].as_str().expect("uuid string");
assert!(!conv_id.is_empty());
assert_eq!(sc["model"], json!("o3"));
// Now send a message to the created conversation and expect an OK result.
let send_id = mcp
.send_user_message_tool_call("Hello", conv_id)
// Add a listener so we receive notifications for this conversation (not strictly required for this test).
let add_listener_id = mcp
.send_add_conversation_listener_request(AddConversationListenerParams { conversation_id })
.await
.expect("send message");
.expect("send addConversationListener");
let _sub: AddConversationSubscriptionResponse =
to_response::<AddConversationSubscriptionResponse>(
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(add_listener_id)),
)
.await
.expect("addConversationListener timeout")
.expect("addConversationListener resp"),
)
.expect("deserialize addConversationListener response");
// Now send a user message via the wire API and expect an OK (empty object) result.
let send_id = mcp
.send_send_user_message_request(SendUserMessageParams {
conversation_id,
items: vec![InputItem::Text {
text: "Hello".to_string(),
}],
})
.await
.expect("send sendUserMessage");
let send_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(send_id)),
)
.await
.expect("send response timeout")
.expect("send response error");
assert_eq!(
send_resp.result["structuredContent"],
json!({ "status": "ok" })
);
.expect("sendUserMessage timeout")
.expect("sendUserMessage resp");
let _ok: SendUserMessageResponse = to_response::<SendUserMessageResponse>(send_resp)
.expect("deserialize sendUserMessage response");
// avoid race condition by waiting for the mock server to receive the chat.completions request
let deadline = std::time::Instant::now() + DEFAULT_READ_TIMEOUT;
+76 -69
View File
@@ -3,15 +3,24 @@
use std::path::Path;
use codex_core::protocol::TurnAbortReason;
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_mcp_server::CodexToolCallParam;
use serde_json::json;
use codex_protocol::mcp_protocol::AddConversationListenerParams;
use codex_protocol::mcp_protocol::InterruptConversationParams;
use codex_protocol::mcp_protocol::InterruptConversationResponse;
use codex_protocol::mcp_protocol::NewConversationParams;
use codex_protocol::mcp_protocol::NewConversationResponse;
use codex_protocol::mcp_protocol::SendUserMessageParams;
use codex_protocol::mcp_protocol::SendUserMessageResponse;
use mcp_types::JSONRPCResponse;
use mcp_types::RequestId;
use tempfile::TempDir;
use tokio::time::timeout;
use mcp_test_support::McpProcess;
use mcp_test_support::create_mock_chat_completions_server;
use mcp_test_support::create_shell_sse_response;
use mcp_test_support::to_response;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
@@ -39,93 +48,91 @@ async fn shell_command_interruption() -> anyhow::Result<()> {
let shell_command = vec![
"powershell".to_string(),
"-Command".to_string(),
"Start-Sleep -Seconds 60".to_string(),
"Start-Sleep -Seconds 10".to_string(),
];
#[cfg(not(target_os = "windows"))]
let shell_command = vec!["sleep".to_string(), "60".to_string()];
let workdir_for_shell_function_call = TempDir::new()?;
let shell_command = vec!["sleep".to_string(), "10".to_string()];
let tmp = TempDir::new()?;
// Temporary Codex home with config pointing at the mock server.
let codex_home = tmp.path().join("codex_home");
std::fs::create_dir(&codex_home)?;
let working_directory = tmp.path().join("workdir");
std::fs::create_dir(&working_directory)?;
// Create mock server with a single SSE response: the long sleep command
let server = create_mock_chat_completions_server(vec![
create_shell_sse_response(
shell_command.clone(),
Some(workdir_for_shell_function_call.path()),
Some(60_000), // 60 seconds timeout in ms
"call_sleep",
)?,
create_shell_sse_response(
shell_command.clone(),
Some(workdir_for_shell_function_call.path()),
Some(60_000), // 60 seconds timeout in ms
"call_sleep",
)?,
])
let server = create_mock_chat_completions_server(vec![create_shell_sse_response(
shell_command.clone(),
Some(&working_directory),
Some(10_000), // 10 seconds timeout in ms
"call_sleep",
)?])
.await;
create_config_toml(&codex_home, server.uri())?;
// Create Codex configuration
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), server.uri())?;
let mut mcp_process = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp_process.initialize()).await??;
// Start MCP server and initialize.
let mut mcp = McpProcess::new(&codex_home).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
// Send codex tool call that triggers "sleep 60"
let codex_request_id = mcp_process
.send_codex_tool_call(CodexToolCallParam {
cwd: None,
prompt: "First Run: run `sleep 60`".to_string(),
model: None,
profile: None,
approval_policy: None,
sandbox: None,
config: None,
base_instructions: None,
include_plan_tool: None,
// 1) newConversation
let new_conv_id = mcp
.send_new_conversation_request(NewConversationParams {
cwd: Some(working_directory.to_string_lossy().into_owned()),
..Default::default()
})
.await?;
let new_conv_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(new_conv_id)),
)
.await??;
let new_conv_resp = to_response::<NewConversationResponse>(new_conv_resp)?;
let NewConversationResponse {
conversation_id, ..
} = new_conv_resp;
let session_id = mcp_process
.read_stream_until_configured_response_message()
// 2) addConversationListener
let add_listener_id = mcp
.send_add_conversation_listener_request(AddConversationListenerParams { conversation_id })
.await?;
let _add_listener_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(add_listener_id)),
)
.await??;
// 3) sendUserMessage (should trigger notifications; we only validate an OK response)
let send_user_id = mcp
.send_send_user_message_request(SendUserMessageParams {
conversation_id,
items: vec![codex_protocol::mcp_protocol::InputItem::Text {
text: "run first sleep command".to_string(),
}],
})
.await?;
let send_user_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(send_user_id)),
)
.await??;
let SendUserMessageResponse {} = to_response::<SendUserMessageResponse>(send_user_resp)?;
// Give the command a moment to start
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
// Send interrupt notification
mcp_process
.send_notification(
"notifications/cancelled",
Some(json!({ "requestId": codex_request_id })),
)
// 4) send interrupt request
let interrupt_id = mcp
.send_interrupt_conversation_request(InterruptConversationParams { conversation_id })
.await?;
// Expect Codex to emit a TurnAborted event notification
let _turn_aborted = timeout(
let interrupt_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp_process.read_stream_until_notification_message("turn_aborted"),
mcp.read_stream_until_response_message(RequestId::Integer(interrupt_id)),
)
.await??;
let InterruptConversationResponse { abort_reason } =
to_response::<InterruptConversationResponse>(interrupt_resp)?;
assert_eq!(TurnAbortReason::Interrupted, abort_reason);
let codex_reply_request_id = mcp_process
.send_codex_reply_tool_call(&session_id, "Second Run: run `sleep 60`")
.await?;
// Give the command a moment to start
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
// Send interrupt notification
mcp_process
.send_notification(
"notifications/cancelled",
Some(json!({ "requestId": codex_reply_request_id })),
)
.await?;
// Expect Codex to emit a TurnAborted event notification
let _turn_aborted = timeout(
DEFAULT_READ_TIMEOUT,
mcp_process.read_stream_until_notification_message("turn_aborted"),
)
.await??;
Ok(())
}
+91 -66
View File
@@ -1,16 +1,21 @@
use std::path::Path;
use std::thread::sleep;
use std::time::Duration;
use codex_mcp_server::CodexToolCallParam;
use codex_protocol::mcp_protocol::AddConversationListenerParams;
use codex_protocol::mcp_protocol::AddConversationSubscriptionResponse;
use codex_protocol::mcp_protocol::ConversationId;
use codex_protocol::mcp_protocol::InputItem;
use codex_protocol::mcp_protocol::NewConversationParams;
use codex_protocol::mcp_protocol::NewConversationResponse;
use codex_protocol::mcp_protocol::SendUserMessageParams;
use codex_protocol::mcp_protocol::SendUserMessageResponse;
use mcp_test_support::McpProcess;
use mcp_test_support::create_final_assistant_message_sse_response;
use mcp_test_support::create_mock_chat_completions_server;
use mcp_types::JSONRPC_VERSION;
use mcp_test_support::to_response;
use mcp_types::JSONRPCNotification;
use mcp_types::JSONRPCResponse;
use mcp_types::RequestId;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -31,76 +36,94 @@ async fn test_send_message_success() {
create_config_toml(codex_home.path(), &server.uri()).expect("write config.toml");
// Start MCP server process and initialize.
let mut mcp_process = McpProcess::new(codex_home.path())
let mut mcp = McpProcess::new(codex_home.path())
.await
.expect("spawn mcp process");
timeout(DEFAULT_READ_TIMEOUT, mcp_process.initialize())
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize())
.await
.expect("init timed out")
.expect("init failed");
// Kick off a Codex session so we have a valid session_id.
let codex_request_id = mcp_process
.send_codex_tool_call(CodexToolCallParam {
prompt: "Start a session".to_string(),
..Default::default()
})
// Start a conversation using the new wire API.
let new_conv_id = mcp
.send_new_conversation_request(NewConversationParams::default())
.await
.expect("send codex tool call");
// Wait for the session_configured event to get the session_id.
let session_id = mcp_process
.read_stream_until_configured_response_message()
.await
.expect("read session_configured");
// The original codex call will finish quickly given our mock; consume its response.
timeout(
.expect("send newConversation");
let new_conv_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp_process.read_stream_until_response_message(RequestId::Integer(codex_request_id)),
mcp.read_stream_until_response_message(RequestId::Integer(new_conv_id)),
)
.await
.expect("codex response timeout")
.expect("codex response error");
.expect("newConversation timeout")
.expect("newConversation resp");
let NewConversationResponse {
conversation_id, ..
} = to_response::<_>(new_conv_resp).expect("deserialize newConversation response");
// Now exercise the send-user-message tool.
let send_msg_request_id = mcp_process
.send_user_message_tool_call("Hello again", &session_id)
// 2) addConversationListener
let add_listener_id = mcp
.send_add_conversation_listener_request(AddConversationListenerParams { conversation_id })
.await
.expect("send send-message tool call");
.expect("send addConversationListener");
let add_listener_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(add_listener_id)),
)
.await
.expect("addConversationListener timeout")
.expect("addConversationListener resp");
let AddConversationSubscriptionResponse { subscription_id: _ } =
to_response::<_>(add_listener_resp).expect("deserialize addConversationListener response");
// Now exercise sendUserMessage twice.
send_message("Hello", conversation_id, &mut mcp).await;
send_message("Hello again", conversation_id, &mut mcp).await;
}
#[expect(clippy::expect_used)]
async fn send_message(message: &str, conversation_id: ConversationId, mcp: &mut McpProcess) {
// Now exercise sendUserMessage.
let send_id = mcp
.send_send_user_message_request(SendUserMessageParams {
conversation_id,
items: vec![InputItem::Text {
text: message.to_string(),
}],
})
.await
.expect("send sendUserMessage");
let response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp_process.read_stream_until_response_message(RequestId::Integer(send_msg_request_id)),
mcp.read_stream_until_response_message(RequestId::Integer(send_id)),
)
.await
.expect("send-user-message response timeout")
.expect("send-user-message response error");
.expect("sendUserMessage response timeout")
.expect("sendUserMessage response error");
let _ok: SendUserMessageResponse = to_response::<SendUserMessageResponse>(response)
.expect("deserialize sendUserMessage response");
// Verify the task_finished notification is received.
// Note this also ensures that the final request to the server was made.
let task_finished_notification: JSONRPCNotification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("codex/event/task_complete"),
)
.await
.expect("task_finished_notification timeout")
.expect("task_finished_notification resp");
let serde_json::Value::Object(map) = task_finished_notification
.params
.expect("notification should have params")
else {
panic!("task_finished_notification should have params");
};
assert_eq!(
JSONRPCResponse {
jsonrpc: JSONRPC_VERSION.into(),
id: RequestId::Integer(send_msg_request_id),
result: json!({
"content": [
{
"text": "{\"status\":\"ok\"}",
"type": "text",
}
],
"isError": false,
"structuredContent": {
"status": "ok"
}
}),
},
response
map.get("conversationId")
.expect("should have conversationId"),
&serde_json::Value::String(conversation_id.to_string())
);
// wait for the server to hear the user message
sleep(Duration::from_secs(5));
// Ensure the server and tempdir live until end of test
drop(server);
}
#[tokio::test]
@@ -113,24 +136,26 @@ async fn test_send_message_session_not_found() {
.expect("timeout")
.expect("init");
let unknown = uuid::Uuid::new_v4().to_string();
let unknown = ConversationId(uuid::Uuid::new_v4());
let req_id = mcp
.send_user_message_tool_call("ping", &unknown)
.send_send_user_message_request(SendUserMessageParams {
conversation_id: unknown,
items: vec![InputItem::Text {
text: "ping".to_string(),
}],
})
.await
.expect("send tool");
.expect("send sendUserMessage");
let resp: JSONRPCResponse = timeout(
// Expect an error response for unknown conversation.
let err = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
mcp.read_stream_until_error_message(RequestId::Integer(req_id)),
)
.await
.expect("timeout")
.expect("resp");
let result = resp.result.clone();
let content = result["content"][0]["text"].as_str().unwrap_or("");
assert!(content.contains("Session does not exist"));
assert_eq!(result["isError"], json!(true));
.expect("error");
assert_eq!(err.id, RequestId::Integer(req_id));
}
// ---------------------------------------------------------------------------
+1
View File
@@ -9,3 +9,4 @@ workspace = true
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
ts-rs = { version = "11", features = ["serde-json-impl"] }
+4 -2
View File
@@ -17,9 +17,9 @@ from typing import Any, Literal
SCHEMA_VERSION = "2025-06-18"
JSONRPC_VERSION = "2.0"
STANDARD_DERIVE = "#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]\n"
STANDARD_DERIVE = "#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]\n"
STANDARD_HASHABLE_DERIVE = (
"#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq)]\n"
"#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq, TS)]\n"
)
# Will be populated with the schema's `definitions` map in `main()` so that
@@ -75,6 +75,8 @@ use serde::Serialize;
use serde::de::DeserializeOwned;
use std::convert::TryFrom;
use ts_rs::TS;
pub const MCP_SCHEMA_VERSION: &str = "{SCHEMA_VERSION}";
pub const JSONRPC_VERSION: &str = "{JSONRPC_VERSION}";
+126 -124
View File
@@ -10,6 +10,8 @@ use serde::Serialize;
use serde::de::DeserializeOwned;
use std::convert::TryFrom;
use ts_rs::TS;
pub const MCP_SCHEMA_VERSION: &str = "2025-06-18";
pub const JSONRPC_VERSION: &str = "2.0";
@@ -31,7 +33,7 @@ fn default_jsonrpc() -> String {
}
/// Optional annotations for the client. The client can use annotations to inform how objects are used or displayed
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct Annotations {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audience: Option<Vec<Role>>,
@@ -46,7 +48,7 @@ pub struct Annotations {
}
/// Audio provided to or from an LLM.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct AudioContent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
@@ -57,14 +59,14 @@ pub struct AudioContent {
}
/// Base interface for metadata with name (identifier) and title (display name) properties.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct BaseMetadata {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct BlobResourceContents {
pub blob: String,
#[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
@@ -72,7 +74,7 @@ pub struct BlobResourceContents {
pub uri: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct BooleanSchema {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
@@ -83,7 +85,7 @@ pub struct BooleanSchema {
pub r#type: String, // &'static str = "boolean"
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum CallToolRequest {}
impl ModelContextProtocolRequest for CallToolRequest {
@@ -92,7 +94,7 @@ impl ModelContextProtocolRequest for CallToolRequest {
type Result = CallToolResult;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct CallToolRequestParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arguments: Option<serde_json::Value>,
@@ -100,7 +102,7 @@ pub struct CallToolRequestParams {
}
/// The server's response to a tool call.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct CallToolResult {
pub content: Vec<ContentBlock>,
#[serde(rename = "isError", default, skip_serializing_if = "Option::is_none")]
@@ -121,7 +123,7 @@ impl From<CallToolResult> for serde_json::Value {
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum CancelledNotification {}
impl ModelContextProtocolNotification for CancelledNotification {
@@ -129,7 +131,7 @@ impl ModelContextProtocolNotification for CancelledNotification {
type Params = CancelledNotificationParams;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct CancelledNotificationParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
@@ -138,7 +140,7 @@ pub struct CancelledNotificationParams {
}
/// Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ClientCapabilities {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub elicitation: Option<serde_json::Value>,
@@ -151,7 +153,7 @@ pub struct ClientCapabilities {
}
/// Present if the client supports listing roots.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ClientCapabilitiesRoots {
#[serde(
rename = "listChanged",
@@ -161,7 +163,7 @@ pub struct ClientCapabilitiesRoots {
pub list_changed: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(untagged)]
pub enum ClientNotification {
CancelledNotification(CancelledNotification),
@@ -170,7 +172,7 @@ pub enum ClientNotification {
RootsListChangedNotification(RootsListChangedNotification),
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(tag = "method", content = "params")]
pub enum ClientRequest {
#[serde(rename = "initialize")]
@@ -203,7 +205,7 @@ pub enum ClientRequest {
CompleteRequest(<CompleteRequest as ModelContextProtocolRequest>::Params),
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(untagged)]
pub enum ClientResult {
Result(Result),
@@ -212,7 +214,7 @@ pub enum ClientResult {
ElicitResult(ElicitResult),
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum CompleteRequest {}
impl ModelContextProtocolRequest for CompleteRequest {
@@ -221,7 +223,7 @@ impl ModelContextProtocolRequest for CompleteRequest {
type Result = CompleteResult;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct CompleteRequestParams {
pub argument: CompleteRequestParamsArgument,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -230,20 +232,20 @@ pub struct CompleteRequestParams {
}
/// Additional, optional context for completions
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct CompleteRequestParamsContext {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arguments: Option<serde_json::Value>,
}
/// The argument's information
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct CompleteRequestParamsArgument {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(untagged)]
pub enum CompleteRequestParamsRef {
PromptReference(PromptReference),
@@ -251,12 +253,12 @@ pub enum CompleteRequestParamsRef {
}
/// The server's response to a completion/complete request
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct CompleteResult {
pub completion: CompleteResultCompletion,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct CompleteResultCompletion {
#[serde(rename = "hasMore", default, skip_serializing_if = "Option::is_none")]
pub has_more: Option<bool>,
@@ -273,7 +275,7 @@ impl From<CompleteResult> for serde_json::Value {
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(untagged)]
pub enum ContentBlock {
TextContent(TextContent),
@@ -283,7 +285,7 @@ pub enum ContentBlock {
EmbeddedResource(EmbeddedResource),
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum CreateMessageRequest {}
impl ModelContextProtocolRequest for CreateMessageRequest {
@@ -292,7 +294,7 @@ impl ModelContextProtocolRequest for CreateMessageRequest {
type Result = CreateMessageResult;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct CreateMessageRequestParams {
#[serde(
rename = "includeContext",
@@ -328,7 +330,7 @@ pub struct CreateMessageRequestParams {
}
/// The client's response to a sampling/create_message request from the server. The client should inform the user before returning the sampled message, to allow them to inspect the response (human in the loop) and decide whether to allow the server to see it.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct CreateMessageResult {
pub content: CreateMessageResultContent,
pub model: String,
@@ -341,7 +343,7 @@ pub struct CreateMessageResult {
pub stop_reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(untagged)]
pub enum CreateMessageResultContent {
TextContent(TextContent),
@@ -357,10 +359,10 @@ impl From<CreateMessageResult> for serde_json::Value {
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct Cursor(String);
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum ElicitRequest {}
impl ModelContextProtocolRequest for ElicitRequest {
@@ -369,7 +371,7 @@ impl ModelContextProtocolRequest for ElicitRequest {
type Result = ElicitResult;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ElicitRequestParams {
pub message: String,
#[serde(rename = "requestedSchema")]
@@ -378,7 +380,7 @@ pub struct ElicitRequestParams {
/// A restricted subset of JSON Schema.
/// Only top-level properties are allowed, without nesting.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ElicitRequestParamsRequestedSchema {
pub properties: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -387,7 +389,7 @@ pub struct ElicitRequestParamsRequestedSchema {
}
/// The client's response to an elicitation request.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ElicitResult {
pub action: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -406,7 +408,7 @@ impl From<ElicitResult> for serde_json::Value {
///
/// It is up to the client how best to render embedded resources for the benefit
/// of the LLM and/or the user.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct EmbeddedResource {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
@@ -414,7 +416,7 @@ pub struct EmbeddedResource {
pub r#type: String, // &'static str = "resource"
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(untagged)]
pub enum EmbeddedResourceResource {
TextResourceContents(TextResourceContents),
@@ -423,7 +425,7 @@ pub enum EmbeddedResourceResource {
pub type EmptyResult = Result;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct EnumSchema {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
@@ -435,7 +437,7 @@ pub struct EnumSchema {
pub r#type: String, // &'static str = "string"
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum GetPromptRequest {}
impl ModelContextProtocolRequest for GetPromptRequest {
@@ -444,7 +446,7 @@ impl ModelContextProtocolRequest for GetPromptRequest {
type Result = GetPromptResult;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct GetPromptRequestParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arguments: Option<serde_json::Value>,
@@ -452,7 +454,7 @@ pub struct GetPromptRequestParams {
}
/// The server's response to a prompts/get request from the client.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct GetPromptResult {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
@@ -468,7 +470,7 @@ impl From<GetPromptResult> for serde_json::Value {
}
/// An image provided to or from an LLM.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ImageContent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
@@ -479,7 +481,7 @@ pub struct ImageContent {
}
/// Describes the name and version of an MCP implementation, with an optional title for UI representation.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct Implementation {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -487,7 +489,7 @@ pub struct Implementation {
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum InitializeRequest {}
impl ModelContextProtocolRequest for InitializeRequest {
@@ -496,7 +498,7 @@ impl ModelContextProtocolRequest for InitializeRequest {
type Result = InitializeResult;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct InitializeRequestParams {
pub capabilities: ClientCapabilities,
#[serde(rename = "clientInfo")]
@@ -506,7 +508,7 @@ pub struct InitializeRequestParams {
}
/// After receiving an initialize request from the client, the server sends this response.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct InitializeResult {
pub capabilities: ServerCapabilities,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -525,7 +527,7 @@ impl From<InitializeResult> for serde_json::Value {
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum InitializedNotification {}
impl ModelContextProtocolNotification for InitializedNotification {
@@ -534,7 +536,7 @@ impl ModelContextProtocolNotification for InitializedNotification {
}
/// A response to a request that indicates an error occurred.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct JSONRPCError {
pub error: JSONRPCErrorError,
pub id: RequestId,
@@ -542,7 +544,7 @@ pub struct JSONRPCError {
pub jsonrpc: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct JSONRPCErrorError {
pub code: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -551,7 +553,7 @@ pub struct JSONRPCErrorError {
}
/// Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(untagged)]
pub enum JSONRPCMessage {
Request(JSONRPCRequest),
@@ -561,7 +563,7 @@ pub enum JSONRPCMessage {
}
/// A notification which does not expect a response.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct JSONRPCNotification {
#[serde(rename = "jsonrpc", default = "default_jsonrpc")]
pub jsonrpc: String,
@@ -571,7 +573,7 @@ pub struct JSONRPCNotification {
}
/// A request that expects a response.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct JSONRPCRequest {
pub id: RequestId,
#[serde(rename = "jsonrpc", default = "default_jsonrpc")]
@@ -582,7 +584,7 @@ pub struct JSONRPCRequest {
}
/// A successful (non-error) response to a request.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct JSONRPCResponse {
pub id: RequestId,
#[serde(rename = "jsonrpc", default = "default_jsonrpc")]
@@ -590,7 +592,7 @@ pub struct JSONRPCResponse {
pub result: Result,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum ListPromptsRequest {}
impl ModelContextProtocolRequest for ListPromptsRequest {
@@ -599,14 +601,14 @@ impl ModelContextProtocolRequest for ListPromptsRequest {
type Result = ListPromptsResult;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ListPromptsRequestParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
/// The server's response to a prompts/list request from the client.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ListPromptsResult {
#[serde(
rename = "nextCursor",
@@ -625,7 +627,7 @@ impl From<ListPromptsResult> for serde_json::Value {
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum ListResourceTemplatesRequest {}
impl ModelContextProtocolRequest for ListResourceTemplatesRequest {
@@ -634,14 +636,14 @@ impl ModelContextProtocolRequest for ListResourceTemplatesRequest {
type Result = ListResourceTemplatesResult;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ListResourceTemplatesRequestParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
/// The server's response to a resources/templates/list request from the client.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ListResourceTemplatesResult {
#[serde(
rename = "nextCursor",
@@ -661,7 +663,7 @@ impl From<ListResourceTemplatesResult> for serde_json::Value {
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum ListResourcesRequest {}
impl ModelContextProtocolRequest for ListResourcesRequest {
@@ -670,14 +672,14 @@ impl ModelContextProtocolRequest for ListResourcesRequest {
type Result = ListResourcesResult;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ListResourcesRequestParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
/// The server's response to a resources/list request from the client.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ListResourcesResult {
#[serde(
rename = "nextCursor",
@@ -696,7 +698,7 @@ impl From<ListResourcesResult> for serde_json::Value {
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum ListRootsRequest {}
impl ModelContextProtocolRequest for ListRootsRequest {
@@ -708,7 +710,7 @@ impl ModelContextProtocolRequest for ListRootsRequest {
/// The client's response to a roots/list request from the server.
/// This result contains an array of Root objects, each representing a root directory
/// or file that the server can operate on.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ListRootsResult {
pub roots: Vec<Root>,
}
@@ -721,7 +723,7 @@ impl From<ListRootsResult> for serde_json::Value {
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum ListToolsRequest {}
impl ModelContextProtocolRequest for ListToolsRequest {
@@ -730,14 +732,14 @@ impl ModelContextProtocolRequest for ListToolsRequest {
type Result = ListToolsResult;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ListToolsRequestParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
/// The server's response to a tools/list request from the client.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ListToolsResult {
#[serde(
rename = "nextCursor",
@@ -760,7 +762,7 @@ impl From<ListToolsResult> for serde_json::Value {
///
/// These map to syslog message severities, as specified in RFC-5424:
/// https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum LoggingLevel {
#[serde(rename = "alert")]
Alert,
@@ -780,7 +782,7 @@ pub enum LoggingLevel {
Warning,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum LoggingMessageNotification {}
impl ModelContextProtocolNotification for LoggingMessageNotification {
@@ -788,7 +790,7 @@ impl ModelContextProtocolNotification for LoggingMessageNotification {
type Params = LoggingMessageNotificationParams;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct LoggingMessageNotificationParams {
pub data: serde_json::Value,
pub level: LoggingLevel,
@@ -800,7 +802,7 @@ pub struct LoggingMessageNotificationParams {
///
/// Keys not declared here are currently left unspecified by the spec and are up
/// to the client to interpret.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ModelHint {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
@@ -817,7 +819,7 @@ pub struct ModelHint {
/// These preferences are always advisory. The client MAY ignore them. It is also
/// up to the client to decide how to interpret these preferences and how to
/// balance them against other considerations.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ModelPreferences {
#[serde(
rename = "costPriority",
@@ -841,14 +843,14 @@ pub struct ModelPreferences {
pub speed_priority: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct Notification {
pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct NumberSchema {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
@@ -861,20 +863,20 @@ pub struct NumberSchema {
pub r#type: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct PaginatedRequest {
pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<PaginatedRequestParams>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct PaginatedRequestParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct PaginatedResult {
#[serde(
rename = "nextCursor",
@@ -892,7 +894,7 @@ impl From<PaginatedResult> for serde_json::Value {
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum PingRequest {}
impl ModelContextProtocolRequest for PingRequest {
@@ -903,7 +905,7 @@ impl ModelContextProtocolRequest for PingRequest {
/// Restricted schema definitions that only allow primitive types
/// without nested objects or arrays.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(untagged)]
pub enum PrimitiveSchemaDefinition {
StringSchema(StringSchema),
@@ -912,7 +914,7 @@ pub enum PrimitiveSchemaDefinition {
EnumSchema(EnumSchema),
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum ProgressNotification {}
impl ModelContextProtocolNotification for ProgressNotification {
@@ -920,7 +922,7 @@ impl ModelContextProtocolNotification for ProgressNotification {
type Params = ProgressNotificationParams;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ProgressNotificationParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
@@ -931,7 +933,7 @@ pub struct ProgressNotificationParams {
pub total: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq, TS)]
#[serde(untagged)]
pub enum ProgressToken {
String(String),
@@ -939,7 +941,7 @@ pub enum ProgressToken {
}
/// A prompt or prompt template that the server offers.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct Prompt {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
@@ -951,7 +953,7 @@ pub struct Prompt {
}
/// Describes an argument that a prompt can accept.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct PromptArgument {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
@@ -962,7 +964,7 @@ pub struct PromptArgument {
pub title: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum PromptListChangedNotification {}
impl ModelContextProtocolNotification for PromptListChangedNotification {
@@ -974,14 +976,14 @@ impl ModelContextProtocolNotification for PromptListChangedNotification {
///
/// This is similar to `SamplingMessage`, but also supports the embedding of
/// resources from the MCP server.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct PromptMessage {
pub content: ContentBlock,
pub role: Role,
}
/// Identifies a prompt.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct PromptReference {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -989,7 +991,7 @@ pub struct PromptReference {
pub r#type: String, // &'static str = "ref/prompt"
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum ReadResourceRequest {}
impl ModelContextProtocolRequest for ReadResourceRequest {
@@ -998,18 +1000,18 @@ impl ModelContextProtocolRequest for ReadResourceRequest {
type Result = ReadResourceResult;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ReadResourceRequestParams {
pub uri: String,
}
/// The server's response to a resources/read request from the client.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ReadResourceResult {
pub contents: Vec<ReadResourceResultContents>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(untagged)]
pub enum ReadResourceResultContents {
TextResourceContents(TextResourceContents),
@@ -1024,14 +1026,14 @@ impl From<ReadResourceResult> for serde_json::Value {
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct Request {
pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq, TS)]
#[serde(untagged)]
pub enum RequestId {
String(String),
@@ -1039,7 +1041,7 @@ pub enum RequestId {
}
/// A known resource that the server is capable of reading.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct Resource {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
@@ -1056,7 +1058,7 @@ pub struct Resource {
}
/// The contents of a specific resource or sub-resource.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ResourceContents {
#[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
@@ -1066,7 +1068,7 @@ pub struct ResourceContents {
/// A resource that the server is capable of reading, included in a prompt or tool call result.
///
/// Note: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ResourceLink {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
@@ -1083,7 +1085,7 @@ pub struct ResourceLink {
pub uri: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum ResourceListChangedNotification {}
impl ModelContextProtocolNotification for ResourceListChangedNotification {
@@ -1092,7 +1094,7 @@ impl ModelContextProtocolNotification for ResourceListChangedNotification {
}
/// A template description for resources available on the server.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ResourceTemplate {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
@@ -1108,13 +1110,13 @@ pub struct ResourceTemplate {
}
/// A reference to a resource or resource template definition.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ResourceTemplateReference {
pub r#type: String, // &'static str = "ref/resource"
pub uri: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum ResourceUpdatedNotification {}
impl ModelContextProtocolNotification for ResourceUpdatedNotification {
@@ -1122,7 +1124,7 @@ impl ModelContextProtocolNotification for ResourceUpdatedNotification {
type Params = ResourceUpdatedNotificationParams;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ResourceUpdatedNotificationParams {
pub uri: String,
}
@@ -1130,7 +1132,7 @@ pub struct ResourceUpdatedNotificationParams {
pub type Result = serde_json::Value;
/// The sender or recipient of messages and data in a conversation.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum Role {
#[serde(rename = "assistant")]
Assistant,
@@ -1139,14 +1141,14 @@ pub enum Role {
}
/// Represents a root directory or file that the server can operate on.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct Root {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub uri: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum RootsListChangedNotification {}
impl ModelContextProtocolNotification for RootsListChangedNotification {
@@ -1155,13 +1157,13 @@ impl ModelContextProtocolNotification for RootsListChangedNotification {
}
/// Describes a message issued to or received from an LLM API.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct SamplingMessage {
pub content: SamplingMessageContent,
pub role: Role,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(untagged)]
pub enum SamplingMessageContent {
TextContent(TextContent),
@@ -1170,7 +1172,7 @@ pub enum SamplingMessageContent {
}
/// Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ServerCapabilities {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completions: Option<serde_json::Value>,
@@ -1187,7 +1189,7 @@ pub struct ServerCapabilities {
}
/// Present if the server offers any tools to call.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ServerCapabilitiesTools {
#[serde(
rename = "listChanged",
@@ -1198,7 +1200,7 @@ pub struct ServerCapabilitiesTools {
}
/// Present if the server offers any resources to read.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ServerCapabilitiesResources {
#[serde(
rename = "listChanged",
@@ -1211,7 +1213,7 @@ pub struct ServerCapabilitiesResources {
}
/// Present if the server offers any prompt templates.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ServerCapabilitiesPrompts {
#[serde(
rename = "listChanged",
@@ -1221,7 +1223,7 @@ pub struct ServerCapabilitiesPrompts {
pub list_changed: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(tag = "method", content = "params")]
pub enum ServerNotification {
#[serde(rename = "notifications/cancelled")]
@@ -1250,7 +1252,7 @@ pub enum ServerNotification {
),
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(untagged)]
pub enum ServerRequest {
PingRequest(PingRequest),
@@ -1259,7 +1261,7 @@ pub enum ServerRequest {
ElicitRequest(ElicitRequest),
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum ServerResult {
@@ -1275,7 +1277,7 @@ pub enum ServerResult {
CompleteResult(CompleteResult),
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum SetLevelRequest {}
impl ModelContextProtocolRequest for SetLevelRequest {
@@ -1284,12 +1286,12 @@ impl ModelContextProtocolRequest for SetLevelRequest {
type Result = Result;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct SetLevelRequestParams {
pub level: LoggingLevel,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct StringSchema {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
@@ -1304,7 +1306,7 @@ pub struct StringSchema {
pub r#type: String, // &'static str = "string"
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum SubscribeRequest {}
impl ModelContextProtocolRequest for SubscribeRequest {
@@ -1313,13 +1315,13 @@ impl ModelContextProtocolRequest for SubscribeRequest {
type Result = Result;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct SubscribeRequestParams {
pub uri: String,
}
/// Text provided to or from an LLM.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct TextContent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
@@ -1327,7 +1329,7 @@ pub struct TextContent {
pub r#type: String, // &'static str = "text"
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct TextResourceContents {
#[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
@@ -1336,7 +1338,7 @@ pub struct TextResourceContents {
}
/// Definition for a tool the client can call.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct Tool {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
@@ -1357,7 +1359,7 @@ pub struct Tool {
/// An optional JSON Schema object defining the structure of the tool's output returned in
/// the structuredContent field of a CallToolResult.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ToolOutputSchema {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub properties: Option<serde_json::Value>,
@@ -1367,7 +1369,7 @@ pub struct ToolOutputSchema {
}
/// A JSON Schema object defining the expected parameters for the tool.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ToolInputSchema {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub properties: Option<serde_json::Value>,
@@ -1384,7 +1386,7 @@ pub struct ToolInputSchema {
///
/// Clients should never make tool use decisions based on ToolAnnotations
/// received from untrusted servers.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct ToolAnnotations {
#[serde(
rename = "destructiveHint",
@@ -1414,7 +1416,7 @@ pub struct ToolAnnotations {
pub title: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum ToolListChangedNotification {}
impl ModelContextProtocolNotification for ToolListChangedNotification {
@@ -1422,7 +1424,7 @@ impl ModelContextProtocolNotification for ToolListChangedNotification {
type Params = Option<serde_json::Value>;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub enum UnsubscribeRequest {}
impl ModelContextProtocolRequest for UnsubscribeRequest {
@@ -1431,7 +1433,7 @@ impl ModelContextProtocolRequest for UnsubscribeRequest {
type Result = Result;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
pub struct UnsubscribeRequestParams {
pub uri: String,
}
+3
View File
@@ -18,3 +18,6 @@ serde_json = "1"
strum = "0.27.2"
strum_macros = "0.27.2"
uuid = { version = "1", features = ["serde", "v4"] }
[dev-dependencies]
pretty_assertions = "1.4.1"
+15
View File
@@ -30,3 +30,18 @@ pub enum ReasoningSummary {
/// Option to disable reasoning summaries.
None,
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Default, Serialize, Display)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum SandboxMode {
#[serde(rename = "read-only")]
#[default]
ReadOnly,
#[serde(rename = "workspace-write")]
WorkspaceWrite,
#[serde(rename = "danger-full-access")]
DangerFullAccess,
}
+1
View File
@@ -1,4 +1,5 @@
pub mod config_types;
pub mod mcp_protocol;
pub mod message_history;
pub mod parse_command;
pub mod plan_tool;
@@ -2,19 +2,17 @@ use std::collections::HashMap;
use std::fmt::Display;
use std::path::PathBuf;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::FileChange;
use codex_core::protocol::ReviewDecision;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::TurnAbortReason;
use codex_core::protocol_config_types::ReasoningEffort;
use codex_core::protocol_config_types::ReasoningSummary;
use crate::config_types::ReasoningEffort;
use crate::config_types::ReasoningSummary;
use crate::config_types::SandboxMode;
use crate::protocol::AskForApproval;
use crate::protocol::FileChange;
use crate::protocol::ReviewDecision;
use crate::protocol::SandboxPolicy;
use crate::protocol::TurnAbortReason;
use mcp_types::RequestId;
use serde::Deserialize;
use serde::Serialize;
use crate::codex_tool_config::CodexToolCallApprovalPolicy;
use crate::codex_tool_config::CodexToolCallSandboxMode;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -91,11 +89,11 @@ pub struct NewConversationParams {
/// Approval policy for shell commands generated by the model:
/// `untrusted`, `on-failure`, `on-request`, `never`.
#[serde(skip_serializing_if = "Option::is_none")]
pub approval_policy: Option<CodexToolCallApprovalPolicy>,
pub approval_policy: Option<AskForApproval>,
/// Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`.
#[serde(skip_serializing_if = "Option::is_none")]
pub sandbox: Option<CodexToolCallSandboxMode>,
pub sandbox: Option<SandboxMode>,
/// Individual config settings that will override what is in
/// CODEX_HOME/config.toml.
@@ -308,7 +306,7 @@ mod tests {
model: Some("gpt-5".to_string()),
profile: None,
cwd: None,
approval_policy: Some(CodexToolCallApprovalPolicy::OnRequest),
approval_policy: Some(AskForApproval::OnRequest),
sandbox: None,
config: None,
base_instructions: None,
+1 -1
View File
@@ -752,7 +752,7 @@ pub struct TurnAbortedEvent {
pub reason: TurnAbortReason,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TurnAbortReason {
Interrupted,
@@ -101,7 +101,7 @@ pub(crate) fn render_rows(
if Some(i) == state.selected_idx {
cell = cell.style(
Style::default()
.fg(Color::Yellow)
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
);
} else if *is_current {
-4
View File
@@ -1,4 +0,0 @@
use ratatui::style::Color;
pub(crate) const LIGHT_BLUE: Color = Color::Rgb(134, 238, 255);
pub(crate) const SUCCESS_GREEN: Color = Color::Rgb(169, 230, 158);
+9 -9
View File
@@ -1,4 +1,3 @@
use crate::colors::LIGHT_BLUE;
use crate::diff_render::create_diff_summary;
use crate::exec_command::relativize_to_home;
use crate::exec_command::strip_bash_lc_and_escape;
@@ -252,12 +251,13 @@ fn new_parsed_command(
lines.push(Line::from(spans));
}
Some(o) if o.exit_code == 0 => {
lines.push(Line::from("✓ Completed".green().bold()));
lines.push(Line::from(vec!["✓".green(), " Completed".into()]));
}
Some(o) => {
lines.push(Line::from(
format!("✗ Failed (exit {})", o.exit_code).red().bold(),
));
lines.push(Line::from(vec![
"✗".red(),
format!(" Failed (exit {})", o.exit_code).into(),
]));
}
};
@@ -304,7 +304,7 @@ fn new_parsed_command(
let prefix = if j == 0 { first_prefix } else { " " };
lines.push(Line::from(vec![
Span::styled(prefix, Style::default().add_modifier(Modifier::DIM)),
Span::styled(line_text.to_string(), Style::default().fg(LIGHT_BLUE)),
line_text.to_string().dim(),
]));
}
}
@@ -711,7 +711,7 @@ pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> PlainHistoryCell {
Span::styled(
step,
Style::default()
.fg(Color::Blue)
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
),
@@ -913,9 +913,9 @@ fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
.unwrap_or_default();
let invocation_spans = vec![
Span::styled(invocation.server.clone(), Style::default().fg(Color::Blue)),
Span::styled(invocation.server.clone(), Style::default().fg(Color::Cyan)),
Span::raw("."),
Span::styled(invocation.tool.clone(), Style::default().fg(Color::Blue)),
Span::styled(invocation.tool.clone(), Style::default().fg(Color::Cyan)),
Span::raw("("),
Span::styled(args_str, Style::default().add_modifier(Modifier::DIM)),
Span::raw(")"),
+2 -2
View File
@@ -2,6 +2,7 @@
// The standalone `codex-tui` binary prints a short help message before the
// alternate‑screen mode starts; that file opts‑out locally via `allow`.
#![deny(clippy::print_stdout, clippy::print_stderr)]
#![deny(clippy::disallowed_methods)]
use app::App;
use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
use codex_core::config::Config;
@@ -9,11 +10,11 @@ use codex_core::config::ConfigOverrides;
use codex_core::config::ConfigToml;
use codex_core::config::find_codex_home;
use codex_core::config::load_config_as_toml_with_cli_overrides;
use codex_core::config_types::SandboxMode;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::SandboxPolicy;
use codex_login::CodexAuth;
use codex_ollama::DEFAULT_OSS_MODEL;
use codex_protocol::config_types::SandboxMode;
use std::fs::OpenOptions;
use std::path::PathBuf;
use tracing::error;
@@ -28,7 +29,6 @@ mod bottom_pane;
mod chatwidget;
mod citation_regex;
mod cli;
mod colors;
mod common;
pub mod custom_terminal;
mod diff_render;
+14 -26
View File
@@ -9,6 +9,7 @@ use ratatui::prelude::Widget;
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::Paragraph;
@@ -19,8 +20,6 @@ use codex_login::AuthMode;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::colors::LIGHT_BLUE;
use crate::colors::SUCCESS_GREEN;
use crate::onboarding::onboarding_screen::KeyboardHandler;
use crate::onboarding::onboarding_screen::StepStateProvider;
use crate::shimmer::shimmer_spans;
@@ -131,11 +130,8 @@ impl AuthModeWidget {
let line1 = if is_selected {
Line::from(vec![
Span::styled(
format!("{} {}. ", caret, idx + 1),
Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM),
),
Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)),
format!("{} {}. ", caret, idx + 1).cyan().dim(),
text.to_string().cyan(),
])
} else {
Line::from(format!(" {}. {text}", idx + 1))
@@ -143,7 +139,8 @@ impl AuthModeWidget {
let line2 = if is_selected {
Line::from(format!(" {description}"))
.style(Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM))
.fg(Color::Cyan)
.add_modifier(Modifier::DIM)
} else {
Line::from(format!(" {description}"))
.style(Style::default().add_modifier(Modifier::DIM))
@@ -166,6 +163,8 @@ impl AuthModeWidget {
));
lines.push(Line::from(""));
lines.push(
// AE: Following styles.md, this should probably be Cyan because it's a user input tip.
// But leaving this for a future cleanup.
Line::from(" Press Enter to continue")
.style(Style::default().add_modifier(Modifier::DIM)),
);
@@ -197,12 +196,7 @@ impl AuthModeWidget {
lines.push(Line::from(" If the link doesn't open automatically, open the following link to authenticate:"));
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
state.auth_url.as_str(),
Style::default()
.fg(LIGHT_BLUE)
.add_modifier(Modifier::UNDERLINED),
),
state.auth_url.as_str().cyan().underlined(),
]));
lines.push(Line::from(""));
}
@@ -218,8 +212,7 @@ impl AuthModeWidget {
fn render_chatgpt_success_message(&self, area: Rect, buf: &mut Buffer) {
let lines = vec![
Line::from("✓ Signed in with your ChatGPT account")
.style(Style::default().fg(SUCCESS_GREEN)),
Line::from("✓ Signed in with your ChatGPT account").fg(Color::Green),
Line::from(""),
Line::from("> Before you start:"),
Line::from(""),
@@ -233,8 +226,7 @@ impl AuthModeWidget {
])
.style(Style::default().add_modifier(Modifier::DIM)),
Line::from(""),
Line::from(" Codex can make mistakes")
.style(Style::default().fg(Color::White)),
Line::from(" Codex can make mistakes"),
Line::from(" Review the code it writes and commands it runs")
.style(Style::default().add_modifier(Modifier::DIM)),
Line::from(""),
@@ -248,7 +240,7 @@ impl AuthModeWidget {
])
.style(Style::default().add_modifier(Modifier::DIM)),
Line::from(""),
Line::from(" Press Enter to continue").style(Style::default().fg(LIGHT_BLUE)),
Line::from(" Press Enter to continue").fg(Color::Cyan),
];
Paragraph::new(lines)
@@ -257,10 +249,7 @@ impl AuthModeWidget {
}
fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer) {
let lines = vec![
Line::from("✓ Signed in with your ChatGPT account")
.style(Style::default().fg(SUCCESS_GREEN)),
];
let lines = vec![Line::from("✓ Signed in with your ChatGPT account").fg(Color::Green)];
Paragraph::new(lines)
.wrap(Wrap { trim: false })
@@ -268,8 +257,7 @@ impl AuthModeWidget {
}
fn render_env_var_found(&self, area: Rect, buf: &mut Buffer) {
let lines =
vec![Line::from("✓ Using OPENAI_API_KEY").style(Style::default().fg(SUCCESS_GREEN))];
let lines = vec![Line::from("✓ Using OPENAI_API_KEY").fg(Color::Green)];
Paragraph::new(lines)
.wrap(Wrap { trim: false })
@@ -281,7 +269,7 @@ impl AuthModeWidget {
Line::from(
" To use Codex with the OpenAI API, set OPENAI_API_KEY in your environment",
)
.style(Style::default().fg(Color::Blue)),
.style(Style::default().fg(Color::Cyan)),
Line::from(""),
Line::from(" Press Enter to return")
.style(Style::default().add_modifier(Modifier::DIM)),
@@ -18,8 +18,6 @@ use ratatui::widgets::Paragraph;
use ratatui::widgets::WidgetRef;
use ratatui::widgets::Wrap;
use crate::colors::LIGHT_BLUE;
use crate::onboarding::onboarding_screen::KeyboardHandler;
use crate::onboarding::onboarding_screen::StepStateProvider;
@@ -77,13 +75,7 @@ impl WidgetRef for &TrustDirectoryWidget {
|idx: usize, option: TrustDirectorySelection, text: &str| -> Line<'static> {
let is_selected = self.highlighted == option;
if is_selected {
Line::from(vec![
Span::styled(
format!("> {}. ", idx + 1),
Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM),
),
Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)),
])
Line::from(format!("> {}. {text}", idx + 1)).cyan()
} else {
Line::from(format!(" {}. {}", idx + 1, text))
}
@@ -117,6 +109,8 @@ impl WidgetRef for &TrustDirectoryWidget {
lines.push(Line::from(format!(" {error}")).fg(Color::Red));
lines.push(Line::from(""));
}
// AE: Following styles.md, this should probably be Cyan because it's a user input tip.
// But leaving this for a future cleanup.
lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM));
Paragraph::new(lines)
+8 -3
View File
@@ -46,9 +46,14 @@ pub(crate) fn shimmer_spans(text: &str) -> Vec<Span<'static>> {
let brightness = 0.4 + 0.6 * t;
let level = (brightness * 255.0).clamp(0.0, 255.0) as u8;
let style = if has_true_color {
Style::default()
.fg(Color::Rgb(level, level, level))
.add_modifier(Modifier::BOLD)
// Allow custom RGB colors, as the implementation is thoughtfully
// adjusting the level of the default foreground color.
#[allow(clippy::disallowed_methods)]
{
Style::default()
.fg(Color::Rgb(level, level, level))
.add_modifier(Modifier::BOLD)
}
} else {
color_for_level(level)
};
+1 -1
View File
@@ -145,7 +145,7 @@ impl UserApprovalWidget<'_> {
} => {
let cmd = strip_bash_lc_and_escape(command);
let mut contents: Vec<Line> = to_command_display(
vec!["? ".fg(Color::Blue), "Codex wants to run ".bold()],
vec!["? ".fg(Color::Cyan), "Codex wants to run ".bold()],
cmd,
vec![],
);
+21
View File
@@ -0,0 +1,21 @@
# Headers, primary, and secondary text
- **Headers:** Use `bold`. For markdown with various header levels, leave in the `#` signs.
- **Primary text:** Default.
- **Secondary text:** Use `dim`.
# Foreground colors
- **Default:** Most of the time, just use the default foreground color. `reset` can help get it back.
- **User input tips, selection, and status indicators:** Use ANSI `cyan`.
- **Success and additions:** Use ANSI `green`.
- **Errors, failures and deletions:** Use ANSI `red`.
- **Codex:** Use ANSI `magenta`.
# Avoid
- Avoid custom colors because there's no guarantee that they'll contrast well or look good in various terminal color themes. (`shimmer.rs` is an exception that works well because we take the default colors and just adjust their levels.)
- Avoid ANSI `black` & `white` as foreground colors because the default terminal theme color will do a better job. (Use `reset` if you need to in order to get those.) The exception is if you need contrast rendering over a manually colored background.
- Avoid ANSI `blue` and `yellow` because for now the style guide doesn't use them. Prefer a foreground color mentioned above.
(There are some rules to try to catch this in `clippy.toml`.)