Skip to repository content725 lines · 25.7 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:05:13.936Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
terminal_thread_metadata_store.rs
1use std::path::{Path, PathBuf};
2
3use anyhow::Context as _;
4use chrono::{DateTime, Utc};
5use collections::{HashMap, HashSet};
6use db::{
7 sqlez::{
8 bindable::Column, domain::Domain, statement::Statement,
9 thread_safe_connection::ThreadSafeConnection,
10 },
11 sqlez_macros::sql,
12};
13use futures::{FutureExt, future::Shared};
14use gpui::{AppContext as _, Entity, Global, Task};
15use remote::{RemoteConnectionOptions, same_remote_connection_identity};
16use ui::{App, Context, SharedString};
17use util::ResultExt as _;
18use workspace::PathList;
19
20use crate::{TerminalId, thread_metadata_store::WorktreePaths};
21
22pub fn init(cx: &mut App) {
23 TerminalThreadMetadataStore::init_global(cx);
24}
25
26struct GlobalTerminalThreadMetadataStore(Entity<TerminalThreadMetadataStore>);
27impl Global for GlobalTerminalThreadMetadataStore {}
28
29#[cfg(any(test, feature = "test-support"))]
30pub struct TestTerminalMetadataDbName(pub String);
31#[cfg(any(test, feature = "test-support"))]
32impl Global for TestTerminalMetadataDbName {}
33
34#[cfg(any(test, feature = "test-support"))]
35impl TestTerminalMetadataDbName {
36 pub fn global(cx: &App) -> String {
37 cx.try_global::<Self>()
38 .map(|global| global.0.clone())
39 .unwrap_or_else(|| {
40 let thread = std::thread::current();
41 let test_name = thread.name().unwrap_or("unknown_test");
42 format!("TERMINAL_THREAD_METADATA_DB_{}", test_name)
43 })
44 }
45}
46
47#[derive(Debug, Clone, PartialEq)]
48pub struct TerminalThreadMetadata {
49 pub terminal_id: TerminalId,
50 pub title: SharedString,
51 pub custom_title: Option<SharedString>,
52 pub created_at: DateTime<Utc>,
53 pub worktree_paths: WorktreePaths,
54 pub remote_connection: Option<RemoteConnectionOptions>,
55 pub working_directory: Option<PathBuf>,
56}
57
58impl TerminalThreadMetadata {
59 pub fn folder_paths(&self) -> &PathList {
60 self.worktree_paths.folder_path_list()
61 }
62
63 pub fn main_worktree_paths(&self) -> &PathList {
64 self.worktree_paths.main_worktree_path_list()
65 }
66
67 pub fn display_title(&self) -> SharedString {
68 compose_terminal_thread_title(
69 self.title.as_ref(),
70 self.custom_title.as_ref().map(|title| title.as_ref()),
71 )
72 }
73}
74
75pub(crate) fn compose_terminal_thread_title(
76 terminal_title: &str,
77 custom_title: Option<&str>,
78) -> SharedString {
79 let Some(custom_title) = custom_title.filter(|title| !title.trim().is_empty()) else {
80 return SharedString::from(terminal_title.to_string());
81 };
82
83 if let Some(prefix) = terminal_title_prefix(terminal_title) {
84 SharedString::from(format!("{prefix}{custom_title}"))
85 } else {
86 SharedString::from(custom_title.to_string())
87 }
88}
89
90pub(crate) fn terminal_title_without_prefix(title: &str) -> &str {
91 terminal_title_prefix(title)
92 .map(|prefix| &title[prefix.len()..])
93 .unwrap_or(title)
94}
95
96pub fn terminal_title_prefix(title: &str) -> Option<&str> {
97 let mut prefix_byte_len = 0;
98 let mut saw_prefix_character = false;
99 let mut saw_whitespace_after_prefix = false;
100
101 let mut chars = title.chars().peekable();
102 while let Some(character) = chars.next() {
103 if character.is_alphanumeric() {
104 return None;
105 }
106
107 if character.is_whitespace() {
108 if !saw_prefix_character {
109 return None;
110 }
111
112 prefix_byte_len += character.len_utf8();
113 saw_whitespace_after_prefix = true;
114
115 while let Some(character) = chars.peek() {
116 if !character.is_whitespace() {
117 break;
118 }
119
120 prefix_byte_len += character.len_utf8();
121 chars.next();
122 }
123
124 break;
125 }
126
127 saw_prefix_character = true;
128 prefix_byte_len += character.len_utf8();
129 }
130
131 if saw_whitespace_after_prefix {
132 Some(&title[..prefix_byte_len])
133 } else {
134 None
135 }
136}
137
138pub struct TerminalThreadMetadataStore {
139 db: TerminalThreadMetadataDb,
140 terminals: HashMap<TerminalId, TerminalThreadMetadata>,
141 terminals_by_paths: HashMap<PathList, HashSet<TerminalId>>,
142 terminals_by_main_paths: HashMap<PathList, HashSet<TerminalId>>,
143 reload_task: Option<Shared<Task<()>>>,
144 pending_terminal_ops_tx: async_channel::Sender<DbOperation>,
145 _db_operations_task: Task<()>,
146}
147
148#[derive(Debug, PartialEq)]
149enum DbOperation {
150 Upsert(TerminalThreadMetadata),
151 Delete(TerminalId),
152}
153
154impl DbOperation {
155 fn id(&self) -> TerminalId {
156 match self {
157 DbOperation::Upsert(metadata) => metadata.terminal_id,
158 DbOperation::Delete(terminal_id) => *terminal_id,
159 }
160 }
161}
162
163impl TerminalThreadMetadataStore {
164 #[cfg(not(any(test, feature = "test-support")))]
165 pub fn init_global(cx: &mut App) {
166 if cx.has_global::<GlobalTerminalThreadMetadataStore>() {
167 return;
168 }
169
170 let db = TerminalThreadMetadataDb::global(cx);
171 let terminal_store = cx.new(|cx| Self::new(db, cx));
172 cx.set_global(GlobalTerminalThreadMetadataStore(terminal_store));
173 }
174
175 #[cfg(any(test, feature = "test-support"))]
176 pub fn init_global(cx: &mut App) {
177 let db_name = TestTerminalMetadataDbName::global(cx);
178 let db = gpui::block_on(db::open_test_db::<TerminalThreadMetadataDb>(&db_name));
179 let terminal_store = cx.new(|cx| Self::new(TerminalThreadMetadataDb(db), cx));
180 cx.set_global(GlobalTerminalThreadMetadataStore(terminal_store));
181 }
182
183 pub fn try_global(cx: &App) -> Option<Entity<Self>> {
184 cx.try_global::<GlobalTerminalThreadMetadataStore>()
185 .map(|store| store.0.clone())
186 }
187
188 pub fn global(cx: &App) -> Entity<Self> {
189 cx.global::<GlobalTerminalThreadMetadataStore>().0.clone()
190 }
191
192 pub fn entry(&self, terminal_id: TerminalId) -> Option<&TerminalThreadMetadata> {
193 self.terminals.get(&terminal_id)
194 }
195
196 pub fn entries(&self) -> impl Iterator<Item = &TerminalThreadMetadata> + '_ {
197 self.terminals.values()
198 }
199
200 pub fn reload_task(&self) -> Shared<Task<()>> {
201 self.reload_task
202 .clone()
203 .unwrap_or_else(|| Task::ready(()).shared())
204 }
205
206 pub fn entries_for_path<'a>(
207 &'a self,
208 path_list: &PathList,
209 remote_connection: Option<&'a RemoteConnectionOptions>,
210 ) -> impl Iterator<Item = &'a TerminalThreadMetadata> + 'a {
211 self.terminals_by_paths
212 .get(path_list)
213 .into_iter()
214 .flatten()
215 .filter_map(|id| self.terminals.get(id))
216 .filter(move |terminal| {
217 same_remote_connection_identity(
218 terminal.remote_connection.as_ref(),
219 remote_connection,
220 )
221 })
222 }
223
224 pub fn entries_for_main_worktree_path<'a>(
225 &'a self,
226 path_list: &PathList,
227 remote_connection: Option<&'a RemoteConnectionOptions>,
228 ) -> impl Iterator<Item = &'a TerminalThreadMetadata> + 'a {
229 self.terminals_by_main_paths
230 .get(path_list)
231 .into_iter()
232 .flatten()
233 .filter_map(|id| self.terminals.get(id))
234 .filter(move |terminal| {
235 same_remote_connection_identity(
236 terminal.remote_connection.as_ref(),
237 remote_connection,
238 )
239 })
240 }
241
242 pub fn path_is_referenced_by_terminal(
243 &self,
244 terminal_id: Option<TerminalId>,
245 path: &Path,
246 remote_connection: Option<&RemoteConnectionOptions>,
247 ) -> bool {
248 self.entries().any(|terminal| {
249 Some(terminal.terminal_id) != terminal_id
250 && same_remote_connection_identity(
251 terminal.remote_connection.as_ref(),
252 remote_connection,
253 )
254 && terminal
255 .folder_paths()
256 .paths()
257 .iter()
258 .any(|folder_path| folder_path.as_path() == path)
259 })
260 }
261
262 pub fn save(&mut self, metadata: TerminalThreadMetadata, cx: &mut Context<Self>) {
263 self.save_internal(metadata);
264 cx.notify();
265 }
266
267 pub fn change_worktree_paths(
268 &mut self,
269 current_folder_paths: &PathList,
270 remote_connection: Option<&RemoteConnectionOptions>,
271 mutate: impl Fn(&mut WorktreePaths),
272 cx: &mut Context<Self>,
273 ) {
274 let terminal_ids: Vec<_> = self
275 .terminals_by_paths
276 .get(current_folder_paths)
277 .into_iter()
278 .flatten()
279 .filter(|id| {
280 self.terminals.get(id).is_some_and(|terminal| {
281 same_remote_connection_identity(
282 terminal.remote_connection.as_ref(),
283 remote_connection,
284 )
285 })
286 })
287 .copied()
288 .collect();
289
290 if terminal_ids.is_empty() {
291 return;
292 }
293
294 for terminal_id in terminal_ids {
295 if let Some(mut terminal) = self.terminals.get(&terminal_id).cloned() {
296 mutate(&mut terminal.worktree_paths);
297 self.save_internal(terminal);
298 }
299 }
300
301 cx.notify();
302 }
303
304 fn save_internal(&mut self, metadata: TerminalThreadMetadata) {
305 if let Some(existing) = self.terminals.get(&metadata.terminal_id) {
306 if existing.folder_paths() != metadata.folder_paths()
307 && let Some(ids) = self.terminals_by_paths.get_mut(existing.folder_paths())
308 {
309 ids.remove(&metadata.terminal_id);
310 }
311
312 if existing.main_worktree_paths() != metadata.main_worktree_paths()
313 && let Some(ids) = self
314 .terminals_by_main_paths
315 .get_mut(existing.main_worktree_paths())
316 {
317 ids.remove(&metadata.terminal_id);
318 }
319 }
320
321 self.cache_terminal_metadata(metadata.clone());
322 self.pending_terminal_ops_tx
323 .try_send(DbOperation::Upsert(metadata))
324 .log_err();
325 }
326
327 fn cache_terminal_metadata(&mut self, metadata: TerminalThreadMetadata) {
328 self.terminals
329 .insert(metadata.terminal_id, metadata.clone());
330
331 self.terminals_by_paths
332 .entry(metadata.folder_paths().clone())
333 .or_default()
334 .insert(metadata.terminal_id);
335
336 if !metadata.main_worktree_paths().is_empty() {
337 self.terminals_by_main_paths
338 .entry(metadata.main_worktree_paths().clone())
339 .or_default()
340 .insert(metadata.terminal_id);
341 }
342 }
343
344 pub fn delete(&mut self, terminal_id: TerminalId, cx: &mut Context<Self>) {
345 if let Some(terminal) = self.terminals.remove(&terminal_id) {
346 if let Some(ids) = self.terminals_by_paths.get_mut(terminal.folder_paths()) {
347 ids.remove(&terminal_id);
348 }
349 if !terminal.main_worktree_paths().is_empty()
350 && let Some(ids) = self
351 .terminals_by_main_paths
352 .get_mut(terminal.main_worktree_paths())
353 {
354 ids.remove(&terminal_id);
355 }
356 }
357 self.pending_terminal_ops_tx
358 .try_send(DbOperation::Delete(terminal_id))
359 .log_err();
360 cx.notify();
361 }
362
363 fn new(db: TerminalThreadMetadataDb, cx: &mut Context<Self>) -> Self {
364 let (tx, rx) = async_channel::unbounded();
365 let _db_operations_task = cx.background_spawn({
366 let db = db.clone();
367 async move {
368 while let Ok(first_update) = rx.recv().await {
369 let mut updates = vec![first_update];
370 while let Ok(update) = rx.try_recv() {
371 updates.push(update);
372 }
373 let updates = Self::dedup_db_operations(updates);
374 for operation in updates {
375 match operation {
376 DbOperation::Upsert(metadata) => {
377 db.save(metadata).await.log_err();
378 }
379 DbOperation::Delete(terminal_id) => {
380 db.delete(terminal_id).await.log_err();
381 }
382 }
383 }
384 }
385 }
386 });
387
388 let mut this = Self {
389 db,
390 terminals: HashMap::default(),
391 terminals_by_paths: HashMap::default(),
392 terminals_by_main_paths: HashMap::default(),
393 reload_task: None,
394 pending_terminal_ops_tx: tx,
395 _db_operations_task,
396 };
397 this.reload(cx);
398 this
399 }
400
401 fn dedup_db_operations(operations: Vec<DbOperation>) -> Vec<DbOperation> {
402 let mut ops = HashMap::default();
403 for operation in operations.into_iter().rev() {
404 if ops.contains_key(&operation.id()) {
405 continue;
406 }
407 ops.insert(operation.id(), operation);
408 }
409 ops.into_values().collect()
410 }
411
412 fn reload(&mut self, cx: &mut Context<Self>) {
413 let db = self.db.clone();
414 self.reload_task = Some(
415 cx.spawn(async move |this, cx| {
416 let rows = cx
417 .background_spawn(async move {
418 db.list()
419 .context("Failed to fetch terminal thread metadata")
420 })
421 .await
422 .log_err()
423 .unwrap_or_default();
424
425 this.update(cx, |this, cx| {
426 this.terminals.clear();
427 this.terminals_by_paths.clear();
428 this.terminals_by_main_paths.clear();
429
430 for row in rows {
431 this.cache_terminal_metadata(row);
432 }
433
434 cx.notify();
435 })
436 .ok();
437 })
438 .shared(),
439 );
440 }
441}
442
443struct TerminalThreadMetadataDb(ThreadSafeConnection);
444
445impl Domain for TerminalThreadMetadataDb {
446 const NAME: &str = stringify!(TerminalThreadMetadataDb);
447
448 const MIGRATIONS: &[&str] = &[sql!(
449 CREATE TABLE IF NOT EXISTS sidebar_terminal_threads(
450 terminal_id TEXT PRIMARY KEY,
451 title TEXT NOT NULL,
452 custom_title TEXT,
453 created_at TEXT NOT NULL,
454 working_directory TEXT,
455 folder_paths TEXT,
456 folder_paths_order TEXT,
457 main_worktree_paths TEXT,
458 main_worktree_paths_order TEXT,
459 remote_connection TEXT
460 ) STRICT;
461 )];
462}
463
464db::static_connection!(TerminalThreadMetadataDb, []);
465
466impl TerminalThreadMetadataDb {
467 pub fn list(&self) -> anyhow::Result<Vec<TerminalThreadMetadata>> {
468 self.select::<TerminalThreadMetadata>(
469 "SELECT terminal_id, title, custom_title, created_at, \
470 working_directory, folder_paths, folder_paths_order, main_worktree_paths, \
471 main_worktree_paths_order, remote_connection \
472 FROM sidebar_terminal_threads \
473 ORDER BY created_at DESC",
474 )?()
475 }
476
477 pub async fn save(&self, row: TerminalThreadMetadata) -> anyhow::Result<()> {
478 let terminal_id = row.terminal_id.to_key_string();
479 let title = row.title.to_string();
480 let custom_title = row.custom_title.as_ref().map(ToString::to_string);
481 let created_at = row.created_at.to_rfc3339();
482 let working_directory = row
483 .working_directory
484 .as_ref()
485 .map(|path| path.to_string_lossy().into_owned());
486 let serialized = row.folder_paths().serialize();
487 let (folder_paths, folder_paths_order) = if row.folder_paths().is_empty() {
488 (None, None)
489 } else {
490 (Some(serialized.paths), Some(serialized.order))
491 };
492 let main_serialized = row.main_worktree_paths().serialize();
493 let (main_worktree_paths, main_worktree_paths_order) =
494 if row.main_worktree_paths().is_empty() {
495 (None, None)
496 } else {
497 (Some(main_serialized.paths), Some(main_serialized.order))
498 };
499 let remote_connection = row
500 .remote_connection
501 .as_ref()
502 .map(serde_json::to_string)
503 .transpose()
504 .context("serialize terminal thread remote connection")?;
505
506 self.write(move |conn| {
507 let sql = "INSERT INTO sidebar_terminal_threads(terminal_id, title, custom_title, created_at, working_directory, folder_paths, folder_paths_order, main_worktree_paths, main_worktree_paths_order, remote_connection) \
508 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10) \
509 ON CONFLICT(terminal_id) DO UPDATE SET \
510 title = excluded.title, \
511 custom_title = excluded.custom_title, \
512 created_at = excluded.created_at, \
513 working_directory = excluded.working_directory, \
514 folder_paths = excluded.folder_paths, \
515 folder_paths_order = excluded.folder_paths_order, \
516 main_worktree_paths = excluded.main_worktree_paths, \
517 main_worktree_paths_order = excluded.main_worktree_paths_order, \
518 remote_connection = excluded.remote_connection";
519 let mut stmt = Statement::prepare(conn, sql)?;
520 let mut i = stmt.bind(&terminal_id, 1)?;
521 i = stmt.bind(&title, i)?;
522 i = stmt.bind(&custom_title, i)?;
523 i = stmt.bind(&created_at, i)?;
524 i = stmt.bind(&working_directory, i)?;
525 i = stmt.bind(&folder_paths, i)?;
526 i = stmt.bind(&folder_paths_order, i)?;
527 i = stmt.bind(&main_worktree_paths, i)?;
528 i = stmt.bind(&main_worktree_paths_order, i)?;
529 stmt.bind(&remote_connection, i)?;
530 stmt.exec()
531 })
532 .await
533 }
534
535 pub async fn delete(&self, terminal_id: TerminalId) -> anyhow::Result<()> {
536 let terminal_id = terminal_id.to_key_string();
537 self.write(move |conn| {
538 let mut stmt = Statement::prepare(
539 conn,
540 "DELETE FROM sidebar_terminal_threads WHERE terminal_id = ?",
541 )?;
542 stmt.bind(&terminal_id, 1)?;
543 stmt.exec()
544 })
545 .await
546 }
547}
548
549impl Column for TerminalThreadMetadata {
550 fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
551 let (terminal_id, next): (String, i32) = Column::column(statement, start_index)?;
552 let (title, next): (String, i32) = Column::column(statement, next)?;
553 let (custom_title, next): (Option<String>, i32) = Column::column(statement, next)?;
554 let (created_at, next): (String, i32) = Column::column(statement, next)?;
555 let (working_directory, next): (Option<String>, i32) = Column::column(statement, next)?;
556 let (folder_paths_str, next): (Option<String>, i32) = Column::column(statement, next)?;
557 let (folder_paths_order_str, next): (Option<String>, i32) =
558 Column::column(statement, next)?;
559 let (main_worktree_paths_str, next): (Option<String>, i32) =
560 Column::column(statement, next)?;
561 let (main_worktree_paths_order_str, next): (Option<String>, i32) =
562 Column::column(statement, next)?;
563 let (remote_connection_json, next): (Option<String>, i32) =
564 Column::column(statement, next)?;
565
566 let folder_paths = folder_paths_str
567 .map(|paths| {
568 PathList::deserialize(&util::path_list::SerializedPathList {
569 paths,
570 order: folder_paths_order_str.unwrap_or_default(),
571 })
572 })
573 .unwrap_or_default();
574
575 let main_worktree_paths = main_worktree_paths_str
576 .map(|paths| {
577 PathList::deserialize(&util::path_list::SerializedPathList {
578 paths,
579 order: main_worktree_paths_order_str.unwrap_or_default(),
580 })
581 })
582 .unwrap_or_default();
583
584 let remote_connection = remote_connection_json
585 .as_deref()
586 .map(serde_json::from_str::<RemoteConnectionOptions>)
587 .transpose()
588 .context("deserialize terminal thread remote connection")?;
589
590 let worktree_paths = WorktreePaths::from_path_lists(main_worktree_paths, folder_paths)
591 .unwrap_or_else(|_| WorktreePaths::default());
592
593 Ok((
594 TerminalThreadMetadata {
595 terminal_id: TerminalId::from_key_string(&terminal_id)?,
596 title: SharedString::from(title),
597 custom_title: custom_title
598 .filter(|title| !title.trim().is_empty())
599 .map(SharedString::from),
600 created_at: DateTime::parse_from_rfc3339(&created_at)?.with_timezone(&Utc),
601 worktree_paths,
602 remote_connection,
603 working_directory: working_directory.map(PathBuf::from),
604 },
605 next,
606 ))
607 }
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613 use gpui::TestAppContext;
614 use std::path::Path;
615
616 fn init_test(cx: &mut TestAppContext) {
617 cx.update(|cx| {
618 TerminalThreadMetadataStore::init_global(cx);
619 });
620 cx.run_until_parked();
621 }
622
623 fn metadata(title: &str, worktree_paths: WorktreePaths) -> TerminalThreadMetadata {
624 let now = Utc::now();
625 TerminalThreadMetadata {
626 terminal_id: TerminalId::new(),
627 title: SharedString::from(title.to_string()),
628 custom_title: None,
629 created_at: now,
630 worktree_paths,
631 remote_connection: None,
632 working_directory: None,
633 }
634 }
635
636 #[test]
637 fn test_terminal_title_prefix_preserves_non_alphanumeric_prefixes() {
638 assert_eq!(terminal_title_prefix("✳ Thinking"), Some("✳ "));
639 assert_eq!(terminal_title_prefix(">>> Thinking"), Some(">>> "));
640 assert_eq!(terminal_title_prefix("⠋ Running"), Some("⠋ "));
641 assert_eq!(terminal_title_prefix("* Claude"), Some("* "));
642 assert_eq!(terminal_title_prefix("✳Thinking"), None);
643 assert_eq!(terminal_title_prefix("Thinking"), None);
644 assert_eq!(terminal_title_prefix(" Thinking"), None);
645 assert_eq!(terminal_title_prefix("✳"), None);
646 assert_eq!(terminal_title_prefix("v1 Running"), None);
647 }
648
649 #[test]
650 fn test_terminal_thread_display_title_combines_raw_and_custom_titles() {
651 let mut metadata = metadata(
652 "⠋ Thinking",
653 WorktreePaths::from_folder_paths(&PathList::default()),
654 );
655 metadata.custom_title = Some("Fix bug".into());
656 assert_eq!(metadata.display_title().as_ref(), "⠋ Fix bug");
657
658 metadata.title = "Thinking".into();
659 assert_eq!(metadata.display_title().as_ref(), "Fix bug");
660 }
661
662 #[gpui::test]
663 async fn test_change_worktree_paths_reindexes_terminal_metadata(cx: &mut TestAppContext) {
664 init_test(cx);
665
666 let old_main_paths = PathList::new(&[Path::new("/repo")]);
667 let old_folder_paths = PathList::new(&[Path::new("/repo-feature")]);
668 let new_main_path = Path::new("/repo");
669 let new_folder_path = Path::new("/repo-feature-renamed");
670 let new_folder_paths = PathList::new(&[new_folder_path]);
671 let metadata = metadata(
672 "Dev Server",
673 WorktreePaths::from_path_lists(old_main_paths.clone(), old_folder_paths.clone())
674 .unwrap(),
675 );
676 let terminal_id = metadata.terminal_id;
677
678 cx.update(|cx| {
679 TerminalThreadMetadataStore::global(cx).update(cx, |store, cx| {
680 store.save(metadata, cx);
681 });
682 });
683
684 cx.update(|cx| {
685 TerminalThreadMetadataStore::global(cx).update(cx, |store, cx| {
686 store.change_worktree_paths(
687 &old_folder_paths,
688 None,
689 |paths| {
690 paths.add_path(new_main_path, new_folder_path);
691 paths.remove_folder_path(Path::new("/repo-feature"));
692 },
693 cx,
694 );
695 });
696 });
697
698 cx.update(|cx| {
699 let store = TerminalThreadMetadataStore::global(cx);
700 let store = store.read(cx);
701 assert!(
702 store
703 .entries_for_path(&old_folder_paths, None)
704 .next()
705 .is_none()
706 );
707 assert_eq!(
708 store
709 .entries_for_path(&new_folder_paths, None)
710 .map(|entry| entry.terminal_id)
711 .collect::<Vec<_>>(),
712 vec![terminal_id]
713 );
714 assert_eq!(
715 store
716 .entry(terminal_id)
717 .unwrap()
718 .main_worktree_paths()
719 .paths(),
720 old_main_paths.paths()
721 );
722 });
723 }
724}
725