1use crate::action::Action;
7use rustyclaw_core::gateway::{ServerFrame, ServerPayload, StatusType};
8
9pub struct FrameAction {
11 pub action: Option<Action>,
12 pub update_ui: bool,
13}
14
15impl FrameAction {
16 pub fn none() -> Self {
17 Self {
18 action: None,
19 update_ui: false,
20 }
21 }
22 pub fn update(action: Action) -> Self {
23 Self {
24 action: Some(action),
25 update_ui: true,
26 }
27 }
28 pub fn just_action(action: Action) -> Self {
29 Self {
30 action: Some(action),
31 update_ui: false,
32 }
33 }
34}
35
36pub fn server_frame_to_action(frame: &ServerFrame) -> FrameAction {
39 use ServerPayload;
40
41 match &frame.payload {
42 ServerPayload::Hello { .. } => {
43 FrameAction::just_action(Action::Info("Gateway connected.".into()))
44 }
45 ServerPayload::Status { status, detail } => {
46 use StatusType::*;
47 match status {
48 ModelConfigured => {
49 FrameAction::just_action(Action::Info(format!("Model: {detail}")))
50 }
51 CredentialsLoaded => FrameAction::just_action(Action::Info(detail.clone())),
52 CredentialsMissing => FrameAction::just_action(Action::Warning(detail.clone())),
53 ModelConnecting => FrameAction::just_action(Action::Info(detail.clone())),
54 ModelReady => FrameAction::just_action(Action::Success(detail.clone())),
55 ModelError => FrameAction::just_action(Action::Error(detail.clone())),
56 NoModel => FrameAction::just_action(Action::Warning(detail.clone())),
57 VaultLocked => FrameAction::just_action(Action::GatewayVaultLocked),
58 }
59 }
60 ServerPayload::AuthChallenge { .. } => {
61 FrameAction::just_action(Action::GatewayAuthChallenge)
62 }
63 ServerPayload::AuthResult { ok, message, retry } => {
64 if *ok {
65 FrameAction::update(Action::GatewayAuthenticated)
66 } else if retry.unwrap_or(false) {
67 FrameAction::just_action(Action::Warning(
68 message
69 .clone()
70 .unwrap_or_else(|| "Invalid code. Try again.".into()),
71 ))
72 } else {
73 FrameAction::just_action(Action::Error(
74 message
75 .clone()
76 .unwrap_or_else(|| "Authentication failed.".into()),
77 ))
78 }
79 }
80 ServerPayload::AuthLocked { message, .. } => {
81 FrameAction::just_action(Action::Error(message.clone()))
82 }
83 ServerPayload::VaultUnlocked { ok, message } => {
84 if *ok {
85 FrameAction::update(Action::GatewayVaultUnlocked)
86 } else {
87 FrameAction::just_action(Action::Error(
88 message
89 .clone()
90 .unwrap_or_else(|| "Failed to unlock vault.".into()),
91 ))
92 }
93 }
94 ServerPayload::ReloadResult {
95 ok,
96 provider,
97 model,
98 message,
99 } => {
100 if *ok {
101 FrameAction::just_action(Action::GatewayReloaded {
102 provider: provider.clone(),
103 model: model.clone(),
104 })
105 } else {
106 FrameAction::just_action(Action::Error(format!(
107 "Reload failed: {}",
108 message.as_deref().unwrap_or("Unknown error")
109 )))
110 }
111 }
112 ServerPayload::SecretsListResult { ok: _, entries } => {
113 FrameAction::just_action(Action::SecretsListResult {
114 entries: entries.clone(),
115 })
116 }
117 ServerPayload::SecretsStoreResult { ok, message } => {
118 FrameAction::just_action(Action::SecretsStoreResult {
119 ok: *ok,
120 message: message.clone(),
121 })
122 }
123 ServerPayload::SecretsGetResult {
124 ok: _, key, value, ..
125 } => FrameAction::just_action(Action::SecretsGetResult {
126 key: key.clone(),
127 value: value.clone(),
128 }),
129 ServerPayload::SecretsPeekResult {
130 ok,
131 fields,
132 message,
133 } => FrameAction::just_action(Action::SecretsPeekResult {
134 name: String::new(),
135 ok: *ok,
136 fields: fields.clone(),
137 message: message.clone(),
138 }),
139 ServerPayload::SecretsSetPolicyResult { ok, message } => {
140 FrameAction::just_action(Action::SecretsSetPolicyResult {
141 ok: *ok,
142 message: message.clone(),
143 })
144 }
145 ServerPayload::SecretsSetDisabledResult { ok, message: _, .. } => {
146 FrameAction::just_action(Action::SecretsSetDisabledResult {
147 ok: *ok,
148 cred_name: String::new(),
149 disabled: false,
150 })
151 }
152 ServerPayload::SecretsDeleteResult { ok, .. } => {
153 FrameAction::just_action(Action::SecretsDeleteCredentialResult {
154 ok: *ok,
155 cred_name: String::new(),
156 })
157 }
158 ServerPayload::SecretsDeleteCredentialResult { ok, .. } => {
159 FrameAction::just_action(Action::SecretsDeleteCredentialResult {
160 ok: *ok,
161 cred_name: String::new(),
162 })
163 }
164 ServerPayload::SecretsHasTotpResult { has_totp } => {
165 FrameAction::just_action(Action::SecretsHasTotpResult {
166 has_totp: *has_totp,
167 })
168 }
169 ServerPayload::SecretsSetupTotpResult { ok, uri, message } => {
170 FrameAction::just_action(Action::SecretsSetupTotpResult {
171 ok: *ok,
172 uri: uri.clone(),
173 message: message.clone(),
174 })
175 }
176 ServerPayload::SecretsVerifyTotpResult { ok, .. } => {
177 FrameAction::just_action(Action::SecretsVerifyTotpResult { ok: *ok })
178 }
179 ServerPayload::SecretsRemoveTotpResult { ok, .. } => {
180 FrameAction::just_action(Action::SecretsRemoveTotpResult { ok: *ok })
181 }
182 ServerPayload::StreamStart => FrameAction::just_action(Action::GatewayStreamStart),
183 ServerPayload::ThinkingStart => FrameAction::just_action(Action::GatewayThinkingStart),
184 ServerPayload::ThinkingDelta { .. } => {
185 FrameAction::just_action(Action::GatewayThinkingDelta)
186 }
187 ServerPayload::ThinkingEnd => FrameAction::just_action(Action::GatewayThinkingEnd),
188 ServerPayload::Chunk { delta } => {
189 FrameAction::just_action(Action::GatewayChunk(delta.clone()))
190 }
191 ServerPayload::ResponseDone { .. } => FrameAction::just_action(Action::GatewayResponseDone),
192 ServerPayload::ToolCall {
193 id,
194 name,
195 arguments,
196 } => FrameAction::just_action(Action::GatewayToolCall {
197 id: id.clone(),
198 name: name.clone(),
199 arguments: arguments.clone(),
200 }),
201 ServerPayload::ToolResult {
202 id,
203 name,
204 result,
205 is_error,
206 } => FrameAction::just_action(Action::GatewayToolResult {
207 id: id.clone(),
208 name: name.clone(),
209 result: result.clone(),
210 is_error: *is_error,
211 }),
212 ServerPayload::Error { message, .. } => {
213 FrameAction::just_action(Action::Error(message.clone()))
214 }
215 ServerPayload::Info { message } => FrameAction::just_action(Action::Info(message.clone())),
216 ServerPayload::ToolApprovalRequest {
217 id,
218 name,
219 arguments,
220 } => FrameAction::just_action(Action::ToolApprovalRequest {
221 id: id.clone(),
222 name: name.clone(),
223 arguments: arguments.clone(),
224 }),
225 ServerPayload::UserPromptRequest { id, prompt } => {
226 let mut prompt = prompt.clone();
227 prompt.id = id.clone();
228 FrameAction::just_action(Action::UserPromptRequest(prompt))
229 }
230 ServerPayload::TasksUpdate { tasks: _ } => {
231 FrameAction::none()
233 }
234 ServerPayload::ThreadsUpdate {
235 threads,
236 foreground_id,
237 } => FrameAction::just_action(Action::ThreadsUpdate {
238 threads: threads
239 .iter()
240 .map(|t| crate::action::ThreadInfo {
241 id: t.id,
242 label: t.label.clone(),
243 description: t.description.clone(),
244 status: t.status.clone(),
245 kind_icon: t.kind_icon.clone(),
246 status_icon: t.status_icon.clone(),
247 is_foreground: t.is_foreground,
248 message_count: t.message_count,
249 has_summary: t.has_summary,
250 })
251 .collect(),
252 foreground_id: *foreground_id,
253 }),
254 ServerPayload::ThreadCreated {
255 thread_id: _,
256 label: _,
257 } => {
258 FrameAction::none()
260 }
261 ServerPayload::ThreadSwitched {
262 thread_id,
263 context_summary,
264 } => {
265 FrameAction::just_action(Action::ThreadSwitched {
267 thread_id: *thread_id,
268 context_summary: context_summary.clone(),
269 })
270 }
271 ServerPayload::Empty => FrameAction::none(),
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use crate::action::Action;
279
280 #[test]
281 fn test_frame_action_none() {
282 let action = FrameAction::none();
283 assert!(action.action.is_none());
284 assert!(!action.update_ui);
285 }
286
287 #[test]
288 fn test_frame_action_update() {
289 let action = FrameAction::update(Action::Update);
290 assert!(matches!(action.action, Some(Action::Update)));
291 assert!(action.update_ui);
292 }
293
294 #[test]
295 fn test_frame_action_just_action() {
296 let action = FrameAction::just_action(Action::Tick);
297 assert!(matches!(action.action, Some(Action::Tick)));
298 assert!(!action.update_ui);
299 }
300
301 mod action_conversion {
302 use super::*;
303 use crate::action::Action;
304 use rustyclaw_core::gateway::{SecretEntryDto, ServerFrameType};
305
306 #[test]
307 fn test_hello_frame_to_action() {
308 let frame = ServerFrame {
309 frame_type: ServerFrameType::Hello,
310 payload: ServerPayload::Hello {
311 agent: "test".into(),
312 settings_dir: "/tmp".into(),
313 vault_locked: false,
314 provider: None,
315 model: None,
316 },
317 };
318
319 let result = server_frame_to_action(&frame);
320 assert!(matches!(result.action, Some(Action::Info(_))));
321 }
322
323 #[test]
324 fn test_status_model_ready_to_action() {
325 let frame = ServerFrame {
326 frame_type: ServerFrameType::Status,
327 payload: ServerPayload::Status {
328 status: StatusType::ModelReady,
329 detail: "Claude 3.5 Sonnet".into(),
330 },
331 };
332
333 let result = server_frame_to_action(&frame);
334 assert!(matches!(result.action, Some(Action::Success(_))));
335 }
336
337 #[test]
338 fn test_status_vault_locked_to_action() {
339 let frame = ServerFrame {
340 frame_type: ServerFrameType::Status,
341 payload: ServerPayload::Status {
342 status: StatusType::VaultLocked,
343 detail: "Vault is locked".into(),
344 },
345 };
346
347 let result = server_frame_to_action(&frame);
348 assert!(matches!(result.action, Some(Action::GatewayVaultLocked)));
349 }
350
351 #[test]
352 fn test_chunk_frame_to_action() {
353 let frame = ServerFrame {
354 frame_type: ServerFrameType::Chunk,
355 payload: ServerPayload::Chunk {
356 delta: "Hello".into(),
357 },
358 };
359
360 let result = server_frame_to_action(&frame);
361 match result.action {
362 Some(Action::GatewayChunk(text)) => assert_eq!(text, "Hello"),
363 _ => panic!("Expected GatewayChunk action"),
364 }
365 }
366
367 #[test]
368 fn test_tool_call_frame_to_action() {
369 let frame = ServerFrame {
370 frame_type: ServerFrameType::ToolCall,
371 payload: ServerPayload::ToolCall {
372 id: "call_001".into(),
373 name: "read_file".into(),
374 arguments: r#"{"path":"/tmp/test"}"#.into(),
375 },
376 };
377
378 let result = server_frame_to_action(&frame);
379 match result.action {
380 Some(Action::GatewayToolCall {
381 id,
382 name,
383 arguments: _,
384 }) => {
385 assert_eq!(id, "call_001");
386 assert_eq!(name, "read_file");
387 }
388 _ => panic!("Expected GatewayToolCall action"),
389 }
390 }
391
392 #[test]
393 fn test_error_frame_to_action() {
394 let frame = ServerFrame {
395 frame_type: ServerFrameType::Error,
396 payload: ServerPayload::Error {
397 ok: false,
398 message: "Connection failed".into(),
399 },
400 };
401
402 let result = server_frame_to_action(&frame);
403 match result.action {
404 Some(Action::Error(msg)) => assert_eq!(msg, "Connection failed"),
405 _ => panic!("Expected Error action"),
406 }
407 }
408
409 #[test]
410 fn test_secrets_list_result_to_action() {
411 let frame = ServerFrame {
412 frame_type: ServerFrameType::SecretsListResult,
413 payload: ServerPayload::SecretsListResult {
414 ok: true,
415 entries: vec![SecretEntryDto {
416 name: "api_key".into(),
417 label: "API Key".into(),
418 kind: "ApiKey".into(),
419 policy: "always".into(),
420 disabled: false,
421 }],
422 },
423 };
424
425 let result = server_frame_to_action(&frame);
426 match result.action {
427 Some(Action::SecretsListResult { entries }) => {
428 assert_eq!(entries.len(), 1);
429 }
430 _ => panic!("Expected SecretsListResult action"),
431 }
432 }
433
434 #[test]
435 fn test_auth_challenge_to_action() {
436 let frame = ServerFrame {
437 frame_type: ServerFrameType::AuthChallenge,
438 payload: ServerPayload::AuthChallenge {
439 method: "totp".into(),
440 },
441 };
442
443 let result = server_frame_to_action(&frame);
444 assert!(matches!(result.action, Some(Action::GatewayAuthChallenge)));
445 }
446
447 #[test]
448 fn test_response_done_to_action() {
449 let frame = ServerFrame {
450 frame_type: ServerFrameType::ResponseDone,
451 payload: ServerPayload::ResponseDone { ok: true },
452 };
453
454 let result = server_frame_to_action(&frame);
455 assert!(matches!(result.action, Some(Action::GatewayResponseDone)));
456 }
457
458 #[test]
459 fn test_streaming_frames_to_actions() {
460 let start_frame = ServerFrame {
461 frame_type: ServerFrameType::StreamStart,
462 payload: ServerPayload::StreamStart,
463 };
464 assert!(matches!(
465 server_frame_to_action(&start_frame).action,
466 Some(Action::GatewayStreamStart)
467 ));
468
469 let thinking_frame = ServerFrame {
470 frame_type: ServerFrameType::ThinkingStart,
471 payload: ServerPayload::ThinkingStart,
472 };
473 assert!(matches!(
474 server_frame_to_action(&thinking_frame).action,
475 Some(Action::GatewayThinkingStart)
476 ));
477
478 let end_frame = ServerFrame {
479 frame_type: ServerFrameType::ThinkingEnd,
480 payload: ServerPayload::ThinkingEnd,
481 };
482 assert!(matches!(
483 server_frame_to_action(&end_frame).action,
484 Some(Action::GatewayThinkingEnd)
485 ));
486 }
487 }
488}