Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:04:36.510Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

terminal_thread_metadata_store.rs

725 lines · 25.7 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.