Add missing "nullable" macro to protocol structs that contain optional fields (#5901)

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.
This commit is contained in:
Eric Traut
2025-10-29 14:09:47 -05:00
committed by GitHub
parent 3183935bd7
commit 069a38a06c
10 changed files with 223 additions and 0 deletions

View File

@@ -534,3 +534,150 @@ fn generate_index_ts(out_dir: &Path) -> Result<PathBuf> {
.with_context(|| format!("Failed to write {}", index_path.display()))?; .with_context(|| format!("Failed to write {}", index_path.display()))?;
Ok(index_path) 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<T>` 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(())
}
}

View File

@@ -30,6 +30,7 @@ pub enum JSONRPCMessage {
/// A request that expects a response. /// A request that expects a response.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct JSONRPCRequest { pub struct JSONRPCRequest {
pub id: RequestId, pub id: RequestId,
pub method: String, pub method: String,
@@ -39,6 +40,7 @@ pub struct JSONRPCRequest {
/// A notification which does not expect a response. /// A notification which does not expect a response.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct JSONRPCNotification { pub struct JSONRPCNotification {
pub method: String, pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -60,6 +62,7 @@ pub struct JSONRPCError {
} }
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct JSONRPCErrorError { pub struct JSONRPCErrorError {
pub code: i64, pub code: i64,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]

View File

@@ -248,6 +248,7 @@ pub enum Account {
#[serde(rename = "chatgpt", rename_all = "camelCase")] #[serde(rename = "chatgpt", rename_all = "camelCase")]
#[ts(rename = "chatgpt", rename_all = "camelCase")] #[ts(rename = "chatgpt", rename_all = "camelCase")]
ChatGpt { ChatGpt {
#[ts(optional = nullable)]
email: Option<String>, email: Option<String>,
plan_type: PlanType, plan_type: PlanType,
}, },
@@ -266,6 +267,7 @@ pub struct InitializeParams {
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ClientInfo { pub struct ClientInfo {
pub name: String, pub name: String,
@@ -281,6 +283,7 @@ pub struct InitializeResponse {
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct NewConversationParams { pub struct NewConversationParams {
/// Optional override for the model name (e.g. "o3", "o4-mini"). /// 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)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct NewConversationResponse { pub struct NewConversationResponse {
pub conversation_id: ConversationId, pub conversation_id: ConversationId,
@@ -335,6 +339,7 @@ pub struct NewConversationResponse {
} }
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ResumeConversationResponse { pub struct ResumeConversationResponse {
pub conversation_id: ConversationId, pub conversation_id: ConversationId,
@@ -368,6 +373,7 @@ pub struct GetConversationSummaryResponse {
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ListConversationsParams { pub struct ListConversationsParams {
/// Optional page size; defaults to a reasonable server-side value. /// 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)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ConversationSummary { pub struct ConversationSummary {
pub conversation_id: ConversationId, pub conversation_id: ConversationId,
@@ -398,6 +405,7 @@ pub struct ConversationSummary {
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ListConversationsResponse { pub struct ListConversationsResponse {
pub items: Vec<ConversationSummary>, pub items: Vec<ConversationSummary>,
@@ -408,6 +416,7 @@ pub struct ListConversationsResponse {
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ListModelsParams { pub struct ListModelsParams {
/// Optional page size; defaults to a reasonable server-side value. /// 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)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ListModelsResponse { pub struct ListModelsResponse {
pub items: Vec<Model>, pub items: Vec<Model>,
@@ -449,6 +459,7 @@ pub struct ListModelsResponse {
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct UploadFeedbackParams { pub struct UploadFeedbackParams {
pub classification: String, pub classification: String,
@@ -482,6 +493,7 @@ pub enum LoginAccountParams {
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct LoginAccountResponse { pub struct LoginAccountResponse {
/// Only set if the login method is ChatGPT. /// Only set if the login method is ChatGPT.
@@ -498,6 +510,7 @@ pub struct LoginAccountResponse {
pub struct LogoutAccountResponse {} pub struct LogoutAccountResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ResumeConversationParams { pub struct ResumeConversationParams {
/// Absolute path to the rollout JSONL file, when explicitly resuming a known rollout. /// Absolute path to the rollout JSONL file, when explicitly resuming a known rollout.
@@ -589,6 +602,7 @@ pub struct LogoutChatGptParams {}
pub struct LogoutChatGptResponse {} pub struct LogoutChatGptResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GetAuthStatusParams { pub struct GetAuthStatusParams {
/// If true, include the current auth token (if available) in the response. /// 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)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ExecOneOffCommandParams { pub struct ExecOneOffCommandParams {
/// Command argv to execute. /// Command argv to execute.
@@ -631,6 +646,7 @@ pub struct GetAccountRateLimitsResponse {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(optional_fields = nullable)]
pub struct GetAuthStatusResponse { pub struct GetAuthStatusResponse {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub auth_method: Option<AuthMode>, pub auth_method: Option<AuthMode>,
@@ -651,6 +667,7 @@ pub struct GetUserAgentResponse {
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct UserInfoResponse { pub struct UserInfoResponse {
/// Note: `alleged_user_email` is not currently verified. We read it from /// 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)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SetDefaultModelParams { pub struct SetDefaultModelParams {
/// If set to None, this means `model` should be cleared in config.toml. /// 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 /// client-configurable settings that can be specified in the NewConversation
/// and SendUserTurn requests. /// and SendUserTurn requests.
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)] #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct UserSavedConfig { pub struct UserSavedConfig {
/// Approvals /// Approvals
@@ -724,6 +743,7 @@ pub struct UserSavedConfig {
/// MCP representation of a [`codex_core::config_profile::ConfigProfile`]. /// MCP representation of a [`codex_core::config_profile::ConfigProfile`].
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)] #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Profile { pub struct Profile {
pub model: Option<String>, pub model: Option<String>,
@@ -738,6 +758,7 @@ pub struct Profile {
} }
/// MCP representation of a [`codex_core::config::ToolsToml`]. /// MCP representation of a [`codex_core::config::ToolsToml`].
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)] #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Tools { pub struct Tools {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -748,6 +769,7 @@ pub struct Tools {
/// MCP representation of a [`codex_core::config_types::SandboxWorkspaceWrite`]. /// MCP representation of a [`codex_core::config_types::SandboxWorkspaceWrite`].
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)] #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SandboxSettings { pub struct SandboxSettings {
#[serde(default)] #[serde(default)]
@@ -768,6 +790,7 @@ pub struct SendUserMessageParams {
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SendUserTurnParams { pub struct SendUserTurnParams {
pub conversation_id: ConversationId, pub conversation_id: ConversationId,
@@ -911,6 +934,7 @@ server_request_definitions! {
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ApplyPatchApprovalParams { pub struct ApplyPatchApprovalParams {
pub conversation_id: ConversationId, pub conversation_id: ConversationId,
@@ -928,6 +952,7 @@ pub struct ApplyPatchApprovalParams {
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ExecCommandApprovalParams { pub struct ExecCommandApprovalParams {
pub conversation_id: ConversationId, pub conversation_id: ConversationId,
@@ -954,6 +979,7 @@ pub struct ApplyPatchApprovalResponse {
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")] #[ts(rename_all = "camelCase")]
pub struct FuzzyFileSearchParams { pub struct FuzzyFileSearchParams {
@@ -966,6 +992,7 @@ pub struct FuzzyFileSearchParams {
/// Superset of [`codex_file_search::FileMatch`] /// Superset of [`codex_file_search::FileMatch`]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct FuzzyFileSearchResult { pub struct FuzzyFileSearchResult {
pub root: String, pub root: String,
pub path: String, pub path: String,
@@ -981,6 +1008,7 @@ pub struct FuzzyFileSearchResponse {
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct LoginChatGptCompleteNotification { pub struct LoginChatGptCompleteNotification {
#[schemars(with = "String")] #[schemars(with = "String")]
@@ -991,6 +1019,7 @@ pub struct LoginChatGptCompleteNotification {
} }
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SessionConfiguredNotification { pub struct SessionConfiguredNotification {
/// Name left as session_id instead of conversation_id for backwards compatibility. /// 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)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AuthStatusChangeNotification { pub struct AuthStatusChangeNotification {
/// Current authentication method; omitted if signed out. /// Current authentication method; omitted if signed out.

View File

@@ -61,6 +61,7 @@ impl SandboxRiskCategory {
} }
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct ExecApprovalRequestEvent { pub struct ExecApprovalRequestEvent {
/// Identifier for the associated exec call, if available. /// Identifier for the associated exec call, if available.
pub call_id: String, pub call_id: String,
@@ -78,6 +79,7 @@ pub struct ExecApprovalRequestEvent {
} }
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct ApplyPatchApprovalRequestEvent { pub struct ApplyPatchApprovalRequestEvent {
/// Responses API call id for the associated patch apply call, if available. /// Responses API call id for the associated patch apply call, if available.
pub call_id: String, pub call_id: String,

View File

@@ -11,6 +11,7 @@ use ts_rs::TS;
pub const PROMPTS_CMD_PREFIX: &str = "prompts"; pub const PROMPTS_CMD_PREFIX: &str = "prompts";
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct CustomPrompt { pub struct CustomPrompt {
pub name: String, pub name: String,
pub path: PathBuf, pub path: PathBuf,

View File

@@ -49,6 +49,7 @@ pub enum ContentItem {
pub enum ResponseItem { pub enum ResponseItem {
Message { Message {
#[serde(skip_serializing)] #[serde(skip_serializing)]
#[ts(optional = nullable)]
id: Option<String>, id: Option<String>,
role: String, role: String,
content: Vec<ContentItem>, content: Vec<ContentItem>,
@@ -58,20 +59,25 @@ pub enum ResponseItem {
id: String, id: String,
summary: Vec<ReasoningItemReasoningSummary>, summary: Vec<ReasoningItemReasoningSummary>,
#[serde(default, skip_serializing_if = "should_serialize_reasoning_content")] #[serde(default, skip_serializing_if = "should_serialize_reasoning_content")]
#[ts(optional = nullable)]
content: Option<Vec<ReasoningItemContent>>, content: Option<Vec<ReasoningItemContent>>,
#[ts(optional = nullable)]
encrypted_content: Option<String>, encrypted_content: Option<String>,
}, },
LocalShellCall { LocalShellCall {
/// Set when using the chat completions API. /// Set when using the chat completions API.
#[serde(skip_serializing)] #[serde(skip_serializing)]
#[ts(optional = nullable)]
id: Option<String>, id: Option<String>,
/// Set when using the Responses API. /// Set when using the Responses API.
#[ts(optional = nullable)]
call_id: Option<String>, call_id: Option<String>,
status: LocalShellStatus, status: LocalShellStatus,
action: LocalShellAction, action: LocalShellAction,
}, },
FunctionCall { FunctionCall {
#[serde(skip_serializing)] #[serde(skip_serializing)]
#[ts(optional = nullable)]
id: Option<String>, id: Option<String>,
name: String, name: String,
// The Responses API returns the function call arguments as a *string* that contains // The Responses API returns the function call arguments as a *string* that contains
@@ -92,8 +98,10 @@ pub enum ResponseItem {
}, },
CustomToolCall { CustomToolCall {
#[serde(skip_serializing)] #[serde(skip_serializing)]
#[ts(optional = nullable)]
id: Option<String>, id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
status: Option<String>, status: Option<String>,
call_id: String, call_id: String,
@@ -114,8 +122,10 @@ pub enum ResponseItem {
// } // }
WebSearchCall { WebSearchCall {
#[serde(skip_serializing)] #[serde(skip_serializing)]
#[ts(optional = nullable)]
id: Option<String>, id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
status: Option<String>, status: Option<String>,
action: WebSearchAction, action: WebSearchAction,
}, },
@@ -193,6 +203,7 @@ pub enum LocalShellAction {
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct LocalShellExecAction { pub struct LocalShellExecAction {
pub command: Vec<String>, pub command: Vec<String>,
pub timeout_ms: Option<u64>, pub timeout_ms: Option<u64>,
@@ -285,6 +296,7 @@ impl From<Vec<UserInput>> for ResponseInputItem {
/// If the `name` of a `ResponseItem::FunctionCall` is either `container.exec` /// If the `name` of a `ResponseItem::FunctionCall` is either `container.exec`
/// or shell`, the `arguments` field should deserialize to this struct. /// or shell`, the `arguments` field should deserialize to this struct.
#[derive(Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[derive(Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct ShellToolCallParams { pub struct ShellToolCallParams {
pub command: Vec<String>, pub command: Vec<String>,
pub workdir: Option<String>, pub workdir: Option<String>,
@@ -317,6 +329,7 @@ pub enum FunctionCallOutputContentItem {
/// `content_items` with the structured form that the Responses/Chat /// `content_items` with the structured form that the Responses/Chat
/// Completions APIs understand. /// Completions APIs understand.
#[derive(Debug, Default, Clone, PartialEq, JsonSchema, TS)] #[derive(Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct FunctionCallOutputPayload { pub struct FunctionCallOutputPayload {
pub content: String, pub content: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]

View File

@@ -18,11 +18,14 @@ pub enum ParsedCommand {
}, },
ListFiles { ListFiles {
cmd: String, cmd: String,
#[ts(optional = nullable)]
path: Option<String>, path: Option<String>,
}, },
Search { Search {
cmd: String, cmd: String,
#[ts(optional = nullable)]
query: Option<String>, query: Option<String>,
#[ts(optional = nullable)]
path: Option<String>, path: Option<String>,
}, },
Unknown { Unknown {

View File

@@ -21,6 +21,7 @@ pub struct PlanItemArg {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
#[ts(optional_fields = nullable)]
pub struct UpdatePlanArgs { pub struct UpdatePlanArgs {
#[serde(default)] #[serde(default)]
pub explanation: Option<String>, pub explanation: Option<String>,

View File

@@ -563,6 +563,7 @@ pub struct ItemCompletedEvent {
} }
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct ExitedReviewModeEvent { pub struct ExitedReviewModeEvent {
pub review_output: Option<ReviewOutputEvent>, pub review_output: Option<ReviewOutputEvent>,
} }
@@ -575,11 +576,13 @@ pub struct ErrorEvent {
} }
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct TaskCompleteEvent { pub struct TaskCompleteEvent {
pub last_agent_message: Option<String>, pub last_agent_message: Option<String>,
} }
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct TaskStartedEvent { pub struct TaskStartedEvent {
pub model_context_window: Option<i64>, pub model_context_window: Option<i64>,
} }
@@ -599,9 +602,11 @@ pub struct TokenUsage {
} }
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct TokenUsageInfo { pub struct TokenUsageInfo {
pub total_token_usage: TokenUsage, pub total_token_usage: TokenUsage,
pub last_token_usage: TokenUsage, pub last_token_usage: TokenUsage,
#[ts(optional = nullable)]
#[ts(type = "number | null")] #[ts(type = "number | null")]
pub model_context_window: Option<i64>, pub model_context_window: Option<i64>,
} }
@@ -662,25 +667,30 @@ impl TokenUsageInfo {
} }
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct TokenCountEvent { pub struct TokenCountEvent {
pub info: Option<TokenUsageInfo>, pub info: Option<TokenUsageInfo>,
pub rate_limits: Option<RateLimitSnapshot>, pub rate_limits: Option<RateLimitSnapshot>,
} }
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct RateLimitSnapshot { pub struct RateLimitSnapshot {
pub primary: Option<RateLimitWindow>, pub primary: Option<RateLimitWindow>,
pub secondary: Option<RateLimitWindow>, pub secondary: Option<RateLimitWindow>,
} }
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct RateLimitWindow { pub struct RateLimitWindow {
/// Percentage (0-100) of the window that has been consumed. /// Percentage (0-100) of the window that has been consumed.
pub used_percent: f64, pub used_percent: f64,
/// Rolling window duration, in minutes. /// Rolling window duration, in minutes.
#[ts(optional = nullable)]
#[ts(type = "number | null")] #[ts(type = "number | null")]
pub window_minutes: Option<i64>, pub window_minutes: Option<i64>,
/// Unix timestamp (seconds since epoch) when the window resets. /// Unix timestamp (seconds since epoch) when the window resets.
#[ts(optional = nullable)]
#[ts(type = "number | null")] #[ts(type = "number | null")]
pub resets_at: Option<i64>, pub resets_at: Option<i64>,
} }
@@ -794,6 +804,7 @@ pub struct AgentMessageEvent {
} }
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct UserMessageEvent { pub struct UserMessageEvent {
pub message: String, pub message: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -829,6 +840,7 @@ pub struct AgentReasoningDeltaEvent {
} }
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
#[ts(optional_fields = nullable)]
pub struct McpInvocation { pub struct McpInvocation {
/// Name of the MCP server as defined in the config. /// Name of the MCP server as defined in the config.
pub server: String, pub server: String,
@@ -947,6 +959,7 @@ pub enum SessionSource {
} }
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)] #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct SessionMeta { pub struct SessionMeta {
pub id: ConversationId, pub id: ConversationId,
pub timestamp: String, pub timestamp: String,
@@ -975,6 +988,7 @@ impl Default for SessionMeta {
} }
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct SessionMetaLine { pub struct SessionMetaLine {
#[serde(flatten)] #[serde(flatten)]
pub meta: SessionMeta, pub meta: SessionMeta,
@@ -1010,6 +1024,7 @@ impl From<CompactedItem> for ResponseItem {
} }
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)] #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct TurnContextItem { pub struct TurnContextItem {
pub cwd: PathBuf, pub cwd: PathBuf,
pub approval_policy: AskForApproval, pub approval_policy: AskForApproval,
@@ -1028,6 +1043,7 @@ pub struct RolloutLine {
} }
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)] #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct GitInfo { pub struct GitInfo {
/// Current commit hash (SHA) /// Current commit hash (SHA)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -1161,6 +1177,7 @@ pub struct BackgroundEventEvent {
} }
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct DeprecationNoticeEvent { pub struct DeprecationNoticeEvent {
/// Concise summary of what is deprecated. /// Concise summary of what is deprecated.
pub summary: String, pub summary: String,
@@ -1170,12 +1187,14 @@ pub struct DeprecationNoticeEvent {
} }
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct UndoStartedEvent { pub struct UndoStartedEvent {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>, pub message: Option<String>,
} }
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct UndoCompletedEvent { pub struct UndoCompletedEvent {
pub success: bool, pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -1220,6 +1239,7 @@ pub struct TurnDiffEvent {
} }
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct GetHistoryEntryResponseEvent { pub struct GetHistoryEntryResponseEvent {
pub offset: usize, pub offset: usize,
pub log_id: u64, pub log_id: u64,
@@ -1269,6 +1289,7 @@ pub struct ListCustomPromptsResponseEvent {
} }
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, TS)] #[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct SessionConfiguredEvent { pub struct SessionConfiguredEvent {
/// Name left as session_id instead of conversation_id for backwards compatibility. /// Name left as session_id instead of conversation_id for backwards compatibility.
pub session_id: ConversationId, pub session_id: ConversationId,
@@ -1329,6 +1350,7 @@ pub enum FileChange {
}, },
Update { Update {
unified_diff: String, unified_diff: String,
#[ts(optional = nullable)]
move_path: Option<PathBuf>, move_path: Option<PathBuf>,
}, },
} }

View File

@@ -28,6 +28,7 @@ type CommitID = String;
/// Details of a ghost commit created from a repository state. /// Details of a ghost commit created from a repository state.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct GhostCommit { pub struct GhostCommit {
id: CommitID, id: CommitID,
parent: Option<CommitID>, parent: Option<CommitID>,