#![allow(dead_code)]
use crate::models::SystemPrompt;
use crate::project_context::{ProjectContext, load_project_context_with_parents};
use crate::tui::app::AppMode;
use std::path::Path;
pub const HANDOFF_RELATIVE_PATH: &str = ".deepseek/handoff.md";
fn load_handoff_block(workspace: &Path) -> Option<String> {
let path = workspace.join(HANDOFF_RELATIVE_PATH);
let raw = std::fs::read_to_string(&path).ok()?;
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
Some(format!(
"## Previous Session Handoff\n\nThe previous session in this workspace left a handoff at `{}`. Consider it the first artifact to read on this turn — open blockers, in-flight changes, and recent decisions live there. Update or rewrite it before exiting if state changes materially.\n\n{}",
HANDOFF_RELATIVE_PATH, trimmed
))
}
pub const BASE_PROMPT: &str = include_str!("prompts/base.md");
pub const CALM_PERSONALITY: &str = include_str!("prompts/personalities/calm.md");
pub const PLAYFUL_PERSONALITY: &str = include_str!("prompts/personalities/playful.md");
pub const AGENT_MODE: &str = include_str!("prompts/modes/agent.md");
pub const PLAN_MODE: &str = include_str!("prompts/modes/plan.md");
pub const YOLO_MODE: &str = include_str!("prompts/modes/yolo.md");
pub const AUTO_APPROVAL: &str = include_str!("prompts/approvals/auto.md");
pub const SUGGEST_APPROVAL: &str = include_str!("prompts/approvals/suggest.md");
pub const NEVER_APPROVAL: &str = include_str!("prompts/approvals/never.md");
pub const COMPACT_TEMPLATE: &str = include_str!("prompts/compact.md");
pub const AGENT_PROMPT: &str = include_str!("prompts/agent.txt");
pub const YOLO_PROMPT: &str = include_str!("prompts/yolo.txt");
pub const PLAN_PROMPT: &str = include_str!("prompts/plan.txt");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Personality {
Calm,
Playful,
}
impl Personality {
#[must_use]
pub fn from_settings(calm_mode: bool) -> Self {
if calm_mode {
Self::Calm
} else {
Self::Calm
}
}
fn prompt(self) -> &'static str {
match self {
Self::Calm => CALM_PERSONALITY,
Self::Playful => PLAYFUL_PERSONALITY,
}
}
}
fn mode_prompt(mode: AppMode) -> &'static str {
match mode {
AppMode::Agent => AGENT_MODE,
AppMode::Yolo => YOLO_MODE,
AppMode::Plan => PLAN_MODE,
}
}
fn approval_prompt(mode: AppMode) -> &'static str {
match mode {
AppMode::Agent => SUGGEST_APPROVAL,
AppMode::Yolo => AUTO_APPROVAL,
AppMode::Plan => NEVER_APPROVAL,
}
}
pub fn compose_prompt(mode: AppMode, personality: Personality) -> String {
let parts: [&str; 4] = [
BASE_PROMPT.trim(),
personality.prompt().trim(),
mode_prompt(mode).trim(),
approval_prompt(mode).trim(),
];
let mut out =
String::with_capacity(parts.iter().map(|p| p.len()).sum::<usize>() + (parts.len() - 1) * 2);
for (i, part) in parts.iter().enumerate() {
if i > 0 {
out.push('\n');
out.push('\n');
}
out.push_str(part);
}
out
}
fn compose_mode_prompt(mode: AppMode) -> String {
compose_prompt(mode, Personality::Calm)
}
pub fn system_prompt_for_mode(mode: AppMode) -> SystemPrompt {
SystemPrompt::Text(compose_mode_prompt(mode))
}
pub fn system_prompt_for_mode_with_personality(
mode: AppMode,
personality: Personality,
) -> SystemPrompt {
SystemPrompt::Text(compose_prompt(mode, personality))
}
pub fn system_prompt_for_mode_with_context(
mode: AppMode,
workspace: &Path,
working_set_summary: Option<&str>,
) -> SystemPrompt {
let mode_prompt = compose_mode_prompt(mode);
let project_context = load_project_context_with_parents(workspace);
let mut full_prompt = if let Some(project_block) = project_context.as_system_block() {
format!("{}\n\n{}", mode_prompt, project_block)
} else {
let summary = crate::utils::summarize_project(workspace);
let tree = crate::utils::project_tree(workspace, 2); format!(
"{}\n\n### Project Structure (Automatic Map)\n**Summary:** {}\n\n**Tree:**\n```\n{}\n```",
mode_prompt, summary, tree
)
};
if let Some(summary) = working_set_summary
&& !summary.trim().is_empty()
{
full_prompt = format!("{full_prompt}\n\n{summary}");
}
if let Some(handoff_block) = load_handoff_block(workspace) {
full_prompt = format!("{full_prompt}\n\n{handoff_block}");
}
if matches!(mode, AppMode::Agent | AppMode::Yolo) {
full_prompt.push_str(
"\n\n## Context Management\n\n\
When the conversation gets long (you'll see a context usage indicator), you can:\n\
1. Use `/compact` to summarize earlier context and free up space\n\
2. The system will preserve important information (files you're working on, recent messages, tool results)\n\
3. After compaction, you'll see a summary of what was discussed and can continue seamlessly\n\n\
If you notice context is getting long (>80%), proactively suggest using `/compact` to the user."
);
}
full_prompt.push_str("\n\n");
full_prompt.push_str(COMPACT_TEMPLATE);
SystemPrompt::Text(full_prompt)
}
pub fn build_system_prompt(base: &str, project_context: Option<&ProjectContext>) -> SystemPrompt {
let full_prompt =
match project_context.and_then(super::project_context::ProjectContext::as_system_block) {
Some(project_block) => format!("{}\n\n{}", base.trim(), project_block),
None => base.trim().to_string(),
};
SystemPrompt::Text(full_prompt)
}
pub fn base_system_prompt() -> SystemPrompt {
SystemPrompt::Text(BASE_PROMPT.trim().to_string())
}
pub fn normal_system_prompt() -> SystemPrompt {
system_prompt_for_mode(AppMode::Agent)
}
pub fn agent_system_prompt() -> SystemPrompt {
system_prompt_for_mode(AppMode::Agent)
}
pub fn yolo_system_prompt() -> SystemPrompt {
system_prompt_for_mode(AppMode::Yolo)
}
pub fn plan_system_prompt() -> SystemPrompt {
system_prompt_for_mode(AppMode::Plan)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
const HANDOFF_BLOCK_MARKER: &str = "left a handoff at `.deepseek/handoff.md`";
#[test]
fn handoff_artifact_is_prepended_to_system_prompt_when_present() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let handoff_dir = workspace.join(".deepseek");
std::fs::create_dir_all(&handoff_dir).unwrap();
std::fs::write(
handoff_dir.join("handoff.md"),
"# Session handoff — prior\n\n## Active task\nFinish #32.\n\n## Open blockers\n- [ ] write the basic version\n",
)
.unwrap();
let prompt = match system_prompt_for_mode_with_context(AppMode::Agent, workspace, None) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(prompt.contains(HANDOFF_BLOCK_MARKER));
assert!(prompt.contains("Finish #32."));
assert!(prompt.contains("write the basic version"));
}
#[test]
fn missing_handoff_does_not_inject_block() {
let tmp = tempdir().expect("tempdir");
let prompt = match system_prompt_for_mode_with_context(AppMode::Agent, tmp.path(), None) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(!prompt.contains(HANDOFF_BLOCK_MARKER));
}
#[test]
fn empty_handoff_file_does_not_inject_block() {
let tmp = tempdir().expect("tempdir");
let dir = tmp.path().join(".deepseek");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("handoff.md"), " \n\n ").unwrap();
let prompt = match system_prompt_for_mode_with_context(AppMode::Agent, tmp.path(), None) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(!prompt.contains(HANDOFF_BLOCK_MARKER));
}
#[test]
fn compose_prompt_includes_all_layers() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("You are DeepSeek TUI"));
assert!(prompt.contains("Personality: Calm"));
assert!(prompt.contains("Mode: Agent"));
assert!(prompt.contains("Approval Policy: Suggest"));
}
#[test]
fn compose_prompt_deterministic_order() {
let prompt = compose_prompt(AppMode::Yolo, Personality::Calm);
let base_pos = prompt.find("You are DeepSeek TUI").unwrap();
let personality_pos = prompt.find("Personality: Calm").unwrap();
let mode_pos = prompt.find("Mode: YOLO").unwrap();
let approval_pos = prompt.find("Approval Policy: Auto").unwrap();
assert!(base_pos < personality_pos);
assert!(personality_pos < mode_pos);
assert!(mode_pos < approval_pos);
}
#[test]
fn each_mode_gets_correct_approval() {
assert!(
compose_prompt(AppMode::Agent, Personality::Calm).contains("Approval Policy: Suggest")
);
assert!(compose_prompt(AppMode::Yolo, Personality::Calm).contains("Approval Policy: Auto"));
assert!(
compose_prompt(AppMode::Plan, Personality::Calm).contains("Approval Policy: Never")
);
}
#[test]
fn personality_switches_correctly() {
let calm = compose_prompt(AppMode::Agent, Personality::Calm);
let playful = compose_prompt(AppMode::Agent, Personality::Playful);
assert!(calm.contains("Personality: Calm"));
assert!(playful.contains("Personality: Playful"));
assert!(!calm.contains("Personality: Playful"));
}
#[test]
fn compact_template_is_included_in_full_prompt() {
let tmp = tempdir().expect("tempdir");
let prompt = match system_prompt_for_mode_with_context(AppMode::Agent, tmp.path(), None) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(prompt.contains("## Compaction Handoff"));
assert!(prompt.contains("### Active task"));
assert!(prompt.contains("### Files touched"));
assert!(prompt.contains("### Key decisions"));
assert!(prompt.contains("### Open blockers"));
assert!(prompt.contains("### Next step"));
}
#[test]
fn when_not_to_use_sections_present() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("When NOT to use certain tools"));
assert!(prompt.contains("### `apply_patch`"));
assert!(prompt.contains("### `edit_file`"));
assert!(prompt.contains("### `exec_shell`"));
assert!(prompt.contains("### `agent_spawn`"));
assert!(prompt.contains("### `rlm`"));
}
#[test]
fn subagent_done_sentinel_section_present() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("Sub-agent completion sentinel"));
assert!(prompt.contains("<deepseek:subagent.done>"));
assert!(prompt.contains("Integration protocol"));
}
#[test]
fn preamble_rhythm_section_present() {
let prompt = compose_prompt(AppMode::Agent, Personality::Calm);
assert!(prompt.contains("Preamble Rhythm"));
assert!(prompt.contains("I'll start by reading the module structure"));
}
#[test]
fn legacy_constants_still_available() {
assert!(!AGENT_PROMPT.is_empty());
assert!(!YOLO_PROMPT.is_empty());
assert!(!PLAN_PROMPT.is_empty());
}
}