返回 CodeWhale
rename.rs
根目录 / crates / tui / src / commands / groups / session / rename.rs
1 //! `/rename` command — set a custom title for the current session.
2
3 use crate::commands::traits::{CommandInfo, RegisterCommand};
4 use crate::localization::MessageId;
5 use crate::session_manager::{SessionManager, update_session};
6 use crate::tui::app::App;
7
8 use super::CommandResult;
9
10 const MAX_TITLE_LEN: usize = 100;
11
12 pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
13 name: "rename",
14 aliases: &["gaiming", "chongmingming"],
15 usage: "/rename <new title>",
16 description_id: MessageId::CmdRenameDescription,
17 };
18
19 pub(in crate::commands) struct RenameCmd;
20
21 impl RegisterCommand for RenameCmd {
22 fn info() -> &'static CommandInfo {
23 &COMMAND_INFO
24 }
25
26 fn execute(app: &mut App, arg: Option<&str>) -> CommandResult {
27 rename(app, arg)
28 }
29 }
30
31 /// Rename the current session to the given title.
32 ///
33 /// Usage: `/rename <new title>`
34 ///
35 /// The new title is persisted immediately to `~/.deepseek/sessions/<id>.json`
36 /// so the updated name is visible the next time the session picker is opened.
37 pub fn rename(app: &mut App, arg: Option<&str>) -> CommandResult {
38 let new_title = match arg.map(str::trim).filter(|s| !s.is_empty()) {
39 Some(t) => t,
40 None => return CommandResult::error("Usage: /rename <new title>"),
41 };
42
43 if new_title.chars().count() > MAX_TITLE_LEN {
44 return CommandResult::error(format!("Title too long (max {MAX_TITLE_LEN} characters)"));
45 }
46
47 let session_id = match &app.current_session_id {
48 Some(id) => id.clone(),
49 None => {
50 return CommandResult::error(
51 "No active session. Send a message first to start a session.",
52 );
53 }
54 };
55
56 let manager = match SessionManager::default_location() {
57 Ok(m) => m,
58 Err(e) => return CommandResult::error(format!("Could not open sessions directory: {e}")),
59 };
60
61 rename_with_manager(new_title, &session_id, &manager, app)
62 }
63
64 pub(crate) fn rename_with_manager(
65 new_title: &str,
66 session_id: &str,
67 manager: &SessionManager,
68 app: &mut App,
69 ) -> CommandResult {
70 let mut session = match manager.load_session(session_id) {
71 Ok(s) => s,
72 Err(e) => return CommandResult::error(format!("Could not load session: {e}")),
73 };
74
75 // Sync with current App state to avoid overwriting unsaved messages.
76 session = update_session(
77 session,
78 &app.api_messages,
79 u64::from(app.session.total_tokens),
80 app.system_prompt.as_ref(),
81 );
82 session.work_state = match app.work_state_snapshot() {
83 Ok(state) => state,
84 Err(err) => {
85 return CommandResult::error(format!(
86 "Could not snapshot Work state before rename: {err}"
87 ));
88 }
89 };
90 session.context_references = app.session_context_references.clone();
91 session.artifacts = app.session_artifacts.clone();
92 session.last_auto_route = app.auto_route_for_persistence();
93 session.metadata.model = app.model_selection_for_persistence();
94 session
95 .metadata
96 .set_model_provider_route(app.api_provider.as_str(), app.provider_id_for_persistence());
97 session.metadata.workspace.clone_from(&app.workspace);
98 session.metadata.mode = Some(app.mode.as_setting().to_string());
99 app.sync_cost_to_metadata(&mut session.metadata);
100 session.metadata.title = new_title.to_string();
101
102 match manager.save_session(&session) {
103 Ok(_) => {
104 app.current_session_metadata = Some(session.metadata.clone());
105 app.session_title = Some(new_title.to_string());
106 if let Err(err) = app.publish_pending_work_state() {
107 return CommandResult::error(format!(
108 "Session renamed, but Work views were not published: {err}"
109 ));
110 }
111 CommandResult::message(format!("Session renamed to \"{new_title}\""))
112 }
113 Err(e) => CommandResult::error(format!("Could not save session: {e}")),
114 }
115 }
116
117 #[cfg(test)]
118 mod tests {
119 use super::*;
120 use crate::config::Config;
121 use crate::session_manager::{SessionManager, create_saved_session_with_mode};
122 use crate::tui::app::{App, TuiOptions};
123 use tempfile::TempDir;
124
125 fn make_app(tmpdir: &TempDir) -> App {
126 App::new(
127 TuiOptions {
128 skills_dir: tmpdir.path().join("skills"),
129 memory_path: tmpdir.path().join("memory.md"),
130 notes_path: tmpdir.path().join("notes.txt"),
131 mcp_config_path: tmpdir.path().join("mcp.json"),
132 ..crate::test_support::test_tui_options(tmpdir.path())
133 },
134 &Config::default(),
135 )
136 }
137
138 fn make_session_manager(tmpdir: &TempDir) -> SessionManager {
139 SessionManager::new(tmpdir.path().join("sessions")).unwrap()
140 }
141
142 #[test]
143 fn rename_without_arg_returns_error() {
144 let tmp = TempDir::new().unwrap();
145 let mut app = make_app(&tmp);
146 let r = rename(&mut app, None);
147 assert!(r.is_error);
148 assert!(r.message.unwrap().contains("Usage:"));
149 }
150
151 #[test]
152 fn rename_with_empty_arg_returns_error() {
153 let tmp = TempDir::new().unwrap();
154 let mut app = make_app(&tmp);
155 let r = rename(&mut app, Some(" "));
156 assert!(r.is_error);
157 assert!(r.message.unwrap().contains("Usage:"));
158 }
159
160 #[test]
161 fn rename_without_active_session_returns_error() {
162 let tmp = TempDir::new().unwrap();
163 let mut app = make_app(&tmp);
164 app.current_session_id = None;
165 let r = rename(&mut app, Some("My Session"));
166 assert!(r.is_error);
167 assert!(r.message.unwrap().contains("No active session"));
168 }
169
170 #[test]
171 fn rename_title_too_long_returns_error() {
172 let tmp = TempDir::new().unwrap();
173 let mut app = make_app(&tmp);
174 let long_title = "a".repeat(MAX_TITLE_LEN + 1);
175 let r = rename(&mut app, Some(&long_title));
176 assert!(r.is_error);
177 assert!(r.message.unwrap().contains("too long"));
178 }
179
180 #[test]
181 fn rename_persists_new_title() {
182 let tmp = TempDir::new().unwrap();
183 let manager = make_session_manager(&tmp);
184 let mut app = make_app(&tmp);
185
186 let stale_prompt = crate::models::SystemPrompt::Text("stale prompt".to_string());
187 let session = create_saved_session_with_mode(
188 &[],
189 "deepseek-v4-pro",
190 tmp.path(),
191 0,
192 Some(&stale_prompt),
193 None,
194 );
195 let session_id = session.metadata.id.clone();
196 manager.save_session(&session).unwrap();
197 app.set_model_selection("local-code-model".to_string());
198 app.set_provider_identity(crate::config::ApiProvider::Custom, "lm-studio");
199 app.mode = crate::tui::app::AppMode::Operate;
200 app.system_prompt = None;
201 {
202 let mut todos = app.todos.try_lock().expect("todos lock");
203 todos.add(
204 "live rename state".to_string(),
205 crate::tools::todo::TodoStatus::InProgress,
206 );
207 }
208 let expected_work_state = app.work_state_snapshot().expect("work snapshot");
209
210 let result = rename_with_manager("Brand New Title", &session_id, &manager, &mut app);
211 assert!(!result.is_error);
212 assert!(result.message.unwrap().contains("Brand New Title"));
213
214 let reloaded = manager.load_session(&session_id).unwrap();
215 assert_eq!(reloaded.metadata.title, "Brand New Title");
216 assert_eq!(reloaded.work_state, expected_work_state);
217 assert!(reloaded.system_prompt.is_none());
218 assert_eq!(reloaded.metadata.model, "local-code-model");
219 assert_eq!(reloaded.metadata.model_provider, "custom");
220 assert_eq!(
221 reloaded.metadata.model_provider_id.as_deref(),
222 Some("lm-studio")
223 );
224 assert_eq!(reloaded.metadata.workspace, app.workspace);
225 assert_eq!(reloaded.metadata.mode.as_deref(), Some("operate"));
226 assert_eq!(app.session_title.as_deref(), Some("Brand New Title"));
227 assert_eq!(
228 app.current_session_metadata
229 .as_ref()
230 .map(|metadata| metadata.title.as_str()),
231 Some("Brand New Title")
232 );
233 }
234
235 #[test]
236 fn rename_title_at_max_length_succeeds() {
237 let tmp = TempDir::new().unwrap();
238 let manager = make_session_manager(&tmp);
239 let mut app = make_app(&tmp);
240
241 let session =
242 create_saved_session_with_mode(&[], "deepseek-v4-pro", tmp.path(), 0, None, None);
243 let session_id = session.metadata.id.clone();
244 manager.save_session(&session).unwrap();
245
246 let max_title = "中".repeat(MAX_TITLE_LEN);
247 let result = rename_with_manager(&max_title, &session_id, &manager, &mut app);
248 assert!(!result.is_error);
249
250 let reloaded = manager.load_session(&session_id).unwrap();
251 assert_eq!(reloaded.metadata.title, max_title);
252 }
253 }
254
254 lines RUST