diff --git a/crates/tty7-core/src/core/config.rs b/crates/tty7-core/src/core/config.rs index ccbb916e..c1197f65 100644 --- a/crates/tty7-core/src/core/config.rs +++ b/crates/tty7-core/src/core/config.rs @@ -419,6 +419,9 @@ pub enum WindowBackdrop { pub enum SidebarGrouping { #[default] Repo, + /// By repository where there is one; a tab whose cwd is known not to be + /// in a repo groups under that cwd instead of falling to Scratch. + RepoOrDirectory, None, } @@ -1201,6 +1204,27 @@ mod tests { assert!(!back.sidebar_diff_preview); } + #[test] + fn sidebar_grouping_defaults_and_round_trips_leniently() { + assert_eq!(Config::default().sidebar_grouping, SidebarGrouping::Repo); + + let text = serde_json::to_string(&Config { + sidebar_grouping: SidebarGrouping::RepoOrDirectory, + ..Config::default() + }) + .unwrap(); + assert!(text.contains("\"sidebar_grouping\":\"repo-or-directory\"")); + let back: Config = serde_json::from_str(&text).unwrap(); + assert_eq!(back.sidebar_grouping, SidebarGrouping::RepoOrDirectory); + + let flat: Config = serde_json::from_str(r#"{"sidebar_grouping":"none"}"#).unwrap(); + assert_eq!(flat.sidebar_grouping, SidebarGrouping::None); + + // Unknown values fall back to Repo instead of rejecting the whole config. + let lenient: Config = serde_json::from_str(r#"{"sidebar_grouping":"folders"}"#).unwrap(); + assert_eq!(lenient.sidebar_grouping, SidebarGrouping::Repo); + } + #[test] fn dim_inactive_panes_defaults_on_and_round_trips() { assert!(Config::default().dim_inactive_panes); diff --git a/docs/reference/configuration.mdx b/docs/reference/configuration.mdx index 7827dc40..604a2a30 100644 --- a/docs/reference/configuration.mdx +++ b/docs/reference/configuration.mdx @@ -69,7 +69,7 @@ their id from the file name. [More about themes →](/customization/themes) |---|---|---|---| | `tab_bar_position` | enum | `"left"` | `left` (sidebar) or `top` (strip). | | `new_tab_position` | enum | `"after-current"` | Or `end`. | -| `sidebar_grouping` | enum | `"repo"` | Or `none` for a flat list. | +| `sidebar_grouping` | enum | `"repo"` | Or `repo-or-directory` to group non-repo tabs by their folder, or `none` for a flat list. | | `sidebar_diff_preview` | bool | `true` | Clicking a row's `+N −M` opens the diff overlay. | | `sidebar_width` | number | `220` | Pixels (100–2000). | | `sidebar_collapsed` | bool | `false` | | diff --git a/docs/window/sidebar.mdx b/docs/window/sidebar.mdx index ef9236e8..23ce4ffb 100644 --- a/docs/window/sidebar.mdx +++ b/docs/window/sidebar.mdx @@ -22,7 +22,10 @@ not its history — switching branches or `cd`-ing around inside a repository never moves a row out from under its header. **Settings → Window & Tabs → Sidebar grouping** switches between *By repo* (the -default) and *Flat*. +default), *By repo or folder*, and *Flat*. The default collects every non-repo +tab in Scratch; *By repo or folder* groups those tabs under their working +directory instead — for agents running in plain folders — leaving Scratch to +tabs whose shell has not reported a directory yet. ## What a row tells you @@ -52,9 +55,8 @@ they simply stop opening the overlay. ## Rearranging Drag a row to reorder it within its group, or drag a whole group header to move -the group. A row cannot be dragged into a different group: with the default repo -grouping a tab's group comes from its working directory, so `cd` is what moves -it. +the group. A row cannot be dragged into a different group: a tab's group comes +from its working directory, so `cd` is what moves it. ## Naming diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index 8e6243be..ae11bcfb 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -547,7 +547,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { } L10nKey::SettingsSidebarGrouping => "Sidebar grouping", L10nKey::SettingsSidebarGroupingDesc => { - "Group sidebar tabs under a header per git repository, with non-repo tabs in a Scratch section. Only applies to the left sidebar." + "Group sidebar tabs under a header per git repository. Non-repo tabs collect in a Scratch section, or under their working directory with \"By repo or folder\". Only applies to the left sidebar." } L10nKey::SettingsDiffPreviewFromCounts => "Open diff preview from sidebar counts", L10nKey::SettingsDiffPreviewFromCountsDesc => { @@ -574,6 +574,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsTop => "Top", L10nKey::SettingsLeft => "Left", L10nKey::SettingsByRepo => "By repo", + L10nKey::SettingsByRepoOrFolder => "By repo or folder", L10nKey::SettingsFlat => "Flat", L10nKey::SettingsPreset => "Preset", L10nKey::SettingsPresetDesc => { @@ -834,7 +835,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { "tray menu bar status item agent attention system icon" } L10nKey::SettingsSearchSidebarGroupingKeywords => { - "tabs group repo repository git scratch header sidebar flat" + "tabs group repo repository git scratch header sidebar flat folder directory cwd" } L10nKey::SettingsSearchSmartSelectionKeywords => { "double click word url path select semantic bracket email" diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index db677de2..c56c435d 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -554,7 +554,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { } L10nKey::SettingsSidebarGrouping => "サイドバーのグループ化", L10nKey::SettingsSidebarGroupingDesc => { - "git リポジトリごとにサイドバータブをまとめ、リポジトリ外のタブはスクラッチセクションに置きます。左サイドバーにのみ適用" + "git リポジトリごとにサイドバータブをまとめます。リポジトリ外のタブはスクラッチセクションに置くか、「リポジトリ/フォルダ別」なら作業ディレクトリごとにまとめます。左サイドバーにのみ適用" } L10nKey::SettingsDiffPreviewFromCounts => "サイドバーのカウントから Diff プレビューを開く", L10nKey::SettingsDiffPreviewFromCountsDesc => { @@ -579,6 +579,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsTop => "上部", L10nKey::SettingsLeft => "左側", L10nKey::SettingsByRepo => "リポジトリ別", + L10nKey::SettingsByRepoOrFolder => "リポジトリ/フォルダ別", L10nKey::SettingsFlat => "フラット表示", L10nKey::SettingsPreset => "プリセット", L10nKey::SettingsPresetDesc => { @@ -880,7 +881,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { "トレイ メニューバー ステータス アイコン エージェント 通知 システム tray icon menu bar status system attention" } L10nKey::SettingsSearchSidebarGroupingKeywords => { - "タブ グループ リポジトリ git スクラッチ ヘッダー サイドバー フラット sidebar grouping tabs repo repository git scratch header flat" + "タブ グループ リポジトリ git スクラッチ ヘッダー サイドバー フラット フォルダ ディレクトリ sidebar grouping tabs repo repository git scratch header flat folder directory" } L10nKey::SettingsSearchSmartSelectionKeywords => { "ダブルクリック 単語 url パス 選択 セマンティック 括弧 メール smart selection double click word url path bracket email" diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 47119d6a..715130e1 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -478,6 +478,7 @@ l10n_keys! { SettingsTop, SettingsLeft, SettingsByRepo, + SettingsByRepoOrFolder, SettingsFlat, SettingsPreset, SettingsPresetDesc, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index 6a8030fb..a0c287bc 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -480,7 +480,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsTabBarPositionDesc => "将标签页显示为顶部横向条或左侧垂直侧栏。", L10nKey::SettingsSidebarGrouping => "侧栏分组", L10nKey::SettingsSidebarGroupingDesc => { - "按 git 仓库在标题下对侧栏标签页分组,非仓库标签页放在“草稿”分组。仅适用于左侧栏。" + "按 git 仓库在标题下对侧栏标签页分组;非仓库标签页放在“草稿”分组,选“按仓库或文件夹”时则按其工作目录分组。仅适用于左侧栏。" } L10nKey::SettingsDiffPreviewFromCounts => "从侧栏计数打开 diff 预览", L10nKey::SettingsDiffPreviewFromCountsDesc => { @@ -503,6 +503,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsTop => "顶部", L10nKey::SettingsLeft => "左侧", L10nKey::SettingsByRepo => "按仓库", + L10nKey::SettingsByRepoOrFolder => "按仓库或文件夹", L10nKey::SettingsFlat => "平铺", L10nKey::SettingsPreset => "预设", L10nKey::SettingsPresetDesc => { @@ -786,7 +787,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { "显示托盘图标 托盘 菜单栏 状态 图标 show tray icon menu bar status" } L10nKey::SettingsSearchSidebarGroupingKeywords => { - "侧栏分组 标签页 分组 仓库 git 侧栏 sidebar grouping tabs repo repository" + "侧栏分组 标签页 分组 仓库 git 侧栏 文件夹 目录 sidebar grouping tabs repo repository folder directory" } L10nKey::SettingsSearchSmartSelectionKeywords => { "智能选择 双击 选择 单词 URL 路径 邮箱 括号 smart selection double click" diff --git a/src/ui/settings.rs b/src/ui/settings.rs index bddb839f..e0cd9668 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -5774,7 +5774,8 @@ impl Tty7App { let sidebar_diff_preview = cfg.sidebar_diff_preview; let sidebar_grouping_idx = match cfg.sidebar_grouping { crate::core::config::SidebarGrouping::Repo => 0, - crate::core::config::SidebarGrouping::None => 1, + crate::core::config::SidebarGrouping::RepoOrDirectory => 1, + crate::core::config::SidebarGrouping::None => 2, }; let notify_idx = match cfg.notify_on_command_finish { NotifyMode::Never => 0, @@ -5885,14 +5886,18 @@ impl Tty7App { .into_any_element(); let sidebar_grouping_radio = self.segmented( "wt-sidebar-grouping", - &[t(L10nKey::SettingsByRepo), t(L10nKey::SettingsFlat)], + &[ + t(L10nKey::SettingsByRepo), + t(L10nKey::SettingsByRepoOrFolder), + t(L10nKey::SettingsFlat), + ], sidebar_grouping_idx, cx, |this, ix, _w, cx| { - let grouping = if ix == 0 { - crate::core::config::SidebarGrouping::Repo - } else { - crate::core::config::SidebarGrouping::None + let grouping = match ix { + 0 => crate::core::config::SidebarGrouping::Repo, + 1 => crate::core::config::SidebarGrouping::RepoOrDirectory, + _ => crate::core::config::SidebarGrouping::None, }; this.set_sidebar_grouping(grouping, cx); }, diff --git a/src/ui/tab_sidebar.rs b/src/ui/tab_sidebar.rs index e958a18d..bee35363 100644 --- a/src/ui/tab_sidebar.rs +++ b/src/ui/tab_sidebar.rs @@ -1176,21 +1176,22 @@ impl Tty7App { } fn sidebar_group_keys(&self, cx: &gpui::App) -> Vec> { - let grouping = cx.global::().sidebar_grouping == SidebarGrouping::Repo; + let grouping = cx.global::().sidebar_grouping; self.tabs .iter() .map(|tab| { - if !grouping { + if grouping == SidebarGrouping::None { return None; } let cwd = tab.pane.first_leaf().and_then(|leaf| { let view = leaf.terminal()?.read(cx); Some((view.host_id(), view.git_status_cwd()?.to_path_buf())) }); - if let Some(known) = - cwd.and_then(|(id, cwd)| cx.global::().known_repo_for(id, &cwd)) - { - *tab.sidebar_group.borrow_mut() = known; + if let Some((id, cwd)) = cwd { + let known = cx.global::().known_repo_for(id, &cwd); + if let Some(group) = resolved_group(grouping, known, &cwd) { + *tab.sidebar_group.borrow_mut() = group; + } } tab.sidebar_group.borrow().clone() }) @@ -1220,8 +1221,10 @@ impl Tty7App { } /// Which group a tab about to be spawned in `cwd` belongs to, when the - /// repo probe for that directory has already landed. `Some(None)` means - /// the cache knows it is not a repo; a bare `None` means it never looked. + /// repo probe for that directory has already landed. A bare `None` means + /// the cache never looked; `Some` is the group [`resolved_group`] reached + /// — the repo home, the cwd itself under repo-or-directory grouping, or + /// `Some(None)` for Scratch. /// /// A tab's group otherwise starts empty and only fills in once its shell /// has started and reported a cwd, which parks every new tab in the @@ -1233,15 +1236,34 @@ impl Tty7App { cwd: Option<&Path>, cx: &gpui::App, ) -> Option> { + let cwd = cwd?; let host = self .window_workspace(cx) .as_ref() .map_or(crate::ui::host_ops::HostId::LOCAL, |ws| ws.target.host_id()); - cx.try_global::()? - .known_repo_for(host, cwd?) + let known = cx.try_global::()?.known_repo_for(host, cwd); + resolved_group(cx.global::().sidebar_grouping, known, cwd) } } +/// The group a probed cwd resolves to under `grouping`: the repo home when +/// the cache found one, otherwise Scratch — or the cwd itself under +/// repo-or-directory grouping, so a shell in a plain folder still gets a +/// header. `known` is the cache's three-valued answer; a probe that never +/// ran resolves to `None`, no decision, and the tab keeps whatever group it +/// already has rather than bouncing through Scratch mid-probe. +fn resolved_group( + grouping: SidebarGrouping, + known: Option>, + cwd: &Path, +) -> Option> { + Some(match known? { + Some(root) => Some(root), + None if grouping == SidebarGrouping::RepoOrDirectory => Some(cwd.to_path_buf()), + None => None, + }) +} + #[derive(Debug, PartialEq)] struct Section { key: Option, @@ -1422,6 +1444,37 @@ mod tests { assert_eq!(diff_click_cwd::(&cfg, None), None); } + #[test] + fn a_probed_non_repo_groups_by_folder_only_in_the_fallback_mode() { + let cwd = p("/w/plain"); + assert_eq!( + resolved_group(SidebarGrouping::RepoOrDirectory, Some(None), &cwd), + Some(Some(p("/w/plain"))) + ); + assert_eq!( + resolved_group(SidebarGrouping::Repo, Some(None), &cwd), + Some(None), + "under Repo a probed non-repo still falls to Scratch" + ); + // Never probed: no decision in either mode, so the tab keeps the + // group it already has instead of bouncing through Scratch. + assert_eq!(resolved_group(SidebarGrouping::Repo, None, &cwd), None); + assert_eq!( + resolved_group(SidebarGrouping::RepoOrDirectory, None, &cwd), + None + ); + } + + #[test] + fn a_known_repo_home_wins_over_the_folder_in_both_modes() { + for mode in [SidebarGrouping::Repo, SidebarGrouping::RepoOrDirectory] { + assert_eq!( + resolved_group(mode, Some(Some(p("/w/repo"))), &p("/w/repo/sub")), + Some(Some(p("/w/repo"))) + ); + } + } + #[test] fn sections_order_groups_by_first_appearance_scratch_last() { let keys = vec![