1use std::collections::HashMap;
2use std::fmt::Display;
3use std::path::PathBuf;
4
5use crate::config_types::ReasoningEffort;
6use crate::config_types::ReasoningSummary;
7use crate::config_types::SandboxMode;
8use crate::config_types::Verbosity;
9use crate::dynamic_tools::DynamicToolSpec;
10use crate::models::PermissionProfile;
11use crate::protocol::AskForApproval;
12use crate::protocol::EventMsg;
13use crate::protocol::FileChange;
14use crate::protocol::ReviewDecision;
15use crate::protocol::SandboxPolicy;
16use crate::protocol::TurnAbortReason;
17use mcp_types::JSONRPCNotification;
18use mcp_types::RequestId;
19use serde::Deserialize;
20use serde::Serialize;
21use strum_macros::Display;
22use ts_rs::TS;
23use uuid::Uuid;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, TS, Hash)]
26#[ts(type = "string")]
27pub struct ConversationId {
28 uuid: Uuid,
29}
30
31impl ConversationId {
32 pub fn new() -> Self {
33 Self {
34 uuid: Uuid::new_v4(),
35 }
36 }
37
38 pub fn from_string(s: &str) -> Result<Self, uuid::Error> {
39 Ok(Self {
40 uuid: Uuid::parse_str(s)?,
41 })
42 }
43}
44
45impl From<Uuid> for ConversationId {
46 fn from(uuid: Uuid) -> Self {
47 Self { uuid }
48 }
49}
50
51impl From<ConversationId> for Uuid {
52 fn from(id: ConversationId) -> Self {
53 id.uuid
54 }
55}
56
57impl Default for ConversationId {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63impl Display for ConversationId {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 write!(f, "{}", self.uuid)
66 }
67}
68
69impl Serialize for ConversationId {
70 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
71 where
72 S: serde::Serializer,
73 {
74 serializer.collect_str(&self.uuid)
75 }
76}
77
78impl<'de> Deserialize<'de> for ConversationId {
79 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
80 where
81 D: serde::Deserializer<'de>,
82 {
83 let value = String::deserialize(deserializer)?;
84 let uuid = Uuid::parse_str(&value).map_err(serde::de::Error::custom)?;
85 Ok(Self { uuid })
86 }
87}
88
89#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)]
90#[ts(type = "string")]
91pub struct GitSha(pub String);
92
93impl GitSha {
94 pub fn new(sha: &str) -> Self {
95 Self(sha.to_string())
96 }
97}
98
99#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Display, TS)]
100#[serde(rename_all = "lowercase")]
101pub enum AuthMode {
102 ApiKey,
103 ChatGPT,
104 #[serde(rename = "chatgptAuthTokens")]
105 #[ts(rename = "chatgptAuthTokens")]
106 #[strum(serialize = "chatgptAuthTokens")]
107 ChatgptAuthTokens,
108 Hanzo,
110}
111
112impl AuthMode {
113 pub fn is_chatgpt(self) -> bool {
114 matches!(self, AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens)
115 }
116
117 pub fn is_hanzo(self) -> bool {
118 matches!(self, AuthMode::Hanzo)
119 }
120}
121
122#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
124#[serde(tag = "method", rename_all = "camelCase")]
125pub enum ClientRequest {
126 Initialize {
127 #[serde(rename = "id")]
128 request_id: RequestId,
129 params: InitializeParams,
130 },
131 NewConversation {
132 #[serde(rename = "id")]
133 request_id: RequestId,
134 params: NewConversationParams,
135 },
136 ListConversations {
138 #[serde(rename = "id")]
139 request_id: RequestId,
140 params: ListConversationsParams,
141 },
142 ResumeConversation {
144 #[serde(rename = "id")]
145 request_id: RequestId,
146 params: ResumeConversationParams,
147 },
148 ArchiveConversation {
149 #[serde(rename = "id")]
150 request_id: RequestId,
151 params: ArchiveConversationParams,
152 },
153 SendUserMessage {
154 #[serde(rename = "id")]
155 request_id: RequestId,
156 params: SendUserMessageParams,
157 },
158 SendUserTurn {
159 #[serde(rename = "id")]
160 request_id: RequestId,
161 params: SendUserTurnParams,
162 },
163 InterruptConversation {
164 #[serde(rename = "id")]
165 request_id: RequestId,
166 params: InterruptConversationParams,
167 },
168 AddConversationListener {
169 #[serde(rename = "id")]
170 request_id: RequestId,
171 params: AddConversationListenerParams,
172 },
173 RemoveConversationListener {
174 #[serde(rename = "id")]
175 request_id: RequestId,
176 params: RemoveConversationListenerParams,
177 },
178 GitDiffToRemote {
179 #[serde(rename = "id")]
180 request_id: RequestId,
181 params: GitDiffToRemoteParams,
182 },
183 LoginApiKey {
184 #[serde(rename = "id")]
185 request_id: RequestId,
186 params: LoginApiKeyParams,
187 },
188 LoginChatGpt {
189 #[serde(rename = "id")]
190 request_id: RequestId,
191
192 #[ts(type = "undefined")]
193 #[serde(skip_serializing_if = "Option::is_none")]
194 params: Option<()>,
195 },
196 CancelLoginChatGpt {
197 #[serde(rename = "id")]
198 request_id: RequestId,
199 params: CancelLoginChatGptParams,
200 },
201 LogoutChatGpt {
202 #[serde(rename = "id")]
203 request_id: RequestId,
204
205 #[ts(type = "undefined")]
206 #[serde(skip_serializing_if = "Option::is_none")]
207 params: Option<()>,
208 },
209 GetAuthStatus {
210 #[serde(rename = "id")]
211 request_id: RequestId,
212 params: GetAuthStatusParams,
213 },
214 GetUserSavedConfig {
215 #[serde(rename = "id")]
216 request_id: RequestId,
217
218 #[ts(type = "undefined")]
219 #[serde(skip_serializing_if = "Option::is_none")]
220 params: Option<()>,
221 },
222 SetDefaultModel {
223 #[serde(rename = "id")]
224 request_id: RequestId,
225 params: SetDefaultModelParams,
226 },
227 GetUserAgent {
228 #[serde(rename = "id")]
229 request_id: RequestId,
230
231 #[ts(type = "undefined")]
232 #[serde(skip_serializing_if = "Option::is_none")]
233 params: Option<()>,
234 },
235 UserInfo {
236 #[serde(rename = "id")]
237 request_id: RequestId,
238
239 #[ts(type = "undefined")]
240 #[serde(skip_serializing_if = "Option::is_none")]
241 params: Option<()>,
242 },
243 FuzzyFileSearch {
244 #[serde(rename = "id")]
245 request_id: RequestId,
246 params: FuzzyFileSearchParams,
247 },
248 ExecOneOffCommand {
250 #[serde(rename = "id")]
251 request_id: RequestId,
252 params: ExecOneOffCommandParams,
253 },
254}
255
256#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)]
257#[serde(rename_all = "camelCase")]
258pub struct InitializeParams {
259 pub client_info: ClientInfo,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub capabilities: Option<InitializeCapabilities>,
262}
263
264#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, TS)]
265#[serde(rename_all = "camelCase")]
266pub struct InitializeCapabilities {
267 #[serde(default)]
269 pub experimental_api: bool,
270
271 #[serde(skip_serializing_if = "Option::is_none")]
274 #[ts(optional = nullable)]
275 pub opt_out_notification_methods: Option<Vec<String>>,
276}
277
278#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)]
279#[serde(rename_all = "camelCase")]
280pub struct ClientInfo {
281 pub name: String,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub title: Option<String>,
284 pub version: String,
285}
286
287#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
288#[serde(rename_all = "camelCase")]
289pub struct InitializeResponse {
290 pub user_agent: String,
291}
292
293#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)]
294#[serde(rename_all = "camelCase")]
295pub struct NewConversationParams {
296 #[serde(skip_serializing_if = "Option::is_none")]
298 pub model: Option<String>,
299
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub profile: Option<String>,
303
304 #[serde(skip_serializing_if = "Option::is_none")]
307 pub cwd: Option<String>,
308
309 #[serde(skip_serializing_if = "Option::is_none")]
312 pub approval_policy: Option<AskForApproval>,
313
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub sandbox: Option<SandboxMode>,
317
318 #[serde(skip_serializing_if = "Option::is_none")]
321 pub config: Option<HashMap<String, serde_json::Value>>,
322
323 #[serde(skip_serializing_if = "Option::is_none")]
325 pub base_instructions: Option<String>,
326
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub include_plan_tool: Option<bool>,
330
331 #[serde(skip_serializing_if = "Option::is_none")]
333 pub include_apply_patch_tool: Option<bool>,
334
335 #[serde(skip_serializing_if = "Option::is_none")]
337 pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
338}
339
340#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
341#[serde(rename_all = "camelCase")]
342pub struct NewConversationResponse {
343 pub conversation_id: ConversationId,
344 pub model: String,
345 #[serde(skip_serializing_if = "Option::is_none")]
347 pub reasoning_effort: Option<ReasoningEffort>,
348 pub rollout_path: PathBuf,
349}
350
351#[derive(Serialize, Deserialize, Debug, Clone, TS)]
352#[serde(rename_all = "camelCase")]
353pub struct ResumeConversationResponse {
354 pub conversation_id: ConversationId,
355 pub model: String,
356 #[serde(skip_serializing_if = "Option::is_none")]
357 pub initial_messages: Option<Vec<EventMsg>>,
358}
359
360#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)]
361#[serde(rename_all = "camelCase")]
362pub struct ListConversationsParams {
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub page_size: Option<usize>,
366 #[serde(skip_serializing_if = "Option::is_none")]
368 pub cursor: Option<String>,
369}
370
371#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
372#[serde(rename_all = "camelCase")]
373pub struct ConversationSummary {
374 pub conversation_id: ConversationId,
375 pub path: PathBuf,
376 pub preview: String,
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub timestamp: Option<String>,
380}
381
382#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
383#[serde(rename_all = "camelCase")]
384pub struct ListConversationsResponse {
385 pub items: Vec<ConversationSummary>,
386 #[serde(skip_serializing_if = "Option::is_none")]
389 pub next_cursor: Option<String>,
390}
391
392#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
393#[serde(rename_all = "camelCase")]
394pub struct ResumeConversationParams {
395 pub path: PathBuf,
397 #[serde(skip_serializing_if = "Option::is_none")]
399 pub overrides: Option<NewConversationParams>,
400}
401
402#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
403#[serde(rename_all = "camelCase")]
404pub struct AddConversationSubscriptionResponse {
405 pub subscription_id: Uuid,
406}
407
408#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
410#[serde(rename_all = "camelCase")]
411pub struct ArchiveConversationParams {
412 pub conversation_id: ConversationId,
413 pub rollout_path: PathBuf,
414}
415
416#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
417#[serde(rename_all = "camelCase")]
418pub struct ArchiveConversationResponse {}
419
420#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
421#[serde(rename_all = "camelCase")]
422pub struct RemoveConversationSubscriptionResponse {}
423
424#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
425#[serde(rename_all = "camelCase")]
426pub struct LoginApiKeyParams {
427 pub api_key: String,
428}
429
430#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
431#[serde(rename_all = "camelCase")]
432pub struct LoginApiKeyResponse {}
433
434#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
435#[serde(rename_all = "camelCase")]
436pub struct LoginChatGptResponse {
437 pub login_id: Uuid,
438 pub auth_url: String,
440}
441
442#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
443#[serde(rename_all = "camelCase")]
444pub struct GitDiffToRemoteResponse {
445 pub sha: GitSha,
446 pub diff: String,
447}
448
449#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
450#[serde(rename_all = "camelCase")]
451pub struct CancelLoginChatGptParams {
452 pub login_id: Uuid,
453}
454
455#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
456#[serde(rename_all = "camelCase")]
457pub struct GitDiffToRemoteParams {
458 pub cwd: PathBuf,
459}
460
461#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
462#[serde(rename_all = "camelCase")]
463pub struct CancelLoginChatGptResponse {}
464
465#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
466#[serde(rename_all = "camelCase")]
467pub struct LogoutChatGptParams {}
468
469#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
470#[serde(rename_all = "camelCase")]
471pub struct LogoutChatGptResponse {}
472
473#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
474#[serde(rename_all = "camelCase")]
475pub struct GetAuthStatusParams {
476 #[serde(skip_serializing_if = "Option::is_none")]
478 pub include_token: Option<bool>,
479 #[serde(skip_serializing_if = "Option::is_none")]
481 pub refresh_token: Option<bool>,
482}
483
484#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
485#[serde(rename_all = "camelCase")]
486pub struct ExecOneOffCommandParams {
487 pub command: Vec<String>,
489 pub timeout_ms: Option<u64>,
492 #[serde(skip_serializing_if = "Option::is_none")]
494 pub cwd: Option<PathBuf>,
495 #[serde(skip_serializing_if = "Option::is_none")]
497 pub sandbox_policy: Option<SandboxPolicy>,
498}
499
500#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
501#[serde(rename_all = "camelCase")]
502pub struct ExecArbitraryCommandResponse {
503 pub exit_code: i32,
504 pub stdout: String,
505 pub stderr: String,
506}
507
508#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
509#[serde(rename_all = "camelCase")]
510pub struct GetAuthStatusResponse {
511 #[serde(skip_serializing_if = "Option::is_none")]
512 pub auth_method: Option<AuthMode>,
513 #[serde(skip_serializing_if = "Option::is_none")]
514 pub auth_token: Option<String>,
515
516 #[serde(skip_serializing_if = "Option::is_none")]
520 pub requires_openai_auth: Option<bool>,
521}
522
523#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
524#[serde(rename_all = "camelCase")]
525pub struct GetUserAgentResponse {
526 pub user_agent: String,
527}
528
529#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
530#[serde(rename_all = "camelCase")]
531pub struct UserInfoResponse {
532 pub alleged_user_email: Option<String>,
537}
538
539#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
540#[serde(rename_all = "camelCase")]
541pub struct GetUserSavedConfigResponse {
542 pub config: UserSavedConfig,
543}
544
545#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
546#[serde(rename_all = "camelCase")]
547pub struct SetDefaultModelParams {
548 #[serde(skip_serializing_if = "Option::is_none")]
550 pub model: Option<String>,
551 #[serde(skip_serializing_if = "Option::is_none")]
554 pub reasoning_effort: Option<ReasoningEffort>,
555}
556
557#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
558#[serde(rename_all = "camelCase")]
559pub struct SetDefaultModelResponse {}
560
561#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, TS)]
565#[serde(rename_all = "camelCase")]
566pub struct UserSavedConfig {
567 #[serde(skip_serializing_if = "Option::is_none")]
569 pub approval_policy: Option<AskForApproval>,
570 #[serde(skip_serializing_if = "Option::is_none")]
571 pub sandbox_mode: Option<SandboxMode>,
572 #[serde(skip_serializing_if = "Option::is_none")]
573 pub sandbox_settings: Option<SandboxSettings>,
574
575 #[serde(skip_serializing_if = "Option::is_none")]
577 pub model: Option<String>,
578 #[serde(skip_serializing_if = "Option::is_none")]
579 pub model_reasoning_effort: Option<ReasoningEffort>,
580 #[serde(skip_serializing_if = "Option::is_none")]
581 pub model_reasoning_summary: Option<ReasoningSummary>,
582 #[serde(skip_serializing_if = "Option::is_none")]
583 pub model_verbosity: Option<Verbosity>,
584
585 #[serde(skip_serializing_if = "Option::is_none")]
587 pub tools: Option<Tools>,
588
589 #[serde(skip_serializing_if = "Option::is_none")]
591 pub profile: Option<String>,
592 #[serde(default)]
593 pub profiles: HashMap<String, Profile>,
594}
595
596#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, TS)]
598#[serde(rename_all = "camelCase")]
599pub struct Profile {
600 pub model: Option<String>,
601 pub model_provider: Option<String>,
604 pub approval_policy: Option<AskForApproval>,
605 pub model_reasoning_effort: Option<ReasoningEffort>,
606 pub model_reasoning_summary: Option<ReasoningSummary>,
607 pub model_verbosity: Option<Verbosity>,
608 pub chatgpt_base_url: Option<String>,
609}
610#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, TS)]
612#[serde(rename_all = "camelCase")]
613pub struct Tools {
614 #[serde(skip_serializing_if = "Option::is_none")]
615 pub web_search: Option<bool>,
616 #[serde(skip_serializing_if = "Option::is_none")]
617 pub view_image: Option<bool>,
618}
619
620#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, TS)]
622#[serde(rename_all = "camelCase")]
623pub struct SandboxSettings {
624 #[serde(default)]
625 pub writable_roots: Vec<PathBuf>,
626 #[serde(skip_serializing_if = "Option::is_none")]
627 pub network_access: Option<bool>,
628 #[serde(skip_serializing_if = "Option::is_none")]
629 pub exclude_tmpdir_env_var: Option<bool>,
630 #[serde(skip_serializing_if = "Option::is_none")]
631 pub exclude_slash_tmp: Option<bool>,
632}
633
634#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
635#[serde(rename_all = "camelCase")]
636pub struct SendUserMessageParams {
637 pub conversation_id: ConversationId,
638 pub items: Vec<InputItem>,
639}
640
641#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
642#[serde(rename_all = "camelCase")]
643pub struct SendUserTurnParams {
644 pub conversation_id: ConversationId,
645 pub items: Vec<InputItem>,
646 pub cwd: PathBuf,
647 pub approval_policy: AskForApproval,
648 pub sandbox_policy: SandboxPolicy,
649 pub model: String,
650 #[serde(skip_serializing_if = "Option::is_none")]
651 pub effort: Option<ReasoningEffort>,
652 pub summary: ReasoningSummary,
653}
654
655#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
656#[serde(rename_all = "camelCase")]
657pub struct SendUserTurnResponse {}
658
659#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
660#[serde(rename_all = "camelCase")]
661pub struct InterruptConversationParams {
662 pub conversation_id: ConversationId,
663}
664
665#[derive(Serialize, Deserialize, Debug, Clone, TS)]
666#[serde(rename_all = "camelCase")]
667pub struct InterruptConversationResponse {
668 pub abort_reason: TurnAbortReason,
669}
670
671#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
672#[serde(rename_all = "camelCase")]
673pub struct SendUserMessageResponse {}
674
675#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
676#[serde(rename_all = "camelCase")]
677pub struct AddConversationListenerParams {
678 pub conversation_id: ConversationId,
679}
680
681#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
682#[serde(rename_all = "camelCase")]
683pub struct RemoveConversationListenerParams {
684 pub subscription_id: Uuid,
685}
686
687#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
688#[serde(rename_all = "camelCase")]
689#[serde(tag = "type", content = "data")]
690pub enum InputItem {
691 Text {
692 text: String,
693 },
694 Image {
696 image_url: String,
697 },
698
699 LocalImage {
702 path: PathBuf,
703 },
704}
705
706pub const APPLY_PATCH_APPROVAL_METHOD: &str = "applyPatchApproval";
709pub const EXEC_COMMAND_APPROVAL_METHOD: &str = "execCommandApproval";
710pub const DYNAMIC_TOOL_CALL_METHOD: &str = "dynamicToolCall";
711
712#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
714#[serde(tag = "method", rename_all = "camelCase")]
715pub enum ServerRequest {
716 ApplyPatchApproval {
718 #[serde(rename = "id")]
719 request_id: RequestId,
720 params: ApplyPatchApprovalParams,
721 },
722 ExecCommandApproval {
724 #[serde(rename = "id")]
725 request_id: RequestId,
726 params: ExecCommandApprovalParams,
727 },
728 DynamicToolCall {
730 #[serde(rename = "id")]
731 request_id: RequestId,
732 params: DynamicToolCallParams,
733 },
734}
735
736#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
737pub struct ApplyPatchApprovalParams {
738 pub conversation_id: ConversationId,
739 pub call_id: String,
742 pub file_changes: HashMap<PathBuf, FileChange>,
743 #[serde(skip_serializing_if = "Option::is_none")]
745 pub reason: Option<String>,
746 #[serde(skip_serializing_if = "Option::is_none")]
749 pub grant_root: Option<PathBuf>,
750}
751
752#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
753pub struct ExecCommandApprovalParams {
754 pub conversation_id: ConversationId,
755 pub call_id: String,
758 #[serde(default, skip_serializing_if = "Option::is_none")]
760 pub approval_id: Option<String>,
761 pub command: Vec<String>,
762 pub cwd: PathBuf,
763 #[serde(skip_serializing_if = "Option::is_none")]
764 pub reason: Option<String>,
765 #[serde(default, skip_serializing_if = "Option::is_none")]
766 pub additional_permissions: Option<PermissionProfile>,
767}
768
769#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
770pub struct ExecCommandApprovalResponse {
771 pub decision: ReviewDecision,
772}
773
774#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
775pub struct DynamicToolCallParams {
776 pub conversation_id: ConversationId,
777 pub turn_id: String,
778 pub call_id: String,
779 pub tool: String,
780 pub arguments: serde_json::Value,
781}
782
783#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
784pub struct DynamicToolCallResponse {
785 pub output: String,
786 pub success: bool,
787}
788
789#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
790pub struct ApplyPatchApprovalResponse {
791 pub decision: ReviewDecision,
792}
793
794#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
795#[serde(rename_all = "camelCase")]
796#[ts(rename_all = "camelCase")]
797pub struct FuzzyFileSearchParams {
798 pub query: String,
799 pub roots: Vec<String>,
800 #[serde(skip_serializing_if = "Option::is_none")]
802 pub cancellation_token: Option<String>,
803}
804
805#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
807pub struct FuzzyFileSearchResult {
808 pub root: String,
809 pub path: String,
810 pub file_name: String,
811 pub score: u32,
812 #[serde(skip_serializing_if = "Option::is_none")]
813 pub indices: Option<Vec<u32>>,
814}
815
816#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
817pub struct FuzzyFileSearchResponse {
818 pub files: Vec<FuzzyFileSearchResult>,
819}
820
821#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
822#[serde(rename_all = "camelCase")]
823pub struct LoginChatGptCompleteNotification {
824 pub login_id: Uuid,
825 pub success: bool,
826 #[serde(skip_serializing_if = "Option::is_none")]
827 pub error: Option<String>,
828}
829
830#[derive(Serialize, Deserialize, Debug, Clone, TS)]
831#[serde(rename_all = "camelCase")]
832pub struct SessionConfiguredNotification {
833 pub session_id: ConversationId,
835
836 pub model: String,
838
839 #[serde(skip_serializing_if = "Option::is_none")]
841 pub reasoning_effort: Option<ReasoningEffort>,
842
843 pub history_log_id: u64,
845
846 pub history_entry_count: usize,
848
849 #[serde(skip_serializing_if = "Option::is_none")]
852 pub initial_messages: Option<Vec<EventMsg>>,
853
854 pub rollout_path: PathBuf,
855}
856
857#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
858#[serde(rename_all = "camelCase")]
859pub struct AuthStatusChangeNotification {
860 #[serde(skip_serializing_if = "Option::is_none")]
862 pub auth_method: Option<AuthMode>,
863}
864
865#[derive(Serialize, Deserialize, Debug, Clone, TS, Display)]
867#[serde(tag = "method", content = "params", rename_all = "camelCase")]
868#[strum(serialize_all = "camelCase")]
869pub enum ServerNotification {
870 AuthStatusChange(AuthStatusChangeNotification),
872
873 LoginChatGptComplete(LoginChatGptCompleteNotification),
875
876 SessionConfigured(SessionConfiguredNotification),
878}
879
880impl ServerNotification {
881 pub fn to_params(self) -> Result<serde_json::Value, serde_json::Error> {
882 match self {
883 ServerNotification::AuthStatusChange(params) => serde_json::to_value(params),
884 ServerNotification::LoginChatGptComplete(params) => serde_json::to_value(params),
885 ServerNotification::SessionConfigured(params) => serde_json::to_value(params),
886 }
887 }
888}
889
890impl TryFrom<JSONRPCNotification> for ServerNotification {
891 type Error = serde_json::Error;
892
893 fn try_from(value: JSONRPCNotification) -> Result<Self, Self::Error> {
894 serde_json::from_value(serde_json::to_value(value)?)
895 }
896}
897
898#[derive(Serialize, Deserialize, Debug, Clone, TS, Display)]
900#[serde(tag = "method", content = "params", rename_all = "camelCase")]
901#[strum(serialize_all = "camelCase")]
902pub enum ClientNotification {
903 Initialized,
904}
905
906#[cfg(test)]
907mod tests {
908 use super::*;
909 use anyhow::Result;
910 use pretty_assertions::assert_eq;
911 use serde_json::json;
912
913 #[test]
914 fn serialize_new_conversation() -> Result<()> {
915 let request = ClientRequest::NewConversation {
916 request_id: RequestId::Integer(42),
917 params: NewConversationParams {
918 model: Some("gpt-5.1-codex".to_string()),
919 profile: None,
920 cwd: None,
921 approval_policy: Some(AskForApproval::OnRequest),
922 sandbox: None,
923 config: None,
924 base_instructions: None,
925 include_plan_tool: None,
926 include_apply_patch_tool: None,
927 dynamic_tools: None,
928 },
929 };
930 assert_eq!(
931 json!({
932 "method": "newConversation",
933 "id": 42,
934 "params": {
935 "model": "gpt-5.1-codex",
936 "approvalPolicy": "on-request"
937 }
938 }),
939 serde_json::to_value(&request)?,
940 );
941 Ok(())
942 }
943
944 #[test]
945 fn test_conversation_id_default_is_not_zeroes() {
946 let id = ConversationId::default();
947 assert_ne!(id.uuid, Uuid::nil());
948 }
949
950 #[test]
951 fn conversation_id_serializes_as_plain_string() -> Result<()> {
952 let id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
953
954 assert_eq!(
955 json!("67e55044-10b1-426f-9247-bb680e5fe0c8"),
956 serde_json::to_value(id)?
957 );
958 Ok(())
959 }
960
961 #[test]
962 fn conversation_id_deserializes_from_plain_string() -> Result<()> {
963 let id: ConversationId =
964 serde_json::from_value(json!("67e55044-10b1-426f-9247-bb680e5fe0c8"))?;
965
966 assert_eq!(
967 ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?,
968 id,
969 );
970 Ok(())
971 }
972
973 #[test]
974 fn serialize_client_notification() -> Result<()> {
975 let notification = ClientNotification::Initialized;
976 assert_eq!(
978 json!({
979 "method": "initialized",
980 }),
981 serde_json::to_value(¬ification)?,
982 );
983 Ok(())
984 }
985}