From 09ce35ee6e8af1bfe091d975460fe7c440d5e5a0 Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 23 Mar 2026 14:48:30 +0100 Subject: [PATCH 1/4] refactor(skills): replace hardcoded relative path with symlink in zeph-skills Add crates/zeph-skills/skills -> ../../.zeph/skills symlink so build.rs and bundled.rs reference $CARGO_MANIFEST_DIR/skills instead of the brittle ../../.zeph/skills traversal. --- crates/zeph-skills/build.rs | 10 +++++----- crates/zeph-skills/skills | 1 + crates/zeph-skills/src/bundled.rs | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) create mode 120000 crates/zeph-skills/skills diff --git a/crates/zeph-skills/build.rs b/crates/zeph-skills/build.rs index 77be07ed..2898015f 100644 --- a/crates/zeph-skills/build.rs +++ b/crates/zeph-skills/build.rs @@ -5,15 +5,15 @@ fn main() { #[cfg(feature = "bundled-skills")] { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let skills_path = std::path::Path::new(&manifest_dir).join("../../.zeph/skills"); + let skills_path = std::path::Path::new(&manifest_dir).join("skills"); assert!( skills_path.exists(), - "bundled-skills feature is enabled but `.zeph/skills/` directory \ - was not found at `{}`. Ensure the workspace root contains the \ - `.zeph/skills/` directory before building with this feature.", + "bundled-skills feature is enabled but `skills/` symlink or directory \ + was not found at `{}`. Ensure `crates/zeph-skills/skills` points to the \ + workspace `.zeph/skills/` directory before building with this feature.", skills_path.display() ); // Re-run if skills dir changes. - println!("cargo:rerun-if-changed=../../.zeph/skills"); + println!("cargo:rerun-if-changed=skills"); } } diff --git a/crates/zeph-skills/skills b/crates/zeph-skills/skills new file mode 120000 index 00000000..14fdf38f --- /dev/null +++ b/crates/zeph-skills/skills @@ -0,0 +1 @@ +../../.zeph/skills \ No newline at end of file diff --git a/crates/zeph-skills/src/bundled.rs b/crates/zeph-skills/src/bundled.rs index 11d5b8aa..35f36428 100644 --- a/crates/zeph-skills/src/bundled.rs +++ b/crates/zeph-skills/src/bundled.rs @@ -26,7 +26,7 @@ use std::path::Path; use include_dir::{Dir, include_dir}; use tracing::{debug, info, warn}; -static BUNDLED_SKILLS_DIR: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/../../.zeph/skills"); +static BUNDLED_SKILLS_DIR: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skills"); /// Summary of a single provisioning run. #[derive(Debug, Default)] From 267c9a8eb8df3453665fe627f69e98e1bb6538cc Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 23 Mar 2026 15:39:24 +0100 Subject: [PATCH 2/4] =?UTF-8?q?feat(tui):=20Phase=201=20dynamic=20metrics?= =?UTF-8?q?=20=E2=80=94=20model=20info,=20session=20config,=20infra=20stat?= =?UTF-8?q?us?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 8 new fields to MetricsSnapshot: embedding_model, token_budget, compaction_threshold, vault_backend, active_channel, self_learning_enabled, cache_enabled, autosave_enabled. Populated at startup in runner.rs. Redesign Resources panel with LLM/Session/Infra grouped sections; collapse Session/Infra to summary lines when terminal height < 30. Status bar now shows active model name, replacing the low-value Panel toggle indicator. --- CHANGELOG.md | 4 + crates/zeph-core/src/metrics.rs | 18 ++ crates/zeph-tui/src/widgets/resources.rs | 225 ++++++++++++++---- ...ests__resources_with_extended_context.snap | 16 +- ...ces__tests__resources_with_full_infra.snap | 34 +++ ...urces__tests__resources_with_provider.snap | 14 +- ...s__status__tests__status_bar_snapshot.snap | 2 +- crates/zeph-tui/src/widgets/status.rs | 103 ++++---- src/runner.rs | 39 +++ 9 files changed, 350 insertions(+), 105 deletions(-) create mode 100644 crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__resources__tests__resources_with_full_infra.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 148662f6..72abafbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added + +- feat(tui): Phase 1 dynamic metrics in TUI — 8 new fields in `MetricsSnapshot` (`embedding_model`, `token_budget`, `compaction_threshold`, `vault_backend`, `active_channel`, `self_learning_enabled`, `cache_enabled`, `autosave_enabled`); Resources panel redesigned with LLM/Session/Infra grouped sections and overflow collapse at height < 30; status bar shows active model name replacing the low-value Panel toggle indicator + ### Fixed - ci: increase publish-crates timeout from 20 to 60 minutes and add `no-verify: true` to skip recompilation during publish (workspace has 21 crates; sequential publish with 15 s delays exceeded the previous limit) diff --git a/crates/zeph-core/src/metrics.rs b/crates/zeph-core/src/metrics.rs index 586c6b3c..0f49c0ef 100644 --- a/crates/zeph-core/src/metrics.rs +++ b/crates/zeph-core/src/metrics.rs @@ -267,6 +267,24 @@ pub struct MetricsSnapshot { pub tool_cache_entries: usize, /// Number of semantic-tier facts in memory (0 when tier promotion disabled). pub semantic_fact_count: u64, + + // --- Phase 1: dynamic config metrics --- + /// Embedding model name (e.g. "nomic-embed-text"). Empty when not configured. + pub embedding_model: String, + /// Configured max token budget for the context window. + pub token_budget: Option, + /// Token threshold that triggers soft compaction (0.0–1.0 ratio × budget). + pub compaction_threshold: Option, + /// Vault backend identifier: "age", "env", or "none". + pub vault_backend: String, + /// Active I/O channel: "cli", "telegram", or "tui". + pub active_channel: String, + /// Whether the self-learning engine is enabled for this session. + pub self_learning_enabled: bool, + /// Whether semantic response caching is enabled. + pub cache_enabled: bool, + /// Whether assistant messages are auto-saved to memory. + pub autosave_enabled: bool, } /// Strip ASCII control characters and ANSI escape sequences from a string for safe TUI display. diff --git a/crates/zeph-tui/src/widgets/resources.rs b/crates/zeph-tui/src/widgets/resources.rs index 260d7d4e..7e4ddaf0 100644 --- a/crates/zeph-tui/src/widgets/resources.rs +++ b/crates/zeph-tui/src/widgets/resources.rs @@ -9,59 +9,133 @@ use ratatui::widgets::{Block, Borders, Paragraph}; use crate::metrics::MetricsSnapshot; use crate::theme::Theme; +#[allow(clippy::too_many_lines)] pub fn render(metrics: &MetricsSnapshot, frame: &mut Frame, area: Rect) { let theme = Theme::default(); - let mut res_lines = vec![ - Line::from(format!(" Provider: {}", metrics.provider_name)), - Line::from(format!(" Model: {}", metrics.model_name)), - Line::from(format!(" Context: {}", metrics.context_tokens)), - Line::from(format!(" Session: {}", metrics.total_tokens)), - Line::from(format!(" API calls: {}", metrics.api_calls)), - Line::from(format!(" Latency: {}ms", metrics.last_llm_latency_ms)), - ]; + let collapsed = area.height < 30; + + let mut lines: Vec> = Vec::new(); + + // LLM section + lines.push(Line::from(" LLM")); + lines.push(Line::from(format!( + " Provider: {}", + metrics.provider_name + ))); + lines.push(Line::from(format!(" Model: {}", metrics.model_name))); + if !metrics.embedding_model.is_empty() { + lines.push(Line::from(format!( + " Embed: {}", + metrics.embedding_model + ))); + } + lines.push(Line::from(format!( + " Context: {} | Latency: {}ms", + metrics.context_tokens, metrics.last_llm_latency_ms + ))); if metrics.extended_context { - res_lines.push(Line::from(" Max context: 1M")); + lines.push(Line::from(" Max context: 1M")); } - if metrics.cache_creation_tokens > 0 || metrics.cache_read_tokens > 0 { - res_lines.push(Line::from(format!( - " Cache write: {}", - metrics.cache_creation_tokens + + // Session section + if collapsed { + lines.push(Line::from(format!( + " Session: {} tok | {} calls", + metrics.total_tokens, metrics.api_calls ))); - res_lines.push(Line::from(format!( - " Cache read: {}", - metrics.cache_read_tokens + } else { + lines.push(Line::from(" Session")); + lines.push(Line::from(format!( + " Tokens: {} | API: {}", + metrics.total_tokens, metrics.api_calls ))); + if let Some(budget) = metrics.token_budget { + if let Some(threshold) = metrics.compaction_threshold { + lines.push(Line::from(format!( + " Budget: {budget} | Compact: {threshold}" + ))); + } else { + lines.push(Line::from(format!(" Budget: {budget}"))); + } + } + if metrics.cache_creation_tokens > 0 || metrics.cache_read_tokens > 0 { + lines.push(Line::from(format!( + " Cache W:{} R:{}", + metrics.cache_creation_tokens, metrics.cache_read_tokens + ))); + } + if metrics.filter_applications > 0 { + #[allow(clippy::cast_precision_loss)] + let hit_pct = if metrics.filter_total_commands > 0 { + metrics.filter_filtered_commands as f64 / metrics.filter_total_commands as f64 + * 100.0 + } else { + 0.0 + }; + lines.push(Line::from(format!( + " Filter: {}/{} ({hit_pct:.0}% hit)", + metrics.filter_filtered_commands, metrics.filter_total_commands, + ))); + #[allow(clippy::cast_precision_loss)] + let pct = if metrics.filter_raw_tokens > 0 { + metrics.filter_saved_tokens as f64 / metrics.filter_raw_tokens as f64 * 100.0 + } else { + 0.0 + }; + lines.push(Line::from(format!( + " Filter saved: {} tok ({pct:.0}%)", + metrics.filter_saved_tokens, + ))); + } } - if metrics.filter_applications > 0 { - #[allow(clippy::cast_precision_loss)] - let hit_pct = if metrics.filter_total_commands > 0 { - metrics.filter_filtered_commands as f64 / metrics.filter_total_commands as f64 * 100.0 - } else { - 0.0 - }; - res_lines.push(Line::from(format!( - " Filter: {}/{} commands ({hit_pct:.0}% hit rate)", - metrics.filter_filtered_commands, metrics.filter_total_commands, - ))); - #[allow(clippy::cast_precision_loss)] - let pct = if metrics.filter_raw_tokens > 0 { - metrics.filter_saved_tokens as f64 / metrics.filter_raw_tokens as f64 * 100.0 - } else { - 0.0 - }; - res_lines.push(Line::from(format!( - " Filter saved: {} tok ({pct:.0}%)", - metrics.filter_saved_tokens, - ))); - res_lines.push(Line::from(format!( - " Confidence: F/{} P/{} B/{}", - metrics.filter_confidence_full, - metrics.filter_confidence_partial, - metrics.filter_confidence_fallback, - ))); + + // Infra section + if collapsed { + let mut infra_parts: Vec = Vec::new(); + if !metrics.vault_backend.is_empty() { + infra_parts.push(format!("vault:{}", metrics.vault_backend)); + } + if !metrics.active_channel.is_empty() { + infra_parts.push(format!("ch:{}", metrics.active_channel)); + } + if !infra_parts.is_empty() { + lines.push(Line::from(format!(" Infra: {}", infra_parts.join(" | ")))); + } + } else { + lines.push(Line::from(" Infra")); + match ( + metrics.vault_backend.as_str(), + metrics.active_channel.as_str(), + ) { + ("", "") => {} + (v, "") => lines.push(Line::from(format!(" Vault: {v}"))), + ("", c) => lines.push(Line::from(format!(" Channel: {c}"))), + (v, c) => lines.push(Line::from(format!(" Vault: {v} | Channel: {c}"))), + } + + let mut flags: Vec<&str> = Vec::new(); + if metrics.self_learning_enabled { + flags.push("Learning: ON"); + } + if metrics.cache_enabled { + flags.push("Cache: ON"); + } + if metrics.autosave_enabled { + flags.push("Autosave: ON"); + } + if !flags.is_empty() { + lines.push(Line::from(format!(" {}", flags.join(" | ")))); + } + if metrics.mcp_server_count > 0 { + lines.push(Line::from(format!( + " MCP: {} servers, {} tools", + metrics.mcp_server_count, metrics.mcp_tool_count + ))); + } } - let resources = Paragraph::new(res_lines).block( + + let resources = Paragraph::new(lines).block( Block::default() .borders(Borders::ALL) .border_style(theme.panel_border) @@ -89,7 +163,7 @@ mod tests { ..MetricsSnapshot::default() }; - let output = render_to_string(35, 10, |frame, area| { + let output = render_to_string(35, 12, |frame, area| { super::render(&metrics, frame, area); }); assert_snapshot!(output); @@ -108,7 +182,7 @@ mod tests { ..MetricsSnapshot::default() }; - let output = render_to_string(35, 11, |frame, area| { + let output = render_to_string(35, 13, |frame, area| { super::render(&metrics, frame, area); }); assert!( @@ -117,4 +191,63 @@ mod tests { ); assert_snapshot!(output); } + + #[test] + fn resources_with_full_infra() { + let metrics = MetricsSnapshot { + provider_name: "claude".into(), + model_name: "claude-sonnet-4-6".into(), + context_tokens: 10000, + total_tokens: 15000, + api_calls: 7, + last_llm_latency_ms: 180, + embedding_model: "nomic-embed-text".into(), + token_budget: Some(200_000), + compaction_threshold: Some(120_000), + vault_backend: "age".into(), + active_channel: "tui".into(), + self_learning_enabled: true, + cache_enabled: true, + autosave_enabled: true, + mcp_server_count: 2, + mcp_tool_count: 14, + ..MetricsSnapshot::default() + }; + + let output = render_to_string(40, 30, |frame, area| { + super::render(&metrics, frame, area); + }); + assert!( + output.contains("Vault: age"), + "expected vault backend; got: {output:?}" + ); + assert!( + output.contains("Channel: tui"), + "expected channel; got: {output:?}" + ); + assert!( + output.contains("Learning: ON"), + "expected learning flag; got: {output:?}" + ); + assert_snapshot!(output); + } + + #[test] + fn resources_collapsed_when_small_height() { + let metrics = MetricsSnapshot { + provider_name: "claude".into(), + model_name: "claude-sonnet-4-6".into(), + vault_backend: "age".into(), + active_channel: "tui".into(), + ..MetricsSnapshot::default() + }; + + let output = render_to_string(40, 20, |frame, area| { + super::render(&metrics, frame, area); + }); + assert!( + output.contains("vault:age"), + "collapsed mode should show vault inline; got: {output:?}" + ); + } } diff --git a/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__resources__tests__resources_with_extended_context.snap b/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__resources__tests__resources_with_extended_context.snap index 1e765cc7..d76411d2 100644 --- a/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__resources__tests__resources_with_extended_context.snap +++ b/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__resources__tests__resources_with_extended_context.snap @@ -3,13 +3,15 @@ source: crates/zeph-tui/src/widgets/resources.rs expression: output --- ┌ Resources ──────────────────────┐ -│ Provider: claude │ -│ Model: claude-sonnet-4-6 │ -│ Context: 50000 │ -│ Session: 75000 │ -│ API calls: 3 │ -│ Latency: 400ms │ -│ Max context: 1M │ +│ LLM │ +│ Provider: claude │ +│ Model: claude-sonnet-4-6 │ +│ Context: 50000 | Latency: 400│ +│ Max context: 1M │ +│ Session: 75000 tok | 3 calls │ +│ │ +│ │ +│ │ │ │ │ │ └─────────────────────────────────┘ diff --git a/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__resources__tests__resources_with_full_infra.snap b/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__resources__tests__resources_with_full_infra.snap new file mode 100644 index 00000000..1312d6fc --- /dev/null +++ b/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__resources__tests__resources_with_full_infra.snap @@ -0,0 +1,34 @@ +--- +source: crates/zeph-tui/src/widgets/resources.rs +expression: output +--- +┌ Resources ───────────────────────────┐ +│ LLM │ +│ Provider: claude │ +│ Model: claude-sonnet-4-6 │ +│ Embed: nomic-embed-text │ +│ Context: 10000 | Latency: 180ms │ +│ Session │ +│ Tokens: 15000 | API: 7 │ +│ Budget: 200000 | Compact: 120000 │ +│ Infra │ +│ Vault: age | Channel: tui │ +│ Learning: ON | Cache: ON | Autosav│ +│ MCP: 2 servers, 14 tools │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘ diff --git a/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__resources__tests__resources_with_provider.snap b/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__resources__tests__resources_with_provider.snap index c1389b19..6fb7e661 100644 --- a/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__resources__tests__resources_with_provider.snap +++ b/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__resources__tests__resources_with_provider.snap @@ -3,12 +3,14 @@ source: crates/zeph-tui/src/widgets/resources.rs expression: output --- ┌ Resources ──────────────────────┐ -│ Provider: claude │ -│ Model: opus-4 │ -│ Context: 8000 │ -│ Session: 12000 │ -│ API calls: 5 │ -│ Latency: 250ms │ +│ LLM │ +│ Provider: claude │ +│ Model: opus-4 │ +│ Context: 8000 | Latency: 250m│ +│ Session: 12000 tok | 5 calls │ +│ │ +│ │ +│ │ │ │ │ │ └─────────────────────────────────┘ diff --git a/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__status__tests__status_bar_snapshot.snap b/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__status__tests__status_bar_snapshot.snap index b54a56a6..9610d176 100644 --- a/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__status__tests__status_bar_snapshot.snap +++ b/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__status__tests__status_bar_snapshot.snap @@ -2,4 +2,4 @@ source: crates/zeph-tui/src/widgets/status.rs expression: output --- - [Insert] | Panel: ON | Skills: 2/5 | Tokens: 4.2k | qdrant: OK | API: 12 | 2m 15s + [Insert] | Skills: 2/5 | Tokens: 4.2k | qdrant: OK | API: 12 | 2m 15s diff --git a/crates/zeph-tui/src/widgets/status.rs b/crates/zeph-tui/src/widgets/status.rs index bcee131a..a0e88e92 100644 --- a/crates/zeph-tui/src/widgets/status.rs +++ b/crates/zeph-tui/src/widgets/status.rs @@ -21,10 +21,58 @@ pub fn render(app: &App, metrics: &MetricsSnapshot, frame: &mut Frame, area: Rec let uptime = format_uptime(metrics.uptime_seconds); - let panel = if app.show_side_panels() { "ON" } else { "OFF" }; + let plan_mode_segment = plan_mode_segment(app, metrics); + let cancel_hint = if app.is_agent_busy() && app.input_mode() == InputMode::Normal { + " | [Esc to cancel]" + } else { + "" + }; + + let qdrant_segment = if metrics.qdrant_available { + format!(" | {}: OK", metrics.vector_backend) + } else { + String::new() + }; + + let filter_segment = build_filter_segment(metrics); + + let main_text = format!( + " [{mode}]{model}{plan_mode_segment} | Skills: {active}/{total} | Tokens: {tok}{qdrant_segment}{filter_segment}", + model = if metrics.model_name.is_empty() { + String::new() + } else { + format!(" | {}", metrics.model_name) + }, + active = metrics.active_skills.len(), + total = metrics.total_skills, + tok = format_tokens(metrics.total_tokens), + ); + + let mut spans: Vec> = vec![Span::styled(main_text, theme.status_bar)]; + append_security_spans(&mut spans, metrics, &theme); + if metrics.server_compaction_events > 0 { + spans.push(Span::styled(" | ", theme.status_bar)); + spans.push(Span::styled( + format!("[SC: {}]", metrics.server_compaction_events), + Style::default().fg(Color::Cyan), + )); + } + + let suffix = format!( + " | API: {api} | {uptime}{cancel_hint}", + api = metrics.api_calls + ); + spans.push(Span::styled(suffix, theme.status_bar)); + + let line = Line::from(spans); + let paragraph = Paragraph::new(line).style(theme.status_bar); + frame.render_widget(paragraph, area); +} + +fn plan_mode_segment<'a>(app: &App, metrics: &MetricsSnapshot) -> &'a str { // MF3: show current side-panel mode when a plan graph is active. - let plan_mode_segment = if metrics + if metrics .orchestration_graph .as_ref() .is_some_and(|s| !s.is_stale()) @@ -36,22 +84,12 @@ pub fn render(app: &App, metrics: &MetricsSnapshot, frame: &mut Frame, area: Rec } } else { "" - }; - - let cancel_hint = if app.is_agent_busy() && app.input_mode() == InputMode::Normal { - " | [Esc to cancel]" - } else { - "" - }; - - let qdrant_segment = if metrics.qdrant_available { - format!(" | {}: OK", metrics.vector_backend) - } else { - String::new() - }; + } +} - #[allow(clippy::cast_precision_loss)] - let filter_segment = if metrics.filter_applications > 0 { +#[allow(clippy::cast_precision_loss)] +fn build_filter_segment(metrics: &MetricsSnapshot) -> String { + if metrics.filter_applications > 0 { let savings = if metrics.filter_raw_tokens > 0 { metrics.filter_saved_tokens as f64 / metrics.filter_raw_tokens as f64 * 100.0 } else { @@ -63,17 +101,10 @@ pub fn render(app: &App, metrics: &MetricsSnapshot, frame: &mut Frame, area: Rec ) } else { String::new() - }; - - let main_text = format!( - " [{mode}] | Panel: {panel}{plan_mode_segment} | Skills: {active}/{total} | Tokens: {tok}{qdrant_segment}{filter_segment}", - active = metrics.active_skills.len(), - total = metrics.total_skills, - tok = format_tokens(metrics.total_tokens), - ); - - let mut spans: Vec> = vec![Span::styled(main_text, theme.status_bar)]; + } +} +fn append_security_spans(spans: &mut Vec>, metrics: &MetricsSnapshot, theme: &Theme) { let injection_flags = metrics.sanitizer_injection_flags; let exfil_total = metrics.exfiltration_images_blocked + metrics.exfiltration_tool_urls_flagged @@ -108,24 +139,6 @@ pub fn render(app: &App, metrics: &MetricsSnapshot, frame: &mut Frame, area: Rec }; spans.push(Span::styled(label, Style::default().fg(color))); } - - if metrics.server_compaction_events > 0 { - spans.push(Span::styled(" | ", theme.status_bar)); - spans.push(Span::styled( - format!("[SC: {}]", metrics.server_compaction_events), - Style::default().fg(Color::Cyan), - )); - } - - let suffix = format!( - " | API: {api} | {uptime}{cancel_hint}", - api = metrics.api_calls, - ); - spans.push(Span::styled(suffix, theme.status_bar)); - - let line = Line::from(spans); - let paragraph = Paragraph::new(line).style(theme.status_bar); - frame.render_widget(paragraph, area); } #[allow(clippy::cast_precision_loss)] diff --git a/src/runner.rs b/src/runner.rs index 991c169d..6e5c2e1d 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1243,8 +1243,47 @@ pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> { let (metrics_tx, metrics_rx) = tokio::sync::watch::channel(zeph_core::metrics::MetricsSnapshot::default()); + // Determine active channel name for metrics. + #[cfg(feature = "tui")] + let active_channel_name = if tui_active { + "tui" + } else if config + .telegram + .as_ref() + .and_then(|t| t.token.as_ref()) + .is_some() + { + "telegram" + } else { + "cli" + }; + #[cfg(not(feature = "tui"))] + let active_channel_name = if config + .telegram + .as_ref() + .and_then(|t| t.token.as_ref()) + .is_some() + { + "telegram" + } else { + "cli" + }; + metrics_tx.send_modify(|m| { config.llm.effective_model().clone_into(&mut m.model_name); + embed_model.clone_into(&mut m.embedding_model); + m.token_budget = u32::try_from(budget_tokens).ok(); + m.compaction_threshold = u32::try_from(budget_tokens).ok().map(|b| { + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let threshold = + (f64::from(b) * f64::from(config.memory.soft_compaction_threshold)) as u32; + threshold + }); + config.vault.backend.clone_into(&mut m.vault_backend); + active_channel_name.clone_into(&mut m.active_channel); + m.self_learning_enabled = config.skills.learning.enabled; + m.cache_enabled = config.llm.semantic_cache_enabled; + m.autosave_enabled = config.memory.autosave_assistant; }); #[cfg(all(feature = "tui", feature = "scheduler"))] let metrics_tx_for_sched = metrics_tx.clone(); From a9d03b0fbdf7ddff266ec58d71b4568a894afa5e Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 23 Mar 2026 15:53:59 +0100 Subject: [PATCH 3/4] fix(config): add [security.guardrail] section to default.toml for migration (#2158) --- config/default.toml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/config/default.toml b/config/default.toml index 6acb69da..e7649b30 100644 --- a/config/default.toml +++ b/config/default.toml @@ -518,6 +518,24 @@ sources = ["web_scrape", "a2a_message"] # (e.g. "claude", "ollama", "openai", or a compatible entry name) model = "claude" +[security.guardrail] +# Enable LLM-based input guardrail classification (default: false, requires guardrail feature) +enabled = false +# Provider to use for guardrail classification (e.g. "ollama", "claude") +# provider = "ollama" +# Model to use for guardrail (e.g. "llama-guard-3:1b") +# model = "llama-guard-3:1b" +# Timeout for each guardrail LLM call in milliseconds (default: 500) +timeout_ms = 500 +# Action when a message is flagged: "block" or "warn" (default: "block") +action = "block" +# What to do on timeout or LLM error: "closed" (block) or "open" (allow) (default: "closed") +fail_strategy = "closed" +# Also scan tool outputs before they enter message history (default: false) +scan_tool_output = false +# Maximum characters to send to the guard model (default: 4096) +max_input_chars = 4096 + # [telegram] # token = "your-bot-token" # Allowed usernames (empty = allow all except for /start command) From 2b5af719cc7e33711ee7f0b0dbfedeac131e1156 Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 23 Mar 2026 15:57:00 +0100 Subject: [PATCH 4/4] fix(config): remove duplicate [security.guardrail] section after merge --- config/default.toml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/config/default.toml b/config/default.toml index 1ae56ac9..25f03a2a 100644 --- a/config/default.toml +++ b/config/default.toml @@ -536,24 +536,6 @@ sources = ["web_scrape", "a2a_message"] # (e.g. "claude", "ollama", "openai", or a compatible entry name) model = "claude" -[security.guardrail] -# Enable LLM-based input guardrail classification (default: false, requires guardrail feature) -enabled = false -# Provider to use for guardrail classification (e.g. "ollama", "claude") -# provider = "ollama" -# Model to use for guardrail (e.g. "llama-guard-3:1b") -# model = "llama-guard-3:1b" -# Timeout for each guardrail LLM call in milliseconds (default: 500) -timeout_ms = 500 -# Action when a message is flagged: "block" or "warn" (default: "block") -action = "block" -# What to do on timeout or LLM error: "closed" (block) or "open" (allow) (default: "closed") -fail_strategy = "closed" -# Also scan tool outputs before they enter message history (default: false) -scan_tool_output = false -# Maximum characters to send to the guard model (default: 4096) -max_input_chars = 4096 - # [telegram] # token = "your-bot-token" # Allowed usernames (empty = allow all except for /start command)