use iced::advanced::{image as advanced_image, text};
use iced::widget::{container, image, markdown};
use iced::{ContentFit, Element, Font, Length, Pixels, padding};
use std::borrow::Borrow;
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::hash::Hash;
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock, Mutex};
use std::time::SystemTime;
use super::selectable;
use crate::util::media_target::{self, MarkerTarget};
use crate::util::{
MEDIA_MARKER_RE, MediaMarkerKind, UnwrapPoison, file_name_or_path, parse_media_marker,
};
use ::image as image_crate;
pub(crate) struct MarkerContext<'a> {
pub workspace: Option<&'a Path>,
pub refusals: bool,
}
pub(crate) struct Preprocessed {
pub text: String,
pub files: Vec<FileMarker>,
}
pub(crate) fn preprocess(content: &str, markers: &MarkerContext<'_>) -> Preprocessed {
let s = replace_transcription(content);
let rendered = replace_media_markers(&s, markers);
Preprocessed {
text: replace_saved_annotations(&rendered.text),
files: rendered.files,
}
}
fn replace_transcription(s: &str) -> String {
static RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"\[(Audio|Video) transcription of [^\]]+\]:\s*")
.expect("transcription regex must compile")
});
RE.replace_all(s, |caps: ®ex::Captures| {
match caps.get(1).map(|m| m.as_str()) {
Some("Audio") => "🔊 ",
_ => "🎬 ",
}
})
.to_string()
}
fn replace_media_markers(s: &str, markers: &MarkerContext<'_>) -> Preprocessed {
let roots: Vec<PathBuf> = markers
.workspace
.map(Path::to_path_buf)
.into_iter()
.collect();
let mut files = Vec::new();
let text = MEDIA_MARKER_RE
.replace_all(s, |caps: ®ex::Captures| {
let (kind, target) = parse_media_marker(caps);
let target = target.trim();
render_marker(kind, target, markers, &roots, &mut files)
.unwrap_or_else(|| caps.get_match().as_str().to_string())
})
.to_string();
Preprocessed { text, files }
}
fn render_marker(
kind: MediaMarkerKind,
target: &str,
markers: &MarkerContext<'_>,
roots: &[PathBuf],
files: &mut Vec<FileMarker>,
) -> Option<String> {
if !markers.refusals {
return render_own_text(kind, target, files);
}
match media_target::resolve_marker_target(target, roots) {
MarkerTarget::NotAReference => None,
MarkerTarget::Inline => Some(match kind {
MediaMarkerKind::Image => inline_image(target),
MediaMarkerKind::Audio | MediaMarkerKind::Video | MediaMarkerKind::File => {
media_target::inline_marker_refusal(kind)
}
}),
MarkerTarget::Remote => Some(match media_target::remote_marker(kind, target) {
media_target::RemoteMarker::Refused(notice) => notice,
media_target::RemoteMarker::Deliverable => {
delivered_marker(kind, target, target, files)
}
}),
MarkerTarget::Local(path) => Some(match media_target::local_refusal(kind, &path) {
Some(reason) => media_target::marker_refusal_for(target, reason),
None => delivered_marker(kind, target, &path.to_string_lossy(), files),
}),
}
}
fn delivered_marker(
kind: MediaMarkerKind,
target: &str,
resolved: &str,
files: &mut Vec<FileMarker>,
) -> String {
match kind {
MediaMarkerKind::Image => image_item(resolved),
MediaMarkerKind::Audio | MediaMarkerKind::Video => media_label(kind, target),
MediaMarkerKind::File => push_file_marker(target, resolved.to_string(), files),
}
}
fn render_own_text(
kind: MediaMarkerKind,
target: &str,
files: &mut Vec<FileMarker>,
) -> Option<String> {
match kind {
MediaMarkerKind::Image => {
if target.starts_with("data:")
|| target.starts_with(crate::util::DATA_URI_OMITTED_PREFIX)
{
Some(inline_image(target))
} else if media_target::is_valid_remote_url(target) || Path::new(target).is_absolute() {
Some(image_item(target))
} else {
None
}
}
MediaMarkerKind::Audio | MediaMarkerKind::Video => Some(media_label(kind, target)),
MediaMarkerKind::File => {
let click_target = if target.starts_with("data:")
|| target.starts_with(crate::util::DATA_URI_OMITTED_PREFIX)
{
String::new()
} else {
target.to_string()
};
Some(push_file_marker(target, click_target, files))
}
}
}
fn media_label(kind: MediaMarkerKind, target: &str) -> String {
match kind {
MediaMarkerKind::Audio => format!("🎵 {}", file_name_or_path(target)),
_ => format!("🎬 Video: {}", file_name_or_path(target)),
}
}
fn push_file_marker(target: &str, click_target: String, files: &mut Vec<FileMarker>) -> String {
let label = file_marker_label(target);
files.push(FileMarker {
label: label.clone(),
target: click_target,
});
label
}
fn inline_image(target: &str) -> String {
if target.starts_with(crate::util::DATA_URI_OMITTED_PREFIX) {
"🖼️ image".to_string()
} else {
image_item(target)
}
}
fn image_item(target: &str) -> String {
if target
.chars()
.any(|character| character.is_whitespace() || matches!(character, '(' | ')'))
{
format!("")
} else {
format!("")
}
}
fn file_marker_label(path: &str) -> String {
let path = path.trim();
if path.starts_with("data:") || path.starts_with(crate::util::DATA_URI_OMITTED_PREFIX) {
"📄 file".to_string()
} else {
format!("📄 {}", file_name_or_path(path))
}
}
fn replace_saved_annotations(s: &str) -> String {
static SAVED_IMAGE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"\n*\[Saved image: [^\]]*\]")
.expect("saved image annotation regex must compile")
});
static SAVED_VIDEO_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"\[Saved video: ([^\]]*)\]")
.expect("saved video annotation regex must compile")
});
let s = SAVED_IMAGE_RE.replace_all(s, "").to_string();
SAVED_VIDEO_RE
.replace_all(&s, |caps: ®ex::Captures| {
let path = caps.get(1).map_or("", |m| m.as_str());
format!("🎬 Video: {}", file_name_or_path(path))
})
.to_string()
}
const MAX_IMAGE_LONGEST_SIDE_PX: u32 = 2048;
fn downscale_raster(width: u32, height: u32, rgba: Vec<u8>) -> Option<(u32, u32, Vec<u8>)> {
if width <= MAX_IMAGE_LONGEST_SIDE_PX && height <= MAX_IMAGE_LONGEST_SIDE_PX {
return Some((width, height, rgba));
}
let img = image_crate::DynamicImage::ImageRgba8(image_crate::RgbaImage::from_raw(
width, height, rgba,
)?);
let resized = img
.thumbnail(MAX_IMAGE_LONGEST_SIDE_PX, MAX_IMAGE_LONGEST_SIDE_PX)
.into_rgba8();
Some((resized.width(), resized.height(), resized.into_raw()))
}
#[must_use]
fn render_raster(bytes: &[u8]) -> Option<(u32, u32, Vec<u8>)> {
let (width, height, rgba) =
media_target::decode_raster_bytes(bytes, media_target::raster_decode_limits())?;
downscale_raster(width, height, rgba)
}
#[must_use]
fn decode_data_uri_image(uri: &str) -> Option<(u32, u32, Vec<u8>)> {
let (width, height, rgba) =
media_target::decode_native_data_uri(uri, media_target::raster_decode_limits())?;
downscale_raster(width, height, rgba)
}
struct BoundedLruCache<K, V> {
entries: HashMap<K, V>,
lru: VecDeque<K>,
cap: usize,
}
impl<K, V> BoundedLruCache<K, V>
where
K: Hash + Eq + Clone,
{
fn with_capacity(cap: usize) -> Self {
Self {
entries: HashMap::new(),
lru: VecDeque::new(),
cap,
}
}
fn get<Q>(&mut self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let hit = self.entries.get(key);
if hit.is_some() {
if let Some(pos) = self.lru.iter().position(|k| k.borrow() == key) {
let key = self.lru.remove(pos).expect("position found by iteration");
self.lru.push_back(key);
}
}
hit
}
fn insert(&mut self, key: K, value: V) {
if let Some(pos) = self.lru.iter().position(|k| k == &key) {
self.lru.remove(pos);
}
self.entries.insert(key.clone(), value);
self.lru.push_back(key);
while self.entries.len() > self.cap {
let oldest = self
.lru
.pop_front()
.expect("lru mirrors entries, so a front key always exists");
self.entries.remove(&oldest);
}
}
}
const MAX_CACHED_IMAGES: usize = 32;
#[derive(Debug)]
enum CachedImage {
Decoded(image::Handle),
Failed,
}
impl CachedImage {
fn from_raster(raster: Option<(u32, u32, Vec<u8>)>) -> Self {
match raster {
Some((width, height, rgba)) => {
Self::Decoded(image::Handle::from_rgba(width, height, rgba))
}
None => Self::Failed,
}
}
fn handle(&self) -> Option<image::Handle> {
match self {
Self::Decoded(handle) => Some(handle.clone()),
Self::Failed => None,
}
}
}
static DATA_URI_IMAGE_CACHE: LazyLock<Mutex<BoundedLruCache<String, CachedImage>>> =
LazyLock::new(|| Mutex::new(BoundedLruCache::with_capacity(MAX_CACHED_IMAGES)));
#[must_use]
fn cached_image<K, Q>(
cache: &Mutex<BoundedLruCache<K, CachedImage>>,
key: &Q,
decode: impl FnOnce() -> Option<(u32, u32, Vec<u8>)>,
) -> Option<image::Handle>
where
K: Hash + Eq + Clone + Borrow<Q>,
Q: Hash + Eq + ToOwned<Owned = K> + ?Sized,
{
if let Some(cached) = cache.lock().unwrap_poison().get(key) {
return cached.handle();
}
let outcome = CachedImage::from_raster(decode());
let handle = outcome.handle();
cache.lock().unwrap_poison().insert(key.to_owned(), outcome);
handle
}
#[must_use]
fn data_uri_image_handle(uri: &str) -> Option<image::Handle> {
media_target::data_uri_base64_payload(uri)?;
cached_image(&DATA_URI_IMAGE_CACHE, uri, || decode_data_uri_image(uri))
}
static LOCAL_IMAGE_CACHE: LazyLock<Mutex<BoundedLruCache<(PathBuf, SystemTime), CachedImage>>> =
LazyLock::new(|| Mutex::new(BoundedLruCache::with_capacity(MAX_CACHED_IMAGES)));
#[must_use]
fn local_path_image_handle(path: &str) -> Option<image::Handle> {
let p = Path::new(path);
if !p.is_absolute() {
return None;
}
let meta = std::fs::metadata(p).ok()?;
if !meta.is_file() || meta.len() > crate::util::INBOUND_IMAGE_MAX_INPUT_BYTES {
return None;
}
let mtime = meta.modified().ok()?;
let canonical = std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
cached_image(&LOCAL_IMAGE_CACHE, &(canonical.clone(), mtime), || {
std::fs::read(&canonical)
.ok()
.and_then(|b| render_raster(&b))
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FileMarker {
pub label: String,
pub target: String,
}
const FILE_LINK_PREFIX: &str = "mahbot-file:";
pub(crate) fn file_link_target(uri: &str) -> Option<&str> {
uri.strip_prefix(FILE_LINK_PREFIX)
}
fn file_link_uri(target: &str) -> String {
format!("{FILE_LINK_PREFIX}{target}")
}
fn resolve_file_marker_target(target: &str, workspace: Option<&Path>) -> Result<PathBuf, String> {
if target.is_empty() {
return Err("The marker does not name a file.".to_string());
}
if media_target::is_valid_remote_url(target) {
return Err(format!(
"Not a local file: {}",
media_target::marker_name(target)
));
}
let raw = Path::new(target);
if raw.is_absolute() {
return Ok(raw.to_path_buf());
}
workspace
.map(|root| root.join(raw))
.ok_or_else(|| format!("Cannot open '{target}': the conversation's workspace is unknown."))
}
pub(crate) fn reveal_file_marker(target: &str, workspace: Option<&Path>) -> Result<(), String> {
let path = resolve_file_marker_target(target, workspace)?;
if !path.exists() {
return Err(format!("File not found: {}", path.display()));
}
if !path.is_file() {
return Err(format!("Not a file: {}", path.display()));
}
super::reveal_in_file_manager(&path)
}
struct SelectableMediaViewer<'a> {
files: &'a [FileMarker],
linked: RefCell<Vec<bool>>,
}
pub(crate) fn selectable_markdown_view<'a>(
items: &'a [markdown::Item],
settings: impl Into<markdown::Settings>,
files: &'a [FileMarker],
) -> Element<'a, markdown::Uri, iced::Theme, iced::Renderer> {
markdown::view_with(items, settings, &SelectableMediaViewer::new(files))
}
impl<'a> SelectableMediaViewer<'a> {
fn new(files: &'a [FileMarker]) -> Self {
Self {
files,
linked: RefCell::new(vec![false; files.len()]),
}
}
fn marker_spans(
&self,
spans: &Arc<[text::Span<'static, markdown::Uri>]>,
) -> Arc<[text::Span<'static, markdown::Uri>]> {
self.linkified_spans(spans)
.map_or_else(|| spans.clone(), Arc::from)
}
fn linkified_spans(
&self,
spans: &[text::Span<'static, markdown::Uri>],
) -> Option<Vec<text::Span<'static, markdown::Uri>>> {
let mut linked = self.linked.borrow_mut();
if !self.files.iter().enumerate().any(|(index, file)| {
!linked[index]
&& spans
.iter()
.any(|span| can_link(span) && span.text.contains(&file.label))
}) {
return None;
}
let mut out = Vec::with_capacity(spans.len() + self.files.len());
for span in spans {
if !can_link(span) {
out.push(span.clone());
continue;
}
let mut rest: &str = span.text.as_ref();
let mut split = false;
while let Some((at, index)) = self
.files
.iter()
.enumerate()
.filter(|(i, _)| !linked[*i])
.filter_map(|(i, file)| rest.find(&file.label).map(|at| (at, i)))
.min_by_key(|(at, i)| (*at, std::cmp::Reverse(self.files[*i].label.len())))
{
split = true;
if at > 0 {
out.push(span_with_text(span, &rest[..at]));
}
let label = &self.files[index].label;
let mut link = span_with_text(span, label);
link.link = Some(file_link_uri(&self.files[index].target));
out.push(link);
linked[index] = true;
rest = &rest[at + label.len()..];
}
if !split {
out.push(span.clone());
} else if !rest.is_empty() {
out.push(span_with_text(span, rest));
}
}
Some(out)
}
}
fn can_link(span: &text::Span<'static, markdown::Uri>) -> bool {
span.link.is_none() && span.highlight.is_none()
}
fn span_with_text(
span: &text::Span<'static, markdown::Uri>,
text: &str,
) -> text::Span<'static, markdown::Uri> {
text::Span {
text: std::borrow::Cow::Owned(text.to_owned()),
..span.clone()
}
}
impl<'a, Theme, Renderer> markdown::Viewer<'a, markdown::Uri, Theme, Renderer>
for SelectableMediaViewer<'a>
where
Theme: markdown::Catalog + 'a,
Renderer: text::Renderer<Paragraph = iced::advanced::graphics::text::Paragraph, Font = Font>
+ advanced_image::Renderer<Handle = image::Handle>
+ 'a,
{
fn on_link_click(url: markdown::Uri) -> markdown::Uri {
url
}
fn image(
&self,
settings: markdown::Settings,
url: &'a markdown::Uri,
_title: &'a str,
_alt: &markdown::Text,
) -> Element<'a, markdown::Uri, Theme, Renderer> {
let url_str = url.as_str();
if url_str.starts_with("data:") {
return match data_uri_image_handle(url_str) {
Some(handle) => image::Image::new(handle)
.width(Length::Fill)
.content_fit(ContentFit::Contain)
.into(),
None => iced::widget::text("🖼️ image")
.size(settings.text_size)
.into(),
};
}
if let Some(handle) = local_path_image_handle(url_str) {
image::Image::new(handle)
.width(Length::Fill)
.content_fit(ContentFit::Contain)
.into()
} else {
let filename = file_name_or_path(url_str);
iced::widget::text(format!("🖼️ {filename}"))
.size(settings.text_size)
.into()
}
}
fn heading(
&self,
settings: markdown::Settings,
level: &'a markdown::HeadingLevel,
text: &'a markdown::Text,
index: usize,
) -> Element<'a, markdown::Uri, Theme, Renderer> {
let size = match level {
markdown::HeadingLevel::H1 => settings.h1_size,
markdown::HeadingLevel::H2 => settings.h2_size,
markdown::HeadingLevel::H3 => settings.h3_size,
markdown::HeadingLevel::H4 => settings.h4_size,
markdown::HeadingLevel::H5 => settings.h5_size,
markdown::HeadingLevel::H6 => settings.h6_size,
};
let spans = text.spans(settings.style);
let rich = selectable::rich_text(self.marker_spans(&spans)).size(size);
container(rich.on_link_click(|url| url))
.padding(padding::top(if index > 0 {
settings.text_size / 2.0
} else {
Pixels::ZERO
}))
.into()
}
fn paragraph(
&self,
settings: markdown::Settings,
text: &markdown::Text,
) -> Element<'a, markdown::Uri, Theme, Renderer> {
let spans = text.spans(settings.style);
selectable::rich_text(self.marker_spans(&spans))
.size(settings.text_size)
.on_link_click(|url| url)
.into()
}
fn code_block(
&self,
settings: markdown::Settings,
_language: Option<&'a str>,
_code: &'a str,
lines: &'a [markdown::Text],
) -> Element<'a, markdown::Uri, Theme, Renderer> {
selectable::markdown::code_block(settings, lines, |url| url)
}
fn unordered_list(
&self,
settings: markdown::Settings,
bullets: &'a [markdown::Bullet],
) -> Element<'a, markdown::Uri, Theme, Renderer> {
selectable::markdown::unordered_list(self, settings, bullets)
}
fn ordered_list(
&self,
settings: markdown::Settings,
start: u64,
bullets: &'a [markdown::Bullet],
) -> Element<'a, markdown::Uri, Theme, Renderer> {
selectable::markdown::ordered_list(self, settings, start, bullets)
}
}
#[cfg(test)]
mod tests {
use super::*;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use std::io::Cursor;
fn agent(workspace: &Path) -> MarkerContext<'_> {
MarkerContext {
workspace: Some(workspace),
refusals: true,
}
}
fn owner(workspace: &Path) -> MarkerContext<'_> {
MarkerContext {
workspace: Some(workspace),
refusals: false,
}
}
#[expect(clippy::too_many_lines)] #[test]
fn preprocess_table() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let photo = root.join("photo.png");
let spaced = root.join("my file.png");
let recording = root.join("recording.ogg");
let nested = root.join("dir/subdir/rec.ogg");
let clip = root.join("video.mp4");
let report = root.join("generated/report.pdf");
std::fs::write(&photo, tiny_png(2, 1)).unwrap();
std::fs::write(&spaced, tiny_png(2, 1)).unwrap();
std::fs::write(&recording, b"ogg").unwrap();
std::fs::create_dir_all(nested.parent().unwrap()).unwrap();
std::fs::write(&nested, b"ogg").unwrap();
std::fs::write(&clip, b"mp4").unwrap();
std::fs::create_dir_all(report.parent().unwrap()).unwrap();
std::fs::write(&report, b"pdf").unwrap();
std::fs::write(root.join("README"), b"readme").unwrap();
let photo_str = photo.to_string_lossy().into_owned();
let spaced_str = spaced.to_string_lossy().into_owned();
let valid_uri = data_uri(&tiny_png(2, 1));
let gone = root.join("cleaned_up.png");
let gone_str = gone.to_string_lossy().into_owned();
let root_name = root.file_name().unwrap().to_string_lossy().into_owned();
let cases: Vec<(&str, String, String)> = vec![
(
"resolves a workspace-relative image marker",
"Look [IMAGE:photo.png] here".into(),
format!("Look  here"),
),
(
"uses an absolute image marker as it is",
format!("Look [IMAGE:{photo_str}] here"),
format!("Look  here"),
),
(
"delivers markers written in lowercase",
"img [image:photo.png] hear [audio:recording.ogg]".into(),
format!("img  hear 🎵 recording.ogg"),
),
(
"refuses a lowercase marker it cannot deliver instead of showing it",
"see [image:cleaned_up.png]".into(),
"see Could not send \"cleaned_up.png\": file not found.".into(),
),
(
"resolves a relative image marker with spaces in the path, quoted for the markdown parser",
"img [IMAGE:my file.png]".into(),
format!("img "),
),
(
"reports a relative image marker whose file is gone",
"Look [IMAGE:cleaned_up.png] here".into(),
"Look Could not send \"cleaned_up.png\": file not found. here".into(),
),
(
"reports an absolute image marker whose file is gone instead of drawing a placeholder",
format!("Look [IMAGE:{gone_str}] here"),
"Look Could not send \"cleaned_up.png\": file not found. here".into(),
),
(
"reports a directory image target",
format!("[IMAGE:{}]", root.display()),
format!("Could not send \"{root_name}\": it is a directory."),
),
(
"reports a target that is not an image",
{
let text = root.join("notes.txt");
std::fs::write(&text, b"not an image").unwrap();
format!("[IMAGE:{}]", text.display())
},
"Could not send \"notes.txt\": it is not an image.".into(),
),
(
"renders the omitted-data-URI placeholder as the image affordance, not raw marker text",
format!(
"Look [IMAGE:{} (35 bytes)>] here",
crate::util::DATA_URI_OMITTED_PREFIX
),
"Look 🖼️ image here".into(),
),
(
"renders an inline image payload through the viewer",
format!("Look [IMAGE:{valid_uri}] here"),
format!("Look  here"),
),
(
"keeps a bare word of prose as written",
"use `[IMAGE:path]` or `[VIDEO:...]` here".into(),
"use `[IMAGE:path]` or `[VIDEO:...]` here".into(),
),
(
"resolves a relative audio marker",
"Listen [AUDIO:recording.ogg]".into(),
"Listen 🎵 recording.ogg".into(),
),
(
"resolves a relative audio marker in a nested path",
"hear [AUDIO:dir/subdir/rec.ogg]".into(),
"hear 🎵 rec.ogg".into(),
),
(
"reports a missing audio marker",
"Listen [AUDIO:gone.ogg]".into(),
"Listen Could not send \"gone.ogg\": file not found.".into(),
),
(
"resolves a relative video marker",
"Watch [VIDEO:video.mp4]".into(),
"Watch 🎬 Video: video.mp4".into(),
),
(
"names a file marker's resolved target",
"Get [FILE:generated/report.pdf]".into(),
"Get 📄 report.pdf".into(),
),
(
"delivers a workspace file whose name is a bare word",
"Get [FILE:README]".into(),
"Get 📄 README".into(),
),
(
"reports a file marker whose file is gone",
"Get [FILE:generated/gone.pdf]".into(),
"Get Could not send \"gone.pdf\": file not found.".into(),
),
(
"reports a file marker that names an address",
"Get [FILE:https://example.com/a.pdf]".into(),
"Get Could not send \"a.pdf\": an address is not a local file.".into(),
),
(
"refuses a file marker's inline payload instead of naming the blob",
"Get [FILE:data:text/plain;base64,aGVsbG8=]".into(),
"Get Could not send \"file\": it carries an inline payload, not a file.".into(),
),
(
"replaces an audio transcription block",
"[Audio transcription of recording.ogg]: Hello world".into(),
"🔊 Hello world".into(),
),
(
"renders every line of a multiline audio transcription under 🔊",
"[Audio transcription of voice.ogg]: Line one\nLine two".into(),
"🔊 Line one\nLine two".into(),
),
(
"renders every line of a multiline video transcription under 🎬 and leaves following message content untouched",
"[Video transcription of clip.mp4]: Line one\nLine two\n\nEdit this".into(),
"🎬 Line one\nLine two\n\nEdit this".into(),
),
(
"video-transcription format contains Video and is handled before the [VIDEO:...] pattern",
"[Video transcription of clip.mp4]: hi there [VIDEO:video.mp4]".into(),
"🎬 hi there 🎬 Video: video.mp4".into(),
),
(
"audio-transcription format contains Audio and is handled before the [AUDIO:...] pattern",
"[Audio transcription of msg.ogg]: hi there [AUDIO:recording.ogg]".into(),
"🔊 hi there 🎵 recording.ogg".into(),
),
(
"replaces mixed image/audio/video markers",
"![]() [IMAGE:photo.png] and [AUDIO:recording.ogg] end [VIDEO:video.mp4]".into(),
format!("![]()  and 🎵 recording.ogg end 🎬 Video: video.mp4"),
),
(
"leaves content without markers unchanged",
"Hello world".into(),
"Hello world".into(),
),
(
"returns the empty string unchanged",
String::new(),
String::new(),
),
(
"strips a saved image annotation and its blank-line separator",
format!("[IMAGE:{valid_uri}]\n\n[Saved image: /uploads/img.png]"),
format!(""),
),
(
"strips a saved image annotation at the start with no preceding separator",
"[Saved image: /uploads/img.png]".into(),
String::new(),
),
(
"turns a saved video annotation into a visible clip placeholder",
"Clip [Saved video: /uploads/clip.mp4] here".into(),
"Clip 🎬 Video: clip.mp4 here".into(),
),
(
"keeps the saved video clip visible alongside its transcription",
"[Video transcription of clip.mp4]: Line one\n\n[Saved video: /uploads/clip.mp4]"
.into(),
"🎬 Line one\n\n🎬 Video: clip.mp4".into(),
),
];
for (i, (name, input, expected)) in cases.iter().enumerate() {
assert_eq!(
preprocess(input, &agent(root)).text,
*expected,
"case {i} ({name})"
);
}
}
#[test]
fn own_text_keeps_the_structural_display() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let photo = root.join("photo.png");
std::fs::write(&photo, tiny_png(2, 1)).unwrap();
let photo_str = photo.to_string_lossy().into_owned();
assert_eq!(
preprocess("[IMAGE:generated/x.png] [VIDEO:clip.mp4]", &owner(root)).text,
"[IMAGE:generated/x.png] 🎬 Video: clip.mp4"
);
assert_eq!(
preprocess(
&format!("[IMAGE:{photo_str}] [IMAGE:/tmp/gone.png]"),
&owner(root)
)
.text,
format!(" ")
);
assert_eq!(
preprocess("[FILE:notes.txt]", &owner(root)).files,
vec![FileMarker {
label: "📄 notes.txt".to_string(),
target: "notes.txt".to_string(),
}]
);
}
#[test]
fn file_markers_pair_labels_with_resolved_targets() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let report = root.join("generated/report.pdf");
std::fs::create_dir_all(report.parent().unwrap()).unwrap();
std::fs::write(&report, b"pdf").unwrap();
let notes = root.join("notes.txt");
std::fs::write(¬es, b"notes").unwrap();
let readme = root.join("README");
std::fs::write(&readme, b"readme").unwrap();
let body = "Get [FILE:generated/report.pdf] and [FILE:notes.txt] and [FILE:README] then \
[FILE:generated/gone.pdf] and [FILE:data:text/plain;base64,aGVsbG8=]";
let rendered = preprocess(body, &agent(root));
assert_eq!(
rendered.files,
vec![
FileMarker {
label: "📄 report.pdf".to_string(),
target: report.to_string_lossy().into_owned(),
},
FileMarker {
label: "📄 notes.txt".to_string(),
target: notes.to_string_lossy().into_owned(),
},
FileMarker {
label: "📄 README".to_string(),
target: readme.to_string_lossy().into_owned(),
},
],
"only the labels the text carries are link candidates"
);
assert!(
!rendered.text.contains("[FILE:"),
"no marker survives in what the user reads: {}",
rendered.text
);
}
#[test]
fn linkified_spans_links_only_the_marker_label() {
let span = text::Span::new("Get 📄 report.pdf now");
let files = [FileMarker {
label: "📄 report.pdf".to_string(),
target: "/tmp/report.pdf".to_string(),
}];
let spans = SelectableMediaViewer::new(&files)
.linkified_spans(&[span])
.unwrap();
let texts: Vec<&str> = spans.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(texts, ["Get ", "📄 report.pdf", " now"]);
assert_eq!(spans[0].link, None);
assert_eq!(spans[1].link, Some(file_link_uri("/tmp/report.pdf")));
assert_eq!(
spans[1].color, None,
"a file marker must keep its plain colour"
);
assert!(
!spans[1].underline,
"a file marker must not be underlined at rest"
);
assert_eq!(spans[2].link, None);
}
#[test]
fn linkified_spans_links_same_named_markers_to_their_own_targets() {
let files = [
FileMarker {
label: "📄 notes.txt".to_string(),
target: "first/notes.txt".to_string(),
},
FileMarker {
label: "📄 notes.txt".to_string(),
target: "second/notes.txt".to_string(),
},
];
let viewer = SelectableMediaViewer::new(&files);
let first = viewer
.linkified_spans(&[text::Span::new("Saved 📄 notes.txt")])
.unwrap();
let second = viewer
.linkified_spans(&[text::Span::new("Again 📄 notes.txt")])
.unwrap();
assert_eq!(
first[1].link,
Some(file_link_uri("first/notes.txt")),
"each block must link the next marker, not always the first"
);
assert_eq!(second[1].link, Some(file_link_uri("second/notes.txt")));
}
#[test]
fn file_marker_targets_resolve_against_the_workspace() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
assert_eq!(
resolve_file_marker_target("generated/report.pdf", Some(root)).unwrap(),
root.join("generated/report.pdf"),
"a relative target resolves against the conversation's workspace"
);
let absolute = root.join("elsewhere/report.pdf");
assert_eq!(
resolve_file_marker_target(&absolute.to_string_lossy(), Some(root)).unwrap(),
absolute,
"an absolute target is used as it is"
);
assert_eq!(
resolve_file_marker_target("report.pdf", None).unwrap_err(),
"Cannot open 'report.pdf': the conversation's workspace is unknown."
);
assert_eq!(
resolve_file_marker_target("https://example.com/report.pdf", Some(root)).unwrap_err(),
"Not a local file: report.pdf"
);
assert_eq!(
resolve_file_marker_target("", Some(root)).unwrap_err(),
"The marker does not name a file."
);
}
#[test]
fn reveal_file_marker_refuses_a_target_that_is_not_a_file() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let missing = root.join("generated/report.pdf");
assert_eq!(
reveal_file_marker("generated/report.pdf", Some(root)).unwrap_err(),
format!("File not found: {}", missing.display()),
"a relative target must resolve against the workspace"
);
std::fs::create_dir(root.join("folder")).unwrap();
assert_eq!(
reveal_file_marker("folder", Some(root)).unwrap_err(),
format!("Not a file: {}", root.join("folder").display())
);
}
#[test]
fn file_name_or_path_table() {
let cases: &[(&str, &str, &str)] = &[
("unix path", "/foo/bar.txt", "bar.txt"),
("nested path", "/foo/bar/doc.txt", "doc.txt"),
("no directory component", "bar.txt", "bar.txt"),
("trailing slash is normalized on Unix", "/foo/bar/", "bar"),
];
for (i, (name, input, expected)) in cases.iter().enumerate() {
assert_eq!(file_name_or_path(input), *expected, "case {i} ({name})");
}
}
fn tiny_png(width: u32, height: u32) -> Vec<u8> {
let img = ::image::RgbaImage::from_pixel(width, height, ::image::Rgba([255, 0, 0, 255]));
let mut buf = Vec::new();
img.write_to(&mut Cursor::new(&mut buf), ::image::ImageFormat::Png)
.expect("test PNG must encode");
buf
}
fn data_uri(png: &[u8]) -> String {
format!("data:image/png;base64,{}", STANDARD.encode(png))
}
#[test]
fn decode_data_uri_image_table() {
let valid_uri = data_uri(&tiny_png(2, 1));
let invalid_base64 = "data:image/png;base64,%%%not-base64%%%".to_string();
let non_image = format!("data:text/plain;base64,{}", STANDARD.encode(b"hello world"));
let oversized_payload = format!(
"data:image/png;base64,{}",
"A".repeat(media_target::MAX_DATA_URI_ENCODED_BYTES + 1)
);
let oversized_dimensions = data_uri(&tiny_png(MAX_IMAGE_LONGEST_SIDE_PX + 1, 1));
let missing_marker = "data:image/png,AAAA".to_string();
let mut jbuf = Vec::new();
::image::RgbImage::from_pixel(1, 1, ::image::Rgb([255, 0, 0]))
.write_to(&mut Cursor::new(&mut jbuf), ::image::ImageFormat::Jpeg)
.expect("test JPEG must encode");
let mismatched_uri = format!("data:image/png;base64,{}", STANDARD.encode(&jbuf));
#[expect(clippy::type_complexity)] let cases: &[(&str, String, Option<(u32, u32, Vec<u8>)>)] = &[
(
"decodes a solid-red 2×1 PNG",
valid_uri,
Some((2, 1, vec![255, 0, 0, 255, 255, 0, 0, 255])),
),
("rejects invalid base64", invalid_base64, None),
("rejects non-image bytes", non_image, None),
(
"rejects an oversized payload by a pure length check before any base64 work",
oversized_payload,
None,
),
(
"downscales a valid but over-display-cap raster to the render cap",
oversized_dimensions,
Some((
MAX_IMAGE_LONGEST_SIDE_PX,
1,
[255, 0, 0, 255].repeat(MAX_IMAGE_LONGEST_SIDE_PX as usize),
)),
),
(
"rejects a data URI missing the base64 marker",
missing_marker,
None,
),
(
"rejects a data URI whose declared subtype mismatches the actual bytes",
mismatched_uri,
None,
),
];
for (i, (name, uri, expected)) in cases.iter().enumerate() {
assert_eq!(
decode_data_uri_image(uri).as_ref(),
expected.as_ref(),
"case {i} ({name})"
);
}
}
#[test]
fn data_uri_image_handle_caches_stable_handle() {
let uri = data_uri(&tiny_png(3, 2));
let first = data_uri_image_handle(&uri).expect("valid data URI decodes");
let second = data_uri_image_handle(&uri).expect("cached hit still decodes");
assert_eq!(first.id(), second.id());
}
#[test]
fn data_uri_image_handle_caches_failures() {
let uri = "data:image/png;base64,%%%not-base64%%%";
assert_eq!(data_uri_image_handle(uri), None);
{
let cache = DATA_URI_IMAGE_CACHE.lock().unwrap_poison();
assert!(
matches!(cache.entries.get(uri), Some(CachedImage::Failed)),
"failed decode must be cached, got {:?}",
cache.entries.get(uri)
);
}
assert_eq!(data_uri_image_handle(uri), None);
}
#[test]
fn data_uri_image_handle_rejects_oversized_payload_without_caching() {
let huge = "A".repeat(media_target::MAX_DATA_URI_ENCODED_BYTES + 1);
let uri = format!("data:image/png;base64,{huge}");
assert_eq!(data_uri_image_handle(&uri), None);
let cache = DATA_URI_IMAGE_CACHE.lock().unwrap_poison();
assert!(
!cache.entries.contains_key(&uri),
"over-cap payload must not be cached"
);
}
#[test]
fn data_uri_marker_survives_markdown_parse() {
let uri = data_uri(&tiny_png(2, 1));
let tmp = tempfile::tempdir().unwrap();
let processed = preprocess(&format!("[IMAGE:{uri}]"), &agent(tmp.path())).text;
let items: Vec<_> = markdown::parse(&processed).collect();
let [markdown::Item::Image { url, .. }] = items.as_slice() else {
panic!("expected a single Image item, got {items:?}");
};
assert_eq!(url.as_str(), uri);
}
#[test]
fn spaced_path_marker_survives_markdown_parse() {
let tmp = tempfile::tempdir().unwrap();
let spaced = tmp.path().join("my file.png");
std::fs::write(&spaced, tiny_png(2, 1)).unwrap();
let processed = preprocess("[IMAGE:my file.png]", &agent(tmp.path())).text;
assert_eq!(
processed,
format!("", spaced.to_string_lossy())
);
let items: Vec<_> = markdown::parse(&processed).collect();
let [markdown::Item::Image { url, .. }] = items.as_slice() else {
panic!("expected a single Image item, got {items:?}");
};
assert_eq!(url.as_str(), spaced.to_string_lossy());
}
#[test]
fn bounded_cache_duplicate_insert_keeps_eviction_exact() {
let mut cache = BoundedLruCache::with_capacity(2);
cache.insert(1, "a");
cache.insert(1, "a2"); cache.insert(2, "b");
assert_eq!(cache.lru.len(), 2, "duplicate key must not survive in lru");
assert_eq!(cache.entries.len(), 2);
assert_eq!(cache.get(&2), Some(&"b"));
cache.insert(3, "c");
assert_eq!(cache.lru.len(), 2, "eviction must stay exact");
assert_eq!(cache.get(&1), None, "oldest (1) must be evicted");
assert_eq!(cache.get(&2), Some(&"b"));
assert_eq!(cache.get(&3), Some(&"c"));
}
#[test]
fn cached_image_returns_a_cached_failure_without_decoding() {
let cache = Mutex::new(BoundedLruCache::with_capacity(MAX_CACHED_IMAGES));
cache
.lock()
.unwrap_poison()
.insert(String::from("bad"), CachedImage::Failed);
let handle = cached_image(&cache, "bad", || {
panic!("a cached failure must not be decoded again")
});
assert!(handle.is_none());
}
#[test]
fn cached_image_decodes_without_holding_the_cache_lock() {
let cache = Mutex::new(BoundedLruCache::with_capacity(MAX_CACHED_IMAGES));
let handle = cached_image(&cache, "raster", || {
assert!(cache.try_lock().is_ok(), "decode must run unlocked");
Some((1, 1, vec![0, 0, 0, 0]))
});
assert!(handle.is_some(), "the decoded handle is returned");
assert!(matches!(
cache.lock().unwrap_poison().get("raster"),
Some(CachedImage::Decoded(_))
));
}
}