diff --git a/crates/goat-agent/src/accounts.rs b/crates/goat-agent/src/accounts.rs index 6dd608d..274145f 100644 --- a/crates/goat-agent/src/accounts.rs +++ b/crates/goat-agent/src/accounts.rs @@ -277,15 +277,17 @@ pub(crate) async fn handle_login( return; } let resolved = match credential { - LoginCredential::ApiKey(secret) => Credential::ApiKey(SecretString::from(secret)), - LoginCredential::OAuth => match run_self_oauth(&provider, ctx.events, ctx.registry).await { - Ok(tokens) => Credential::OAuth(tokens), - Err(message) => { - login_failed(&provider, ctx.events, message).await; - emit_accounts_changed(ctx.events, ctx.registry, ctx.credentials).await; - return; + LoginCredential::ApiKey { key: secret } => Credential::ApiKey(SecretString::from(secret)), + LoginCredential::OAuth {} => { + match run_self_oauth(&provider, ctx.events, ctx.registry).await { + Ok(tokens) => Credential::OAuth(tokens), + Err(message) => { + login_failed(&provider, ctx.events, message).await; + emit_accounts_changed(ctx.events, ctx.registry, ctx.credentials).await; + return; + } } - }, + } }; finalize_login(ctx, provider, name, key, resolved).await; } diff --git a/crates/goat-agent/src/lib.rs b/crates/goat-agent/src/lib.rs index 5fd52ea..208510c 100644 --- a/crates/goat-agent/src/lib.rs +++ b/crates/goat-agent/src/lib.rs @@ -360,7 +360,7 @@ async fn run(agent: GoatAgent, mut ops: mpsc::Receiver, events: mpsc::Sender break; } } - Op::Clear => { + Op::Clear {} => { state.conversation.clear(); state.tracker.invalidate(); state.thread_id = None; @@ -421,7 +421,7 @@ async fn run(agent: GoatAgent, mut ops: mpsc::Receiver, events: mpsc::Sender .await; accounts::clear_account_registries(&account_registries); } - Op::ListThreads => { + Op::ListThreads {} => { threads::handle_list_threads(&store, &cwd, &events).await; } Op::Resume { thread_id: tid } => { @@ -436,7 +436,7 @@ async fn run(agent: GoatAgent, mut ops: mpsc::Receiver, events: mpsc::Sender ) .await; } - Op::ResumeLatest => { + Op::ResumeLatest {} => { threads::handle_resume_latest( &store, &skills, @@ -451,7 +451,7 @@ async fn run(agent: GoatAgent, mut ops: mpsc::Receiver, events: mpsc::Sender Op::RenameThread { title } => { threads::handle_rename(&store, state.thread_id, title, &events).await; } - Op::Shutdown => break, + Op::Shutdown {} => break, } } mcp.shutdown().await; @@ -937,7 +937,7 @@ mod tests { break; } } - ops.send(Op::Shutdown).await.unwrap(); + ops.send(Op::Shutdown {}).await.unwrap(); let provider2 = MockProvider { id: "mock".to_owned(), @@ -1474,12 +1474,12 @@ mod tests { .unwrap(); drain_until_task_done(&mut events).await; - ops.send(Op::ResumeLatest).await.unwrap(); + ops.send(Op::ResumeLatest {}).await.unwrap(); while let Some(event) = events.recv().await { if let Event::ConversationRestored { entries, .. } = event { assert!(entries.iter().any(|entry| matches!( entry, - goat_protocol::TranscriptEntry::User(text) if text == "hello there" + goat_protocol::TranscriptEntry::User { text } if text == "hello there" ))); return; } @@ -1509,8 +1509,8 @@ mod tests { let session = Session::spawn(agent); let (ops, mut events, _handle) = session.into_parts(); - ops.send(Op::ResumeLatest).await.unwrap(); - ops.send(Op::Shutdown).await.unwrap(); + ops.send(Op::ResumeLatest {}).await.unwrap(); + ops.send(Op::Shutdown {}).await.unwrap(); let mut saw_notify = false; while let Some(event) = events.recv().await { @@ -1565,7 +1565,7 @@ mod tests { .unwrap(); drain_until_task_done(&mut events).await; - ops.send(Op::Clear).await.unwrap(); + ops.send(Op::Clear {}).await.unwrap(); ops.send(Op::SubmitMessage { id: TaskId(2), diff --git a/crates/goat-agent/src/plan.rs b/crates/goat-agent/src/plan.rs index 8c73eca..7975f10 100644 --- a/crates/goat-agent/src/plan.rs +++ b/crates/goat-agent/src/plan.rs @@ -140,7 +140,7 @@ pub(crate) async fn run_propose_plan( res = rx => res, }; match decision { - Ok(PlanDecision::Approve) => { + Ok(PlanDecision::Approve {}) => { if let Some(cell) = env.transition { let inject = approved_plan_inject(&path); *cell diff --git a/crates/goat-agent/src/threads.rs b/crates/goat-agent/src/threads.rs index 61f9173..438edae 100644 --- a/crates/goat-agent/src/threads.rs +++ b/crates/goat-agent/src/threads.rs @@ -200,7 +200,7 @@ pub(crate) async fn handle_resume( Some((command, output)) => { entries.push(TranscriptEntry::Shell { command, output }); } - None => entries.push(TranscriptEntry::User(text.clone())), + None => entries.push(TranscriptEntry::User { text: text.clone() }), } } parsed.push((stored.id, MessageRole::User, content)); @@ -215,9 +215,9 @@ pub(crate) async fn handle_resume( for block in &content { match block { ContentBlock::Text { text } => match role { - MessageRole::User => entries.push(TranscriptEntry::User(text.clone())), + MessageRole::User => entries.push(TranscriptEntry::User { text: text.clone() }), MessageRole::Assistant => { - entries.push(TranscriptEntry::Assistant(text.clone())); + entries.push(TranscriptEntry::Assistant { text: text.clone() }); } MessageRole::System => {} }, diff --git a/crates/goat-agent/src/turn.rs b/crates/goat-agent/src/turn.rs index 4ae682a..87038de 100644 --- a/crates/goat-agent/src/turn.rs +++ b/crates/goat-agent/src/turn.rs @@ -159,7 +159,7 @@ pub(crate) async fn handle_idle_op( Op::RenameThread { title } => { crate::threads::handle_rename(store, thread_id, title, events).await; } - Op::ListThreads => { + Op::ListThreads {} => { crate::threads::handle_list_threads(store, cwd, events).await; } _ => {} @@ -318,7 +318,7 @@ pub(crate) async fn handle_shell( Some(Op::Interrupt { id: target_id }) if target_id == id => { break ShellEnd::Interrupted; } - Some(Op::Shutdown) | None => break ShellEnd::Shutdown, + Some(Op::Shutdown {}) | None => break ShellEnd::Shutdown, Some(op) => deferred.push(op), }, } @@ -489,7 +489,7 @@ pub(crate) async fn handle_compact( } } Some(Op::Interrupt { id: target_id }) if target_id == id => token.cancel(), - Some(Op::Shutdown) | None => { + Some(Op::Shutdown {}) | None => { shutdown = true; token.cancel(); } @@ -700,7 +700,7 @@ async fn run_one_turn( } } Some(Op::Interrupt { id: target_id }) if target_id == id => token.cancel(), - Some(Op::Shutdown) | None => { + Some(Op::Shutdown {}) | None => { shutdown = true; token.cancel(); } diff --git a/crates/goat-client/src/lib.rs b/crates/goat-client/src/lib.rs index 80f369e..aff6614 100644 --- a/crates/goat-client/src/lib.rs +++ b/crates/goat-client/src/lib.rs @@ -107,7 +107,7 @@ fn spawn_pumps(conn: ClientConn, session: SessionId, client_id: u64) -> maybe_op = ops_rx.recv() => { let Some(op) = maybe_op else { break }; let frame = match op { - Op::Shutdown => ClientFrame::Goodbye, + Op::Shutdown {} => ClientFrame::Goodbye {}, Op::Interrupt { .. } | Op::Answer { .. } | Op::DequeueMessage { .. } @@ -217,7 +217,7 @@ pub async fn status(socket_path: &Path) -> Result, C }) .await?; expect_welcome(&mut conn).await?; - conn.send(&ClientFrame::ListSessions).await?; + conn.send(&ClientFrame::ListSessions {}).await?; match conn.recv().await? { ServerFrame::Sessions { sessions } => Ok(sessions), _ => Err(ClientError::Handshake), @@ -232,7 +232,7 @@ pub async fn stop(socket_path: &Path) -> Result<(), ClientError> { }) .await?; expect_welcome(&mut conn).await?; - conn.send(&ClientFrame::StopDaemon).await?; + conn.send(&ClientFrame::StopDaemon {}).await?; Ok(()) } @@ -289,7 +289,7 @@ pub async fn list_devices(socket_path: &Path) -> Result Ok(devices), ServerFrame::Error { message } => Err(ClientError::OpenFailed(message)), diff --git a/crates/goat-code/src/main.rs b/crates/goat-code/src/main.rs index b203db7..00fb3df 100644 --- a/crates/goat-code/src/main.rs +++ b/crates/goat-code/src/main.rs @@ -89,9 +89,9 @@ async fn run_tui(worktree_label: Option, r#continue: bool) -> color_eyre .ok_or_else(|| color_eyre::eyre::eyre!(goat_config::HOME_NOT_FOUND))?; let daemon_exe = std::env::current_exe()?; let resume = if r#continue { - goat_wire::ResumeMode::Latest + goat_wire::ResumeMode::Latest {} } else { - goat_wire::ResumeMode::New + goat_wire::ResumeMode::New {} }; let attachment = goat_client::connect(&socket_path, &daemon_exe, cwd, resume).await?; @@ -135,7 +135,7 @@ async fn run_daemon_command(command: DaemonCommand) -> color_eyre::Result<()> { } else { for s in sessions { let flag = match s.state { - goat_wire::SessionLiveState::WaitingOnAsk => " (waiting on ask)", + goat_wire::SessionLiveState::WaitingOnAsk {} => " (waiting on ask)", _ => "", }; println!( diff --git a/crates/goat-code/src/update.rs b/crates/goat-code/src/update.rs index e80acf4..e18db6a 100644 --- a/crates/goat-code/src/update.rs +++ b/crates/goat-code/src/update.rs @@ -85,7 +85,8 @@ async fn drain_daemon(force: bool) -> color_eyre::Result<()> { .filter(|s| { matches!( s.state, - goat_wire::SessionLiveState::Active | goat_wire::SessionLiveState::WaitingOnAsk + goat_wire::SessionLiveState::Active {} + | goat_wire::SessionLiveState::WaitingOnAsk {} ) }) .count(); diff --git a/crates/goat-commands/src/lib.rs b/crates/goat-commands/src/lib.rs index efe58f2..b1576fa 100644 --- a/crates/goat-commands/src/lib.rs +++ b/crates/goat-commands/src/lib.rs @@ -141,10 +141,10 @@ fn default_skill_shape() -> CommandShape { fn skill_shape(command: &SkillCommandShape) -> CommandShape { match command { - SkillCommandShape::Arguments(arguments) => { + SkillCommandShape::Arguments { items: arguments } => { CommandShape::Parameters(arguments.iter().map(skill_parameter).collect()) } - SkillCommandShape::Subcommands(subcommands) => { + SkillCommandShape::Subcommands { items: subcommands } => { CommandShape::Branches(subcommands.iter().map(skill_branch).collect()) } } @@ -169,9 +169,9 @@ fn skill_parameter(parameter: &SkillParameterInfo) -> ParameterSpec { fn skill_value(value: &SkillParameterValue) -> ParameterValue { match value { - SkillParameterValue::Word => ParameterValue::Word, - SkillParameterValue::Integer => ParameterValue::Integer, - SkillParameterValue::Choice(choices) => ParameterValue::Choice( + SkillParameterValue::Word {} => ParameterValue::Word, + SkillParameterValue::Integer {} => ParameterValue::Integer, + SkillParameterValue::Choice { options: choices } => ParameterValue::Choice( choices .iter() .map(|choice| ChoiceSpec { @@ -180,7 +180,7 @@ fn skill_value(value: &SkillParameterValue) -> ParameterValue { }) .collect(), ), - SkillParameterValue::TextTail => ParameterValue::TextTail, + SkillParameterValue::TextTail {} => ParameterValue::TextTail, } } @@ -361,12 +361,14 @@ mod tests { registry.set_skills(&[SkillInfo { name: "review".to_owned(), description: "review".to_owned(), - command: Some(SkillCommandShape::Arguments(vec![SkillParameterInfo { - name: "target".to_owned(), - description: "target".to_owned(), - required: true, - value: SkillParameterValue::Word, - }])), + command: Some(SkillCommandShape::Arguments { + items: vec![SkillParameterInfo { + name: "target".to_owned(), + description: "target".to_owned(), + required: true, + value: SkillParameterValue::Word {}, + }], + }), }]); let CommandEffect::Submit(text) = registry.resolve_line("/review src/lib.rs") else { panic!("expected submit"); @@ -381,16 +383,18 @@ mod tests { registry.set_skills(&[SkillInfo { name: "review".to_owned(), description: "review".to_owned(), - command: Some(SkillCommandShape::Subcommands(vec![SkillBranchInfo { - name: "security".to_owned(), - description: "security".to_owned(), - arguments: vec![SkillParameterInfo { - name: "focus".to_owned(), - description: "focus".to_owned(), - required: false, - value: SkillParameterValue::TextTail, + command: Some(SkillCommandShape::Subcommands { + items: vec![SkillBranchInfo { + name: "security".to_owned(), + description: "security".to_owned(), + arguments: vec![SkillParameterInfo { + name: "focus".to_owned(), + description: "focus".to_owned(), + required: false, + value: SkillParameterValue::TextTail {}, + }], }], - }])), + }), }]); let CommandEffect::Submit(text) = registry.resolve_line("/review security auth flow") else { @@ -406,11 +410,13 @@ mod tests { registry.set_skills(&[SkillInfo { name: "review".to_owned(), description: "review".to_owned(), - command: Some(SkillCommandShape::Subcommands(vec![SkillBranchInfo { - name: "security".to_owned(), - description: "security".to_owned(), - arguments: Vec::new(), - }])), + command: Some(SkillCommandShape::Subcommands { + items: vec![SkillBranchInfo { + name: "security".to_owned(), + description: "security".to_owned(), + arguments: Vec::new(), + }], + }), }]); assert!(matches!( registry.resolve_line("/review nope"), diff --git a/crates/goat-core/src/engine.rs b/crates/goat-core/src/engine.rs index 15ac2ae..71eccfe 100644 --- a/crates/goat-core/src/engine.rs +++ b/crates/goat-core/src/engine.rs @@ -35,7 +35,7 @@ impl Session { } pub async fn shutdown(self) { - let _ = self.ops.send(Op::Shutdown).await; + let _ = self.ops.send(Op::Shutdown {}).await; let _ = self.handle.await; } } diff --git a/crates/goat-core/src/lib.rs b/crates/goat-core/src/lib.rs index 9a22311..95d5805 100644 --- a/crates/goat-core/src/lib.rs +++ b/crates/goat-core/src/lib.rs @@ -31,7 +31,7 @@ mod tests { }) .await; } - Op::Shutdown => break, + Op::Shutdown {} => break, _ => {} } } diff --git a/crates/goat-daemon/src/conn.rs b/crates/goat-daemon/src/conn.rs index 30255d1..c50165a 100644 --- a/crates/goat-daemon/src/conn.rs +++ b/crates/goat-daemon/src/conn.rs @@ -173,7 +173,7 @@ async fn dispatch( } Disposition::Continue } - ClientFrame::ListSessions => { + ClientFrame::ListSessions {} => { let sessions = manager.list_sessions().await; let _ = out_tx.send(ServerFrame::Sessions { sessions }).await; Disposition::Continue @@ -220,7 +220,7 @@ async fn dispatch( } Disposition::Continue } - ClientFrame::ListDevices => { + ClientFrame::ListDevices {} => { match manager.list_devices().await { Ok(devices) => { let _ = out_tx.send(ServerFrame::Devices { devices }).await; @@ -242,7 +242,7 @@ async fn dispatch( } Disposition::Continue } - ClientFrame::StopDaemon => { + ClientFrame::StopDaemon {} => { if origin.is_local() { shutdown.cancel(); Disposition::Closed @@ -255,7 +255,7 @@ async fn dispatch( Disposition::Continue } } - ClientFrame::Goodbye => Disposition::Closed, + ClientFrame::Goodbye {} => Disposition::Closed, } } diff --git a/crates/goat-daemon/src/manager.rs b/crates/goat-daemon/src/manager.rs index 93f41c6..47e7a64 100644 --- a/crates/goat-daemon/src/manager.rs +++ b/crates/goat-daemon/src/manager.rs @@ -141,7 +141,7 @@ impl Manager { resume: ResumeMode, ) -> Result { let normalized = Self::normalize_cwd(&cwd); - if matches!(resume, ResumeMode::Latest) { + if matches!(resume, ResumeMode::Latest {}) { let table = self.inner.sessions.lock().await; if let Some(id) = Self::find_live_by_cwd_locked(&table, &normalized) { return Ok(id); @@ -174,7 +174,7 @@ impl Manager { next_seq: 0, next_task: 1, subscribers: Vec::new(), - state: goat_wire::SessionLiveState::Idle, + state: goat_wire::SessionLiveState::Idle {}, snapshot: None, tokens: 0, open_asks: 0, @@ -184,10 +184,10 @@ impl Manager { let id = { let mut table = self.inner.sessions.lock().await; - if matches!(resume, ResumeMode::Latest) + if matches!(resume, ResumeMode::Latest {}) && let Some(existing) = Self::find_live_by_cwd_locked(&table, &normalized) { - let _ = ops.send(Op::Shutdown).await; + let _ = ops.send(Op::Shutdown {}).await; return Ok(existing); } table.insert( @@ -203,9 +203,9 @@ impl Manager { spawn_pump(self.clone(), id, inner, events, handle, store_for_pump); match resume { - ResumeMode::New => {} - ResumeMode::Latest => { - let _ = ops.send(Op::ResumeLatest).await; + ResumeMode::New {} => {} + ResumeMode::Latest {} => { + let _ = ops.send(Op::ResumeLatest {}).await; } ResumeMode::Thread { thread_id } => { let _ = ops.send(Op::Resume { thread_id }).await; @@ -294,7 +294,7 @@ impl Manager { table.remove(&session); ops }; - let _ = ops.send(Op::Shutdown).await; + let _ = ops.send(Op::Shutdown {}).await; tracing::info!(session = session.0, "evicted idle session with no windows"); } @@ -365,11 +365,11 @@ impl Manager { let name = entry.file_name().to_string_lossy().into_owned(); let file_type = entry.file_type().map_err(|e| format!("file_type: {e}"))?; let kind = if file_type.is_symlink() { - goat_wire::DirEntryKind::Symlink + goat_wire::DirEntryKind::Symlink {} } else if file_type.is_dir() { - goat_wire::DirEntryKind::Directory + goat_wire::DirEntryKind::Directory {} } else { - goat_wire::DirEntryKind::File + goat_wire::DirEntryKind::File {} }; children.push(goat_wire::DirEntry { name, kind }); } @@ -408,7 +408,7 @@ impl Manager { let inner = live.inner.lock().await; inner.ops.clone() }; - let _ = ops.send(Op::Shutdown).await; + let _ = ops.send(Op::Shutdown {}).await; Ok(()) } diff --git a/crates/goat-daemon/src/session.rs b/crates/goat-daemon/src/session.rs index 6f91f57..fa214b2 100644 --- a/crates/goat-daemon/src/session.rs +++ b/crates/goat-daemon/src/session.rs @@ -140,7 +140,7 @@ impl SessionInner { pub(crate) fn evictable(&self) -> bool { self.subscribers.is_empty() && self.open_asks == 0 - && matches!(self.state, SessionLiveState::Idle) + && matches!(self.state, SessionLiveState::Idle {}) } } @@ -149,10 +149,10 @@ const MAX_RETAINED_EVENTS: usize = 4096; fn update_state_from_event(state: &mut SessionLiveState, event: &Event) { match event { Event::TaskStarted { .. } | Event::AskDismissed { .. } => { - *state = SessionLiveState::Active; + *state = SessionLiveState::Active {}; } - Event::AskStarted { .. } => *state = SessionLiveState::WaitingOnAsk, - Event::TaskDone { .. } => *state = SessionLiveState::Idle, + Event::AskStarted { .. } => *state = SessionLiveState::WaitingOnAsk {}, + Event::TaskDone { .. } => *state = SessionLiveState::Idle {}, _ => {} } } @@ -228,7 +228,7 @@ mod tests { next_seq: 0, next_task: 1, subscribers: Vec::new(), - state: SessionLiveState::Idle, + state: SessionLiveState::Idle {}, snapshot: None, tokens: 0, open_asks: 0, diff --git a/crates/goat-daemon/tests/remote_e2e.rs b/crates/goat-daemon/tests/remote_e2e.rs index d9e4bea..b82d75e 100644 --- a/crates/goat-daemon/tests/remote_e2e.rs +++ b/crates/goat-daemon/tests/remote_e2e.rs @@ -240,7 +240,7 @@ async fn remote_pair_and_open_session_over_mtls() { &mut ws, &ClientFrame::OpenSession { cwd: dir.path().display().to_string(), - resume: ResumeMode::New, + resume: ResumeMode::New {}, }, ) .await; @@ -262,7 +262,7 @@ async fn revoked_device_cannot_reconnect() { let device_id = { let mut conn = local_conn(&socket).await; - conn.send(&ClientFrame::ListDevices).await.unwrap(); + conn.send(&ClientFrame::ListDevices {}).await.unwrap(); match conn.recv().await.unwrap() { ServerFrame::Devices { devices } => devices[0].id.clone(), other => panic!("expected Devices, got {other:?}"), diff --git a/crates/goat-daemon/tests/roundtrip.rs b/crates/goat-daemon/tests/roundtrip.rs index 01c1014..800b16e 100644 --- a/crates/goat-daemon/tests/roundtrip.rs +++ b/crates/goat-daemon/tests/roundtrip.rs @@ -49,7 +49,7 @@ async fn open_session_and_list() { conn.send(&ClientFrame::OpenSession { cwd: dir.path().display().to_string(), - resume: ResumeMode::New, + resume: ResumeMode::New {}, }) .await .unwrap(); @@ -59,7 +59,7 @@ async fn open_session_and_list() { }; let mut lister = connect(&socket).await; - lister.send(&ClientFrame::ListSessions).await.unwrap(); + lister.send(&ClientFrame::ListSessions {}).await.unwrap(); match lister.recv().await.unwrap() { ServerFrame::Sessions { sessions } => { assert!(sessions.iter().any(|s| s.session == session)); @@ -76,7 +76,7 @@ async fn submit_message_flows_back_as_events() { conn.send(&ClientFrame::OpenSession { cwd: dir.path().display().to_string(), - resume: ResumeMode::New, + resume: ResumeMode::New {}, }) .await .unwrap(); @@ -125,7 +125,7 @@ async fn reattach_by_cwd_returns_same_session() { let mut a = connect(&socket).await; a.send(&ClientFrame::OpenSession { cwd: dir.path().display().to_string(), - resume: ResumeMode::New, + resume: ResumeMode::New {}, }) .await .unwrap(); @@ -137,7 +137,7 @@ async fn reattach_by_cwd_returns_same_session() { let mut b = connect(&socket).await; b.send(&ClientFrame::OpenSession { cwd: dir.path().display().to_string(), - resume: ResumeMode::Latest, + resume: ResumeMode::Latest {}, }) .await .unwrap(); @@ -159,7 +159,7 @@ async fn kill_session_removes_it_from_the_list() { let mut conn = connect(&socket).await; conn.send(&ClientFrame::OpenSession { cwd: dir.path().display().to_string(), - resume: ResumeMode::New, + resume: ResumeMode::New {}, }) .await .unwrap(); @@ -175,7 +175,7 @@ async fn kill_session_removes_it_from_the_list() { .unwrap(); tokio::time::sleep(Duration::from_millis(200)).await; - admin.send(&ClientFrame::ListSessions).await.unwrap(); + admin.send(&ClientFrame::ListSessions {}).await.unwrap(); match admin.recv().await.unwrap() { ServerFrame::Sessions { sessions } => { assert!(!sessions.iter().any(|s| s.session == session)); diff --git a/crates/goat-protocol/src/event.rs b/crates/goat-protocol/src/event.rs index ede7c97..2703668 100644 --- a/crates/goat-protocol/src/event.rs +++ b/crates/goat-protocol/src/event.rs @@ -14,6 +14,7 @@ pub enum NotifyKind { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] pub enum Event { TaskStarted { id: TaskId, diff --git a/crates/goat-protocol/src/lib.rs b/crates/goat-protocol/src/lib.rs index 6b3bee5..b50ee0b 100644 --- a/crates/goat-protocol/src/lib.rs +++ b/crates/goat-protocol/src/lib.rs @@ -8,7 +8,10 @@ pub use types::*; #[cfg(test)] mod tests { - use super::{ToolImageData, ToolOutcome}; + use super::{ + Event, LoginCredential, Op, PlanDecision, TaskId, ToolCallId, ToolImageData, ToolOutcome, + TranscriptEntry, + }; #[test] fn tool_outcome_image_round_trips() { @@ -37,4 +40,112 @@ mod tests { let back: ToolOutcome = serde_json::from_str(&json).unwrap(); assert_eq!(outcome, back); } + + #[test] + fn op_unit_variants_serialize_as_type_object() { + assert_eq!( + serde_json::to_string(&Op::Clear {}).unwrap(), + r#"{"type":"Clear"}"# + ); + assert_eq!( + serde_json::to_string(&Op::ListThreads {}).unwrap(), + r#"{"type":"ListThreads"}"# + ); + assert_eq!( + serde_json::to_string(&Op::ResumeLatest {}).unwrap(), + r#"{"type":"ResumeLatest"}"# + ); + assert_eq!( + serde_json::to_string(&Op::Shutdown {}).unwrap(), + r#"{"type":"Shutdown"}"# + ); + } + + #[test] + fn op_struct_variants_serialize_flat_with_type() { + let op = Op::SubmitMessage { + id: TaskId(1), + text: "hi".to_owned(), + }; + let json = serde_json::to_string(&op).unwrap(); + assert_eq!(json, r#"{"type":"SubmitMessage","id":"1","text":"hi"}"#); + let back: Op = serde_json::from_str(&json).unwrap(); + assert_eq!(back, op); + } + + #[test] + fn event_serializes_flat_with_type() { + let ev = Event::TextDelta { + id: TaskId(1), + chunk: "x".to_owned(), + }; + let json = serde_json::to_string(&ev).unwrap(); + assert_eq!(json, r#"{"type":"TextDelta","id":"1","chunk":"x"}"#); + let back: Event = serde_json::from_str(&json).unwrap(); + assert_eq!(back, ev); + } + + #[test] + fn transcript_entry_user_serializes_with_type() { + let entry = TranscriptEntry::User { + text: "hello".to_owned(), + }; + let json = serde_json::to_string(&entry).unwrap(); + assert_eq!(json, r#"{"type":"User","text":"hello"}"#); + let back: TranscriptEntry = serde_json::from_str(&json).unwrap(); + assert_eq!(back, entry); + } + + #[test] + fn plan_decision_approve_serializes_with_type() { + let json = serde_json::to_string(&PlanDecision::Approve {}).unwrap(); + assert_eq!(json, r#"{"type":"Approve"}"#); + let back: PlanDecision = serde_json::from_str(&json).unwrap(); + assert_eq!(back, PlanDecision::Approve {}); + } + + #[test] + fn login_credential_api_key_serializes_with_type() { + let cred = LoginCredential::ApiKey { + key: "sk-x".to_owned(), + }; + let json = serde_json::to_string(&cred).unwrap(); + assert_eq!(json, r#"{"type":"ApiKey","key":"sk-x"}"#); + let back: LoginCredential = serde_json::from_str(&json).unwrap(); + assert_eq!(back, cred); + } + + #[test] + fn op_answer_roundtrips() { + let op = Op::Answer { + id: TaskId(2), + call: ToolCallId(5), + answers: vec!["yes".to_owned()], + }; + let json = serde_json::to_string(&op).unwrap(); + let back: Op = serde_json::from_str(&json).unwrap(); + assert_eq!(back, op); + } + + #[test] + fn task_id_serializes_as_string() { + assert_eq!(serde_json::to_string(&TaskId(42)).unwrap(), r#""42""#); + } + + #[test] + fn task_id_deserializes_from_string_and_number() { + let from_str: TaskId = serde_json::from_str(r#""42""#).unwrap(); + let from_num: TaskId = serde_json::from_str("42").unwrap(); + assert_eq!(from_str, TaskId(42)); + assert_eq!(from_num, TaskId(42)); + } + + #[test] + fn task_id_above_js_safe_integer_roundtrips() { + let big = TaskId(9_007_199_254_740_993); + let json = serde_json::to_string(&big).unwrap(); + assert_eq!(json, r#""9007199254740993""#); + let back: TaskId = serde_json::from_str(&json).unwrap(); + assert_eq!(back, big); + } } diff --git a/crates/goat-protocol/src/op.rs b/crates/goat-protocol/src/op.rs index 3cdb433..42299f2 100644 --- a/crates/goat-protocol/src/op.rs +++ b/crates/goat-protocol/src/op.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::{LoginCredential, Mode, ModelTarget, PlanDecision, TaskId, ToolCallId}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type")] pub enum Op { SubmitMessage { id: TaskId, @@ -15,7 +16,7 @@ pub enum Op { Interrupt { id: TaskId, }, - Clear, + Clear {}, SelectModel { target: ModelTarget, }, @@ -32,11 +33,11 @@ pub enum Op { provider: String, name: String, }, - ListThreads, + ListThreads {}, Resume { thread_id: i64, }, - ResumeLatest, + ResumeLatest {}, RenameThread { title: String, }, @@ -60,5 +61,5 @@ pub enum Op { call: ToolCallId, decision: PlanDecision, }, - Shutdown, + Shutdown {}, } diff --git a/crates/goat-protocol/src/types.rs b/crates/goat-protocol/src/types.rs index 8c6d735..fa64e0b 100644 --- a/crates/goat-protocol/src/types.rs +++ b/crates/goat-protocol/src/types.rs @@ -1,6 +1,35 @@ use std::fmt; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +pub mod id_serde { + use super::{Deserializer, Serializer}; + use serde::de::Visitor; + + pub fn serialize(v: &u64, s: S) -> Result { + s.serialize_str(&v.to_string()) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + struct V; + impl Visitor<'_> for V { + type Value = u64; + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("u64 as string or integer") + } + fn visit_str(self, v: &str) -> Result { + v.parse().map_err(E::custom) + } + fn visit_u64(self, v: u64) -> Result { + Ok(v) + } + fn visit_i64(self, v: i64) -> Result { + u64::try_from(v).map_err(E::custom) + } + } + d.deserialize_any(V) + } +} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct Usage { @@ -24,18 +53,42 @@ pub struct RateLimitSnapshot { pub representative: Option, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct TaskId(pub u64); +impl Serialize for TaskId { + fn serialize(&self, s: S) -> Result { + id_serde::serialize(&self.0, s) + } +} + +impl<'de> Deserialize<'de> for TaskId { + fn deserialize>(d: D) -> Result { + id_serde::deserialize(d).map(Self) + } +} + impl fmt::Display for TaskId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ToolCallId(pub u64); +impl Serialize for ToolCallId { + fn serialize(&self, s: S) -> Result { + id_serde::serialize(&self.0, s) + } +} + +impl<'de> Deserialize<'de> for ToolCallId { + fn deserialize>(d: D) -> Result { + id_serde::deserialize(d).map(Self) + } +} + impl fmt::Display for ToolCallId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) @@ -145,8 +198,9 @@ impl Mode { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type")] pub enum PlanDecision { - Approve, + Approve {}, Reject { feedback: String }, } @@ -185,9 +239,14 @@ pub struct ThreadSummary { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type")] pub enum TranscriptEntry { - User(String), - Assistant(String), + User { + text: String, + }, + Assistant { + text: String, + }, Tool { call: ToolCall, outcome: ToolOutcome, @@ -233,9 +292,10 @@ pub struct AccountEntry { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type")] pub enum LoginCredential { - ApiKey(String), - OAuth, + ApiKey { key: String }, + OAuth {}, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -247,10 +307,10 @@ pub struct SkillInfo { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] +#[serde(tag = "type", rename_all = "snake_case")] pub enum SkillCommandShape { - Arguments(Vec), - Subcommands(Vec), + Arguments { items: Vec }, + Subcommands { items: Vec }, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -271,12 +331,12 @@ pub struct SkillParameterInfo { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] +#[serde(tag = "type", rename_all = "snake_case")] pub enum SkillParameterValue { - Word, - Integer, - Choice(Vec), - TextTail, + Word {}, + Integer {}, + Choice { options: Vec }, + TextTail {}, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/goat-skill/src/lib.rs b/crates/goat-skill/src/lib.rs index 222c544..264e981 100644 --- a/crates/goat-skill/src/lib.rs +++ b/crates/goat-skill/src/lib.rs @@ -193,12 +193,12 @@ fn command_shape( ) -> Result, String> { match (arguments, subcommands) { (Some(_), Some(_)) => Err("arguments and subcommands cannot both be present".to_owned()), - (Some(arguments), None) => Ok(Some(SkillCommandShape::Arguments( - arguments.into_iter().map(parameter_info).collect(), - ))), - (None, Some(subcommands)) => Ok(Some(SkillCommandShape::Subcommands( - subcommands.into_iter().map(branch_info).collect(), - ))), + (Some(arguments), None) => Ok(Some(SkillCommandShape::Arguments { + items: arguments.into_iter().map(parameter_info).collect(), + })), + (None, Some(subcommands)) => Ok(Some(SkillCommandShape::Subcommands { + items: subcommands.into_iter().map(branch_info).collect(), + })), (None, None) => Ok(None), } } @@ -222,25 +222,25 @@ fn parameter_info(parameter: ManifestParameter) -> SkillParameterInfo { fn value_info(value: ManifestValue) -> SkillParameterValue { match value { - ManifestValue::Kind(ManifestValueKind::Word) => SkillParameterValue::Word, - ManifestValue::Kind(ManifestValueKind::Integer) => SkillParameterValue::Integer, - ManifestValue::Kind(ManifestValueKind::TextTail) => SkillParameterValue::TextTail, - ManifestValue::Choice { choice } => SkillParameterValue::Choice( - choice + ManifestValue::Kind(ManifestValueKind::Word) => SkillParameterValue::Word {}, + ManifestValue::Kind(ManifestValueKind::Integer) => SkillParameterValue::Integer {}, + ManifestValue::Kind(ManifestValueKind::TextTail) => SkillParameterValue::TextTail {}, + ManifestValue::Choice { choice } => SkillParameterValue::Choice { + options: choice .into_iter() .map(|choice| SkillChoiceInfo { value: choice.value, description: choice.description, }) .collect(), - ), + }, } } fn validate_command(command: &SkillCommandShape) -> Result<(), String> { match command { - SkillCommandShape::Arguments(arguments) => validate_parameters(arguments), - SkillCommandShape::Subcommands(subcommands) => { + SkillCommandShape::Arguments { items: arguments } => validate_parameters(arguments), + SkillCommandShape::Subcommands { items: subcommands } => { if subcommands.is_empty() { return Err("subcommands cannot be empty".to_owned()); } @@ -283,7 +283,7 @@ fn validate_parameters(arguments: &[SkillParameterInfo]) -> Result<(), String> { } else if optional_seen { return Err("required argument cannot follow optional argument".to_owned()); } - if matches!(argument.value, SkillParameterValue::TextTail) { + if matches!(argument.value, SkillParameterValue::TextTail {}) { if text_tail_seen { return Err("text_tail cannot appear more than once".to_owned()); } @@ -292,7 +292,7 @@ fn validate_parameters(arguments: &[SkillParameterInfo]) -> Result<(), String> { return Err("text_tail must be last".to_owned()); } } - if let SkillParameterValue::Choice(choices) = &argument.value { + if let SkillParameterValue::Choice { options: choices } = &argument.value { if choices.is_empty() { return Err(format!("choice argument {} cannot be empty", argument.name)); } @@ -370,11 +370,11 @@ mod tests { "review", ) .unwrap(); - let Some(SkillCommandShape::Arguments(arguments)) = parsed.command else { + let Some(SkillCommandShape::Arguments { items: arguments }) = parsed.command else { panic!("expected arguments"); }; assert_eq!(arguments[0].name, "instructions"); - assert_eq!(arguments[0].value, SkillParameterValue::TextTail); + assert_eq!(arguments[0].value, SkillParameterValue::TextTail {}); } #[test] @@ -384,7 +384,7 @@ mod tests { "review", ) .unwrap(); - let Some(SkillCommandShape::Subcommands(subcommands)) = parsed.command else { + let Some(SkillCommandShape::Subcommands { items: subcommands }) = parsed.command else { panic!("expected subcommands"); }; assert_eq!(subcommands[0].name, "security"); diff --git a/crates/goat-tui/src/app/engine.rs b/crates/goat-tui/src/app/engine.rs index bee3bb3..8d4cd8b 100644 --- a/crates/goat-tui/src/app/engine.rs +++ b/crates/goat-tui/src/app/engine.rs @@ -62,8 +62,8 @@ impl App { self.follow = true; for entry in entries { match entry { - TranscriptEntry::User(text) => self.transcript.push_user(text), - TranscriptEntry::Assistant(text) => { + TranscriptEntry::User { text } => self.transcript.push_user(text), + TranscriptEntry::Assistant { text } => { self.transcript.commit_text(&text); } TranscriptEntry::Tool { call, outcome } => { diff --git a/crates/goat-tui/src/app/keys.rs b/crates/goat-tui/src/app/keys.rs index 50a0742..62d2b46 100644 --- a/crates/goat-tui/src/app/keys.rs +++ b/crates/goat-tui/src/app/keys.rs @@ -682,7 +682,7 @@ impl App { return vec![Op::ResolvePlan { id, call, - decision: PlanDecision::Approve, + decision: PlanDecision::Approve {}, }]; } Vec::new() diff --git a/crates/goat-tui/src/app/mod.rs b/crates/goat-tui/src/app/mod.rs index 9cd7e2e..ab1fdee 100644 --- a/crates/goat-tui/src/app/mod.rs +++ b/crates/goat-tui/src/app/mod.rs @@ -367,7 +367,7 @@ impl App { return Vec::new(); } self.pending.resume = Some(ResumeIntent::Picker); - vec![Op::ListThreads] + vec![Op::ListThreads {}] } CommandEffect::ResumeIndex(index) => { if self.turn.active.is_some() { @@ -378,7 +378,7 @@ impl App { return Vec::new(); } self.pending.resume = Some(ResumeIntent::Index(index)); - vec![Op::ListThreads] + vec![Op::ListThreads {}] } CommandEffect::OpenConfig => { self.overlay = Overlay::Config(Config::new( @@ -408,7 +408,7 @@ impl App { self.clear_ctx_indicator(); self.scroll = 0; self.follow = true; - vec![Op::Clear] + vec![Op::Clear {}] } CommandEffect::CompactConversation(instructions) => { let id = TaskId(self.next_task); @@ -1049,7 +1049,7 @@ pub async fn run( ) .await; tui::restore(); - let _ = ops.send(Op::Shutdown).await; + let _ = ops.send(Op::Shutdown {}).await; result } @@ -1192,7 +1192,7 @@ mod tests { assert!(matches!( ops.as_slice(), [Op::ResolvePlan { - decision: PlanDecision::Approve, + decision: PlanDecision::Approve {}, .. }] )); @@ -1306,7 +1306,7 @@ mod tests { assert!(matches!( ops.as_slice(), [Op::ResolvePlan { - decision: PlanDecision::Approve, + decision: PlanDecision::Approve {}, .. }] )); @@ -1655,7 +1655,7 @@ mod tests { app.follow = false; app.composer.insert_str("/clear"); let ops = app.submit(); - assert!(matches!(ops.as_slice(), [Op::Clear])); + assert!(matches!(ops.as_slice(), [Op::Clear {}])); assert!(app.transcript.items.is_empty()); assert_eq!(app.scroll, 0); assert!(app.follow); @@ -1896,7 +1896,7 @@ mod tests { use goat_protocol::ThreadSummary; let mut app = App::new(Theme::dark()); let ops = app.dispatch_slash_command("/resume"); - assert!(matches!(ops.as_slice(), [Op::ListThreads])); + assert!(matches!(ops.as_slice(), [Op::ListThreads {}])); let ops = app.on_engine(EngineEvent::ThreadsListed { threads: vec![ThreadSummary { id: 7, @@ -1914,7 +1914,7 @@ mod tests { use goat_protocol::ThreadSummary; let mut app = App::new(Theme::dark()); let ops = app.dispatch_slash_command("/resume 1"); - assert!(matches!(ops.as_slice(), [Op::ListThreads])); + assert!(matches!(ops.as_slice(), [Op::ListThreads {}])); let ops = app.on_engine(EngineEvent::ThreadsListed { threads: vec![ThreadSummary { id: 42, @@ -1943,8 +1943,12 @@ mod tests { compaction_threshold: None, mode: goat_protocol::Mode::Normal, entries: vec![ - TranscriptEntry::User("hello".to_owned()), - TranscriptEntry::Assistant("hi there".to_owned()), + TranscriptEntry::User { + text: "hello".to_owned(), + }, + TranscriptEntry::Assistant { + text: "hi there".to_owned(), + }, TranscriptEntry::Tool { call: ToolCall { id: ToolCallId(1), diff --git a/crates/goat-tui/src/config/mod.rs b/crates/goat-tui/src/config/mod.rs index bb60964..d963b3f 100644 --- a/crates/goat-tui/src/config/mod.rs +++ b/crates/goat-tui/src/config/mod.rs @@ -338,7 +338,7 @@ impl Config { ConfigOutcome::AddAccount { provider, name, - credential: LoginCredential::OAuth, + credential: LoginCredential::OAuth {}, } } else { if key.is_empty() { @@ -357,7 +357,7 @@ impl Config { ConfigOutcome::AddAccount { provider, name, - credential: LoginCredential::ApiKey(key), + credential: LoginCredential::ApiKey { key }, } } } @@ -720,7 +720,7 @@ mod tests { ref provider, ref credential, .. - } if provider == "anthropic" && matches!(credential, LoginCredential::OAuth) + } if provider == "anthropic" && matches!(credential, LoginCredential::OAuth {}) )); assert!(matches!(config.stage, super::InputStage::Waiting { .. })); } @@ -786,7 +786,7 @@ mod tests { assert!(matches!( out3, ConfigOutcome::AddAccount { ref provider, ref name, ref credential } - if provider == "anthropic" && name == "mykey" && matches!(credential, LoginCredential::ApiKey(k) if k == "sk-test") + if provider == "anthropic" && name == "mykey" && matches!(credential, LoginCredential::ApiKey { key: k } if k == "sk-test") )); } diff --git a/crates/goat-wire/src/lib.rs b/crates/goat-wire/src/lib.rs index c1027b6..a8ffd45 100644 --- a/crates/goat-wire/src/lib.rs +++ b/crates/goat-wire/src/lib.rs @@ -87,11 +87,11 @@ mod tests { children: vec![ DirEntry { name: "src".to_owned(), - kind: DirEntryKind::Directory, + kind: DirEntryKind::Directory {}, }, DirEntry { name: "main.rs".to_owned(), - kind: DirEntryKind::File, + kind: DirEntryKind::File {}, }, ], }; @@ -127,4 +127,42 @@ mod tests { server.send(&devices).await.unwrap(); assert_eq!(client.recv().await.unwrap(), devices); } + + #[test] + fn client_frame_list_sessions_serializes_as_type_object() { + let json = serde_json::to_string(&ClientFrame::ListSessions {}).unwrap(); + assert_eq!(json, r#"{"type":"ListSessions"}"#); + let back: ClientFrame = serde_json::from_str(&json).unwrap(); + assert_eq!(back, ClientFrame::ListSessions {}); + } + + #[test] + fn client_frame_hello_serializes_flat() { + let frame = ClientFrame::Hello { + version: PROTOCOL_VERSION, + }; + let json = serde_json::to_string(&frame).unwrap(); + assert!(json.contains(r#""type":"Hello""#)); + assert!(!json.contains(r#"{"Hello":"#)); + let back: ClientFrame = serde_json::from_str(&json).unwrap(); + assert_eq!(back, frame); + } + + #[test] + fn server_frame_event_nests_event_type_separately() { + use goat_protocol::Event; + let frame = ServerFrame::Event { + session: SessionId(1), + seq: 0, + event: Event::TextDelta { + id: goat_protocol::TaskId(1), + chunk: "x".to_owned(), + }, + }; + let json = serde_json::to_string(&frame).unwrap(); + assert!(json.contains(r#""type":"Event""#)); + assert!(json.contains(r#""type":"TextDelta""#)); + let back: ServerFrame = serde_json::from_str(&json).unwrap(); + assert_eq!(back, frame); + } } diff --git a/crates/goat-wire/src/protocol.rs b/crates/goat-wire/src/protocol.rs index c2863dc..46d4ee8 100644 --- a/crates/goat-wire/src/protocol.rs +++ b/crates/goat-wire/src/protocol.rs @@ -1,16 +1,41 @@ -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use goat_protocol::{Event, ModelTarget, Op, TranscriptEntry}; -pub const PROTOCOL_VERSION: u32 = 2; +pub const PROTOCOL_VERSION: u32 = 3; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct SessionId(pub u64); -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +impl Serialize for SessionId { + fn serialize(&self, s: S) -> Result { + goat_protocol::id_serde::serialize(&self.0, s) + } +} + +impl<'de> Deserialize<'de> for SessionId { + fn deserialize>(d: D) -> Result { + goat_protocol::id_serde::deserialize(d).map(Self) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ClientId(pub u64); +impl Serialize for ClientId { + fn serialize(&self, s: S) -> Result { + goat_protocol::id_serde::serialize(&self.0, s) + } +} + +impl<'de> Deserialize<'de> for ClientId { + fn deserialize>(d: D) -> Result { + goat_protocol::id_serde::deserialize(d).map(Self) + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] pub enum ClientFrame { Hello { version: u32, @@ -24,6 +49,7 @@ pub enum ClientFrame { }, Submit { session: SessionId, + #[serde(with = "goat_protocol::id_serde")] correlation: u64, op: Op, }, @@ -31,7 +57,7 @@ pub enum ClientFrame { session: SessionId, op: Op, }, - ListSessions, + ListSessions {}, ListDirectory { path: String, }, @@ -41,22 +67,24 @@ pub enum ClientFrame { PairDevice { label: String, }, - ListDevices, + ListDevices {}, RevokeDevice { device: String, }, - StopDaemon, - Goodbye, + StopDaemon {}, + Goodbye {}, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type")] pub enum ResumeMode { - New, - Latest, + New {}, + Latest {}, Thread { thread_id: i64 }, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] pub enum ServerFrame { Welcome { version: u32, @@ -88,6 +116,7 @@ pub enum ServerFrame { }, CorrelationAssigned { session: SessionId, + #[serde(with = "goat_protocol::id_serde")] correlation: u64, task: goat_protocol::TaskId, }, @@ -132,10 +161,11 @@ pub struct SessionInfo { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type")] pub enum SessionLiveState { - Idle, - Active, - WaitingOnAsk, + Idle {}, + Active {}, + WaitingOnAsk {}, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -145,8 +175,9 @@ pub struct DirEntry { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type")] pub enum DirEntryKind { - Directory, - File, - Symlink, + Directory {}, + File {}, + Symlink {}, }