From 420db4bb889b1db65d2b95aa21d1f923845314ff Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 22 Jul 2026 12:50:41 -0600 Subject: [PATCH] feat: created a new builtin function for agents who can spawn other agents to list available agents via agent__list_available --- src/config/agent.rs | 13 +++ src/config/mod.rs | 1 + src/config/prompts.rs | 3 +- src/function/mod.rs | 3 +- src/function/supervisor.rs | 85 +++++++++++--- src/render/markdown.rs | 224 ++++++++++++++++++------------------- 6 files changed, 199 insertions(+), 130 deletions(-) diff --git a/src/config/agent.rs b/src/config/agent.rs index b18c02c..de3b45d 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -1016,6 +1016,19 @@ pub fn list_agents() -> Vec { agents } +pub fn list_agents_with_descriptions() -> Vec<(String, String)> { + list_agents() + .into_iter() + .map(|name| { + let description = AgentConfig::load(&paths::agent_config_file(&name)) + .ok() + .map(|c| c.description) + .unwrap_or_default(); + (name, description) + }) + .collect() +} + pub fn complete_agent_variables(agent_name: &str) -> Vec<(String, Option)> { let config_path = paths::agent_config_file(agent_name); if !config_path.exists() { diff --git a/src/config/mod.rs b/src/config/mod.rs index cd05a38..f862a15 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -22,6 +22,7 @@ mod update; pub use self::agent::{ Agent, AgentVariable, AgentVariables, complete_agent_variables, list_agents, + list_agents_with_descriptions, }; #[allow(unused_imports)] pub use self::app_config::AppConfig; diff --git a/src/config/prompts.rs b/src/config/prompts.rs index cd82f14..54786ed 100644 --- a/src/config/prompts.rs +++ b/src/config/prompts.rs @@ -84,7 +84,8 @@ pub(in crate::config) const DEFAULT_SPAWN_INSTRUCTIONS: &str = indoc! {" | `agent__spawn` | Spawn a subagent in the background. Returns an `id` immediately. | | `agent__check` | Non-blocking check: is the agent done yet? Returns PENDING or result. | | `agent__collect` | Blocking wait: wait for an agent to finish, return its output. | - | `agent__list` | List all spawned agents and their status. | + | `agent__list_available` | List all agent types you can spawn (name + description). Use this to discover specialists before calling `agent__spawn`. | + | `agent__list_running` | List all subagents YOU have spawned, with their status. | | `agent__cancel` | Cancel a running agent by ID. | | `agent__task_create` | Create a task in the dependency-aware task queue. | | `agent__task_list` | List all tasks and their status/dependencies. | diff --git a/src/function/mod.rs b/src/function/mod.rs index cf96c1f..079a2f4 100644 --- a/src/function/mod.rs +++ b/src/function/mod.rs @@ -1793,7 +1793,8 @@ mod tests { assert!(f.contains("agent__spawn")); assert!(f.contains("agent__check")); assert!(f.contains("agent__collect")); - assert!(f.contains("agent__list")); + assert!(f.contains("agent__list_running")); + assert!(f.contains("agent__list_available")); assert!(f.contains("agent__cancel")); assert!(f.contains("agent__reply_escalation")); } diff --git a/src/function/supervisor.rs b/src/function/supervisor.rs index e73ba68..907b26a 100644 --- a/src/function/supervisor.rs +++ b/src/function/supervisor.rs @@ -1,6 +1,8 @@ use super::{FunctionDeclaration, JsonSchema}; use crate::client::{Model, ModelType, call_chat_completions}; -use crate::config::{Agent, AppState, Input, RequestContext, Role, RoleLike}; +use crate::config::{ + Agent, AppState, Input, RequestContext, Role, RoleLike, list_agents_with_descriptions, +}; use crate::supervisor::mailbox::{Envelope, EnvelopePayload, Inbox}; use crate::supervisor::{AgentExitStatus, AgentHandle, AgentResult, Supervisor}; use crate::utils::{AbortSignal, create_abort_signal, wait_abort_signal}; @@ -193,8 +195,18 @@ pub fn supervisor_function_declarations() -> Vec { agent: false, }, FunctionDeclaration { - name: format!("{SUPERVISOR_FUNCTION_PREFIX}list"), - description: "List all currently running subagents and their status.".to_string(), + name: format!("{SUPERVISOR_FUNCTION_PREFIX}list_running"), + description: "List all subagents YOU have spawned that are still tracked by the supervisor, with their status. Use this to see which of your background agents are still active. To discover which agent types you can spawn in the first place, use `agent__list_available` instead.".to_string(), + parameters: JsonSchema { + type_value: Some("object".to_string()), + properties: Some(IndexMap::new()), + ..Default::default() + }, + agent: false, + }, + FunctionDeclaration { + name: format!("{SUPERVISOR_FUNCTION_PREFIX}list_available"), + description: "List all agent types installed and available to spawn (name + description). Use this to discover what specialists exist before calling `agent__spawn` — especially when you're unsure which agent to delegate to. This is the discovery counterpart to `agent__list_running` (which reports agents you have already spawned).".to_string(), parameters: JsonSchema { type_value: Some("object".to_string()), properties: Some(IndexMap::new()), @@ -384,7 +396,8 @@ pub async fn handle_supervisor_tool( "spawn" => handle_spawn(ctx, args).await, "check" => handle_check(ctx, args).await, "collect" => handle_collect(ctx, args).await, - "list" => handle_list(ctx), + "list_running" => handle_list_running(ctx), + "list_available" => handle_list_available(), "cancel" => handle_cancel(ctx, args).await, "send_message" => handle_send_message(ctx, args), "check_inbox" => handle_check_inbox(ctx), @@ -920,7 +933,7 @@ async fn handle_collect(ctx: &mut RequestContext, args: &Value) -> Result } } -fn handle_list(ctx: &mut RequestContext) -> Result { +fn handle_list_running(ctx: &mut RequestContext) -> Result { let supervisor = ctx .supervisor .as_ref() @@ -948,6 +961,26 @@ fn handle_list(ctx: &mut RequestContext) -> Result { })) } +fn handle_list_available() -> Result { + let entries = list_agents_with_descriptions(); + let count = entries.len(); + let agents: Vec = entries + .into_iter() + .map(|(name, description)| { + if description.is_empty() { + json!({ "name": name }) + } else { + json!({ "name": name, "description": description }) + } + }) + .collect(); + + Ok(json!({ + "count": count, + "agents": agents, + })) +} + async fn handle_cancel(ctx: &mut RequestContext, args: &Value) -> Result { let id = args .get("id") @@ -1434,32 +1467,39 @@ mod tests { } #[test] - fn handle_list_empty_supervisor() { + fn handle_list_running_empty_supervisor() { let mut ctx = ctx_with_supervisor(4, 3); - let result = handle_list(&mut ctx).unwrap(); + let result = handle_list_running(&mut ctx).unwrap(); assert_eq!(result["active_count"], 0); assert_eq!(result["max_concurrent"], 4); assert!(result["agents"].as_array().unwrap().is_empty()); } #[test] - fn handle_list_with_agents() { + fn handle_list_running_with_agents() { let mut ctx = ctx_with_supervisor(4, 3); register_fake_agent(&mut ctx, "a1", "explore"); register_fake_agent(&mut ctx, "a2", "coder"); - let result = handle_list(&mut ctx).unwrap(); + let result = handle_list_running(&mut ctx).unwrap(); assert_eq!(result["active_count"], 2); let agents = result["agents"].as_array().unwrap(); assert_eq!(agents.len(), 2); } #[test] - fn handle_list_no_supervisor_errors() { + fn handle_list_running_no_supervisor_errors() { let mut ctx = RequestContext::new(default_app_state(), WorkingMode::Cmd); - let result = handle_list(&mut ctx); + let result = handle_list_running(&mut ctx); assert!(result.is_err()); } + #[test] + fn handle_list_available_returns_shape() { + let result = handle_list_available().unwrap(); + assert!(result["count"].is_number()); + assert!(result["agents"].is_array()); + } + #[test] fn handle_check_unknown_agent() { let mut ctx = ctx_with_supervisor(4, 3); @@ -1753,13 +1793,30 @@ mod tests { } #[test] - fn dispatch_routes_list() { + fn dispatch_routes_list_running() { let mut ctx = ctx_with_supervisor(4, 3); - let result = - run_async(handle_supervisor_tool(&mut ctx, "agent__list", &json!({}))).unwrap(); + let result = run_async(handle_supervisor_tool( + &mut ctx, + "agent__list_running", + &json!({}), + )) + .unwrap(); assert!(result["active_count"].is_number()); } + #[test] + fn dispatch_routes_list_available() { + let mut ctx = ctx_with_supervisor(4, 3); + let result = run_async(handle_supervisor_tool( + &mut ctx, + "agent__list_available", + &json!({}), + )) + .unwrap(); + assert!(result["count"].is_number()); + assert!(result["agents"].is_array()); + } + #[test] fn dispatch_routes_task_list() { let mut ctx = ctx_with_supervisor(4, 3); diff --git a/src/render/markdown.rs b/src/render/markdown.rs index 8402f75..180f8b4 100644 --- a/src/render/markdown.rs +++ b/src/render/markdown.rs @@ -22,40 +22,29 @@ static LANG_MAPS: LazyLock> = LazyLock::new(|| { m }); -static HEADING_RE: LazyLock = - LazyLock::new(|| Regex::new(r"^\s*(#{1,6}) +.+").unwrap()); +static HEADING_RE: LazyLock = LazyLock::new(|| Regex::new(r"^\s*(#{1,6}) +.+").unwrap()); static BLOCKQUOTE_RE: LazyLock = LazyLock::new(|| Regex::new(r"^\s*>").unwrap()); static TASK_ITEM_RE: LazyLock = LazyLock::new(|| Regex::new(r"^\s*[-*+] \[([ xX])\] +.+").unwrap()); -static BULLET_ITEM_RE: LazyLock = - LazyLock::new(|| Regex::new(r"^\s*[-*+] +.+").unwrap()); -static NUMBERED_ITEM_RE: LazyLock = - LazyLock::new(|| Regex::new(r"^\s*\d+\. +.+").unwrap()); +static BULLET_ITEM_RE: LazyLock = LazyLock::new(|| Regex::new(r"^\s*[-*+] +.+").unwrap()); +static NUMBERED_ITEM_RE: LazyLock = LazyLock::new(|| Regex::new(r"^\s*\d+\. +.+").unwrap()); static HRULE_RE: LazyLock = LazyLock::new(|| Regex::new(r"^\s*(-{3,}|_{3,}|\*{3,})\s*$").unwrap()); static TABLE_SEPARATOR_RE: LazyLock = LazyLock::new(|| Regex::new(r"^\s*\|(\s*:?-+:?\s*\|)+\s*$").unwrap()); -static TABLE_ROW_RE: LazyLock = - LazyLock::new(|| Regex::new(r"^\s*\|.*\|\s*$").unwrap()); +static TABLE_ROW_RE: LazyLock = LazyLock::new(|| Regex::new(r"^\s*\|.*\|\s*$").unwrap()); -static INLINE_CODE_RE: LazyLock = - LazyLock::new(|| Regex::new(r"`([^`\n]+)`").unwrap()); +static INLINE_CODE_RE: LazyLock = LazyLock::new(|| Regex::new(r"`([^`\n]+)`").unwrap()); static IMAGE_RE: LazyLock = LazyLock::new(|| Regex::new(r"!\[([^\]]*)\]\(([^)]+)\)").unwrap()); -static LINK_RE: LazyLock = - LazyLock::new(|| Regex::new(r"\[([^\]]+)\]\(([^)]+)\)").unwrap()); -static BOLD_AST_RE: LazyLock = - LazyLock::new(|| Regex::new(r"\*\*([^*\n]+)\*\*").unwrap()); -static BOLD_US_RE: LazyLock = - LazyLock::new(|| Regex::new(r"__([^_\n]+)__").unwrap()); -static ITALIC_AST_RE: LazyLock = LazyLock::new(|| { - Regex::new(r"(? = LazyLock::new(|| { - Regex::new(r"(? = - LazyLock::new(|| Regex::new(r"~~([^~\n]+)~~").unwrap()); +static LINK_RE: LazyLock = LazyLock::new(|| Regex::new(r"\[([^\]]+)\]\(([^)]+)\)").unwrap()); +static BOLD_AST_RE: LazyLock = LazyLock::new(|| Regex::new(r"\*\*([^*\n]+)\*\*").unwrap()); +static BOLD_US_RE: LazyLock = LazyLock::new(|| Regex::new(r"__([^_\n]+)__").unwrap()); +static ITALIC_AST_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(? = + LazyLock::new(|| Regex::new(r"(? = LazyLock::new(|| Regex::new(r"~~([^~\n]+)~~").unwrap()); static CODE_PLACEHOLDER_RE: LazyLock = LazyLock::new(|| Regex::new(r"\x00C(\d+)\x00").unwrap()); @@ -107,10 +96,7 @@ fn detect_line_kind(line: &str) -> LineKind { } fn parse_table_row(line: &str) -> Vec { - let inner = line - .trim() - .trim_start_matches('|') - .trim_end_matches('|'); + let inner = line.trim().trim_start_matches('|').trim_end_matches('|'); inner.split('|').map(|c| c.trim().to_string()).collect() } @@ -230,8 +216,7 @@ fn render_blockquote(line: &str, styles: &MarkdownStyles, wrap_width: Option) - let prefix_width = 2; let leading_width = indent.chars().count(); - let effective_width = (wrap_width as usize) - .saturating_sub(leading_width + prefix_width); + let effective_width = (wrap_width as usize).saturating_sub(leading_width + prefix_width); let wrapped = wrap_plain_content(content, effective_width); if wrapped.is_empty() { return format!("{indent}{bullet} "); @@ -299,8 +283,7 @@ fn render_numbered(line: &str, styles: &MarkdownStyles, wrap_width: Option) let prefix_width = number.chars().count() + 2; let leading_width = indent.chars().count(); - let effective_width = (wrap_width as usize) - .saturating_sub(leading_width + prefix_width); + let effective_width = (wrap_width as usize).saturating_sub(leading_width + prefix_width); let wrapped = wrap_plain_content(after, effective_width); if wrapped.is_empty() { return format!("{indent}{number}{styled_dot} "); @@ -343,8 +326,7 @@ fn render_task( let prefix_width = 4; let leading_width = indent.chars().count(); - let effective_width = (wrap_width as usize) - .saturating_sub(leading_width + prefix_width); + let effective_width = (wrap_width as usize).saturating_sub(leading_width + prefix_width); let wrapped = wrap_plain_content(after_brackets, effective_width); if wrapped.is_empty() { return format!("{indent}{styled_brackets} "); @@ -706,20 +688,15 @@ impl MarkdownRender { } for row in rows { - let styled_row: Vec = row - .iter() - .map(|c| apply_inline(c, &self.styles)) - .collect(); + let styled_row: Vec = + row.iter().map(|c| apply_inline(c, &self.styles)).collect(); table.add_row(styled_row); } colorize_box_chars(&table.to_string(), self.styles.table_border) } - fn check_line( - &self, - line: &str, - ) -> (LineType, LineKind, Option, bool) { + fn check_line(&self, line: &str) -> (LineType, LineKind, Option, bool) { let mut line_type = self.prev_line_type; let mut code_syntax = self.code_syntax.clone(); let mut is_code = false; @@ -1034,8 +1011,7 @@ impl MarkdownStyles { &["string.other.link", "constant"], truecolor, ); - let strikethrough = - resolve_scope_style(theme, "markup.deleted", &["invalid"], truecolor); + let strikethrough = resolve_scope_style(theme, "markup.deleted", &["invalid"], truecolor); let hrule = resolve_scope_style(theme, "comment", &["punctuation"], truecolor); let table_border = resolve_scope_style( theme, @@ -1201,26 +1177,18 @@ std::error::Error>> { fn minimal_root_scope_theme() -> Theme { let mut theme = Theme::default(); - theme.scopes.push(theme_item( - "string", - syntect_rgb(0xff, 0xdd, 0x00), - None, - )); - theme.scopes.push(theme_item( - "comment", - syntect_rgb(0x88, 0x88, 0x88), - None, - )); - theme.scopes.push(theme_item( - "keyword", - syntect_rgb(0xaa, 0x00, 0xff), - None, - )); - theme.scopes.push(theme_item( - "constant", - syntect_rgb(0x00, 0xcc, 0xff), - None, - )); + theme + .scopes + .push(theme_item("string", syntect_rgb(0xff, 0xdd, 0x00), None)); + theme + .scopes + .push(theme_item("comment", syntect_rgb(0x88, 0x88, 0x88), None)); + theme + .scopes + .push(theme_item("keyword", syntect_rgb(0xaa, 0x00, 0xff), None)); + theme + .scopes + .push(theme_item("constant", syntect_rgb(0x00, 0xcc, 0xff), None)); theme.scopes.push(theme_item( "entity.name.tag", syntect_rgb(0x11, 0x22, 0x33), @@ -1236,11 +1204,9 @@ std::error::Error>> { syntect_rgb(0xde, 0xad, 0xbe), Some(FontStyle::BOLD), )); - theme.scopes.push(theme_item( - "invalid", - syntect_rgb(0xff, 0x00, 0x00), - None, - )); + theme + .scopes + .push(theme_item("invalid", syntect_rgb(0xff, 0x00, 0x00), None)); theme.scopes.push(theme_item( "punctuation", syntect_rgb(0x77, 0x77, 0x77), @@ -1269,13 +1235,10 @@ std::error::Error>> { #[test] fn resolve_scope_style_falls_back_when_primary_missing() { let mut theme = Theme::default(); - theme.scopes.push(theme_item( - "comment", - syntect_rgb(0x33, 0x44, 0x55), - None, - )); - let resolved = - resolve_scope_style(&theme, "markup.italic", &["nope", "comment"], true); + theme + .scopes + .push(theme_item("comment", syntect_rgb(0x33, 0x44, 0x55), None)); + let resolved = resolve_scope_style(&theme, "markup.italic", &["nope", "comment"], true); assert_eq!(resolved.fg, Some(rgb(0x33, 0x44, 0x55))); } @@ -1417,10 +1380,7 @@ std::error::Error>> { #[test] fn detect_line_kind_non_table_pipe_line_stays_paragraph() { - assert_eq!( - detect_line_kind("use `a | b` for or"), - LineKind::Paragraph, - ); + assert_eq!(detect_line_kind("use `a | b` for or"), LineKind::Paragraph,); assert_eq!(detect_line_kind("| trailing"), LineKind::Paragraph); assert_eq!(detect_line_kind("no closer |"), LineKind::Paragraph); } @@ -1492,7 +1452,10 @@ std::error::Error>> { fn colorize_box_chars_wraps_border_runs() { let input = "┌─┐\nabc\n└─┘"; let output = colorize_box_chars(input, Color::Red); - assert!(output.starts_with("\x1b["), "border run starts with SGR: {output:?}"); + assert!( + output.starts_with("\x1b["), + "border run starts with SGR: {output:?}" + ); assert!(output.contains("abc"), "non-border content preserved"); assert!(output.contains("┌")); assert!(output.contains("└")); @@ -1520,12 +1483,13 @@ std::error::Error>> { ]; let output = render.render_table(header, alignments, rows); for expected in ["A", "B", "C", "1", "2", "3", "4", "5", "6"] { - assert!(output.contains(expected), "cell {expected:?} in output: {output:?}"); + assert!( + output.contains(expected), + "cell {expected:?} in output: {output:?}" + ); } assert!( - output - .chars() - .any(|c| matches!(c, '\u{2500}'..='\u{257F}')), + output.chars().any(|c| matches!(c, '\u{2500}'..='\u{257F}')), "table has box-drawing chars: {output:?}", ); } @@ -1610,9 +1574,7 @@ std::error::Error>> { assert!(output.contains(cell), "cell {cell:?} rendered: {output:?}"); } assert!( - output - .chars() - .any(|c| matches!(c, '\u{2500}'..='\u{257F}')), + output.chars().any(|c| matches!(c, '\u{2500}'..='\u{257F}')), "output has box-drawing chars: {output:?}", ); assert!(output.contains("after"), "trailing paragraph preserved"); @@ -1684,8 +1646,7 @@ std::error::Error>> { fn multiple_tables_in_one_input() { let options = RenderOptions::default(); let mut render = MarkdownRender::init(options).unwrap(); - let text = - "| A |\n|---|\n| 1 |\n\n| B |\n|---|\n| 2 |\n"; + let text = "| A |\n|---|\n| 1 |\n\n| B |\n|---|\n| 2 |\n"; let output = render.render(text); let tail = render.finalize(); let combined = format!("{output}{tail}"); @@ -1718,10 +1679,7 @@ std::error::Error>> { output.contains("| A | B |"), "raw pipes preserved: {output:?}", ); - assert!( - render.table_state.is_none(), - "no state entered in raw mode", - ); + assert!(render.table_state.is_none(), "no state entered in raw mode",); } #[test] @@ -1748,7 +1706,10 @@ std::error::Error>> { assert!(output.contains('\n'), "wrapped output: {output:?}"); let lines: Vec<&str> = output.split('\n').collect(); for cont in &lines[1..] { - assert!(cont.starts_with(" "), "4-space indent for `42. `: {cont:?}"); + assert!( + cont.starts_with(" "), + "4-space indent for `42. `: {cont:?}" + ); } } @@ -1775,7 +1736,10 @@ std::error::Error>> { assert!(output.contains('\n'), "wrapped output: {output:?}"); let lines: Vec<&str> = output.split('\n').collect(); for cont in &lines[1..] { - assert!(cont.starts_with(" "), "4-space indent for `[ ] `: {cont:?}"); + assert!( + cont.starts_with(" "), + "4-space indent for `[ ] `: {cont:?}" + ); } } @@ -1822,7 +1786,10 @@ std::error::Error>> { let line = "- **bold** text with `code` that will wrap onto several lines"; let output = render_markdown_line(line, LineKind::BulletItem, &styles, Some(22)); assert!(output.contains('\n'), "wrapped output: {output:?}"); - assert!(!output.contains("**bold**"), "asterisks stripped: {output:?}"); + assert!( + !output.contains("**bold**"), + "asterisks stripped: {output:?}" + ); assert!(!output.contains("`code`"), "backticks stripped: {output:?}"); assert!(output.contains("bold")); assert!(output.contains("code")); @@ -1849,7 +1816,10 @@ std::error::Error>> { assert!(output.contains("Some paragraph.")); assert!(output.contains("•"), "bullet glyph rendered"); assert!(output.contains("│"), "blockquote pipe rendered"); - assert!(output.contains("A") && output.contains("1"), "table cells rendered"); + assert!( + output.contains("A") && output.contains("1"), + "table cells rendered" + ); assert!( output.chars().any(|c| matches!(c, '\u{2500}'..='\u{257F}')), "table borders rendered", @@ -2023,7 +1993,10 @@ std::error::Error>> { assert!(result.contains("foo")); assert!(result.contains("bar")); assert!(result.contains("baz")); - assert!(result.contains("\x1b[1m"), "bold applied around code: {result:?}"); + assert!( + result.contains("\x1b[1m"), + "bold applied around code: {result:?}" + ); } #[test] @@ -2069,7 +2042,10 @@ std::error::Error>> { fn images_emit_labeled_link() { let styles = test_styles(); let result = apply_inline("![alt text](https://img.example/x.png)", &styles); - assert!(!result.contains("!["), "raw image marker removed: {result:?}"); + assert!( + !result.contains("!["), + "raw image marker removed: {result:?}" + ); assert!(result.contains("Image: alt text")); assert!(result.contains("https://img.example/x.png")); assert!(result.contains("\x1b]8;;https://img.example/x.png\x1b\\")); @@ -2079,7 +2055,10 @@ std::error::Error>> { fn image_processed_before_link() { let styles = test_styles(); let result = apply_inline("![alt](https://example.com)", &styles); - assert!(!result.starts_with('!'), "no stray ! left behind: {result:?}"); + assert!( + !result.starts_with('!'), + "no stray ! left behind: {result:?}" + ); assert!(result.contains("Image:")); } @@ -2105,7 +2084,10 @@ std::error::Error>> { let hashes = "#".repeat(level as usize); let line = format!("{hashes} Title"); let result = render_markdown_line(&line, LineKind::Heading(level), &styles, None); - assert!(result.contains(&hashes), "H{level} keeps hashes: {result:?}"); + assert!( + result.contains(&hashes), + "H{level} keeps hashes: {result:?}" + ); assert!(result.contains("Title")); assert!(result.contains("\x1b[1m"), "H{level} bold: {result:?}"); } @@ -2199,18 +2181,19 @@ std::error::Error>> { #[test] fn render_paragraph_delegates_to_inline() { let styles = test_styles(); - let result = - render_markdown_line("hello **world**", LineKind::Paragraph, &styles, None); + let result = render_markdown_line("hello **world**", LineKind::Paragraph, &styles, None); assert!(!result.contains("**"), "bold markers stripped: {result:?}"); assert!(result.contains("world")); - assert!(result.contains("\x1b[1m"), "bold applied via inline: {result:?}"); + assert!( + result.contains("\x1b[1m"), + "bold applied via inline: {result:?}" + ); } #[test] fn render_bullet_runs_inline_on_content() { let styles = test_styles(); - let result = - render_markdown_line("- see `code`", LineKind::BulletItem, &styles, None); + let result = render_markdown_line("- see `code`", LineKind::BulletItem, &styles, None); assert!(result.contains("•")); assert!(!result.contains('`'), "backticks stripped: {result:?}"); assert!(result.contains("see ")); @@ -2220,8 +2203,12 @@ std::error::Error>> { #[test] fn render_blockquote_runs_inline_on_content() { let styles = test_styles(); - let result = - render_markdown_line("> visit [here](https://example.com)", LineKind::Blockquote, &styles, None); + let result = render_markdown_line( + "> visit [here](https://example.com)", + LineKind::Blockquote, + &styles, + None, + ); assert!(result.contains("│ ")); assert!(result.contains("here")); assert!(result.contains("https://example.com")); @@ -2244,7 +2231,10 @@ std::error::Error>> { let options = RenderOptions::default(); let render = MarkdownRender::init(options).unwrap(); let partial = render.render_line("**bo"); - assert!(partial.contains("**bo"), "unclosed bold preserved: {partial:?}"); + assert!( + partial.contains("**bo"), + "unclosed bold preserved: {partial:?}" + ); } #[test] @@ -2252,7 +2242,10 @@ std::error::Error>> { let options = RenderOptions::default(); let render = MarkdownRender::init(options).unwrap(); let partial = render.render_line("[label](https://exa"); - assert!(partial.contains("[label]"), "unclosed link preserved: {partial:?}"); + assert!( + partial.contains("[label]"), + "unclosed link preserved: {partial:?}" + ); } #[test] @@ -2291,7 +2284,10 @@ std::error::Error>> { let mut render = MarkdownRender::init(options).unwrap(); let text = "```rust\nfn main() {}\n```\n"; let output = render.render(text); - assert!(output.contains("fn main()"), "code content preserved: {output:?}"); + assert!( + output.contains("fn main()"), + "code content preserved: {output:?}" + ); } #[test]