From 069a38a06c929d335d5a04f4a040cd0e69cd78a0 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 29 Oct 2025 14:09:47 -0500 Subject: [PATCH] Add missing "nullable" macro to protocol structs that contain optional fields (#5901) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR addresses a current hole in the TypeScript code generation for the API server protocol. Fields that are marked as "Optional<>" in the Rust code are serialized such that the value is omitted when it is deserialized — appearing as `undefined`, but the TS type indicates (incorrectly) that it is always defined but possibly `null`. This can lead to subtle errors that the TypeScript compiler doesn't catch. The fix is to include the `#[ts(optional_fields = nullable)]` macro for all protocol structs that contain one or more `Optional<>` fields. This PR also includes a new test that validates that all TS protocol code containing "| null" in its type is marked optional ("?") to catch cases where `#[ts(optional_fields = nullable)]` is omitted. --- codex-rs/app-server-protocol/src/export.rs | 147 ++++++++++++++++++ .../app-server-protocol/src/jsonrpc_lite.rs | 3 + codex-rs/app-server-protocol/src/protocol.rs | 30 ++++ codex-rs/protocol/src/approvals.rs | 2 + codex-rs/protocol/src/custom_prompts.rs | 1 + codex-rs/protocol/src/models.rs | 13 ++ codex-rs/protocol/src/parse_command.rs | 3 + codex-rs/protocol/src/plan_tool.rs | 1 + codex-rs/protocol/src/protocol.rs | 22 +++ codex-rs/utils/git/src/lib.rs | 1 + 10 files changed, 223 insertions(+) diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index b6961a73..2fff3a42 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -534,3 +534,150 @@ fn generate_index_ts(out_dir: &Path) -> Result { .with_context(|| format!("Failed to write {}", index_path.display()))?; Ok(index_path) } + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + use std::collections::BTreeSet; + use std::fs; + use std::path::PathBuf; + use uuid::Uuid; + + #[test] + fn generated_ts_omits_undefined_unions_for_optionals() -> Result<()> { + let output_dir = std::env::temp_dir().join(format!("codex_ts_types_{}", Uuid::now_v7())); + fs::create_dir(&output_dir)?; + + struct TempDirGuard(PathBuf); + + impl Drop for TempDirGuard { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } + } + + let _guard = TempDirGuard(output_dir.clone()); + + generate_ts(&output_dir, None)?; + + let mut undefined_offenders = Vec::new(); + let mut missing_optional_marker = BTreeSet::new(); + let mut stack = vec![output_dir]; + while let Some(dir) = stack.pop() { + for entry in fs::read_dir(&dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + stack.push(path); + continue; + } + + if matches!(path.extension().and_then(|ext| ext.to_str()), Some("ts")) { + let contents = fs::read_to_string(&path)?; + if contents.contains("| undefined") { + undefined_offenders.push(path.clone()); + } + + const SKIP_PREFIXES: &[&str] = &[ + "const ", + "let ", + "var ", + "export const ", + "export let ", + "export var ", + ]; + + 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; + }; + + let line_start_idx = contents[..colon_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(',') { + 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('{') { + 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('}') { + segment_start_idx = segment_start_idx.max(line_start_idx + rel_idx + 1); + } + + let mut field_prefix = contents[segment_start_idx..colon_idx].trim(); + if field_prefix.is_empty() { + search_start = abs_idx + 5; + continue; + } + + if let Some(comment_idx) = field_prefix.rfind("*/") { + field_prefix = field_prefix[comment_idx + 2..].trim_start(); + } + + if field_prefix.is_empty() { + search_start = abs_idx + 5; + continue; + } + + if SKIP_PREFIXES + .iter() + .any(|prefix| field_prefix.starts_with(prefix)) + { + search_start = abs_idx + 5; + continue; + } + + if field_prefix.contains('(') { + search_start = abs_idx + 5; + continue; + } + + 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(); + + missing_optional_marker.insert(format!( + "{}:{}: {offending_snippet}", + path.display(), + line_number + )); + + search_start = abs_idx + 5; + } + } + } + } + + assert!( + undefined_offenders.is_empty(), + "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. + assert!( + missing_optional_marker.is_empty(), + "Generated TypeScript has nullable fields without an optional marker: {missing_optional_marker:?}" + ); + + Ok(()) + } +} diff --git a/codex-rs/app-server-protocol/src/jsonrpc_lite.rs b/codex-rs/app-server-protocol/src/jsonrpc_lite.rs index c0b88b02..88554176 100644 --- a/codex-rs/app-server-protocol/src/jsonrpc_lite.rs +++ b/codex-rs/app-server-protocol/src/jsonrpc_lite.rs @@ -30,6 +30,7 @@ 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, @@ -39,6 +40,7 @@ pub struct JSONRPCRequest { /// 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")] @@ -60,6 +62,7 @@ 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")] diff --git a/codex-rs/app-server-protocol/src/protocol.rs b/codex-rs/app-server-protocol/src/protocol.rs index 7ade5f4e..74ad15f6 100644 --- a/codex-rs/app-server-protocol/src/protocol.rs +++ b/codex-rs/app-server-protocol/src/protocol.rs @@ -248,6 +248,7 @@ pub enum Account { #[serde(rename = "chatgpt", rename_all = "camelCase")] #[ts(rename = "chatgpt", rename_all = "camelCase")] ChatGpt { + #[ts(optional = nullable)] email: Option, plan_type: PlanType, }, @@ -266,6 +267,7 @@ 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, @@ -281,6 +283,7 @@ 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"). @@ -324,6 +327,7 @@ pub struct NewConversationParams { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct NewConversationResponse { pub conversation_id: ConversationId, @@ -335,6 +339,7 @@ pub struct NewConversationResponse { } #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] +#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ResumeConversationResponse { pub conversation_id: ConversationId, @@ -368,6 +373,7 @@ 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. @@ -385,6 +391,7 @@ pub struct ListConversationsParams { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ConversationSummary { pub conversation_id: ConversationId, @@ -398,6 +405,7 @@ pub struct ConversationSummary { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ListConversationsResponse { pub items: Vec, @@ -408,6 +416,7 @@ pub struct ListConversationsResponse { } #[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. @@ -439,6 +448,7 @@ 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, @@ -449,6 +459,7 @@ pub struct ListModelsResponse { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct UploadFeedbackParams { pub classification: String, @@ -482,6 +493,7 @@ 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. @@ -498,6 +510,7 @@ 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. @@ -589,6 +602,7 @@ 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. @@ -600,6 +614,7 @@ pub struct GetAuthStatusParams { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ExecOneOffCommandParams { /// Command argv to execute. @@ -631,6 +646,7 @@ 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, @@ -651,6 +667,7 @@ 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 @@ -667,6 +684,7 @@ 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. @@ -686,6 +704,7 @@ 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 @@ -724,6 +743,7 @@ pub struct UserSavedConfig { /// 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, @@ -738,6 +758,7 @@ pub struct Profile { } /// 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")] @@ -748,6 +769,7 @@ pub struct Tools { /// 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)] @@ -768,6 +790,7 @@ 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, @@ -911,6 +934,7 @@ 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, @@ -928,6 +952,7 @@ pub struct ApplyPatchApprovalParams { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[ts(optional_fields = nullable)] #[serde(rename_all = "camelCase")] pub struct ExecCommandApprovalParams { pub conversation_id: ConversationId, @@ -954,6 +979,7 @@ 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 { @@ -966,6 +992,7 @@ pub struct FuzzyFileSearchParams { /// 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, @@ -981,6 +1008,7 @@ 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")] @@ -991,6 +1019,7 @@ pub struct LoginChatGptCompleteNotification { } #[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. @@ -1019,6 +1048,7 @@ pub struct SessionConfiguredNotification { } #[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. diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index d608dba6..77a1b50f 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -61,6 +61,7 @@ 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, @@ -78,6 +79,7 @@ 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 7472d1b4..790e0df8 100644 --- a/codex-rs/protocol/src/custom_prompts.rs +++ b/codex-rs/protocol/src/custom_prompts.rs @@ -11,6 +11,7 @@ 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 96f17bab..f7b31c4d 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -49,6 +49,7 @@ pub enum ContentItem { pub enum ResponseItem { Message { #[serde(skip_serializing)] + #[ts(optional = nullable)] id: Option, role: String, content: Vec, @@ -58,20 +59,25 @@ pub enum ResponseItem { id: String, summary: Vec, #[serde(default, skip_serializing_if = "should_serialize_reasoning_content")] + #[ts(optional = nullable)] content: Option>, + #[ts(optional = nullable)] encrypted_content: Option, }, LocalShellCall { /// Set when using the chat completions API. #[serde(skip_serializing)] + #[ts(optional = nullable)] 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)] id: Option, name: String, // The Responses API returns the function call arguments as a *string* that contains @@ -92,8 +98,10 @@ pub enum ResponseItem { }, CustomToolCall { #[serde(skip_serializing)] + #[ts(optional = nullable)] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] status: Option, call_id: String, @@ -114,8 +122,10 @@ pub enum ResponseItem { // } WebSearchCall { #[serde(skip_serializing)] + #[ts(optional = nullable)] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] status: Option, action: WebSearchAction, }, @@ -193,6 +203,7 @@ 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, @@ -285,6 +296,7 @@ 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, @@ -317,6 +329,7 @@ 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 77926f11..b74713b4 100644 --- a/codex-rs/protocol/src/parse_command.rs +++ b/codex-rs/protocol/src/parse_command.rs @@ -18,11 +18,14 @@ 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 a9038eb0..a95fe32e 100644 --- a/codex-rs/protocol/src/plan_tool.rs +++ b/codex-rs/protocol/src/plan_tool.rs @@ -21,6 +21,7 @@ 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 0c8a58b9..ffb5740e 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -563,6 +563,7 @@ pub struct ItemCompletedEvent { } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] +#[ts(optional_fields = nullable)] pub struct ExitedReviewModeEvent { pub review_output: Option, } @@ -575,11 +576,13 @@ 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, } @@ -599,9 +602,11 @@ 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, } @@ -662,25 +667,30 @@ 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, } @@ -794,6 +804,7 @@ 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")] @@ -829,6 +840,7 @@ 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, @@ -947,6 +959,7 @@ pub enum SessionSource { } #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)] +#[ts(optional_fields = nullable)] pub struct SessionMeta { pub id: ConversationId, pub timestamp: String, @@ -975,6 +988,7 @@ impl Default for SessionMeta { } #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] +#[ts(optional_fields = nullable)] pub struct SessionMetaLine { #[serde(flatten)] pub meta: SessionMeta, @@ -1010,6 +1024,7 @@ 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, @@ -1028,6 +1043,7 @@ 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")] @@ -1161,6 +1177,7 @@ 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, @@ -1170,12 +1187,14 @@ 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")] @@ -1220,6 +1239,7 @@ pub struct TurnDiffEvent { } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] +#[ts(optional_fields = nullable)] pub struct GetHistoryEntryResponseEvent { pub offset: usize, pub log_id: u64, @@ -1269,6 +1289,7 @@ 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, @@ -1329,6 +1350,7 @@ 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 cf2887aa..57ab05b3 100644 --- a/codex-rs/utils/git/src/lib.rs +++ b/codex-rs/utils/git/src/lib.rs @@ -28,6 +28,7 @@ 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,