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 = match crate::core::bounce_tracker::global().lock() {
106 Ok(bt) => bt.format_summary(),
107 _ => String::new(),
108 };
109 let msg = format!(
110 "Context Review:\n{summary}\nAdjusted savings: {adjusted} tokens\n{bounce_info}\n\n\
111 Use ctx_metrics() for detailed breakdown or ctx_plan(task=\"review context state\") for mode recommendations.",
112 );
113 GetPromptResult::new(vec![PromptMessage::new_text(
114 PromptMessageRole::Assistant,
115 msg,
116 )])
117}
118
119fn get_context_reset() -> GetPromptResult {
120 GetPromptResult::new(vec![PromptMessage::new_text(
121 PromptMessageRole::Assistant,
122 "Reset context: Use ctx_control(action=\"reset\") to clear all overlays and reset ledger states.",
123 )])
124}
125
126fn get_context_pin(path: &str) -> GetPromptResult {
127 let msg = format!(
128 "Pin file: Use ctx_control(action=\"pin\", target=\"{path}\") to keep this file in full context regardless of pressure."
129 );
130 GetPromptResult::new(vec![PromptMessage::new_text(
131 PromptMessageRole::Assistant,
132 msg,
133 )])
134}
135
136fn get_context_budget(tokens: &str) -> GetPromptResult {
137 let msg = format!(
138 "Set budget: Configure the context window to {tokens} tokens. \
139 Use ctx_session(action=\"budget\", value=\"{tokens}\") or set LCTX_CONTEXT_BUDGET={tokens} in your environment."
140 );
141 GetPromptResult::new(vec![PromptMessage::new_text(
142 PromptMessageRole::Assistant,
143 msg,
144 )])
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn list_returns_five_prompts() {
153 let prompts = list_prompts();
154 assert_eq!(prompts.len(), 5);
155 }
156
157 #[test]
158 fn context_focus_has_task_arg() {
159 let prompts = list_prompts();
160 let focus = prompts.iter().find(|p| p.name == "context-focus").unwrap();
161 let args = focus.arguments.as_ref().unwrap();
162 assert_eq!(args[0].name, "task");
163 }
164
165 #[test]
166 fn get_unknown_prompt_returns_none() {
167 let ledger = crate::core::context_ledger::ContextLedger::new();
168 let params = GetPromptRequestParams::new("unknown-prompt");
169 assert!(get_prompt(¶ms, &ledger).is_none());
170 }
171
172 #[test]
173 fn get_context_review_returns_result() {
174 let ledger = crate::core::context_ledger::ContextLedger::new();
175 let params = GetPromptRequestParams::new("context-review");
176 let result = get_prompt(¶ms, &ledger);
177 assert!(result.is_some());
178 }
179}