Skip to main content

deepseek_rust_cli/tui/
app.rs

1use std::time::Instant;
2
3use crate::api::types::TokenUsage;
4
5pub fn load_global_history() -> Vec<String> {
6    let path = std::path::PathBuf::from(".deep/input_history.json");
7    if let Ok(content) = std::fs::read_to_string(path) {
8        if let Ok(history) = serde_json::from_str::<Vec<String>>(&content) {
9            return history;
10        }
11    }
12    Vec::new()
13}
14
15pub fn save_global_history(history: &[String]) {
16    let path = std::path::PathBuf::from(".deep/input_history.json");
17    if let Some(parent) = path.parent() {
18        let _ = std::fs::create_dir_all(parent);
19    }
20    if let Ok(json) = serde_json::to_string_pretty(history) {
21        let _ = std::fs::write(path, json);
22    }
23}
24
25pub struct App {
26    pub input: String,
27    /// Byte position of cursor within input (0 = start)
28    pub cursor_pos: usize,
29    pub awaiting_approval: bool,
30    pub spinner_frame: usize,
31    pub current_task: Option<String>,
32    /// When the current *task label* last changed (for display only)
33    pub task_start_time: Option<Instant>,
34    /// When the entire agent job started (never reset until finish_task)
35    pub job_start_time: Option<Instant>,
36    pub cwd: String,
37    pub model: String,
38    pub history: Vec<String>,
39    pub history_index: Option<usize>,
40    /// Footer is always 4 lines: status, folder+token, input, queue
41    pub footer_height: u16,
42    pub token_usage: TokenUsage,
43    /// When true, ignore incoming AgentEvents (after abort)
44    pub aborted: bool,
45    /// Pending commands that were sent but not yet completed.
46    /// Index 0 is the currently-running command; the rest are queued.
47    pub queued_commands: Vec<String>,
48    pub log_x: u16,
49    pub log_y: u16,
50    pub reasoning_started: bool,
51    pub content_started: bool,
52    pub is_path_traversal_warning: bool,
53}
54
55impl Default for App {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl App {
62    pub fn new() -> Self {
63        let history = load_global_history();
64        Self {
65            input: String::new(),
66            cursor_pos: 0,
67            awaiting_approval: false,
68            spinner_frame: 0,
69            current_task: None,
70            task_start_time: None,
71            job_start_time: None,
72            cwd: std::env::current_dir()
73                .map(|p| p.display().to_string())
74                .unwrap_or_else(|_| ".".to_string()),
75            model: String::from("unknown"),
76            history,
77            history_index: None,
78            footer_height: 4, // Fixed: Status, Folder+Token, Input, Queue
79            token_usage: TokenUsage::default(),
80            aborted: false,
81            queued_commands: Vec::new(),
82            log_x: 0,
83            log_y: 0,
84            reasoning_started: false,
85            content_started: false,
86            is_path_traversal_warning: false,
87        }
88    }
89
90    pub fn next_history(&mut self) {
91        if self.history.is_empty() {
92            return;
93        }
94        let next_index = match self.history_index {
95            Some(i) => {
96                if i > 0 {
97                    Some(i - 1)
98                } else {
99                    Some(0)
100                }
101            }
102            None => Some(self.history.len().saturating_sub(1)),
103        };
104        if let Some(idx) = next_index {
105            self.history_index = Some(idx);
106            self.input = self.history[idx].clone();
107            self.cursor_pos = self.input.len();
108        }
109    }
110
111    pub fn prev_history(&mut self) {
112        if self.history.is_empty() {
113            return;
114        }
115        let next_index = match self.history_index {
116            Some(i) => {
117                if i < self.history.len().saturating_sub(1) {
118                    Some(i + 1)
119                } else {
120                    self.input.clear();
121                    self.cursor_pos = 0;
122                    None
123                }
124            }
125            None => None,
126        };
127        self.history_index = next_index;
128        if let Some(idx) = self.history_index {
129            self.input = self.history[idx].clone();
130            self.cursor_pos = self.input.len();
131        }
132    }
133
134    pub fn start_task(&mut self, task: String) {
135        // Set job-wide timer once at the beginning of the agent job
136        if self.job_start_time.is_none() {
137            self.job_start_time = Some(Instant::now());
138        }
139        if self.current_task.as_ref() != Some(&task) {
140            self.current_task = Some(task);
141            self.task_start_time = Some(Instant::now());
142        }
143    }
144
145    pub fn finish_task(&mut self) {
146        self.current_task = None;
147        self.task_start_time = None;
148        self.job_start_time = None;
149        self.awaiting_approval = false;
150        self.is_path_traversal_warning = false;
151        self.aborted = false;
152        // Pop the just-completed command from queue
153        if !self.queued_commands.is_empty() {
154            self.queued_commands.remove(0);
155        }
156    }
157
158    pub fn tick(&mut self) {
159        self.spinner_frame = self.spinner_frame.wrapping_add(1);
160    }
161
162    /// Total tokens used so far
163    pub fn total_tokens(&self) -> u64 {
164        self.token_usage.prompt_tokens + self.token_usage.completion_tokens
165    }
166}