diff --git a/crates/flynt-agent/src/main.rs b/crates/flynt-agent/src/main.rs index 4897dbe..4dc9f89 100644 --- a/crates/flynt-agent/src/main.rs +++ b/crates/flynt-agent/src/main.rs @@ -37,6 +37,21 @@ fn default_project_root() -> PathBuf { .join("Flynt") } +fn is_transport_disconnect_message(msg: &str) -> bool { + let msg = msg.to_lowercase(); + msg.contains("broken pipe") + || msg.contains("os error 32") + || msg.contains("connection closed") + || msg.contains("closed connection") + || msg.contains("connection reset") + || msg.contains("transport disconnected") + || msg.contains("channel closed") +} + +fn is_transport_disconnect_error(err: &omegon_extension::Error) -> bool { + is_transport_disconnect_message(&err.to_string()) +} + #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt() @@ -54,8 +69,8 @@ async fn main() -> Result<()> { "FLYNT_PROJECT", &["OMEGON_PROJECT_ROOT", "FLYNT_VAULT", "CODEX_VAULT"], ) - .map(PathBuf::from) - .unwrap_or_else(default_project_root); + .map(PathBuf::from) + .unwrap_or_else(default_project_root); std::fs::create_dir_all(&project_root)?; let project = Arc::new(Project::open(&project_root)?); @@ -67,9 +82,13 @@ async fn main() -> Result<()> { match mode { Some("--mcp") => { // MCP server mode — compatible with Claude Code, Cursor, etc. - omegon_extension::mcp_shim::serve_mcp(ext) - .await - .expect("flynt MCP server failed"); + if let Err(e) = omegon_extension::mcp_shim::serve_mcp(ext).await { + if is_transport_disconnect_error(&e) { + tracing::warn!("flynt MCP client disconnected: {e}"); + return Ok(()); + } + return Err(anyhow::anyhow!("flynt MCP server failed: {e}")); + } } Some("--help") | Some("help") | Some("-h") => { println!("flynt-agent — project document and task tools for omegon"); @@ -81,9 +100,15 @@ async fn main() -> Result<()> { println!(" flynt-agent --help Show this help"); println!(); println!("ENVIRONMENT:"); - println!(" FLYNT_PROJECT Project directory (default: cwd if it looks like a project, otherwise ~/Documents/Flynt)"); - println!(" Also accepts OMEGON_PROJECT_ROOT from the native extension host."); - println!(" Legacy aliases (deprecated): FLYNT_VAULT, CODEX_VAULT"); + println!( + " FLYNT_PROJECT Project directory (default: cwd if it looks like a project, otherwise ~/Documents/Flynt)" + ); + println!( + " Also accepts OMEGON_PROJECT_ROOT from the native extension host." + ); + println!( + " Legacy aliases (deprecated): FLYNT_VAULT, CODEX_VAULT" + ); } Some("--rpc") | _ => { // Default: run as omegon extension (v2 bidirectional protocol) @@ -98,7 +123,7 @@ async fn main() -> Result<()> { #[cfg(test)] mod tests { - use super::looks_like_project_root; + use super::{is_transport_disconnect_message, looks_like_project_root}; #[test] fn recognizes_flynt_project_root() { @@ -119,4 +144,15 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); assert!(!looks_like_project_root(tmp.path())); } + + #[test] + fn classifies_transport_disconnects() { + assert!(is_transport_disconnect_message("Broken pipe (os error 32)")); + assert!(is_transport_disconnect_message("connection closed")); + assert!(is_transport_disconnect_message( + "transport disconnected while writing response" + )); + assert!(!is_transport_disconnect_message("method not found")); + assert!(!is_transport_disconnect_message("invalid params")); + } } diff --git a/crates/flynt-app/src/components/sidebar.rs b/crates/flynt-app/src/components/sidebar.rs index 7f554f7..187ce20 100644 --- a/crates/flynt-app/src/components/sidebar.rs +++ b/crates/flynt-app/src/components/sidebar.rs @@ -319,6 +319,9 @@ fn TreeFile(meta: DocumentMeta, depth: u32) -> Element { }, style: "padding-left: {indent + 20.0}px;", onclick: move |_| { + if let Ok(Some(doc)) = ctx.project().store.get_document(&id) { + let _ = document::eval(&crate::views::notes::cm6_fast_swap_js(&doc.content)); + } tab_state.write().open(id.clone(), title.clone()); // Only write route if we're not already on Notes — avoids // triggering a full app route re-evaluation for no reason. @@ -373,6 +376,9 @@ fn TreeFile(meta: DocumentMeta, depth: u32) -> Element { *ctx_menu.write() = None; match action.as_str() { "open-tab" => { + if let Ok(Some(doc)) = ctx.project().store.get_document(&id_for_tab) { + let _ = document::eval(&crate::views::notes::cm6_fast_swap_js(&doc.content)); + } tab_state.write().open(id_for_tab.clone(), title_for_tab.clone()); *active_route.write() = Route::Notes; } diff --git a/crates/flynt-app/src/components/tab_bar.rs b/crates/flynt-app/src/components/tab_bar.rs index c981790..58b51ad 100644 --- a/crates/flynt-app/src/components/tab_bar.rs +++ b/crates/flynt-app/src/components/tab_bar.rs @@ -1,14 +1,16 @@ -use dioxus::prelude::*; -use flynt_core::store::ProjectStore; use crate::bootstrap::AppContext; use crate::state::TabState; +use dioxus::prelude::*; +use flynt_core::store::ProjectStore; #[component] pub fn TabBar() -> Element { let tab_state = use_context::>(); let tabs = tab_state.read().tabs.clone(); - if tabs.is_empty() { return rsx! { div { class: "tab-bar tab-bar-empty" } }; } + if tabs.is_empty() { + return rsx! { div { class: "tab-bar tab-bar-empty" } }; + } rsx! { div { class: "tab-bar", @@ -33,7 +35,12 @@ pub fn TabBar() -> Element { } #[component] -fn TabItem(index: usize, title: String, doc_id: flynt_core::models::DocumentId, is_active: bool) -> Element { +fn TabItem( + index: usize, + title: String, + doc_id: flynt_core::models::DocumentId, + is_active: bool, +) -> Element { let ctx = use_context::(); let mut tab_state = use_context::>(); let mut renaming = use_signal(|| false); @@ -44,7 +51,12 @@ fn TabItem(index: usize, title: String, doc_id: flynt_core::models::DocumentId, rsx! { div { class: if is_active { "tab active" } else { "tab" }, - onclick: move |_| tab_state.write().active = i, + onclick: move |_| { + if let Ok(Some(doc)) = ctx.project().store.get_document(&doc_id) { + let _ = document::eval(&crate::views::notes::cm6_fast_swap_js(&doc.content)); + } + tab_state.write().active = i; + }, ondoubleclick: move |_| { *rename_input.write() = title.clone(); *renaming.write() = true; diff --git a/crates/flynt-app/src/views/notes.rs b/crates/flynt-app/src/views/notes.rs index 38de267..31b707f 100644 --- a/crates/flynt-app/src/views/notes.rs +++ b/crates/flynt-app/src/views/notes.rs @@ -314,6 +314,23 @@ fn preprocess(src: &str) -> String { // ── CM6 init JS ───────────────────────────────────────────────────────────── +pub(crate) fn cm6_fast_swap_js(content: &str) -> String { + let escaped = serde_json::to_string(content).unwrap_or_else(|_| "\"\"".into()); + format!( + r#" +(function() {{ + const container = document.getElementById('flynt-cm-editor'); + const cm = window._flyntCM; + if (!container || !cm || !cm.dom || !container.contains(cm.dom)) return false; + const next = {escaped}; + cm.dispatch({{ changes: {{ from: 0, to: cm.state.doc.length, insert: next }} }}); + cm.scrollDOM.scrollTop = 0; + return true; +}})(); +"# + ) +} + fn cm6_init_js(content: &str) -> String { let escaped = serde_json::to_string(content).unwrap_or_else(|_| "\"\"".into()); format!(