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) <noreply@anthropic.com>
This commit is contained in:
Dave
2026-03-18 11:23:50 +00:00
parent 4bf01c6cca
commit 81a5660f11
4 changed files with 94 additions and 2 deletions

View File

@@ -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>) -> 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");
}
}

View File

@@ -49,6 +49,10 @@ pub struct BotConfig {
/// backwards compatibility so existing bot.toml files still parse.
#[allow(dead_code)]
pub model: Option<String>,
/// Display name the bot uses to identify itself in conversations.
/// If unset, the bot falls back to "Assistant".
#[serde(default)]
pub display_name: Option<String>,
}
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();