use super::{
apply_stack, clarity::ClarityLens, depth::DepthLens, focus::FocusLens, harmonic::HarmonicLens,
narrow::NarrowLens, refract::RefractLens, wide::WideLens, LensContext, LensOutput,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReadMode {
Auto,
Full,
Map,
Signatures,
Diff,
Aggressive,
Entropy,
Task,
Reference,
Truncate,
Lines { start: usize, end: usize },
}
impl ReadMode {
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"auto" => Some(Self::Auto),
"full" => Some(Self::Full),
"map" | "wide" => Some(Self::Map),
"signatures" | "refract" => Some(Self::Signatures),
"narrow" | "truncate" => Some(Self::Truncate),
"diff" | "depth" => Some(Self::Diff),
"aggressive" | "clarity" => Some(Self::Aggressive),
"entropy" | "harmonic" => Some(Self::Entropy),
"task" | "focus" => Some(Self::Task),
"reference" => Some(Self::Reference),
other if other.starts_with("lines:") => {
let range = &other["lines:".len()..];
let mut parts = range.splitn(2, '-');
let start: usize = parts.next()?.parse().ok()?;
let end: usize = parts.next()?.parse().ok()?;
if start == 0 || end < start {
return None;
}
Some(Self::Lines { start, end })
}
_ => None,
}
}
pub fn description(&self) -> &'static str {
match self {
Self::Auto => "Let the FilterMesh auto-select the best lens stack for the command",
Self::Full => "No compression — forward exact bytes unchanged",
Self::Map => "Structural overview: expand context, show directory/module layout",
Self::Signatures => "AST signatures only — strip all function bodies, keep types",
Self::Diff => "Diff-optimised: preserve structure and added/removed markers",
Self::Aggressive => "Maximum token reduction via Clarity + Narrow pipeline",
Self::Entropy => {
"Entropy-scored semantic clustering — keeps highest-information chunks"
}
Self::Task => "Task-conditioned relevance filter — keep lines relevant to current goal",
Self::Reference => "Import/type/doc layer — ideal for building reference context",
Self::Truncate => "Budget-based head+tail truncation — fits any output to token limit",
Self::Lines { .. } => "Extract an exact line range (1-based, inclusive)",
}
}
pub fn use_case(&self) -> &'static str {
match self {
Self::Auto => "Default — bctx picks the right mode based on the command",
Self::Full => "Debugging bctx itself; piping to another tool that needs raw output",
Self::Map => "Understanding an unfamiliar repo; building project structure context",
Self::Signatures => "Passing an API surface to an LLM; code review context preamble",
Self::Diff => "Reviewing a PR; analysing git diff output",
Self::Aggressive => "Token budget is tight; output is noisy logs or test output",
Self::Entropy => "Long prose/markdown files where most content is boilerplate",
Self::Task => "Agent has a specific task hint; filter output to what's relevant",
Self::Reference => "Building a context bundle of types and imports for an LLM prompt",
Self::Truncate => "Long log output or huge files that must fit a fixed token budget",
Self::Lines { .. } => "Focussing on a known region of a file (e.g. a failing function)",
}
}
pub fn lens_stack(&self) -> &'static str {
match self {
Self::Auto => "FilterMesh (dynamic)",
Self::Full => "none (passthrough)",
Self::Map => "Wide",
Self::Signatures => "Refract",
Self::Diff => "Depth → Clarity",
Self::Aggressive => "Clarity → Narrow",
Self::Entropy => "Harmonic",
Self::Task => "Focus",
Self::Reference => "Depth",
Self::Truncate => "Narrow",
Self::Lines { .. } => "line-range extractor",
}
}
pub fn savings_estimate(&self) -> &'static str {
match self {
Self::Auto => "varies",
Self::Full => "0%",
Self::Map => "0% (full context)",
Self::Signatures => "60–85%",
Self::Diff => "30–55%",
Self::Aggressive => "70–90%",
Self::Entropy => "50–75%",
Self::Task => "40–70%",
Self::Reference => "35–60%",
Self::Truncate => "20–80% (budget-dependent)",
Self::Lines { .. } => "depends on range",
}
}
pub fn all_named() -> &'static [ReadMode] {
use std::sync::OnceLock;
static ALL: OnceLock<Vec<ReadMode>> = OnceLock::new();
ALL.get_or_init(|| {
vec![
ReadMode::Auto,
ReadMode::Full,
ReadMode::Map,
ReadMode::Signatures,
ReadMode::Diff,
ReadMode::Aggressive,
ReadMode::Entropy,
ReadMode::Task,
ReadMode::Reference,
ReadMode::Truncate,
]
})
}
pub fn name(&self) -> String {
match self {
Self::Auto => "auto".into(),
Self::Full => "full".into(),
Self::Map => "map".into(),
Self::Signatures => "signatures".into(),
Self::Diff => "diff".into(),
Self::Aggressive => "aggressive".into(),
Self::Entropy => "entropy".into(),
Self::Task => "task".into(),
Self::Reference => "reference".into(),
Self::Truncate => "narrow".into(),
Self::Lines { start, end } => format!("lines:{start}-{end}"),
}
}
pub fn apply(&self, input: &str, ctx: &LensContext) -> LensOutput {
match self {
Self::Auto => LensOutput::passthrough(input),
Self::Full => LensOutput::passthrough(input),
Self::Map => apply_stack(&[Box::new(WideLens)], input, ctx),
Self::Signatures => apply_stack(&[Box::new(RefractLens)], input, ctx),
Self::Diff => apply_stack(&[Box::new(DepthLens), Box::new(ClarityLens)], input, ctx),
Self::Aggressive => {
apply_stack(&[Box::new(ClarityLens), Box::new(NarrowLens)], input, ctx)
}
Self::Entropy => apply_stack(&[Box::new(HarmonicLens)], input, ctx),
Self::Task => apply_stack(&[Box::new(FocusLens)], input, ctx),
Self::Reference => apply_stack(&[Box::new(DepthLens)], input, ctx),
Self::Truncate => apply_stack(&[Box::new(NarrowLens)], input, ctx),
Self::Lines { start, end } => extract_lines(input, *start, *end),
}
}
}
fn extract_lines(input: &str, start: usize, end: usize) -> LensOutput {
use forge::budget::estimator::TokenEstimator;
let tokens_before = TokenEstimator::count_nonblocking(input);
let extracted: String = input
.lines()
.enumerate()
.filter(|(i, _)| {
let line_no = i + 1;
line_no >= start && line_no <= end
})
.map(|(_, line)| line)
.collect::<Vec<_>>()
.join("\n");
let tokens_after = TokenEstimator::count_nonblocking(&extracted);
LensOutput {
content: extracted,
tokens_before,
tokens_after,
applied: vec!["lines".into()],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_all_named_modes() {
assert_eq!(ReadMode::parse("auto"), Some(ReadMode::Auto));
assert_eq!(ReadMode::parse("full"), Some(ReadMode::Full));
assert_eq!(ReadMode::parse("map"), Some(ReadMode::Map));
assert_eq!(ReadMode::parse("signatures"), Some(ReadMode::Signatures));
assert_eq!(ReadMode::parse("diff"), Some(ReadMode::Diff));
assert_eq!(ReadMode::parse("aggressive"), Some(ReadMode::Aggressive));
assert_eq!(ReadMode::parse("entropy"), Some(ReadMode::Entropy));
assert_eq!(ReadMode::parse("task"), Some(ReadMode::Task));
assert_eq!(ReadMode::parse("reference"), Some(ReadMode::Reference));
assert_eq!(ReadMode::parse("narrow"), Some(ReadMode::Truncate));
assert_eq!(ReadMode::parse("truncate"), Some(ReadMode::Truncate));
}
#[test]
fn parse_lines_mode() {
assert_eq!(
ReadMode::parse("lines:10-50"),
Some(ReadMode::Lines { start: 10, end: 50 })
);
assert_eq!(
ReadMode::parse("lines:1-1"),
Some(ReadMode::Lines { start: 1, end: 1 })
);
}
#[test]
fn parse_lines_invalid_returns_none() {
assert_eq!(ReadMode::parse("lines:0-10"), None); assert_eq!(ReadMode::parse("lines:50-10"), None); assert_eq!(ReadMode::parse("lines:abc"), None);
}
#[test]
fn parse_unknown_returns_none() {
assert_eq!(ReadMode::parse("unknown"), None);
assert_eq!(ReadMode::parse(""), None);
}
#[test]
fn parse_is_case_insensitive() {
assert_eq!(ReadMode::parse("SIGNATURES"), Some(ReadMode::Signatures));
assert_eq!(ReadMode::parse("Aggressive"), Some(ReadMode::Aggressive));
}
#[test]
fn name_round_trips() {
let modes = [
ReadMode::Auto,
ReadMode::Full,
ReadMode::Map,
ReadMode::Signatures,
ReadMode::Diff,
ReadMode::Aggressive,
ReadMode::Entropy,
ReadMode::Task,
ReadMode::Reference,
ReadMode::Truncate,
ReadMode::Lines { start: 5, end: 20 },
];
for mode in &modes {
let name = mode.name();
if !matches!(mode, ReadMode::Lines { .. }) {
assert_eq!(
ReadMode::parse(&name),
Some(mode.clone()),
"failed for {name}"
);
}
}
}
#[test]
fn lines_mode_extracts_correct_range() {
let input = "line1\nline2\nline3\nline4\nline5";
let ctx = LensContext::new(2000);
let out = ReadMode::Lines { start: 2, end: 4 }.apply(input, &ctx);
assert_eq!(out.content, "line2\nline3\nline4");
}
#[test]
fn lines_mode_out_of_bounds_clamps_gracefully() {
let input = "line1\nline2";
let ctx = LensContext::new(2000);
let out = ReadMode::Lines { start: 1, end: 100 }.apply(input, &ctx);
assert_eq!(out.content, "line1\nline2");
}
#[test]
fn full_mode_is_passthrough() {
let input = "hello world";
let ctx = LensContext::new(2000);
let out = ReadMode::Full.apply(input, &ctx);
assert_eq!(out.content, input);
assert_eq!(out.applied, Vec::<String>::new());
}
}