1use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7
8use futures::StreamExt;
9use futures::future::BoxFuture;
10use futures::stream::BoxStream;
11use oino_ai::JsonValue;
12use oino_core::events::{ClientEvent, OinoEvent, ReplaceReason};
13use oino_core::runtime::{AgentSessionRuntime, ForkPosition};
14use oino_core::session::{AgentSession, CycleDirection, InputSource, PromptOptions, RewindResult};
15use oino_core::ui::UiResponse;
16use oino_session::EntryId;
17use serde::Serialize;
18use serde_json::json;
19
20use crate::backend::{ClientError, SessionBackend};
21use crate::follow::Followers;
22use crate::protocol::{AgentCommand, OinoCommand, RpcCommand, RpcResponseData, RpcSessionState};
23
24#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct ServerInfo {
27 pub name: String,
29 pub version: String,
31}
32
33impl Default for ServerInfo {
34 fn default() -> Self {
35 Self {
36 name: "oino".to_owned(),
37 version: env!("CARGO_PKG_VERSION").to_owned(),
38 }
39 }
40}
41
42pub struct InProcessBackend {
47 shared: Arc<Shared>,
48 handle: Option<tokio::runtime::Handle>,
49}
50
51struct Shared {
52 runtime: Arc<AgentSessionRuntime>,
53 server: ServerInfo,
54 closed: AtomicBool,
55 followers: Followers,
56}
57
58impl std::fmt::Debug for InProcessBackend {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.debug_struct("InProcessBackend")
61 .field("server", &self.shared.server)
62 .finish_non_exhaustive()
63 }
64}
65
66fn data(value: impl Serialize) -> Result<RpcResponseData, ClientError> {
67 serde_json::to_value(value)
68 .map(Some)
69 .map_err(|e| ClientError::Protocol(e.to_string()))
70}
71
72fn failed(error: impl std::fmt::Display) -> ClientError {
73 ClientError::Command(error.to_string())
74}
75
76fn find_agent(
77 session: &AgentSession,
78 agent_id: &str,
79) -> Result<Arc<oino_core::agents::ChildAgent>, ClientError> {
80 session
81 .find_agent(agent_id)
82 .ok_or_else(|| ClientError::Command(format!("Unknown agent: {agent_id}")))
83}
84
85impl InProcessBackend {
86 #[must_use]
88 pub fn new(runtime: Arc<AgentSessionRuntime>) -> Self {
89 Self::with_server_info(runtime, ServerInfo::default())
90 }
91
92 #[must_use]
95 pub fn with_server_info(runtime: Arc<AgentSessionRuntime>, server: ServerInfo) -> Self {
96 let handle = tokio::runtime::Handle::try_current()
97 .ok()
98 .or_else(|| runtime.session().runtime_handle());
99 Self {
100 shared: Arc::new(Shared {
101 runtime,
102 server,
103 closed: AtomicBool::new(false),
104 followers: Followers::default(),
105 }),
106 handle,
107 }
108 }
109
110 #[must_use]
112 pub fn runtime(&self) -> &Arc<AgentSessionRuntime> {
113 &self.shared.runtime
114 }
115
116 #[must_use]
118 pub fn state(&self) -> RpcSessionState {
119 self.shared.state()
120 }
121
122 fn on_runtime<T: Send + 'static>(
124 &self,
125 task: impl Future<Output = T> + Send + 'static,
126 closed: T,
127 ) -> BoxFuture<'static, T> {
128 match (tokio::runtime::Handle::try_current(), &self.handle) {
129 (Err(_), Some(handle)) => {
130 let join = handle.spawn(task);
131 Box::pin(async move { join.await.unwrap_or(closed) })
132 }
133 _ => Box::pin(task),
134 }
135 }
136}
137
138impl Shared {
139 fn session(&self) -> AgentSession {
140 self.runtime.session()
141 }
142
143 fn path(&self, path: &str) -> PathBuf {
146 let path = Path::new(path);
147 if path.is_absolute() {
148 path.to_path_buf()
149 } else {
150 self.runtime.services().cwd.join(path)
151 }
152 }
153
154 fn rebind(&self, reason: ReplaceReason, cancelled: bool) {
156 if cancelled {
157 return;
158 }
159 let session = self.session();
160 let announce = ClientEvent::Oino(OinoEvent::SessionReplaced {
161 session_file: session.session_file().map(|p| p.display().to_string()),
162 session_id: session.session_id(),
163 reason,
164 });
165 self.followers
166 .rebind(|| self.runtime.session().subscribe(), &announce);
167 }
168
169 fn state(&self) -> RpcSessionState {
170 let session = self.session();
171 let auto_compaction_enabled = session.with_settings(|s| s.settings().compaction_enabled());
172 RpcSessionState {
173 model: session.model(),
174 thinking_level: session.thinking_level(),
175 is_streaming: session.is_streaming(),
176 is_compacting: session.is_compacting(),
177 steering_mode: session.steering_mode(),
178 follow_up_mode: session.follow_up_mode(),
179 session_file: session.session_file().map(|p| p.display().to_string()),
180 session_id: session.session_id(),
181 session_name: session.session_name(),
182 auto_compaction_enabled,
183 message_count: session.messages().len(),
184 pending_message_count: session.pending_message_count(),
185 }
186 }
187
188 async fn dispatch(
189 &self,
190 request_id: Option<String>,
191 command: RpcCommand,
192 ) -> Result<RpcResponseData, ClientError> {
193 if self.closed.load(Ordering::Acquire) {
194 return Err(ClientError::Closed);
195 }
196 match command {
197 RpcCommand::Agent(command) => self.agent(request_id, command).await,
198 RpcCommand::Oino(command) => self.oino(command).await,
199 }
200 }
201
202 #[allow(
203 clippy::too_many_lines,
204 reason = "one arm per agent command, mirroring handleCommand"
205 )]
206 async fn agent(
207 &self,
208 request_id: Option<String>,
209 command: AgentCommand,
210 ) -> Result<RpcResponseData, ClientError> {
211 let session = self.session();
212 match command {
213 AgentCommand::Prompt {
214 message,
215 images,
216 streaming_behavior,
217 } => {
218 let disposition = session
219 .prompt(
220 message,
221 PromptOptions {
222 images,
223 streaming_behavior,
224 source: InputSource::Rpc,
225 ..PromptOptions::default()
226 },
227 )
228 .await
229 .map_err(failed)?;
230 data(json!({ "disposition": disposition }))
231 }
232 AgentCommand::Steer { message, images } => {
233 let disposition = session
234 .steer(message, images, InputSource::Rpc)
235 .await
236 .map_err(failed)?;
237 data(json!({ "disposition": disposition }))
238 }
239 AgentCommand::FollowUp { message, images } => {
240 let disposition = session
241 .follow_up(message, images, InputSource::Rpc)
242 .await
243 .map_err(failed)?;
244 data(json!({ "disposition": disposition }))
245 }
246 AgentCommand::Abort => {
247 session.abort().await;
248 Ok(None)
249 }
250 AgentCommand::ClearQueue => {
251 let (steering, follow_up) = session.clear_queue();
252 data(json!({ "steering": steering, "followUp": follow_up }))
253 }
254 AgentCommand::NewSession { parent_session } => {
255 let parent = parent_session.map(|p| self.path(&p));
256 let result = self.runtime.new_session(parent).await.map_err(failed)?;
257 self.rebind(ReplaceReason::New, result.cancelled);
258 data(json!({ "cancelled": result.cancelled }))
259 }
260 AgentCommand::GetState => data(self.state()),
261 AgentCommand::SetModel { provider, model_id } => {
262 let model = session
263 .models()
264 .available()
265 .into_iter()
266 .find(|m| m.provider == provider && m.id == model_id)
267 .ok_or_else(|| {
268 ClientError::Command(format!("Model not found: {provider}/{model_id}"))
269 })?;
270 let model = session.set_model(model, true).await.map_err(failed)?;
271 data(model)
272 }
273 AgentCommand::CycleModel => {
274 let result = session
275 .cycle_model(CycleDirection::Forward, true)
276 .await
277 .map_err(failed)?;
278 data(result)
279 }
280 AgentCommand::GetAvailableModels => {
281 data(json!({ "models": session.models().available() }))
282 }
283 AgentCommand::SetThinkingLevel { level } => {
284 session.set_thinking_level(level, true).await;
285 Ok(None)
286 }
287 AgentCommand::CycleThinkingLevel => {
288 let level = session.cycle_thinking_level(true).await;
289 Ok(Some(level.map_or(
290 JsonValue::Null,
291 |level| json!({ "level": level }),
292 )))
293 }
294 AgentCommand::GetAvailableThinkingLevels => {
295 data(json!({ "levels": session.available_thinking_levels() }))
296 }
297 AgentCommand::SetSteeringMode { mode } => {
298 session.set_steering_mode(mode);
299 Ok(None)
300 }
301 AgentCommand::SetFollowUpMode { mode } => {
302 session.set_follow_up_mode(mode);
303 Ok(None)
304 }
305 AgentCommand::Compact {
306 custom_instructions,
307 } => data(session.compact(custom_instructions).await.map_err(failed)?),
308 AgentCommand::SetAutoCompaction { enabled } => {
309 session.set_auto_compaction_enabled(enabled);
310 Ok(None)
311 }
312 AgentCommand::SetAutoRetry { enabled } => {
313 session.set_auto_retry_enabled(enabled);
314 Ok(None)
315 }
316 AgentCommand::AbortRetry => {
317 session.abort_retry();
318 Ok(None)
319 }
320 AgentCommand::Bash {
321 command,
322 exclude_from_context,
323 } => {
324 let result = session
325 .execute_bash(command, exclude_from_context.unwrap_or(false), request_id)
326 .await
327 .map_err(failed)?;
328 data(result)
329 }
330 AgentCommand::AbortBash => {
331 session.abort_bash();
332 Ok(None)
333 }
334 AgentCommand::GetSessionStats => data(session.session_stats()),
335 AgentCommand::ExportHtml { output_path } => {
336 let path = session
337 .export_html(output_path.map(|p| self.path(&p)))
338 .await
339 .map_err(failed)?;
340 data(json!({ "path": path.display().to_string() }))
341 }
342 AgentCommand::SwitchSession { session_path } => {
343 let result = self
344 .runtime
345 .switch_session(self.path(&session_path), None)
346 .await
347 .map_err(failed)?;
348 self.rebind(ReplaceReason::Resume, result.cancelled);
349 data(json!({ "cancelled": result.cancelled }))
350 }
351 AgentCommand::Fork { entry_id } => {
352 let result = self
353 .runtime
354 .fork(&EntryId::new(entry_id), ForkPosition::Before)
355 .await
356 .map_err(failed)?;
357 self.rebind(ReplaceReason::Fork, result.cancelled);
358 let mut value = json!({ "cancelled": result.cancelled });
359 if let Some(text) = result.selected_text {
360 value = json!({ "text": text, "cancelled": result.cancelled });
361 }
362 Ok(Some(value))
363 }
364 AgentCommand::Clone => {
365 let leaf = session.with_manager(|m| m.leaf_id().cloned());
366 if leaf.is_none() {
367 return Err(ClientError::Command(
368 "Cannot clone session: no current entry selected".to_owned(),
369 ));
370 }
371 let result = self.runtime.clone_session().await.map_err(failed)?;
372 self.rebind(ReplaceReason::Clone, result.cancelled);
373 data(json!({ "cancelled": result.cancelled }))
374 }
375 AgentCommand::GetForkMessages => {
376 data(json!({ "messages": session.user_messages_for_forking() }))
377 }
378 AgentCommand::GetEntries { since } => {
379 let (entries, leaf) =
380 session.with_manager(|m| (m.entries().to_vec(), m.leaf_id().cloned()));
381 let entries = match since {
382 None => entries,
383 Some(since) => {
384 let index = entries
385 .iter()
386 .position(|e| e.id.as_str() == since)
387 .ok_or_else(|| {
388 ClientError::Command(format!("Entry not found: {since}"))
389 })?;
390 entries[index + 1..].to_vec()
391 }
392 };
393 data(json!({ "entries": entries, "leafId": leaf }))
394 }
395 AgentCommand::GetTree => {
396 let (tree, leaf) = session.with_manager(|m| (m.tree(), m.leaf_id().cloned()));
397 data(json!({ "tree": tree, "leafId": leaf }))
398 }
399 AgentCommand::GetLastAssistantText => {
400 data(json!({ "text": session.last_assistant_text() }))
401 }
402 AgentCommand::SetSessionName { name } => {
403 let name = name.trim();
404 if name.is_empty() {
405 return Err(ClientError::Command(
406 "Session name cannot be empty".to_owned(),
407 ));
408 }
409 session
410 .set_session_name(Some(name.to_owned()))
411 .await
412 .map_err(failed)?;
413 Ok(None)
414 }
415 AgentCommand::GetMessages => data(json!({ "messages": session.messages() })),
416 AgentCommand::GetCommands => data(json!({ "commands": session.commands() })),
417 }
418 }
419
420 async fn oino(&self, command: OinoCommand) -> Result<RpcResponseData, ClientError> {
421 let session = self.session();
422 match command {
423 OinoCommand::Hello { client, .. } => {
424 session.register_frontend(&client.name);
425 data(json!({
426 "server": {"name": self.server.name, "version": self.server.version},
427 "protocol": 1,
428 "sessionId": session.session_id(),
429 "safeMode": self
430 .runtime
431 .services()
432 .safe_mode
433 .as_ref()
434 .is_some_and(|plan| plan.disable_extensions),
435 }))
436 }
437 OinoCommand::UiSnapshot => {
438 let snapshot = session.ui_snapshot();
439 let mut value = serde_json::to_value(snapshot).unwrap_or(JsonValue::Null);
440 if let Some(object) = value.as_object_mut() {
441 object.insert(
444 "requests".to_owned(),
445 serde_json::to_value(session.ui().snapshot()).unwrap_or(JsonValue::Null),
446 );
447 }
448 data(value)
449 }
450 OinoCommand::UiVisible {
451 entry_id,
452 visible,
453 frontend,
454 } => {
455 let frontend = frontend
456 .as_deref()
457 .filter(|name| !name.is_empty())
458 .unwrap_or("rpc");
459 session.ui_visible(frontend, &entry_id, visible);
460 Ok(None)
461 }
462 OinoCommand::UiResync { .. } => data(session.ui_resync()),
463 OinoCommand::NavigateTree { target_id, options } => {
464 let result = session
465 .navigate_tree(&EntryId::new(target_id), options)
466 .await
467 .map_err(failed)?;
468 data(result)
469 }
470 OinoCommand::SetLabel { entry_id, label } => {
471 session
472 .set_label(&EntryId::new(entry_id), label)
473 .await
474 .map_err(failed)?;
475 Ok(None)
476 }
477 OinoCommand::ImportJsonl { path } => {
478 let result = self
479 .runtime
480 .import_jsonl(self.path(&path), None)
481 .await
482 .map_err(failed)?;
483 self.rebind(ReplaceReason::Import, result.cancelled);
484 data(json!({ "cancelled": result.cancelled }))
485 }
486 OinoCommand::ExportJsonl { output_path } => {
487 let path = session
488 .export_jsonl(output_path.map(|p| self.path(&p)))
489 .await
490 .map_err(failed)?;
491 data(json!({ "path": path.display().to_string() }))
492 }
493 OinoCommand::GetContextUsage => data(session.context_usage()),
494 command @ (OinoCommand::Topics { .. }
495 | OinoCommand::BusGet { .. }
496 | OinoCommand::Agents { .. }
497 | OinoCommand::AgentMessages { .. }
498 | OinoCommand::AgentAbort { .. }
499 | OinoCommand::QueueEdit(_)
500 | OinoCommand::Checkpoints
501 | OinoCommand::Rewind { .. }
502 | OinoCommand::RunningBash
503 | OinoCommand::Extensions
504 | OinoCommand::SetExtensionEnabled { .. }) => self.gui(&session, command).await,
505 command => self.frontend(&session, command).await,
506 }
507 }
508
509 async fn gui(
512 &self,
513 session: &AgentSession,
514 command: OinoCommand,
515 ) -> Result<RpcResponseData, ClientError> {
516 match command {
517 OinoCommand::Topics {
518 subscribe,
519 unsubscribe,
520 } => {
521 let topics = session.subscribe_topics(&subscribe, &unsubscribe).await;
522 data(json!({ "topics": topics, "patterns": session.topic_patterns() }))
523 }
524 OinoCommand::BusGet { topic } => Ok(Some(session.bus_get(&topic).map_or_else(
525 || json!({ "data": null }),
526 |v| json!({ "data": v.data, "publisher": v.publisher }),
527 ))),
528 OinoCommand::Agents { follow } => {
529 if let Some(follow) = follow {
530 session.follow_agents(follow);
531 }
532 data(json!({ "agents": session.agent_tree(), "follow": session.follows_agents() }))
533 }
534 OinoCommand::AgentMessages { agent_id } => {
535 let agent = find_agent(session, &agent_id)?;
536 data(json!({ "messages": agent.messages() }))
537 }
538 OinoCommand::AgentAbort { agent_id } => {
539 find_agent(session, &agent_id)?.abort();
540 Ok(None)
541 }
542 OinoCommand::QueueEdit(edit) => {
543 session.edit_queue(edit).await.map_err(failed)?;
544 Ok(None)
545 }
546 OinoCommand::Checkpoints => data(json!({ "checkpoints": session.checkpoints() })),
547 OinoCommand::Rewind {
548 entry_id,
549 files,
550 conversation,
551 } => self.rewind(session, &entry_id, files, conversation).await,
552 OinoCommand::RunningBash => data(json!({ "calls": session.running_bash().await })),
553 OinoCommand::Extensions => data(json!({ "extensions": session.extension_infos() })),
554 OinoCommand::SetExtensionEnabled { id, enabled } => {
555 session
556 .set_extension_enabled(&id, enabled)
557 .map_err(failed)?;
558 Ok(None)
559 }
560 other => Err(ClientError::Protocol(format!(
561 "{} is not a GUI command",
562 other.kind()
563 ))),
564 }
565 }
566
567 async fn rewind(
570 &self,
571 session: &AgentSession,
572 entry_id: &str,
573 files: bool,
574 conversation: bool,
575 ) -> Result<RpcResponseData, ClientError> {
576 if session.is_streaming() {
577 return Err(ClientError::Command(
578 "Cannot rewind while the agent is running".to_owned(),
579 ));
580 }
581 let mut result = if files && (session.has_checkpoint(entry_id) || !conversation) {
582 session.rewind_files(entry_id).map_err(failed)?
583 } else {
584 RewindResult::default()
585 };
586 if conversation {
587 let forked = self
588 .runtime
589 .fork(&EntryId::new(entry_id), ForkPosition::Before)
590 .await
591 .map_err(failed)?;
592 self.rebind(ReplaceReason::Fork, forked.cancelled);
593 result.cancelled = forked.cancelled;
594 result.text = forked.selected_text;
595 }
596 data(result)
597 }
598
599 fn render_request(
601 command: &OinoCommand,
602 ) -> Result<oino_core::session::RenderRequest, ClientError> {
603 let OinoCommand::Render {
604 target,
605 key,
606 entry_id,
607 turn_entry_id,
608 turn_index,
609 running,
610 density,
611 expanded,
612 width,
613 data: payload,
614 } = command
615 else {
616 return Err(ClientError::Protocol(format!(
617 "{} is not a render command",
618 command.kind()
619 )));
620 };
621 let target = oino_core::session::RenderTarget::parse(target)
622 .ok_or_else(|| ClientError::Protocol(format!("render target `{target}`")))?;
623 Ok(oino_core::session::RenderRequest {
624 target,
625 key: key.clone(),
626 entry_id: entry_id.clone(),
627 turn_entry_id: turn_entry_id.clone(),
628 turn_index: *turn_index,
629 running: *running,
630 density: density.as_deref().map_or_else(Default::default, |d| {
631 oino_core::session::RenderDensity::parse(d).unwrap_or_default()
632 }),
633 expanded: *expanded,
634 width: width.as_deref().map_or_else(Default::default, |w| {
635 oino_core::session::RenderWidth::parse(w).unwrap_or_default()
636 }),
637 data: payload.clone(),
638 })
639 }
640
641 #[allow(
643 clippy::too_many_lines,
644 reason = "one arm per frontend command, mirroring the RPC surface"
645 )]
646 async fn frontend(
647 &self,
648 session: &AgentSession,
649 command: OinoCommand,
650 ) -> Result<RpcResponseData, ClientError> {
651 match command {
652 OinoCommand::GetCompletions { text, cursor } => {
653 data(json!({ "groups": session.completions(&text, cursor).await }))
654 }
655 OinoCommand::UiAction {
656 panel_id,
657 action_id,
658 value,
659 values,
660 path,
661 source,
662 } => {
663 let action = oino_core::ui::UiAction {
664 panel_id,
665 action_id,
666 value,
667 values: values.and_then(|values| values.as_object().cloned()),
668 path: path.and_then(|path| serde_json::from_value(path).ok()),
669 source: source.as_deref().and_then(|name| match name {
670 "tui" => Some(oino_core::ui::ActionSource::Tui),
671 "app" => Some(oino_core::ui::ActionSource::App),
672 "rpc" => Some(oino_core::ui::ActionSource::Rpc),
673 _ => None,
674 }),
675 };
676 session.ui_action_with(&action).map_err(failed)?;
677 Ok(None)
678 }
679 OinoCommand::EditorEvent(event) => {
680 let consumed = session.editor_event(event).await;
681 data(json!({ "consumed": consumed }))
682 }
683 OinoCommand::InvokeShortcut { shortcut_id } => {
684 session.invoke_shortcut(&shortcut_id).map_err(failed)?;
685 Ok(None)
686 }
687 OinoCommand::GetShortcuts => {
688 let shortcuts: Vec<JsonValue> = session
689 .runner()
690 .shortcuts()
691 .into_iter()
692 .map(|(extension, spec)| {
693 json!({
694 "id": spec.id,
695 "defaultKeys": spec.default_keys,
696 "description": spec.description,
697 "extensionId": extension,
698 })
699 })
700 .collect();
701 data(json!({ "shortcuts": shortcuts }))
702 }
703 command @ (OinoCommand::GetSettingsSchema
704 | OinoCommand::GetExtensionSettings { .. }
705 | OinoCommand::SetExtensionSetting { .. }
706 | OinoCommand::UnsetExtensionSetting { .. }
707 | OinoCommand::GetSettings { .. }
708 | OinoCommand::SetSetting { .. }) => self.settings_command(session, command),
709 OinoCommand::ServiceCall {
710 target,
711 name,
712 payload,
713 } => data(
714 session
715 .service_call(target.as_deref(), &name, payload)
716 .await
717 .map_err(failed)?,
718 ),
719 OinoCommand::AuthProviders => data(json!({ "providers": session.auth_providers() })),
720 OinoCommand::Login { provider } => {
721 session.login(&provider).await.map_err(failed)?;
722 Ok(None)
723 }
724 OinoCommand::Logout { provider } => {
725 session.logout(&provider).map_err(failed)?;
726 Ok(None)
727 }
728 OinoCommand::SetApiKey { provider, key } => {
729 session.set_api_key(&provider, &key).await.map_err(failed)?;
730 Ok(None)
731 }
732 render @ OinoCommand::Render { .. } => {
733 let node = session
734 .render_spec(&Self::render_request(&render)?)
735 .map_err(failed)?;
736 data(json!({ "node": node }))
737 }
738 OinoCommand::Hello { .. }
739 | OinoCommand::Topics { .. }
740 | OinoCommand::BusGet { .. }
741 | OinoCommand::Agents { .. }
742 | OinoCommand::AgentMessages { .. }
743 | OinoCommand::AgentAbort { .. }
744 | OinoCommand::QueueEdit(_)
745 | OinoCommand::Checkpoints
746 | OinoCommand::Rewind { .. }
747 | OinoCommand::RunningBash
748 | OinoCommand::Extensions
749 | OinoCommand::SetExtensionEnabled { .. }
750 | OinoCommand::UiSnapshot
751 | OinoCommand::UiVisible { .. }
752 | OinoCommand::UiResync { .. }
753 | OinoCommand::NavigateTree { .. }
754 | OinoCommand::SetLabel { .. }
755 | OinoCommand::ImportJsonl { .. }
756 | OinoCommand::ExportJsonl { .. }
757 | OinoCommand::GetContextUsage => Err(ClientError::Protocol(format!(
758 "{} is not a frontend command",
759 command.kind()
760 ))),
761 }
762 }
763
764 fn settings_command(
766 &self,
767 session: &AgentSession,
768 command: OinoCommand,
769 ) -> Result<RpcResponseData, ClientError> {
770 let registry = self.runtime.extension_settings();
771 match command {
772 OinoCommand::GetSettingsSchema => data(json!({ "namespaces": registry.schema() })),
773 OinoCommand::GetExtensionSettings { namespace, scope } => {
774 if registry.namespace(&namespace).is_none() {
775 return Err(ClientError::Command(format!(
776 "Unknown settings namespace: {namespace}"
777 )));
778 }
779 let resolved = registry.resolved(&namespace, scope);
781 data(json!({
782 "values": resolved.values,
783 "diagnostics": resolved.diagnostics,
784 }))
785 }
786 OinoCommand::SetExtensionSetting {
787 namespace,
788 key,
789 value,
790 scope,
791 } => {
792 let value = value.filter(|v| !v.is_null());
793 registry
794 .set(&namespace, &key, value, scope)
795 .map_err(failed)?;
796 Ok(None)
797 }
798 OinoCommand::UnsetExtensionSetting {
799 namespace,
800 key,
801 scope,
802 } => {
803 registry
804 .set(&namespace, &key, None, scope)
805 .map_err(failed)?;
806 Ok(None)
807 }
808 OinoCommand::GetSettings { key } => {
809 let merged = registry.merged_settings();
810 match key {
811 None => data(json!({ "settings": merged })),
812 Some(key) => {
813 let path: Vec<&str> = key.split('.').collect();
814 let value = oino_settings::json::get_path(&merged, &path).cloned();
815 data(json!({ "value": value }))
816 }
817 }
818 }
819 OinoCommand::SetSetting { key, value, scope } => {
820 let path: Vec<&str> = key.split('.').filter(|p| !p.is_empty()).collect();
821 if path.is_empty() {
822 return Err(ClientError::Command("Setting key cannot be empty".into()));
823 }
824 let value = value.filter(|v| !v.is_null());
825 session
826 .with_settings(|settings| settings.set(scope, &path, value))
827 .map_err(failed)?;
828 Ok(None)
829 }
830 other => Err(ClientError::Protocol(format!(
831 "{} is not a settings command",
832 other.kind()
833 ))),
834 }
835 }
836}
837
838impl SessionBackend for InProcessBackend {
839 fn send(&self, command: RpcCommand) -> BoxFuture<'_, Result<RpcResponseData, ClientError>> {
840 self.send_with_id(None, command)
841 }
842
843 fn send_with_id(
844 &self,
845 request_id: Option<String>,
846 command: RpcCommand,
847 ) -> BoxFuture<'_, Result<RpcResponseData, ClientError>> {
848 let shared = Arc::clone(&self.shared);
849 self.on_runtime(
850 async move { shared.dispatch(request_id, command).await },
851 Err(ClientError::Closed),
852 )
853 }
854
855 fn events(&self) -> BoxStream<'static, ClientEvent> {
858 self.shared
859 .followers
860 .stream(self.shared.session().subscribe())
861 .boxed()
862 }
863
864 fn ui_respond(&self, response: UiResponse) {
865 self.shared.session().ui().respond(response);
866 }
867
868 fn session_id(&self) -> String {
869 self.shared.session().session_id()
870 }
871
872 fn shutdown(&self) -> BoxFuture<'_, ()> {
873 let shared = Arc::clone(&self.shared);
874 self.on_runtime(
875 async move {
876 if !shared.closed.swap(true, Ordering::AcqRel) {
877 shared.runtime.dispose().await;
878 shared.followers.close();
879 }
880 },
881 (),
882 )
883 }
884
885 fn shutdown_requested(&self) -> BoxFuture<'static, ()> {
887 let runtime = Arc::clone(&self.shared.runtime);
888 self.on_runtime(
889 async move {
890 let mut requested = runtime.shutdown_requested();
891 if requested.wait_for(|on| *on).await.is_err() {
892 return std::future::pending().await;
893 }
894 runtime.session().wait_for_idle().await;
895 },
896 (),
897 )
898 }
899}