From 30171141fe638c5de77cef15bf918f529cc0136f Mon Sep 17 00:00:00 2001 From: Josh Long Date: Mon, 23 Feb 2026 22:15:11 -0500 Subject: [PATCH 1/3] chore: bump 0.2.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6eeaa9a..a332eaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,7 +34,7 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "ambits" -version = "0.10.0" +version = "0.11.0" dependencies = [ "clap", "color-eyre", diff --git a/Cargo.toml b/Cargo.toml index 70e571c..a78b99d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ambits" -version = "0.10.0" +version = "0.11.0" edition = "2021" description = "command line utility for symbols used by Claude agents" repository = "https://github.com/joshLong145/ambits" From 829e258cc9697b6547c3265558e43d396725f620 Mon Sep 17 00:00:00 2001 From: o-k-a-y <22223097+o-k-a-y@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:47:08 -0500 Subject: [PATCH 2/3] feat: vibecheck integration with attribution panel --- Cargo.lock | 272 ++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 7 +- src/app.rs | 45 ++++++-- src/main.rs | 15 +++ src/ui/mod.rs | 2 + src/ui/stats.rs | 83 ++++++++++++++- 6 files changed, 405 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a332eaa..52e1953 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,7 @@ dependencies = [ "tree-sitter-python", "tree-sitter-rust", "tree-sitter-typescript", + "vibecheck-core", ] [[package]] @@ -105,6 +106,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "autocfg" version = "1.5.0" @@ -407,6 +414,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "divan" version = "0.1.21" @@ -594,6 +622,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" @@ -628,6 +662,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + [[package]] name = "indoc" version = "2.0.7" @@ -792,7 +836,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", ] [[package]] @@ -899,6 +943,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "owo-colors" version = "4.2.3" @@ -985,6 +1035,15 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "redb" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eca1e9d98d5a7e9002d0013e18d5a9b000aee942eb134883a82f06ebffb6c01" +dependencies = [ + "libc", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1003,6 +1062,17 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.12.2" @@ -1146,6 +1216,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ + "indexmap", "itoa", "memchr", "serde", @@ -1153,6 +1224,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1299,6 +1379,26 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -1308,6 +1408,47 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tracing" version = "0.1.44" @@ -1351,17 +1492,38 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.24.7" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5387dffa7ffc7d2dae12b50c6f7aab8ff79d6210147c6613561fc3d474c6f75" +checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" dependencies = [ "cc", "regex", "regex-syntax", + "serde_json", "streaming-iterator", "tree-sitter-language", ] +[[package]] +name = "tree-sitter-go" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13d476345220dbe600147dd444165c5791bf85ef53e28acbedd46112ee18431" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-javascript" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf40bf599e0416c16c125c3cec10ee5ddc7d1bb8b0c60fa5c4de249ad34dc1b1" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-language" version = "0.1.7" @@ -1370,9 +1532,9 @@ checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" [[package]] name = "tree-sitter-python" -version = "0.23.6" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04" +checksum = "6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c" dependencies = [ "cc", "tree-sitter-language", @@ -1380,9 +1542,9 @@ dependencies = [ [[package]] name = "tree-sitter-rust" -version = "0.23.3" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8ccb3e3a3495c8a943f6c3fd24c3804c471fd7f4f16087623c7fa4c0068e8a" +checksum = "4b9b18034c684a2420722be8b2a91c9c44f2546b631c039edf575ccba8c61be1" dependencies = [ "cc", "tree-sitter-language", @@ -1457,6 +1619,27 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vibecheck-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e749353fdad910d6141cf5e2ce572b5936dffa9ed5d04c5a89bfacc2e04223" +dependencies = [ + "anyhow", + "dirs", + "ignore", + "redb", + "serde", + "serde_json", + "sha2", + "toml", + "tree-sitter", + "tree-sitter-go", + "tree-sitter-javascript", + "tree-sitter-python", + "tree-sitter-rust", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -1564,6 +1747,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -1600,6 +1792,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -1633,6 +1840,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1645,6 +1858,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -1657,6 +1876,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1681,6 +1906,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -1693,6 +1924,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -1705,6 +1942,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -1717,6 +1960,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -1729,6 +1978,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index a78b99d..e1834c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,9 @@ exclude = [ [dependencies] ratatui = "0.29" crossterm = "0.28" -tree-sitter = "0.24" -tree-sitter-rust = "0.23" -tree-sitter-python = "0.23" +tree-sitter = "0.25" +tree-sitter-rust = "0.24" +tree-sitter-python = "0.25" tree-sitter-typescript = "0.23" ignore = "0.4" notify = "7" @@ -27,6 +27,7 @@ sha2 = "0.10" color-eyre = "0.6" serde-pickle = "1.2" flume = "0.12" +vibecheck-core = "0.5.0" [dev-dependencies] tempfile = "3" diff --git a/src/app.rs b/src/app.rs index 75df54d..933866d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,8 +1,10 @@ +use std::collections::HashMap; use std::fs::File; use std::io::{BufWriter, Write}; use std::path::{Path, PathBuf}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}; +use vibecheck_core::report::{Attribution, Report}; use crate::coverage::count_symbols; use crate::symbols::{ProjectTree, SymbolNode}; @@ -44,6 +46,8 @@ pub struct TreeRow { pub coverage_status: Option, pub file_coverage_seen: usize, pub file_coverage_total: usize, + /// Model attribution for this row, populated from vibecheck analysis. + pub attribution: Option, } /// Which panel is focused. @@ -87,6 +91,9 @@ pub struct App { // Sort mode for tree view. pub sort_mode: SortMode, + // Whether to show the vibecheck attribution panel (experimental). + pub show_attribution: bool, + // Search. pub search_mode: bool, pub search_query: String, @@ -96,6 +103,9 @@ pub struct App { // Optional event log writer. pub event_log: Option>, + + /// Model attribution data per file: absolute path → Report. + pub attributions: HashMap, } impl App { @@ -123,10 +133,12 @@ impl App { agent_selection_index: 0, focus: FocusPanel::Tree, sort_mode: SortMode::Alphabetical, + show_attribution: false, search_mode: false, search_query: String::new(), session_id: None, event_log, + attributions: HashMap::new(), }; app.rebuild_tree_rows(); app @@ -186,6 +198,21 @@ impl App { ReadDepth::Unseen }; + // Look up model attribution for this file. + let abs_path = self.project_root.join(&file.file_path); + let report = self.attributions.get(&abs_path); + let file_vibecheck = report.map(|r| r.attribution.clone()); + + // Build a name → Attribution lookup for symbols within this file. + let sym_attributions: HashMap = report + .and_then(|r| r.symbol_reports.as_ref()) + .map(|syms| { + syms.iter() + .map(|sr| (sr.metadata.name.clone(), sr.attribution.clone())) + .collect() + }) + .unwrap_or_default(); + rows.push(TreeRow { symbol_id: file_id.clone(), display_name: file_path.clone(), @@ -200,11 +227,12 @@ impl App { coverage_status: Some(status), file_coverage_seen: seen, file_coverage_total: total, + attribution: file_vibecheck, }); if is_expanded { for sym in &file.symbols { - flatten_symbol(sym, 1, &self.collapsed, &self.ledger, agent_filter, &mut rows); + flatten_symbol(sym, 1, &self.collapsed, &self.ledger, &sym_attributions, &mut rows); } } } @@ -258,6 +286,9 @@ impl App { }; self.rebuild_tree_rows(); } + KeyCode::Char('v') => { + self.show_attribution = !self.show_attribution; + } KeyCode::Char('a') => self.cycle_agent_filter(), KeyCode::Char('A') => self.cycle_agent_filter_backward(), KeyCode::Tab => self.cycle_focus(), @@ -572,14 +603,13 @@ fn flatten_symbol( depth: usize, collapsed: &std::collections::HashSet, ledger: &ContextLedger, - agent_filter: Option<&str>, + sym_attributions: &HashMap, rows: &mut Vec, ) { let is_expanded = !collapsed.contains(&sym.id); - let read_depth = match agent_filter { - Some(agent_id) => ledger.depth_of_for_agent(&sym.id, agent_id), - None => ledger.depth_of(&sym.id), - }; + let read_depth = ledger.depth_of(&sym.id); + + let attribution = sym_attributions.get(&sym.name).cloned(); rows.push(TreeRow { symbol_id: sym.id.clone(), @@ -595,11 +625,12 @@ fn flatten_symbol( coverage_status: None, file_coverage_seen: 0, file_coverage_total: 0, + attribution, }); if is_expanded { for child in &sym.children { - flatten_symbol(child, depth + 1, collapsed, ledger, agent_filter, rows); + flatten_symbol(child, depth + 1, collapsed, ledger, sym_attributions, rows); } } } diff --git a/src/main.rs b/src/main.rs index 59d1593..acc7314 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ use ambits::app::App; use events::AppEvent; use ambits::parser::ParserRegistry; use ambits::symbols::ProjectTree; +use vibecheck_core::report::Report; #[derive(ClapParser, Debug)] #[command(name = "ambits", about = "Visualize LLM agent context coverage")] @@ -120,6 +121,18 @@ fn main() -> Result<()> { scan_project(&project_path, ®istry)? }; + // Analyze each file for model attribution. Results are keyed by absolute path. + // vibecheck uses a content-addressed on-disk cache (SHA-256 → Report stored in redb), + // so only files that changed since the last run incur analysis cost. + let attributions: std::collections::HashMap = project_tree + .files + .iter() + .filter_map(|f| { + let abs = project_path.join(&f.file_path); + vibecheck_core::analyze_file_symbols(&abs).ok().map(|r| (abs, r)) + }) + .collect(); + if cli.dump { dump_tree(&project_path, &project_tree); return Ok(()); @@ -162,6 +175,8 @@ fn main() -> Result<()> { let mut app = App::new(project_tree, project_path.clone(), event_log); app.session_id = session_id.clone(); + app.attributions = attributions; + app.rebuild_tree_rows(); // Pre-populate the ledger from existing session logs. if let (Some(ref log_dir), Some(ref session_id)) = (&log_dir, &session_id) { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7fbb95d..611ccf6 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -60,6 +60,8 @@ fn render_status_bar(f: &mut Frame, app: &App, area: ratatui::layout::Rect) { }), Span::styled("[a/A]", Style::default().fg(Color::DarkGray)), Span::raw("gents "), + Span::styled("[v]", Style::default().fg(Color::DarkGray)), + Span::raw("ibecheck "), Span::styled("[tab]", Style::default().fg(Color::DarkGray)), Span::raw("focus "), ]; diff --git a/src/ui/stats.rs b/src/ui/stats.rs index c2ce2e2..6ea740d 100644 --- a/src/ui/stats.rs +++ b/src/ui/stats.rs @@ -4,6 +4,7 @@ use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph}; +use vibecheck_core::report::{Attribution, ModelFamily}; use ambits::app::{App, FocusPanel}; use ambits::tracking::ReadDepth; @@ -17,7 +18,7 @@ pub fn render(f: &mut Frame, app: &App, area: Rect) { }; let block = Block::default() - .title(" Coverage Stats ") + .title(panel_title(app)) .borders(Borders::ALL) .border_style(border_style); @@ -181,9 +182,15 @@ pub fn render(f: &mut Frame, app: &App, area: Rect) { } } + if app.show_attribution { + let selected = app.tree_rows.get(app.selected_index).and_then(|r| r.attribution.as_ref()); + lines.push(Line::from("")); + lines.extend(attribution_lines(selected)); + } + // Scroll to keep the selected agent visible when the panel is focused. let visible_height = area.height.saturating_sub(2) as usize; // -2 for borders - let scroll_offset = if app.focus == FocusPanel::Stats && !app.agents_seen.is_empty() { + let scroll_offset = if !app.show_attribution && app.focus == FocusPanel::Stats && !app.agents_seen.is_empty() { // The agent list starts after the fixed header lines. // "All" entry is at header_lines, agents start at header_lines + 1. let header_lines = lines.len().saturating_sub(app.flattened_agents().len() + 1); // +1 for "All" @@ -221,6 +228,78 @@ fn short_id(id: &str) -> String { } } +/// Build the stats panel title, appending the name of each active overlay. +/// Add a new segment here whenever a new toggle is introduced. +fn panel_title(app: &App) -> String { + let mut title = String::from(" Coverage Stats"); + if app.show_attribution { + title.push_str(" · Attribution"); + } + title.push_str(" [v] "); + title +} + +fn family_color(family: ModelFamily) -> Color { + let (r, g, b) = family.rgb(); + Color::Rgb(r, g, b) +} + +fn attribution_lines(attribution: Option<&Attribution>) -> Vec> { + let Some(attr) = attribution else { + return vec![ + Line::from(vec![Span::styled( + " Attribution", + Style::default().fg(Color::White).add_modifier(Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + " No data for selection", + Style::default().fg(Color::DarkGray), + )]), + ]; + }; + + let primary_color = family_color(attr.primary); + let mut lines = vec![ + Line::from(vec![Span::styled( + " Attribution", + Style::default().fg(Color::White).add_modifier(Modifier::BOLD), + )]), + Line::from(vec![ + Span::raw(" "), + Span::styled( + format!("{}", attr.primary), + Style::default().fg(primary_color).add_modifier(Modifier::BOLD), + ), + Span::styled( + format!(" {:.0}% confident", attr.confidence * 100.0), + Style::default().fg(Color::DarkGray), + ), + ]), + Line::from(""), + ]; + + let mut scores: Vec<(ModelFamily, f64)> = + attr.scores.iter().map(|(&f, &s)| (f, s)).collect(); + scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + + for (family, score) in scores { + let pct = (score * 100.0).round() as u32; + let bar_len = (score * 12.0).round() as usize; + let bar = "█".repeat(bar_len); + let color = family_color(family); + lines.push(Line::from(vec![ + Span::styled( + format!(" {:<8}", format!("{family}")), + Style::default().fg(Color::DarkGray), + ), + Span::styled(format!("{bar:<12}"), Style::default().fg(color)), + Span::styled(format!("{pct:>3}%"), Style::default().fg(color)), + ])); + } + + lines +} + fn coverage_color(pct: u32) -> Color { match pct { 0..=20 => colors::PCT_LOW, From 48e73247a646ac52f816ea5716f3d150b8cda7cd Mon Sep 17 00:00:00 2001 From: o-k-a-y <22223097+o-k-a-y@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:41:18 -0500 Subject: [PATCH 3/3] fix: use per-agent depth in flatten_symbol when agent filter is active flatten_symbol was always using the aggregate depth_of(), ignoring the active agent filter. This caused symbol-level depth indicators in the tree view to show aggregate coverage even when filtering by a specific agent. Now passes agent_filter through and uses depth_of_for_agent() to match the behavior already present in count_symbols(). --- src/app.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 933866d..c112e7e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -232,7 +232,7 @@ impl App { if is_expanded { for sym in &file.symbols { - flatten_symbol(sym, 1, &self.collapsed, &self.ledger, &sym_attributions, &mut rows); + flatten_symbol(sym, 1, &self.collapsed, &self.ledger, agent_filter, &sym_attributions, &mut rows); } } } @@ -603,11 +603,15 @@ fn flatten_symbol( depth: usize, collapsed: &std::collections::HashSet, ledger: &ContextLedger, + agent_filter: Option<&str>, sym_attributions: &HashMap, rows: &mut Vec, ) { let is_expanded = !collapsed.contains(&sym.id); - let read_depth = ledger.depth_of(&sym.id); + let read_depth = match agent_filter { + Some(agent_id) => ledger.depth_of_for_agent(&sym.id, agent_id), + None => ledger.depth_of(&sym.id), + }; let attribution = sym_attributions.get(&sym.name).cloned(); @@ -630,7 +634,7 @@ fn flatten_symbol( if is_expanded { for child in &sym.children { - flatten_symbol(child, depth + 1, collapsed, ledger, sym_attributions, rows); + flatten_symbol(child, depth + 1, collapsed, ledger, agent_filter, sym_attributions, rows); } } }