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