From 1cdc05d4f1d44fa3fafdd4b8c06a8b81fe32b24c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Jan 2026 16:09:34 +0000 Subject: [PATCH 1/2] Add configurable agent name and system prompt Adds [agent] section to config.toml with two options: - name: Custom agent name (default: "Codey") - system_prompt: Custom intro/personality for system prompt The agent name is used in: - Chat display header (instead of hardcoded "Codey") - Welcome message ("Welcome to {name}!") - Default system prompt intro ("You are {name}, an AI...") If system_prompt is provided, it completely replaces the default intro paragraph while keeping the capabilities/guidelines section. https://claude.ai/code/session_01UZjkuMRFsSvatYjoZAAmA6 --- config.example.toml | 16 +++++++++ src/app.rs | 11 +++--- src/config.rs | 57 ++++++++++++++++++++++++++++++ src/prompts.rs | 84 +++++++++++++++++++++++++++++++++++++++++---- src/ui/chat.rs | 15 ++++---- 5 files changed, 166 insertions(+), 17 deletions(-) diff --git a/config.example.toml b/config.example.toml index adf182c..6282fe6 100644 --- a/config.example.toml +++ b/config.example.toml @@ -11,6 +11,22 @@ model = "claude-sonnet-4-20250514" # Maximum tokens for responses max_tokens = 8192 +# ============================================================================= +# Agent Persona +# ============================================================================= +# Customize the agent's name and personality. These settings affect how the +# agent introduces itself and displays its name in the chat interface. + +[agent] +# Custom name for the agent (default: "Codey") +# This appears in the chat header and welcome message. +# name = "Jarvis" + +# Custom system prompt intro/personality (replaces the default "You are Codey..." paragraph) +# Use this to give the agent a different persona or specialized behavior. +# The capabilities and guidelines section is automatically appended. +# system_prompt = "You are Jarvis, a sophisticated AI assistant with a dry wit and encyclopedic knowledge." + [auth] # Authentication method: "oauth" or "api_key" method = "oauth" diff --git a/src/app.rs b/src/app.rs index 273f24d..7b3b090 100644 --- a/src/app.rs +++ b/src/app.rs @@ -22,7 +22,7 @@ use crate::llm::{Agent, AgentId, AgentRegistry, AgentStatus, AgentStep, RequestM #[cfg(feature = "profiling")] use crate::{profile_frame, profile_span}; use crate::notifications::{Notification, NotificationQueue}; -use crate::prompts::{SystemPrompt, COMPACTION_PROMPT, WELCOME_MESSAGE}; +use crate::prompts::{SystemPrompt, COMPACTION_PROMPT, welcome_message}; use crate::tool_filter::ToolFilters; use crate::tools::{ init_agent_context, init_browser_context, update_agent_oauth, EffectResult, ToolDecision, @@ -276,10 +276,12 @@ impl App { None }; + let agent_name = config.agent.name().to_string(); + Ok(Self { config, terminal, - chat: ChatView::new(transcript, terminal_size.0, chat_height), + chat: ChatView::new(transcript, terminal_size.0, chat_height, agent_name), input: InputBox::new(), should_quit: false, continue_session, @@ -338,7 +340,8 @@ impl App { init_browser_context(&self.config.browser); // Use dynamic prompt builder so mdsh commands are re-executed on each LLM call - let system_prompt = SystemPrompt::new(); + let system_prompt = SystemPrompt::with_config(&self.config); + let agent_name = system_prompt.agent_name().to_string(); let mut agent = Agent::with_dynamic_prompt( AgentRuntimeConfig::foreground(&self.config), Box::new(move || system_prompt.build()), @@ -350,7 +353,7 @@ impl App { agent.restore_from_transcript(&self.chat.transcript); } else { self.chat - .add_turn(Role::Assistant, TextBlock::pending(WELCOME_MESSAGE)); + .add_turn(Role::Assistant, TextBlock::pending(&welcome_message(&agent_name))); } self.agents.register(agent); diff --git a/src/config.rs b/src/config.rs index 79da3bc..88c8b71 100644 --- a/src/config.rs +++ b/src/config.rs @@ -104,6 +104,7 @@ impl AgentRuntimeConfig { #[serde(default)] pub struct Config { pub general: GeneralConfig, + pub agent: AgentPersonaConfig, pub agents: AgentsConfig, pub auth: AuthConfig, pub ui: UiConfig, @@ -117,6 +118,7 @@ impl Default for Config { fn default() -> Self { Self { general: GeneralConfig::default(), + agent: AgentPersonaConfig::default(), agents: AgentsConfig::default(), auth: AuthConfig::default(), ui: UiConfig::default(), @@ -258,6 +260,25 @@ impl Default for GeneralConfig { } } +/// Agent persona configuration (name and personality) +#[cfg(feature = "cli")] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default)] +pub struct AgentPersonaConfig { + /// Custom name for the agent (defaults to "Codey") + pub name: Option, + /// Custom system prompt intro/personality (replaces the default "You are Codey..." paragraph) + pub system_prompt: Option, +} + +#[cfg(feature = "cli")] +impl AgentPersonaConfig { + /// Get the agent name, falling back to default + pub fn name(&self) -> &str { + self.name.as_deref().unwrap_or(crate::prompts::DEFAULT_AGENT_NAME) + } +} + #[cfg(feature = "cli")] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] @@ -537,4 +558,40 @@ deny = ["\\.env$"] assert_eq!(config.tools.read_file.allow, vec!["\\.rs$"]); assert_eq!(config.tools.read_file.deny, vec!["\\.env$"]); } + + #[test] + fn test_agent_persona_defaults() { + let config = Config::default(); + assert!(config.agent.name.is_none()); + assert!(config.agent.system_prompt.is_none()); + assert_eq!(config.agent.name(), "Codey"); + } + + #[test] + fn test_parse_agent_persona() { + let toml = r#" +[agent] +name = "Jarvis" +system_prompt = "You are Jarvis, Tony Stark's AI assistant." +"#; + let config: Config = toml::from_str(toml).unwrap(); + assert_eq!(config.agent.name, Some("Jarvis".to_string())); + assert_eq!( + config.agent.system_prompt, + Some("You are Jarvis, Tony Stark's AI assistant.".to_string()) + ); + assert_eq!(config.agent.name(), "Jarvis"); + } + + #[test] + fn test_agent_persona_name_only() { + let toml = r#" +[agent] +name = "Assistant" +"#; + let config: Config = toml::from_str(toml).unwrap(); + assert_eq!(config.agent.name, Some("Assistant".to_string())); + assert!(config.agent.system_prompt.is_none()); + assert_eq!(config.agent.name(), "Assistant"); + } } diff --git a/src/prompts.rs b/src/prompts.rs index 7be3ee5..707e24a 100644 --- a/src/prompts.rs +++ b/src/prompts.rs @@ -16,13 +16,35 @@ const ESH_SCRIPT: &str = include_str!("../lib/esh/esh"); /// Filename for custom system prompt additions pub const SYSTEM_MD_FILENAME: &str = "SYSTEM.md"; -/// Welcome message shown when the application starts +/// Default agent name +pub const DEFAULT_AGENT_NAME: &str = "Codey"; + +/// Generate welcome message with the given agent name +pub fn welcome_message(name: &str) -> String { + format!( + "Welcome to {}! I'm your AI coding assistant. How can I help you today?", + name + ) +} + +/// Default welcome message (for backward compatibility) pub const WELCOME_MESSAGE: &str = "Welcome to Codey! I'm your AI coding assistant. How can I help you today?"; -/// Main system prompt for the primary agent -pub const SYSTEM_PROMPT: &str = r#"You are Codey, an AI coding assistant running in a terminal interface. +/// Default intro paragraph for the system prompt +pub const DEFAULT_SYSTEM_INTRO: &str = + "You are Codey, an AI coding assistant running in a terminal interface."; + +/// Generate the default intro with a custom agent name +pub fn default_system_intro(name: &str) -> String { + format!( + "You are {}, an AI coding assistant running in a terminal interface.", + name + ) +} +/// System prompt capabilities and guidelines (appended after the intro) +pub const SYSTEM_PROMPT_BODY: &str = r#" ## Capabilities You have access to the following tools: - `read_file`: Read file contents, optionally with line ranges @@ -79,6 +101,23 @@ You will be notified with a message when background tasks finish. - Always get confirmation before making destructive changes (this includes building a release) "#; +/// Build the base system prompt with optional config overrides +pub fn build_base_system_prompt(config: Option<&Config>) -> String { + let intro = match config { + Some(cfg) => { + // If custom system_prompt is provided, use it directly + if let Some(ref custom) = cfg.agent.system_prompt { + custom.clone() + } else { + // Otherwise use default intro with possibly custom name + default_system_intro(cfg.agent.name()) + } + } + None => DEFAULT_SYSTEM_INTRO.to_string(), + }; + format!("{}{}", intro, SYSTEM_PROMPT_BODY) +} + /// Prompt used when compacting conversation context pub const COMPACTION_PROMPT: &str = r#"The conversation context is getting large and needs to be compacted. @@ -115,7 +154,7 @@ You have read-only access to: /// A system prompt builder that supports dynamic content via esh templates. /// /// The prompt is composed of: -/// 1. The base system prompt (static) +/// 1. The base system prompt (configurable intro + capabilities/guidelines) /// 2. User SYSTEM.md from ~/.config/codey/ (optional, dynamic) /// 3. Project SYSTEM.md from .codey/ (optional, dynamic) /// @@ -125,10 +164,14 @@ You have read-only access to: pub struct SystemPrompt { user_path: Option, project_path: PathBuf, + /// Custom agent name (None = use default "Codey") + agent_name: Option, + /// Custom system prompt intro (None = use default) + custom_intro: Option, } impl SystemPrompt { - /// Create a new SystemPrompt with default paths. + /// Create a new SystemPrompt with default paths and no config overrides. pub fn new() -> Self { let user_path = Config::config_dir().map(|d| d.join(SYSTEM_MD_FILENAME)); let project_path = Path::new(CODEY_DIR).join(SYSTEM_MD_FILENAME); @@ -136,18 +179,45 @@ impl SystemPrompt { Self { user_path, project_path, + agent_name: None, + custom_intro: None, + } + } + + /// Create a new SystemPrompt with config-based overrides. + pub fn with_config(config: &Config) -> Self { + let user_path = Config::config_dir().map(|d| d.join(SYSTEM_MD_FILENAME)); + let project_path = Path::new(CODEY_DIR).join(SYSTEM_MD_FILENAME); + + Self { + user_path, + project_path, + agent_name: config.agent.name.clone(), + custom_intro: config.agent.system_prompt.clone(), } } + /// Get the agent name (custom or default) + pub fn agent_name(&self) -> &str { + self.agent_name.as_deref().unwrap_or(DEFAULT_AGENT_NAME) + } + /// Build the complete system prompt. /// /// This reads and processes all SYSTEM.md files, executing any embedded /// shell commands via esh (`<%= command %>`). The result is the concatenation of: - /// - Base system prompt + /// - Base system prompt (with optional custom intro) /// - User SYSTEM.md content (if exists) /// - Project SYSTEM.md content (if exists) pub fn build(&self) -> String { - let mut prompt = SYSTEM_PROMPT.to_string(); + // Build the intro portion + let intro = if let Some(ref custom) = self.custom_intro { + custom.clone() + } else { + default_system_intro(self.agent_name()) + }; + + let mut prompt = format!("{}{}", intro, SYSTEM_PROMPT_BODY); // Append user SYSTEM.md if it exists if let Some(ref user_path) = self.user_path { diff --git a/src/ui/chat.rs b/src/ui/chat.rs index fea57f6..a8cc6b4 100644 --- a/src/ui/chat.rs +++ b/src/ui/chat.rs @@ -50,10 +50,12 @@ pub struct ChatView { frozen_turn_ids: HashSet, /// Mapping of turn ID to line count (for frozen turns) turn_line_counts: HashMap, + /// Agent name for display (configurable) + agent_name: String, } impl ChatView { - pub fn new(transcript: Transcript, width: u16, max_lines: usize) -> Self { + pub fn new(transcript: Transcript, width: u16, max_lines: usize, agent_name: String) -> Self { Self { transcript, width, @@ -62,6 +64,7 @@ impl ChatView { committed_count: 0, frozen_turn_ids: HashSet::new(), turn_line_counts: HashMap::new(), + agent_name, } } @@ -140,7 +143,7 @@ impl ChatView { if self.frozen_turn_ids.contains(&turn.id) { continue; } - let render = Self::render_turn_to_lines(turn, self.width); + let render = Self::render_turn_to_lines(turn, self.width, &self.agent_name); self.turn_line_counts.insert(turn.id, render.len()); active_lines.extend(render); } @@ -214,7 +217,7 @@ impl ChatView { } /// Render a turn to lines (header + content + separator) - fn render_turn_to_lines(turn: &Turn, width: u16) -> Vec> { + fn render_turn_to_lines(turn: &Turn, width: u16, agent_name: &str) -> Vec> { #[cfg(feature = "profiling")] let _span = profile_span!("ChatView::render_turn_to_lines"); @@ -223,19 +226,19 @@ impl ChatView { // Role header let (role_text, role_style) = match turn.role { Role::User => ( - "You", + "You".to_string(), Style::default() .fg(Color::Green) .add_modifier(Modifier::BOLD), ), Role::Assistant => ( - "Codey", + agent_name.to_string(), Style::default() .fg(Color::Cyan) .add_modifier(Modifier::BOLD), ), Role::System => ( - "System", + "System".to_string(), Style::default() .fg(Color::Yellow) .add_modifier(Modifier::BOLD), From 2657bc96c95aaf556174c4930c54b90d6e242ecb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Jan 2026 16:10:17 +0000 Subject: [PATCH 2/2] Update Cargo.lock (remove unused patch) https://claude.ai/code/session_01UZjkuMRFsSvatYjoZAAmA6 --- Cargo.lock | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2db172..e60ca8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5239,7 +5239,3 @@ name = "zmij" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" - -[[patch.unused]] -name = "ratatui-core" -version = "0.1.0-beta.0"