From 81a5660f11386dfc0c5cb00d32acd25a11298045 Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 18 Mar 2026 11:23:50 +0000 Subject: [PATCH] story-kit: merge 277 - bot uses configured display_name Adds system_prompt parameter to chat_stream so the Matrix bot passes "Your name is {name}" to Claude Code. Reads display_name from bot.toml config. Resolved conflicts by integrating bot_name into master's permission-handling code structure. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/llm/chat.rs | 1 + server/src/llm/providers/claude_code.rs | 8 +++++ server/src/matrix/bot.rs | 42 +++++++++++++++++++++-- server/src/matrix/config.rs | 45 +++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/server/src/llm/chat.rs b/server/src/llm/chat.rs index edd8b6b..41ef6d8 100644 --- a/server/src/llm/chat.rs +++ b/server/src/llm/chat.rs @@ -294,6 +294,7 @@ where &user_message, &project_root.to_string_lossy(), config.session_id.as_deref(), + None, &mut cancel_rx, |token| on_token(token), |thinking| on_thinking(thinking), diff --git a/server/src/llm/providers/claude_code.rs b/server/src/llm/providers/claude_code.rs index a945af9..9346b4b 100644 --- a/server/src/llm/providers/claude_code.rs +++ b/server/src/llm/providers/claude_code.rs @@ -42,6 +42,7 @@ impl ClaudeCodeProvider { user_message: &str, project_root: &str, session_id: Option<&str>, + system_prompt: Option<&str>, cancel_rx: &mut watch::Receiver, mut on_token: F, mut on_thinking: T, @@ -55,6 +56,7 @@ impl ClaudeCodeProvider { let message = user_message.to_string(); let cwd = project_root.to_string(); let resume_id = session_id.map(|s| s.to_string()); + let sys_prompt = system_prompt.map(|s| s.to_string()); let cancelled = Arc::new(AtomicBool::new(false)); let cancelled_clone = cancelled.clone(); @@ -79,6 +81,7 @@ impl ClaudeCodeProvider { &message, &cwd, resume_id.as_deref(), + sys_prompt.as_deref(), cancelled, token_tx, thinking_tx, @@ -147,6 +150,7 @@ fn run_pty_session( user_message: &str, cwd: &str, resume_session_id: Option<&str>, + system_prompt: Option<&str>, cancelled: Arc, token_tx: tokio::sync::mpsc::UnboundedSender, thinking_tx: tokio::sync::mpsc::UnboundedSender, @@ -185,6 +189,10 @@ fn run_pty_session( // a tool requires user approval, instead of using PTY stdin/stdout. cmd.arg("--permission-prompt-tool"); cmd.arg("mcp__story-kit__prompt_permission"); + if let Some(sys) = system_prompt { + cmd.arg("--system"); + cmd.arg(sys); + } cmd.cwd(cwd); // Keep TERM reasonable but disable color cmd.env("NO_COLOR", "1"); diff --git a/server/src/matrix/bot.rs b/server/src/matrix/bot.rs index b65c830..2d0a4f0 100644 --- a/server/src/matrix/bot.rs +++ b/server/src/matrix/bot.rs @@ -162,6 +162,9 @@ pub struct BotContext { /// How long to wait for a user to respond to a permission prompt before /// denying (fail-closed). pub permission_timeout_secs: u64, + /// The name the bot uses to refer to itself. Derived from `display_name` + /// in bot.toml; defaults to "Assistant" when unset. + pub bot_name: String, } // --------------------------------------------------------------------------- @@ -313,6 +316,11 @@ pub async fn run_bot( persisted.len() ); + let bot_name = config + .display_name + .clone() + .unwrap_or_else(|| "Assistant".to_string()); + let ctx = BotContext { bot_user_id, target_room_ids, @@ -324,6 +332,7 @@ pub async fn run_bot( perm_rx, pending_perm_replies: Arc::new(TokioMutex::new(HashMap::new())), permission_timeout_secs: config.permission_timeout_secs, + bot_name, }; slog!("[matrix-bot] Cryptographic identity verification is always ON — commands from unencrypted rooms or unverified devices are rejected"); @@ -741,6 +750,11 @@ async fn handle_message( // Prior conversation context is carried by the Claude Code session. let prompt = format_user_prompt(&sender, &user_message); + let bot_name = &ctx.bot_name; + let system_prompt = format!( + "Your name is {bot_name}. Refer to yourself as {bot_name}, not Claude." + ); + let provider = ClaudeCodeProvider::new(); let (cancel_tx, mut cancel_rx) = watch::channel(false); // Keep the sender alive for the duration of the call. @@ -778,6 +792,7 @@ async fn handle_message( &prompt, &project_root_str, resume_session_id.as_deref(), + Some(&system_prompt), &mut cancel_rx, move |token| { let mut buf = buffer_for_callback.lock().unwrap(); @@ -1205,6 +1220,7 @@ mod tests { perm_rx: Arc::new(TokioMutex::new(perm_rx)), pending_perm_replies: Arc::new(TokioMutex::new(HashMap::new())), permission_timeout_secs: 120, + bot_name: "Assistant".to_string(), }; // Clone must work (required by Matrix SDK event handler injection). let _cloned = ctx.clone(); @@ -1638,8 +1654,6 @@ mod tests { #[test] fn is_permission_approval_strips_at_mention_prefix() { - // "@botname yes" should still be treated as approval — the mention - // prefix is stripped before checking the token. assert!(is_permission_approval("@timmy yes")); assert!(!is_permission_approval("@timmy no")); } @@ -1649,4 +1663,28 @@ mod tests { assert!(is_permission_approval(" yes ")); assert!(is_permission_approval("\tyes\n")); } + + // -- bot_name / system prompt ------------------------------------------- + + #[test] + fn bot_name_system_prompt_format() { + let bot_name = "Timmy"; + let system_prompt = + format!("Your name is {bot_name}. Refer to yourself as {bot_name}, not Claude."); + assert_eq!( + system_prompt, + "Your name is Timmy. Refer to yourself as Timmy, not Claude." + ); + } + + #[test] + fn bot_name_defaults_to_assistant_when_display_name_absent() { + // When display_name is not set in bot.toml, bot_name should be "Assistant". + // This mirrors the logic in run_bot: config.display_name.clone().unwrap_or_else(...) + fn resolve_bot_name(display_name: Option) -> String { + display_name.unwrap_or_else(|| "Assistant".to_string()) + } + assert_eq!(resolve_bot_name(None), "Assistant"); + assert_eq!(resolve_bot_name(Some("Timmy".to_string())), "Timmy"); + } } diff --git a/server/src/matrix/config.rs b/server/src/matrix/config.rs index e58d607..a13ea29 100644 --- a/server/src/matrix/config.rs +++ b/server/src/matrix/config.rs @@ -49,6 +49,10 @@ pub struct BotConfig { /// backwards compatibility so existing bot.toml files still parse. #[allow(dead_code)] pub model: Option, + /// Display name the bot uses to identify itself in conversations. + /// If unset, the bot falls back to "Assistant". + #[serde(default)] + pub display_name: Option, } impl BotConfig { @@ -265,6 +269,47 @@ history_size = 50 assert_eq!(config.history_size, 50); } + #[test] + fn load_reads_display_name() { + let tmp = tempfile::tempdir().unwrap(); + let sk = tmp.path().join(".story_kit"); + fs::create_dir_all(&sk).unwrap(); + fs::write( + sk.join("bot.toml"), + r#" +homeserver = "https://matrix.example.com" +username = "@bot:example.com" +password = "secret" +room_ids = ["!abc:example.com"] +enabled = true +display_name = "Timmy" +"#, + ) + .unwrap(); + let config = BotConfig::load(tmp.path()).unwrap(); + assert_eq!(config.display_name.as_deref(), Some("Timmy")); + } + + #[test] + fn load_display_name_defaults_to_none_when_absent() { + let tmp = tempfile::tempdir().unwrap(); + let sk = tmp.path().join(".story_kit"); + fs::create_dir_all(&sk).unwrap(); + fs::write( + sk.join("bot.toml"), + r#" +homeserver = "https://matrix.example.com" +username = "@bot:example.com" +password = "secret" +room_ids = ["!abc:example.com"] +enabled = true +"#, + ) + .unwrap(); + let config = BotConfig::load(tmp.path()).unwrap(); + assert!(config.display_name.is_none()); + } + #[test] fn load_uses_default_permission_timeout() { let tmp = tempfile::tempdir().unwrap();