diff --git a/README.md b/README.md index cc0e2fd..64e634a 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ agcanvas bridges the gap between visual design and code generation. It's a **col - **Shape Drawing** — Rectangles, ellipses, lines, arrows, text directly on canvas - **Selection & Editing** — Select, move, resize shapes with corner handles - **Mermaid Diagrams** — Write Mermaid syntax, render as SVG on canvas -- **Sessions/Tabs** — Multiple canvases in tabs, each with independent state +- **Sessions/Tabs** — Multiple canvases in tabs, each with independent state, creator tracking (human vs agent), descriptions, and timestamps - **Pan/Zoom** — Smooth canvas navigation ### AI Agent Integration @@ -179,7 +179,9 @@ Same MCP config format — add the `agcanvas` entry to your Codex MCP configurat | Tool | Description | |------|-------------| -| `list_sessions` | List all open tabs/sessions in agcanvas | +| `list_sessions` | List all open tabs/sessions with creator info, descriptions, timestamps. Supports sorting by name, created_at, created_by, element_count | +| `create_session` | Create a new session/tab from an agent, with name, description, and creator identity | +| `update_session` | Update an existing session's name or description | | `get_element_tree` | Get the full parsed SVG element tree (structured JSON) | | `describe_canvas` | Get a human-readable description of the canvas | | `get_element_by_id` | Look up a specific element by ID | @@ -214,20 +216,45 @@ All requests support an optional `session_id` parameter. If omitted, the active ```json {"type": "ListSessions"} +{"type": "ListSessions", "sort_by": "created_at", "sort_order": "desc"} ``` +Sort fields: `name`, `created_at` (default), `created_by`, `element_count`. Order: `asc` (default), `desc`. + Response: ```json { "type": "Sessions", "sessions": [ - {"id": "session-1", "name": "Tab 1", "has_svg": true, "element_count": 15}, - {"id": "session-2", "name": "Tab 2", "has_svg": false, "element_count": null} + {"id": "session-1", "name": "Tab 1", "has_svg": true, "element_count": 15, "description": null, "created_by": {"type": "Human"}, "created_at": 1707500000}, + {"id": "session-2", "name": "Agent Work", "has_svg": false, "element_count": null, "description": "Architecture diagram", "created_by": {"type": "Agent", "name": "Claude"}, "created_at": 1707500100} ], "active_session": "session-1" } ``` +#### Create session (agent) + +```json +{"type": "CreateSession", "name": "My Session", "description": "Working on auth flow", "created_by_name": "Claude"} +``` + +Response: +```json +{"type": "SessionCreated", "session": {"id": "session-3", "name": "My Session", ...}} +``` + +#### Update session + +```json +{"type": "UpdateSession", "session_id": "session-1", "name": "Renamed", "description": "Updated description"} +``` + +Response: +```json +{"type": "SessionUpdated", "session": {"id": "session-1", "name": "Renamed", ...}} +``` + #### Get full element tree ```json @@ -354,8 +381,9 @@ crates/ - [x] Selection, move, resize with handles - [x] Mermaid diagram rendering - [x] MCP server bridge for AI coding tools +- [x] Agent draw commands (modify canvas from agent) +- [x] Session metadata (creator tracking, descriptions, timestamps, sorting) - [ ] Real code generation (not just stubs) -- [ ] Agent draw commands (modify canvas from agent) - [ ] Export to file - [ ] Diff view (before/after agent changes) - [ ] Plugin system for code generators diff --git a/crates/agcanvas-mcp/src/tools.rs b/crates/agcanvas-mcp/src/tools.rs index a4c14e4..a0509b3 100644 --- a/crates/agcanvas-mcp/src/tools.rs +++ b/crates/agcanvas-mcp/src/tools.rs @@ -11,6 +11,36 @@ pub struct SessionIdParam { pub session_id: Option, } +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +pub struct ListSessionsParam { + #[schemars( + description = "Sort field: 'name', 'created_at' (default), 'created_by', or 'element_count'" + )] + pub sort_by: Option, + #[schemars(description = "Sort order: 'asc' (default) or 'desc'")] + pub sort_order: Option, +} + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +pub struct CreateSessionParam { + #[schemars(description = "Session name. If omitted, auto-generated.")] + pub name: Option, + #[schemars(description = "Session description.")] + pub description: Option, + #[schemars(description = "Name of the agent creating the session (identifies the creator).")] + pub created_by_name: Option, +} + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +pub struct UpdateSessionParam { + #[schemars(description = "ID of the session to update.")] + pub session_id: String, + #[schemars(description = "New session name.")] + pub name: Option, + #[schemars(description = "New session description.")] + pub description: Option, +} + #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct GetElementParam { #[schemars(description = "Session ID to target. If omitted, uses the active session.")] @@ -155,10 +185,62 @@ impl AgCanvasServer { } #[tool( - description = "List all open sessions/tabs in agcanvas. Returns session IDs, names, and whether they have SVG or drawing content loaded." + description = "List all open sessions/tabs in agcanvas. Returns session IDs, names, creator info, description, timestamps, and whether they have SVG or drawing content loaded. Supports sorting." )] - async fn list_sessions(&self) -> Result { - let request = serde_json::json!({"type": "ListSessions"}); + async fn list_sessions( + &self, + Parameters(params): Parameters, + ) -> Result { + let mut request = serde_json::json!({"type": "ListSessions"}); + let obj = request.as_object_mut().unwrap(); + if let Some(v) = params.sort_by { + obj.insert("sort_by".into(), v.into()); + } + if let Some(v) = params.sort_order { + obj.insert("sort_order".into(), v.into()); + } + self.call_agcanvas(&request).await + } + + #[tool( + description = "Create a new session/tab in agcanvas. The session is created by an agent. Returns the created session with its ID and metadata." + )] + async fn create_session( + &self, + Parameters(params): Parameters, + ) -> Result { + let mut request = serde_json::json!({"type": "CreateSession"}); + let obj = request.as_object_mut().unwrap(); + if let Some(v) = params.name { + obj.insert("name".into(), v.into()); + } + if let Some(v) = params.description { + obj.insert("description".into(), v.into()); + } + if let Some(v) = params.created_by_name { + obj.insert("created_by_name".into(), v.into()); + } + self.call_agcanvas(&request).await + } + + #[tool( + description = "Update an existing session's name or description. Only provided fields are changed." + )] + async fn update_session( + &self, + Parameters(params): Parameters, + ) -> Result { + let mut request = serde_json::json!({ + "type": "UpdateSession", + "session_id": params.session_id, + }); + let obj = request.as_object_mut().unwrap(); + if let Some(v) = params.name { + obj.insert("name".into(), v.into()); + } + if let Some(v) = params.description { + obj.insert("description".into(), v.into()); + } self.call_agcanvas(&request).await } diff --git a/crates/agcanvas/src/agent/mod.rs b/crates/agcanvas/src/agent/mod.rs index 2ca35ec..d4dde7d 100644 --- a/crates/agcanvas/src/agent/mod.rs +++ b/crates/agcanvas/src/agent/mod.rs @@ -1,5 +1,5 @@ mod protocol; mod server; -pub use protocol::{DrawingCommand, GuiEvent}; +pub use protocol::{DrawingCommand, GuiEvent, SessionCommand}; pub use server::AgentServer; diff --git a/crates/agcanvas/src/agent/protocol.rs b/crates/agcanvas/src/agent/protocol.rs index c7fb6c1..10326b9 100644 --- a/crates/agcanvas/src/agent/protocol.rs +++ b/crates/agcanvas/src/agent/protocol.rs @@ -1,6 +1,6 @@ use crate::drawing::{DrawingElement, Shape, ShapeStyle}; use crate::element_tree::{ElementTree, TreeMetadata}; -use crate::session::SessionInfo; +use crate::session::{SessionCreator, SessionInfo, SessionSortField, SortOrder}; use egui::{Color32, Pos2, Vec2}; use serde::{Deserialize, Serialize}; @@ -56,7 +56,27 @@ pub enum GuiEvent { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum AgentRequest { - ListSessions, + ListSessions { + #[serde(default)] + sort_by: Option, + #[serde(default)] + sort_order: Option, + }, + CreateSession { + #[serde(default)] + name: Option, + #[serde(default)] + description: Option, + #[serde(default)] + created_by_name: Option, + }, + UpdateSession { + session_id: String, + #[serde(default)] + name: Option, + #[serde(default)] + description: Option, + }, GetTree { #[serde(default)] session_id: Option, @@ -192,6 +212,12 @@ pub enum AgentResponse { sessions: Vec, active_session: Option, }, + SessionCreated { + session: SessionInfo, + }, + SessionUpdated { + session: SessionInfo, + }, Tree { session_id: String, tree: ElementTree, @@ -261,6 +287,25 @@ pub enum DrawingCommand { }, } +// --------------------------------------------------------------------------- +// Agent → GUI session commands (reverse sync channel for session management) +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone)] +pub enum SessionCommand { + Create { + id: String, + name: String, + description: Option, + created_by: SessionCreator, + }, + Update { + session_id: String, + name: Option, + description: Option, + }, +} + // --------------------------------------------------------------------------- // Code generation targets // --------------------------------------------------------------------------- diff --git a/crates/agcanvas/src/agent/server.rs b/crates/agcanvas/src/agent/server.rs index 8b8db30..59d7e16 100644 --- a/crates/agcanvas/src/agent/server.rs +++ b/crates/agcanvas/src/agent/server.rs @@ -1,8 +1,9 @@ use super::protocol::{ build_shape, build_style, AgentRequest, AgentResponse, CodeGenTarget, DrawingCommand, GuiEvent, + SessionCommand, }; use crate::drawing::DrawingElement; -use crate::session::SessionStore; +use crate::session::{SessionCreator, SessionStore}; use anyhow::Result; use futures_util::{SinkExt, StreamExt}; use std::sync::Arc; @@ -16,16 +17,22 @@ pub struct AgentServer { sessions: Arc>, event_tx: broadcast::Sender, command_tx: mpsc::UnboundedSender, + session_command_tx: mpsc::UnboundedSender, port: u16, } impl AgentServer { - pub fn new(port: u16, command_tx: mpsc::UnboundedSender) -> Self { + pub fn new( + port: u16, + command_tx: mpsc::UnboundedSender, + session_command_tx: mpsc::UnboundedSender, + ) -> Self { let (event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY); Self { sessions: Arc::new(RwLock::new(SessionStore::new())), event_tx, command_tx, + session_command_tx, port, } } @@ -49,9 +56,17 @@ impl AgentServer { let event_tx = self.event_tx.clone(); let event_rx = self.event_tx.subscribe(); let command_tx = self.command_tx.clone(); + let session_command_tx = self.session_command_tx.clone(); tokio::spawn(async move { - if let Err(e) = - handle_connection(stream, sessions, event_rx, event_tx, command_tx).await + if let Err(e) = handle_connection( + stream, + sessions, + event_rx, + event_tx, + command_tx, + session_command_tx, + ) + .await { tracing::error!("Connection error: {}", e); } @@ -68,6 +83,7 @@ async fn handle_connection( mut event_rx: broadcast::Receiver, event_tx: broadcast::Sender, command_tx: mpsc::UnboundedSender, + session_command_tx: mpsc::UnboundedSender, ) -> Result<()> { let ws_stream = tokio_tungstenite::accept_async(stream).await?; let (mut write, mut read) = ws_stream.split(); @@ -91,7 +107,7 @@ async fn handle_connection( let response = match serde_json::from_str::(&text) { Ok(request) => { process_request( - request, &sessions, &event_tx, &command_tx, + request, &sessions, &event_tx, &command_tx, &session_command_tx, ).await } Err(e) => AgentResponse::Error { @@ -134,18 +150,98 @@ async fn process_request( sessions: &Arc>, event_tx: &broadcast::Sender, command_tx: &mpsc::UnboundedSender, + session_command_tx: &mpsc::UnboundedSender, ) -> AgentResponse { match request { AgentRequest::Ping => AgentResponse::Pong, - AgentRequest::ListSessions => { + AgentRequest::ListSessions { + sort_by, + sort_order, + } => { let store = sessions.read().await; + let sessions_list = store + .list_sessions_sorted(sort_by.unwrap_or_default(), sort_order.unwrap_or_default()); AgentResponse::Sessions { - sessions: store.list_sessions(), + sessions: sessions_list, active_session: store.active_session_id().map(|s| s.to_string()), } } + AgentRequest::CreateSession { + name, + description, + created_by_name, + } => { + let mut store = sessions.write().await; + let id = store.next_session_id(); + let session_name = name.unwrap_or_else(|| format!("Agent Session {}", &id)); + let created_by = SessionCreator::Agent { + name: created_by_name, + }; + + let info = crate::session::SessionInfo { + id: id.clone(), + name: session_name.clone(), + has_svg: false, + element_count: None, + drawing_element_count: 0, + description: description.clone(), + created_by: created_by.clone(), + created_at: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0), + }; + + store.add_session(info.clone(), None); + drop(store); + + let _ = session_command_tx.send(SessionCommand::Create { + id: id.clone(), + name: session_name, + description, + created_by, + }); + let _ = event_tx.send(GuiEvent::SessionCreated { + session: info.clone(), + }); + + AgentResponse::SessionCreated { session: info } + } + + AgentRequest::UpdateSession { + session_id, + name, + description, + } => { + let mut store = sessions.write().await; + let desc_update = description.as_ref().map(|d| Some(d.clone())); + if !store.update_session_meta(&session_id, name.clone(), desc_update) { + return AgentResponse::Error { + message: format!("Session '{}' not found", session_id), + }; + } + let info = store + .list_sessions() + .into_iter() + .find(|s| s.id == session_id); + drop(store); + + let _ = session_command_tx.send(SessionCommand::Update { + session_id: session_id.clone(), + name, + description, + }); + + match info { + Some(session) => AgentResponse::SessionUpdated { session }, + None => AgentResponse::Error { + message: format!("Session '{}' not found after update", session_id), + }, + } + } + AgentRequest::GetTree { session_id } => { let store = sessions.read().await; match store.get_tree(session_id.as_deref()) { diff --git a/crates/agcanvas/src/app.rs b/crates/agcanvas/src/app.rs index 11e2fa9..da33f58 100644 --- a/crates/agcanvas/src/app.rs +++ b/crates/agcanvas/src/app.rs @@ -1,4 +1,4 @@ -use crate::agent::{AgentServer, DrawingCommand, GuiEvent}; +use crate::agent::{AgentServer, DrawingCommand, GuiEvent, SessionCommand}; use crate::canvas::{CanvasInteraction, CanvasState}; use crate::clipboard::ClipboardManager; use crate::drawing::{ @@ -6,7 +6,7 @@ use crate::drawing::{ screen_to_canvas, DragState, DrawingElement, Shape, ShapeStyle, Tool, }; use crate::mermaid::render_mermaid_to_svg; -use crate::session::{Session, SessionStore}; +use crate::session::{Session, SessionCreator, SessionStore}; use crate::svg::{parse_svg, SvgRenderer}; use egui::{Color32, ColorImage, TextureOptions}; use std::sync::Arc; @@ -23,6 +23,7 @@ pub struct AgCanvasApp { sessions_handle: Arc>, event_tx: broadcast::Sender, command_rx: mpsc::UnboundedReceiver, + session_command_rx: mpsc::UnboundedReceiver, clipboard: Option, show_tree_panel: bool, show_description: bool, @@ -43,7 +44,8 @@ impl AgCanvasApp { let runtime = Runtime::new().expect("Failed to create tokio runtime"); let (command_tx, command_rx) = mpsc::unbounded_channel(); - let server = AgentServer::new(AGENT_PORT, command_tx); + let (session_command_tx, session_command_rx) = mpsc::unbounded_channel(); + let server = AgentServer::new(AGENT_PORT, command_tx, session_command_tx); let sessions_handle = server.sessions_handle(); let event_tx = server.event_sender(); @@ -62,6 +64,7 @@ impl AgCanvasApp { sessions_handle, event_tx, command_rx, + session_command_rx, clipboard, show_tree_panel: false, show_description: false, @@ -83,7 +86,7 @@ impl AgCanvasApp { self.session_counter += 1; let id = format!("session-{}", self.session_counter); let name = format!("Tab {}", self.session_counter); - let session = Session::new(id.clone(), name); + let session = Session::new(id.clone(), name, SessionCreator::Human); let info = session.info(); self.sessions.push(session); @@ -321,6 +324,46 @@ impl AgCanvasApp { } } + fn drain_session_commands(&mut self) { + while let Ok(cmd) = self.session_command_rx.try_recv() { + match cmd { + SessionCommand::Create { + id, + name, + description, + created_by, + } => { + let session = + Session::new(id.clone(), name, created_by).with_description(description); + self.sessions.push(session); + self.active_session_idx = self.sessions.len() - 1; + + if let Some(num) = id.strip_prefix("session-") { + if let Ok(n) = num.parse::() { + if n >= self.session_counter { + self.session_counter = n; + } + } + } + } + SessionCommand::Update { + session_id, + name, + description, + } => { + if let Some(session) = self.sessions.iter_mut().find(|s| s.id == session_id) { + if let Some(n) = name { + session.name = n; + } + if let Some(d) = description { + session.description = Some(d); + } + } + } + } + } + } + fn sync_drawing_elements_to_store(&self) { let sessions_handle = self.sessions_handle.clone(); let elements_by_session: Vec<(String, Vec)> = self @@ -534,6 +577,7 @@ fn handle_shape_tool( impl eframe::App for AgCanvasApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.drain_drawing_commands(); + self.drain_session_commands(); let mut paste = false; let mut new_tab = false; @@ -678,10 +722,15 @@ impl eframe::App for AgCanvasApp { let is_active = idx == self.active_session_idx; let has_content = session.element_tree.is_some() || !session.drawing_elements.is_empty(); + + let creator_icon = match &session.created_by { + SessionCreator::Human => "", + SessionCreator::Agent { .. } => "\u{1F916} ", + }; let label = if has_content { - format!("{} *", session.name) + format!("{}{} *", creator_icon, session.name) } else { - session.name.clone() + format!("{}{}", creator_icon, session.name) }; let button = egui::Button::new(&label).fill(if is_active { @@ -695,6 +744,14 @@ impl eframe::App for AgCanvasApp { switch_idx = Some(idx); } + let mut tooltip_parts = vec![format!("Created by: {}", session.created_by)]; + if let Some(desc) = &session.description { + if !desc.is_empty() { + tooltip_parts.push(desc.clone()); + } + } + tab_response.clone().on_hover_text(tooltip_parts.join("\n")); + if self.sessions.len() > 1 { tab_response.context_menu(|ui| { if ui.button("Close").clicked() { @@ -1003,6 +1060,16 @@ impl eframe::App for AgCanvasApp { if self.last_drawing_sync.elapsed().as_millis() > 500 { self.last_drawing_sync = std::time::Instant::now(); self.sync_drawing_elements_to_store(); + + let sessions_handle = self.sessions_handle.clone(); + let counter = self.session_counter; + std::thread::spawn(move || { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + let mut store = sessions_handle.write().await; + store.set_counter_minimum(counter); + }); + }); } ctx.request_repaint(); diff --git a/crates/agcanvas/src/session.rs b/crates/agcanvas/src/session.rs index 67ee645..201bea4 100644 --- a/crates/agcanvas/src/session.rs +++ b/crates/agcanvas/src/session.rs @@ -5,6 +5,61 @@ use crate::svg::SvgRenderer; use egui::TextureHandle; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::time::{SystemTime, UNIX_EPOCH}; + +// --------------------------------------------------------------------------- +// Session metadata types +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(tag = "type")] +pub enum SessionCreator { + #[default] + Human, + Agent { + #[serde(default)] + name: Option, + }, +} + +impl std::fmt::Display for SessionCreator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SessionCreator::Human => write!(f, "Human"), + SessionCreator::Agent { name: Some(n) } => write!(f, "Agent: {}", n), + SessionCreator::Agent { name: None } => write!(f, "Agent"), + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum SessionSortField { + Name, + #[default] + CreatedAt, + CreatedBy, + ElementCount, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum SortOrder { + #[default] + Asc, + Desc, +} + +fn unix_now() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0) +} + +// --------------------------------------------------------------------------- +// SessionInfo — lightweight summary sent over the wire +// --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionInfo { @@ -13,6 +68,12 @@ pub struct SessionInfo { pub has_svg: bool, pub element_count: Option, pub drawing_element_count: usize, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub created_by: SessionCreator, + #[serde(default)] + pub created_at: i64, } pub struct Session { @@ -28,10 +89,14 @@ pub struct Session { pub selected_element_id: Option, pub active_tool: Tool, pub drag_state: DragState, + + pub description: Option, + pub created_by: SessionCreator, + pub created_at: i64, } impl Session { - pub fn new(id: String, name: String) -> Self { + pub fn new(id: String, name: String, created_by: SessionCreator) -> Self { Self { id, name, @@ -44,9 +109,17 @@ impl Session { selected_element_id: None, active_tool: Tool::default(), drag_state: DragState::default(), + description: None, + created_by, + created_at: unix_now(), } } + pub fn with_description(mut self, description: Option) -> Self { + self.description = description; + self + } + pub fn info(&self) -> SessionInfo { SessionInfo { id: self.id.clone(), @@ -54,6 +127,9 @@ impl Session { has_svg: self.element_tree.is_some(), element_count: self.element_tree.as_ref().map(|t| t.metadata.element_count), drawing_element_count: self.drawing_elements.len(), + description: self.description.clone(), + created_by: self.created_by.clone(), + created_at: self.created_at, } } @@ -98,6 +174,7 @@ pub struct SessionData { pub struct SessionStore { sessions: HashMap, active_session_id: Option, + session_counter: usize, } impl SessionStore { @@ -234,6 +311,63 @@ impl SessionStore { self.sessions.values().map(|d| d.info.clone()).collect() } + pub fn list_sessions_sorted( + &self, + sort_by: SessionSortField, + order: SortOrder, + ) -> Vec { + let mut sessions: Vec = self.list_sessions(); + sessions.sort_by(|a, b| { + let cmp = match sort_by { + SessionSortField::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()), + SessionSortField::CreatedAt => a.created_at.cmp(&b.created_at), + SessionSortField::CreatedBy => { + a.created_by.to_string().cmp(&b.created_by.to_string()) + } + SessionSortField::ElementCount => { + let ac = a.element_count.unwrap_or(0) + a.drawing_element_count; + let bc = b.element_count.unwrap_or(0) + b.drawing_element_count; + ac.cmp(&bc) + } + }; + match order { + SortOrder::Asc => cmp, + SortOrder::Desc => cmp.reverse(), + } + }); + sessions + } + + pub fn next_session_id(&mut self) -> String { + self.session_counter += 1; + format!("session-{}", self.session_counter) + } + + pub fn set_counter_minimum(&mut self, min: usize) { + if min > self.session_counter { + self.session_counter = min; + } + } + + pub fn update_session_meta( + &mut self, + session_id: &str, + name: Option, + description: Option>, + ) -> bool { + if let Some(data) = self.sessions.get_mut(session_id) { + if let Some(n) = name { + data.info.name = n; + } + if let Some(d) = description { + data.info.description = d; + } + true + } else { + false + } + } + pub fn active_session_id(&self) -> Option<&str> { self.active_session_id.as_deref() }