git.iko.soy/opencode-android
on master help sign in
248 lines 7.7 kB history raw
1//! Wire types for the opencode HTTP + SSE API.
2//!
3//! These mirror the JSON shapes the opencode server emits (`sst/opencode`).
4//! They are deliberately lenient: every unknown field is ignored and every
5//! discriminated union has a catch-all `Other` arm so that new server versions
6//! (which add event / part / tool-state variants over time) never break the
7//! stream parser.
9use serde::Deserialize;
11// ─── Sessions ────────────────────────────────────────────────────────────────
13#[derive(Deserialize, Debug, Clone)]
14pub struct WireSession {
15 pub id: String,
16 #[serde(default)]
17 pub title: String,
20// ─── Messages ────────────────────────────────────────────────────────────────
22/// One element of `GET /session/{id}/message`: `{ info, parts }`.
23#[derive(Deserialize, Debug)]
24pub struct WireMessageEnvelope {
25 pub info: WireMessageInfo,
26 #[serde(default)]
27 pub parts: Vec<WirePart>,
30#[derive(Deserialize, Debug, Clone)]
31pub struct WireMessageInfo {
32 pub id: String,
33 #[serde(default)]
34 pub role: String,
35 #[serde(default, rename = "sessionID")]
36 pub session_id: String,
37 #[serde(default)]
38 pub time: WireTime,
39 #[serde(default)]
40 pub error: Option<WireError>,
43#[derive(Deserialize, Debug, Clone, Default)]
44pub struct WireTime {
45 #[serde(default)]
46 pub created: u64,
47 /// Present once an assistant message has finished generating.
48 #[serde(default)]
49 pub completed: Option<u64>,
52/// Message / part content. Only the variants a chat UI renders are modelled;
53/// everything else (`file`, `agent`, `step-start`, `step-finish`, `snapshot`,
54/// `patch`, …) falls through to `Other`.
55#[derive(Deserialize, Debug, Clone)]
56#[serde(tag = "type", rename_all = "lowercase")]
57pub enum WirePart {
58 Text {
59 #[serde(default)]
60 id: String,
61 #[serde(default, rename = "messageID")]
62 message_id: String,
63 #[serde(default)]
64 text: String,
65 #[serde(default)]
66 synthetic: bool,
67 },
68 Reasoning {
69 #[serde(default)]
70 id: String,
71 #[serde(default, rename = "messageID")]
72 message_id: String,
73 #[serde(default)]
74 text: String,
75 },
76 Tool {
77 #[serde(default)]
78 id: String,
79 #[serde(default, rename = "messageID")]
80 message_id: String,
81 #[serde(default)]
82 tool: String,
83 #[serde(default)]
84 state: serde_json::Value,
85 },
86 #[serde(other)]
87 Other,
90impl WirePart {
91 pub fn message_id(&self) -> &str {
92 match self {
93 WirePart::Text { message_id, .. }
94 | WirePart::Reasoning { message_id, .. }
95 | WirePart::Tool { message_id, .. } => message_id,
96 WirePart::Other => "",
97 }
98 }
101/// Compact view of a `tool` part's state (the union keyed on `status`).
102pub struct ToolStateView {
103 pub status: String,
104 pub title: Option<String>,
107/// Pull the `status` and a human title/output out of a tool part's `state` blob.
108pub fn tool_state_view(state: &serde_json::Value) -> ToolStateView {
109 let status = state
110 .get("status")
111 .and_then(|v| v.as_str())
112 .unwrap_or("pending")
113 .to_string();
114 let title = state
115 .get("title")
116 .and_then(|v| v.as_str())
117 .or_else(|| state.get("error").and_then(|v| v.as_str()))
118 .map(str::to_string);
119 ToolStateView { status, title }
122// ─── Errors ──────────────────────────────────────────────────────────────────
124#[derive(Deserialize, Debug, Clone)]
125pub struct WireError {
126 #[serde(default)]
127 pub name: String,
128 #[serde(default)]
129 pub data: serde_json::Value,
132impl WireError {
133 /// Best human-readable message: `data.message` if present, else the error name.
134 pub fn message(&self) -> String {
135 self.data
136 .get("message")
137 .and_then(|v| v.as_str())
138 .map(str::to_string)
139 .filter(|s| !s.is_empty())
140 .unwrap_or_else(|| {
141 if self.name.is_empty() {
142 "Unknown error".to_string()
143 } else {
144 self.name.clone()
145 }
146 })
147 }
150// ─── SSE `/event` stream ─────────────────────────────────────────────────────
152/// The SSE envelope: `{ "type": "...", "properties": { ... } }`. Unknown event
153/// types decode to `Other` and are ignored.
154#[derive(Deserialize, Debug)]
155#[serde(tag = "type")]
156pub enum ServerEvent {
157 #[serde(rename = "message.updated")]
158 MessageUpdated { properties: MessageUpdatedProps },
159 #[serde(rename = "message.part.updated")]
160 MessagePartUpdated { properties: PartUpdatedProps },
161 #[serde(rename = "message.part.removed")]
162 MessagePartRemoved { properties: PartRemovedProps },
163 #[serde(rename = "message.removed")]
164 MessageRemoved { properties: MessageRemovedProps },
165 #[serde(rename = "session.idle")]
166 SessionIdle { properties: SessionIdProps },
167 #[serde(rename = "session.error")]
168 SessionError { properties: SessionErrorProps },
169 #[serde(rename = "session.updated")]
170 SessionUpdated { properties: SessionInfoProps },
171 #[serde(rename = "session.created")]
172 SessionCreated { properties: SessionInfoProps },
173 #[serde(rename = "session.deleted")]
174 SessionDeleted { properties: SessionDeletedProps },
175 #[serde(other)]
176 Other,
179#[derive(Deserialize, Debug)]
180pub struct MessageUpdatedProps {
181 pub info: WireMessageInfo,
184#[derive(Deserialize, Debug)]
185pub struct PartUpdatedProps {
186 pub part: WirePart,
187 #[serde(default)]
188 pub delta: Option<String>,
191#[derive(Deserialize, Debug)]
192pub struct PartRemovedProps {
193 #[serde(default, rename = "messageID")]
194 pub message_id: String,
195 #[serde(default, rename = "partID")]
196 pub part_id: String,
199#[derive(Deserialize, Debug)]
200pub struct MessageRemovedProps {
201 #[serde(default, rename = "messageID")]
202 pub message_id: String,
205#[derive(Deserialize, Debug)]
206pub struct SessionIdProps {
207 #[serde(default, rename = "sessionID")]
208 pub session_id: String,
211#[derive(Deserialize, Debug)]
212pub struct SessionErrorProps {
213 #[serde(default, rename = "sessionID")]
214 pub session_id: Option<String>,
215 #[serde(default)]
216 pub error: Option<WireError>,
219#[derive(Deserialize, Debug)]
220pub struct SessionInfoProps {
221 pub info: WireSession,
224#[derive(Deserialize, Debug)]
225pub struct SessionDeletedProps {
226 #[serde(default, rename = "sessionID")]
227 pub session_id: Option<String>,
228 #[serde(default)]
229 pub info: Option<WireSession>,
232// ─── Parse helpers ───────────────────────────────────────────────────────────
234pub fn parse_sessions(body: &str) -> Vec<WireSession> {
235 serde_json::from_str(body).unwrap_or_default()
238pub fn parse_session(body: &str) -> Option<WireSession> {
239 serde_json::from_str(body).ok()
242pub fn parse_messages(body: &str) -> Vec<WireMessageEnvelope> {
243 serde_json::from_str(body).unwrap_or_default()
246pub fn parse_event(json: &str) -> Option<ServerEvent> {
247 serde_json::from_str(json).ok()