1//! Wire types for the opencode HTTP + SSE API.
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
11// ─── Sessions ────────────────────────────────────────────────────────────────
13#[derive(Deserialize, Debug, Clone)]
14pub struct WireSession {
20// ─── Messages ────────────────────────────────────────────────────────────────
22/// One element of `GET /session/{id}/message`: `{ info, parts }`.
23#[derive(Deserialize, Debug)]
24pub struct WireMessageEnvelope {
25 pub info: WireMessageInfo,
27 pub parts: Vec<WirePart>,
30#[derive(Deserialize, Debug, Clone)]
31pub struct WireMessageInfo {
35 #[serde(default, rename = "sessionID")]
36 pub session_id: String,
40 pub error: Option<WireError>,
43#[derive(Deserialize, Debug, Clone, Default)]
47 /// Present once an assistant message has finished generating.
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")]
61 #[serde(default, rename = "messageID")]
71 #[serde(default, rename = "messageID")]
79 #[serde(default, rename = "messageID")]
84 state: serde_json::Value,
91 pub fn message_id(&self) -> &str {
93 WirePart::Text { message_id, .. }
94 | WirePart::Reasoning { message_id, .. }
95 | WirePart::Tool { message_id, .. } => message_id,
96 WirePart::Other => "",
101/// Compact view of a `tool` part's state (the union keyed on `status`).
102pub struct ToolStateView {
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 {
111 .and_then(|v| v.as_str())
112 .unwrap_or("pending")
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 {
129 pub data: serde_json::Value,
133 /// Best human-readable message: `data.message` if present, else the error name.
134 pub fn message(&self) -> String {
137 .and_then(|v| v.as_str())
139 .filter(|s| !s.is_empty())
141 if self.name.is_empty() {
142 "Unknown error".to_string()
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 },
179#[derive(Deserialize, Debug)]
180pub struct MessageUpdatedProps {
181 pub info: WireMessageInfo,
184#[derive(Deserialize, Debug)]
185pub struct PartUpdatedProps {
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")]
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>,
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>,
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()
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list