返回 DeepSeek-TUI-2026
goal.rs
根目录 / crates / tui / src / commands / goal.rs
1 //! /goal command — set a session objective with token budget and progress tracking.
2
3 use crate::tui::app::App;
4
5 use super::CommandResult;
6
7 /// Set or show the current goal
8 pub fn goal(app: &mut App, arg: Option<&str>) -> CommandResult {
9 match arg {
10 Some("clear") | Some("reset") | Some("done") => {
11 app.goal.goal_objective = None;
12 app.goal.goal_token_budget = None;
13 app.goal.goal_started_at = None;
14 CommandResult::message("Goal cleared.")
15 }
16 Some(text) if !text.is_empty() => {
17 // Parse optional budget: "/goal Implement login | budget: 50000"
18 let (objective, budget) = parse_goal_budget(text);
19 app.goal.goal_objective = Some(objective.clone());
20 app.goal.goal_token_budget = budget;
21 app.goal.goal_started_at = Some(std::time::Instant::now());
22 let budget_str = budget
23 .map(|b| format!(" (budget: {b} tokens)"))
24 .unwrap_or_default();
25 CommandResult::message(format!(
26 "Goal set: \"{}\"{} — tracking progress.",
27 objective, budget_str
28 ))
29 }
30 _ => {
31 // Show current goal
32 if let Some(ref obj) = app.goal.goal_objective {
33 // #447: render long elapsed times as `2d 3h` rather
34 // than Rust's default Debug `Duration` (which produces
35 // `188415.234s` or similar for multi-day goals).
36 let elapsed = app
37 .goal
38 .goal_started_at
39 .map(|t| crate::tui::notifications::humanize_duration(t.elapsed()))
40 .unwrap_or_else(|| "unknown".to_string());
41 let budget_str = app
42 .goal
43 .goal_token_budget
44 .map(|b| {
45 let used = app.session.total_conversation_tokens;
46 let pct = if b > 0 {
47 (used as f64 / b as f64 * 100.0).min(100.0)
48 } else {
49 0.0
50 };
51 format!(" | tokens: {used}/{b} ({pct:.0}%)")
52 })
53 .unwrap_or_default();
54 CommandResult::message(format!("Goal: \"{obj}\" — elapsed: {elapsed}{budget_str}"))
55 } else {
56 CommandResult::message(
57 "No goal set. Use /goal <objective> [budget: N] to set one.\n\
58 /goal clear — remove the current goal.",
59 )
60 }
61 }
62 }
63 }
64
65 /// Parse optional token budget from goal text: "Implement login | budget: 50000"
66 fn parse_goal_budget(text: &str) -> (String, Option<u32>) {
67 if let Some((obj, rest)) = text.split_once(" | budget:") {
68 let budget = rest
69 .split_whitespace()
70 .next()
71 .and_then(|s| s.parse::<u32>().ok());
72 (obj.trim().to_string(), budget)
73 } else if let Some((obj, rest)) = text.split_once("budget:") {
74 let budget = rest
75 .split_whitespace()
76 .next()
77 .and_then(|s| s.parse::<u32>().ok());
78 (obj.trim().to_string(), budget)
79 } else {
80 (text.trim().to_string(), None)
81 }
82 }
83
84 #[cfg(test)]
85 mod tests {
86 use super::*;
87 use crate::config::Config;
88 use crate::tui::app::{App, TuiOptions};
89 use std::path::PathBuf;
90
91 fn create_test_app() -> App {
92 let options = TuiOptions {
93 model: "deepseek-v4-flash".to_string(),
94 workspace: PathBuf::from("."),
95 config_path: None,
96 config_profile: None,
97 allow_shell: false,
98 use_alt_screen: true,
99 use_mouse_capture: false,
100 use_bracketed_paste: true,
101 max_subagents: 1,
102 skills_dir: PathBuf::from("."),
103 memory_path: PathBuf::from("memory.md"),
104 notes_path: PathBuf::from("notes.txt"),
105 mcp_config_path: PathBuf::from("mcp.json"),
106 use_memory: false,
107 start_in_agent_mode: true,
108 skip_onboarding: true,
109 yolo: false,
110 resume_session_id: None,
111 initial_input: None,
112 };
113 App::new(options, &Config::default())
114 }
115
116 #[test]
117 fn test_set_goal() {
118 let mut app = create_test_app();
119 let result = goal(&mut app, Some("Fix the login bug"));
120 assert!(result.message.unwrap().contains("Goal set"));
121 assert_eq!(
122 app.goal.goal_objective.as_deref(),
123 Some("Fix the login bug")
124 );
125 }
126
127 #[test]
128 fn test_set_goal_with_budget() {
129 let mut app = create_test_app();
130 let _ = goal(&mut app, Some("Refactor auth | budget: 50000"));
131 assert_eq!(app.goal.goal_objective.as_deref(), Some("Refactor auth"));
132 assert_eq!(app.goal.goal_token_budget, Some(50_000));
133 }
134
135 #[test]
136 fn test_clear_goal() {
137 let mut app = create_test_app();
138 app.goal.goal_objective = Some("test".to_string());
139 let _ = goal(&mut app, Some("clear"));
140 assert!(app.goal.goal_objective.is_none());
141 assert!(app.goal.goal_token_budget.is_none());
142 }
143
144 #[test]
145 fn test_show_goal_when_none() {
146 let mut app = create_test_app();
147 let result = goal(&mut app, None);
148 assert!(result.message.unwrap().contains("No goal set"));
149 }
150
151 #[test]
152 fn test_parse_budget() {
153 assert_eq!(
154 parse_goal_budget("Do a thing | budget: 50000"),
155 ("Do a thing".to_string(), Some(50_000))
156 );
157 assert_eq!(
158 parse_goal_budget("Simple goal"),
159 ("Simple goal".to_string(), None)
160 );
161 assert_eq!(
162 parse_goal_budget("Goal budget:1000"),
163 ("Goal".to_string(), Some(1000))
164 );
165 }
166 }
167
167 lines RUST