From 8942d8c2b63b75a6668227f393bc65b7ea2868db Mon Sep 17 00:00:00 2001 From: jaberjaber23 Date: Tue, 3 Mar 2026 05:20:05 +0300 Subject: [PATCH] bugfixes release --- Cargo.lock | 28 +-- Cargo.toml | 2 +- crates/openfang-api/src/channel_bridge.rs | 60 ++--- crates/openfang-api/src/routes.rs | 103 +++++++- crates/openfang-api/static/css/components.css | 115 +++++++++ crates/openfang-api/static/index_body.html | 48 +++- .../openfang-api/static/js/pages/channels.js | 11 +- crates/openfang-api/static/js/pages/chat.js | 90 +++++++ crates/openfang-channels/src/router.rs | 46 +++- crates/openfang-cli/src/main.rs | 19 +- crates/openfang-cli/src/tui/chat_runner.rs | 179 +++++++++++++- crates/openfang-cli/src/tui/mod.rs | 180 +++++++++++++- crates/openfang-cli/src/tui/screens/chat.rs | 227 +++++++++++++++++- crates/openfang-hands/bundled/clip/HAND.toml | 7 + .../openfang-hands/bundled/twitter/HAND.toml | 7 + 15 files changed, 1046 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2246fa3..9ba5e5dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3866,7 +3866,7 @@ dependencies = [ [[package]] name = "openfang-api" -version = "0.3.2" +version = "0.3.3" dependencies = [ "async-trait", "axum", @@ -3902,7 +3902,7 @@ dependencies = [ [[package]] name = "openfang-channels" -version = "0.3.2" +version = "0.3.3" dependencies = [ "async-trait", "axum", @@ -3933,7 +3933,7 @@ dependencies = [ [[package]] name = "openfang-cli" -version = "0.3.2" +version = "0.3.3" dependencies = [ "clap", "clap_complete", @@ -3960,7 +3960,7 @@ dependencies = [ [[package]] name = "openfang-desktop" -version = "0.3.2" +version = "0.3.3" dependencies = [ "axum", "open", @@ -3986,7 +3986,7 @@ dependencies = [ [[package]] name = "openfang-extensions" -version = "0.3.2" +version = "0.3.3" dependencies = [ "aes-gcm", "argon2", @@ -4014,7 +4014,7 @@ dependencies = [ [[package]] name = "openfang-hands" -version = "0.3.2" +version = "0.3.3" dependencies = [ "chrono", "dashmap", @@ -4031,7 +4031,7 @@ dependencies = [ [[package]] name = "openfang-kernel" -version = "0.3.2" +version = "0.3.3" dependencies = [ "async-trait", "chrono", @@ -4067,7 +4067,7 @@ dependencies = [ [[package]] name = "openfang-memory" -version = "0.3.2" +version = "0.3.3" dependencies = [ "async-trait", "chrono", @@ -4086,7 +4086,7 @@ dependencies = [ [[package]] name = "openfang-migrate" -version = "0.3.2" +version = "0.3.3" dependencies = [ "chrono", "dirs 6.0.0", @@ -4105,7 +4105,7 @@ dependencies = [ [[package]] name = "openfang-runtime" -version = "0.3.2" +version = "0.3.3" dependencies = [ "anyhow", "async-trait", @@ -4137,7 +4137,7 @@ dependencies = [ [[package]] name = "openfang-skills" -version = "0.3.2" +version = "0.3.3" dependencies = [ "chrono", "hex", @@ -4160,7 +4160,7 @@ dependencies = [ [[package]] name = "openfang-types" -version = "0.3.2" +version = "0.3.3" dependencies = [ "async-trait", "chrono", @@ -4179,7 +4179,7 @@ dependencies = [ [[package]] name = "openfang-wire" -version = "0.3.2" +version = "0.3.3" dependencies = [ "async-trait", "chrono", @@ -8791,7 +8791,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xtask" -version = "0.3.2" +version = "0.3.3" [[package]] name = "yoke" diff --git a/Cargo.toml b/Cargo.toml index a4fbf6e3..0fa20a1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -version = "0.3.3" +version = "0.3.4" edition = "2021" license = "Apache-2.0 OR MIT" repository = "https://github.com/RightNow-AI/openfang" diff --git a/crates/openfang-api/src/channel_bridge.rs b/crates/openfang-api/src/channel_bridge.rs index c7a29da0..6d30b191 100644 --- a/crates/openfang-api/src/channel_bridge.rs +++ b/crates/openfang-api/src/channel_bridge.rs @@ -934,30 +934,6 @@ fn parse_trigger_pattern(s: &str) -> Option { - router.set_default(agent_id); - info!("{adapter_name} default agent: {name} ({agent_id})"); - } - _ => match handle.spawn_agent_by_name(name).await { - Ok(agent_id) => { - router.set_default(agent_id); - info!("{adapter_name}: spawned default agent {name} ({agent_id})"); - } - Err(e) => { - warn!("{adapter_name}: could not find or spawn default agent '{name}': {e}"); - } - }, - } -} - /// Read a token from an env var, returning None with a warning if missing/empty. fn read_token(env_var: &str, adapter_name: &str) -> Option { match std::env::var(env_var) { @@ -1545,12 +1521,40 @@ pub async fn start_channel_bridge_with_config( return (None, Vec::new()); } - // Resolve default agent from first adapter that has one configured + // Resolve per-channel default agents AND set the first one as system-wide fallback let mut router = AgentRouter::new(); - for (_, default_agent) in &adapters { + let mut system_default_set = false; + for (adapter, default_agent) in &adapters { if let Some(ref name) = default_agent { - resolve_default_agent(&handle, name, &mut router, "Channel bridge").await; - break; // Only need one default + // Resolve agent name to ID + let agent_id = match handle.find_agent_by_name(name).await { + Ok(Some(id)) => Some(id), + _ => match handle.spawn_agent_by_name(name).await { + Ok(id) => Some(id), + Err(e) => { + warn!( + "{}: could not find or spawn default agent '{}': {e}", + adapter.name(), + name + ); + None + } + }, + }; + if let Some(agent_id) = agent_id { + // Register per-channel default + let channel_key = format!("{:?}", adapter.channel_type()); + info!( + "{} default agent: {name} ({agent_id}) [channel: {channel_key}]", + adapter.name() + ); + router.set_channel_default(channel_key, agent_id); + // First configured default also becomes system-wide fallback + if !system_default_set { + router.set_default(agent_id); + system_default_set = true; + } + } } } diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index 06fde54f..4f7baf6d 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -1050,7 +1050,7 @@ pub async fn send_message_stream( // --------------------------------------------------------------------------- /// Field type for the channel configuration form. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq)] enum FieldType { Secret, Text, @@ -1763,12 +1763,16 @@ fn is_channel_configured(config: &openfang_types::config::ChannelsConfig, name: } /// Build a JSON field descriptor, checking env var presence but never exposing secrets. -fn build_field_json(f: &ChannelField) -> serde_json::Value { +/// For non-secret fields, includes the actual config value from `config_values` if available. +fn build_field_json( + f: &ChannelField, + config_values: Option<&serde_json::Value>, +) -> serde_json::Value { let has_value = f .env_var .map(|ev| std::env::var(ev).map(|v| !v.is_empty()).unwrap_or(false)) .unwrap_or(false); - serde_json::json!({ + let mut field = serde_json::json!({ "key": f.key, "label": f.label, "type": f.field_type.as_str(), @@ -1777,7 +1781,41 @@ fn build_field_json(f: &ChannelField) -> serde_json::Value { "has_value": has_value, "placeholder": f.placeholder, "advanced": f.advanced, - }) + }); + // For non-secret fields, include the actual saved config value so the + // dashboard can pre-populate forms when editing existing configs. + if f.env_var.is_none() { + if let Some(obj) = config_values.and_then(|v| v.as_object()) { + if let Some(val) = obj.get(f.key) { + // Convert arrays to comma-separated string for list fields + let display_val = if f.field_type == FieldType::List { + if let Some(arr) = val.as_array() { + serde_json::Value::String( + arr.iter() + .filter_map(|v| { + v.as_str() + .map(|s| s.to_string()) + .or_else(|| Some(v.to_string())) + }) + .collect::>() + .join(", "), + ) + } else { + val.clone() + } + } else { + val.clone() + }; + field["value"] = display_val; + if !val.is_null() + && val.as_str().map(|s| !s.is_empty()).unwrap_or(true) + { + field["has_value"] = serde_json::Value::Bool(true); + } + } + } + } + field } /// Find a channel definition by name. @@ -1785,6 +1823,56 @@ fn find_channel_meta(name: &str) -> Option<&'static ChannelMeta> { CHANNEL_REGISTRY.iter().find(|c| c.name == name) } +/// Serialize a channel's config to a JSON Value for pre-populating dashboard forms. +fn channel_config_values( + config: &openfang_types::config::ChannelsConfig, + name: &str, +) -> Option { + match name { + "telegram" => config.telegram.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "discord" => config.discord.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "slack" => config.slack.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "whatsapp" => config.whatsapp.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "signal" => config.signal.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "matrix" => config.matrix.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "email" => config.email.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "teams" => config.teams.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "mattermost" => config.mattermost.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "irc" => config.irc.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "google_chat" => config.google_chat.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "twitch" => config.twitch.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "rocketchat" => config.rocketchat.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "zulip" => config.zulip.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "xmpp" => config.xmpp.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "line" => config.line.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "viber" => config.viber.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "messenger" => config.messenger.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "reddit" => config.reddit.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "mastodon" => config.mastodon.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "bluesky" => config.bluesky.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "feishu" => config.feishu.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "revolt" => config.revolt.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "nextcloud" => config.nextcloud.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "guilded" => config.guilded.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "keybase" => config.keybase.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "threema" => config.threema.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "nostr" => config.nostr.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "webex" => config.webex.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "pumble" => config.pumble.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "flock" => config.flock.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "twist" => config.twist.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "mumble" => config.mumble.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "dingtalk" => config.dingtalk.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "discourse" => config.discourse.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "gitter" => config.gitter.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "ntfy" => config.ntfy.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "gotify" => config.gotify.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "webhook" => config.webhook.as_ref().and_then(|c| serde_json::to_value(c).ok()), + "linkedin" => config.linkedin.as_ref().and_then(|c| serde_json::to_value(c).ok()), + _ => None, + } +} + /// GET /api/channels — List all 40 channel adapters with status and field metadata. pub async fn list_channels(State(state): State>) -> impl IntoResponse { // Read the live channels config (updated on every hot-reload) instead of the @@ -1810,7 +1898,12 @@ pub async fn list_channels(State(state): State>) -> impl IntoRespo .unwrap_or(true) }); - let fields: Vec = meta.fields.iter().map(build_field_json).collect(); + let config_vals = channel_config_values(&live_channels, meta.name); + let fields: Vec = meta + .fields + .iter() + .map(|f| build_field_json(f, config_vals.as_ref())) + .collect(); channels.push(serde_json::json!({ "name": meta.name, diff --git a/crates/openfang-api/static/css/components.css b/crates/openfang-api/static/css/components.css index b13cc1d9..4d03b633 100644 --- a/crates/openfang-api/static/css/components.css +++ b/crates/openfang-api/static/css/components.css @@ -898,6 +898,121 @@ mark.search-highlight { .slash-menu-item:last-child { border-bottom: none; } .slash-menu-item:hover, .slash-menu-item.slash-active { background: var(--surface2); } +/* Model switcher dropdown */ +.model-switcher-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 10px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 20px; + color: var(--text-dim); + font-family: var(--font-mono); + font-size: 11px; + cursor: pointer; + max-width: 200px; + transition: all 0.15s; + white-space: nowrap; +} +.model-switcher-btn:hover { border-color: var(--accent); color: var(--text); } +.model-switcher-btn:disabled { opacity: 0.4; cursor: not-allowed; } +.model-switcher-btn:disabled:hover { border-color: var(--border); color: var(--text-dim); } +.model-switcher-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 150px; +} +.model-switcher-chevron { + transition: transform 0.2s; + flex-shrink: 0; + opacity: 0.5; +} +.model-switcher-chevron.open { transform: rotate(180deg); } +.model-switcher-dropdown { + position: absolute; + bottom: calc(100% + 6px); + left: 0; + width: 340px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + z-index: 100; + overflow: hidden; +} +.model-switcher-search { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-bottom: 1px solid var(--border); +} +.model-switcher-search input { + flex: 1; + background: none; + border: none; + color: var(--text); + font-family: var(--font-mono); + font-size: 12px; + outline: none; +} +.model-switcher-list { + max-height: 320px; + overflow-y: auto; + overscroll-behavior: contain; +} +.model-switcher-group-header { + position: sticky; + top: 0; + z-index: 1; + padding: 6px 12px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); + background: var(--surface2); + border-bottom: 1px solid var(--border); +} +.model-switcher-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + cursor: pointer; + transition: background 0.1s; +} +.model-switcher-item:hover { background: var(--surface2); } +.model-switcher-item.active { + background: var(--accent-subtle, rgba(255,92,0,0.06)); + cursor: default; +} +.model-switcher-item-name { + font-size: 12px; + font-weight: 500; + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.model-switcher-tier { + display: inline-block; + padding: 1px 5px; + border-radius: 8px; + font-size: 9px; + font-weight: 600; + letter-spacing: 0.3px; + text-transform: uppercase; + flex-shrink: 0; +} +.model-switcher-tier.tier-frontier { background: rgba(168,85,247,0.15); color: #a855f7; } +.model-switcher-tier.tier-smart { background: rgba(59,130,246,0.15); color: #3b82f6; } +.model-switcher-tier.tier-balanced { background: rgba(34,197,94,0.15); color: #22c55e; } +.model-switcher-tier.tier-fast { background: rgba(245,158,11,0.15); color: #f59e0b; } +.model-switcher-tier.tier-local { background: rgba(148,163,184,0.12); color: var(--text-dim); } + /* Sidebar footer */ .sidebar-footer { padding: 8px 0; diff --git a/crates/openfang-api/static/index_body.html b/crates/openfang-api/static/index_body.html index 0aed74f2..629e2b2f 100644 --- a/crates/openfang-api/static/index_body.html +++ b/crates/openfang-api/static/index_body.html @@ -746,9 +746,55 @@ - +