Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 7 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()),
Expand All @@ -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);

Expand Down
57 changes: 57 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(),
Expand Down Expand Up @@ -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<String>,
/// Custom system prompt intro/personality (replaces the default "You are Codey..." paragraph)
pub system_prompt: Option<String>,
}

#[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)]
Expand Down Expand Up @@ -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");
}
}
84 changes: 77 additions & 7 deletions src/prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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)
///
Expand All @@ -125,29 +164,60 @@ You have read-only access to:
pub struct SystemPrompt {
user_path: Option<PathBuf>,
project_path: PathBuf,
/// Custom agent name (None = use default "Codey")
agent_name: Option<String>,
/// Custom system prompt intro (None = use default)
custom_intro: Option<String>,
}

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);

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 {
Expand Down
15 changes: 9 additions & 6 deletions src/ui/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ pub struct ChatView {
frozen_turn_ids: HashSet<usize>,
/// Mapping of turn ID to line count (for frozen turns)
turn_line_counts: HashMap<usize, usize>,
/// 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,
Expand All @@ -62,6 +64,7 @@ impl ChatView {
committed_count: 0,
frozen_turn_ids: HashSet::new(),
turn_line_counts: HashMap::new(),
agent_name,
}
}

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -214,7 +217,7 @@ impl ChatView {
}

/// Render a turn to lines (header + content + separator)
fn render_turn_to_lines(turn: &Turn, width: u16) -> Vec<Line<'static>> {
fn render_turn_to_lines(turn: &Turn, width: u16, agent_name: &str) -> Vec<Line<'static>> {
#[cfg(feature = "profiling")]
let _span = profile_span!("ChatView::render_turn_to_lines");

Expand All @@ -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),
Expand Down