lean_ctx/server/
prompts.rs1use 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(¶ms, &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(¶ms, &ledger);
178 assert!(result.is_some());
179 }
180}