1use futures::FutureExt;
7use futures::future::BoxFuture;
8use oino_ai::JsonValue;
9use oino_core::extensions::settings::SettingsLayer;
10use serde::de::DeserializeOwned;
11use serde::{Deserialize, Serialize};
12
13use crate::backend::{ClientError, SessionBackend};
14use crate::protocol::{
15 AgentInfo, AuthProviderInfo, Checkpoint, CompletionGroup, EditorEvent, ExtensionInfo,
16 OinoCommand, QueueEdit, QueueKind, RewindResult, RpcResponseData, RunningBash, SettingsScope,
17 TopicValue,
18};
19
20#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct ShortcutInfo {
24 pub id: String,
26 #[serde(default)]
28 pub default_keys: Vec<String>,
29 #[serde(default)]
31 pub description: String,
32 #[serde(default)]
34 pub extension_id: String,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub struct RewindOptions {
40 pub files: bool,
42 pub conversation: bool,
44}
45
46impl Default for RewindOptions {
47 fn default() -> Self {
48 Self {
49 files: true,
50 conversation: true,
51 }
52 }
53}
54
55#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
57pub struct AgentTree {
58 pub agents: Vec<AgentInfo>,
60 pub follow: bool,
62}
63
64fn whole<T: DeserializeOwned>(data: RpcResponseData) -> Result<T, ClientError> {
65 serde_json::from_value(data.unwrap_or(JsonValue::Null))
66 .map_err(|e| ClientError::Protocol(e.to_string()))
67}
68
69fn field<T: DeserializeOwned>(data: RpcResponseData, name: &str) -> Result<T, ClientError> {
70 let value = data
71 .and_then(|mut d| d.get_mut(name).map(JsonValue::take))
72 .unwrap_or(JsonValue::Null);
73 serde_json::from_value(value).map_err(|e| ClientError::Protocol(format!("{name}: {e}")))
74}
75
76pub trait FrontendApi {
78 fn completions(
80 &self,
81 text: &str,
82 cursor: usize,
83 ) -> BoxFuture<'_, Result<Vec<CompletionGroup>, ClientError>>;
84 fn ui_action(
86 &self,
87 panel_id: &str,
88 action_id: &str,
89 value: Option<JsonValue>,
90 ) -> BoxFuture<'_, Result<(), ClientError>>;
91 fn ui_visible(&self, entry_id: &str, visible: bool) -> BoxFuture<'_, Result<(), ClientError>>;
94 fn ui_resync(&self, rev: u64) -> BoxFuture<'_, Result<JsonValue, ClientError>>;
96 fn editor_event(&self, event: EditorEvent) -> BoxFuture<'_, Result<bool, ClientError>>;
98 fn invoke_shortcut(&self, id: &str) -> BoxFuture<'_, Result<(), ClientError>>;
100 fn shortcuts(&self) -> BoxFuture<'_, Result<Vec<ShortcutInfo>, ClientError>>;
102 fn settings_schema(&self) -> BoxFuture<'_, Result<Vec<JsonValue>, ClientError>>;
104 fn extension_settings(
106 &self,
107 namespace: &str,
108 layer: SettingsLayer,
109 ) -> BoxFuture<'_, Result<JsonValue, ClientError>>;
110 fn set_extension_setting(
112 &self,
113 namespace: &str,
114 key: &str,
115 value: Option<JsonValue>,
116 scope: SettingsScope,
117 ) -> BoxFuture<'_, Result<(), ClientError>>;
118 fn settings(&self) -> BoxFuture<'_, Result<JsonValue, ClientError>>;
120 fn set_setting(
122 &self,
123 key: &str,
124 value: Option<JsonValue>,
125 scope: SettingsScope,
126 ) -> BoxFuture<'_, Result<(), ClientError>>;
127 fn service_call(
129 &self,
130 target: Option<&str>,
131 name: &str,
132 payload: JsonValue,
133 ) -> BoxFuture<'_, Result<JsonValue, ClientError>>;
134 fn auth_providers(&self) -> BoxFuture<'_, Result<Vec<AuthProviderInfo>, ClientError>>;
136 fn login(&self, provider: &str) -> BoxFuture<'_, Result<(), ClientError>>;
138 fn logout(&self, provider: &str) -> BoxFuture<'_, Result<(), ClientError>>;
140 fn set_api_key(&self, provider: &str, key: &str) -> BoxFuture<'_, Result<(), ClientError>>;
142 fn topics(
144 &self,
145 subscribe: &[&str],
146 unsubscribe: &[&str],
147 ) -> BoxFuture<'_, Result<Vec<TopicValue>, ClientError>>;
148 fn bus_get(&self, topic: &str) -> BoxFuture<'_, Result<Option<JsonValue>, ClientError>>;
150 fn agents(&self, follow: Option<bool>) -> BoxFuture<'_, Result<AgentTree, ClientError>>;
152 fn agent_messages(
154 &self,
155 agent_id: &str,
156 ) -> BoxFuture<'_, Result<Vec<oino_ai::AgentMessage>, ClientError>>;
157 fn agent_abort(&self, agent_id: &str) -> BoxFuture<'_, Result<(), ClientError>>;
159 fn queue_edit(&self, edit: QueueEdit) -> BoxFuture<'_, Result<(), ClientError>>;
161 fn queue_remove(
163 &self,
164 queue: QueueKind,
165 index: usize,
166 ) -> BoxFuture<'_, Result<(), ClientError>>;
167 fn queue_send_now(
169 &self,
170 queue: QueueKind,
171 index: usize,
172 ) -> BoxFuture<'_, Result<(), ClientError>>;
173 fn queue_move(
175 &self,
176 queue: QueueKind,
177 from: usize,
178 to: usize,
179 ) -> BoxFuture<'_, Result<(), ClientError>>;
180 fn queue_set_text(
182 &self,
183 queue: QueueKind,
184 index: usize,
185 text: &str,
186 ) -> BoxFuture<'_, Result<(), ClientError>>;
187 fn checkpoints(&self) -> BoxFuture<'_, Result<Vec<Checkpoint>, ClientError>>;
189 fn rewind(
191 &self,
192 entry_id: &str,
193 options: RewindOptions,
194 ) -> BoxFuture<'_, Result<RewindResult, ClientError>>;
195 fn running_bash(&self) -> BoxFuture<'_, Result<Vec<RunningBash>, ClientError>>;
197 fn extensions(&self) -> BoxFuture<'_, Result<Vec<ExtensionInfo>, ClientError>>;
199 fn set_extension_enabled(
201 &self,
202 id: &str,
203 enabled: bool,
204 ) -> BoxFuture<'_, Result<(), ClientError>>;
205 fn render(&self, request: RenderRow) -> BoxFuture<'_, Result<Option<JsonValue>, ClientError>>;
208}
209
210#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct RenderRow {
214 pub target: String,
216 pub key: String,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub entry_id: Option<String>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub turn_entry_id: Option<String>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub turn_index: Option<u64>,
227 #[serde(default)]
229 pub running: bool,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub density: Option<String>,
233 #[serde(default)]
235 pub expanded: bool,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub width: Option<String>,
239 #[serde(default)]
241 pub data: JsonValue,
242}
243
244impl<B: SessionBackend + ?Sized> FrontendApi for B {
245 fn completions(
246 &self,
247 text: &str,
248 cursor: usize,
249 ) -> BoxFuture<'_, Result<Vec<CompletionGroup>, ClientError>> {
250 let command = OinoCommand::GetCompletions {
251 text: text.to_owned(),
252 cursor,
253 };
254 self.send(command.into())
255 .map(|r| field(r?, "groups"))
256 .boxed()
257 }
258
259 fn ui_action(
260 &self,
261 panel_id: &str,
262 action_id: &str,
263 value: Option<JsonValue>,
264 ) -> BoxFuture<'_, Result<(), ClientError>> {
265 let command = OinoCommand::UiAction {
266 panel_id: panel_id.to_owned(),
267 action_id: action_id.to_owned(),
268 value,
269 values: None,
270 path: None,
271 source: None,
272 };
273 self.send(command.into()).map(|r| r.map(drop)).boxed()
274 }
275
276 fn ui_visible(&self, entry_id: &str, visible: bool) -> BoxFuture<'_, Result<(), ClientError>> {
277 let command = OinoCommand::UiVisible {
278 entry_id: entry_id.to_owned(),
279 visible,
280 frontend: None,
281 };
282 self.send(command.into()).map(|r| r.map(drop)).boxed()
283 }
284
285 fn ui_resync(&self, rev: u64) -> BoxFuture<'_, Result<JsonValue, ClientError>> {
286 let command = OinoCommand::UiResync { scope: None, rev };
287 self.send(command.into()).map(|r| whole(r?)).boxed()
288 }
289
290 fn editor_event(&self, event: EditorEvent) -> BoxFuture<'_, Result<bool, ClientError>> {
291 self.send(OinoCommand::EditorEvent(event).into())
292 .map(|r| field(r?, "consumed"))
293 .boxed()
294 }
295
296 fn invoke_shortcut(&self, id: &str) -> BoxFuture<'_, Result<(), ClientError>> {
297 let command = OinoCommand::InvokeShortcut {
298 shortcut_id: id.to_owned(),
299 };
300 self.send(command.into()).map(|r| r.map(drop)).boxed()
301 }
302
303 fn shortcuts(&self) -> BoxFuture<'_, Result<Vec<ShortcutInfo>, ClientError>> {
304 self.send(OinoCommand::GetShortcuts.into())
305 .map(|r| field(r?, "shortcuts"))
306 .boxed()
307 }
308
309 fn settings_schema(&self) -> BoxFuture<'_, Result<Vec<JsonValue>, ClientError>> {
310 self.send(OinoCommand::GetSettingsSchema.into())
311 .map(|r| field(r?, "namespaces"))
312 .boxed()
313 }
314
315 fn extension_settings(
316 &self,
317 namespace: &str,
318 layer: SettingsLayer,
319 ) -> BoxFuture<'_, Result<JsonValue, ClientError>> {
320 let command = OinoCommand::GetExtensionSettings {
321 namespace: namespace.to_owned(),
322 scope: layer,
323 };
324 self.send(command.into())
325 .map(|r| field(r?, "values"))
326 .boxed()
327 }
328
329 fn set_extension_setting(
330 &self,
331 namespace: &str,
332 key: &str,
333 value: Option<JsonValue>,
334 scope: SettingsScope,
335 ) -> BoxFuture<'_, Result<(), ClientError>> {
336 let command = match value {
337 Some(value) => OinoCommand::SetExtensionSetting {
338 namespace: namespace.to_owned(),
339 key: key.to_owned(),
340 value: Some(value),
341 scope,
342 },
343 None => OinoCommand::UnsetExtensionSetting {
344 namespace: namespace.to_owned(),
345 key: key.to_owned(),
346 scope,
347 },
348 };
349 self.send(command.into()).map(|r| r.map(drop)).boxed()
350 }
351
352 fn settings(&self) -> BoxFuture<'_, Result<JsonValue, ClientError>> {
353 self.send(OinoCommand::GetSettings { key: None }.into())
354 .map(|r| field(r?, "settings"))
355 .boxed()
356 }
357
358 fn set_setting(
359 &self,
360 key: &str,
361 value: Option<JsonValue>,
362 scope: SettingsScope,
363 ) -> BoxFuture<'_, Result<(), ClientError>> {
364 let command = OinoCommand::SetSetting {
365 key: key.to_owned(),
366 value,
367 scope,
368 };
369 self.send(command.into()).map(|r| r.map(drop)).boxed()
370 }
371
372 fn service_call(
373 &self,
374 target: Option<&str>,
375 name: &str,
376 payload: JsonValue,
377 ) -> BoxFuture<'_, Result<JsonValue, ClientError>> {
378 let command = OinoCommand::ServiceCall {
379 target: target.map(str::to_owned),
380 name: name.to_owned(),
381 payload,
382 };
383 self.send(command.into())
384 .map(|r| r.map(Option::unwrap_or_default))
385 .boxed()
386 }
387
388 fn auth_providers(&self) -> BoxFuture<'_, Result<Vec<AuthProviderInfo>, ClientError>> {
389 self.send(OinoCommand::AuthProviders.into())
390 .map(|r| field(r?, "providers"))
391 .boxed()
392 }
393
394 fn login(&self, provider: &str) -> BoxFuture<'_, Result<(), ClientError>> {
395 let command = OinoCommand::Login {
396 provider: provider.to_owned(),
397 };
398 self.send(command.into()).map(|r| r.map(drop)).boxed()
399 }
400
401 fn logout(&self, provider: &str) -> BoxFuture<'_, Result<(), ClientError>> {
402 let command = OinoCommand::Logout {
403 provider: provider.to_owned(),
404 };
405 self.send(command.into()).map(|r| r.map(drop)).boxed()
406 }
407
408 fn set_api_key(&self, provider: &str, key: &str) -> BoxFuture<'_, Result<(), ClientError>> {
409 let command = OinoCommand::SetApiKey {
410 provider: provider.to_owned(),
411 key: key.to_owned(),
412 };
413 self.send(command.into()).map(|r| r.map(drop)).boxed()
414 }
415
416 fn topics(
417 &self,
418 subscribe: &[&str],
419 unsubscribe: &[&str],
420 ) -> BoxFuture<'_, Result<Vec<TopicValue>, ClientError>> {
421 let command = OinoCommand::Topics {
422 subscribe: subscribe.iter().map(|s| (*s).to_owned()).collect(),
423 unsubscribe: unsubscribe.iter().map(|s| (*s).to_owned()).collect(),
424 };
425 self.send(command.into())
426 .map(|r| field(r?, "topics"))
427 .boxed()
428 }
429
430 fn bus_get(&self, topic: &str) -> BoxFuture<'_, Result<Option<JsonValue>, ClientError>> {
431 let command = OinoCommand::BusGet {
432 topic: topic.to_owned(),
433 };
434 self.send(command.into())
435 .map(|r| field::<JsonValue>(r?, "data").map(|v| (!v.is_null()).then_some(v)))
436 .boxed()
437 }
438
439 fn agents(&self, follow: Option<bool>) -> BoxFuture<'_, Result<AgentTree, ClientError>> {
440 self.send(OinoCommand::Agents { follow }.into())
441 .map(|r| whole(r?))
442 .boxed()
443 }
444
445 fn agent_messages(
446 &self,
447 agent_id: &str,
448 ) -> BoxFuture<'_, Result<Vec<oino_ai::AgentMessage>, ClientError>> {
449 let command = OinoCommand::AgentMessages {
450 agent_id: agent_id.to_owned(),
451 };
452 self.send(command.into())
453 .map(|r| field(r?, "messages"))
454 .boxed()
455 }
456
457 fn agent_abort(&self, agent_id: &str) -> BoxFuture<'_, Result<(), ClientError>> {
458 let command = OinoCommand::AgentAbort {
459 agent_id: agent_id.to_owned(),
460 };
461 self.send(command.into()).map(|r| r.map(drop)).boxed()
462 }
463
464 fn queue_edit(&self, edit: QueueEdit) -> BoxFuture<'_, Result<(), ClientError>> {
465 self.send(OinoCommand::QueueEdit(edit).into())
466 .map(|r| r.map(drop))
467 .boxed()
468 }
469
470 fn queue_remove(
471 &self,
472 queue: QueueKind,
473 index: usize,
474 ) -> BoxFuture<'_, Result<(), ClientError>> {
475 self.queue_edit(QueueEdit::Remove { queue, index })
476 }
477
478 fn queue_send_now(
479 &self,
480 queue: QueueKind,
481 index: usize,
482 ) -> BoxFuture<'_, Result<(), ClientError>> {
483 self.queue_edit(QueueEdit::SendNow { queue, index })
484 }
485
486 fn queue_move(
487 &self,
488 queue: QueueKind,
489 from: usize,
490 to: usize,
491 ) -> BoxFuture<'_, Result<(), ClientError>> {
492 self.queue_edit(QueueEdit::Move { queue, from, to })
493 }
494
495 fn queue_set_text(
496 &self,
497 queue: QueueKind,
498 index: usize,
499 text: &str,
500 ) -> BoxFuture<'_, Result<(), ClientError>> {
501 self.queue_edit(QueueEdit::Edit {
502 queue,
503 index,
504 text: text.to_owned(),
505 })
506 }
507
508 fn checkpoints(&self) -> BoxFuture<'_, Result<Vec<Checkpoint>, ClientError>> {
509 self.send(OinoCommand::Checkpoints.into())
510 .map(|r| field(r?, "checkpoints"))
511 .boxed()
512 }
513
514 fn rewind(
515 &self,
516 entry_id: &str,
517 options: RewindOptions,
518 ) -> BoxFuture<'_, Result<RewindResult, ClientError>> {
519 let command = OinoCommand::Rewind {
520 entry_id: entry_id.to_owned(),
521 files: options.files,
522 conversation: options.conversation,
523 };
524 self.send(command.into()).map(|r| whole(r?)).boxed()
525 }
526
527 fn running_bash(&self) -> BoxFuture<'_, Result<Vec<RunningBash>, ClientError>> {
528 self.send(OinoCommand::RunningBash.into())
529 .map(|r| field(r?, "calls"))
530 .boxed()
531 }
532
533 fn extensions(&self) -> BoxFuture<'_, Result<Vec<ExtensionInfo>, ClientError>> {
534 self.send(OinoCommand::Extensions.into())
535 .map(|r| field(r?, "extensions"))
536 .boxed()
537 }
538
539 fn render(&self, request: RenderRow) -> BoxFuture<'_, Result<Option<JsonValue>, ClientError>> {
540 let command = OinoCommand::Render {
541 target: request.target,
542 key: request.key,
543 entry_id: request.entry_id,
544 turn_entry_id: request.turn_entry_id,
545 turn_index: request.turn_index,
546 running: request.running,
547 density: request.density,
548 expanded: request.expanded,
549 width: request.width,
550 data: request.data,
551 };
552 self.send(command.into())
553 .map(|r| field::<JsonValue>(r?, "node").map(|v| (!v.is_null()).then_some(v)))
554 .boxed()
555 }
556
557 fn set_extension_enabled(
558 &self,
559 id: &str,
560 enabled: bool,
561 ) -> BoxFuture<'_, Result<(), ClientError>> {
562 let command = OinoCommand::SetExtensionEnabled {
563 id: id.to_owned(),
564 enabled,
565 };
566 self.send(command.into()).map(|r| r.map(drop)).boxed()
567 }
568}