Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ name = "bee"
path = "src/lib.rs"

[features]
default = []
default = ["tui"]
# Real kernel enforcement. Pulls bee-userspace's eBPF-LSM path (nightly bpf toolchain + BPF-LSM
# kernel to build/attach). WITHOUT this feature the harness runs tools as hardened, credential-
# stripped host processes with NO scope — enough for offline (MockModel) tests and live-provider
Expand Down Expand Up @@ -179,10 +179,12 @@ rmcp = { version = "2.2", default-features = false, features = ["client", "trans
# still pins `ratatui ^0.29`, which cannot unify with the 0.30 the TUI is built on, so cargo would
# resolve a *second* ratatui whose `Text`/`Buffer` are different types — widgets from one cannot
# render into the other's buffer. `tui-markdown` 0.3.8 moved to `ratatui-core ^0.1`, which is exactly
# what ratatui 0.30 is built on, so there is one ratatui in the tree. `default-features = false`
# drops `highlight-code` (and its `syntect` + `ansi-to-tui` deps): bee colors code blocks from the
# active theme's roles (005-themes), not from a second, theme-blind highlighter.
tui-markdown = { version = "0.3.8", default-features = false, optional = true }
# what ratatui 0.30 is built on, so there is one ratatui in the tree. `highlight-code` (syntect)
# colors recognized fenced blocks; 005's objection — a second, theme-blind highlighter — is answered
# in `tui::markdown`, which picks the bundled code theme nearest the active bee theme and strips the
# highlighter's colors under `NO_COLOR`. `default-features = false` + the explicit feature keeps the
# list deliberate rather than whatever the crate's default grows into.
tui-markdown = { version = "0.3.8", default-features = false, features = ["highlight-code"], optional = true }

serde = { workspace = true }
serde_json = { workspace = true }
Expand Down
3 changes: 2 additions & 1 deletion src/app/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ pub struct ReplArgs {
pub no_bee: bool,
/// Color theme (built-in name or a custom one from config). Overrides `BEE_THEME` and the
/// config file. Built-ins: honeycomb (default), catppuccin-{mocha,latte,frappe,macchiato},
/// dracula, nord.
/// dracula, nord, gruvbox, tokyo-night, rose-pine — or `auto` to pick a flavor from the
/// terminal's detected light/dark background.
#[arg(long)]
pub theme: Option<String>,
/// Standalone MCP config TOML (top-level `[mcp]` + `[[mcp.servers]]`). Requires building with
Expand Down
2 changes: 1 addition & 1 deletion src/app/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub struct RunArgs {
#[arg(long)]
pub quiet: bool,
/// Color theme (built-in name or a custom one from config). Overrides `BEE_THEME` and the
/// config file.
/// config file. `auto` picks a flavor from the terminal's detected light/dark background.
#[arg(long)]
pub theme: Option<String>,
/// How much screen the agent may claim: `none`, `panels`, `panels-wide`, or `takeover`.
Expand Down
184 changes: 182 additions & 2 deletions src/render_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use std::sync::{Arc, Mutex};
use rhai::{Array, Dynamic, Engine, EvalAltResult};

use crate::render_spec::{
AnimationSpec, Bar, Direction, Dot, DotState, EffectSpec, GridCell, PanelOp, Point, RenderSpec,
Renderable, Row, Series, SpriteSpec,
AnimationSpec, Bar, Direction, Dot, DotState, EffectSpec, GridCell, HeatRow, PanelOp, Point,
RenderSpec, Renderable, Row, Series, SpriteSpec,
};
use crate::visual_gate;

Expand Down Expand Up @@ -229,6 +229,8 @@ fn validate_panel_id(id: &str) -> Result<(), Box<EvalAltResult>> {
enum ChartKind {
Bar,
Line,
Scatter,
Area,
Spark,
}

Expand Down Expand Up @@ -282,6 +284,29 @@ impl ChartBuilder {
})
.collect(),
},
ChartKind::Scatter => RenderSpec::ScatterPlot {
title: self.title.clone(),
series: self
.series
.iter()
.map(|(label, pts)| Series {
label: label.clone(),
points: pts.lock().expect("series").clone(),
})
.collect(),
},
ChartKind::Area => RenderSpec::AreaChart {
title: self.title.clone(),
series: self
.series
.iter()
.map(|(label, pts)| Series {
label: label.clone(),
points: pts.lock().expect("series").clone(),
})
.collect(),
color: self.color.clone(),
},
ChartKind::Spark => RenderSpec::Sparkline {
title: self.title.clone(),
data: self.spark.clone(),
Expand All @@ -296,6 +321,25 @@ pub struct SeriesHandle {
points: Arc<Mutex<Vec<Point>>>,
}

/// Builder behind `heatmap`.
#[derive(Clone)]
pub struct HeatmapBuilder {
title: String,
rows: Vec<HeatRow>,
color: Option<String>,
effect: Option<EffectSpec>,
}

impl HeatmapBuilder {
fn to_spec(&self) -> RenderSpec {
RenderSpec::Heatmap {
title: self.title.clone(),
rows: self.rows.clone(),
color: self.color.clone(),
}
}
}

/// Builder behind `table`.
#[derive(Clone)]
pub struct TableBuilder {
Expand Down Expand Up @@ -359,6 +403,27 @@ impl DotGridBuilder {
}
}

/// Builder behind `log_tail`.
#[derive(Clone)]
pub struct LogTailBuilder {
title: String,
lines: Vec<crate::render_spec::LogLine>,
max_rows: Option<u16>,
/// The transition the agent attached with `widget.effect(e)` (009 FR-022).
/// `None` means the default transition for the target, never "no animation".
effect: Option<EffectSpec>,
}

impl LogTailBuilder {
fn to_spec(&self) -> RenderSpec {
RenderSpec::LogTail {
title: self.title.clone(),
lines: self.lines.clone(),
max_rows: self.max_rows,
}
}
}

/// Builder behind `text`.
#[derive(Clone)]
pub struct TextBuilder {
Expand Down Expand Up @@ -596,6 +661,13 @@ fn dynamic_to_renderable(d: Dynamic) -> Result<Renderable, Box<EvalAltResult>> {
effect: b.effect,
});
}
if d.is::<HeatmapBuilder>() {
let b = d.cast::<HeatmapBuilder>();
return Ok(Renderable {
spec: b.to_spec(),
effect: b.effect,
});
}
if d.is::<TableBuilder>() {
let b = d.cast::<TableBuilder>();
return Ok(Renderable {
Expand All @@ -617,6 +689,13 @@ fn dynamic_to_renderable(d: Dynamic) -> Result<Renderable, Box<EvalAltResult>> {
effect: b.effect,
});
}
if d.is::<LogTailBuilder>() {
let b = d.cast::<LogTailBuilder>();
return Ok(Renderable {
spec: b.to_spec(),
effect: b.effect,
});
}
if d.is::<TextBuilder>() {
let b = d.cast::<TextBuilder>();
return Ok(Renderable {
Expand Down Expand Up @@ -686,6 +765,7 @@ pub fn register(engine: &mut Engine, ctx: RenderContext) {
engine.register_type_with_name::<TableBuilder>("Table");
engine.register_type_with_name::<GaugeBuilder>("Gauge");
engine.register_type_with_name::<DotGridBuilder>("DotGrid");
engine.register_type_with_name::<LogTailBuilder>("LogTail");
engine.register_type_with_name::<TextBuilder>("Text");
engine.register_type_with_name::<LayoutBuilder>("Layout");
engine.register_type_with_name::<RenderSpec>("Widget");
Expand All @@ -698,6 +778,12 @@ pub fn register(engine: &mut Engine, ctx: RenderContext) {
engine.register_fn("line_chart", |title: String| {
ChartBuilder::new(ChartKind::Line, title)
});
engine.register_fn("scatter", |title: String| {
ChartBuilder::new(ChartKind::Scatter, title)
});
engine.register_fn("area_chart", |title: String| {
ChartBuilder::new(ChartKind::Area, title)
});
engine.register_fn("sparkline", |title: String, data: Array| {
let mut c = ChartBuilder::new(ChartKind::Spark, title);
c.spark = array_to_u64(data);
Expand All @@ -723,6 +809,42 @@ pub fn register(engine: &mut Engine, ctx: RenderContext) {
engine.register_fn("point", |s: &mut SeriesHandle, x: f64, y: f64| {
s.points.lock().expect("series").push(Point { x, y });
});
engine.register_fn("point", |s: &mut SeriesHandle, x: i64, y: i64| {
s.points.lock().expect("series").push(Point {
x: x as f64,
y: y as f64,
});
});

// --- Heatmaps ---
engine.register_type_with_name::<HeatmapBuilder>("Heatmap");
engine.register_fn("heatmap", |title: String| HeatmapBuilder {
title,
rows: Vec::new(),
color: None,
effect: None,
});
engine.register_fn(
"row",
|h: &mut HeatmapBuilder, label: String, values: Array| {
h.rows.push(HeatRow {
label,
values: values
.into_iter()
.map(|v| {
if let Ok(f) = v.as_float() {
f
} else {
v.as_int().unwrap_or(0) as f64
}
})
.collect(),
});
},
);
engine.register_fn("color", |h: &mut HeatmapBuilder, name: String| {
h.color = Some(name);
});

// --- Tables ---
engine.register_fn("table", |title: String| TableBuilder {
Expand Down Expand Up @@ -762,6 +884,29 @@ pub fn register(engine: &mut Engine, ctx: RenderContext) {
engine.register_fn("color", |g: &mut GaugeBuilder, name: String| {
g.color = Some(name)
});
engine.register_fn("log_tail", |title: String| LogTailBuilder {
title,
lines: Vec::new(),
max_rows: None,
effect: None,
});
engine.register_fn("line", |l: &mut LogTailBuilder, text: String| {
l.lines
.push(crate::render_spec::LogLine { text, level: None });
});
engine.register_fn(
"line",
|l: &mut LogTailBuilder, text: String, level: String| {
l.lines.push(crate::render_spec::LogLine {
text,
level: Some(level),
});
},
);
engine.register_fn("max_rows", |l: &mut LogTailBuilder, n: i64| {
l.max_rows = Some(n.clamp(1, 40) as u16);
});

engine.register_fn("dots", |title: String| DotGridBuilder {
title,
dots: Vec::new(),
Expand Down Expand Up @@ -797,6 +942,9 @@ pub fn register(engine: &mut Engine, ctx: RenderContext) {
engine.register_fn("style", |t: &mut TextBuilder, name: String| {
t.style = Some(name)
});
engine.register_fn("color", |t: &mut TextBuilder, name: String| {
t.style = Some(name)
});
engine.register_fn("bold", |t: &mut TextBuilder| t.bold = true);
engine.register_fn("dim", |t: &mut TextBuilder| t.dim = true);
// Markdown the agent writes as source and bee renders through the active theme (010). One verb,
Expand Down Expand Up @@ -1079,8 +1227,10 @@ pub fn register(engine: &mut Engine, ctx: RenderContext) {
}
attach_effect!(
ChartBuilder,
HeatmapBuilder,
TableBuilder,
GaugeBuilder,
LogTailBuilder,
DotGridBuilder,
TextBuilder,
LayoutBuilder,
Expand Down Expand Up @@ -1184,6 +1334,36 @@ mod tests {
));
}

#[test]
fn log_tail_builds_lines_with_and_without_levels() {
let out = run(concat!(
r#"let l = log_tail("denials");"#,
r#" l.line("policy loaded");"#,
r#" l.line("open /etc/shadow blocked", "error");"#,
r#" l.max_rows(4);"#,
r#" render_to("audit", l);"#,
))
.unwrap();
let [PanelOp::Upsert { id, spec, .. }] = &out.panel_ops[..] else {
panic!("expected one upsert, got {:?}", out.panel_ops);
};
assert_eq!(id, "audit");
match spec {
RenderSpec::LogTail {
title,
lines,
max_rows,
} => {
assert_eq!(title, "denials");
assert_eq!(max_rows, &Some(4));
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].level, None);
assert_eq!(lines[1].level.as_deref(), Some("error"));
}
other => panic!("expected a log tail, got {other:?}"),
}
}

#[test]
fn render_commits_inline_and_no_panel_ops() {
let out = run(r#"render(text("hi"));"#).unwrap();
Expand Down
Loading
Loading