Skip to main content

lean_ctx/server/
prompts.rs

1use rmcp::model::{
2    GetPromptRequestParams, GetPromptResult, Prompt, PromptArgument, PromptMessage,
3    PromptMessageRole,
4};
5
6fn required_arg(name: &str, desc: &str) -> PromptArgument {
7    PromptArgument::new(name)
8        .with_description(desc)
9        .with_required(true)
10}
11
12pub fn list_prompts() -> Vec<Prompt> {
13    vec![
14        Prompt::new(
15            "context-focus",
16            Some("Set task intent and optimize context for a specific task"),
17            Some(vec![required_arg("task", "What you are working on")]),
18        ),
19        Prompt::new(
20            "context-review",
21            Some("Review current context state: items, pressure, budget, recommendations"),
22            None,
23        ),
24        Prompt::new(
25            "context-reset",
26            Some("Clear all overlays and reset context state"),
27            None,
28        ),
29        Prompt::new(
30            "context-pin",
31            Some("Pin a file to keep it in full context"),
32            Some(vec![required_arg("path", "Path to the file to pin")]),
33        ),
34        Prompt::new(
35            "context-budget",
36            Some("Set the token budget for this session"),
37            Some(vec![required_arg(
38                "tokens",
39                "Max tokens for the context window",
40            )]),
41        ),
42    ]
43}
44
45pub fn get_prompt(
46    params: &GetPromptRequestParams,
47    ledger: &crate::core::context_ledger::ContextLedger,
48) -> Option<GetPromptResult> {
49    match params.name.as_str() {
50        "context-focus" => {
51            let task = params
52                .arguments
53                .as_ref()
54                .and_then(|a| a.get("task"))
55                .and_then(|v| v.as_str())
56                .unwrap_or("general development");
57            Some(get_context_focus(task, ledger))
58        }
59        "context-review" => Some(get_context_review(ledger)),
60        "context-reset" => Some(get_context_reset()),
61        "context-pin" => {
62            let path = params
63                .arguments
64                .as_ref()
65                .and_then(|a| a.get("path"))
66                .and_then(|v| v.as_str())
67                .unwrap_or("");
68            Some(get_context_pin(path))
69        }
70        "context-budget" => {
71            let tokens = params
72                .arguments
73                .as_ref()
74                .and_then(|a| a.get("tokens"))
75                .and_then(|v| v.as_str())
76                .unwrap_or("128000");
77            Some(get_context_budget(tokens))
78        }
79        _ => None,
80    }
81}
82
83fn get_context_focus(
84    task: &str,
85    ledger: &crate::core::context_ledger::ContextLedger,
86) -> GetPromptResult {
87    let pressure = ledger.pressure();
88    let msg = format!(
89        "Focus context on task: {task}\n\
90         Current state: {} files, {:.0}% pressure\n\
91         Use ctx_plan(task=\"{task}\") to compute optimal modes for all tracked files.\n\
92         Files matching this task's intent targets should be read as 'full', others compressed.",
93        ledger.entries.len(),
94        pressure.utilization * 100.0,
95    );
96    GetPromptResult::new(vec![PromptMessage::new_text(
97        PromptMessageRole::Assistant,
98        msg,
99    )])
100}
101
102fn get_context_review(ledger: &crate::core::context_ledger::ContextLedger) -> GetPromptResult {
103    let summary = ledger.format_summary();
104    let adjusted = ledger.adjusted_total_saved();
105    let bounce_info = if let Ok(bt) = crate::core::bounce_tracker::global().lock() {
106        bt.format_summary()
107    } else {
108        String::new()
109    };
110    let msg = format!(
111        "Context Review:\n{summary}\nAdjusted savings: {adjusted} tokens\n{bounce_info}\n\n\
112         Use ctx_metrics() for detailed breakdown or ctx_plan(task=\"review context state\") for mode recommendations.",
113    );
114    GetPromptResult::new(vec![PromptMessage::new_text(
115        PromptMessageRole::Assistant,
116        msg,
117    )])
118}
119
120fn get_context_reset() -> GetPromptResult {
121    GetPromptResult::new(vec![PromptMessage::new_text(
122        PromptMessageRole::Assistant,
123        "Reset context: Use ctx_control(action=\"reset\") to clear all overlays and reset ledger states.",
124    )])
125}
126
127fn get_context_pin(path: &str) -> GetPromptResult {
128    let msg = format!(
129        "Pin file: Use ctx_control(action=\"pin\", target=\"{path}\") to keep this file in full context regardless of pressure."
130    );
131    GetPromptResult::new(vec![PromptMessage::new_text(
132        PromptMessageRole::Assistant,
133        msg,
134    )])
135}
136
137fn get_context_budget(tokens: &str) -> GetPromptResult {
138    let msg = format!(
139        "Set budget: Configure the context window to {tokens} tokens. \
140         Use ctx_session(action=\"budget\", value=\"{tokens}\") or set LCTX_CONTEXT_BUDGET={tokens} in your environment."
141    );
142    GetPromptResult::new(vec![PromptMessage::new_text(
143        PromptMessageRole::Assistant,
144        msg,
145    )])
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn list_returns_five_prompts() {
154        let prompts = list_prompts();
155        assert_eq!(prompts.len(), 5);
156    }
157
158    #[test]
159    fn context_focus_has_task_arg() {
160        let prompts = list_prompts();
161        let focus = prompts.iter().find(|p| p.name == "context-focus").unwrap();
162        let args = focus.arguments.as_ref().unwrap();
163        assert_eq!(args[0].name, "task");
164    }
165
166    #[test]
167    fn get_unknown_prompt_returns_none() {
168        let ledger = crate::core::context_ledger::ContextLedger::new();
169        let params = GetPromptRequestParams::new("unknown-prompt");
170        assert!(get_prompt(&params, &ledger).is_none());
171    }
172
173    #[test]
174    fn get_context_review_returns_result() {
175        let ledger = crate::core::context_ledger::ContextLedger::new();
176        let params = GetPromptRequestParams::new("context-review");
177        let result = get_prompt(&params, &ledger);
178        assert!(result.is_some());
179    }
180}