Skip to main content

codeoid_protocol/
client.rs

1//! Client → Daemon messages.
2//!
3//! Every request carries an `id` so the daemon's `response.ok` /
4//! `response.error` / `session.list.result` can be correlated back to the
5//! caller.
6//!
7//! # Wire-format notes
8//!
9//! The TS daemon expects `camelCase` fields (e.g. `sessionId`, `approvalId`).
10//! Every struct-like variant below gets a per-variant `rename_all = "camelCase"`
11//! so `session_id` serializes as `sessionId`, etc. `#[serde(rename_all)]` on
12//! the enum itself only affects variant names, not variant fields — hence
13//! the repetition.
14
15use serde::{Deserialize, Serialize};
16use serde_json;
17
18use crate::session::SessionMode;
19
20/// Tagged union of every message a client can send the daemon.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(tag = "type")]
23pub enum ClientMessage {
24    #[serde(rename = "session.create", rename_all = "camelCase")]
25    SessionCreate {
26        id: String,
27        name: String,
28        workdir: String,
29    },
30
31    #[serde(rename = "session.list", rename_all = "camelCase")]
32    SessionList { id: String },
33
34    /// Request the backend's selectable model catalog. Daemon answers with
35    /// [`DaemonMessage::ModelsListResult`](crate::daemon::DaemonMessage::ModelsListResult).
36    #[serde(rename = "models.list", rename_all = "camelCase")]
37    ModelsList { id: String },
38
39    #[serde(rename = "session.attach", rename_all = "camelCase")]
40    SessionAttach { id: String, session_id: String },
41
42    #[serde(rename = "session.detach", rename_all = "camelCase")]
43    SessionDetach { id: String, session_id: String },
44
45    #[serde(rename = "session.send", rename_all = "camelCase")]
46    SessionSend {
47        id: String,
48        session_id: String,
49        text: String,
50        #[serde(default, skip_serializing_if = "Option::is_none")]
51        attachments: Option<Vec<Attachment>>,
52        #[serde(default, skip_serializing_if = "Option::is_none")]
53        priority: Option<SendPriority>,
54    },
55
56    #[serde(rename = "session.interrupt", rename_all = "camelCase")]
57    SessionInterrupt { id: String, session_id: String },
58
59    #[serde(rename = "session.approve", rename_all = "camelCase")]
60    SessionApprove {
61        id: String,
62        session_id: String,
63        approval_id: String,
64        approved: bool,
65        /// Optional patch shallow-merged into the original tool input
66        /// before the SDK runs the tool. Required for AskUserQuestion
67        /// where it carries `{ "answers": { "<question>": "..." } }`.
68        /// Omitted for binary approvals (Bash, Edit, ExitPlanMode, …).
69        #[serde(default, skip_serializing_if = "Option::is_none")]
70        updated_input: Option<serde_json::Value>,
71    },
72
73    #[serde(rename = "session.destroy", rename_all = "camelCase")]
74    SessionDestroy { id: String, session_id: String },
75
76    #[serde(rename = "session.set_mode", rename_all = "camelCase")]
77    SessionSetMode {
78        id: String,
79        session_id: String,
80        mode: SessionMode,
81        #[serde(default, skip_serializing_if = "Option::is_none")]
82        max_turns: Option<u32>,
83    },
84
85    #[serde(rename = "session.pin", rename_all = "camelCase")]
86    SessionPin {
87        id: String,
88        session_id: String,
89        path: String,
90    },
91
92    #[serde(rename = "session.unpin", rename_all = "camelCase")]
93    SessionUnpin {
94        id: String,
95        session_id: String,
96        path: String,
97    },
98
99    #[serde(rename = "session.rotate", rename_all = "camelCase")]
100    SessionRotate { id: String, session_id: String },
101
102    #[serde(rename = "session.search", rename_all = "camelCase")]
103    SessionSearch {
104        id: String,
105        query: String,
106        #[serde(default, skip_serializing_if = "Option::is_none")]
107        scope: Option<SearchScope>,
108        #[serde(default, skip_serializing_if = "Option::is_none")]
109        workdir: Option<String>,
110        #[serde(default, skip_serializing_if = "Option::is_none")]
111        limit: Option<u32>,
112    },
113
114    #[serde(rename = "session.set_model", rename_all = "camelCase")]
115    SessionSetModel {
116        id: String,
117        session_id: String,
118        model: String,
119        #[serde(default, skip_serializing_if = "Option::is_none")]
120        fallback_model: Option<Option<String>>,
121    },
122
123    #[serde(rename = "session.rename", rename_all = "camelCase")]
124    SessionRename {
125        id: String,
126        session_id: String,
127        name: String,
128    },
129
130    /// Snapshot of Claude Code config (`~/.claude/` + workdir `.claude/`)
131    /// for the focused session — agents, skills, MCP servers, hooks.
132    /// Daemon answers with `ClaudeConfigResult`.
133    #[serde(rename = "claude.config", rename_all = "camelCase")]
134    ClaudeConfig { id: String, session_id: String },
135
136    /// Export a session as a portable `ShareBundle`.
137    #[serde(rename = "session.export", rename_all = "camelCase")]
138    SessionExport {
139        id: String,
140        session_id: String,
141        #[serde(default, skip_serializing_if = "Option::is_none")]
142        include_memory: Option<bool>,
143        #[serde(default, skip_serializing_if = "Option::is_none")]
144        include_pinned_files: Option<bool>,
145        #[serde(default, skip_serializing_if = "Option::is_none")]
146        alias_override: Option<String>,
147        #[serde(default, skip_serializing_if = "Option::is_none")]
148        to_file: Option<bool>,
149    },
150
151    /// Import a session bundle. The TUI typically uses the `file`
152    /// variant (operator pastes a path); the web pushes inline JSON.
153    #[serde(rename = "session.import", rename_all = "camelCase")]
154    SessionImport {
155        id: String,
156        source: SessionImportSource,
157        target_workdir: String,
158        #[serde(default, skip_serializing_if = "Option::is_none")]
159        name_override: Option<String>,
160        #[serde(default, skip_serializing_if = "Option::is_none")]
161        write_pinned_files: Option<bool>,
162    },
163}
164
165/// Source of a `session.import` bundle.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(tag = "kind", rename_all = "camelCase")]
168pub enum SessionImportSource {
169    Inline { bundle: serde_json::Value },
170    File { path: String },
171}
172
173impl ClientMessage {
174    /// Correlation id used to match a [`DaemonMessage::ResponseOk`](crate::daemon::DaemonMessage::ResponseOk)
175    /// or [`DaemonMessage::ResponseError`](crate::daemon::DaemonMessage::ResponseError) back to this request.
176    #[must_use]
177    pub fn request_id(&self) -> &str {
178        match self {
179            Self::SessionCreate { id, .. }
180            | Self::SessionList { id }
181            | Self::ModelsList { id }
182            | Self::SessionAttach { id, .. }
183            | Self::SessionDetach { id, .. }
184            | Self::SessionSend { id, .. }
185            | Self::SessionInterrupt { id, .. }
186            | Self::SessionApprove { id, .. }
187            | Self::SessionDestroy { id, .. }
188            | Self::SessionSetMode { id, .. }
189            | Self::SessionPin { id, .. }
190            | Self::SessionUnpin { id, .. }
191            | Self::SessionRotate { id, .. }
192            | Self::SessionSearch { id, .. }
193            | Self::SessionSetModel { id, .. }
194            | Self::SessionRename { id, .. }
195            | Self::ClaudeConfig { id, .. }
196            | Self::SessionExport { id, .. }
197            | Self::SessionImport { id, .. } => id,
198        }
199    }
200}
201
202/// One-shot attachment pushed with `session.send`.
203#[derive(Debug, Clone, Serialize, Deserialize)]
204#[serde(rename_all = "camelCase")]
205pub struct Attachment {
206    pub path: String,
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub content: Option<String>,
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub mime_type: Option<String>,
211    /// Base64-encoded bytes, mutually exclusive with `content`.
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub data: Option<String>,
214}
215
216/// Mid-turn priority hint.
217///
218/// * `Now` — interrupt the agent's current turn and observe immediately.
219/// * `Next` — let the current turn finish, then pick this up.
220/// * `Later` — queue as a standard follow-up (default).
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
222#[serde(rename_all = "snake_case")]
223pub enum SendPriority {
224    Now,
225    Next,
226    Later,
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(rename_all = "snake_case")]
231pub enum SearchScope {
232    Workspace,
233    All,
234}