diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index 2fff3a4277..c8facffdb5 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -545,7 +545,7 @@ mod tests { use uuid::Uuid; #[test] - fn generated_ts_omits_undefined_unions_for_optionals() -> Result<()> { + fn generated_ts_has_no_optional_nullable_fields() -> Result<()> { let output_dir = std::env::temp_dir().join(format!("codex_ts_types_{}", Uuid::now_v7())); fs::create_dir(&output_dir)?; @@ -562,7 +562,7 @@ mod tests { generate_ts(&output_dir, None)?; let mut undefined_offenders = Vec::new(); - let mut missing_optional_marker = BTreeSet::new(); + let mut optional_nullable_offenders = BTreeSet::new(); let mut stack = vec![output_dir]; while let Some(dir) = stack.pop() { for entry in fs::read_dir(&dir)? { @@ -591,27 +591,80 @@ mod tests { let mut search_start = 0; while let Some(idx) = contents[search_start..].find("| null") { let abs_idx = search_start + idx; - let Some(colon_idx) = contents[..abs_idx].rfind(':') else { - search_start = abs_idx + 5; - continue; - }; + // Find the property-colon for this field by scanning forward + // from the start of the segment and ignoring nested braces, + // brackets, and parens. This avoids colons inside nested + // type literals like `{ [k in string]?: string }`. - let line_start_idx = contents[..colon_idx] - .rfind('\n') - .map(|i| i + 1) - .unwrap_or(0); + let line_start_idx = + contents[..abs_idx].rfind('\n').map(|i| i + 1).unwrap_or(0); let mut segment_start_idx = line_start_idx; - if let Some(rel_idx) = contents[line_start_idx..colon_idx].rfind(',') { + if let Some(rel_idx) = contents[line_start_idx..abs_idx].rfind(',') { segment_start_idx = segment_start_idx.max(line_start_idx + rel_idx + 1); } - if let Some(rel_idx) = contents[line_start_idx..colon_idx].rfind('{') { + if let Some(rel_idx) = contents[line_start_idx..abs_idx].rfind('{') { segment_start_idx = segment_start_idx.max(line_start_idx + rel_idx + 1); } - if let Some(rel_idx) = contents[line_start_idx..colon_idx].rfind('}') { + if let Some(rel_idx) = contents[line_start_idx..abs_idx].rfind('}') { segment_start_idx = segment_start_idx.max(line_start_idx + rel_idx + 1); } + // Scan forward for the colon that separates the field name from its type. + let mut level_brace = 0_i32; + let mut level_brack = 0_i32; + let mut level_paren = 0_i32; + let mut in_single = false; + let mut in_double = false; + let mut escape = false; + let mut prop_colon_idx = None; + for (i, ch) in contents[segment_start_idx..abs_idx].char_indices() { + let idx_abs = segment_start_idx + i; + if escape { + escape = false; + continue; + } + match ch { + '\\' => { + // Only treat as escape when inside a string. + if in_single || in_double { + escape = true; + } + } + '\'' => { + if !in_double { + in_single = !in_single; + } + } + '"' => { + if !in_single { + in_double = !in_double; + } + } + '{' if !in_single && !in_double => level_brace += 1, + '}' if !in_single && !in_double => level_brace -= 1, + '[' if !in_single && !in_double => level_brack += 1, + ']' if !in_single && !in_double => level_brack -= 1, + '(' if !in_single && !in_double => level_paren += 1, + ')' if !in_single && !in_double => level_paren -= 1, + ':' if !in_single + && !in_double + && level_brace == 0 + && level_brack == 0 + && level_paren == 0 => + { + prop_colon_idx = Some(idx_abs); + break; + } + _ => {} + } + } + + let Some(colon_idx) = prop_colon_idx else { + search_start = abs_idx + 5; + continue; + }; + let mut field_prefix = contents[segment_start_idx..colon_idx].trim(); if field_prefix.is_empty() { search_start = abs_idx + 5; @@ -640,25 +693,26 @@ mod tests { continue; } + // If the last non-whitespace before ':' is '?', then this is an + // optional field with a nullable type (i.e., "?: T | null"), + // which we explicitly disallow. if field_prefix.chars().rev().find(|c| !c.is_whitespace()) == Some('?') { - search_start = abs_idx + 5; - continue; + let line_number = + contents[..abs_idx].chars().filter(|c| *c == '\n').count() + 1; + let offending_line_end = contents[line_start_idx..] + .find('\n') + .map(|i| line_start_idx + i) + .unwrap_or(contents.len()); + let offending_snippet = + contents[line_start_idx..offending_line_end].trim(); + + optional_nullable_offenders.insert(format!( + "{}:{}: {offending_snippet}", + path.display(), + line_number + )); } - let line_number = - contents[..abs_idx].chars().filter(|c| *c == '\n').count() + 1; - let offending_line_end = contents[line_start_idx..] - .find('\n') - .map(|i| line_start_idx + i) - .unwrap_or(contents.len()); - let offending_snippet = contents[line_start_idx..offending_line_end].trim(); - - missing_optional_marker.insert(format!( - "{}:{}: {offending_snippet}", - path.display(), - line_number - )); - search_start = abs_idx + 5; } } @@ -670,12 +724,12 @@ mod tests { "Generated TypeScript still includes unions with `undefined` in {undefined_offenders:?}" ); - // If this test fails, it means that a struct field that is `Option` in Rust - // is being generated as `T | null` in TypeScript, without the optional marker - // (`?`). To fix this, add #[ts(optional_fields = nullable)] to the struct definition. + // If this assertion fails, it means a field was generated as + // "?: T | null" — i.e., both optional (undefined) and nullable (null). + // We only want either "?: T" or ": T | null". assert!( - missing_optional_marker.is_empty(), - "Generated TypeScript has nullable fields without an optional marker: {missing_optional_marker:?}" + optional_nullable_offenders.is_empty(), + "Generated TypeScript has optional fields with nullable types (disallowed '?: T | null'), add #[ts(optional)] to fix:\n{optional_nullable_offenders:?}" ); Ok(()) diff --git a/codex-rs/app-server-protocol/src/jsonrpc_lite.rs b/codex-rs/app-server-protocol/src/jsonrpc_lite.rs index 88554176d9..65ffa2a005 100644 --- a/codex-rs/app-server-protocol/src/jsonrpc_lite.rs +++ b/codex-rs/app-server-protocol/src/jsonrpc_lite.rs @@ -30,20 +30,20 @@ pub enum JSONRPCMessage { /// A request that expects a response. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct JSONRPCRequest { pub id: RequestId, pub method: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub params: Option, } /// A notification which does not expect a response. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct JSONRPCNotification { pub method: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub params: Option, } @@ -62,10 +62,10 @@ pub struct JSONRPCError { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct JSONRPCErrorError { pub code: i64, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub data: Option, pub message: String, } diff --git a/codex-rs/app-server-protocol/src/protocol.rs b/codex-rs/app-server-protocol/src/protocol.rs index 05c9d91fb2..ca5188f52d 100644 --- a/codex-rs/app-server-protocol/src/protocol.rs +++ b/codex-rs/app-server-protocol/src/protocol.rs @@ -248,7 +248,6 @@ pub enum Account { #[serde(rename = "chatgpt", rename_all = "camelCase")] #[ts(rename = "chatgpt", rename_all = "camelCase")] ChatGpt { - #[ts(optional = nullable)] email: Option, plan_type: PlanType, }, @@ -267,11 +266,9 @@ pub struct InitializeParams { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ClientInfo { pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, pub version: String, } @@ -283,42 +280,33 @@ pub struct InitializeResponse { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct NewConversationParams { /// Optional override for the model name (e.g. "o3", "o4-mini"). - #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, /// Override the model provider to use for this session. - #[serde(skip_serializing_if = "Option::is_none")] pub model_provider: Option, /// Configuration profile from config.toml to specify default options. - #[serde(skip_serializing_if = "Option::is_none")] pub profile: Option, /// Working directory for the session. If relative, it is resolved against /// the server process's current working directory. - #[serde(skip_serializing_if = "Option::is_none")] pub cwd: Option, /// 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, /// Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`. - #[serde(skip_serializing_if = "Option::is_none")] pub sandbox: Option, /// Individual config settings that will override what is in /// CODEX_HOME/config.toml. - #[serde(skip_serializing_if = "Option::is_none")] pub config: Option>, /// The set of instructions to use instead of the default ones. - #[serde(skip_serializing_if = "Option::is_none")] pub base_instructions: Option, /// Prompt used during conversation compaction. @@ -326,29 +314,24 @@ pub struct NewConversationParams { pub compact_prompt: Option, /// Whether to include the apply patch tool in the conversation. - #[serde(skip_serializing_if = "Option::is_none")] pub include_apply_patch_tool: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct NewConversationResponse { pub conversation_id: ConversationId, pub model: String, /// Note this could be ignored by the model. - #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, pub rollout_path: PathBuf, } #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ResumeConversationResponse { pub conversation_id: ConversationId, pub model: String, - #[serde(skip_serializing_if = "Option::is_none")] pub initial_messages: Option>, pub rollout_path: PathBuf, } @@ -377,57 +360,46 @@ pub struct GetConversationSummaryResponse { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ListConversationsParams { /// Optional page size; defaults to a reasonable server-side value. - #[serde(skip_serializing_if = "Option::is_none")] pub page_size: Option, /// Opaque pagination cursor returned by a previous call. - #[serde(skip_serializing_if = "Option::is_none")] pub cursor: Option, /// Optional model provider filter (matches against session metadata). /// - None => filter by the server's default model provider /// - Some([]) => no filtering, include all providers /// - Some([...]) => only include sessions with one of the specified providers - #[serde(skip_serializing_if = "Option::is_none")] pub model_providers: Option>, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ConversationSummary { pub conversation_id: ConversationId, pub path: PathBuf, pub preview: String, /// RFC3339 timestamp string for the session start, if available. - #[serde(skip_serializing_if = "Option::is_none")] pub timestamp: Option, /// Model provider recorded for the session (resolved when absent in metadata). pub model_provider: String, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ListConversationsResponse { pub items: Vec, /// Opaque cursor to pass to the next call to continue after the last item. /// if None, there are no more items to return. - #[serde(skip_serializing_if = "Option::is_none")] pub next_cursor: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ListModelsParams { /// Optional page size; defaults to a reasonable server-side value. - #[serde(skip_serializing_if = "Option::is_none")] pub page_size: Option, /// Opaque pagination cursor returned by a previous call. - #[serde(skip_serializing_if = "Option::is_none")] pub cursor: Option, } @@ -452,24 +424,19 @@ pub struct ReasoningEffortOption { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ListModelsResponse { pub items: Vec, /// Opaque cursor to pass to the next call to continue after the last item. /// if None, there are no more items to return. - #[serde(skip_serializing_if = "Option::is_none")] pub next_cursor: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct UploadFeedbackParams { pub classification: String, - #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, pub include_logs: bool, } @@ -497,7 +464,6 @@ pub enum LoginAccountParams { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct LoginAccountResponse { /// Only set if the login method is ChatGPT. @@ -514,20 +480,15 @@ pub struct LoginAccountResponse { pub struct LogoutAccountResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ResumeConversationParams { /// Absolute path to the rollout JSONL file, when explicitly resuming a known rollout. - #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, /// If the rollout path is not known, it can be discovered via the conversation id at the cost of extra latency. - #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, /// if the rollout path or conversation id is not known, it can be resumed from given history - #[serde(skip_serializing_if = "Option::is_none")] pub history: Option>, /// Optional overrides to apply when spawning the resumed session. - #[serde(skip_serializing_if = "Option::is_none")] pub overrides: Option, } @@ -606,19 +567,15 @@ pub struct LogoutChatGptParams {} pub struct LogoutChatGptResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct GetAuthStatusParams { /// If true, include the current auth token (if available) in the response. - #[serde(skip_serializing_if = "Option::is_none")] pub include_token: Option, /// If true, attempt to refresh the token before returning status. - #[serde(skip_serializing_if = "Option::is_none")] pub refresh_token: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ExecOneOffCommandParams { /// Command argv to execute. @@ -627,10 +584,8 @@ pub struct ExecOneOffCommandParams { /// If not specified, a sensible default is used server-side. pub timeout_ms: Option, /// Optional working directory for the process. Defaults to server config cwd. - #[serde(skip_serializing_if = "Option::is_none")] pub cwd: Option, /// Optional explicit sandbox policy overriding the server default. - #[serde(skip_serializing_if = "Option::is_none")] pub sandbox_policy: Option, } @@ -650,17 +605,13 @@ pub struct GetAccountRateLimitsResponse { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] -#[ts(optional_fields = nullable)] pub struct GetAuthStatusResponse { - #[serde(skip_serializing_if = "Option::is_none")] pub auth_method: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub auth_token: Option, // Indicates that auth method must be valid to use the server. // This can be false if using a custom provider that is configured // with requires_openai_auth == false. - #[serde(skip_serializing_if = "Option::is_none")] pub requires_openai_auth: Option, } @@ -671,7 +622,6 @@ pub struct GetUserAgentResponse { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct UserInfoResponse { /// Note: `alleged_user_email` is not currently verified. We read it from @@ -688,15 +638,12 @@ pub struct GetUserSavedConfigResponse { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct SetDefaultModelParams { /// If set to None, this means `model` should be cleared in config.toml. - #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, /// If set to None, this means `model_reasoning_effort` should be cleared /// in config.toml. - #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, } @@ -708,46 +655,32 @@ pub struct SetDefaultModelResponse {} /// client-configurable settings that can be specified in the NewConversation /// and SendUserTurn requests. #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct UserSavedConfig { /// Approvals - #[serde(skip_serializing_if = "Option::is_none")] pub approval_policy: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub sandbox_mode: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub sandbox_settings: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub forced_chatgpt_workspace_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub forced_login_method: Option, /// Model-specific configuration - #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub model_reasoning_effort: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub model_reasoning_summary: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub model_verbosity: Option, /// Tools - #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option, /// Profiles - #[serde(skip_serializing_if = "Option::is_none")] pub profile: Option, - #[serde(default)] pub profiles: HashMap, } /// MCP representation of a [`codex_core::config_profile::ConfigProfile`]. #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct Profile { pub model: Option, @@ -760,29 +693,23 @@ pub struct Profile { pub model_verbosity: Option, pub chatgpt_base_url: Option, } + /// MCP representation of a [`codex_core::config::ToolsToml`]. #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct Tools { - #[serde(skip_serializing_if = "Option::is_none")] pub web_search: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub view_image: Option, } /// MCP representation of a [`codex_core::config::types::SandboxWorkspaceWrite`]. #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct SandboxSettings { #[serde(default)] pub writable_roots: Vec, - #[serde(skip_serializing_if = "Option::is_none")] pub network_access: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub exclude_tmpdir_env_var: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub exclude_slash_tmp: Option, } @@ -794,7 +721,6 @@ pub struct SendUserMessageParams { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct SendUserTurnParams { pub conversation_id: ConversationId, @@ -803,7 +729,6 @@ pub struct SendUserTurnParams { pub approval_policy: AskForApproval, pub sandbox_policy: SandboxPolicy, pub model: String, - #[serde(skip_serializing_if = "Option::is_none")] pub effort: Option, pub summary: ReasoningSummary, } @@ -938,7 +863,6 @@ server_request_definitions! { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ApplyPatchApprovalParams { pub conversation_id: ConversationId, @@ -947,16 +871,13 @@ pub struct ApplyPatchApprovalParams { pub call_id: String, pub file_changes: HashMap, /// Optional explanatory reason (e.g. request for extra write access). - #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, /// When set, the agent is asking the user to allow writes under this root /// for the remainder of the session (unclear if this is honored today). - #[serde(skip_serializing_if = "Option::is_none")] pub grant_root: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ExecCommandApprovalParams { pub conversation_id: ConversationId, @@ -965,9 +886,7 @@ pub struct ExecCommandApprovalParams { pub call_id: String, pub command: Vec, pub cwd: PathBuf, - #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub risk: Option, pub parsed_cmd: Vec, } @@ -983,26 +902,22 @@ pub struct ApplyPatchApprovalResponse { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] pub struct FuzzyFileSearchParams { pub query: String, pub roots: Vec, // if provided, will cancel any previous request that used the same value - #[serde(skip_serializing_if = "Option::is_none")] pub cancellation_token: Option, } /// Superset of [`codex_file_search::FileMatch`] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct FuzzyFileSearchResult { pub root: String, pub path: String, pub file_name: String, pub score: u32, - #[serde(skip_serializing_if = "Option::is_none")] pub indices: Option>, } @@ -1012,18 +927,15 @@ pub struct FuzzyFileSearchResponse { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct LoginChatGptCompleteNotification { #[schemars(with = "String")] pub login_id: Uuid, pub success: bool, - #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, } #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct SessionConfiguredNotification { /// Name left as session_id instead of conversation_id for backwards compatibility. @@ -1033,7 +945,6 @@ pub struct SessionConfiguredNotification { pub model: String, /// The effort the model is putting into reasoning about the user's request. - #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, /// Identifier of the history log file (inode on Unix, 0 otherwise). @@ -1045,18 +956,15 @@ pub struct SessionConfiguredNotification { /// Optional initial messages (as events) for resumed sessions. /// When present, UIs can use these to seed the history. - #[serde(skip_serializing_if = "Option::is_none")] pub initial_messages: Option>, pub rollout_path: PathBuf, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct AuthStatusChangeNotification { /// Current authentication method; omitted if signed out. - #[serde(skip_serializing_if = "Option::is_none")] pub auth_method: Option, } @@ -1139,7 +1047,14 @@ mod tests { "id": 42, "params": { "model": "gpt-5-codex", - "approvalPolicy": "on-request" + "modelProvider": null, + "profile": null, + "cwd": null, + "approvalPolicy": "on-request", + "sandbox": null, + "config": null, + "baseInstructions": null, + "includeApplyPatchTool": null } }), serde_json::to_value(&request)?, @@ -1212,6 +1127,7 @@ mod tests { "command": ["echo", "hello"], "cwd": "/tmp", "reason": "because tests", + "risk": null, "parsedCmd": [ { "type": "unknown", @@ -1356,7 +1272,10 @@ mod tests { json!({ "method": "model/list", "id": 6, - "params": {} + "params": { + "pageSize": null, + "cursor": null + } }), serde_json::to_value(&request)?, ); diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index feaa19e37a..3f4497fb23 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -166,6 +166,7 @@ mod tests { "params": { "loginId": Uuid::nil(), "success": true, + "error": null, }, }), serde_json::to_value(jsonrpc_notification) diff --git a/codex-rs/mcp-types/generate_mcp_types.py b/codex-rs/mcp-types/generate_mcp_types.py index 1906033537..7535b4c760 100755 --- a/codex-rs/mcp-types/generate_mcp_types.py +++ b/codex-rs/mcp-types/generate_mcp_types.py @@ -332,6 +332,7 @@ class StructField: name: str type_name: str serde: str | None = None + ts: str | None = None comment: str | None = None def append(self, out: list[str], supports_const: bool) -> None: @@ -339,6 +340,8 @@ def append(self, out: list[str], supports_const: bool) -> None: out.append(f" // {self.comment}\n") if self.serde: out.append(f" {self.serde}\n") + if self.ts: + out.append(f" {self.ts}\n") if self.viz == "const": if supports_const: out.append(f" const {self.name}: {self.type_name};\n") @@ -378,9 +381,9 @@ def define_struct( prop_type = f"Option<{prop_type}>" rs_prop = rust_prop_name(prop_name, is_optional) if prop_type.startswith("&'static str"): - fields.append(StructField("const", rs_prop.name, prop_type, rs_prop.serde)) + fields.append(StructField("const", rs_prop.name, prop_type, rs_prop.serde, rs_prop.ts)) else: - fields.append(StructField("pub", rs_prop.name, prop_type, rs_prop.serde)) + fields.append(StructField("pub", rs_prop.name, prop_type, rs_prop.serde, rs_prop.ts)) # Special-case: add Codex-specific user_agent to Implementation if name == "Implementation": @@ -390,6 +393,7 @@ def define_struct( "user_agent", "Option", '#[serde(default, skip_serializing_if = "Option::is_none")]', + '#[ts(optional)]', "This is an extra field that the Codex MCP server sends as part of InitializeResult.", ) ) @@ -474,7 +478,6 @@ def define_string_enum( out.append(f" {capitalize(value)},\n") out.append("}\n\n") - return out def define_untagged_enum(name: str, type_list: list[str], out: list[str]) -> None: @@ -590,7 +593,7 @@ def get_serde_annotation_for_anyof_type(type_name: str) -> str | None: def map_type( - typedef: dict[str, any], + typedef: dict[str, Any], prop_name: str | None = None, struct_name: str | None = None, ) -> str: @@ -665,7 +668,8 @@ class RustProp: name: str # serde annotation, if necessary serde: str | None = None - + # ts annotation, if necessary + ts: str | None = None def rust_prop_name(name: str, is_optional: bool) -> RustProp: """Convert a JSON property name to a Rust property name.""" @@ -684,6 +688,7 @@ def rust_prop_name(name: str, is_optional: bool) -> RustProp: prop_name = name serde_annotations = [] + ts_str = None if is_rename: serde_annotations.append(f'rename = "{name}"') if is_optional: @@ -691,13 +696,18 @@ def rust_prop_name(name: str, is_optional: bool) -> RustProp: serde_annotations.append('skip_serializing_if = "Option::is_none"') if serde_annotations: + # Also mark optional fields for ts-rs generation. serde_str = f"#[serde({', '.join(serde_annotations)})]" else: serde_str = None - return RustProp(prop_name, serde_str) + + if is_optional and serde_str: + ts_str = "#[ts(optional)]" + + return RustProp(prop_name, serde_str, ts_str) -def to_snake_case(name: str) -> str: +def to_snake_case(name: str) -> str | None: """Convert a camelCase or PascalCase name to snake_case.""" snake_case = name[0].lower() + "".join("_" + c.lower() if c.isupper() else c for c in name[1:]) if snake_case != name: diff --git a/codex-rs/mcp-types/src/lib.rs b/codex-rs/mcp-types/src/lib.rs index 98dab7252b..e390322fc8 100644 --- a/codex-rs/mcp-types/src/lib.rs +++ b/codex-rs/mcp-types/src/lib.rs @@ -37,14 +37,17 @@ fn default_jsonrpc() -> String { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct Annotations { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub audience: Option>, #[serde( rename = "lastModified", default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub last_modified: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub priority: Option, } @@ -52,6 +55,7 @@ pub struct Annotations { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct AudioContent { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub annotations: Option, pub data: String, #[serde(rename = "mimeType")] @@ -64,6 +68,7 @@ pub struct AudioContent { pub struct BaseMetadata { pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, } @@ -71,6 +76,7 @@ pub struct BaseMetadata { pub struct BlobResourceContents { pub blob: String, #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub mime_type: Option, pub uri: String, } @@ -78,10 +84,13 @@ pub struct BlobResourceContents { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct BooleanSchema { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub default: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, pub r#type: String, // &'static str = "boolean" } @@ -98,6 +107,7 @@ impl ModelContextProtocolRequest for CallToolRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct CallToolRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub arguments: Option, pub name: String, } @@ -107,12 +117,14 @@ pub struct CallToolRequestParams { pub struct CallToolResult { pub content: Vec, #[serde(rename = "isError", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub is_error: Option, #[serde( rename = "structuredContent", default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub structured_content: Option, } @@ -135,6 +147,7 @@ impl ModelContextProtocolNotification for CancelledNotification { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct CancelledNotificationParams { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub reason: Option, #[serde(rename = "requestId")] pub request_id: RequestId, @@ -144,12 +157,16 @@ pub struct CancelledNotificationParams { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ClientCapabilities { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub elicitation: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub experimental: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub roots: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub sampling: Option, } @@ -161,6 +178,7 @@ pub struct ClientCapabilitiesRoots { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub list_changed: Option, } @@ -228,6 +246,7 @@ impl ModelContextProtocolRequest for CompleteRequest { pub struct CompleteRequestParams { pub argument: CompleteRequestParamsArgument, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub context: Option, pub r#ref: CompleteRequestParamsRef, } @@ -236,6 +255,7 @@ pub struct CompleteRequestParams { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct CompleteRequestParamsContext { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub arguments: Option, } @@ -262,8 +282,10 @@ pub struct CompleteResult { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct CompleteResultCompletion { #[serde(rename = "hasMore", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub has_more: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub total: Option, pub values: Vec, } @@ -302,31 +324,37 @@ pub struct CreateMessageRequestParams { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub include_context: Option, #[serde(rename = "maxTokens")] pub max_tokens: i64, pub messages: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub metadata: Option, #[serde( rename = "modelPreferences", default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub model_preferences: Option, #[serde( rename = "stopSequences", default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub stop_sequences: Option>, #[serde( rename = "systemPrompt", default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub system_prompt: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub temperature: Option, } @@ -341,6 +369,7 @@ pub struct CreateMessageResult { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub stop_reason: Option, } @@ -385,6 +414,7 @@ pub struct ElicitRequestParams { pub struct ElicitRequestParamsRequestedSchema { pub properties: serde_json::Value, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub required: Option>, pub r#type: String, // &'static str = "object" } @@ -394,6 +424,7 @@ pub struct ElicitRequestParamsRequestedSchema { pub struct ElicitResult { pub action: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub content: Option, } @@ -412,6 +443,7 @@ impl From for serde_json::Value { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct EmbeddedResource { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub annotations: Option, pub resource: EmbeddedResourceResource, pub r#type: String, // &'static str = "resource" @@ -429,11 +461,14 @@ pub type EmptyResult = Result; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct EnumSchema { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, pub r#enum: Vec, #[serde(rename = "enumNames", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub enum_names: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, pub r#type: String, // &'static str = "string" } @@ -450,6 +485,7 @@ impl ModelContextProtocolRequest for GetPromptRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct GetPromptRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub arguments: Option, pub name: String, } @@ -458,6 +494,7 @@ pub struct GetPromptRequestParams { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct GetPromptResult { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, pub messages: Vec, } @@ -474,6 +511,7 @@ impl From for serde_json::Value { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ImageContent { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub annotations: Option, pub data: String, #[serde(rename = "mimeType")] @@ -486,10 +524,12 @@ pub struct ImageContent { pub struct Implementation { pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, pub version: String, // This is an extra field that the Codex MCP server sends as part of InitializeResult. #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub user_agent: Option, } @@ -516,6 +556,7 @@ pub struct InitializeRequestParams { pub struct InitializeResult { pub capabilities: ServerCapabilities, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub instructions: Option, #[serde(rename = "protocolVersion")] pub protocol_version: String, @@ -552,6 +593,7 @@ pub struct JSONRPCError { pub struct JSONRPCErrorError { pub code: i64, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub data: Option, pub message: String, } @@ -573,6 +615,7 @@ pub struct JSONRPCNotification { pub jsonrpc: String, pub method: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub params: Option, } @@ -584,6 +627,7 @@ pub struct JSONRPCRequest { pub jsonrpc: String, pub method: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub params: Option, } @@ -608,6 +652,7 @@ impl ModelContextProtocolRequest for ListPromptsRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ListPromptsRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub cursor: Option, } @@ -619,6 +664,7 @@ pub struct ListPromptsResult { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub next_cursor: Option, pub prompts: Vec, } @@ -643,6 +689,7 @@ impl ModelContextProtocolRequest for ListResourceTemplatesRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ListResourceTemplatesRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub cursor: Option, } @@ -654,6 +701,7 @@ pub struct ListResourceTemplatesResult { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub next_cursor: Option, #[serde(rename = "resourceTemplates")] pub resource_templates: Vec, @@ -679,6 +727,7 @@ impl ModelContextProtocolRequest for ListResourcesRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ListResourcesRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub cursor: Option, } @@ -690,6 +739,7 @@ pub struct ListResourcesResult { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub next_cursor: Option, pub resources: Vec, } @@ -739,6 +789,7 @@ impl ModelContextProtocolRequest for ListToolsRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ListToolsRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub cursor: Option, } @@ -750,6 +801,7 @@ pub struct ListToolsResult { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub next_cursor: Option, pub tools: Vec, } @@ -799,6 +851,7 @@ pub struct LoggingMessageNotificationParams { pub data: serde_json::Value, pub level: LoggingLevel, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub logger: Option, } @@ -809,6 +862,7 @@ pub struct LoggingMessageNotificationParams { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ModelHint { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub name: Option, } @@ -830,20 +884,24 @@ pub struct ModelPreferences { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub cost_priority: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub hints: Option>, #[serde( rename = "intelligencePriority", default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub intelligence_priority: Option, #[serde( rename = "speedPriority", default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub speed_priority: Option, } @@ -851,18 +909,23 @@ pub struct ModelPreferences { pub struct Notification { pub method: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub params: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct NumberSchema { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub maximum: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub minimum: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, pub r#type: String, } @@ -871,12 +934,14 @@ pub struct NumberSchema { pub struct PaginatedRequest { pub method: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub params: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct PaginatedRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub cursor: Option, } @@ -887,6 +952,7 @@ pub struct PaginatedResult { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub next_cursor: Option, } @@ -929,11 +995,13 @@ impl ModelContextProtocolNotification for ProgressNotification { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ProgressNotificationParams { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub message: Option, pub progress: f64, #[serde(rename = "progressToken")] pub progress_token: ProgressToken, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub total: Option, } @@ -948,11 +1016,14 @@ pub enum ProgressToken { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct Prompt { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub arguments: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, } @@ -960,11 +1031,14 @@ pub struct Prompt { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct PromptArgument { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub required: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, } @@ -991,6 +1065,7 @@ pub struct PromptMessage { pub struct PromptReference { pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, pub r#type: String, // &'static str = "ref/prompt" } @@ -1034,6 +1109,7 @@ impl From for serde_json::Value { pub struct Request { pub method: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub params: Option, } @@ -1048,15 +1124,20 @@ pub enum RequestId { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct Resource { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub annotations: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub mime_type: Option, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub size: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, pub uri: String, } @@ -1065,6 +1146,7 @@ pub struct Resource { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ResourceContents { #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub mime_type: Option, pub uri: String, } @@ -1075,15 +1157,20 @@ pub struct ResourceContents { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ResourceLink { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub annotations: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub mime_type: Option, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub size: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, pub r#type: String, // &'static str = "resource_link" pub uri: String, @@ -1101,13 +1188,17 @@ impl ModelContextProtocolNotification for ResourceListChangedNotification { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ResourceTemplate { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub annotations: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub mime_type: Option, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, #[serde(rename = "uriTemplate")] pub uri_template: String, @@ -1148,6 +1239,7 @@ pub enum Role { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct Root { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub name: Option, pub uri: String, } @@ -1179,16 +1271,22 @@ pub enum SamplingMessageContent { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ServerCapabilities { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub completions: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub experimental: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub logging: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub prompts: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub resources: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub tools: Option, } @@ -1200,6 +1298,7 @@ pub struct ServerCapabilitiesTools { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub list_changed: Option, } @@ -1211,8 +1310,10 @@ pub struct ServerCapabilitiesResources { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub list_changed: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub subscribe: Option, } @@ -1224,6 +1325,7 @@ pub struct ServerCapabilitiesPrompts { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub list_changed: Option, } @@ -1298,14 +1400,19 @@ pub struct SetLevelRequestParams { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct StringSchema { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub format: Option, #[serde(rename = "maxLength", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub max_length: Option, #[serde(rename = "minLength", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub min_length: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, pub r#type: String, // &'static str = "string" } @@ -1328,6 +1435,7 @@ pub struct SubscribeRequestParams { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct TextContent { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub annotations: Option, pub text: String, pub r#type: String, // &'static str = "text" @@ -1336,6 +1444,7 @@ pub struct TextContent { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct TextResourceContents { #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub mime_type: Option, pub text: String, pub uri: String, @@ -1345,8 +1454,10 @@ pub struct TextResourceContents { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct Tool { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub annotations: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub description: Option, #[serde(rename = "inputSchema")] pub input_schema: ToolInputSchema, @@ -1356,8 +1467,10 @@ pub struct Tool { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub output_schema: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, } @@ -1366,8 +1479,10 @@ pub struct Tool { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ToolOutputSchema { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub properties: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub required: Option>, pub r#type: String, // &'static str = "object" } @@ -1376,8 +1491,10 @@ pub struct ToolOutputSchema { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] pub struct ToolInputSchema { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub properties: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub required: Option>, pub r#type: String, // &'static str = "object" } @@ -1397,26 +1514,31 @@ pub struct ToolAnnotations { default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub destructive_hint: Option, #[serde( rename = "idempotentHint", default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub idempotent_hint: Option, #[serde( rename = "openWorldHint", default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub open_world_hint: Option, #[serde( rename = "readOnlyHint", default, skip_serializing_if = "Option::is_none" )] + #[ts(optional)] pub read_only_hint: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub title: Option, } diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index 77a1b50fec..d608dba639 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -61,7 +61,6 @@ impl SandboxRiskCategory { } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct ExecApprovalRequestEvent { /// Identifier for the associated exec call, if available. pub call_id: String, @@ -79,7 +78,6 @@ pub struct ExecApprovalRequestEvent { } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct ApplyPatchApprovalRequestEvent { /// Responses API call id for the associated patch apply call, if available. pub call_id: String, diff --git a/codex-rs/protocol/src/custom_prompts.rs b/codex-rs/protocol/src/custom_prompts.rs index 790e0df821..7472d1b4c5 100644 --- a/codex-rs/protocol/src/custom_prompts.rs +++ b/codex-rs/protocol/src/custom_prompts.rs @@ -11,7 +11,6 @@ use ts_rs::TS; pub const PROMPTS_CMD_PREFIX: &str = "prompts"; #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct CustomPrompt { pub name: String, pub path: PathBuf, diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index f7b31c4d6b..a824ee91e4 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -48,36 +48,35 @@ pub enum ContentItem { #[serde(tag = "type", rename_all = "snake_case")] pub enum ResponseItem { Message { - #[serde(skip_serializing)] - #[ts(optional = nullable)] + #[serde(default, skip_serializing)] + #[ts(skip)] id: Option, role: String, content: Vec, }, Reasoning { #[serde(default, skip_serializing)] + #[ts(skip)] id: String, summary: Vec, #[serde(default, skip_serializing_if = "should_serialize_reasoning_content")] - #[ts(optional = nullable)] + #[ts(optional)] content: Option>, - #[ts(optional = nullable)] encrypted_content: Option, }, LocalShellCall { /// Set when using the chat completions API. - #[serde(skip_serializing)] - #[ts(optional = nullable)] + #[serde(default, skip_serializing)] + #[ts(skip)] id: Option, /// Set when using the Responses API. - #[ts(optional = nullable)] call_id: Option, status: LocalShellStatus, action: LocalShellAction, }, FunctionCall { - #[serde(skip_serializing)] - #[ts(optional = nullable)] + #[serde(default, skip_serializing)] + #[ts(skip)] id: Option, name: String, // The Responses API returns the function call arguments as a *string* that contains @@ -97,11 +96,11 @@ pub enum ResponseItem { output: FunctionCallOutputPayload, }, CustomToolCall { - #[serde(skip_serializing)] - #[ts(optional = nullable)] + #[serde(default, skip_serializing)] + #[ts(skip)] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] + #[ts(optional)] status: Option, call_id: String, @@ -121,11 +120,11 @@ pub enum ResponseItem { // "action": {"type":"search","query":"weather: San Francisco, CA"} // } WebSearchCall { - #[serde(skip_serializing)] - #[ts(optional = nullable)] + #[serde(default, skip_serializing)] + #[ts(skip)] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] + #[ts(optional)] status: Option, action: WebSearchAction, }, @@ -203,7 +202,6 @@ pub enum LocalShellAction { } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct LocalShellExecAction { pub command: Vec, pub timeout_ms: Option, @@ -296,7 +294,6 @@ impl From> for ResponseInputItem { /// If the `name` of a `ResponseItem::FunctionCall` is either `container.exec` /// or shell`, the `arguments` field should deserialize to this struct. #[derive(Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct ShellToolCallParams { pub command: Vec, pub workdir: Option, @@ -329,7 +326,6 @@ pub enum FunctionCallOutputContentItem { /// `content_items` with the structured form that the Responses/Chat /// Completions APIs understand. #[derive(Debug, Default, Clone, PartialEq, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct FunctionCallOutputPayload { pub content: String, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/codex-rs/protocol/src/parse_command.rs b/codex-rs/protocol/src/parse_command.rs index b74713b452..77926f11b6 100644 --- a/codex-rs/protocol/src/parse_command.rs +++ b/codex-rs/protocol/src/parse_command.rs @@ -18,14 +18,11 @@ pub enum ParsedCommand { }, ListFiles { cmd: String, - #[ts(optional = nullable)] path: Option, }, Search { cmd: String, - #[ts(optional = nullable)] query: Option, - #[ts(optional = nullable)] path: Option, }, Unknown { diff --git a/codex-rs/protocol/src/plan_tool.rs b/codex-rs/protocol/src/plan_tool.rs index a95fe32efd..a9038eb03b 100644 --- a/codex-rs/protocol/src/plan_tool.rs +++ b/codex-rs/protocol/src/plan_tool.rs @@ -21,7 +21,6 @@ pub struct PlanItemArg { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(deny_unknown_fields)] -#[ts(optional_fields = nullable)] pub struct UpdatePlanArgs { #[serde(default)] pub explanation: Option, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index b16baab9b0..dcb0299e49 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -661,7 +661,6 @@ impl HasLegacyEvent for EventMsg { } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct ExitedReviewModeEvent { pub review_output: Option, } @@ -674,13 +673,11 @@ pub struct ErrorEvent { } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct TaskCompleteEvent { pub last_agent_message: Option, } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct TaskStartedEvent { pub model_context_window: Option, } @@ -700,11 +697,9 @@ pub struct TokenUsage { } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct TokenUsageInfo { pub total_token_usage: TokenUsage, pub last_token_usage: TokenUsage, - #[ts(optional = nullable)] #[ts(type = "number | null")] pub model_context_window: Option, } @@ -765,30 +760,25 @@ impl TokenUsageInfo { } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct TokenCountEvent { pub info: Option, pub rate_limits: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct RateLimitSnapshot { pub primary: Option, pub secondary: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct RateLimitWindow { /// Percentage (0-100) of the window that has been consumed. pub used_percent: f64, /// Rolling window duration, in minutes. - #[ts(optional = nullable)] #[ts(type = "number | null")] pub window_minutes: Option, /// Unix timestamp (seconds since epoch) when the window resets. - #[ts(optional = nullable)] #[ts(type = "number | null")] pub resets_at: Option, } @@ -902,7 +892,6 @@ pub struct AgentMessageEvent { } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct UserMessageEvent { pub message: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -938,7 +927,6 @@ pub struct AgentReasoningDeltaEvent { } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)] -#[ts(optional_fields = nullable)] pub struct McpInvocation { /// Name of the MCP server as defined in the config. pub server: String, @@ -1067,7 +1055,6 @@ pub enum SubAgentSource { } #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct SessionMeta { pub id: ConversationId, pub timestamp: String, @@ -1096,7 +1083,6 @@ impl Default for SessionMeta { } #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct SessionMetaLine { #[serde(flatten)] pub meta: SessionMeta, @@ -1132,7 +1118,6 @@ impl From for ResponseItem { } #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct TurnContextItem { pub cwd: PathBuf, pub approval_policy: AskForApproval, @@ -1151,7 +1136,6 @@ pub struct RolloutLine { } #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct GitInfo { /// Current commit hash (SHA) #[serde(skip_serializing_if = "Option::is_none")] @@ -1285,7 +1269,6 @@ pub struct BackgroundEventEvent { } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct DeprecationNoticeEvent { /// Concise summary of what is deprecated. pub summary: String, @@ -1295,14 +1278,12 @@ pub struct DeprecationNoticeEvent { } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct UndoStartedEvent { #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct UndoCompletedEvent { pub success: bool, #[serde(skip_serializing_if = "Option::is_none")] @@ -1347,7 +1328,6 @@ pub struct TurnDiffEvent { } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct GetHistoryEntryResponseEvent { pub offset: usize, pub log_id: u64, @@ -1397,7 +1377,6 @@ pub struct ListCustomPromptsResponseEvent { } #[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct SessionConfiguredEvent { /// Name left as session_id instead of conversation_id for backwards compatibility. pub session_id: ConversationId, @@ -1458,7 +1437,6 @@ pub enum FileChange { }, Update { unified_diff: String, - #[ts(optional = nullable)] move_path: Option, }, } diff --git a/codex-rs/utils/git/src/lib.rs b/codex-rs/utils/git/src/lib.rs index 57ab05b38b..cf2887aa8f 100644 --- a/codex-rs/utils/git/src/lib.rs +++ b/codex-rs/utils/git/src/lib.rs @@ -28,7 +28,6 @@ type CommitID = String; /// Details of a ghost commit created from a repository state. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] -#[ts(optional_fields = nullable)] pub struct GhostCommit { id: CommitID, parent: Option,