Cherry picks for v0.233.x preview release (#54368)

Eric Holk , Nathan Sobo , Max Brunsfeld , Anthony Eid , Danilo Leal , Bennet Bo Fenner , Danilo Leal , Mikayla Maki , Cameron Mcloughlin , VinΓ­cius Dutra , and Smit Barmase created

Cherry picks for the next v0.233.x preview release, focused on sidebar
and agent panel fixes and improvements. Built incrementally so CI can
validate as we go.

### Cherry-picked PRs

Listed in the order they were applied (chronological merge order on
`main`). `[x]` = applied on this branch; `[ ]` = todo; `[-]` = skipped.

- [x] #53956 β€” sidebar: Remove View More batched thread expansion
- [x] #53998 β€” Make flexible dock widths more closely match full-height
pane splits
- [x] #54006 β€” agent: Inline worktree info fetching to remove expects in
agent_panel
- [x] #53854 β€” Simplify parallel agents onboarding
- [x] #54025 β€” sidebar: Add some UI adjustments
- [-] #53982 β€” sidebar: Refactor thread time storage (skipped β€” paired
with revert #54078, net zero)
- [-] #54078 β€” Revert "sidebar: Refactor thread time storage (#53982)"
(skipped β€” paired with #53982, net zero)
- [x] #54090 β€” agent_ui: Do not show token limit callout for external
agents
- [x] #54126 β€” agent_ui: Adjust thread item component and fix thread
switcher
- [x] #54075 β€” Rename Archive view to Thread History
- [x] #54125 β€” agent: Auto-select user model when there's no default
- [x] #54128 β€” sidebar: Fix cmd-click in the header not taking to the
last workspace
- [x] #54178 β€” agent_ui: Fix UI issues with activity bar
- [x] #54187 β€” agent_ui: Only surface the regenerate title item for the
Zed agent
- [x] #54205 β€” agent: Clean up old remove worktree manage folder code
- [x] #54207 β€” Add list of open workspaces to the project group menu in
the sidebar
- [x] #54198 β€” Fix remote projects not appearing in the sidebar after
adding them to the window
- [x] #54206 β€” Feature flag overrides
- [-] #53100 β€” workspace: Skip read-only paths when choosing default
save location (skipped for this release)
- [-] #54183 β€” Move the worktree picker to the title bar + make it
always visible (skipped β€” very large refactor, deferred to a future
release)
- [x] #54314 β€” agent_ui: Add `show thread metadata` action
- [x] #54317 β€” sidebar: Open project header ellipsis menu on right-click
- [x] #54316 β€” agent_ui: Add setting for turning off content max-width
- [x] #54256 β€” Add support for Netpbm image previews
- [x] #54320 β€” sidebar: Consistently set `interacted_at`
- [x] #54318 β€” agent: Respect favorite model settings and sync UI
changes back to settings
- [x] #54348 β€” Always use `ArchiveSelectedThread` action for archiving
threads
- [x] #54353 β€” agent: When opening a remote thread check that the linked
worktree path exists
- [x] #54365 β€” Avoid constantly scrolling thread history to top as agent
generates

### Conflict resolution notes

- **#53956** β€” On `v0.233.x`, `sidebar_tests.rs` still contained
`test_search_finds_threads_hidden_behind_view_more`, which exercised
behavior #53956 removes. Deleted the test along with the rest of the
View More functionality; no changes to the substance of the
cherry-picked patch.
- **#53998** β€” Supersedes standalone cherry-pick PR #54366 (which can be
closed).
- **#53982 / #54078** β€” Skipped both. They form a refactor-and-revert
pair on `main` with net zero change. Since `v0.233.x` already has #54173
("sidebar: Fix sidebar thread times") applied on top of the pre-#53982
state, pulling both in would be churn with no end-state difference.
- **#54025** β€” `crates/ui/src/components/ai/thread_item.rs`: accepted
incoming side for the `worktree_tooltip_title` removal and converted the
patch's `match (wt.kind, wt.branch_name)` structure back to `v0.233.x`'s
`match wt.kind` structure (the #54067 tuple match isn't on this branch).
Dropped the `linked_worktree_count` local after it became unused.
- **#54207** β€” `crates/sidebar/src/sidebar.rs`: accepted incoming side
for the project group menu restructure. This removes the
`show_multi_project_entries` gate around the "Remove Project" entry that
#54025 added β€” matching `main`'s state at #54207's parent.
- **#54314** β€” `crates/agent_ui/src/agent_panel.rs`: merged both sides
of two import conflicts, adding `ShowAllSidebarThreadMetadata` and
`ShowThreadMetadata` alongside the
`CreateWorktree`/`NewWorktreeBranchTarget`/`SwitchWorktree`/`ToggleWorktreeSelector`
imports that remain on `v0.233.x` (#54183 isn't here). Also merged the
`anyhow` and `chrono` `use` lines.
- **#54183** β€” Skipped. It's a 3,379/4,702-line refactor that moves the
worktree picker from the agent panel to the title bar. Too risky for a
preview release; should be handled as a dedicated PR if/when we want it
on `v0.233.x`.

Release Notes:

- Fixed a bug where flexible docks resized incorrectly in certain cases.
([#53998](https://github.com/zed-industries/zed/pull/53998))
- Fixed an issue where resizing a flexible-width panel in the left dock
would also resize fixed-width panels.
([#53998](https://github.com/zed-industries/zed/pull/53998))
- Agent: Fixed worktree and branch labels not showing up in the thread
switcher. ([#54126](https://github.com/zed-industries/zed/pull/54126))
- Agent: Fixed the thread switcher not selecting on hover.
([#54126](https://github.com/zed-industries/zed/pull/54126))
- Renamed the threads Archive view to Thread History and updated its
icon to a clock.
([#54075](https://github.com/zed-industries/zed/pull/54075))
- Agent: Automatically select a model when there's no selected model or
configured default.
([#54125](https://github.com/zed-industries/zed/pull/54125))
- Agent: Fixed a bug where cmd-clicking on the project header wouldn't
actually take you to the last active workspace.
([#54128](https://github.com/zed-industries/zed/pull/54128))
- Agent: Added a new `limit_content_width` setting in the agent panel
that allows turning off the content max-width limit.
([#54316](https://github.com/zed-industries/zed/pull/54316))
- Added support for PNM image previews (`.pbm`, `.ppm`, `.pgm`).
([#54256](https://github.com/zed-industries/zed/pull/54256))
- Agent favorite models now remember and restore per-model thinking,
effort level, and fast mode preferences.
([#54318](https://github.com/zed-industries/zed/pull/54318))

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Cameron Mcloughlin <cameron.studdstreet@gmail.com>
Co-authored-by: VinΓ­cius Dutra <dutravinisousa2@usp.br>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Change summary

Cargo.lock                                                                |  19 
Cargo.toml                                                                |   3 
assets/icons/clock.svg                                                    |   4 
assets/icons/knockouts/archive_bg.svg                                     |  10 
assets/icons/knockouts/archive_fg.svg                                     |   4 
assets/icons/thread_import.svg                                            |   5 
assets/keymaps/default-linux.json                                         |  11 
assets/keymaps/default-macos.json                                         |  11 
assets/keymaps/default-windows.json                                       |  12 
assets/settings/default.json                                              |   6 
crates/agent/src/agent.rs                                                 |  44 
crates/agent/src/native_agent_server.rs                                   |  46 
crates/agent/src/tool_permissions.rs                                      |   2 
crates/agent_settings/src/agent_settings.rs                               |  49 
crates/agent_ui/src/agent_configuration.rs                                |  68 
crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs |  10 
crates/agent_ui/src/agent_panel.rs                                        | 331 
crates/agent_ui/src/agent_ui.rs                                           |  14 
crates/agent_ui/src/conversation_view.rs                                  |   8 
crates/agent_ui/src/conversation_view/thread_view.rs                      | 220 
crates/agent_ui/src/favorite_models.rs                                    |  24 
crates/agent_ui/src/language_model_selector.rs                            |  56 
crates/agent_ui/src/thread_history_view.rs                                |   2 
crates/agent_ui/src/thread_import.rs                                      |  24 
crates/agent_ui/src/thread_metadata_store.rs                              |  17 
crates/agent_ui/src/thread_worktree_archive.rs                            | 181 
crates/agent_ui/src/thread_worktree_picker.rs                             |   1 
crates/agent_ui/src/threads_archive_view.rs                               | 145 
crates/agent_ui/src/ui/undo_reject_toast.rs                               |  24 
crates/ai_onboarding/src/ai_onboarding.rs                                 | 132 
crates/auto_update_ui/Cargo.toml                                          |   1 
crates/auto_update_ui/src/auto_update_ui.rs                               |  41 
crates/component_preview/src/component_preview.rs                         |  14 
crates/csv_preview/src/csv_preview.rs                                     |   4 
crates/debugger_ui/src/debugger_panel.rs                                  |   4 
crates/debugger_ui/src/session/running/memory_view.rs                     |   4 
crates/edit_prediction/src/edit_prediction.rs                             |   4 
crates/edit_prediction_ui/src/edit_prediction_ui.rs                       |   4 
crates/edit_prediction_ui/src/rate_prediction_modal.rs                    |   4 
crates/feature_flags/Cargo.toml                                           |  11 
crates/feature_flags/src/feature_flags.rs                                 | 191 
crates/feature_flags/src/flags.rs                                         |  41 
crates/feature_flags/src/settings.rs                                      |  76 
crates/feature_flags/src/store.rs                                         | 374 
crates/feature_flags_macros/Cargo.toml                                    |  18 
crates/feature_flags_macros/LICENSE-GPL                                   |   1 
crates/feature_flags_macros/src/feature_flags_macros.rs                   | 190 
crates/git/src/repository.rs                                              |   2 
crates/git_ui/src/clone.rs                                                |  12 
crates/git_ui/src/git_panel.rs                                            |  48 
crates/git_ui/src/worktree_picker.rs                                      |  37 
crates/gpui/src/platform.rs                                               |   4 
crates/gpui_linux/src/linux/x11/clipboard.rs                              |   3 
crates/gpui_macos/src/pasteboard.rs                                       |   6 
crates/icons/src/icons.rs                                                 |   2 
crates/json_schema_store/Cargo.toml                                       |   5 
crates/json_schema_store/src/json_schema_store.rs                         |  55 
crates/keymap_editor/src/keymap_editor.rs                                 |  10 
crates/language_model/src/registry.rs                                     |  84 
crates/language_models/src/language_models.rs                             |  62 
crates/notifications/src/status_toast.rs                                  |  90 
crates/onboarding/src/onboarding.rs                                       |  24 
crates/project/src/image_store.rs                                         |   1 
crates/project/src/worktree_store.rs                                      |   6 
crates/project_panel/src/project_panel.rs                                 |  10 
crates/repl/src/notebook/notebook_ui.rs                                   |   4 
crates/settings/src/vscode_import.rs                                      |   1 
crates/settings_content/src/agent.rs                                      |  31 
crates/settings_content/src/settings_content.rs                           |  39 
crates/settings_ui/src/page_data.rs                                       |  98 
crates/settings_ui/src/pages.rs                                           |   2 
crates/settings_ui/src/pages/feature_flags.rs                             | 132 
crates/settings_ui/src/settings_ui.rs                                     |  20 
crates/sidebar/Cargo.toml                                                 |   1 
crates/sidebar/src/sidebar.rs                                             | 709 
crates/sidebar/src/sidebar_tests.rs                                       | 361 
crates/sidebar/src/thread_switcher.rs                                     |  63 
crates/ui/src/components/ai/parallel_agents_illustration.rs               | 199 
crates/ui/src/components/ai/thread_item.rs                                | 570 
crates/ui/src/components/context_menu.rs                                  |  16 
crates/ui/src/components/icon.rs                                          |   3 
crates/ui/src/components/icon/icon_decoration.rs                          |   5 
crates/workspace/src/dock.rs                                              |  43 
crates/workspace/src/multi_workspace.rs                                   | 217 
crates/workspace/src/multi_workspace_tests.rs                             | 150 
crates/workspace/src/pane_group.rs                                        |  55 
crates/workspace/src/persistence/model.rs                                 |  10 
crates/workspace/src/toast_layer.rs                                       |   9 
crates/workspace/src/workspace.rs                                         | 194 
crates/zed/src/main.rs                                                    |   1 
crates/zed/src/visual_test_runner.rs                                      |  18 
crates/zed/src/zed.rs                                                     |   4 
92 files changed, 3,856 insertions(+), 2,025 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -1253,6 +1253,7 @@ dependencies = [
  "fs",
  "gpui",
  "markdown_preview",
+ "notifications",
  "release_channel",
  "semver",
  "serde",
@@ -6137,7 +6138,23 @@ dependencies = [
 name = "feature_flags"
 version = "0.1.0"
 dependencies = [
+ "collections",
+ "feature_flags_macros",
+ "fs",
  "gpui",
+ "inventory",
+ "schemars",
+ "serde_json",
+ "settings",
+]
+
+[[package]]
+name = "feature_flags_macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -9095,6 +9112,7 @@ dependencies = [
  "collections",
  "dap",
  "extension",
+ "feature_flags",
  "gpui",
  "language",
  "parking_lot",
@@ -16043,6 +16061,7 @@ dependencies = [
  "db",
  "editor",
  "extension",
+ "feature_flags",
  "fs",
  "git",
  "gpui",

Cargo.toml πŸ”—

@@ -73,6 +73,7 @@ members = [
     "crates/extension_host",
     "crates/extensions_ui",
     "crates/feature_flags",
+    "crates/feature_flags_macros",
     "crates/feedback",
     "crates/file_finder",
     "crates/file_icons",
@@ -325,6 +326,7 @@ extension = { path = "crates/extension" }
 extension_host = { path = "crates/extension_host" }
 extensions_ui = { path = "crates/extensions_ui" }
 feature_flags = { path = "crates/feature_flags" }
+feature_flags_macros = { path = "crates/feature_flags_macros" }
 feedback = { path = "crates/feedback" }
 file_finder = { path = "crates/file_finder" }
 file_icons = { path = "crates/file_icons" }
@@ -891,6 +893,7 @@ debug = true
 # proc-macros start
 gpui_macros = { opt-level = 3 }
 derive_refineable = { opt-level = 3 }
+feature_flags_macros = { opt-level = 3 }
 settings_macros = { opt-level = 3 }
 sqlez_macros = { opt-level = 3, codegen-units = 1 }
 ui_macros = { opt-level = 3 }

assets/icons/clock.svg πŸ”—

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 4.87936V8L10.229 9.33742" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.6863 11.3137 2 8 2C4.6863 2 2 4.6863 2 8C2 11.3137 4.6863 14 8 14Z" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/knockouts/archive_bg.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_4330_2332)">
-<path d="M8.31127 0.918671C9.28885 0.918671 10.0813 1.71115 10.0813 2.68873C10.0813 3.22911 9.83832 3.71194 9.4566 4.03661V7.99891C9.4566 9.149 8.52427 10.0813 7.37418 10.0813H3.62582C2.47573 10.0813 1.5434 9.149 1.5434 7.99891V4.03661C1.16168 3.71194 0.918671 3.22911 0.918671 2.68873C0.918671 1.71115 1.71115 0.918671 2.68873 0.918671H8.31127Z" fill="#C6CAD0"/>
-</g>
-<defs>
-<clipPath id="clip0_4330_2332">
-<rect width="11" height="11" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/knockouts/archive_fg.svg πŸ”—

@@ -1,4 +0,0 @@
-<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.37637 1.75164C2.21069 1.75164 2.05179 1.81746 1.93463 1.93462C1.81747 2.05178 1.75165 2.21068 1.75165 2.37637V3.00109C1.75165 3.16678 1.81747 3.32568 1.93463 3.44284C2.05179 3.56 2.21069 3.62582 2.37637 3.62582H8.62364C8.78933 3.62582 8.94823 3.56 9.06539 3.44284C9.18255 3.32568 9.24837 3.16678 9.24837 3.00109V2.37637C9.24837 2.21068 9.18255 2.05178 9.06539 1.93462C8.94823 1.81746 8.78933 1.75164 8.62364 1.75164H2.37637Z" fill="#C6CAD0"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M2.37634 4.77041H8.62361V7.99891C8.62361 8.33028 8.49197 8.64808 8.25765 8.8824C8.02334 9.11672 7.70553 9.24836 7.37416 9.24836H3.6258C3.29442 9.24836 2.97662 9.11672 2.7423 8.8824C2.50798 8.64808 2.37634 8.33028 2.37634 7.99891L2.37634 4.77041ZM3.80699 6.39988C3.80699 6.23151 3.87388 6.07002 3.99294 5.95096C4.112 5.8319 4.27348 5.76501 4.44186 5.76501H6.55809C6.72647 5.76501 6.88795 5.8319 7.00701 5.95096C7.12607 6.07002 7.19296 6.23151 7.19296 6.39988C7.19296 6.56826 7.12607 6.72974 7.00701 6.8488C6.88795 6.96786 6.72647 7.03475 6.55809 7.03475H4.44186C4.27348 7.03475 4.112 6.96786 3.99294 6.8488C3.87388 6.72974 3.80699 6.56826 3.80699 6.39988Z" fill="#C6CAD0"/>
-</svg>

assets/icons/thread_import.svg πŸ”—

@@ -1,5 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.31947 5.03803L8.31947 9.28259" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.19576 7.67419L8.31948 9.79792L10.4432 7.67419" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.64894 12.8952C6.89401 13.5339 8.32626 13.7069 9.68759 13.383C11.0489 13.0592 12.2499 12.2598 13.0739 11.1288C13.8979 9.99787 14.291 8.60973 14.1821 7.21464C14.0733 5.81955 13.4698 4.5092 12.4803 3.51972C11.4908 2.53024 10.1805 1.92671 8.78535 1.81787C7.39026 1.70904 6.00218 2.10207 4.87122 2.92612C3.74026 3.75018 2.94082 4.95106 2.61695 6.3124C2.29307 7.67374 2.46606 9.10598 3.10475 10.3511L1.80005 14.1999L5.64894 12.8952Z" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/keymaps/default-linux.json πŸ”—

@@ -384,6 +384,12 @@
       "backspace": "agent::RemoveSelectedThread",
     },
   },
+  {
+    "context": "ThreadsArchiveView",
+    "bindings": {
+      "shift-backspace": "agent::ArchiveSelectedThread",
+    },
+  },
   {
     "context": "RulesLibrary",
     "bindings": {
@@ -720,8 +726,9 @@
       "right": "menu::SelectChild",
       "enter": "menu::Confirm",
       "ctrl-f": "agents_sidebar::FocusSidebarFilter",
-      "ctrl-g": "agents_sidebar::ViewAllThreads",
-      "shift-backspace": "agent::RemoveSelectedThread",
+      "ctrl-g": "agents_sidebar::ToggleThreadHistory",
+      "shift-backspace": "agent::ArchiveSelectedThread",
+      "ctrl-backspace": "agent::RemoveSelectedThread",
       "ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
       "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
     },

assets/keymaps/default-macos.json πŸ”—

@@ -431,6 +431,12 @@
       "shift-backspace": "agent::RemoveSelectedThread",
     },
   },
+  {
+    "context": "ThreadsArchiveView",
+    "bindings": {
+      "backspace": "agent::ArchiveSelectedThread",
+    },
+  },
   {
     "context": "RulesLibrary",
     "use_key_equivalents": true,
@@ -783,8 +789,9 @@
       "right": "menu::SelectChild",
       "enter": "menu::Confirm",
       "cmd-f": "agents_sidebar::FocusSidebarFilter",
-      "cmd-g": "agents_sidebar::ViewAllThreads",
-      "shift-backspace": "agent::RemoveSelectedThread",
+      "cmd-g": "agents_sidebar::ToggleThreadHistory",
+      "shift-backspace": "agent::ArchiveSelectedThread",
+      "cmd-shift-backspace": "agent::RemoveSelectedThread",
       "ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
       "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
     },

assets/keymaps/default-windows.json πŸ”—

@@ -386,6 +386,13 @@
       "backspace": "agent::RemoveSelectedThread",
     },
   },
+  {
+    "context": "ThreadsArchiveView",
+    "use_key_equivalents": true,
+    "bindings": {
+      "shift-backspace": "agent::ArchiveSelectedThread",
+    },
+  },
   {
     "context": "RulesLibrary",
     "use_key_equivalents": true,
@@ -720,8 +727,9 @@
       "right": "menu::SelectChild",
       "enter": "menu::Confirm",
       "ctrl-f": "agents_sidebar::FocusSidebarFilter",
-      "ctrl-g": "agents_sidebar::ViewAllThreads",
-      "shift-backspace": "agent::RemoveSelectedThread",
+      "ctrl-g": "agents_sidebar::ToggleThreadHistory",
+      "shift-backspace": "agent::ArchiveSelectedThread",
+      "ctrl-backspace": "agent::RemoveSelectedThread",
       "ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
       "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
     },

assets/settings/default.json πŸ”—

@@ -990,7 +990,11 @@
     "default_width": 640,
     // Default height when the agent panel is docked to the bottom.
     "default_height": 320,
-    // Maximum content width when the agent panel is wider than this value.
+    // Whether to limit the content width in the agent panel. When enabled,
+    // content will be constrained to `max_content_width` and centered when
+    // the panel is wider, for optimal readability.
+    "limit_content_width": true,
+    // Maximum content width in pixels when limit_content_width is enabled.
     // Content will be centered within the panel.
     "max_content_width": 850,
     // The default model to use when creating new threads.

crates/agent/src/agent.rs πŸ”—

@@ -47,7 +47,7 @@ use prompt_store::{
     WorktreeContext,
 };
 use serde::{Deserialize, Serialize};
-use settings::{LanguageModelSelection, update_settings_file};
+use settings::{LanguageModelSelection, Settings as _, update_settings_file};
 use std::any::Any;
 use std::path::PathBuf;
 use std::rc::Rc;
@@ -201,7 +201,7 @@ impl LanguageModels {
             .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
             .collect::<Vec<_>>();
 
-        cx.background_spawn(async move {
+        cx.spawn(async move |cx| {
             for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
                 if let Err(err) = authenticate_task.await {
                     match err {
@@ -244,6 +244,8 @@ impl LanguageModels {
                     }
                 }
             }
+
+            cx.update(language_models::update_environment_fallback_model);
         })
     }
 }
@@ -365,7 +367,7 @@ impl NativeAgent {
         });
 
         let registry = LanguageModelRegistry::read_global(cx);
-        let summarization_model = registry.thread_summary_model().map(|c| c.model);
+        let summarization_model = registry.thread_summary_model(cx).map(|c| c.model);
 
         let weak = cx.weak_entity();
         let weak_thread = thread_handle.downgrade();
@@ -749,13 +751,14 @@ impl NativeAgent {
 
         let registry = LanguageModelRegistry::read_global(cx);
         let default_model = registry.default_model().map(|m| m.model);
-        let summarization_model = registry.thread_summary_model().map(|m| m.model);
+        let summarization_model = registry.thread_summary_model(cx).map(|m| m.model);
 
         for session in self.sessions.values_mut() {
             session.thread.update(cx, |thread, cx| {
-                if thread.model().is_none()
-                    && let Some(model) = default_model.clone()
-                {
+                let should_update_model = thread.model().is_none()
+                    || (thread.is_empty()
+                        && matches!(event, language_model::Event::DefaultModelChanged));
+                if should_update_model && let Some(model) = default_model.clone() {
                     thread.set_model(model, cx);
                     cx.notify();
                 }
@@ -910,7 +913,7 @@ impl NativeAgent {
                     .get(&project_id)
                     .context("project state not found")?;
                 let summarization_model = LanguageModelRegistry::read_global(cx)
-                    .thread_summary_model()
+                    .thread_summary_model(cx)
                     .map(|c| c.model);
 
                 Ok(cx.new(|cx| {
@@ -1420,16 +1423,29 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
             return Task::ready(Err(anyhow!("Invalid model ID {}", model_id)));
         };
 
-        // We want to reset the effort level when switching models, as the currently-selected effort level may
-        // not be compatible.
-        let effort = model
-            .default_effort_level()
-            .map(|effort_level| effort_level.value.to_string());
+        let favorite = agent_settings::AgentSettings::get_global(cx)
+            .favorite_models
+            .iter()
+            .find(|favorite| {
+                favorite.provider.0 == model.provider_id().0.as_ref()
+                    && favorite.model == model.id().0.as_ref()
+            })
+            .cloned();
+
+        let LanguageModelSelection {
+            enable_thinking,
+            effort,
+            speed,
+            ..
+        } = agent_settings::language_model_to_selection(&model, favorite.as_ref());
 
         thread.update(cx, |thread, cx| {
             thread.set_model(model.clone(), cx);
             thread.set_thinking_effort(effort.clone(), cx);
-            thread.set_thinking_enabled(model.supports_thinking(), cx);
+            thread.set_thinking_enabled(enable_thinking, cx);
+            if let Some(speed) = speed {
+                thread.set_speed(speed, cx);
+            }
         });
 
         update_settings_file(

crates/agent/src/native_agent_server.rs πŸ”—

@@ -2,11 +2,12 @@ use std::{any::Any, rc::Rc, sync::Arc};
 
 use agent_client_protocol as acp;
 use agent_servers::{AgentServer, AgentServerDelegate};
-use agent_settings::AgentSettings;
+use agent_settings::{AgentSettings, language_model_to_selection};
 use anyhow::Result;
 use collections::HashSet;
 use fs::Fs;
 use gpui::{App, Entity, Task};
+use language_model::{LanguageModelId, LanguageModelProviderId, LanguageModelRegistry};
 use project::{AgentId, Project};
 use prompt_store::PromptStore;
 use settings::{LanguageModelSelection, Settings as _, update_settings_file};
@@ -76,7 +77,7 @@ impl AgentServer for NativeAgentServer {
         fs: Arc<dyn Fs>,
         cx: &App,
     ) {
-        let selection = model_id_to_selection(&model_id);
+        let selection = model_id_to_selection(&model_id, cx);
         update_settings_file(fs, cx, move |settings, _| {
             let agent = settings.agent.get_or_insert_default();
             if should_be_favorite {
@@ -89,16 +90,41 @@ impl AgentServer for NativeAgentServer {
 }
 
 /// Convert a ModelId (e.g. "anthropic/claude-3-5-sonnet") to a LanguageModelSelection.
-fn model_id_to_selection(model_id: &acp::ModelId) -> LanguageModelSelection {
+fn model_id_to_selection(model_id: &acp::ModelId, cx: &App) -> LanguageModelSelection {
     let id = model_id.0.as_ref();
     let (provider, model) = id.split_once('/').unwrap_or(("", id));
-    LanguageModelSelection {
-        provider: provider.to_owned().into(),
-        model: model.to_owned(),
-        enable_thinking: false,
-        effort: None,
-        speed: None,
-    }
+
+    let provider_id = LanguageModelProviderId(provider.to_string().into());
+    let model_id_typed = LanguageModelId(model.to_string().into());
+    let resolved = LanguageModelRegistry::global(cx)
+        .read(cx)
+        .provider(&provider_id)
+        .and_then(|p| {
+            p.provided_models(cx)
+                .into_iter()
+                .find(|m| m.id() == model_id_typed)
+        });
+
+    let Some(resolved) = resolved else {
+        return LanguageModelSelection {
+            provider: provider.to_owned().into(),
+            model: model.to_owned(),
+            enable_thinking: false,
+            effort: None,
+            speed: None,
+        };
+    };
+
+    let current_user_selection = AgentSettings::get_global(cx)
+        .default_model
+        .as_ref()
+        .filter(|selection| {
+            selection.provider.0 == resolved.provider_id().0.as_ref()
+                && selection.model == resolved.id().0.as_ref()
+        })
+        .cloned();
+
+    language_model_to_selection(&resolved, current_user_selection.as_ref())
 }
 
 #[cfg(test)]

crates/agent/src/tool_permissions.rs πŸ”—

@@ -574,7 +574,7 @@ mod tests {
             flexible: true,
             default_width: px(300.),
             default_height: px(600.),
-            max_content_width: px(850.),
+            max_content_width: Some(px(850.)),
             default_model: None,
             inline_assistant_model: None,
             inline_assistant_use_streaming_tools: false,

crates/agent_settings/src/agent_settings.rs πŸ”—

@@ -142,7 +142,7 @@ pub struct AgentSettings {
     pub sidebar_side: SidebarDockPosition,
     pub default_width: Pixels,
     pub default_height: Pixels,
-    pub max_content_width: Pixels,
+    pub max_content_width: Option<Pixels>,
     pub default_model: Option<LanguageModelSelection>,
     pub inline_assistant_model: Option<LanguageModelSelection>,
     pub inline_assistant_use_streaming_tools: bool,
@@ -210,7 +210,48 @@ impl AgentSettings {
             .map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model)))
             .collect()
     }
+}
+
+pub fn language_model_to_selection(
+    model: &Arc<dyn LanguageModel>,
+    override_selection: Option<&LanguageModelSelection>,
+) -> LanguageModelSelection {
+    let provider = model.provider_id().0.to_string().into();
+    let model_name = model.id().0.to_string();
+    match override_selection {
+        Some(current) => LanguageModelSelection {
+            provider,
+            model: model_name,
+            enable_thinking: current.enable_thinking && model.supports_thinking(),
+            effort: current
+                .effort
+                .clone()
+                .filter(|value| {
+                    model
+                        .supported_effort_levels()
+                        .iter()
+                        .any(|level| level.value.as_ref() == value.as_str())
+                })
+                .or_else(|| {
+                    model
+                        .default_effort_level()
+                        .map(|effort| effort.value.to_string())
+                }),
+            speed: current.speed.filter(|_| model.supports_fast_mode()),
+        },
+        None => LanguageModelSelection {
+            provider,
+            model: model_name,
+            enable_thinking: model.supports_thinking(),
+            effort: model
+                .default_effort_level()
+                .map(|effort| effort.value.to_string()),
+            speed: None,
+        },
+    }
+}
 
+impl AgentSettings {
     pub fn get_layout(cx: &App) -> WindowLayout {
         let store = cx.global::<SettingsStore>();
         let merged = store.merged_settings();
@@ -593,7 +634,11 @@ impl Settings for AgentSettings {
             sidebar_side: agent.sidebar_side.unwrap(),
             default_width: px(agent.default_width.unwrap()),
             default_height: px(agent.default_height.unwrap()),
-            max_content_width: px(agent.max_content_width.unwrap()),
+            max_content_width: if agent.limit_content_width.unwrap() {
+                Some(px(agent.max_content_width.unwrap()))
+            } else {
+                None
+            },
             flexible: agent.flexible.unwrap(),
             default_model: Some(agent.default_model.unwrap()),
             inline_assistant_model: agent.inline_assistant_model,

crates/agent_ui/src/agent_configuration.rs πŸ”—

@@ -26,7 +26,7 @@ use language_model::{
     ZED_CLOUD_PROVIDER_ID,
 };
 use language_models::AllLanguageModelSettings;
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
 use project::{
     agent_server_store::{AgentId, AgentServerStore, ExternalAgentSource},
     context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
@@ -1330,40 +1330,44 @@ fn show_unable_to_uninstall_extension_with_context_server(
         move |this, _cx| {
             let workspace_handle = workspace_handle.clone();
 
-            this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
-                .dismiss_button(true)
-                .action("Uninstall", move |_, _cx| {
-                    if let Some((extension_id, _)) =
-                        resolve_extension_for_context_server(&context_server_id, _cx)
-                    {
-                        ExtensionStore::global(_cx).update(_cx, |store, cx| {
-                            store
-                                .uninstall_extension(extension_id, cx)
-                                .detach_and_log_err(cx);
-                        });
+            this.icon(
+                Icon::new(IconName::Warning)
+                    .size(IconSize::Small)
+                    .color(Color::Warning),
+            )
+            .dismiss_button(true)
+            .action("Uninstall", move |_, _cx| {
+                if let Some((extension_id, _)) =
+                    resolve_extension_for_context_server(&context_server_id, _cx)
+                {
+                    ExtensionStore::global(_cx).update(_cx, |store, cx| {
+                        store
+                            .uninstall_extension(extension_id, cx)
+                            .detach_and_log_err(cx);
+                    });
 
-                        workspace_handle
-                            .update(_cx, |workspace, cx| {
-                                let fs = workspace.app_state().fs.clone();
-                                cx.spawn({
-                                    let context_server_id = context_server_id.clone();
-                                    async move |_workspace_handle, cx| {
-                                        cx.update(|cx| {
-                                            update_settings_file(fs, cx, move |settings, _| {
-                                                settings
-                                                    .project
-                                                    .context_servers
-                                                    .remove(&context_server_id.0);
-                                            });
+                    workspace_handle
+                        .update(_cx, |workspace, cx| {
+                            let fs = workspace.app_state().fs.clone();
+                            cx.spawn({
+                                let context_server_id = context_server_id.clone();
+                                async move |_workspace_handle, cx| {
+                                    cx.update(|cx| {
+                                        update_settings_file(fs, cx, move |settings, _| {
+                                            settings
+                                                .project
+                                                .context_servers
+                                                .remove(&context_server_id.0);
                                         });
-                                        anyhow::Ok(())
-                                    }
-                                })
-                                .detach_and_log_err(cx);
+                                    });
+                                    anyhow::Ok(())
+                                }
                             })
-                            .log_err();
-                    }
-                })
+                            .detach_and_log_err(cx);
+                        })
+                        .log_err();
+                }
+            })
         },
     );
 

crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs πŸ”—

@@ -9,7 +9,7 @@ use gpui::{
 };
 use language::{Language, LanguageRegistry};
 use markdown::{Markdown, MarkdownElement, MarkdownStyle};
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
 use parking_lot::Mutex;
 use project::{
     context_server_store::{
@@ -631,8 +631,12 @@ impl ConfigureContextServerModal {
                         format!("{} configured successfully.", id.0),
                         cx,
                         |this, _cx| {
-                            this.icon(ToastIcon::new(IconName::ToolHammer).color(Color::Muted))
-                                .action("Dismiss", |_, _| {})
+                            this.icon(
+                                Icon::new(IconName::ToolHammer)
+                                    .size(IconSize::Small)
+                                    .color(Color::Muted),
+                            )
+                            .action("Dismiss", |_, _| {})
                         },
                     );
 

crates/agent_ui/src/agent_panel.rs πŸ”—

@@ -35,8 +35,8 @@ use crate::{
     AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, CreateWorktree,
     Follow, InlineAssistant, LoadThreadFromClipboard, NewThread, NewWorktreeBranchTarget,
     OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell,
-    SwitchWorktree, ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
-    ToggleWorktreeSelector,
+    ShowAllSidebarThreadMetadata, ShowThreadMetadata, SwitchWorktree, ToggleNavigationMenu,
+    ToggleNewThreadMenu, ToggleOptionsMenu, ToggleWorktreeSelector,
     agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
     conversation_view::{AcpThreadViewEvent, ThreadView},
     thread_worktree_picker::ThreadWorktreePicker,
@@ -49,13 +49,14 @@ use crate::{
 use crate::{ExpandMessageEditor, ThreadHistoryView};
 use crate::{ManageProfiles, ThreadHistoryViewEvent};
 use crate::{ThreadHistory, agent_connection_store::AgentConnectionStore};
-use agent_settings::{AgentSettings, WindowLayout};
+use agent_settings::AgentSettings;
 use ai_onboarding::AgentPanelOnboarding;
 use anyhow::{Context as _, Result, anyhow};
+use chrono::{DateTime, Utc};
 use client::UserStore;
 use cloud_api_types::Plan;
 use collections::HashMap;
-use editor::Editor;
+use editor::{Editor, MultiBuffer};
 use extension::ExtensionEvents;
 use extension_host::ExtensionStore;
 use fs::Fs;
@@ -325,6 +326,24 @@ pub fn init(cx: &mut App) {
                         });
                     }
                 })
+                .register_action(|workspace, _: &ShowThreadMetadata, window, cx| {
+                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+                        panel.update(cx, |panel, cx| {
+                            panel.show_thread_metadata(&ShowThreadMetadata, window, cx);
+                        });
+                    }
+                })
+                .register_action(|workspace, _: &ShowAllSidebarThreadMetadata, window, cx| {
+                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+                        panel.update(cx, |panel, cx| {
+                            panel.show_all_sidebar_thread_metadata(
+                                &ShowAllSidebarThreadMetadata,
+                                window,
+                                cx,
+                            );
+                        });
+                    }
+                })
                 .register_action(|workspace, action: &ReviewBranchDiff, window, cx| {
                     let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
                         return;
@@ -624,6 +643,46 @@ fn build_conflicted_files_resolution_prompt(
     content
 }
 
+fn format_timestamp_human(dt: &DateTime<Utc>) -> String {
+    let now = Utc::now();
+    let duration = now.signed_duration_since(*dt);
+
+    let relative = if duration.num_seconds() < 0 {
+        "in the future".to_string()
+    } else if duration.num_seconds() < 60 {
+        let seconds = duration.num_seconds();
+        format!("{seconds} seconds ago")
+    } else if duration.num_minutes() < 60 {
+        let minutes = duration.num_minutes();
+        format!("{minutes} minutes ago")
+    } else if duration.num_hours() < 24 {
+        let hours = duration.num_hours();
+        format!("{hours} hours ago")
+    } else {
+        let days = duration.num_days();
+        format!("{days} days ago")
+    };
+
+    format!("{} ({})", dt.to_rfc3339(), relative)
+}
+
+/// Used for `dev: show thread metadata` action
+fn thread_metadata_to_debug_json(
+    metadata: &crate::thread_metadata_store::ThreadMetadata,
+) -> serde_json::Value {
+    serde_json::json!({
+        "thread_id": metadata.thread_id,
+        "session_id": metadata.session_id.as_ref().map(|s| s.0.to_string()),
+        "agent_id": metadata.agent_id.0.to_string(),
+        "title": metadata.title.as_ref().map(|t| t.to_string()),
+        "updated_at": format_timestamp_human(&metadata.updated_at),
+        "created_at": metadata.created_at.as_ref().map(format_timestamp_human),
+        "interacted_at": metadata.interacted_at.as_ref().map(format_timestamp_human),
+        "worktree_paths": format!("{:?}", metadata.worktree_paths),
+        "archived": metadata.archived,
+    })
+}
+
 pub(crate) struct AgentThread {
     conversation_view: Entity<ConversationView>,
 }
@@ -760,8 +819,6 @@ pub struct AgentPanel {
     pending_serialization: Option<Task<Result<()>>>,
     new_user_onboarding: Entity<AgentPanelOnboarding>,
     new_user_onboarding_upsell_dismissed: AtomicBool,
-    agent_layout_onboarding: Entity<ai_onboarding::AgentLayoutOnboarding>,
-    agent_layout_onboarding_dismissed: AtomicBool,
     selected_agent: Agent,
     worktree_creation_status: Option<(EntityId, WorktreeCreationStatus)>,
     _thread_view_subscription: Option<Subscription>,
@@ -1064,46 +1121,6 @@ impl AgentPanel {
             )
         });
 
-        let weak_panel = cx.entity().downgrade();
-
-        let layout = AgentSettings::get_layout(cx);
-        let is_agent_layout = matches!(layout, WindowLayout::Agent(_));
-
-        let agent_layout_onboarding = cx.new(|_cx| ai_onboarding::AgentLayoutOnboarding {
-            use_agent_layout: Arc::new({
-                let fs = fs.clone();
-                let weak_panel = weak_panel.clone();
-                move |_window, cx| {
-                    let _ = AgentSettings::set_layout(WindowLayout::Agent(None), fs.clone(), cx);
-                    weak_panel
-                        .update(cx, |panel, cx| {
-                            panel.dismiss_agent_layout_onboarding(cx);
-                        })
-                        .ok();
-                }
-            }),
-            revert_to_editor_layout: Arc::new({
-                let fs = fs.clone();
-                let weak_panel = weak_panel.clone();
-                move |_window, cx| {
-                    let _ = AgentSettings::set_layout(WindowLayout::Editor(None), fs.clone(), cx);
-                    weak_panel
-                        .update(cx, |panel, cx| {
-                            panel.dismiss_agent_layout_onboarding(cx);
-                        })
-                        .ok();
-                }
-            }),
-            dismissed: Arc::new(move |_window, cx| {
-                weak_panel
-                    .update(cx, |panel, cx| {
-                        panel.dismiss_agent_layout_onboarding(cx);
-                    })
-                    .ok();
-            }),
-            is_agent_layout,
-        });
-
         // Subscribe to extension events to sync agent servers when extensions change
         let extension_subscription = if let Some(extension_events) = ExtensionEvents::try_global(cx)
         {
@@ -1168,7 +1185,6 @@ impl AgentPanel {
             zoomed: false,
             pending_serialization: None,
             new_user_onboarding: onboarding,
-            agent_layout_onboarding,
             thread_store,
             selected_agent: Agent::default(),
             worktree_creation_status: None,
@@ -1177,9 +1193,6 @@ impl AgentPanel {
             _worktree_creation_task: None,
             show_trust_workspace_message: false,
             new_user_onboarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed(cx)),
-            agent_layout_onboarding_dismissed: AtomicBool::new(AgentLayoutOnboarding::dismissed(
-                cx,
-            )),
             _base_view_observation: None,
             _draft_editor_observation: None,
         };
@@ -2025,6 +2038,108 @@ impl AgentPanel {
         .detach_and_log_err(cx);
     }
 
+    fn show_thread_metadata(
+        &mut self,
+        _: &ShowThreadMetadata,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(thread_id) = self.active_thread_id(cx) else {
+            Self::show_deferred_toast(&self.workspace, "No active thread", cx);
+            return;
+        };
+
+        let Some(store) = ThreadMetadataStore::try_global(cx) else {
+            Self::show_deferred_toast(&self.workspace, "Thread metadata store not available", cx);
+            return;
+        };
+
+        let Some(metadata) = store.read(cx).entry(thread_id).cloned() else {
+            Self::show_deferred_toast(&self.workspace, "No metadata found for active thread", cx);
+            return;
+        };
+
+        let json = thread_metadata_to_debug_json(&metadata);
+        let text = serde_json::to_string_pretty(&json).unwrap_or_default();
+        let title = format!("Thread Metadata: {}", metadata.display_title());
+
+        self.open_json_buffer(title, text, window, cx);
+    }
+
+    fn show_all_sidebar_thread_metadata(
+        &mut self,
+        _: &ShowAllSidebarThreadMetadata,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(store) = ThreadMetadataStore::try_global(cx) else {
+            Self::show_deferred_toast(&self.workspace, "Thread metadata store not available", cx);
+            return;
+        };
+
+        let entries: Vec<serde_json::Value> = store
+            .read(cx)
+            .entries()
+            .filter(|t| !t.archived)
+            .map(thread_metadata_to_debug_json)
+            .collect();
+
+        let json = serde_json::Value::Array(entries);
+        let text = serde_json::to_string_pretty(&json).unwrap_or_default();
+
+        self.open_json_buffer("All Sidebar Thread Metadata".to_string(), text, window, cx);
+    }
+
+    fn open_json_buffer(
+        &self,
+        title: String,
+        text: String,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let json_language = self.language_registry.language_for_name("JSON");
+        let project = self.project.clone();
+        let workspace = self.workspace.clone();
+
+        window
+            .spawn(cx, async move |cx| {
+                let json_language = json_language.await.ok();
+
+                let buffer = project
+                    .update(cx, |project, cx| {
+                        project.create_buffer(json_language, false, cx)
+                    })
+                    .await?;
+
+                buffer.update(cx, |buffer, cx| {
+                    buffer.set_text(text, cx);
+                    buffer.set_capability(language::Capability::ReadWrite, cx);
+                });
+
+                workspace.update_in(cx, |workspace, window, cx| {
+                    let buffer =
+                        cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.clone()));
+
+                    workspace.add_item_to_active_pane(
+                        Box::new(cx.new(|cx| {
+                            let mut editor =
+                                Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
+                            editor.set_breadcrumb_header(title);
+                            editor.disable_mouse_wheel_zoom();
+                            editor
+                        })),
+                        None,
+                        true,
+                        window,
+                        cx,
+                    );
+                })?;
+
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+    }
+
     fn handle_agent_configuration_event(
         &mut self,
         _entity: &Entity<AgentConfiguration>,
@@ -2445,7 +2560,7 @@ impl AgentPanel {
                 &tv,
                 window,
                 |this, _view, event: &AcpThreadViewEvent, _window, cx| match event {
-                    AcpThreadViewEvent::MessageSentOrQueued => {
+                    AcpThreadViewEvent::Interacted => {
                         let Some(thread_id) = this.active_thread_id(cx) else {
                             return;
                         };
@@ -2457,7 +2572,7 @@ impl AgentPanel {
                             this._draft_editor_observation = None;
                         }
                         this.retained_threads.remove(&thread_id);
-                        cx.emit(AgentPanelEvent::MessageSentOrQueued { thread_id });
+                        cx.emit(AgentPanelEvent::ThreadInteracted { thread_id });
                     }
                 },
             )
@@ -3276,26 +3391,6 @@ impl AgentPanel {
             return;
         }
 
-        let (worktree_receivers, worktree_directory_setting) =
-            if matches!(args, WorktreeCreationArgs::New { .. }) {
-                (
-                    Some(
-                        git_repos
-                            .iter()
-                            .map(|repo| repo.update(cx, |repo, _cx| repo.worktrees()))
-                            .collect::<Vec<_>>(),
-                    ),
-                    Some(
-                        ProjectSettings::get_global(cx)
-                            .git
-                            .worktree_directory
-                            .clone(),
-                    ),
-                )
-            } else {
-                (None, None)
-            };
-
         let remote_connection_options = self.project.read(cx).remote_connection_options(cx);
 
         if remote_connection_options.is_some() {
@@ -3332,10 +3427,18 @@ impl AgentPanel {
                     worktree_name,
                     branch_target,
                 } => {
-                    let worktree_receivers = worktree_receivers
-                        .expect("worktree receivers must be prepared for new worktree creation");
-                    let worktree_directory_setting = worktree_directory_setting
-                        .expect("worktree directory must be prepared for new worktree creation");
+                    let worktree_receivers: Vec<_> = this.update_in(cx, |_this, _window, cx| {
+                        git_repos
+                            .iter()
+                            .map(|repo| repo.update(cx, |repo, _cx| repo.worktrees()))
+                            .collect()
+                    })?;
+                    let worktree_directory_setting = this.update_in(cx, |_this, _window, cx| {
+                        ProjectSettings::get_global(cx)
+                            .git
+                            .worktree_directory
+                            .clone()
+                    })?;
 
                     let mut existing_worktree_names = Vec::new();
                     let mut existing_worktree_paths = HashSet::default();
@@ -3740,7 +3843,7 @@ pub enum AgentPanelEvent {
     ActiveViewChanged,
     ThreadFocused,
     RetainedThreadChanged,
-    MessageSentOrQueued { thread_id: ThreadId },
+    ThreadInteracted { thread_id: ThreadId },
 }
 
 impl EventEmitter<PanelEvent> for AgentPanel {}
@@ -3989,12 +4092,14 @@ impl AgentPanel {
             BaseView::AgentThread { conversation_view } => Some(conversation_view.clone()),
             _ => None,
         };
-        let thread_with_messages = match &self.base_view {
-            BaseView::AgentThread { conversation_view } => {
-                conversation_view.read(cx).has_user_submitted_prompt(cx)
-            }
-            _ => false,
-        };
+
+        let can_regenerate_thread_title =
+            conversation_view.as_ref().is_some_and(|conversation_view| {
+                let conversation_view = conversation_view.read(cx);
+                conversation_view.has_user_submitted_prompt(cx)
+                    && conversation_view.as_native_thread(cx).is_some()
+            });
+
         let has_auth_methods = match &self.base_view {
             BaseView::AgentThread { conversation_view } => {
                 conversation_view.read(cx).has_auth_methods()
@@ -4025,7 +4130,7 @@ impl AgentPanel {
                     Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
                         menu = menu.context(focus_handle.clone());
 
-                        if thread_with_messages {
+                        if can_regenerate_thread_title {
                             menu = menu.header("Current Thread");
 
                             if let Some(conversation_view) = conversation_view.as_ref() {
@@ -4159,7 +4264,7 @@ impl AgentPanel {
             let current_path = &repo.work_directory_abs_path;
 
             return linked_worktree_short_name(main_path, current_path)
-                .unwrap_or_else(|| "main".into());
+                .unwrap_or_else(|| "main worktree".into());
         }
 
         project
@@ -4527,9 +4632,8 @@ impl AgentPanel {
 
         let base_container = h_flex()
             .size_full()
-            // TODO: This is only until we remove Agent settings from the panel.
             .when(!is_in_history_or_config, |this| {
-                this.max_w(max_content_width).mx_auto()
+                this.when_some(max_content_width, |this, max_w| this.max_w(max_w).mx_auto())
             })
             .flex_none()
             .justify_between()
@@ -4737,56 +4841,10 @@ impl AgentPanel {
         plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
     }
 
-    fn should_render_agent_layout_onboarding(&self, cx: &mut Context<Self>) -> bool {
-        // We only want to show this for existing users: those who
-        // have used the agent panel before the sidebar was introduced.
-        // We can infer that state by users having seen the onboarding
-        // at one point, but not the agent layout onboarding.
-
-        let has_messages = self.active_thread_has_messages(cx);
-        let is_dismissed = self
-            .agent_layout_onboarding_dismissed
-            .load(Ordering::Acquire);
-
-        if is_dismissed || has_messages {
-            return false;
-        }
-
-        match &self.base_view {
-            BaseView::Uninitialized => false,
-            BaseView::AgentThread { .. } => {
-                let existing_user = self
-                    .new_user_onboarding_upsell_dismissed
-                    .load(Ordering::Acquire);
-                existing_user
-            }
-        }
-    }
-
-    fn render_agent_layout_onboarding(
-        &self,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<impl IntoElement> {
-        if !self.should_render_agent_layout_onboarding(cx) {
-            return None;
-        }
-
-        Some(div().child(self.agent_layout_onboarding.clone()))
-    }
-
-    fn dismiss_agent_layout_onboarding(&mut self, cx: &mut Context<Self>) {
-        self.agent_layout_onboarding_dismissed
-            .store(true, Ordering::Release);
-        AgentLayoutOnboarding::set_dismissed(true, cx);
-        cx.notify();
-    }
-
     fn dismiss_ai_onboarding(&mut self, cx: &mut Context<Self>) {
         self.new_user_onboarding_upsell_dismissed
             .store(true, Ordering::Release);
         OnboardingUpsell::set_dismissed(true, cx);
-        self.dismiss_agent_layout_onboarding(cx);
         cx.notify();
     }
 
@@ -5048,7 +5106,6 @@ impl Render for AgentPanel {
             .child(self.render_toolbar(window, cx))
             .children(self.render_workspace_trust_message(cx))
             .children(self.render_new_user_onboarding(window, cx))
-            .children(self.render_agent_layout_onboarding(window, cx))
             .map(|parent| match self.visible_surface() {
                 VisibleSurface::Uninitialized => parent,
                 VisibleSurface::AgentThread(conversation_view) => parent
@@ -5139,12 +5196,6 @@ impl Dismissable for OnboardingUpsell {
     const KEY: &'static str = "dismissed-trial-upsell";
 }
 
-struct AgentLayoutOnboarding;
-
-impl Dismissable for AgentLayoutOnboarding {
-    const KEY: &'static str = "dismissed-agent-layout-onboarding";
-}
-
 struct TrialEndUpsell;
 
 impl Dismissable for TrialEndUpsell {

crates/agent_ui/src/agent_ui.rs πŸ”—

@@ -112,6 +112,8 @@ actions!(
         OpenHistory,
         /// Adds a context server to the configuration.
         AddContextServer,
+        /// Archives the currently selected thread.
+        ArchiveSelectedThread,
         /// Removes the currently selected thread.
         RemoveSelectedThread,
         /// Starts a chat conversation with follow-up enabled.
@@ -203,6 +205,16 @@ actions!(
     ]
 );
 
+actions!(
+    dev,
+    [
+        /// Shows metadata for the currently active thread.
+        ShowThreadMetadata,
+        /// Shows metadata for all threads in the sidebar.
+        ShowAllSidebarThreadMetadata,
+    ]
+);
+
 /// Action to authorize a tool call with a specific permission option.
 /// This is used by the permission granularity dropdown to authorize tool calls.
 #[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
@@ -766,7 +778,7 @@ mod tests {
             flexible: true,
             default_width: px(300.),
             default_height: px(600.),
-            max_content_width: px(850.),
+            max_content_width: Some(px(850.)),
             default_model: None,
             inline_assistant_model: None,
             inline_assistant_use_streaming_tools: false,

crates/agent_ui/src/conversation_view.rs πŸ”—

@@ -396,18 +396,18 @@ fn affects_thread_metadata(event: &AcpThreadEvent) -> bool {
     match event {
         AcpThreadEvent::NewEntry
         | AcpThreadEvent::TitleUpdated
-        | AcpThreadEvent::EntryUpdated(_)
-        | AcpThreadEvent::EntriesRemoved(_)
         | AcpThreadEvent::ToolAuthorizationRequested(_)
         | AcpThreadEvent::ToolAuthorizationReceived(_)
-        | AcpThreadEvent::Retry(_)
         | AcpThreadEvent::Stopped(_)
         | AcpThreadEvent::Error
         | AcpThreadEvent::LoadError(_)
         | AcpThreadEvent::Refusal
         | AcpThreadEvent::WorkingDirectoriesUpdated => true,
         // --
-        AcpThreadEvent::TokenUsageUpdated
+        AcpThreadEvent::EntryUpdated(_)
+        | AcpThreadEvent::EntriesRemoved(_)
+        | AcpThreadEvent::Retry(_)
+        | AcpThreadEvent::TokenUsageUpdated
         | AcpThreadEvent::PromptCapabilitiesUpdated
         | AcpThreadEvent::AvailableCommandsUpdated(_)
         | AcpThreadEvent::ModeUpdated(_)

crates/agent_ui/src/conversation_view/thread_view.rs πŸ”—

@@ -206,7 +206,7 @@ impl RenderOnce for GeneratingSpinnerElement {
 }
 
 pub enum AcpThreadViewEvent {
-    MessageSentOrQueued,
+    Interacted,
 }
 
 impl EventEmitter<AcpThreadViewEvent> for ThreadView {}
@@ -954,7 +954,6 @@ impl ThreadView {
         let has_queued = self.has_queued_messages();
         if is_editor_empty && self.can_fast_track_queue && has_queued {
             self.can_fast_track_queue = false;
-            cx.emit(AcpThreadViewEvent::MessageSentOrQueued);
             self.send_queued_message_at_index(0, true, window, cx);
             return;
         }
@@ -964,7 +963,7 @@ impl ThreadView {
         }
 
         if is_generating {
-            cx.emit(AcpThreadViewEvent::MessageSentOrQueued);
+            cx.emit(AcpThreadViewEvent::Interacted);
             self.queue_message(message_editor, window, cx);
             return;
         }
@@ -1006,7 +1005,7 @@ impl ThreadView {
             }
         }
 
-        cx.emit(AcpThreadViewEvent::MessageSentOrQueued);
+        cx.emit(AcpThreadViewEvent::Interacted);
         self.send_impl(message_editor, window, cx)
     }
 
@@ -1209,6 +1208,8 @@ impl ThreadView {
             return;
         }
 
+        cx.emit(AcpThreadViewEvent::Interacted);
+
         let message_editor = self.message_editor.clone();
         if thread.read(cx).status() == ThreadStatus::Idle {
             self.send_impl(message_editor, window, cx);
@@ -1371,6 +1372,7 @@ impl ThreadView {
         }
 
         let task = thread.update(cx, |thread, cx| thread.retry(cx));
+        cx.emit(AcpThreadViewEvent::Interacted);
         self.sync_generating_indicator(cx);
         cx.notify();
         cx.spawn(async move |this, cx| {
@@ -1430,6 +1432,7 @@ impl ThreadView {
                 .update(cx, |thread, cx| thread.rewind(user_message_id, cx))
                 .await?;
             this.update_in(cx, |thread, window, cx| {
+                cx.emit(AcpThreadViewEvent::Interacted);
                 thread.send_impl(message_editor, window, cx);
                 thread.focus_handle(cx).focus(window, cx);
             })?;
@@ -1522,6 +1525,8 @@ impl ThreadView {
             return;
         };
 
+        cx.emit(AcpThreadViewEvent::Interacted);
+
         self.message_editor.focus_handle(cx).focus(window, cx);
 
         let content = queued.content;
@@ -2285,13 +2290,15 @@ impl ThreadView {
 
         h_flex()
             .w_full()
+            .px_2()
             .justify_center()
             .child(
                 v_flex()
-                    .flex_basis(max_content_width)
+                    .when_some(max_content_width, |this, max_w| this.flex_basis(max_w))
+                    .when(max_content_width.is_none(), |this| this.w_full())
                     .flex_shrink()
                     .flex_grow_0()
-                    .mx_2()
+                    .max_w_full()
                     .bg(self.activity_bar_bg(cx))
                     .border_1()
                     .border_b_0()
@@ -2844,7 +2851,7 @@ impl ThreadView {
                 IconButton::new("dismiss-plan", IconName::Close)
                     .icon_size(IconSize::XSmall)
                     .shape(ui::IconButtonShape::Square)
-                    .tooltip(Tooltip::text("Clear plan"))
+                    .tooltip(Tooltip::text("Clear Plan"))
                     .on_click(cx.listener(|this, _, _, cx| {
                         this.thread.update(cx, |thread, cx| thread.clear_plan(cx));
                         cx.stop_propagation();
@@ -2868,51 +2875,64 @@ impl ThreadView {
             .max_h_40()
             .overflow_y_scroll()
             .children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
-                let element = h_flex()
-                    .py_1()
-                    .px_2()
-                    .gap_2()
-                    .justify_between()
-                    .bg(cx.theme().colors().editor_background)
-                    .when(index < plan.entries.len() - 1, |parent| {
-                        parent.border_color(cx.theme().colors().border).border_b_1()
-                    })
-                    .child(
-                        h_flex()
-                            .id(("plan_entry", index))
-                            .gap_1p5()
-                            .max_w_full()
-                            .overflow_x_scroll()
-                            .text_xs()
-                            .text_color(cx.theme().colors().text_muted)
-                            .child(match entry.status {
-                                acp::PlanEntryStatus::InProgress => {
-                                    Icon::new(IconName::TodoProgress)
-                                        .size(IconSize::Small)
-                                        .color(Color::Accent)
-                                        .with_rotate_animation(2)
-                                        .into_any_element()
-                                }
-                                acp::PlanEntryStatus::Completed => {
-                                    Icon::new(IconName::TodoComplete)
-                                        .size(IconSize::Small)
-                                        .color(Color::Success)
-                                        .into_any_element()
-                                }
-                                acp::PlanEntryStatus::Pending | _ => {
-                                    Icon::new(IconName::TodoPending)
-                                        .size(IconSize::Small)
-                                        .color(Color::Muted)
-                                        .into_any_element()
-                                }
-                            })
-                            .child(MarkdownElement::new(
-                                entry.content.clone(),
-                                plan_label_markdown_style(&entry.status, window, cx),
-                            )),
-                    );
+                let entry_bg = cx.theme().colors().editor_background;
+                let tooltip_text: SharedString = entry.content.read(cx).source().to_string().into();
 
-                Some(element)
+                Some(
+                    h_flex()
+                        .id(("plan_entry_row", index))
+                        .py_1()
+                        .px_2()
+                        .gap_2()
+                        .justify_between()
+                        .relative()
+                        .bg(entry_bg)
+                        .when(index < plan.entries.len() - 1, |parent| {
+                            parent.border_color(cx.theme().colors().border).border_b_1()
+                        })
+                        .overflow_hidden()
+                        .child(
+                            h_flex()
+                                .id(("plan_entry", index))
+                                .gap_1p5()
+                                .min_w_0()
+                                .text_xs()
+                                .text_color(cx.theme().colors().text_muted)
+                                .child(match entry.status {
+                                    acp::PlanEntryStatus::InProgress => {
+                                        Icon::new(IconName::TodoProgress)
+                                            .size(IconSize::Small)
+                                            .color(Color::Accent)
+                                            .with_rotate_animation(2)
+                                            .into_any_element()
+                                    }
+                                    acp::PlanEntryStatus::Completed => {
+                                        Icon::new(IconName::TodoComplete)
+                                            .size(IconSize::Small)
+                                            .color(Color::Success)
+                                            .into_any_element()
+                                    }
+                                    acp::PlanEntryStatus::Pending | _ => {
+                                        Icon::new(IconName::TodoPending)
+                                            .size(IconSize::Small)
+                                            .color(Color::Muted)
+                                            .into_any_element()
+                                    }
+                                })
+                                .child(MarkdownElement::new(
+                                    entry.content.clone(),
+                                    plan_label_markdown_style(&entry.status, window, cx),
+                                )),
+                        )
+                        .child(div().absolute().top_0().right_0().h_full().w_8().bg(
+                            linear_gradient(
+                                90.,
+                                linear_color_stop(entry_bg, 1.),
+                                linear_color_stop(entry_bg.opacity(0.), 0.),
+                            ),
+                        ))
+                        .tooltip(Tooltip::text(tooltip_text)),
+                )
             }))
             .into_any_element()
     }
@@ -3181,8 +3201,7 @@ impl ThreadView {
                 .child(
                     h_flex()
                         .size_full()
-                        .max_w(max_content_width)
-                        .mx_auto()
+                        .when_some(max_content_width, |this, max_w| this.max_w(max_w).mx_auto())
                         .pl_2()
                         .pr_1()
                         .flex_shrink_0()
@@ -3279,7 +3298,8 @@ impl ThreadView {
             })
             .child(
                 v_flex()
-                    .flex_basis(max_content_width)
+                    .when_some(max_content_width, |this, max_w| this.flex_basis(max_w))
+                    .when(max_content_width.is_none(), |this| this.w_full())
                     .flex_shrink()
                     .flex_grow_0()
                     .when(fills_container, |this| this.h_full())
@@ -3822,12 +3842,22 @@ impl ThreadView {
                         let enable_thinking = !thread.thinking_enabled();
                         thread.set_thinking_enabled(enable_thinking, cx);
 
+                        let favorite_key = thread.model().map(|model| {
+                            (model.provider_id().0.to_string(), model.id().0.to_string())
+                        });
                         let fs = thread.project().read(cx).fs().clone();
                         update_settings_file(fs, cx, move |settings, _| {
-                            if let Some(agent) = settings.agent.as_mut()
-                                && let Some(default_model) = agent.default_model.as_mut()
-                            {
-                                default_model.enable_thinking = enable_thinking;
+                            if let Some(agent) = settings.agent.as_mut() {
+                                if let Some(default_model) = agent.default_model.as_mut() {
+                                    default_model.enable_thinking = enable_thinking;
+                                }
+                                if let Some((provider_id, model_id)) = &favorite_key {
+                                    agent.update_favorite_model(
+                                        provider_id,
+                                        model_id,
+                                        |favorite| favorite.enable_thinking = enable_thinking,
+                                    );
+                                }
                             }
                         });
                     });
@@ -3958,14 +3988,33 @@ impl ThreadView {
                                                     cx,
                                                 );
 
+                                                let favorite_key = thread.model().map(|model| {
+                                                    (
+                                                        model.provider_id().0.to_string(),
+                                                        model.id().0.to_string(),
+                                                    )
+                                                });
                                                 let fs = thread.project().read(cx).fs().clone();
                                                 update_settings_file(fs, cx, move |settings, _| {
-                                                    if let Some(agent) = settings.agent.as_mut()
-                                                        && let Some(default_model) =
+                                                    if let Some(agent) = settings.agent.as_mut() {
+                                                        if let Some(default_model) =
                                                             agent.default_model.as_mut()
-                                                    {
-                                                        default_model.effort =
-                                                            Some(effort.to_string());
+                                                        {
+                                                            default_model.effort =
+                                                                Some(effort.to_string());
+                                                        }
+                                                        if let Some((provider_id, model_id)) =
+                                                            &favorite_key
+                                                        {
+                                                            agent.update_favorite_model(
+                                                                provider_id,
+                                                                model_id,
+                                                                |favorite| {
+                                                                    favorite.effort =
+                                                                        Some(effort.to_string())
+                                                                },
+                                                            );
+                                                        }
                                                     }
                                                 });
                                             });
@@ -4467,10 +4516,12 @@ impl ThreadView {
     fn render_entries(&mut self, cx: &mut Context<Self>) -> List {
         let max_content_width = AgentSettings::get_global(cx).max_content_width;
         let centered_container = move |content: AnyElement| {
-            h_flex()
-                .w_full()
-                .justify_center()
-                .child(div().max_w(max_content_width).w_full().child(content))
+            h_flex().w_full().justify_center().child(
+                div()
+                    .when_some(max_content_width, |this, max_w| this.max_w(max_w))
+                    .w_full()
+                    .child(content),
+            )
         };
 
         list(
@@ -7693,6 +7744,7 @@ impl ThreadView {
                 gpui::ImageFormat::Bmp => "BMP",
                 gpui::ImageFormat::Tiff => "TIFF",
                 gpui::ImageFormat::Ico => "ICO",
+                gpui::ImageFormat::Pnm => "PNM",
             };
             let dimensions = image::ImageReader::new(std::io::Cursor::new(image.bytes()))
                 .with_guessed_format()
@@ -8771,7 +8823,7 @@ impl ThreadView {
     }
 
     fn render_token_limit_callout(&self, cx: &mut Context<Self>) -> Option<Callout> {
-        if self.token_limit_callout_dismissed {
+        if self.token_limit_callout_dismissed || self.as_native_thread(cx).is_none() {
             return None;
         }
 
@@ -8858,12 +8910,20 @@ impl ThreadView {
                 .unwrap_or(Speed::Fast);
             thread.set_speed(new_speed, cx);
 
+            let favorite_key = thread
+                .model()
+                .map(|model| (model.provider_id().0.to_string(), model.id().0.to_string()));
             let fs = thread.project().read(cx).fs().clone();
             update_settings_file(fs, cx, move |settings, _| {
-                if let Some(agent) = settings.agent.as_mut()
-                    && let Some(default_model) = agent.default_model.as_mut()
-                {
-                    default_model.speed = Some(new_speed);
+                if let Some(agent) = settings.agent.as_mut() {
+                    if let Some(default_model) = agent.default_model.as_mut() {
+                        default_model.speed = Some(new_speed);
+                    }
+                    if let Some((provider_id, model_id)) = &favorite_key {
+                        agent.update_favorite_model(provider_id, model_id, |favorite| {
+                            favorite.speed = Some(new_speed)
+                        });
+                    }
                 }
             });
         });
@@ -8904,12 +8964,20 @@ impl ThreadView {
         thread.update(cx, |thread, cx| {
             thread.set_thinking_effort(Some(next_effort.clone()), cx);
 
+            let favorite_key = thread
+                .model()
+                .map(|model| (model.provider_id().0.to_string(), model.id().0.to_string()));
             let fs = thread.project().read(cx).fs().clone();
             update_settings_file(fs, cx, move |settings, _| {
-                if let Some(agent) = settings.agent.as_mut()
-                    && let Some(default_model) = agent.default_model.as_mut()
-                {
-                    default_model.effort = Some(next_effort);
+                if let Some(agent) = settings.agent.as_mut() {
+                    if let Some(default_model) = agent.default_model.as_mut() {
+                        default_model.effort = Some(next_effort.clone());
+                    }
+                    if let Some((provider_id, model_id)) = &favorite_key {
+                        agent.update_favorite_model(provider_id, model_id, |favorite| {
+                            favorite.effort = Some(next_effort)
+                        });
+                    }
                 }
             });
         });

crates/agent_ui/src/favorite_models.rs πŸ”—

@@ -1,27 +1,27 @@
 use std::sync::Arc;
 
+use agent_settings::{AgentSettings, language_model_to_selection};
 use fs::Fs;
 use language_model::LanguageModel;
-use settings::{LanguageModelSelection, update_settings_file};
+use settings::{Settings as _, update_settings_file};
 use ui::App;
 
-fn language_model_to_selection(model: &Arc<dyn LanguageModel>) -> LanguageModelSelection {
-    LanguageModelSelection {
-        provider: model.provider_id().to_string().into(),
-        model: model.id().0.to_string(),
-        enable_thinking: false,
-        effort: None,
-        speed: None,
-    }
-}
-
 pub fn toggle_in_settings(
     model: Arc<dyn LanguageModel>,
     should_be_favorite: bool,
     fs: Arc<dyn Fs>,
     cx: &mut App,
 ) {
-    let selection = language_model_to_selection(&model);
+    let current_user_selection = AgentSettings::get_global(cx)
+        .default_model
+        .as_ref()
+        .filter(|selection| {
+            selection.provider.0 == model.provider_id().0.as_ref()
+                && selection.model == model.id().0.as_ref()
+        })
+        .cloned();
+
+    let selection = language_model_to_selection(&model, current_user_selection.as_ref());
     update_settings_file(fs, cx, move |settings, _| {
         let agent = settings.agent.get_or_insert_default();
         if should_be_favorite {

crates/agent_ui/src/language_model_selector.rs πŸ”—

@@ -8,8 +8,8 @@ use gpui::{
     Subscription, Task,
 };
 use language_model::{
-    AuthenticateError, ConfiguredModel, IconOrSvg, LanguageModel, LanguageModelId,
-    LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
+    ConfiguredModel, IconOrSvg, LanguageModel, LanguageModelId, LanguageModelProvider,
+    LanguageModelProviderId, LanguageModelRegistry,
 };
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate};
@@ -124,7 +124,6 @@ pub struct LanguageModelPickerDelegate {
     all_models: Arc<GroupedModels>,
     filtered_entries: Vec<LanguageModelPickerEntry>,
     selected_index: usize,
-    _authenticate_all_providers_task: Task<()>,
     _subscriptions: Vec<Subscription>,
     popover_styles: bool,
     focus_handle: FocusHandle,
@@ -151,7 +150,6 @@ impl LanguageModelPickerDelegate {
             filtered_entries: entries,
             get_active_model: Arc::new(get_active_model),
             on_toggle_favorite: Arc::new(on_toggle_favorite),
-            _authenticate_all_providers_task: Self::authenticate_all_providers(cx),
             _subscriptions: vec![cx.subscribe_in(
                 &LanguageModelRegistry::global(cx),
                 window,
@@ -197,56 +195,6 @@ impl LanguageModelPickerDelegate {
             .unwrap_or(0)
     }
 
-    /// Authenticates all providers in the [`LanguageModelRegistry`].
-    ///
-    /// We do this so that we can populate the language selector with all of the
-    /// models from the configured providers.
-    fn authenticate_all_providers(cx: &mut App) -> Task<()> {
-        let authenticate_all_providers = LanguageModelRegistry::global(cx)
-            .read(cx)
-            .visible_providers()
-            .iter()
-            .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
-            .collect::<Vec<_>>();
-
-        cx.spawn(async move |_cx| {
-            for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
-                if let Err(err) = authenticate_task.await {
-                    if matches!(err, AuthenticateError::CredentialsNotFound) {
-                        // Since we're authenticating these providers in the
-                        // background for the purposes of populating the
-                        // language selector, we don't care about providers
-                        // where the credentials are not found.
-                    } else {
-                        // Some providers have noisy failure states that we
-                        // don't want to spam the logs with every time the
-                        // language model selector is initialized.
-                        //
-                        // Ideally these should have more clear failure modes
-                        // that we know are safe to ignore here, like what we do
-                        // with `CredentialsNotFound` above.
-                        match provider_id.0.as_ref() {
-                            "lmstudio" | "ollama" => {
-                                // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
-                                //
-                                // These fail noisily, so we don't log them.
-                            }
-                            "copilot_chat" => {
-                                // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
-                            }
-                            _ => {
-                                log::error!(
-                                    "Failed to authenticate provider: {}: {err:#}",
-                                    provider_name.0
-                                );
-                            }
-                        }
-                    }
-                }
-            }
-        })
-    }
-
     pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
         (self.get_active_model)(cx)
     }

crates/agent_ui/src/thread_history_view.rs πŸ”—

@@ -74,7 +74,7 @@ impl ThreadHistoryView {
     ) -> Self {
         let search_editor = cx.new(|cx| {
             let mut editor = Editor::single_line(window, cx);
-            editor.set_placeholder_text("Search threads...", window, cx);
+            editor.set_placeholder_text("Search all threads…", window, cx);
             editor
         });
 

crates/agent_ui/src/thread_import.rs πŸ”—

@@ -11,7 +11,7 @@ use gpui::{
     App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent,
     Render, SharedString, Task, WeakEntity, Window,
 };
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
 use project::{AgentId, AgentRegistryStore, AgentServerStore};
 use release_channel::ReleaseChannel;
 use remote::RemoteConnectionOptions;
@@ -275,8 +275,12 @@ impl ThreadImportModal {
     fn show_imported_threads_toast(&self, imported_count: usize, cx: &mut App) {
         let status_toast = if imported_count == 0 {
             StatusToast::new("No threads found to import.", cx, |this, _cx| {
-                this.icon(ToastIcon::new(IconName::Info).color(Color::Muted))
-                    .dismiss_button(true)
+                this.icon(
+                    Icon::new(IconName::Info)
+                        .size(IconSize::Small)
+                        .color(Color::Muted),
+                )
+                .dismiss_button(true)
             })
         } else {
             let message = if imported_count == 1 {
@@ -285,8 +289,12 @@ impl ThreadImportModal {
                 format!("Imported {imported_count} threads.")
             };
             StatusToast::new(message, cx, |this, _cx| {
-                this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
-                    .dismiss_button(true)
+                this.icon(
+                    Icon::new(IconName::Check)
+                        .size(IconSize::Small)
+                        .color(Color::Success),
+                )
+                .dismiss_button(true)
             })
         };
 
@@ -383,7 +391,7 @@ impl Render for ThreadImportModal {
                             .headline("Import External Agent Threads")
                             .description(
                                 "Import threads from agents like Claude Agent, Codex, and more, whether started in Zed or another client. \
-                                Choose which agents to include, and their threads will appear in your archive."
+                                Choose which agents to include, and their threads will appear in your thread history."
                             )
                             .show_dismiss_button(true),
 
@@ -661,7 +669,7 @@ fn show_cross_channel_import_toast(
 ) {
     let status_toast = if imported_count == 0 {
         StatusToast::new("No new threads found to import.", cx, |this, _cx| {
-            this.icon(ToastIcon::new(IconName::Info).color(Color::Muted))
+            this.icon(Icon::new(IconName::Info).color(Color::Muted))
                 .dismiss_button(true)
         })
     } else {
@@ -671,7 +679,7 @@ fn show_cross_channel_import_toast(
             format!("Imported {imported_count} threads from other channels.")
         };
         StatusToast::new(message, cx, |this, _cx| {
-            this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
+            this.icon(Icon::new(IconName::Check).color(Color::Success))
                 .dismiss_button(true)
         })
     };

crates/agent_ui/src/thread_metadata_store.rs πŸ”—

@@ -346,7 +346,7 @@ pub fn worktree_info_from_thread_paths<S: std::hash::BuildHasher>(
                 .unwrap_or_default();
             linked_short_names.push((short_name.clone(), project_name));
             infos.push(ThreadItemWorktreeInfo {
-                name: short_name,
+                worktree_name: Some(short_name),
                 full_path: SharedString::from(folder_path.display().to_string()),
                 highlight_positions: Vec::new(),
                 kind: WorktreeKind::Linked,
@@ -357,7 +357,7 @@ pub fn worktree_info_from_thread_paths<S: std::hash::BuildHasher>(
                 continue;
             };
             infos.push(ThreadItemWorktreeInfo {
-                name: SharedString::from(name.to_string_lossy().to_string()),
+                worktree_name: Some(SharedString::from(name.to_string_lossy().to_string())),
                 full_path: SharedString::from(folder_path.display().to_string()),
                 highlight_positions: Vec::new(),
                 kind: WorktreeKind::Main,
@@ -370,7 +370,10 @@ pub fn worktree_info_from_thread_paths<S: std::hash::BuildHasher>(
     // folder paths don't all share the same short name, prefix each
     // linked worktree chip with its main project name so the user knows
     // which project it belongs to.
-    let all_same_name = infos.len() > 1 && infos.iter().all(|i| i.name == infos[0].name);
+    let all_same_name = infos.len() > 1
+        && infos
+            .iter()
+            .all(|i| i.worktree_name == infos[0].worktree_name);
 
     if unique_main_count.len() > 1 && !all_same_name {
         for (info, (_short_name, project_name)) in infos
@@ -378,7 +381,9 @@ pub fn worktree_info_from_thread_paths<S: std::hash::BuildHasher>(
             .filter(|i| i.kind == WorktreeKind::Linked)
             .zip(linked_short_names.iter())
         {
-            info.name = SharedString::from(format!("{}:{}", project_name, info.name));
+            if let Some(name) = &info.worktree_name {
+                info.worktree_name = Some(SharedString::from(format!("{}:{}", project_name, name)));
+            }
         }
     }
 
@@ -1171,7 +1176,9 @@ impl ThreadMetadataStore {
             .and_then(|t| t.created_at)
             .unwrap_or_else(|| updated_at);
 
-        let interacted_at = existing_thread.and_then(|t| t.interacted_at);
+        let interacted_at = existing_thread
+            .map(|t| t.interacted_at)
+            .unwrap_or(Some(updated_at));
 
         let agent_id = thread_ref.connection().agent_id();
 

crates/agent_ui/src/thread_worktree_archive.rs πŸ”—

@@ -247,78 +247,9 @@ async fn remove_root_after_worktree_removal(
     // alive until the repo removes the worktree
     drop(project);
     result.context("git worktree metadata cleanup failed")?;
-
-    // Empty-parent cleanup uses local std::fs β€” skip for remote projects.
-    if root.remote_connection.is_none() {
-        remove_empty_parent_dirs_up_to_worktrees_base(
-            root.root_path.clone(),
-            root.main_repo_path.clone(),
-            cx,
-        )
-        .await;
-    }
-
     Ok(())
 }
 
-/// After `git worktree remove` deletes the worktree directory, clean up any
-/// empty parent directories between it and the Zed-managed worktrees base
-/// directory (configured via `git.worktree_directory`). The base directory
-/// itself is never removed.
-///
-/// If the base directory is not an ancestor of `root_path`, no parent
-/// directories are removed.
-async fn remove_empty_parent_dirs_up_to_worktrees_base(
-    root_path: PathBuf,
-    main_repo_path: PathBuf,
-    cx: &mut AsyncApp,
-) {
-    let worktrees_base = cx.update(|cx| worktrees_base_for_repo(&main_repo_path, cx));
-
-    if let Some(worktrees_base) = worktrees_base {
-        cx.background_executor()
-            .spawn(async move {
-                remove_empty_ancestors(&root_path, &worktrees_base);
-            })
-            .await;
-    }
-}
-
-/// Removes empty directories between `child_path` and `base_path`.
-///
-/// Walks upward from `child_path`, removing each empty parent directory,
-/// stopping before `base_path` itself is removed. If `base_path` is not
-/// an ancestor of `child_path`, nothing is removed. If any directory is
-/// non-empty (i.e. `std::fs::remove_dir` fails), the walk stops.
-fn remove_empty_ancestors(child_path: &Path, base_path: &Path) {
-    let mut current = child_path;
-    while let Some(parent) = current.parent() {
-        if parent == base_path {
-            break;
-        }
-        if !parent.starts_with(base_path) {
-            break;
-        }
-        match std::fs::remove_dir(parent) {
-            Ok(()) => {
-                log::info!("Removed empty parent directory: {}", parent.display());
-            }
-            Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => break,
-            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
-                // Already removed by a concurrent process; keep walking upward.
-            }
-            Err(err) => {
-                log::error!(
-                    "Failed to remove parent directory {}: {err}",
-                    parent.display()
-                );
-                break;
-            }
-        }
-        current = parent;
-    }
-}
-
 /// Finds a live `Repository` entity for the given path, or creates a temporary
 /// project to obtain one.
 ///
@@ -913,7 +844,6 @@ mod tests {
     use project::Project;
     use serde_json::json;
     use settings::SettingsStore;
-    use tempfile::TempDir;
     use workspace::MultiWorkspace;
 
     fn init_test(cx: &mut TestAppContext) {
@@ -926,117 +856,6 @@ mod tests {
         });
     }
 
-    #[test]
-    fn test_remove_empty_ancestors_single_empty_parent() {
-        let tmp = TempDir::new().unwrap();
-        let base = tmp.path().join("worktrees");
-        let branch_dir = base.join("my-branch");
-        let child = branch_dir.join("zed");
-
-        std::fs::create_dir_all(&child).unwrap();
-        // Simulate git worktree remove having deleted the child.
-        std::fs::remove_dir(&child).unwrap();
-
-        assert!(branch_dir.exists());
-        remove_empty_ancestors(&child, &base);
-        assert!(!branch_dir.exists(), "empty parent should be removed");
-        assert!(base.exists(), "base directory should be preserved");
-    }
-
-    #[test]
-    fn test_remove_empty_ancestors_nested_empty_parents() {
-        let tmp = TempDir::new().unwrap();
-        let base = tmp.path().join("worktrees");
-        // Branch name with slash creates nested dirs: fix/thing/zed
-        let child = base.join("fix").join("thing").join("zed");
-
-        std::fs::create_dir_all(&child).unwrap();
-        std::fs::remove_dir(&child).unwrap();
-
-        assert!(base.join("fix").join("thing").exists());
-        remove_empty_ancestors(&child, &base);
-        assert!(!base.join("fix").join("thing").exists());
-        assert!(
-            !base.join("fix").exists(),
-            "all empty ancestors should be removed"
-        );
-        assert!(base.exists(), "base directory should be preserved");
-    }
-
-    #[test]
-    fn test_remove_empty_ancestors_stops_at_non_empty_parent() {
-        let tmp = TempDir::new().unwrap();
-        let base = tmp.path().join("worktrees");
-        let branch_dir = base.join("my-branch");
-        let child = branch_dir.join("zed");
-        let sibling = branch_dir.join("other-file.txt");
-
-        std::fs::create_dir_all(&child).unwrap();
-        std::fs::write(&sibling, "content").unwrap();
-        std::fs::remove_dir(&child).unwrap();
-
-        remove_empty_ancestors(&child, &base);
-        assert!(branch_dir.exists(), "non-empty parent should be preserved");
-        assert!(sibling.exists());
-    }
-
-    #[test]
-    fn test_remove_empty_ancestors_not_an_ancestor() {
-        let tmp = TempDir::new().unwrap();
-        let base = tmp.path().join("worktrees");
-        let unrelated = tmp.path().join("other-place").join("branch").join("zed");
-
-        std::fs::create_dir_all(&base).unwrap();
-        std::fs::create_dir_all(&unrelated).unwrap();
-        std::fs::remove_dir(&unrelated).unwrap();
-
-        let parent = unrelated.parent().unwrap();
-        assert!(parent.exists());
-        remove_empty_ancestors(&unrelated, &base);
-        assert!(parent.exists(), "should not remove dirs outside base");
-    }
-
-    #[test]
-    fn test_remove_empty_ancestors_child_is_direct_child_of_base() {
-        let tmp = TempDir::new().unwrap();
-        let base = tmp.path().join("worktrees");
-        let child = base.join("zed");
-
-        std::fs::create_dir_all(&child).unwrap();
-        std::fs::remove_dir(&child).unwrap();
-
-        remove_empty_ancestors(&child, &base);
-        assert!(base.exists(), "base directory should be preserved");
-    }
-
-    #[test]
-    fn test_remove_empty_ancestors_partially_non_empty_chain() {
-        let tmp = TempDir::new().unwrap();
-        let base = tmp.path().join("worktrees");
-        // Structure: base/a/b/c/zed where a/ has another child besides b/
-        let child = base.join("a").join("b").join("c").join("zed");
-        let other_in_a = base.join("a").join("other-branch");
-
-        std::fs::create_dir_all(&child).unwrap();
-        std::fs::create_dir_all(&other_in_a).unwrap();
-        std::fs::remove_dir(&child).unwrap();
-
-        remove_empty_ancestors(&child, &base);
-        assert!(
-            !base.join("a").join("b").join("c").exists(),
-            "c/ should be removed (empty)"
-        );
-        assert!(
-            !base.join("a").join("b").exists(),
-            "b/ should be removed (empty)"
-        );
-        assert!(
-            base.join("a").exists(),
-            "a/ should be preserved (has other-branch sibling)"
-        );
-        assert!(other_in_a.exists());
-    }
-
     #[gpui::test]
     async fn test_build_root_plan_returns_none_for_main_worktree(cx: &mut TestAppContext) {
         init_test(cx);

crates/agent_ui/src/thread_worktree_picker.rs πŸ”—

@@ -361,7 +361,6 @@ impl PickerDelegate for ThreadWorktreePickerDelegate {
         }
 
         // When the user is typing, fuzzy-match worktree names using display_name
-        // For the main worktree, also match against "main"
         let main_worktree_path = repo_worktrees
             .iter()
             .find(|wt| wt.is_main)

crates/agent_ui/src/threads_archive_view.rs πŸ”—

@@ -7,7 +7,7 @@ use crate::agent_connection_store::AgentConnectionStore;
 use crate::thread_metadata_store::{
     ThreadId, ThreadMetadata, ThreadMetadataStore, worktree_info_from_thread_paths,
 };
-use crate::{Agent, DEFAULT_THREAD_TITLE, RemoveSelectedThread};
+use crate::{Agent, ArchiveSelectedThread, DEFAULT_THREAD_TITLE, RemoveSelectedThread};
 
 use agent::ThreadStore;
 use agent_client_protocol as acp;
@@ -30,10 +30,9 @@ use picker::{
 use project::{AgentId, AgentServerStore};
 use settings::Settings as _;
 use theme::ActiveTheme;
-use ui::{AgentThreadStatus, IconDecoration, IconDecorationKind, Tab, ThreadItem};
 use ui::{
-    Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Tooltip, WithScrollbar,
-    prelude::*, utils::platform_title_bar_height,
+    AgentThreadStatus, Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Tab,
+    ThreadItem, Tooltip, WithScrollbar, prelude::*, utils::platform_title_bar_height,
 };
 use ui_input::ErasedEditor;
 use util::ResultExt;
@@ -46,6 +45,13 @@ use workspace::{
 use zed_actions::agents_sidebar::FocusSidebarFilter;
 use zed_actions::editor::{MoveDown, MoveUp};
 
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+enum ThreadFilter {
+    #[default]
+    All,
+    ArchivedOnly,
+}
+
 #[derive(Clone)]
 enum ArchiveListItem {
     BucketSeparator(TimeBucket),
@@ -118,6 +124,7 @@ pub enum ThreadsArchiveViewEvent {
     Close,
     Activate { thread: ThreadMetadata },
     CancelRestore { thread_id: ThreadId },
+    Import,
 }
 
 impl EventEmitter<ThreadsArchiveViewEvent> for ThreadsArchiveView {}
@@ -140,7 +147,7 @@ pub struct ThreadsArchiveView {
     archived_thread_ids: HashSet<ThreadId>,
     archived_branch_names: HashMap<ThreadId, HashMap<PathBuf, String>>,
     _load_branch_names_task: Task<()>,
-    show_archived_only: bool,
+    thread_filter: ThreadFilter,
 }
 
 impl ThreadsArchiveView {
@@ -155,7 +162,7 @@ impl ThreadsArchiveView {
 
         let filter_editor = cx.new(|cx| {
             let mut editor = Editor::single_line(window, cx);
-            editor.set_placeholder_text("Search threads…", window, cx);
+            editor.set_placeholder_text("Search all threads…", window, cx);
             editor
         });
 
@@ -214,7 +221,7 @@ impl ThreadsArchiveView {
             archived_thread_ids: HashSet::default(),
             archived_branch_names: HashMap::default(),
             _load_branch_names_task: Task::ready(()),
-            show_archived_only: false,
+            thread_filter: ThreadFilter::All,
         };
 
         this.update_items(cx);
@@ -253,11 +260,14 @@ impl ThreadsArchiveView {
     }
 
     fn update_items(&mut self, cx: &mut Context<Self>) {
-        let show_archived_only = self.show_archived_only;
+        let thread_filter = self.thread_filter;
         let sessions = ThreadMetadataStore::global(cx)
             .read(cx)
             .entries()
-            .filter(|t| !show_archived_only || t.archived)
+            .filter(|t| match thread_filter {
+                ThreadFilter::All => true,
+                ThreadFilter::ArchivedOnly => t.archived,
+            })
             .sorted_by_cached_key(|t| t.created_at.unwrap_or(t.updated_at))
             .rev()
             .cloned()
@@ -310,11 +320,7 @@ impl ThreadsArchiveView {
         let preserve = self.preserve_selection_on_next_update;
         self.preserve_selection_on_next_update = false;
 
-        let saved_scroll = if preserve {
-            Some(self.list_state.logical_scroll_top())
-        } else {
-            None
-        };
+        let saved_scroll = self.list_state.logical_scroll_top();
 
         self.list_state.reset(items.len());
         self.items = items;
@@ -327,9 +333,9 @@ impl ThreadsArchiveView {
             }
         }
 
-        if let Some(scroll_top) = saved_scroll {
-            self.list_state.scroll_to(scroll_top);
+        self.list_state.scroll_to(saved_scroll);
 
+        if preserve {
             if let Some(ix) = self.selection {
                 let next = self.find_next_selectable(ix).or_else(|| {
                     ix.checked_sub(1)
@@ -389,6 +395,24 @@ impl ThreadsArchiveView {
         ThreadMetadataStore::global(cx).update(cx, |store, cx| store.archive(thread_id, None, cx));
     }
 
+    fn archive_selected_thread(
+        &mut self,
+        _: &ArchiveSelectedThread,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(ix) = self.selection else { return };
+        let Some(ArchiveListItem::Entry { thread, .. }) = self.items.get(ix) else {
+            return;
+        };
+
+        if thread.archived {
+            return;
+        }
+
+        self.archive_thread(thread.thread_id, cx);
+    }
+
     fn unarchive_thread(
         &mut self,
         thread: ThreadMetadata,
@@ -606,24 +630,14 @@ impl ThreadsArchiveView {
                     &branch_names_for_thread,
                 );
 
-                let color = cx.theme().colors();
-                let knockout_color = color
-                    .title_bar_background
-                    .blend(color.panel_background.opacity(0.25));
-                let archived_decoration =
-                    IconDecoration::new(IconDecorationKind::Archive, knockout_color, cx)
-                        .color(color.icon_disabled)
-                        .position(gpui::Point {
-                            x: px(-3.),
-                            y: px(-3.5),
-                        });
+                let archived_color = Color::Custom(cx.theme().colors().icon_muted.opacity(0.6));
 
                 let base = ThreadItem::new(id, thread.display_title())
                     .icon(icon)
                     .when(is_archived, |this| {
-                        this.icon_color(Color::Muted)
+                        this.archived(true)
+                            .icon_color(archived_color)
                             .title_label_color(Color::Muted)
-                            .icon_decoration(archived_decoration)
                     })
                     .when_some(icon_from_external_svg, |this, svg| {
                         this.custom_icon_from_external_svg(svg)
@@ -661,7 +675,6 @@ impl ThreadsArchiveView {
                                     })
                                 }),
                         )
-                        .tooltip(Tooltip::text("Restoring…"))
                         .into_any_element()
                 } else if is_archived {
                     base.action_slot(
@@ -694,9 +707,6 @@ impl ThreadsArchiveView {
                                 })
                             }),
                     )
-                    .tooltip(move |_, cx| {
-                        Tooltip::for_action("Open Archived Thread", &menu::Confirm, cx)
-                    })
                     .on_click({
                         let thread = thread.clone();
                         cx.listener(move |this, _, window, cx| {
@@ -709,7 +719,16 @@ impl ThreadsArchiveView {
                         IconButton::new("archive-thread", IconName::Archive)
                             .icon_size(IconSize::Small)
                             .icon_color(Color::Muted)
-                            .tooltip(Tooltip::text("Archive Thread"))
+                            .tooltip({
+                                move |_window, cx| {
+                                    Tooltip::for_action_in(
+                                        "Archive Thread",
+                                        &ArchiveSelectedThread,
+                                        &focus_handle,
+                                        cx,
+                                    )
+                                }
+                            })
                             .on_click({
                                 let thread_id = thread.thread_id;
                                 cx.listener(move |this, _, _, cx| {
@@ -718,7 +737,6 @@ impl ThreadsArchiveView {
                                 })
                             }),
                     )
-                    .tooltip(move |_, cx| Tooltip::for_action("Open Thread", &menu::Confirm, cx))
                     .on_click({
                         let thread = thread.clone();
                         cx.listener(move |this, _, window, cx| {
@@ -869,14 +887,13 @@ impl ThreadsArchiveView {
             .filter(|item| matches!(item, ArchiveListItem::Entry { .. }))
             .count();
 
+        let has_archived_threads = {
+            let store = ThreadMetadataStore::global(cx).read(cx);
+            store.archived_entries().next().is_some()
+        };
+
         let count_label = if entry_count == 1 {
-            if self.show_archived_only {
-                "1 archived thread".to_string()
-            } else {
-                "1 thread".to_string()
-            }
-        } else if self.show_archived_only {
-            format!("{} archived threads", entry_count)
+            "1 thread".to_string()
         } else {
             format!("{} threads", entry_count)
         };
@@ -895,18 +912,37 @@ impl ThreadsArchiveView {
                     .color(Color::Muted),
             )
             .child(
-                IconButton::new("toggle-archived-only", IconName::ListFilter)
-                    .icon_size(IconSize::Small)
-                    .toggle_state(self.show_archived_only)
-                    .tooltip(Tooltip::text(if self.show_archived_only {
-                        "Show All Threads"
-                    } else {
-                        "Show Archived Only"
-                    }))
-                    .on_click(cx.listener(|this, _, _, cx| {
-                        this.show_archived_only = !this.show_archived_only;
-                        this.update_items(cx);
-                    })),
+                h_flex()
+                    .child(
+                        IconButton::new("thread-import", IconName::Download)
+                            .icon_size(IconSize::Small)
+                            .tooltip(Tooltip::text("Import Threads"))
+                            .on_click(cx.listener(|_this, _, _, cx| {
+                                cx.emit(ThreadsArchiveViewEvent::Import);
+                            })),
+                    )
+                    .child(
+                        IconButton::new("filter-archived-only", IconName::Archive)
+                            .icon_size(IconSize::Small)
+                            .disabled(!has_archived_threads)
+                            .toggle_state(self.thread_filter == ThreadFilter::ArchivedOnly)
+                            .tooltip(Tooltip::text(
+                                if self.thread_filter == ThreadFilter::ArchivedOnly {
+                                    "Show All Threads"
+                                } else {
+                                    "Show Only Archived Threads"
+                                },
+                            ))
+                            .on_click(cx.listener(|this, _, _, cx| {
+                                this.thread_filter =
+                                    if this.thread_filter == ThreadFilter::ArchivedOnly {
+                                        ThreadFilter::All
+                                    } else {
+                                        ThreadFilter::ArchivedOnly
+                                    };
+                                this.update_items(cx);
+                            })),
+                    ),
             )
     }
 }
@@ -989,6 +1025,7 @@ impl Render for ThreadsArchiveView {
             .on_action(cx.listener(Self::select_last))
             .on_action(cx.listener(Self::confirm))
             .on_action(cx.listener(Self::remove_selected_thread))
+            .on_action(cx.listener(Self::archive_selected_thread))
             .size_full()
             .child(self.render_header(window, cx))
             .when(!has_query, |this| this.child(self.render_toolbar(cx)))

crates/agent_ui/src/ui/undo_reject_toast.rs πŸ”—

@@ -1,6 +1,6 @@
 use action_log::ActionLog;
 use gpui::{App, Entity};
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
 use ui::prelude::*;
 use workspace::Workspace;
 
@@ -11,15 +11,19 @@ pub fn show_undo_reject_toast(
 ) {
     let action_log_weak = action_log.downgrade();
     let status_toast = StatusToast::new("Agent Changes Rejected", cx, move |this, _cx| {
-        this.icon(ToastIcon::new(IconName::Undo).color(Color::Muted))
-            .action("Undo", move |_window, cx| {
-                if let Some(action_log) = action_log_weak.upgrade() {
-                    action_log
-                        .update(cx, |action_log, cx| action_log.undo_last_reject(cx))
-                        .detach();
-                }
-            })
-            .dismiss_button(true)
+        this.icon(
+            Icon::new(IconName::Undo)
+                .size(IconSize::Small)
+                .color(Color::Muted),
+        )
+        .action("Undo", move |_window, cx| {
+            if let Some(action_log) = action_log_weak.upgrade() {
+                action_log
+                    .update(cx, |action_log, cx| action_log.undo_last_reject(cx))
+                    .detach();
+            }
+        })
+        .dismiss_button(true)
     });
     workspace.toggle_status_toast(status_toast, cx);
 }

crates/ai_onboarding/src/ai_onboarding.rs πŸ”—

@@ -17,9 +17,7 @@ use std::sync::Arc;
 
 use client::{Client, UserStore, zed_urls};
 use gpui::{AnyElement, Entity, IntoElement, ParentElement};
-use ui::{
-    Divider, List, ListBulletItem, RegisterComponent, Tooltip, Vector, VectorName, prelude::*,
-};
+use ui::{Divider, RegisterComponent, Tooltip, Vector, VectorName, prelude::*};
 
 #[derive(PartialEq)]
 pub enum SignInStatus {
@@ -442,131 +440,3 @@ impl Component for ZedAiOnboarding {
         )
     }
 }
-
-#[derive(RegisterComponent)]
-pub struct AgentLayoutOnboarding {
-    pub use_agent_layout: Arc<dyn Fn(&mut Window, &mut App)>,
-    pub revert_to_editor_layout: Arc<dyn Fn(&mut Window, &mut App)>,
-    pub dismissed: Arc<dyn Fn(&mut Window, &mut App)>,
-    pub is_agent_layout: bool,
-}
-
-impl Render for AgentLayoutOnboarding {
-    fn render(&mut self, _window: &mut ui::Window, _cx: &mut Context<Self>) -> impl IntoElement {
-        let description = "With the new Threads Sidebar, you can manage multiple agents across several projects, all in one window.";
-
-        let dismiss_button = div().absolute().top_0().right_0().child(
-            IconButton::new("dismiss", IconName::Close)
-                .icon_size(IconSize::Small)
-                .on_click({
-                    let dismiss = self.dismissed.clone();
-                    move |_, window, cx| {
-                        telemetry::event!("Agentic Layout Onboarding Dismissed");
-                        dismiss(window, cx)
-                    }
-                }),
-        );
-
-        let primary_button = if self.is_agent_layout {
-            Button::new("revert", "Use Previous Layout")
-                .label_size(LabelSize::Small)
-                .style(ButtonStyle::Outlined)
-                .on_click({
-                    let revert = self.revert_to_editor_layout.clone();
-                    let dismiss = self.dismissed.clone();
-                    move |_, window, cx| {
-                        telemetry::event!("Clicked to Use Previous Layout");
-                        revert(window, cx);
-                        dismiss(window, cx);
-                    }
-                })
-        } else {
-            Button::new("start", "Use New Layout")
-                .label_size(LabelSize::Small)
-                .style(ButtonStyle::Outlined)
-                .on_click({
-                    let use_layout = self.use_agent_layout.clone();
-                    let dismiss = self.dismissed.clone();
-                    move |_, window, cx| {
-                        telemetry::event!("Clicked to Use New Layout");
-                        use_layout(window, cx);
-                        dismiss(window, cx);
-                    }
-                })
-        };
-
-        let content = v_flex()
-            .min_w_0()
-            .w_full()
-            .relative()
-            .gap_1()
-            .child(Label::new("A new workspace layout for agentic workflows"))
-            .child(Label::new(description).color(Color::Muted).mb_2())
-            .child(
-                List::new()
-                    .child(ListBulletItem::new(
-                        "The Sidebar and Agent Panel are on the left by default",
-                    ))
-                    .child(ListBulletItem::new(
-                        "The Project Panel and all other panels shift to the right",
-                    ))
-                    .child(ListBulletItem::new(
-                        "You can always customize your workspace layout in your Settings",
-                    )),
-            )
-            .child(
-                h_flex()
-                    .w_full()
-                    .gap_1()
-                    .flex_wrap()
-                    .justify_end()
-                    .child(
-                        Button::new("learn", "Learn More")
-                            .label_size(LabelSize::Small)
-                            .style(ButtonStyle::OutlinedGhost)
-                            .on_click(move |_, _, cx| {
-                                cx.open_url(&zed_urls::parallel_agents_blog(cx))
-                            }),
-                    )
-                    .child(primary_button),
-            )
-            .child(dismiss_button);
-
-        AgentPanelOnboardingCard::new().child(content)
-    }
-}
-
-impl Component for AgentLayoutOnboarding {
-    fn scope() -> ComponentScope {
-        ComponentScope::Onboarding
-    }
-
-    fn name() -> &'static str {
-        "Agent Layout Onboarding"
-    }
-
-    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
-        let onboarding = cx.new(|_cx| AgentLayoutOnboarding {
-            use_agent_layout: Arc::new(|_, _| {}),
-            revert_to_editor_layout: Arc::new(|_, _| {}),
-            dismissed: Arc::new(|_, _| {}),
-            is_agent_layout: false,
-        });
-
-        Some(
-            v_flex()
-                .min_w_0()
-                .gap_4()
-                .child(single_example(
-                    "Agent Layout Onboarding",
-                    div()
-                        .w_full()
-                        .min_w_40()
-                        .max_w(px(1100.))
-                        .child(onboarding)
-                        .into_any_element(),
-                ))
-                .into_any_element(),
-        )
-    }
-}

crates/auto_update_ui/Cargo.toml πŸ”—

@@ -19,6 +19,7 @@ client.workspace = true
 db.workspace = true
 fs.workspace = true
 editor.workspace = true
+notifications.workspace = true
 gpui.workspace = true
 markdown_preview.workspace = true
 release_channel.workspace = true

crates/auto_update_ui/src/auto_update_ui.rs πŸ”—

@@ -9,6 +9,7 @@ use gpui::{
     App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Window, actions, prelude::*,
 };
 use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
+use notifications::status_toast::StatusToast;
 use release_channel::{AppVersion, ReleaseChannel};
 use semver::Version;
 use serde::Deserialize;
@@ -207,17 +208,17 @@ fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementC
         let fs = <dyn Fs>::global(cx);
         Some(AnnouncementContent {
             heading: "Introducing Parallel Agents".into(),
-            description: "Run multiple agent threads simultaneously across projects.".into(),
+            description: "Run multiple threads of your favorite agents simultaneously across projects in a new workspace layout, tailored for agentic workflows.".into(),
             bullet_items: vec![
                 "Use your favorite agents in parallel".into(),
                 "Optionally isolate agents using worktrees".into(),
                 "Combine multiple projects in one window".into(),
             ],
-            primary_action_label: "Try Now".into(),
+            primary_action_label: "Try Agentic Layout".into(),
             primary_action_url: None,
             primary_action_callback: Some(Arc::new(move |window, cx| {
-                let already_agent_layout =
-                    matches!(AgentSettings::get_layout(cx), WindowLayout::Agent(_));
+                let get_layout = AgentSettings::get_layout(cx);
+                let already_agent_layout = matches!(get_layout, WindowLayout::Agent(_));
 
                 let update;
                 if !already_agent_layout {
@@ -230,6 +231,7 @@ fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementC
                     update = None;
                 }
 
+                let revert_fs = fs.clone();
                 window
                     .spawn(cx, async move |cx| {
                         if let Some(update) = update {
@@ -237,6 +239,35 @@ fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementC
                         }
 
                         cx.update(|window, cx| {
+                            if !already_agent_layout {
+                                if let Some(workspace) = Workspace::for_window(window, cx) {
+                                    let toast = StatusToast::new(
+                                        "You are in the new agentic layout!",
+                                        cx,
+                                        move |this, _cx| {
+                                            this.icon(
+                                                Icon::new(IconName::Check)
+                                                    .size(IconSize::Small)
+                                                    .color(Color::Success),
+                                            )
+                                            .action("Revert", move |_window, cx| {
+                                                let _ = AgentSettings::set_layout(
+                                                    get_layout.clone(),
+                                                    revert_fs.clone(),
+                                                    cx,
+                                                );
+                                            })
+                                            .auto_dismiss(false)
+                                            .dismiss_button(true)
+                                        },
+                                    );
+
+                                    workspace.update(cx, |workspace, cx| {
+                                        workspace.toggle_status_toast(toast, cx);
+                                    });
+                                }
+                            }
+
                             window.dispatch_action(Box::new(FocusWorkspaceSidebar), cx);
                             window.dispatch_action(Box::new(FocusAgent), cx);
                         })
@@ -381,8 +412,10 @@ pub fn notify_if_app_was_updated(cx: &mut App) {
     }
 
     let should_show_notification = updater.read(cx).should_show_update_notification(cx);
+
     cx.spawn(async move |cx| {
         let should_show_notification = should_show_notification.await?;
+
         if should_show_notification {
             cx.update(|cx| {
                 show_update_notification(cx);

crates/component_preview/src/component_preview.rs πŸ”—

@@ -8,7 +8,7 @@ use gpui::{
 };
 use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle};
 use language::LanguageRegistry;
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
 use persistence::ComponentPreviewDb;
 use project::Project;
 use std::{iter::Iterator, ops::Range, sync::Arc};
@@ -561,10 +561,14 @@ impl ComponentPreview {
             workspace.update(cx, |workspace, cx| {
                 let status_toast =
                     StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
-                        this.icon(ToastIcon::new(IconName::GitBranch).color(Color::Muted))
-                            .action("Open Pull Request", |_, cx| {
-                                cx.open_url("https://github.com/")
-                            })
+                        this.icon(
+                            Icon::new(IconName::GitBranch)
+                                .size(IconSize::Small)
+                                .color(Color::Muted),
+                        )
+                        .action("Open Pull Request", |_, cx| {
+                            cx.open_url("https://github.com/")
+                        })
                     });
                 workspace.toggle_status_toast(status_toast, cx)
             });

crates/csv_preview/src/csv_preview.rs πŸ”—

@@ -1,5 +1,5 @@
 use editor::{Editor, EditorEvent};
-use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
+use feature_flags::{FeatureFlag, FeatureFlagAppExt as _, PresenceFlag, register_feature_flag};
 use gpui::{
     AppContext, Entity, EventEmitter, FocusHandle, Focusable, ListAlignment, Task, actions,
 };
@@ -29,7 +29,9 @@ pub struct TabularDataPreviewFeatureFlag;
 
 impl FeatureFlag for TabularDataPreviewFeatureFlag {
     const NAME: &'static str = "tabular-data-preview";
+    type Value = PresenceFlag;
 }
+register_feature_flag!(TabularDataPreviewFeatureFlag);
 
 pub struct CsvPreviewView {
     pub(crate) engine: TableDataEngine,

crates/debugger_ui/src/debugger_panel.rs πŸ”—

@@ -15,7 +15,7 @@ use dap::adapters::DebugAdapterName;
 use dap::{DapRegistry, StartDebuggingRequestArguments};
 use dap::{client::SessionId, debugger_settings::DebuggerSettings};
 use editor::{Editor, MultiBufferOffset, ToPoint};
-use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
+use feature_flags::{FeatureFlag, FeatureFlagAppExt as _, PresenceFlag, register_feature_flag};
 use gpui::{
     Action, App, AsyncWindowContext, ClipboardItem, Context, Corner, DismissEvent, Entity,
     EntityId, EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point,
@@ -50,7 +50,9 @@ pub struct DebuggerHistoryFeatureFlag;
 
 impl FeatureFlag for DebuggerHistoryFeatureFlag {
     const NAME: &'static str = "debugger-history";
+    type Value = PresenceFlag;
 }
+register_feature_flag!(DebuggerHistoryFeatureFlag);
 
 const DEBUG_PANEL_KEY: &str = "DebugPanel";
 

crates/debugger_ui/src/session/running/memory_view.rs πŸ”—

@@ -14,7 +14,7 @@ use gpui::{
     Subscription, Task, TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions,
     anchored, deferred, uniform_list,
 };
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
 use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session};
 use settings::Settings;
 use theme_settings::ThemeSettings;
@@ -480,7 +480,7 @@ impl MemoryView {
                                             cx.emit(DismissEvent)
                                         });
                                     }).detach();
-                                    this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
+                                    this.icon(Icon::new(IconName::XCircle).size(IconSize::Small).color(Color::Error))
                                 }),
                                 cx,
                             );

crates/edit_prediction/src/edit_prediction.rs πŸ”—

@@ -16,7 +16,7 @@ use copilot::{Copilot, Reinstall, SignIn, SignOut};
 use credentials_provider::CredentialsProvider;
 use db::kvp::{Dismissable, KeyValueStore};
 use edit_prediction_context::{RelatedExcerptStore, RelatedExcerptStoreEvent, RelatedFile};
-use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
+use feature_flags::{FeatureFlag, FeatureFlagAppExt as _, PresenceFlag, register_feature_flag};
 use futures::{
     AsyncReadExt as _, FutureExt as _, StreamExt as _,
     channel::mpsc::{self, UnboundedReceiver},
@@ -119,7 +119,9 @@ pub struct EditPredictionJumpsFeatureFlag;
 
 impl FeatureFlag for EditPredictionJumpsFeatureFlag {
     const NAME: &'static str = "edit_prediction_jumps";
+    type Value = PresenceFlag;
 }
+register_feature_flag!(EditPredictionJumpsFeatureFlag);
 
 #[derive(Clone)]
 struct EditPredictionStoreGlobal(Entity<EditPredictionStore>);

crates/edit_prediction_ui/src/edit_prediction_ui.rs πŸ”—

@@ -115,9 +115,9 @@ fn feature_gate_predict_edits_actions(cx: &mut App) {
     })
     .detach();
 
-    cx.observe_flag::<PredictEditsRatePredictionsFeatureFlag, _>(move |is_enabled, cx| {
+    cx.observe_flag::<PredictEditsRatePredictionsFeatureFlag, _>(move |value, cx| {
         if !DisableAiSettings::get_global(cx).disable_ai {
-            if is_enabled {
+            if *value {
                 CommandPaletteFilter::update_global(cx, |filter, _cx| {
                     filter.show_action_types(&rate_completion_action_types);
                 });

crates/edit_prediction_ui/src/rate_prediction_modal.rs πŸ”—

@@ -1,7 +1,7 @@
 use buffer_diff::BufferDiff;
 use edit_prediction::{EditPrediction, EditPredictionRating, EditPredictionStore};
 use editor::{Editor, Inlay, MultiBuffer};
-use feature_flags::FeatureFlag;
+use feature_flags::{FeatureFlag, PresenceFlag, register_feature_flag};
 use gpui::{
     App, BorderStyle, DismissEvent, EdgesRefinement, Entity, EventEmitter, FocusHandle, Focusable,
     Length, StyleRefinement, TextStyleRefinement, Window, actions, prelude::*,
@@ -43,7 +43,9 @@ pub struct PredictEditsRatePredictionsFeatureFlag;
 
 impl FeatureFlag for PredictEditsRatePredictionsFeatureFlag {
     const NAME: &'static str = "predict-edits-rate-completions";
+    type Value = PresenceFlag;
 }
+register_feature_flag!(PredictEditsRatePredictionsFeatureFlag);
 
 pub struct RatePredictionsModal {
     ep_store: Entity<EditPredictionStore>,

crates/feature_flags/Cargo.toml πŸ”—

@@ -12,4 +12,15 @@ workspace = true
 path = "src/feature_flags.rs"
 
 [dependencies]
+collections.workspace = true
+feature_flags_macros.workspace = true
+fs.workspace = true
 gpui.workspace = true
+inventory.workspace = true
+schemars.workspace = true
+serde_json.workspace = true
+settings.workspace = true
+
+[dev-dependencies]
+gpui = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }

crates/feature_flags/src/feature_flags.rs πŸ”—

@@ -1,4 +1,10 @@
+// Makes the derive macro's reference to `::feature_flags::FeatureFlagValue`
+// resolve when the macro is invoked inside this crate itself.
+extern crate self as feature_flags;
+
 mod flags;
+mod settings;
+mod store;
 
 use std::cell::RefCell;
 use std::rc::Rc;
@@ -6,33 +12,98 @@ use std::sync::LazyLock;
 
 use gpui::{App, Context, Global, Subscription, Window};
 
+pub use feature_flags_macros::EnumFeatureFlag;
 pub use flags::*;
-
-#[derive(Default)]
-struct FeatureFlags {
-    flags: Vec<String>,
-    staff: bool,
-}
+pub use settings::{FeatureFlagsSettings, generate_feature_flags_schema};
+pub use store::*;
 
 pub static ZED_DISABLE_STAFF: LazyLock<bool> = LazyLock::new(|| {
     std::env::var("ZED_DISABLE_STAFF").is_ok_and(|value| !value.is_empty() && value != "0")
 });
 
-impl FeatureFlags {
-    fn has_flag<T: FeatureFlag>(&self) -> bool {
-        if T::enabled_for_all() {
-            return true;
+impl Global for FeatureFlagStore {}
+
+pub trait FeatureFlagValue:
+    Sized + Clone + Eq + Default + std::fmt::Debug + Send + Sync + 'static
+{
+    /// Every possible value for this flag, in the order the UI should display them.
+    fn all_variants() -> &'static [Self];
+
+    /// A stable identifier for this variant used when persisting overrides.
+    fn override_key(&self) -> &'static str;
+
+    fn from_wire(wire: &str) -> Option<Self>;
+
+    /// Human-readable label for use in the configuration UI.
+    fn label(&self) -> &'static str {
+        self.override_key()
+    }
+
+    /// The variant that represents "on" β€” what the store resolves to when
+    /// staff rules, `enabled_for_all`, or a server announcement apply.
+    ///
+    /// For enum flags this is usually the same as [`Default::default`] (the
+    /// variant marked `#[default]` in the derive). [`PresenceFlag`] overrides
+    /// this so that `default() == Off` (the "unconfigured" state) but
+    /// `on_variant() == On` (the "enabled" state).
+    fn on_variant() -> Self {
+        Self::default()
+    }
+}
+
+/// Default value type for simple on/off feature flags.
+///
+/// The fallback value is [`PresenceFlag::Off`] so that an absent / unknown
+/// flag reads as disabled; the `on_variant` override pins the "enabled"
+/// state to [`PresenceFlag::On`] so staff / server / `enabled_for_all`
+/// resolution still lights the flag up.
+#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
+pub enum PresenceFlag {
+    On,
+    #[default]
+    Off,
+}
+
+/// Presence flags deref to a `bool` so call sites can use `if *flag` without
+/// spelling out the enum variant β€” or pass them anywhere a `&bool` is wanted.
+impl std::ops::Deref for PresenceFlag {
+    type Target = bool;
+
+    fn deref(&self) -> &bool {
+        match self {
+            PresenceFlag::On => &true,
+            PresenceFlag::Off => &false,
         }
+    }
+}
 
-        if (cfg!(debug_assertions) || self.staff) && !*ZED_DISABLE_STAFF && T::enabled_for_staff() {
-            return true;
+impl FeatureFlagValue for PresenceFlag {
+    fn all_variants() -> &'static [Self] {
+        &[PresenceFlag::On, PresenceFlag::Off]
+    }
+
+    fn override_key(&self) -> &'static str {
+        match self {
+            PresenceFlag::On => "on",
+            PresenceFlag::Off => "off",
         }
+    }
 
-        self.flags.iter().any(|f| f.as_str() == T::NAME)
+    fn label(&self) -> &'static str {
+        match self {
+            PresenceFlag::On => "On",
+            PresenceFlag::Off => "Off",
+        }
     }
-}
 
-impl Global for FeatureFlags {}
+    fn from_wire(_: &str) -> Option<Self> {
+        Some(PresenceFlag::On)
+    }
+
+    fn on_variant() -> Self {
+        PresenceFlag::On
+    }
+}
 
 /// To create a feature flag, implement this trait on a trivial type and use it as
 /// a generic parameter when called [`FeatureFlagAppExt::has_flag`].
@@ -43,6 +114,10 @@ impl Global for FeatureFlags {}
 pub trait FeatureFlag {
     const NAME: &'static str;
 
+    /// The type of value this flag can hold. Use [`PresenceFlag`] for simple
+    /// on/off flags.
+    type Value: FeatureFlagValue;
+
     /// Returns whether this feature flag is enabled for Zed staff.
     fn enabled_for_staff() -> bool {
         true
@@ -55,12 +130,23 @@ pub trait FeatureFlag {
     fn enabled_for_all() -> bool {
         false
     }
+
+    /// Subscribes the current view to changes in the feature flag store, so
+    /// that any mutation of flags or overrides will trigger a re-render.
+    ///
+    /// The returned subscription is immediately detached; use [`observe_flag`]
+    /// directly if you need to hold onto the subscription.
+    fn watch<V: 'static>(cx: &mut Context<V>) {
+        cx.observe_global::<FeatureFlagStore>(|_, cx| cx.notify())
+            .detach();
+    }
 }
 
 pub trait FeatureFlagViewExt<V: 'static> {
+    /// Fires the callback whenever the resolved [`T::Value`] transitions.
     fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
     where
-        F: Fn(bool, &mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static;
+        F: Fn(T::Value, &mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static;
 
     fn when_flag_enabled<T: FeatureFlag>(
         &mut self,
@@ -75,11 +161,16 @@ where
 {
     fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
     where
-        F: Fn(bool, &mut V, &mut Window, &mut Context<V>) + 'static,
+        F: Fn(T::Value, &mut V, &mut Window, &mut Context<V>) + 'static,
     {
-        self.observe_global_in::<FeatureFlags>(window, move |v, window, cx| {
-            let feature_flags = cx.global::<FeatureFlags>();
-            callback(feature_flags.has_flag::<T>(), v, window, cx);
+        let mut last_value: Option<T::Value> = None;
+        self.observe_global_in::<FeatureFlagStore>(window, move |v, window, cx| {
+            let value = cx.flag_value::<T>();
+            if last_value.as_ref() == Some(&value) {
+                return;
+            }
+            last_value = Some(value.clone());
+            callback(value, v, window, cx);
         })
     }
 
@@ -89,8 +180,8 @@ where
         callback: impl Fn(&mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static,
     ) {
         if self
-            .try_global::<FeatureFlags>()
-            .is_some_and(|f| f.has_flag::<T>())
+            .try_global::<FeatureFlagStore>()
+            .is_some_and(|f| f.has_flag::<T>(self))
         {
             self.defer_in(window, move |view, window, cx| {
                 callback(view, window, cx);
@@ -98,11 +189,11 @@ where
             return;
         }
         let subscription = Rc::new(RefCell::new(None));
-        let inner = self.observe_global_in::<FeatureFlags>(window, {
+        let inner = self.observe_global_in::<FeatureFlagStore>(window, {
             let subscription = subscription.clone();
             move |v, window, cx| {
-                let feature_flags = cx.global::<FeatureFlags>();
-                if feature_flags.has_flag::<T>() {
+                let has_flag = cx.global::<FeatureFlagStore>().has_flag::<T>(cx);
+                if has_flag {
                     callback(v, window, cx);
                     subscription.take();
                 }
@@ -121,6 +212,7 @@ pub trait FeatureFlagAppExt {
     fn update_flags(&mut self, staff: bool, flags: Vec<String>);
     fn set_staff(&mut self, staff: bool);
     fn has_flag<T: FeatureFlag>(&self) -> bool;
+    fn flag_value<T: FeatureFlag>(&self) -> T::Value;
     fn is_staff(&self) -> bool;
 
     fn on_flags_ready<F>(&mut self, callback: F) -> Subscription
@@ -129,33 +221,35 @@ pub trait FeatureFlagAppExt {
 
     fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
     where
-        F: FnMut(bool, &mut App) + 'static;
+        F: FnMut(T::Value, &mut App) + 'static;
 }
 
 impl FeatureFlagAppExt for App {
     fn update_flags(&mut self, staff: bool, flags: Vec<String>) {
-        let feature_flags = self.default_global::<FeatureFlags>();
-        feature_flags.staff = staff;
-        feature_flags.flags = flags;
+        let store = self.default_global::<FeatureFlagStore>();
+        store.update_server_flags(staff, flags);
     }
 
     fn set_staff(&mut self, staff: bool) {
-        let feature_flags = self.default_global::<FeatureFlags>();
-        feature_flags.staff = staff;
+        let store = self.default_global::<FeatureFlagStore>();
+        store.set_staff(staff);
     }
 
     fn has_flag<T: FeatureFlag>(&self) -> bool {
-        self.try_global::<FeatureFlags>()
-            .map(|flags| flags.has_flag::<T>())
-            .unwrap_or_else(|| {
-                (cfg!(debug_assertions) && T::enabled_for_staff() && !*ZED_DISABLE_STAFF)
-                    || T::enabled_for_all()
-            })
+        self.try_global::<FeatureFlagStore>()
+            .map(|store| store.has_flag::<T>(self))
+            .unwrap_or_else(|| FeatureFlagStore::has_flag_default::<T>())
+    }
+
+    fn flag_value<T: FeatureFlag>(&self) -> T::Value {
+        self.try_global::<FeatureFlagStore>()
+            .and_then(|store| store.try_flag_value::<T>(self))
+            .unwrap_or_default()
     }
 
     fn is_staff(&self) -> bool {
-        self.try_global::<FeatureFlags>()
-            .map(|flags| flags.staff)
+        self.try_global::<FeatureFlagStore>()
+            .map(|store| store.is_staff())
             .unwrap_or(false)
     }
 
@@ -163,11 +257,11 @@ impl FeatureFlagAppExt for App {
     where
         F: FnMut(OnFlagsReady, &mut App) + 'static,
     {
-        self.observe_global::<FeatureFlags>(move |cx| {
-            let feature_flags = cx.global::<FeatureFlags>();
+        self.observe_global::<FeatureFlagStore>(move |cx| {
+            let store = cx.global::<FeatureFlagStore>();
             callback(
                 OnFlagsReady {
-                    is_staff: feature_flags.staff,
+                    is_staff: store.is_staff(),
                 },
                 cx,
             );
@@ -176,11 +270,16 @@ impl FeatureFlagAppExt for App {
 
     fn observe_flag<T: FeatureFlag, F>(&mut self, mut callback: F) -> Subscription
     where
-        F: FnMut(bool, &mut App) + 'static,
+        F: FnMut(T::Value, &mut App) + 'static,
     {
-        self.observe_global::<FeatureFlags>(move |cx| {
-            let feature_flags = cx.global::<FeatureFlags>();
-            callback(feature_flags.has_flag::<T>(), cx);
+        let mut last_value: Option<T::Value> = None;
+        self.observe_global::<FeatureFlagStore>(move |cx| {
+            let value = cx.flag_value::<T>();
+            if last_value.as_ref() == Some(&value) {
+                return;
+            }
+            last_value = Some(value.clone());
+            callback(value, cx);
         })
     }
 }

crates/feature_flags/src/flags.rs πŸ”—

@@ -1,26 +1,32 @@
-use crate::FeatureFlag;
+use crate::{EnumFeatureFlag, FeatureFlag, PresenceFlag, register_feature_flag};
 
 pub struct NotebookFeatureFlag;
 
 impl FeatureFlag for NotebookFeatureFlag {
     const NAME: &'static str = "notebooks";
+    type Value = PresenceFlag;
 }
+register_feature_flag!(NotebookFeatureFlag);
 
 pub struct PanicFeatureFlag;
 
 impl FeatureFlag for PanicFeatureFlag {
     const NAME: &'static str = "panic";
+    type Value = PresenceFlag;
 }
+register_feature_flag!(PanicFeatureFlag);
 
 pub struct AgentV2FeatureFlag;
 
 impl FeatureFlag for AgentV2FeatureFlag {
     const NAME: &'static str = "agent-v2";
+    type Value = PresenceFlag;
 
     fn enabled_for_staff() -> bool {
         true
     }
 }
+register_feature_flag!(AgentV2FeatureFlag);
 
 /// A feature flag for granting access to beta ACP features.
 ///
@@ -29,50 +35,83 @@ pub struct AcpBetaFeatureFlag;
 
 impl FeatureFlag for AcpBetaFeatureFlag {
     const NAME: &'static str = "acp-beta";
+    type Value = PresenceFlag;
 }
+register_feature_flag!(AcpBetaFeatureFlag);
 
 pub struct AgentSharingFeatureFlag;
 
 impl FeatureFlag for AgentSharingFeatureFlag {
     const NAME: &'static str = "agent-sharing";
+    type Value = PresenceFlag;
 }
+register_feature_flag!(AgentSharingFeatureFlag);
 
 pub struct DiffReviewFeatureFlag;
 
 impl FeatureFlag for DiffReviewFeatureFlag {
     const NAME: &'static str = "diff-review";
+    type Value = PresenceFlag;
 
     fn enabled_for_staff() -> bool {
         false
     }
 }
+register_feature_flag!(DiffReviewFeatureFlag);
 
 pub struct StreamingEditFileToolFeatureFlag;
 
 impl FeatureFlag for StreamingEditFileToolFeatureFlag {
     const NAME: &'static str = "streaming-edit-file-tool";
+    type Value = PresenceFlag;
 
     fn enabled_for_staff() -> bool {
         true
     }
 }
+register_feature_flag!(StreamingEditFileToolFeatureFlag);
 
 pub struct UpdatePlanToolFeatureFlag;
 
 impl FeatureFlag for UpdatePlanToolFeatureFlag {
     const NAME: &'static str = "update-plan-tool";
+    type Value = PresenceFlag;
 
     fn enabled_for_staff() -> bool {
         false
     }
 }
+register_feature_flag!(UpdatePlanToolFeatureFlag);
 
 pub struct ProjectPanelUndoRedoFeatureFlag;
 
 impl FeatureFlag for ProjectPanelUndoRedoFeatureFlag {
     const NAME: &'static str = "project-panel-undo-redo";
+    type Value = PresenceFlag;
 
     fn enabled_for_staff() -> bool {
         true
     }
 }
+register_feature_flag!(ProjectPanelUndoRedoFeatureFlag);
+
+/// Controls how agent thread worktree chips are labeled in the sidebar.
+#[derive(Clone, Copy, PartialEq, Eq, Debug, EnumFeatureFlag)]
+pub enum AgentThreadWorktreeLabel {
+    #[default]
+    Both,
+    Worktree,
+    Branch,
+}
+
+pub struct AgentThreadWorktreeLabelFlag;
+
+impl FeatureFlag for AgentThreadWorktreeLabelFlag {
+    const NAME: &'static str = "agent-thread-worktree-label";
+    type Value = AgentThreadWorktreeLabel;
+
+    fn enabled_for_staff() -> bool {
+        false
+    }
+}
+register_feature_flag!(AgentThreadWorktreeLabelFlag);

crates/feature_flags/src/settings.rs πŸ”—

@@ -0,0 +1,76 @@
+use collections::HashMap;
+use schemars::{Schema, json_schema};
+use serde_json::{Map, Value};
+use settings::{RegisterSetting, Settings, SettingsContent};
+
+use crate::FeatureFlagStore;
+
+#[derive(Clone, Debug, Default, RegisterSetting)]
+pub struct FeatureFlagsSettings {
+    pub overrides: HashMap<String, String>,
+}
+
+impl Settings for FeatureFlagsSettings {
+    fn from_settings(content: &SettingsContent) -> Self {
+        Self {
+            overrides: content
+                .feature_flags
+                .as_ref()
+                .map(|map| map.0.clone())
+                .unwrap_or_default(),
+        }
+    }
+}
+
+/// Produces a JSON schema for the `feature_flags` object that lists each known
+/// flag as a property with its variant keys as an `enum`.
+///
+/// Unknown flags are permitted via `additionalProperties: { "type": "string" }`,
+/// so removing a flag from the binary never turns existing entries in
+/// `settings.json` into validation errors.
+pub fn generate_feature_flags_schema() -> Schema {
+    let mut properties = Map::new();
+
+    for descriptor in FeatureFlagStore::known_flags() {
+        let variants = (descriptor.variants)();
+        let enum_values: Vec<Value> = variants
+            .iter()
+            .map(|v| Value::String(v.override_key.to_string()))
+            .collect();
+        let enum_descriptions: Vec<Value> = variants
+            .iter()
+            .map(|v| Value::String(v.label.to_string()))
+            .collect();
+
+        let mut property = Map::new();
+        property.insert("type".to_string(), Value::String("string".to_string()));
+        property.insert("enum".to_string(), Value::Array(enum_values));
+        // VS Code / json-language-server use `enumDescriptions` for hover docs
+        // on each enum value; schemars passes them through untouched.
+        property.insert(
+            "enumDescriptions".to_string(),
+            Value::Array(enum_descriptions),
+        );
+        property.insert(
+            "description".to_string(),
+            Value::String(format!(
+                "Override for the `{}` feature flag. Default: `{}` (the {} variant).",
+                descriptor.name,
+                (descriptor.default_variant_key)(),
+                (descriptor.default_variant_key)(),
+            )),
+        );
+
+        properties.insert(descriptor.name.to_string(), Value::Object(property));
+    }
+
+    json_schema!({
+        "type": "object",
+        "description": "Local overrides for feature flags, keyed by flag name.",
+        "properties": properties,
+        "additionalProperties": {
+            "type": "string",
+            "description": "Unknown feature flag; retained so removed flags don't trip settings validation."
+        }
+    })
+}

crates/feature_flags/src/store.rs πŸ”—

@@ -0,0 +1,374 @@
+use std::any::TypeId;
+use std::sync::Arc;
+
+use collections::HashMap;
+use fs::Fs;
+use gpui::{App, BorrowAppContext, Subscription};
+use settings::{Settings, SettingsStore, update_settings_file};
+
+use crate::{FeatureFlag, FeatureFlagValue, FeatureFlagsSettings, ZED_DISABLE_STAFF};
+
+pub struct FeatureFlagDescriptor {
+    pub name: &'static str,
+    pub variants: fn() -> Vec<FeatureFlagVariant>,
+    pub on_variant_key: fn() -> &'static str,
+    pub default_variant_key: fn() -> &'static str,
+    pub enabled_for_all: fn() -> bool,
+    pub enabled_for_staff: fn() -> bool,
+    pub type_id: fn() -> TypeId,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct FeatureFlagVariant {
+    pub override_key: &'static str,
+    pub label: &'static str,
+}
+
+inventory::collect!(FeatureFlagDescriptor);
+
+#[doc(hidden)]
+pub mod __private {
+    pub use inventory;
+}
+
+/// Submits a [`FeatureFlagDescriptor`] for this flag so it shows up in the
+/// configuration UI and in `FeatureFlagStore::known_flags()`.
+#[macro_export]
+macro_rules! register_feature_flag {
+    ($flag:ty) => {
+        $crate::__private::inventory::submit! {
+            $crate::FeatureFlagDescriptor {
+                name: <$flag as $crate::FeatureFlag>::NAME,
+                variants: || {
+                    <<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::all_variants()
+                        .iter()
+                        .map(|v| $crate::FeatureFlagVariant {
+                            override_key: <<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::override_key(v),
+                            label: <<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::label(v),
+                        })
+                        .collect()
+                },
+                on_variant_key: || {
+                    <<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::override_key(
+                        &<<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::on_variant(),
+                    )
+                },
+                default_variant_key: || {
+                    <<$flag as $crate::FeatureFlag>::Value as $crate::FeatureFlagValue>::override_key(
+                        &<<$flag as $crate::FeatureFlag>::Value as ::std::default::Default>::default(),
+                    )
+                },
+                enabled_for_all: <$flag as $crate::FeatureFlag>::enabled_for_all,
+                enabled_for_staff: <$flag as $crate::FeatureFlag>::enabled_for_staff,
+                type_id: || std::any::TypeId::of::<$flag>(),
+            }
+        }
+    };
+}
+
+#[derive(Default)]
+pub struct FeatureFlagStore {
+    staff: bool,
+    server_flags: HashMap<String, String>,
+
+    _settings_subscription: Option<Subscription>,
+}
+
+impl FeatureFlagStore {
+    pub fn init(cx: &mut App) {
+        let subscription = cx.observe_global::<SettingsStore>(|cx| {
+            // Touch the global so anything observing `FeatureFlagStore` re-runs
+            cx.update_default_global::<FeatureFlagStore, _>(|_, _| {});
+        });
+
+        cx.update_default_global::<FeatureFlagStore, _>(|store, _| {
+            store._settings_subscription = Some(subscription);
+        });
+    }
+
+    pub fn known_flags() -> impl Iterator<Item = &'static FeatureFlagDescriptor> {
+        let mut seen = collections::HashSet::default();
+        inventory::iter::<FeatureFlagDescriptor>().filter(move |d| seen.insert((d.type_id)()))
+    }
+
+    pub fn is_staff(&self) -> bool {
+        self.staff
+    }
+
+    pub fn set_staff(&mut self, staff: bool) {
+        self.staff = staff;
+    }
+
+    pub fn update_server_flags(&mut self, staff: bool, flags: Vec<String>) {
+        self.staff = staff;
+        self.server_flags.clear();
+        for flag in flags {
+            self.server_flags.insert(flag.clone(), flag);
+        }
+    }
+
+    /// The user's override key for this flag, read directly from
+    /// [`FeatureFlagsSettings`].
+    pub fn override_for<'a>(flag_name: &str, cx: &'a App) -> Option<&'a str> {
+        FeatureFlagsSettings::get_global(cx)
+            .overrides
+            .get(flag_name)
+            .map(String::as_str)
+    }
+
+    /// Applies an override by writing to `settings.json`. The store's own
+    /// `overrides` field will be updated when the settings-store observer
+    /// fires. Pass the [`FeatureFlagValue::override_key`] of the variant
+    /// you want forced.
+    pub fn set_override(flag_name: &str, override_key: String, fs: Arc<dyn Fs>, cx: &App) {
+        let flag_name = flag_name.to_owned();
+        update_settings_file(fs, cx, move |content, _| {
+            content
+                .feature_flags
+                .get_or_insert_default()
+                .insert(flag_name, override_key);
+        });
+    }
+
+    /// Removes any override for the given flag from `settings.json`. Leaves
+    /// an empty `"feature_flags"` object rather than removing the key
+    /// entirely so the user can see it's still a meaningful settings surface.
+    pub fn clear_override(flag_name: &str, fs: Arc<dyn Fs>, cx: &App) {
+        let flag_name = flag_name.to_owned();
+        update_settings_file(fs, cx, move |content, _| {
+            if let Some(map) = content.feature_flags.as_mut() {
+                map.remove(&flag_name);
+            }
+        });
+    }
+
+    /// The resolved value of the flag for the current user, taking overrides,
+    /// `enabled_for_all`, staff rules, and server flags into account in that
+    /// order of precedence. Overrides are read directly from
+    /// [`FeatureFlagsSettings`].
+    pub fn try_flag_value<T: FeatureFlag>(&self, cx: &App) -> Option<T::Value> {
+        // `enabled_for_all` always wins, including over user overrides.
+        if T::enabled_for_all() {
+            return Some(T::Value::on_variant());
+        }
+
+        if let Some(override_key) = FeatureFlagsSettings::get_global(cx).overrides.get(T::NAME) {
+            return variant_from_key::<T::Value>(override_key);
+        }
+
+        // Staff default: resolve to the enabled variant.
+        if (cfg!(debug_assertions) || self.staff) && !*ZED_DISABLE_STAFF && T::enabled_for_staff() {
+            return Some(T::Value::on_variant());
+        }
+
+        // Server-delivered flag.
+        if let Some(wire) = self.server_flags.get(T::NAME) {
+            return T::Value::from_wire(wire);
+        }
+
+        None
+    }
+
+    /// Whether the flag resolves to its "on" value. Best for presence-style
+    /// flags. For enum flags with meaningful non-default variants, prefer
+    /// [`crate::FeatureFlagAppExt::flag_value`].
+    pub fn has_flag<T: FeatureFlag>(&self, cx: &App) -> bool {
+        self.try_flag_value::<T>(cx)
+            .is_some_and(|v| v == T::Value::on_variant())
+    }
+
+    /// Mirrors the resolution order of [`Self::try_flag_value`], but falls
+    /// back to the [`Default`] variant when no rule applies so the UI always
+    /// shows *something* selected β€” matching what
+    /// [`crate::FeatureFlagAppExt::flag_value`] would return.
+    pub fn resolved_key(&self, descriptor: &FeatureFlagDescriptor, cx: &App) -> &'static str {
+        let on_variant_key = (descriptor.on_variant_key)();
+
+        if (descriptor.enabled_for_all)() {
+            return on_variant_key;
+        }
+
+        if let Some(requested) = FeatureFlagsSettings::get_global(cx)
+            .overrides
+            .get(descriptor.name)
+        {
+            if let Some(variant) = (descriptor.variants)()
+                .into_iter()
+                .find(|v| v.override_key == requested.as_str())
+            {
+                return variant.override_key;
+            }
+        }
+
+        if (cfg!(debug_assertions) || self.staff)
+            && !*ZED_DISABLE_STAFF
+            && (descriptor.enabled_for_staff)()
+        {
+            return on_variant_key;
+        }
+
+        if self.server_flags.contains_key(descriptor.name) {
+            return on_variant_key;
+        }
+
+        (descriptor.default_variant_key)()
+    }
+
+    /// Whether this flag is forced on by `enabled_for_all` and therefore not
+    /// user-overridable. The UI uses this to render the row as disabled.
+    pub fn is_forced_on(descriptor: &FeatureFlagDescriptor) -> bool {
+        (descriptor.enabled_for_all)()
+    }
+
+    /// Fallback used when the store isn't installed as a global yet (e.g. very
+    /// early in startup). Matches the pre-existing default behavior.
+    pub fn has_flag_default<T: FeatureFlag>() -> bool {
+        if T::enabled_for_all() {
+            return true;
+        }
+        cfg!(debug_assertions) && T::enabled_for_staff() && !*ZED_DISABLE_STAFF
+    }
+}
+
+fn variant_from_key<V: FeatureFlagValue>(key: &str) -> Option<V> {
+    V::all_variants()
+        .iter()
+        .find(|v| v.override_key() == key)
+        .cloned()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{EnumFeatureFlag, FeatureFlag, PresenceFlag};
+    use gpui::UpdateGlobal;
+    use settings::SettingsStore;
+
+    struct DemoFlag;
+    impl FeatureFlag for DemoFlag {
+        const NAME: &'static str = "demo";
+        type Value = PresenceFlag;
+        fn enabled_for_staff() -> bool {
+            false
+        }
+    }
+
+    #[derive(Clone, Copy, PartialEq, Eq, Debug, EnumFeatureFlag)]
+    enum Intensity {
+        #[default]
+        Low,
+        High,
+    }
+
+    struct IntensityFlag;
+    impl FeatureFlag for IntensityFlag {
+        const NAME: &'static str = "intensity";
+        type Value = Intensity;
+        fn enabled_for_all() -> bool {
+            true
+        }
+    }
+
+    fn init_settings_store(cx: &mut App) {
+        let store = SettingsStore::test(cx);
+        cx.set_global(store);
+        SettingsStore::update_global(cx, |store, _| {
+            store.register_setting::<FeatureFlagsSettings>();
+        });
+    }
+
+    fn set_override(name: &str, value: &str, cx: &mut App) {
+        SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
+            store.update_user_settings(cx, |content| {
+                content
+                    .feature_flags
+                    .get_or_insert_default()
+                    .insert(name.to_string(), value.to_string());
+            });
+        });
+    }
+
+    #[gpui::test]
+    fn server_flag_enables_presence(cx: &mut App) {
+        init_settings_store(cx);
+        let mut store = FeatureFlagStore::default();
+        assert!(!store.has_flag::<DemoFlag>(cx));
+        store.update_server_flags(false, vec!["demo".to_string()]);
+        assert!(store.has_flag::<DemoFlag>(cx));
+    }
+
+    #[gpui::test]
+    fn off_override_beats_server_flag(cx: &mut App) {
+        init_settings_store(cx);
+        let mut store = FeatureFlagStore::default();
+        store.update_server_flags(false, vec!["demo".to_string()]);
+        set_override(DemoFlag::NAME, "off", cx);
+        assert!(!store.has_flag::<DemoFlag>(cx));
+        assert_eq!(
+            store.try_flag_value::<DemoFlag>(cx),
+            Some(PresenceFlag::Off)
+        );
+    }
+
+    #[gpui::test]
+    fn enabled_for_all_wins_over_override(cx: &mut App) {
+        init_settings_store(cx);
+        let store = FeatureFlagStore::default();
+        set_override(IntensityFlag::NAME, "high", cx);
+        assert_eq!(
+            store.try_flag_value::<IntensityFlag>(cx),
+            Some(Intensity::Low)
+        );
+    }
+
+    #[gpui::test]
+    fn enum_override_selects_specific_variant(cx: &mut App) {
+        init_settings_store(cx);
+        let store = FeatureFlagStore::default();
+        // Staff path would normally resolve to `Low`; the override pushes
+        // us to `High` instead.
+        set_override("enum-demo", "high", cx);
+
+        struct EnumDemo;
+        impl FeatureFlag for EnumDemo {
+            const NAME: &'static str = "enum-demo";
+            type Value = Intensity;
+        }
+
+        assert_eq!(store.try_flag_value::<EnumDemo>(cx), Some(Intensity::High));
+    }
+
+    #[gpui::test]
+    fn unknown_variant_key_resolves_to_none(cx: &mut App) {
+        init_settings_store(cx);
+        let store = FeatureFlagStore::default();
+        set_override("enum-demo", "nonsense", cx);
+
+        struct EnumDemo;
+        impl FeatureFlag for EnumDemo {
+            const NAME: &'static str = "enum-demo";
+            type Value = Intensity;
+        }
+
+        assert_eq!(store.try_flag_value::<EnumDemo>(cx), None);
+    }
+
+    #[gpui::test]
+    fn on_override_enables_without_server_or_staff(cx: &mut App) {
+        init_settings_store(cx);
+        let store = FeatureFlagStore::default();
+        set_override(DemoFlag::NAME, "on", cx);
+        assert!(store.has_flag::<DemoFlag>(cx));
+    }
+
+    /// No rule applies, so the store's `try_flag_value` returns `None`. The
+    /// `FeatureFlagAppExt::flag_value` path (used by most callers) falls
+    /// back to [`Default`], which for `PresenceFlag` is `Off`.
+    #[gpui::test]
+    fn presence_flag_defaults_to_off(cx: &mut App) {
+        init_settings_store(cx);
+        let store = FeatureFlagStore::default();
+        assert_eq!(store.try_flag_value::<DemoFlag>(cx), None);
+        assert_eq!(PresenceFlag::default(), PresenceFlag::Off);
+    }
+}

crates/feature_flags_macros/Cargo.toml πŸ”—

@@ -0,0 +1,18 @@
+[package]
+name = "feature_flags_macros"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lib]
+path = "src/feature_flags_macros.rs"
+proc-macro = true
+
+[lints]
+workspace = true
+
+[dependencies]
+proc-macro2.workspace = true
+quote.workspace = true
+syn.workspace = true

crates/feature_flags_macros/src/feature_flags_macros.rs πŸ”—

@@ -0,0 +1,190 @@
+use proc_macro::TokenStream;
+use proc_macro2::{Span, TokenStream as TokenStream2};
+use quote::quote;
+use syn::{Data, DeriveInput, Fields, Ident, LitStr, parse_macro_input};
+
+/// Derives [`feature_flags::FeatureFlagValue`] for a unit-only enum.
+///
+/// Exactly one variant must be marked with `#[default]`. The default variant
+/// is the one returned when the feature flag is announced by the server,
+/// enabled for all users, or enabled by the staff rule β€” it's the "on"
+/// value, and also the fallback for `from_wire`.
+///
+/// The generated impl derives:
+///
+/// * `all_variants` β€” every variant, in source order.
+/// * `override_key` β€” the variant name, lower-cased with dashes between
+///   PascalCase word boundaries (e.g. `NewWorktree` β†’ `"new-worktree"`).
+/// * `label` β€” the variant name with PascalCase boundaries expanded to
+///   spaces (e.g. `NewWorktree` β†’ `"New Worktree"`).
+/// * `from_wire` β€” always returns the default variant, since today the
+///   server wire format is just presence and does not carry a variant.
+///
+/// ## Example
+///
+/// ```ignore
+/// #[derive(Clone, Copy, PartialEq, Eq, Debug, EnumFeatureFlag)]
+/// enum Intensity {
+///     #[default]
+///     Low,
+///     High,
+/// }
+/// ```
+// `attributes(default)` lets users write `#[default]` on a variant even when
+// they're not also deriving `Default`. If `#[derive(Default)]` is present in
+// the same list, it reuses the same attribute β€” there's no conflict, because
+// helper attributes aren't consumed.
+#[proc_macro_derive(EnumFeatureFlag, attributes(default))]
+pub fn derive_enum_feature_flag(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+    match expand(&input) {
+        Ok(tokens) => tokens.into(),
+        Err(e) => e.to_compile_error().into(),
+    }
+}
+
+fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
+    let Data::Enum(data) = &input.data else {
+        return Err(syn::Error::new_spanned(
+            input,
+            "EnumFeatureFlag can only be derived for enums",
+        ));
+    };
+
+    if data.variants.is_empty() {
+        return Err(syn::Error::new_spanned(
+            input,
+            "EnumFeatureFlag requires at least one variant",
+        ));
+    }
+
+    let mut default_ident: Option<&Ident> = None;
+    let mut variant_idents: Vec<&Ident> = Vec::new();
+
+    for variant in &data.variants {
+        if !matches!(variant.fields, Fields::Unit) {
+            return Err(syn::Error::new_spanned(
+                variant,
+                "EnumFeatureFlag only supports unit variants (no fields)",
+            ));
+        }
+        if has_default_attr(variant) {
+            if default_ident.is_some() {
+                return Err(syn::Error::new_spanned(
+                    variant,
+                    "only one variant may be marked with #[default]",
+                ));
+            }
+            default_ident = Some(&variant.ident);
+        }
+        variant_idents.push(&variant.ident);
+    }
+
+    let Some(default_ident) = default_ident else {
+        return Err(syn::Error::new_spanned(
+            input,
+            "EnumFeatureFlag requires exactly one variant to be marked with #[default]",
+        ));
+    };
+
+    let name = &input.ident;
+    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+    let override_key_arms = variant_idents.iter().map(|variant| {
+        let key = LitStr::new(&to_kebab_case(&variant.to_string()), Span::call_site());
+        quote! { #name::#variant => #key }
+    });
+
+    let label_arms = variant_idents.iter().map(|variant| {
+        let label = LitStr::new(&to_space_separated(&variant.to_string()), Span::call_site());
+        quote! { #name::#variant => #label }
+    });
+
+    let all_variants = variant_idents.iter().map(|v| quote! { #name::#v });
+
+    Ok(quote! {
+        impl #impl_generics ::std::default::Default for #name #ty_generics #where_clause {
+            fn default() -> Self {
+                #name::#default_ident
+            }
+        }
+
+        impl #impl_generics ::feature_flags::FeatureFlagValue for #name #ty_generics #where_clause {
+            fn all_variants() -> &'static [Self] {
+                &[ #( #all_variants ),* ]
+            }
+
+            fn override_key(&self) -> &'static str {
+                match self {
+                    #( #override_key_arms ),*
+                }
+            }
+
+            fn label(&self) -> &'static str {
+                match self {
+                    #( #label_arms ),*
+                }
+            }
+
+            fn from_wire(_: &str) -> ::std::option::Option<Self> {
+                ::std::option::Option::Some(#name::#default_ident)
+            }
+        }
+    })
+}
+
+fn has_default_attr(variant: &syn::Variant) -> bool {
+    variant.attrs.iter().any(|a| a.path().is_ident("default"))
+}
+
+/// Converts a PascalCase identifier to lowercase kebab-case.
+///
+/// `"NewWorktree"` β†’ `"new-worktree"`, `"Low"` β†’ `"low"`,
+/// `"HTTPServer"` β†’ `"httpserver"` (acronyms are not split β€” keep variant
+/// names descriptive to avoid this).
+fn to_kebab_case(ident: &str) -> String {
+    let mut out = String::with_capacity(ident.len() + 4);
+    for (i, ch) in ident.chars().enumerate() {
+        if ch.is_ascii_uppercase() {
+            if i != 0 {
+                out.push('-');
+            }
+            out.push(ch.to_ascii_lowercase());
+        } else {
+            out.push(ch);
+        }
+    }
+    out
+}
+
+/// Converts a PascalCase identifier to space-separated word form for display.
+///
+/// `"NewWorktree"` β†’ `"New Worktree"`, `"Low"` β†’ `"Low"`.
+fn to_space_separated(ident: &str) -> String {
+    let mut out = String::with_capacity(ident.len() + 4);
+    for (i, ch) in ident.chars().enumerate() {
+        if ch.is_ascii_uppercase() && i != 0 {
+            out.push(' ');
+        }
+        out.push(ch);
+    }
+    out
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn kebab_case() {
+        assert_eq!(to_kebab_case("Low"), "low");
+        assert_eq!(to_kebab_case("NewWorktree"), "new-worktree");
+        assert_eq!(to_kebab_case("A"), "a");
+    }
+
+    #[test]
+    fn space_separated() {
+        assert_eq!(to_space_separated("Low"), "Low");
+        assert_eq!(to_space_separated("NewWorktree"), "New Worktree");
+    }
+}

crates/git/src/repository.rs πŸ”—

@@ -295,7 +295,7 @@ impl Worktree {
 
     pub fn directory_name(&self, main_worktree_path: Option<&Path>) -> String {
         if self.is_main {
-            return "main".to_string();
+            return "main worktree".to_string();
         }
 
         let dir_name = self

crates/git_ui/src/clone.rs πŸ”—

@@ -1,7 +1,7 @@
 use gpui::{App, Context, WeakEntity, Window};
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
 use std::sync::Arc;
-use ui::{Color, IconName, SharedString};
+use ui::{Color, Icon, IconName, IconSize, SharedString};
 use util::ResultExt;
 use workspace::{self, Workspace};
 
@@ -48,8 +48,12 @@ pub fn clone_and_open(
                 workspace
                     .update(cx, |workspace, cx| {
                         let toast = StatusToast::new(error.to_string(), cx, |this, _| {
-                            this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
-                                .dismiss_button(true)
+                            this.icon(
+                                Icon::new(IconName::XCircle)
+                                    .size(IconSize::Small)
+                                    .color(Color::Error),
+                            )
+                            .dismiss_button(true)
                         });
                         workspace.toggle_status_toast(toast, cx);
                     })

crates/git_ui/src/git_panel.rs πŸ”—

@@ -50,7 +50,7 @@ use language_model::{
 };
 use menu;
 use multi_buffer::ExcerptBoundaryInfo;
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
 use panel::{PanelHeader, panel_button, panel_filled_button, panel_icon_button};
 use project::{
     Fs, Project, ProjectPath,
@@ -2686,7 +2686,7 @@ impl GitPanel {
         }
 
         let Some(ConfiguredModel { provider, model }) =
-            LanguageModelRegistry::read_global(cx).commit_message_model()
+            LanguageModelRegistry::read_global(cx).commit_message_model(cx)
         else {
             return;
         };
@@ -3864,9 +3864,17 @@ impl GitPanel {
             let status_toast = StatusToast::new(message, cx, move |this, _cx| {
                 use remote_output::SuccessStyle::*;
                 match style {
-                    Toast => this.icon(ToastIcon::new(IconName::GitBranch).color(Color::Muted)),
+                    Toast => this.icon(
+                        Icon::new(IconName::GitBranch)
+                            .size(IconSize::Small)
+                            .color(Color::Muted),
+                    ),
                     ToastWithLog { output } => this
-                        .icon(ToastIcon::new(IconName::GitBranch).color(Color::Muted))
+                        .icon(
+                            Icon::new(IconName::GitBranch)
+                                .size(IconSize::Small)
+                                .color(Color::Muted),
+                        )
                         .action("View Log", move |window, cx| {
                             let output = output.clone();
                             let output =
@@ -3878,7 +3886,11 @@ impl GitPanel {
                                 .ok();
                         }),
                     PushPrLink { text, link } => this
-                        .icon(ToastIcon::new(IconName::GitBranch).color(Color::Muted))
+                        .icon(
+                            Icon::new(IconName::GitBranch)
+                                .size(IconSize::Small)
+                                .color(Color::Muted),
+                        )
                         .action(text, move |_, cx| cx.open_url(&link)),
                 }
                 .dismiss_button(true)
@@ -4023,7 +4035,7 @@ impl GitPanel {
 
         let model_registry = LanguageModelRegistry::read_global(cx);
         let has_commit_model_configuration_error = model_registry
-            .configuration_error(model_registry.commit_message_model(), cx)
+            .configuration_error(model_registry.commit_message_model(cx), cx)
             .is_some();
         let can_commit = self.can_commit();
 
@@ -6479,16 +6491,20 @@ pub(crate) fn show_error_toast(
         workspace.update(cx, |workspace, cx| {
             let workspace_weak = cx.weak_entity();
             let toast = StatusToast::new(format!("git {} failed", action), cx, |this, _cx| {
-                this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
-                    .action("View Log", move |window, cx| {
-                        let message = message.clone();
-                        let action = action.clone();
-                        workspace_weak
-                            .update(cx, move |workspace, cx| {
-                                open_output(action, workspace, &message, window, cx)
-                            })
-                            .ok();
-                    })
+                this.icon(
+                    Icon::new(IconName::XCircle)
+                        .size(IconSize::Small)
+                        .color(Color::Error),
+                )
+                .action("View Log", move |window, cx| {
+                    let message = message.clone();
+                    let action = action.clone();
+                    workspace_weak
+                        .update(cx, move |workspace, cx| {
+                            open_output(action, workspace, &message, window, cx)
+                        })
+                        .ok();
+                })
             });
             workspace.toggle_status_toast(toast, cx)
         });

crates/git_ui/src/worktree_picker.rs πŸ”—

@@ -969,7 +969,7 @@ impl PickerDelegate for WorktreeListDelegate {
                             }
                         })),
                 )
-                .when(!entry.is_new, |this| {
+                .when(!entry.is_new && !is_current, |this| {
                     let focus_handle = self.focus_handle.clone();
                     let open_in_new_window_button =
                         IconButton::new(("open-new-window", ix), IconName::ArrowUpRight)
@@ -1007,6 +1007,13 @@ impl PickerDelegate for WorktreeListDelegate {
         let is_creating = selected_entry.is_some_and(|entry| entry.is_new);
         let can_delete = selected_entry
             .is_some_and(|entry| entry.can_delete(self.forbidden_deletion_path.as_ref()));
+        let is_current = selected_entry.is_some_and(|entry| {
+            !entry.is_new
+                && self
+                    .current_worktree_path
+                    .as_ref()
+                    .is_some_and(|current| *current == entry.worktree.path)
+        });
 
         let footer_container = h_flex()
             .w_full()
@@ -1066,20 +1073,22 @@ impl PickerDelegate for WorktreeListDelegate {
                                 }),
                         )
                     })
-                    .child(
-                        Button::new("open-in-new-window", "Open in New Window")
-                            .key_binding(
-                                KeyBinding::for_action_in(
-                                    &menu::SecondaryConfirm,
-                                    &focus_handle,
-                                    cx,
+                    .when(!is_current, |this| {
+                        this.child(
+                            Button::new("open-in-new-window", "Open in New Window")
+                                .key_binding(
+                                    KeyBinding::for_action_in(
+                                        &menu::SecondaryConfirm,
+                                        &focus_handle,
+                                        cx,
+                                    )
+                                    .map(|kb| kb.size(rems_from_px(12.))),
                                 )
-                                .map(|kb| kb.size(rems_from_px(12.))),
-                            )
-                            .on_click(|_, window, cx| {
-                                window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
-                            }),
-                    )
+                                .on_click(|_, window, cx| {
+                                    window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
+                                }),
+                        )
+                    })
                     .child(
                         Button::new("open-in-window", "Open")
                             .key_binding(

crates/gpui/src/platform.rs πŸ”—

@@ -1981,6 +1981,8 @@ pub enum ImageFormat {
     Tiff,
     /// .ico
     Ico,
+    /// Netpbm image formats (.pbm, .ppm, .pgm).
+    Pnm,
 }
 
 impl ImageFormat {
@@ -1995,6 +1997,7 @@ impl ImageFormat {
             ImageFormat::Bmp => "image/bmp",
             ImageFormat::Tiff => "image/tiff",
             ImageFormat::Ico => "image/ico",
+            ImageFormat::Pnm => "image/x-portable-anymap",
         }
     }
 
@@ -2121,6 +2124,7 @@ impl Image {
                     .render_single_frame(&self.bytes, 1.0)
                     .map_err(Into::into);
             }
+            ImageFormat::Pnm => frames_for_image(&self.bytes, image::ImageFormat::Pnm)?,
         };
 
         Ok(Arc::new(RenderImage::new(frames)))

crates/gpui_linux/src/linux/x11/clipboard.rs πŸ”—

@@ -87,7 +87,7 @@ x11rb::atom_manager! {
         BMP__MIME: ImageFormat::mime_type(ImageFormat::Bmp ).as_bytes(),
         TIFF_MIME: ImageFormat::mime_type(ImageFormat::Tiff).as_bytes(),
         ICO__MIME: ImageFormat::mime_type(ImageFormat::Ico ).as_bytes(),
-
+        PNM__MIME: ImageFormat::mime_type(ImageFormat::Pnm ).as_bytes(),
         // This is just some random name for the property on our window, into which
         // the clipboard owner writes the data we requested.
         ARBOARD_CLIPBOARD,
@@ -1005,6 +1005,7 @@ impl Clipboard {
             ImageFormat::Bmp => self.inner.atoms.BMP__MIME,
             ImageFormat::Tiff => self.inner.atoms.TIFF_MIME,
             ImageFormat::Ico => self.inner.atoms.ICO__MIME,
+            ImageFormat::Pnm => self.inner.atoms.PNM__MIME,
         };
         let data = vec![ClipboardData {
             bytes: image.bytes,

crates/gpui_macos/src/pasteboard.rs πŸ”—

@@ -272,6 +272,7 @@ impl From<ImageFormat> for UTType {
             ImageFormat::Bmp => Self::bmp(),
             ImageFormat::Svg => Self::svg(),
             ImageFormat::Ico => Self::ico(),
+            ImageFormat::Pnm => Self::pnm(),
         }
     }
 }
@@ -320,6 +321,11 @@ impl UTType {
         Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType
     }
 
+    pub fn pnm() -> Self {
+        //https://en.wikipedia.org/w/index.php?title=Netpbm&oldid=1336679433 under Uniform Type Identifier
+        Self(unsafe { ns_string("public.pbm") })
+    }
+
     fn inner(&self) -> *const Object {
         self.0
     }

crates/icons/src/icons.rs πŸ”—

@@ -66,6 +66,7 @@ pub enum IconName {
     ChevronUpDown,
     Circle,
     CircleHelp,
+    Clock,
     Close,
     CloudDownload,
     Code,
@@ -242,7 +243,6 @@ pub enum IconName {
     ThinkingModeOff,
     Thread,
     ThreadFromSummary,
-    ThreadImport,
     ThreadsSidebarLeftClosed,
     ThreadsSidebarLeftOpen,
     ThreadsSidebarRightClosed,

crates/json_schema_store/Cargo.toml πŸ”—

@@ -14,12 +14,17 @@ path = "src/json_schema_store.rs"
 [features]
 default = []
 
+[dev-dependencies]
+gpui = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
+
 [dependencies]
 anyhow.workspace = true
 collections.workspace = true
 dap.workspace = true
 parking_lot.workspace = true
 extension.workspace = true
+feature_flags.workspace = true
 gpui.workspace = true
 language.workspace = true
 paths.workspace = true

crates/json_schema_store/src/json_schema_store.rs πŸ”—

@@ -352,13 +352,16 @@ async fn resolve_dynamic_schema(
                 let icon_theme_names = icon_theme_names.as_slice();
                 let theme_names = theme_names.as_slice();
 
-                settings::SettingsStore::json_schema(&settings::SettingsJsonSchemaParams {
-                    language_names,
-                    font_names,
-                    theme_names,
-                    icon_theme_names,
-                    lsp_adapter_names: &lsp_adapter_names,
-                })
+                let mut schema =
+                    settings::SettingsStore::json_schema(&settings::SettingsJsonSchemaParams {
+                        language_names,
+                        font_names,
+                        theme_names,
+                        icon_theme_names,
+                        lsp_adapter_names: &lsp_adapter_names,
+                    });
+                inject_feature_flags_schema(&mut schema);
+                schema
             })
         }
         "project_settings" => {
@@ -374,16 +377,19 @@ async fn resolve_dynamic_schema(
                 .map(|name| name.to_string())
                 .collect::<Vec<_>>();
 
-            settings::SettingsStore::project_json_schema(&settings::SettingsJsonSchemaParams {
-                language_names,
-                lsp_adapter_names: &lsp_adapter_names,
-                // These are not allowed in project-specific settings but
-                // they're still fields required by the
-                // `SettingsJsonSchemaParams` struct.
-                font_names: &[],
-                theme_names: &[],
-                icon_theme_names: &[],
-            })
+            let mut schema =
+                settings::SettingsStore::project_json_schema(&settings::SettingsJsonSchemaParams {
+                    language_names,
+                    lsp_adapter_names: &lsp_adapter_names,
+                    // These are not allowed in project-specific settings but
+                    // they're still fields required by the
+                    // `SettingsJsonSchemaParams` struct.
+                    font_names: &[],
+                    theme_names: &[],
+                    icon_theme_names: &[],
+                });
+            inject_feature_flags_schema(&mut schema);
+            schema
         }
         "debug_tasks" => {
             let adapter_schemas = cx.read_global::<dap::DapRegistry, _>(|dap_registry, _| {
@@ -513,6 +519,21 @@ pub fn all_schema_file_associations(
     file_associations
 }
 
+/// Swaps the placeholder [`settings::FeatureFlagsMap`] subschema produced by
+/// schemars for an enriched one that lists each known flag's variants. The
+/// placeholder is registered in the `settings_content` crate so the
+/// `settings` crate doesn't need a reverse dependency on `feature_flags`.
+fn inject_feature_flags_schema(schema: &mut serde_json::Value) {
+    use schemars::JsonSchema;
+
+    let Some(defs) = schema.get_mut("$defs").and_then(|d| d.as_object_mut()) else {
+        return;
+    };
+    let schema_name = settings::FeatureFlagsMap::schema_name();
+    let enriched = feature_flags::generate_feature_flags_schema().to_value();
+    defs.insert(schema_name.into_owned(), enriched);
+}
+
 fn generate_jsonc_schema() -> serde_json::Value {
     let generator = schemars::generate::SchemaSettings::draft2019_09()
         .with_transform(DefaultDenyUnknownFields)

crates/keymap_editor/src/keymap_editor.rs πŸ”—

@@ -25,7 +25,7 @@ use gpui::{
 };
 use language::{Language, LanguageConfig, ToOffset as _};
 
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
 use project::{CompletionDisplayOptions, Project};
 use settings::{
     BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets, infer_json_indent_size,
@@ -2883,8 +2883,12 @@ impl KeybindingEditorModal {
                                 format!("Saved edits to the {} action.", humanized_action_name),
                                 cx,
                                 move |this, _cx| {
-                                    this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
-                                        .dismiss_button(true)
+                                    this.icon(
+                                        Icon::new(IconName::Check)
+                                            .size(IconSize::Small)
+                                            .color(Color::Success),
+                                    )
+                                    .dismiss_button(true)
                                     // .action("Undo", f) todo: wire the undo functionality
                                 },
                             );

crates/language_model/src/registry.rs πŸ”—

@@ -6,7 +6,6 @@ use collections::{BTreeMap, HashSet};
 use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
 use std::{str::FromStr, sync::Arc};
 use thiserror::Error;
-use util::maybe;
 
 /// Function type for checking if a built-in provider should be hidden.
 /// Returns Some(extension_id) if the provider should be hidden when that extension is installed.
@@ -46,7 +45,9 @@ impl std::fmt::Debug for ConfigurationError {
 #[derive(Default)]
 pub struct LanguageModelRegistry {
     default_model: Option<ConfiguredModel>,
-    default_fast_model: Option<ConfiguredModel>,
+    /// This model is automatically configured by a user's environment after
+    /// authenticating all providers. It's only used when `default_model` is not set.
+    available_fallback_model: Option<ConfiguredModel>,
     inline_assistant_model: Option<ConfiguredModel>,
     commit_message_model: Option<ConfiguredModel>,
     thread_summary_model: Option<ConfiguredModel>,
@@ -349,22 +350,29 @@ impl LanguageModelRegistry {
     }
 
     pub fn set_default_model(&mut self, model: Option<ConfiguredModel>, cx: &mut Context<Self>) {
-        match (self.default_model.as_ref(), model.as_ref()) {
+        match (self.default_model(), model.as_ref()) {
             (Some(old), Some(new)) if old.is_same_as(new) => {}
             (None, None) => {}
             _ => cx.emit(Event::DefaultModelChanged),
         }
-        self.default_fast_model = maybe!({
-            let provider = &model.as_ref()?.provider;
-            let fast_model = provider.default_fast_model(cx)?;
-            Some(ConfiguredModel {
-                provider: provider.clone(),
-                model: fast_model,
-            })
-        });
         self.default_model = model;
     }
 
+    pub fn set_environment_fallback_model(
+        &mut self,
+        model: Option<ConfiguredModel>,
+        cx: &mut Context<Self>,
+    ) {
+        if self.default_model.is_none() {
+            match (self.available_fallback_model.as_ref(), model.as_ref()) {
+                (Some(old), Some(new)) if old.is_same_as(new) => {}
+                (None, None) => {}
+                _ => cx.emit(Event::DefaultModelChanged),
+            }
+        }
+        self.available_fallback_model = model;
+    }
+
     pub fn set_inline_assistant_model(
         &mut self,
         model: Option<ConfiguredModel>,
@@ -410,7 +418,18 @@ impl LanguageModelRegistry {
             return None;
         }
 
-        self.default_model.clone()
+        self.default_model
+            .clone()
+            .or_else(|| self.available_fallback_model.clone())
+    }
+
+    pub fn default_fast_model(&self, cx: &App) -> Option<ConfiguredModel> {
+        let configured = self.default_model()?;
+        let fast_model = configured.provider.default_fast_model(cx)?;
+        Some(ConfiguredModel {
+            provider: configured.provider,
+            model: fast_model,
+        })
     }
 
     pub fn inline_assistant_model(&self) -> Option<ConfiguredModel> {
@@ -424,7 +443,7 @@ impl LanguageModelRegistry {
             .or_else(|| self.default_model.clone())
     }
 
-    pub fn commit_message_model(&self) -> Option<ConfiguredModel> {
+    pub fn commit_message_model(&self, cx: &App) -> Option<ConfiguredModel> {
         #[cfg(debug_assertions)]
         if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
             return None;
@@ -432,11 +451,11 @@ impl LanguageModelRegistry {
 
         self.commit_message_model
             .clone()
-            .or_else(|| self.default_fast_model.clone())
-            .or_else(|| self.default_model.clone())
+            .or_else(|| self.default_fast_model(cx))
+            .or_else(|| self.default_model())
     }
 
-    pub fn thread_summary_model(&self) -> Option<ConfiguredModel> {
+    pub fn thread_summary_model(&self, cx: &App) -> Option<ConfiguredModel> {
         #[cfg(debug_assertions)]
         if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
             return None;
@@ -444,8 +463,8 @@ impl LanguageModelRegistry {
 
         self.thread_summary_model
             .clone()
-            .or_else(|| self.default_fast_model.clone())
-            .or_else(|| self.default_model.clone())
+            .or_else(|| self.default_fast_model(cx))
+            .or_else(|| self.default_model())
     }
 
     /// The models to use for inline assists. Returns the union of the active
@@ -576,6 +595,35 @@ mod tests {
         assert!(!registry_read.should_hide_provider(&LanguageModelProviderId("unknown".into())));
     }
 
+    #[gpui::test]
+    async fn test_configure_environment_fallback_model(cx: &mut gpui::TestAppContext) {
+        let registry = cx.new(|_| LanguageModelRegistry::default());
+
+        let provider = Arc::new(FakeLanguageModelProvider::default());
+        registry.update(cx, |registry, cx| {
+            registry.register_provider(provider.clone(), cx);
+        });
+
+        cx.update(|cx| provider.authenticate(cx)).await.unwrap();
+
+        registry.update(cx, |registry, cx| {
+            let provider = registry.provider(&provider.id()).unwrap();
+            let model = provider.default_model(cx).unwrap();
+
+            registry.set_environment_fallback_model(
+                Some(ConfiguredModel {
+                    provider: provider.clone(),
+                    model: model.clone(),
+                }),
+                cx,
+            );
+
+            let default_model = registry.default_model().unwrap();
+            assert_eq!(default_model.model.id(), model.id());
+            assert_eq!(default_model.provider.id(), provider.id());
+        });
+    }
+
     #[gpui::test]
     fn test_sync_installed_llm_extensions(cx: &mut App) {
         let registry = cx.new(|_| LanguageModelRegistry::default());

crates/language_models/src/language_models.rs πŸ”—

@@ -5,7 +5,9 @@ use client::{Client, UserStore};
 use collections::HashSet;
 use credentials_provider::CredentialsProvider;
 use gpui::{App, Context, Entity};
-use language_model::{LanguageModelProviderId, LanguageModelRegistry};
+use language_model::{
+    ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
+};
 use provider::deepseek::DeepSeekLanguageModelProvider;
 
 pub mod extension;
@@ -116,6 +118,20 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
             cx,
         );
     });
+
+    cx.subscribe(
+        &registry,
+        |_registry, event: &language_model::Event, cx| match event {
+            language_model::Event::ProviderStateChanged(_)
+            | language_model::Event::AddedProvider(_)
+            | language_model::Event::RemovedProvider(_) => {
+                update_environment_fallback_model(cx);
+            }
+            _ => {}
+        },
+    )
+    .detach();
+
     let registry = registry.downgrade();
     cx.observe_global::<SettingsStore>(move |cx| {
         let Some(registry) = registry.upgrade() else {
@@ -143,6 +159,50 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
     .detach();
 }
 
+/// Recomputes and sets the [`LanguageModelRegistry`]'s environment fallback
+/// model based on currently authenticated providers.
+///
+/// Prefers the Zed cloud provider so that, once the user is signed in, we
+/// always pick a Zed-hosted model over models from other authenticated
+/// providers in the environment. If the Zed cloud provider is authenticated
+/// but hasn't finished loading its models yet, we don't fall back to another
+/// provider to avoid flickering between providers during sign in.
+pub fn update_environment_fallback_model(cx: &mut App) {
+    let registry = LanguageModelRegistry::global(cx);
+    let fallback_model = {
+        let registry = registry.read(cx);
+        let cloud_provider = registry.provider(&ZED_CLOUD_PROVIDER_ID);
+        if cloud_provider
+            .as_ref()
+            .is_some_and(|provider| provider.is_authenticated(cx))
+        {
+            cloud_provider.and_then(|provider| {
+                let model = provider
+                    .default_model(cx)
+                    .or_else(|| provider.recommended_models(cx).first().cloned())?;
+                Some(ConfiguredModel { provider, model })
+            })
+        } else {
+            registry
+                .providers()
+                .iter()
+                .filter(|provider| provider.is_authenticated(cx))
+                .find_map(|provider| {
+                    let model = provider
+                        .default_model(cx)
+                        .or_else(|| provider.recommended_models(cx).first().cloned())?;
+                    Some(ConfiguredModel {
+                        provider: provider.clone(),
+                        model,
+                    })
+                })
+        }
+    };
+    registry.update(cx, |registry, cx| {
+        registry.set_environment_fallback_model(fallback_model, cx);
+    });
+}
+
 fn register_openai_compatible_providers(
     registry: &mut LanguageModelRegistry,
     old: &HashSet<Arc<str>>,

crates/notifications/src/status_toast.rs πŸ”—

@@ -5,41 +5,13 @@ use ui::{Tooltip, prelude::*};
 use workspace::{ToastAction, ToastView};
 use zed_actions::toast;
 
-#[derive(Clone, Copy)]
-pub struct ToastIcon {
-    icon: IconName,
-    color: Color,
-}
-
-impl ToastIcon {
-    pub fn new(icon: IconName) -> Self {
-        Self {
-            icon,
-            color: Color::default(),
-        }
-    }
-
-    pub fn color(mut self, color: Color) -> Self {
-        self.color = color;
-        self
-    }
-}
-
-impl From<IconName> for ToastIcon {
-    fn from(icon: IconName) -> Self {
-        Self {
-            icon,
-            color: Color::default(),
-        }
-    }
-}
-
 #[derive(RegisterComponent)]
 pub struct StatusToast {
-    icon: Option<ToastIcon>,
+    icon: Option<Icon>,
     text: SharedString,
     action: Option<ToastAction>,
     show_dismiss: bool,
+    auto_dismiss: bool,
     this_handle: Entity<Self>,
     focus_handle: FocusHandle,
 }
@@ -59,6 +31,7 @@ impl StatusToast {
                     icon: None,
                     action: None,
                     show_dismiss: false,
+                    auto_dismiss: true,
                     this_handle: cx.entity(),
                     focus_handle,
                 },
@@ -67,11 +40,16 @@ impl StatusToast {
         })
     }
 
-    pub fn icon(mut self, icon: ToastIcon) -> Self {
+    pub fn icon(mut self, icon: Icon) -> Self {
         self.icon = Some(icon);
         self
     }
 
+    pub fn auto_dismiss(mut self, auto_dismiss: bool) -> Self {
+        self.auto_dismiss = auto_dismiss;
+        self
+    }
+
     pub fn action(
         mut self,
         label: impl Into<SharedString>,
@@ -116,9 +94,7 @@ impl Render for StatusToast {
             .flex_none()
             .bg(cx.theme().colors().surface_background)
             .shadow_lg()
-            .when_some(self.icon.as_ref(), |this, icon| {
-                this.child(Icon::new(icon.icon).color(icon.color))
-            })
+            .when_some(self.icon.clone(), |this, icon| this.child(icon))
             .child(Label::new(self.text.clone()).color(Color::Default))
             .when_some(self.action.as_ref(), |this, action| {
                 this.child(
@@ -155,6 +131,10 @@ impl ToastView for StatusToast {
     fn action(&self) -> Option<ToastAction> {
         self.action.clone()
     }
+
+    fn auto_dismiss(&self) -> bool {
+        self.auto_dismiss
+    }
 }
 
 impl Focusable for StatusToast {
@@ -183,33 +163,55 @@ impl Component for StatusToast {
         let icon_example = StatusToast::new(
             "Nathan Sobo accepted your contact request",
             cx,
-            |this, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Muted)),
+            |this, _| {
+                this.icon(
+                    Icon::new(IconName::Check)
+                        .size(IconSize::Small)
+                        .color(Color::Muted),
+                )
+            },
         );
 
         let success_example = StatusToast::new("Pushed 4 changes to `zed/main`", cx, |this, _| {
-            this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
+            this.icon(
+                Icon::new(IconName::Check)
+                    .size(IconSize::Small)
+                    .color(Color::Success),
+            )
         });
 
         let error_example = StatusToast::new(
             "git push: Couldn't find remote origin `iamnbutler/zed`",
             cx,
             |this, _cx| {
-                this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
-                    .action("More Info", |_, _| {})
+                this.icon(
+                    Icon::new(IconName::XCircle)
+                        .size(IconSize::Small)
+                        .color(Color::Error),
+                )
+                .action("More Info", |_, _| {})
             },
         );
 
         let warning_example = StatusToast::new("You have outdated settings", cx, |this, _cx| {
-            this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
-                .action("More Info", |_, _| {})
+            this.icon(
+                Icon::new(IconName::Warning)
+                    .size(IconSize::Small)
+                    .color(Color::Warning),
+            )
+            .action("More Info", |_, _| {})
         });
 
         let pr_example =
             StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
-                this.icon(ToastIcon::new(IconName::GitBranch).color(Color::Muted))
-                    .action("Open Pull Request", |_, cx| {
-                        cx.open_url("https://github.com/")
-                    })
+                this.icon(
+                    Icon::new(IconName::GitBranch)
+                        .size(IconSize::Small)
+                        .color(Color::Muted),
+                )
+                .action("Open Pull Request", |_, cx| {
+                    cx.open_url("https://github.com/")
+                })
             });
 
         Some(

crates/onboarding/src/onboarding.rs πŸ”—

@@ -7,7 +7,7 @@ use gpui::{
     FocusHandle, Focusable, Global, IntoElement, KeyContext, Render, ScrollHandle, SharedString,
     Subscription, Task, WeakEntity, Window, actions,
 };
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
 use schemars::JsonSchema;
 use serde::Deserialize;
 use settings::{SettingsStore, VsCodeSettingsSource};
@@ -495,8 +495,12 @@ pub async fn handle_import_vscode_settings(
                     format!("Your {} settings were successfully imported.", source),
                     cx,
                     |this, _| {
-                        this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
-                            .dismiss_button(true)
+                        this.icon(
+                            Icon::new(IconName::Check)
+                                .size(IconSize::Small)
+                                .color(Color::Success),
+                        )
+                        .dismiss_button(true)
                     },
                 );
                 SettingsImportState::update(cx, |state, _| match source {
@@ -514,11 +518,15 @@ pub async fn handle_import_vscode_settings(
                     "Failed to import settings. See log for details",
                     cx,
                     |this, _| {
-                        this.icon(ToastIcon::new(IconName::Close).color(Color::Error))
-                            .action("Open Log", |window, cx| {
-                                window.dispatch_action(workspace::OpenLog.boxed_clone(), cx)
-                            })
-                            .dismiss_button(true)
+                        this.icon(
+                            Icon::new(IconName::Close)
+                                .size(IconSize::Small)
+                                .color(Color::Error),
+                        )
+                        .action("Open Log", |window, cx| {
+                            window.dispatch_action(workspace::OpenLog.boxed_clone(), cx)
+                        })
+                        .dismiss_button(true)
                     },
                 );
                 workspace.toggle_status_toast(error_toast, cx);

crates/project/src/image_store.rs πŸ”—

@@ -902,6 +902,7 @@ fn create_gpui_image(content: Vec<u8>) -> anyhow::Result<Arc<gpui::Image>> {
             image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
             image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
             image::ImageFormat::Ico => gpui::ImageFormat::Ico,
+            image::ImageFormat::Pnm => gpui::ImageFormat::Pnm,
             format => anyhow::bail!("Image format {format:?} not supported"),
         },
         content,

crates/project/src/worktree_store.rs πŸ”—

@@ -1383,12 +1383,6 @@ impl WorktreeStore {
     pub fn paths(&self, cx: &App) -> WorktreePaths {
         let (mains, folders): (Vec<PathBuf>, Vec<PathBuf>) = self
             .visible_worktrees(cx)
-            .filter(|worktree| {
-                let worktree = worktree.read(cx);
-                // Remote worktrees that haven't received their first update
-                // don't have enough data to contribute yet.
-                !worktree.is_remote() || worktree.root_entry().is_some()
-            })
             .map(|worktree| {
                 let snapshot = worktree.read(cx).snapshot();
                 let folder_path = snapshot.abs_path().to_path_buf();

crates/project_panel/src/project_panel.rs πŸ”—

@@ -31,7 +31,7 @@ use gpui::{
 };
 use language::DiagnosticSeverity;
 use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
-use notifications::status_toast::{StatusToast, ToastIcon};
+use notifications::status_toast::StatusToast;
 use project::{
     Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId,
     ProjectPath, Worktree, WorktreeId,
@@ -2275,8 +2275,12 @@ impl ProjectPanel {
                         .update(cx, |panel, cx| {
                             let message = format!("Failed to restore {}: {}", file_name, e);
                             let toast = StatusToast::new(message, cx, |this, _| {
-                                this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
-                                    .dismiss_button(true)
+                                this.icon(
+                                    Icon::new(IconName::XCircle)
+                                        .size(IconSize::Small)
+                                        .color(Color::Error),
+                                )
+                                .dismiss_button(true)
                             });
                             panel
                                 .workspace

crates/repl/src/notebook/notebook_ui.rs πŸ”—

@@ -68,8 +68,8 @@ pub fn init(cx: &mut App) {
     }
 
     cx.observe_flag::<NotebookFeatureFlag, _>({
-        move |is_enabled, cx| {
-            if is_enabled {
+        move |flag, cx| {
+            if *flag {
                 workspace::register_project_item::<NotebookEditor>(cx);
             } else {
                 // todo: there is no way to unregister a project item, so if the feature flag

crates/settings_content/src/agent.rs πŸ”—

@@ -128,6 +128,12 @@ pub struct AgentSettingsContent {
     /// Default: 320
     #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub default_height: Option<f32>,
+    /// Whether to limit the content width in the agent panel. When enabled,
+    /// content will be constrained to `max_content_width` and centered when
+    /// the panel is wider than that value, for optimal readability.
+    ///
+    /// Default: true
+    pub limit_content_width: Option<bool>,
     /// Maximum content width in pixels for the agent panel. Content will be
     /// centered when the panel is wider than this value.
     ///
@@ -269,13 +275,34 @@ impl AgentSettingsContent {
     }
 
     pub fn add_favorite_model(&mut self, model: LanguageModelSelection) {
-        if !self.favorite_models.contains(&model) {
+        // Note: this is intentional to not compare using `PartialEq`here.
+        // Full equality would treat entries that differ just in thinking/effort/speed
+        // as distinct and silently produce duplicates.
+        if !self
+            .favorite_models
+            .iter()
+            .any(|m| m.provider == model.provider && m.model == model.model)
+        {
             self.favorite_models.push(model);
         }
     }
 
     pub fn remove_favorite_model(&mut self, model: &LanguageModelSelection) {
-        self.favorite_models.retain(|m| m != model);
+        self.favorite_models
+            .retain(|m| !(m.provider == model.provider && m.model == model.model));
+    }
+
+    pub fn update_favorite_model<F>(&mut self, provider: &str, model: &str, f: F)
+    where
+        F: FnOnce(&mut LanguageModelSelection),
+    {
+        if let Some(entry) = self
+            .favorite_models
+            .iter_mut()
+            .find(|m| m.provider.0 == provider && m.model == model)
+        {
+            f(entry);
+        }
     }
 
     pub fn set_tool_default_permission(&mut self, tool_id: &str, mode: ToolPermissionMode) {

crates/settings_content/src/settings_content.rs πŸ”—

@@ -211,6 +211,45 @@ pub struct SettingsContent {
     ///
     /// Default: 5
     pub modeline_lines: Option<usize>,
+
+    /// Local overrides for feature flags, keyed by flag name.
+    pub feature_flags: Option<FeatureFlagsMap>,
+}
+
+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, MergeFrom)]
+#[serde(transparent)]
+pub struct FeatureFlagsMap(pub HashMap<String, String>);
+
+// A manual `JsonSchema` impl keeps this type's schema registered under a
+// unique name. The derived impl on a `#[serde(transparent)]` newtype around
+// `HashMap<String, String>` would inline to the map's own schema name (`Map_of_string`),
+// which is shared with every other `HashMap<String, String>` setting field in
+// `SettingsContent`. A named placeholder lets `json_schema_store` find and
+// replace just this field's schema at runtime without clobbering the others.
+impl JsonSchema for FeatureFlagsMap {
+    fn schema_name() -> std::borrow::Cow<'static, str> {
+        "FeatureFlagsMap".into()
+    }
+
+    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+        schemars::json_schema!({
+            "type": "object",
+            "additionalProperties": { "type": "string" }
+        })
+    }
+}
+
+impl std::ops::Deref for FeatureFlagsMap {
+    type Target = HashMap<String, String>;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl std::ops::DerefMut for FeatureFlagsMap {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
 }
 
 impl SettingsContent {

crates/settings_ui/src/page_data.rs πŸ”—

@@ -62,7 +62,7 @@ macro_rules! concat_sections {
 }
 
 pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
-    vec![
+    let mut pages = vec![
         general_page(cx),
         appearance_page(),
         keymap_page(),
@@ -77,7 +77,32 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
         collaboration_page(),
         ai_page(cx),
         network_page(),
-    ]
+    ];
+
+    use feature_flags::FeatureFlagAppExt as _;
+    if cx.is_staff() || cfg!(debug_assertions) {
+        pages.push(developer_page());
+    }
+
+    pages
+}
+
+fn developer_page() -> SettingsPage {
+    SettingsPage {
+        title: "Developer",
+        items: Box::new([
+            SettingsPageItem::SectionHeader("Feature Flags"),
+            SettingsPageItem::SubPageLink(SubPageLink {
+                title: "Feature Flags".into(),
+                r#type: Default::default(),
+                description: None,
+                json_path: Some("feature_flags"),
+                in_json: true,
+                files: USER,
+                render: crate::pages::render_feature_flags_page,
+            }),
+        ]),
+    }
 }
 
 fn general_page(cx: &App) -> SettingsPage {
@@ -5811,23 +5836,58 @@ fn panels_page() -> SettingsPage {
                 metadata: None,
                 files: USER,
             }),
-            SettingsPageItem::SettingItem(SettingItem {
-                title: "Agent Panel Max Content Width",
-                description: "Maximum content width in pixels. Content will be centered when the panel is wider than this value.",
-                field: Box::new(SettingField {
-                    json_path: Some("agent.max_content_width"),
-                    pick: |settings_content| {
-                        settings_content.agent.as_ref()?.max_content_width.as_ref()
-                    },
-                    write: |settings_content, value| {
-                        settings_content
-                            .agent
-                            .get_or_insert_default()
-                            .max_content_width = value;
-                    },
-                }),
-                metadata: None,
-                files: USER,
+            SettingsPageItem::DynamicItem(DynamicItem {
+                discriminant: SettingItem {
+                    files: USER,
+                    title: "Limit Content Width",
+                    description: "Whether to constrain the agent panel content to a maximum width, centering it when the panel is wider, for optimal readability.",
+                    field: Box::new(SettingField::<bool> {
+                        json_path: Some("agent.limit_content_width"),
+                        pick: |settings_content| {
+                            settings_content
+                                .agent
+                                .as_ref()?
+                                .limit_content_width
+                                .as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content
+                                .agent
+                                .get_or_insert_default()
+                                .limit_content_width = value;
+                        },
+                    }),
+                    metadata: None,
+                },
+                pick_discriminant: |settings_content| {
+                    let enabled = settings_content
+                        .agent
+                        .as_ref()?
+                        .limit_content_width
+                        .unwrap_or(true);
+                    Some(if enabled { 1 } else { 0 })
+                },
+                fields: vec![
+                    vec![],
+                    vec![SettingItem {
+                        files: USER,
+                        title: "Max Content Width",
+                        description: "Maximum content width in pixels. Content will be centered when the panel is wider than this value.",
+                        field: Box::new(SettingField {
+                            json_path: Some("agent.max_content_width"),
+                            pick: |settings_content| {
+                                settings_content.agent.as_ref()?.max_content_width.as_ref()
+                            },
+                            write: |settings_content, value| {
+                                settings_content
+                                    .agent
+                                    .get_or_insert_default()
+                                    .max_content_width = value;
+                            },
+                        }),
+                        metadata: None,
+                    }],
+                ],
             }),
         ]
     }

crates/settings_ui/src/pages.rs πŸ”—

@@ -1,6 +1,7 @@
 mod audio_input_output_setup;
 mod audio_test_window;
 mod edit_prediction_provider_setup;
+mod feature_flags;
 mod tool_permissions_setup;
 
 pub(crate) use audio_input_output_setup::{
@@ -8,6 +9,7 @@ pub(crate) use audio_input_output_setup::{
 };
 pub(crate) use audio_test_window::open_audio_test_window;
 pub(crate) use edit_prediction_provider_setup::render_edit_prediction_setup_page;
+pub(crate) use feature_flags::render_feature_flags_page;
 pub(crate) use tool_permissions_setup::render_tool_permissions_setup_page;
 
 pub use tool_permissions_setup::{

crates/settings_ui/src/pages/feature_flags.rs πŸ”—

@@ -0,0 +1,132 @@
+use feature_flags::{FeatureFlagDescriptor, FeatureFlagStore, FeatureFlagVariant};
+use fs::Fs;
+use gpui::{ScrollHandle, prelude::*};
+use ui::{Checkbox, ToggleState, prelude::*};
+
+use crate::SettingsWindow;
+
+pub(crate) fn render_feature_flags_page(
+    _settings_window: &SettingsWindow,
+    scroll_handle: &ScrollHandle,
+    _window: &mut Window,
+    cx: &mut Context<SettingsWindow>,
+) -> AnyElement {
+    // Sort by flag name so the list is stable between renders even though
+    // `inventory::iter` order depends on link order.
+    let mut descriptors: Vec<&'static FeatureFlagDescriptor> =
+        FeatureFlagStore::known_flags().collect();
+    descriptors.sort_by_key(|descriptor| descriptor.name);
+
+    v_flex()
+        .id("feature-flags-page")
+        .min_w_0()
+        .size_full()
+        .pt_2p5()
+        .px_8()
+        .pb_16()
+        .gap_4()
+        .overflow_y_scroll()
+        .track_scroll(scroll_handle)
+        .children(
+            descriptors
+                .into_iter()
+                .map(|descriptor| render_flag_row(descriptor, cx)),
+        )
+        .into_any_element()
+}
+
+fn render_flag_row(
+    descriptor: &'static FeatureFlagDescriptor,
+    cx: &mut Context<SettingsWindow>,
+) -> AnyElement {
+    let forced_on = FeatureFlagStore::is_forced_on(descriptor);
+    let resolved = cx.global::<FeatureFlagStore>().resolved_key(descriptor, cx);
+    let has_override = FeatureFlagStore::override_for(descriptor.name, cx).is_some();
+
+    let header =
+        h_flex()
+            .justify_between()
+            .items_center()
+            .child(
+                h_flex()
+                    .gap_2()
+                    .child(Label::new(descriptor.name).size(LabelSize::Default).color(
+                        if forced_on {
+                            Color::Muted
+                        } else {
+                            Color::Default
+                        },
+                    ))
+                    .when(forced_on, |this| {
+                        this.child(
+                            Label::new("enabled for all")
+                                .size(LabelSize::Small)
+                                .color(Color::Muted),
+                        )
+                    }),
+            )
+            .when(has_override && !forced_on, |this| {
+                let name = descriptor.name;
+                this.child(
+                    Button::new(SharedString::from(format!("reset-{}", name)), "Reset")
+                        .label_size(LabelSize::Small)
+                        .on_click(cx.listener(move |_, _, _, cx| {
+                            FeatureFlagStore::clear_override(name, <dyn Fs>::global(cx), cx);
+                        })),
+                )
+            });
+
+    v_flex()
+        .id(SharedString::from(format!("flag-row-{}", descriptor.name)))
+        .gap_1()
+        .child(header)
+        .child(render_flag_variants(descriptor, resolved, forced_on, cx))
+        .into_any_element()
+}
+
+fn render_flag_variants(
+    descriptor: &'static FeatureFlagDescriptor,
+    resolved: &'static str,
+    forced_on: bool,
+    cx: &mut Context<SettingsWindow>,
+) -> impl IntoElement {
+    let variants: Vec<FeatureFlagVariant> = (descriptor.variants)();
+
+    let row_items = variants.into_iter().map({
+        let name = descriptor.name;
+        move |variant| {
+            let key = variant.override_key;
+            let label = variant.label;
+            let selected = resolved == key;
+            let state = if selected {
+                ToggleState::Selected
+            } else {
+                ToggleState::Unselected
+            };
+            let checkbox_id = SharedString::from(format!("{}-{}", name, key));
+            let disabled = forced_on;
+            let mut checkbox = Checkbox::new(ElementId::from(checkbox_id), state)
+                .label(label)
+                .disabled(disabled);
+            if !disabled {
+                checkbox =
+                    checkbox.on_click(cx.listener(move |_, new_state: &ToggleState, _, cx| {
+                        // Clicking an already-selected option is a no-op rather than a
+                        // "deselect" β€” there's no valid "nothing selected" state.
+                        if *new_state == ToggleState::Unselected {
+                            return;
+                        }
+                        FeatureFlagStore::set_override(
+                            name,
+                            key.to_string(),
+                            <dyn Fs>::global(cx),
+                            cx,
+                        );
+                    }));
+            }
+            checkbox.into_any_element()
+        }
+    });
+
+    h_flex().gap_4().flex_wrap().children(row_items)
+}

crates/settings_ui/src/settings_ui.rs πŸ”—

@@ -1521,6 +1521,17 @@ impl SettingsWindow {
         })
         .detach();
 
+        use feature_flags::FeatureFlagAppExt as _;
+        let mut last_is_staff = cx.is_staff();
+        cx.observe_global_in::<feature_flags::FeatureFlagStore>(window, move |this, window, cx| {
+            let is_staff = cx.is_staff();
+            if is_staff != last_is_staff {
+                last_is_staff = is_staff;
+                this.rebuild_pages(window, cx);
+            }
+        })
+        .detach();
+
         cx.on_window_closed(|cx, _window_id| {
             if let Some(existing_window) = cx
                 .windows()
@@ -2143,6 +2154,15 @@ impl SettingsWindow {
         cx.notify();
     }
 
+    fn rebuild_pages(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
+        self.pages.clear();
+        self.navbar_entries.clear();
+        self.navbar_focus_subscriptions.clear();
+        self.content_handles.clear();
+        self.build_ui(window, cx);
+        self.build_search_index();
+    }
+
     #[track_caller]
     fn fetch_files(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
         self.worktree_root_dirs.clear();

crates/sidebar/Cargo.toml πŸ”—

@@ -24,6 +24,7 @@ agent_ui = { workspace = true, features = ["audio"] }
 anyhow.workspace = true
 chrono.workspace = true
 editor.workspace = true
+feature_flags.workspace = true
 fs.workspace = true
 git.workspace = true
 gpui.workspace = true

crates/sidebar/src/sidebar.rs πŸ”—

@@ -12,12 +12,15 @@ use agent_ui::threads_archive_view::{
     ThreadsArchiveView, ThreadsArchiveViewEvent, format_history_entry_timestamp,
 };
 use agent_ui::{
-    AcpThreadImportOnboarding, Agent, AgentPanel, AgentPanelEvent, CrossChannelImportOnboarding,
-    DEFAULT_THREAD_TITLE, NewThread, RemoveSelectedThread, ThreadId, ThreadImportModal,
+    AcpThreadImportOnboarding, Agent, AgentPanel, AgentPanelEvent, ArchiveSelectedThread,
+    CrossChannelImportOnboarding, DEFAULT_THREAD_TITLE, NewThread, ThreadId, ThreadImportModal,
     channels_with_threads, import_threads_from_other_channels,
 };
 use chrono::{DateTime, Utc};
 use editor::Editor;
+use feature_flags::{
+    AgentThreadWorktreeLabel, AgentThreadWorktreeLabelFlag, FeatureFlag, FeatureFlagAppExt as _,
+};
 use gpui::{
     Action as _, AnyElement, App, ClickEvent, Context, DismissEvent, Entity, EntityId, FocusHandle,
     Focusable, KeyContext, ListState, Modifiers, Pixels, Render, SharedString, Task, WeakEntity,
@@ -50,8 +53,8 @@ use util::path_list::PathList;
 use workspace::{
     CloseWindow, FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, NextProject,
     NextThread, Open, OpenMode, PreviousProject, PreviousThread, ProjectGroupKey, SaveIntent,
-    ShowFewerThreads, ShowMoreThreads, Sidebar as WorkspaceSidebar, SidebarSide, Toast,
-    ToggleWorkspaceSidebar, Workspace, notifications::NotificationId, sidebar_side_context_menu,
+    Sidebar as WorkspaceSidebar, SidebarSide, Toast, ToggleWorkspaceSidebar, Workspace,
+    notifications::NotificationId, sidebar_side_context_menu,
 };
 
 use zed_actions::OpenRecent;
@@ -69,8 +72,8 @@ gpui::actions!(
     [
         /// Creates a new thread in the currently selected or active project group.
         NewThreadInGroup,
-        /// Toggles between the thread list and the archive view.
-        ViewAllThreads,
+        /// Toggles between the thread list and the thread history.
+        ToggleThreadHistory,
     ]
 );
 
@@ -85,13 +88,13 @@ gpui::actions!(
 const DEFAULT_WIDTH: Pixels = px(300.0);
 const MIN_WIDTH: Pixels = px(200.0);
 const MAX_WIDTH: Pixels = px(800.0);
-const DEFAULT_THREADS_SHOWN: usize = 5;
 
 #[derive(Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
 enum SerializedSidebarView {
     #[default]
     ThreadList,
-    Archive,
+    #[serde(alias = "Archive")]
+    History,
 }
 
 #[derive(Default, Serialize, Deserialize)]
@@ -229,10 +232,6 @@ enum ListEntry {
         has_threads: bool,
     },
     Thread(ThreadEntry),
-    ViewMore {
-        key: ProjectGroupKey,
-        is_fully_expanded: bool,
-    },
 }
 
 #[cfg(test)]
@@ -257,7 +256,6 @@ impl ListEntry {
             ListEntry::ProjectHeader { key, .. } => multi_workspace
                 .workspaces_for_project_group(key, cx)
                 .unwrap_or_default(),
-            ListEntry::ViewMore { .. } => Vec::new(),
         }
     }
 }
@@ -330,6 +328,96 @@ fn workspace_path_list(workspace: &Entity<Workspace>, cx: &App) -> PathList {
     PathList::new(&workspace.read(cx).root_paths(cx))
 }
 
+#[derive(Clone)]
+struct WorkspaceMenuWorktreeLabel {
+    icon: Option<IconName>,
+    primary_name: SharedString,
+    secondary_name: Option<SharedString>,
+}
+
+fn workspace_menu_worktree_labels(
+    workspace: &Entity<Workspace>,
+    cx: &App,
+) -> Vec<WorkspaceMenuWorktreeLabel> {
+    let root_paths = workspace.read(cx).root_paths(cx);
+    let show_folder_name = root_paths.len() > 1;
+    let project = workspace.read(cx).project().clone();
+    let repository_snapshots: Vec<_> = project
+        .read(cx)
+        .repositories(cx)
+        .values()
+        .map(|repo| repo.read(cx).snapshot())
+        .collect();
+
+    root_paths
+        .into_iter()
+        .map(|root_path| {
+            let root_path = root_path.as_ref();
+            let folder_name = root_path
+                .file_name()
+                .map(|name| SharedString::from(name.to_string_lossy().to_string()))
+                .unwrap_or_default();
+            let repository_snapshot = repository_snapshots
+                .iter()
+                .find(|snapshot| snapshot.work_directory_abs_path.as_ref() == root_path);
+
+            if let Some(snapshot) = repository_snapshot
+                && snapshot.is_linked_worktree()
+            {
+                let worktree_name = project::linked_worktree_short_name(
+                    snapshot.original_repo_abs_path.as_ref(),
+                    root_path,
+                )
+                .unwrap_or_else(|| folder_name.clone());
+
+                if show_folder_name {
+                    WorkspaceMenuWorktreeLabel {
+                        icon: Some(IconName::GitWorktree),
+                        primary_name: folder_name,
+                        secondary_name: Some(worktree_name),
+                    }
+                } else {
+                    WorkspaceMenuWorktreeLabel {
+                        icon: Some(IconName::GitWorktree),
+                        primary_name: worktree_name,
+                        secondary_name: None,
+                    }
+                }
+            } else {
+                WorkspaceMenuWorktreeLabel {
+                    icon: None,
+                    primary_name: folder_name,
+                    secondary_name: None,
+                }
+            }
+        })
+        .collect()
+}
+
+fn apply_worktree_label_mode(
+    mut worktrees: Vec<ThreadItemWorktreeInfo>,
+    mode: AgentThreadWorktreeLabel,
+) -> Vec<ThreadItemWorktreeInfo> {
+    match mode {
+        AgentThreadWorktreeLabel::Both => {}
+        AgentThreadWorktreeLabel::Worktree => {
+            for wt in &mut worktrees {
+                wt.branch_name = None;
+            }
+        }
+        AgentThreadWorktreeLabel::Branch => {
+            for wt in &mut worktrees {
+                // Fall back to showing the worktree name when no branch is
+                // known; an empty chip would be worse than a mismatched icon.
+                if wt.branch_name.is_some() {
+                    wt.worktree_name = None;
+                }
+            }
+        }
+    }
+    worktrees
+}
+
 /// Shows a [`RemoteConnectionModal`] on the given workspace and establishes
 /// an SSH connection. Suitable for passing to
 /// [`MultiWorkspace::find_or_create_workspace`] as the `connect_remote`
@@ -372,6 +460,7 @@ pub struct Sidebar {
     view: SidebarView,
     restoring_tasks: HashMap<agent_ui::ThreadId, Task<()>>,
     recent_projects_popover_handle: PopoverMenuHandle<SidebarRecentProjects>,
+    project_header_menu_handles: HashMap<usize, PopoverMenuHandle<ContextMenu>>,
     project_header_menu_ix: Option<usize>,
     _subscriptions: Vec<gpui::Subscription>,
     /// For the thread import banners, if there is just one we show "Import
@@ -392,6 +481,8 @@ impl Sidebar {
         cx.on_focus_in(&focus_handle, window, Self::focus_in)
             .detach();
 
+        AgentThreadWorktreeLabelFlag::watch(cx);
+
         let filter_editor = cx.new(|cx| {
             let mut editor = Editor::single_line(window, cx);
             editor.set_use_modal_editing(true);
@@ -465,6 +556,7 @@ impl Sidebar {
             view: SidebarView::default(),
             restoring_tasks: HashMap::new(),
             recent_projects_popover_handle: PopoverMenuHandle::default(),
+            project_header_menu_handles: HashMap::new(),
             project_header_menu_ix: None,
             _subscriptions: Vec::new(),
             import_banners_use_verbose_labels: None,
@@ -486,17 +578,6 @@ impl Sidebar {
             .unwrap_or(false)
     }
 
-    fn group_extra_batches(&self, key: &ProjectGroupKey, cx: &App) -> usize {
-        self.multi_workspace
-            .upgrade()
-            .and_then(|mw| {
-                mw.read(cx)
-                    .group_state_by_key(key)
-                    .and_then(|state| state.visible_thread_count)
-            })
-            .unwrap_or(0)
-    }
-
     fn set_group_expanded(&self, key: &ProjectGroupKey, expanded: bool, cx: &mut Context<Self>) {
         if let Some(mw) = self.multi_workspace.upgrade() {
             mw.update(cx, |mw, cx| {
@@ -508,22 +589,6 @@ impl Sidebar {
         }
     }
 
-    fn set_group_visible_thread_count(
-        &self,
-        key: &ProjectGroupKey,
-        count: Option<usize>,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(mw) = self.multi_workspace.upgrade() {
-            mw.update(cx, |mw, cx| {
-                if let Some(state) = mw.group_state_by_key_mut(key) {
-                    state.visible_thread_count = count;
-                }
-                mw.serialize(cx);
-            });
-        }
-    }
-
     fn is_active_workspace(&self, workspace: &Entity<Workspace>, cx: &App) -> bool {
         self.multi_workspace
             .upgrade()
@@ -671,8 +736,8 @@ impl Sidebar {
                     this.sync_active_entry_from_panel(_agent_panel, cx);
                     this.update_entries(cx);
                 }
-                AgentPanelEvent::MessageSentOrQueued { thread_id } => {
-                    this.record_thread_message_sent_or_queued(thread_id, cx);
+                AgentPanelEvent::ThreadInteracted { thread_id } => {
+                    this.record_thread_interacted(thread_id, cx);
                     this.update_entries(cx);
                 }
             },
@@ -1210,7 +1275,10 @@ impl Sidebar {
                     }
                     let mut worktree_matched = false;
                     for worktree in &mut thread.worktrees {
-                        if let Some(positions) = fuzzy_match_positions(&query, &worktree.name) {
+                        let Some(name) = worktree.worktree_name.as_ref() else {
+                            continue;
+                        };
+                        if let Some(positions) = fuzzy_match_positions(&query, name) {
                             worktree.highlight_positions = positions;
                             worktree_matched = true;
                         }
@@ -1261,55 +1329,13 @@ impl Sidebar {
                     continue;
                 }
 
-                let total = threads.len();
-
-                let extra_batches = self.group_extra_batches(&group_key, cx);
-                let threads_to_show =
-                    DEFAULT_THREADS_SHOWN + (extra_batches * DEFAULT_THREADS_SHOWN);
-                let count = threads_to_show.min(total);
-
-                let mut promoted_threads: HashSet<agent_ui::ThreadId> = HashSet::new();
-
-                // Build visible entries in a single pass. Threads within
-                // the cutoff are always shown. Threads beyond it are shown
-                // only if they should be promoted (running, waiting, or
-                // focused)
-                for (index, thread) in threads.into_iter().enumerate() {
-                    let is_hidden = index >= count;
-
-                    if is_hidden {
-                        let is_notified = notified_threads.contains(&thread.metadata.thread_id);
-                        let is_promoted = thread.status == AgentThreadStatus::Running
-                            || thread.status == AgentThreadStatus::WaitingForConfirmation
-                            || is_notified
-                            || self.active_entry.as_ref().is_some_and(|active| {
-                                active.matches_entry(&ListEntry::Thread(thread.clone()))
-                            });
-                        if is_promoted {
-                            promoted_threads.insert(thread.metadata.thread_id);
-                        }
-                        let is_in_promoted = promoted_threads.contains(&thread.metadata.thread_id);
-                        if !is_in_promoted {
-                            continue;
-                        }
-                    }
-
+                for thread in threads {
                     if let Some(sid) = &thread.metadata.session_id {
                         current_session_ids.insert(sid.clone());
                     }
                     current_thread_ids.insert(thread.metadata.thread_id);
                     entries.push(thread.into());
                 }
-
-                let visible = count + promoted_threads.len();
-                let is_fully_expanded = visible >= total;
-
-                if total > DEFAULT_THREADS_SHOWN {
-                    entries.push(ListEntry::ViewMore {
-                        key: group_key.clone(),
-                        is_fully_expanded,
-                    });
-                }
             }
         }
 
@@ -1407,6 +1433,7 @@ impl Sidebar {
                             panel.active_thread_is_draft(cx)
                                 || panel.active_conversation_view().is_none()
                         });
+                self.project_header_menu_handles.entry(ix).or_default();
                 self.render_project_header(
                     ix,
                     false,
@@ -1423,10 +1450,6 @@ impl Sidebar {
                 )
             }
             ListEntry::Thread(thread) => self.render_thread(ix, thread, is_active, is_selected, cx),
-            ListEntry::ViewMore {
-                key,
-                is_fully_expanded,
-            } => self.render_view_more(ix, key, *is_fully_expanded, is_selected, cx),
         };
 
         if is_group_header_after_first {
@@ -1487,15 +1510,14 @@ impl Sidebar {
         let group_name = SharedString::from(format!("{id_prefix}header-group-{ix}"));
 
         let is_collapsed = self.is_group_collapsed(key, cx);
-        let (disclosure_icon, disclosure_tooltip) = if is_collapsed {
-            (IconName::ChevronRight, "Expand Project")
+        let disclosure_icon = if is_collapsed {
+            IconName::ChevronRight
         } else {
-            (IconName::ChevronDown, "Collapse Project")
+            IconName::ChevronDown
         };
 
         let key_for_toggle = key.clone();
-        let key_for_collapse = key.clone();
-        let view_more_expanded = self.group_extra_batches(key, cx) > 0;
+        let key_for_focus = key.clone();
 
         let label = if highlight_positions.is_empty() {
             Label::new(label.clone())
@@ -1528,8 +1550,6 @@ impl Sidebar {
                 .group_name(group_name_for_gradient.clone())
         };
 
-        let is_ellipsis_menu_open = self.project_header_menu_ix == Some(ix);
-
         let header = h_flex()
             .id(id)
             .group(&group_name)
@@ -1602,9 +1622,6 @@ impl Sidebar {
             .child(gradient_overlay())
             .child(
                 h_flex()
-                    .when(!is_ellipsis_menu_open && !has_active_draft, |this| {
-                        this.visible_on_hover(&group_name)
-                    })
                     .child(gradient_overlay())
                     .on_mouse_down(gpui::MouseButton::Left, |_, _, cx| {
                         cx.stop_propagation();
@@ -1621,6 +1638,7 @@ impl Sidebar {
                         )
                         .icon_size(IconSize::Small)
                         .when(has_active_draft, |this| this.icon_color(Color::Accent))
+                        .when(!has_active_draft, |this| this.visible_on_hover(&group_name))
                         .tooltip(move |_, cx| {
                             Tooltip::for_action_in(
                                 "Start New Agent Thread",
@@ -1641,71 +1659,36 @@ impl Sidebar {
                             },
                         ))
                     })
-                    .when(has_threads && view_more_expanded && !is_collapsed, |this| {
-                        this.child(
-                            IconButton::new(
-                                SharedString::from(format!(
-                                    "{id_prefix}project-header-collapse-{ix}",
-                                )),
-                                IconName::ListCollapse,
-                            )
-                            .icon_size(IconSize::Small)
-                            .tooltip(Tooltip::text("Show Fewer Threads"))
-                            .on_click(cx.listener({
-                                let key_for_collapse = key_for_collapse.clone();
-                                move |this, _, _window, cx| {
-                                    this.selection = None;
-                                    this.set_group_visible_thread_count(
-                                        &key_for_collapse,
-                                        None,
-                                        cx,
-                                    );
-                                    this.update_entries(cx);
-                                }
-                            })),
-                        )
-                    })
-                    .child(self.render_project_header_ellipsis_menu(ix, id_prefix, key, cx)),
+                    .child(self.render_project_header_ellipsis_menu(
+                        ix,
+                        id_prefix,
+                        key,
+                        is_active,
+                        has_threads,
+                        &group_name,
+                        cx,
+                    )),
             )
-            .tooltip(Tooltip::element({
-                move |_, cx| {
-                    v_flex()
-                        .gap_1()
-                        .child(Label::new(disclosure_tooltip))
-                        .child(
-                            h_flex()
-                                .pt_1()
-                                .border_t_1()
-                                .border_color(cx.theme().colors().border_variant)
-                                .child(h_flex().flex_shrink_0().children(render_modifiers(
-                                    &Modifiers::secondary_key(),
-                                    PlatformStyle::platform(),
-                                    None,
-                                    Some(TextSize::Default.rems(cx).into()),
-                                    false,
-                                )))
-                                .child(
-                                    Label::new("-click to activate most recent workspace")
-                                        .color(Color::Muted),
-                                ),
-                        )
-                        .into_any_element()
+            .on_mouse_down(gpui::MouseButton::Right, {
+                let menu_handle = self
+                    .project_header_menu_handles
+                    .get(&ix)
+                    .cloned()
+                    .unwrap_or_default();
+                move |_, window, cx| {
+                    cx.stop_propagation();
+                    menu_handle.toggle(window, cx);
                 }
-            }))
-            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
-                if event.modifiers().platform {
-                    let key = key_for_toggle.clone();
-                    if let Some(workspace) = this.workspace_for_group(&key, cx) {
-                        this.activate_workspace(&workspace, window, cx);
+            })
+            .on_click(
+                cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
+                    if event.modifiers().secondary() {
+                        this.activate_or_open_workspace_for_group(&key_for_focus, window, cx);
                     } else {
-                        this.open_workspace_for_group(&key, window, cx);
+                        this.toggle_collapse(&key_for_toggle, window, cx);
                     }
-                    this.selection = None;
-                    this.active_entry = None;
-                } else {
-                    this.toggle_collapse(&key_for_toggle, window, cx);
-                }
-            }));
+                }),
+            );
 
         if !is_collapsed && !has_threads {
             v_flex()
@@ -1737,49 +1720,37 @@ impl Sidebar {
         ix: usize,
         id_prefix: &str,
         project_group_key: &ProjectGroupKey,
+        is_active: bool,
+        has_threads: bool,
+        group_name: &SharedString,
         cx: &mut Context<Self>,
     ) -> AnyElement {
         let multi_workspace = self.multi_workspace.clone();
         let project_group_key = project_group_key.clone();
 
-        let show_menu = multi_workspace
+        let show_multi_project_entries = multi_workspace
             .read_with(cx, |mw, _| {
                 project_group_key.host().is_none() && mw.project_group_keys().len() >= 2
             })
             .unwrap_or(false);
 
-        if !show_menu {
-            return IconButton::new(
-                SharedString::from(format!("{id_prefix}-close-project-{ix}")),
-                IconName::Close,
-            )
-            .icon_size(IconSize::Small)
-            .tooltip(Tooltip::text("Remove Project"))
-            .on_click(cx.listener({
-                move |_, _, window, cx| {
-                    multi_workspace
-                        .update(cx, |multi_workspace, cx| {
-                            multi_workspace
-                                .remove_project_group(&project_group_key, window, cx)
-                                .detach_and_log_err(cx);
-                        })
-                        .ok();
-                }
-            }))
-            .into_any_element();
-        }
-
         let this = cx.weak_entity();
 
+        let trigger_id = SharedString::from(format!("{id_prefix}-ellipsis-menu-{ix}"));
+        let menu_handle = self
+            .project_header_menu_handles
+            .get(&ix)
+            .cloned()
+            .unwrap_or_default();
+        let is_menu_open = menu_handle.is_deployed();
+
         PopoverMenu::new(format!("{id_prefix}project-header-menu-{ix}"))
+            .with_handle(menu_handle)
             .trigger(
-                IconButton::new(
-                    SharedString::from(format!("{id_prefix}-ellipsis-menu-{ix}")),
-                    IconName::Ellipsis,
-                )
-                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                .icon_size(IconSize::Small)
-                .tooltip(Tooltip::text("Toggle Project Menu")),
+                IconButton::new(trigger_id, IconName::Ellipsis)
+                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+                    .icon_size(IconSize::Small)
+                    .when(!is_menu_open, |el| el.visible_on_hover(group_name)),
             )
             .on_open(Rc::new({
                 let this = this.clone();
@@ -1794,48 +1765,262 @@ impl Sidebar {
             .menu(move |window, cx| {
                 let multi_workspace = multi_workspace.clone();
                 let project_group_key = project_group_key.clone();
+                let this_for_menu = this.clone();
+
+                let open_workspaces = multi_workspace
+                    .read_with(cx, |multi_workspace, cx| {
+                        multi_workspace
+                            .workspaces_for_project_group(&project_group_key, cx)
+                            .unwrap_or_default()
+                    })
+                    .unwrap_or_default();
+
+                let active_workspace = multi_workspace
+                    .read_with(cx, |multi_workspace, _cx| {
+                        multi_workspace.workspace().clone()
+                    })
+                    .ok();
+                let workspace_labels: Vec<_> = open_workspaces
+                    .iter()
+                    .map(|workspace| workspace_menu_worktree_labels(workspace, cx))
+                    .collect();
+                let workspace_is_active: Vec<_> = open_workspaces
+                    .iter()
+                    .map(|workspace| active_workspace.as_ref() == Some(workspace))
+                    .collect();
 
                 let menu =
                     ContextMenu::build_persistent(window, cx, move |menu, _window, menu_cx| {
+                        let menu = menu.end_slot_action(Box::new(menu::SecondaryConfirm));
                         let weak_menu = menu_cx.weak_entity();
 
-                        let menu = menu.entry(
-                            "Open Project in New Window",
-                            Some(Box::new(workspace::MoveProjectToNewWindow)),
-                            {
-                                let project_group_key = project_group_key.clone();
-                                let multi_workspace = multi_workspace.clone();
-                                move |window, cx| {
-                                    multi_workspace
-                                        .update(cx, |multi_workspace, cx| {
-                                            multi_workspace
-                                                .open_project_group_in_new_window(
+                        let menu = menu.when(show_multi_project_entries, |this| {
+                            this.entry(
+                                "Open Project in New Window",
+                                Some(Box::new(workspace::MoveProjectToNewWindow)),
+                                {
+                                    let project_group_key = project_group_key.clone();
+                                    let multi_workspace = multi_workspace.clone();
+                                    move |window, cx| {
+                                        multi_workspace
+                                            .update(cx, |multi_workspace, cx| {
+                                                multi_workspace
+                                                    .open_project_group_in_new_window(
+                                                        &project_group_key,
+                                                        window,
+                                                        cx,
+                                                    )
+                                                    .detach_and_log_err(cx);
+                                            })
+                                            .ok();
+                                    }
+                                },
+                            )
+                        });
+
+                        let menu = menu
+                            .custom_entry(
+                                {
+                                    move |_window, cx| {
+                                        let action = h_flex()
+                                            .opacity(0.6)
+                                            .children(render_modifiers(
+                                                &Modifiers::secondary_key(),
+                                                PlatformStyle::platform(),
+                                                None,
+                                                Some(TextSize::Default.rems(cx).into()),
+                                                false,
+                                            ))
+                                            .child(Label::new("-click").color(Color::Muted));
+
+                                        let label = if has_threads {
+                                            "Focus Last Workspace"
+                                        } else {
+                                            "Focus Workspace"
+                                        };
+
+                                        h_flex()
+                                            .w_full()
+                                            .justify_between()
+                                            .gap_4()
+                                            .child(
+                                                Label::new(label)
+                                                    .when(is_active, |s| s.color(Color::Disabled)),
+                                            )
+                                            .child(action)
+                                            .into_any_element()
+                                    }
+                                },
+                                {
+                                    let project_group_key = project_group_key.clone();
+                                    let this = this_for_menu.clone();
+                                    move |window, cx| {
+                                        if is_active {
+                                            return;
+                                        }
+                                        this.update(cx, |sidebar, cx| {
+                                            if let Some(workspace) =
+                                                sidebar.workspace_for_group(&project_group_key, cx)
+                                            {
+                                                sidebar.activate_workspace(&workspace, window, cx);
+                                            } else {
+                                                sidebar.open_workspace_for_group(
                                                     &project_group_key,
                                                     window,
                                                     cx,
-                                                )
-                                                .detach_and_log_err(cx);
+                                                );
+                                            }
+                                            sidebar.selection = None;
+                                            sidebar.active_entry = None;
                                         })
                                         .ok();
-                                }
-                            },
-                        );
+                                    }
+                                },
+                            )
+                            .selectable(!is_active);
+
+                        let menu = if open_workspaces.is_empty() {
+                            menu
+                        } else {
+                            let mut menu = menu.separator().header("Open Workspaces");
+
+                            for (
+                                workspace_index,
+                                ((workspace, workspace_label), is_active_workspace),
+                            ) in open_workspaces
+                                .iter()
+                                .cloned()
+                                .zip(workspace_labels.iter().cloned())
+                                .zip(workspace_is_active.iter().copied())
+                                .enumerate()
+                            {
+                                let activate_multi_workspace = multi_workspace.clone();
+                                let close_multi_workspace = multi_workspace.clone();
+                                let activate_weak_menu = weak_menu.clone();
+                                let close_weak_menu = weak_menu.clone();
+                                let activate_workspace = workspace.clone();
+                                let close_workspace = workspace.clone();
+
+                                menu = menu.custom_entry(
+                                    move |_window, _cx| {
+                                        let close_multi_workspace = close_multi_workspace.clone();
+                                        let close_weak_menu = close_weak_menu.clone();
+                                        let close_workspace = close_workspace.clone();
+                                        let label_color = if is_active_workspace {
+                                            Color::Accent
+                                        } else {
+                                            Color::Default
+                                        };
+                                        let row_group_name = SharedString::from(format!(
+                                            "workspace-menu-row-{workspace_index}"
+                                        ));
+
+                                        h_flex()
+                                            .group(&row_group_name)
+                                            .w_full()
+                                            .gap_2()
+                                            .justify_between()
+                                            .child(h_flex().min_w_0().gap_2().children(
+                                                workspace_label.iter().map(|label| {
+                                                    h_flex()
+                                                        .min_w_0()
+                                                        .gap_0p5()
+                                                        .when_some(label.icon, |this, icon| {
+                                                            this.child(
+                                                                Icon::new(icon)
+                                                                    .size(IconSize::XSmall)
+                                                                    .color(label_color),
+                                                            )
+                                                        })
+                                                        .child(
+                                                            Label::new(label.primary_name.clone())
+                                                                .color(label_color)
+                                                                .truncate(),
+                                                        )
+                                                        .when_some(
+                                                            label.secondary_name.clone(),
+                                                            |this, secondary_name| {
+                                                                this.child(
+                                                                    Label::new(":")
+                                                                        .color(label_color)
+                                                                        .alpha(0.5),
+                                                                )
+                                                                .child(
+                                                                    Label::new(secondary_name)
+                                                                        .color(label_color)
+                                                                        .truncate(),
+                                                                )
+                                                            },
+                                                        )
+                                                        .into_any_element()
+                                                }),
+                                            ))
+                                            .child(
+                                                IconButton::new(
+                                                    ("close-workspace", workspace_index),
+                                                    IconName::Close,
+                                                )
+                                                .shape(ui::IconButtonShape::Square)
+                                                .visible_on_hover(&row_group_name)
+                                                .tooltip(Tooltip::text("Close Workspace"))
+                                                .on_click(move |_, window, cx| {
+                                                    cx.stop_propagation();
+                                                    window.prevent_default();
+                                                    close_multi_workspace
+                                                        .update(cx, |multi_workspace, cx| {
+                                                            multi_workspace
+                                                                .close_workspace(
+                                                                    &close_workspace,
+                                                                    window,
+                                                                    cx,
+                                                                )
+                                                                .detach_and_log_err(cx);
+                                                        })
+                                                        .ok();
+                                                    close_weak_menu
+                                                        .update(cx, |_, cx| cx.emit(DismissEvent))
+                                                        .ok();
+                                                }),
+                                            )
+                                            .into_any_element()
+                                    },
+                                    move |window, cx| {
+                                        activate_multi_workspace
+                                            .update(cx, |multi_workspace, cx| {
+                                                multi_workspace.activate(
+                                                    activate_workspace.clone(),
+                                                    window,
+                                                    cx,
+                                                );
+                                            })
+                                            .ok();
+                                        activate_weak_menu
+                                            .update(cx, |_, cx| cx.emit(DismissEvent))
+                                            .ok();
+                                    },
+                                );
+                            }
+
+                            menu
+                        };
 
                         let project_group_key = project_group_key.clone();
-                        let multi_workspace = multi_workspace.clone();
-                        menu.entry("Remove Project", None, move |window, cx| {
-                            multi_workspace
-                                .update(cx, |multi_workspace, cx| {
-                                    multi_workspace
-                                        .remove_project_group(&project_group_key, window, cx)
-                                        .detach_and_log_err(cx);
-                                })
-                                .ok();
-                            weak_menu.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
-                        })
+                        let remove_multi_workspace = multi_workspace.clone();
+                        menu.separator()
+                            .entry("Remove Project", None, move |window, cx| {
+                                remove_multi_workspace
+                                    .update(cx, |multi_workspace, cx| {
+                                        multi_workspace
+                                            .remove_project_group(&project_group_key, window, cx)
+                                            .detach_and_log_err(cx);
+                                    })
+                                    .ok();
+                                weak_menu.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
+                            })
                     });
 
                 let this = this.clone();
+
                 window
                     .subscribe(&menu, cx, move |_, _: &gpui::DismissEvent, _window, cx| {
                         this.update(cx, |sidebar, cx| {
@@ -2155,18 +2340,6 @@ impl Sidebar {
                     }
                 }
             }
-            ListEntry::ViewMore {
-                key,
-                is_fully_expanded,
-                ..
-            } => {
-                let key = key.clone();
-                if *is_fully_expanded {
-                    self.reset_thread_group_expansion(&key, cx);
-                } else {
-                    self.expand_thread_group(&key, cx);
-                }
-            }
         }
     }
 
@@ -2733,7 +2906,7 @@ impl Sidebar {
                     self.update_entries(cx);
                 }
             }
-            Some(ListEntry::Thread(_) | ListEntry::ViewMore { .. }) => {
+            Some(ListEntry::Thread(_)) => {
                 for i in (0..ix).rev() {
                     if let Some(ListEntry::ProjectHeader { key, .. }) = self.contents.entries.get(i)
                     {
@@ -2760,7 +2933,7 @@ impl Sidebar {
         // Find the group header for the current selection.
         let header_ix = match self.contents.entries.get(ix) {
             Some(ListEntry::ProjectHeader { .. }) => Some(ix),
-            Some(ListEntry::Thread(_) | ListEntry::ViewMore { .. }) => (0..ix).rev().find(|&i| {
+            Some(ListEntry::Thread(_)) => (0..ix).rev().find(|&i| {
                 matches!(
                     self.contents.entries.get(i),
                     Some(ListEntry::ProjectHeader { .. })
@@ -3369,9 +3542,9 @@ impl Sidebar {
         }
     }
 
-    fn remove_selected_thread(
+    fn archive_selected_thread(
         &mut self,
-        _: &RemoveSelectedThread,
+        _: &ArchiveSelectedThread,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -3398,11 +3571,7 @@ impl Sidebar {
         self.thread_last_accessed.insert(*id, Utc::now());
     }
 
-    fn record_thread_message_sent_or_queued(
-        &mut self,
-        thread_id: &agent_ui::ThreadId,
-        cx: &mut App,
-    ) {
+    fn record_thread_interacted(&mut self, thread_id: &agent_ui::ThreadId, cx: &mut App) {
         let store = ThreadMetadataStore::global(cx);
         store.update(cx, |store, cx| {
             store.update_interacted_at(thread_id, Utc::now(), cx);
@@ -3484,7 +3653,6 @@ impl Sidebar {
                         timestamp,
                     })
                 }
-                _ => None,
             })
             .collect();
 
@@ -3706,6 +3874,11 @@ impl Sidebar {
 
         let is_remote = thread.workspace.is_remote(cx);
 
+        let worktrees = apply_worktree_label_mode(
+            thread.worktrees.clone(),
+            cx.flag_value::<AgentThreadWorktreeLabelFlag>(),
+        );
+
         ThreadItem::new(id, title)
             .base_bg(sidebar_bg)
             .icon(thread.icon)
@@ -3714,7 +3887,7 @@ impl Sidebar {
             .when_some(thread.icon_from_external_svg.clone(), |this, svg| {
                 this.custom_icon_from_external_svg(svg)
             })
-            .worktrees(thread.worktrees.clone())
+            .worktrees(worktrees)
             .timestamp(timestamp)
             .highlight_positions(thread.highlight_positions.to_vec())
             .title_generating(thread.is_title_generating)
@@ -3760,7 +3933,7 @@ impl Sidebar {
                             move |_window, cx| {
                                 Tooltip::for_action_in(
                                     "Archive Thread",
-                                    &RemoveSelectedThread,
+                                    &ArchiveSelectedThread,
                                     &focus_handle,
                                     cx,
                                 )
@@ -3867,38 +4040,6 @@ impl Sidebar {
             .anchor(gpui::Corner::BottomRight)
     }
 
-    fn render_view_more(
-        &self,
-        ix: usize,
-        key: &ProjectGroupKey,
-        is_fully_expanded: bool,
-        is_selected: bool,
-        cx: &mut Context<Self>,
-    ) -> AnyElement {
-        let key = key.clone();
-        let id = SharedString::from(format!("view-more-{}", ix));
-
-        let label: SharedString = if is_fully_expanded {
-            "Collapse".into()
-        } else {
-            "View More".into()
-        };
-
-        ThreadItem::new(id, label)
-            .focused(is_selected)
-            .icon_visible(false)
-            .title_label_color(Color::Muted)
-            .on_click(cx.listener(move |this, _, _window, cx| {
-                this.selection = None;
-                if is_fully_expanded {
-                    this.reset_thread_group_expansion(&key, cx);
-                } else {
-                    this.expand_thread_group(&key, cx);
-                }
-            }))
-            .into_any_element()
-    }
-
     fn new_thread_in_group(
         &mut self,
         _: &NewThreadInGroup,
@@ -3955,7 +4096,7 @@ impl Sidebar {
         let ix = self.selection?;
         match self.contents.entries.get(ix) {
             Some(ListEntry::ProjectHeader { key, .. }) => Some(key.clone()),
-            Some(ListEntry::Thread(_) | ListEntry::ViewMore { .. }) => {
+            Some(ListEntry::Thread(_)) => {
                 (0..ix)
                     .rev()
                     .find_map(|i| match self.contents.entries.get(i) {
@@ -3979,6 +4120,26 @@ impl Sidebar {
         }
     }
 
+    pub(crate) fn activate_or_open_workspace_for_group(
+        &mut self,
+        key: &ProjectGroupKey,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let workspace = self
+            .multi_workspace
+            .upgrade()
+            .and_then(|mw| mw.read(cx).last_active_workspace_for_group(key, cx))
+            .or_else(|| self.workspace_for_group(key, cx));
+        if let Some(workspace) = workspace {
+            self.activate_workspace(&workspace, window, cx);
+        } else {
+            self.open_workspace_for_group(key, window, cx);
+        }
+        self.selection = None;
+        self.active_entry = None;
+    }
+
     fn active_project_group_key(&self, cx: &App) -> Option<ProjectGroupKey> {
         let multi_workspace = self.multi_workspace.upgrade()?;
         let multi_workspace = multi_workspace.read(cx);

crates/sidebar/src/sidebar_tests.rs πŸ”—

@@ -147,11 +147,6 @@ fn assert_remote_project_integration_sidebar_state(
                     title
                 );
             }
-            ListEntry::ViewMore { .. } => {
-                panic!(
-                    "unexpected `View More` entry while simulating remote project integration flicker"
-                );
-            }
         }
     }
 
@@ -454,9 +449,12 @@ fn format_linked_worktree_chips(worktrees: &[ThreadItemWorktreeInfo]) -> String
         if wt.kind == ui::WorktreeKind::Main {
             continue;
         }
-        if !seen.contains(&wt.name) {
-            seen.push(wt.name.clone());
-            chips.push(format!("{{{}}}", wt.name));
+        let Some(name) = wt.worktree_name.as_ref() else {
+            continue;
+        };
+        if !seen.contains(name) {
+            seen.push(name.clone());
+            chips.push(format!("{{{}}}", name));
         }
     }
     if chips.is_empty() {
@@ -519,15 +517,6 @@ fn visible_entries_as_strings(
                             format!("  {title}{worktree}{live}{status_str}{notified}{selected}")
                         }
                     }
-                    ListEntry::ViewMore {
-                        is_fully_expanded, ..
-                    } => {
-                        if *is_fully_expanded {
-                            format!("  - Collapse{}", selected)
-                        } else {
-                            format!("  + View More{}", selected)
-                        }
-                    }
                 }
             })
             .collect()
@@ -545,7 +534,7 @@ async fn test_serialization_round_trip(cx: &mut TestAppContext) {
 
     let project_group_key = project.read_with(cx, |project, cx| project.project_group_key(cx));
 
-    // Set a custom width, collapse the group, and expand "View More".
+    // Set a custom width and collapse the group.
     sidebar.update_in(cx, |sidebar, window, cx| {
         sidebar.set_width(Some(px(420.0)), cx);
         sidebar.toggle_collapse(&project_group_key, window, cx);
@@ -587,7 +576,7 @@ async fn test_restore_serialized_archive_view_does_not_panic(cx: &mut TestAppCon
 
     let serialized = serde_json::to_string(&SerializedSidebar {
         width: Some(400.0),
-        active_view: SerializedSidebarView::Archive,
+        active_view: SerializedSidebarView::History,
     })
     .expect("serialization should succeed");
 
@@ -730,101 +719,6 @@ async fn test_workspace_lifecycle(cx: &mut TestAppContext) {
     );
 }
 
-#[gpui::test]
-async fn test_view_more_pagination(cx: &mut TestAppContext) {
-    let project = init_test_project("/my-project", cx).await;
-    let (multi_workspace, cx) =
-        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
-    let sidebar = setup_sidebar(&multi_workspace, cx);
-
-    save_n_test_threads(12, &project, cx).await;
-
-    multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
-    cx.run_until_parked();
-
-    assert_eq!(
-        visible_entries_as_strings(&sidebar, cx),
-        vec![
-            //
-            "v [my-project]",
-            "  Thread 12",
-            "  Thread 11",
-            "  Thread 10",
-            "  Thread 9",
-            "  Thread 8",
-            "  + View More",
-        ]
-    );
-}
-
-#[gpui::test]
-async fn test_view_more_batched_expansion(cx: &mut TestAppContext) {
-    let project = init_test_project("/my-project", cx).await;
-    let (multi_workspace, cx) =
-        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
-    let sidebar = setup_sidebar(&multi_workspace, cx);
-
-    // Create 17 threads: initially shows 5, then 10, then 15, then all 17 with Collapse
-    save_n_test_threads(17, &project, cx).await;
-
-    let project_group_key = project.read_with(cx, |project, cx| project.project_group_key(cx));
-
-    multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
-    cx.run_until_parked();
-
-    // Initially shows 5 threads + View More
-    let entries = visible_entries_as_strings(&sidebar, cx);
-    assert_eq!(entries.len(), 7); // header + 5 threads + View More
-    assert!(entries.iter().any(|e| e.contains("View More")));
-
-    // Focus and navigate to View More, then confirm to expand by one batch
-    focus_sidebar(&sidebar, cx);
-    for _ in 0..7 {
-        cx.dispatch_action(SelectNext);
-    }
-    cx.dispatch_action(Confirm);
-    cx.run_until_parked();
-
-    // Now shows 10 threads + View More
-    let entries = visible_entries_as_strings(&sidebar, cx);
-    assert_eq!(entries.len(), 12); // header + 10 threads + View More
-    assert!(entries.iter().any(|e| e.contains("View More")));
-
-    // Expand again by one batch
-    sidebar.update_in(cx, |s, _window, cx| {
-        s.expand_thread_group(&project_group_key, cx);
-    });
-    cx.run_until_parked();
-
-    // Now shows 15 threads + View More
-    let entries = visible_entries_as_strings(&sidebar, cx);
-    assert_eq!(entries.len(), 17); // header + 15 threads + View More
-    assert!(entries.iter().any(|e| e.contains("View More")));
-
-    // Expand one more time - should show all 17 threads with Collapse button
-    sidebar.update_in(cx, |s, _window, cx| {
-        s.expand_thread_group(&project_group_key, cx);
-    });
-    cx.run_until_parked();
-
-    // All 17 threads shown with Collapse button
-    let entries = visible_entries_as_strings(&sidebar, cx);
-    assert_eq!(entries.len(), 19); // header + 17 threads + Collapse
-    assert!(!entries.iter().any(|e| e.contains("View More")));
-    assert!(entries.iter().any(|e| e.contains("Collapse")));
-
-    // Click collapse - should go back to showing 5 threads
-    sidebar.update_in(cx, |s, _window, cx| {
-        s.reset_thread_group_expansion(&project_group_key, cx);
-    });
-    cx.run_until_parked();
-
-    // Back to initial state: 5 threads + View More
-    let entries = visible_entries_as_strings(&sidebar, cx);
-    assert_eq!(entries.len(), 7); // header + 5 threads + View More
-    assert!(entries.iter().any(|e| e.contains("View More")));
-}
-
 #[gpui::test]
 async fn test_collapse_and_expand_group(cx: &mut TestAppContext) {
     let project = init_test_project("/my-project", cx).await;
@@ -948,7 +842,6 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
             key: ProjectGroupKey::new(None, collapsed_path.clone()),
             workspaces: Vec::new(),
             expanded: false,
-            visible_thread_count: None,
         });
     });
 
@@ -1092,11 +985,6 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
                 worktrees: Vec::new(),
                 diff_stats: DiffStats::default(),
             }),
-            // View More entry
-            ListEntry::ViewMore {
-                key: ProjectGroupKey::new(None, expanded_path.clone()),
-                is_fully_expanded: false,
-            },
             // Collapsed project header
             ListEntry::ProjectHeader {
                 key: ProjectGroupKey::new(None, collapsed_path.clone()),
@@ -1123,14 +1011,13 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
             "  Error thread * (error)",
             "  Waiting thread (waiting)",
             "  Notified thread * (!)",
-            "  + View More",
             "> [collapsed-project]",
         ]
     );
 
     // Move selection to the collapsed header
     sidebar.update_in(cx, |s, _window, _cx| {
-        s.selection = Some(7);
+        s.selection = Some(6);
     });
 
     assert_eq!(
@@ -1319,40 +1206,6 @@ async fn test_keyboard_confirm_on_project_header_toggles_collapse(cx: &mut TestA
     );
 }
 
-#[gpui::test]
-async fn test_keyboard_confirm_on_view_more_expands(cx: &mut TestAppContext) {
-    let project = init_test_project("/my-project", cx).await;
-    let (multi_workspace, cx) =
-        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
-    let sidebar = setup_sidebar(&multi_workspace, cx);
-
-    save_n_test_threads(8, &project, cx).await;
-    multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
-    cx.run_until_parked();
-
-    // Should show header + 5 threads + "View More"
-    let entries = visible_entries_as_strings(&sidebar, cx);
-    assert_eq!(entries.len(), 7);
-    assert!(entries.iter().any(|e| e.contains("View More")));
-
-    // Focus sidebar (selection starts at None), then navigate down to the "View More" entry (index 6)
-    focus_sidebar(&sidebar, cx);
-    for _ in 0..7 {
-        cx.dispatch_action(SelectNext);
-    }
-    assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(6));
-
-    // Confirm on "View More" to expand
-    cx.dispatch_action(Confirm);
-    cx.run_until_parked();
-
-    // All 8 threads should now be visible with a "Collapse" button
-    let entries = visible_entries_as_strings(&sidebar, cx);
-    assert_eq!(entries.len(), 10); // header + 8 threads + Collapse button
-    assert!(!entries.iter().any(|e| e.contains("View More")));
-    assert!(entries.iter().any(|e| e.contains("Collapse")));
-}
-
 #[gpui::test]
 async fn test_keyboard_expand_and_collapse_selected_entry(cx: &mut TestAppContext) {
     let project = init_test_project("/my-project", cx).await;
@@ -2090,61 +1943,6 @@ async fn test_search_matches_workspace_name(cx: &mut TestAppContext) {
     );
 }
 
-#[gpui::test]
-async fn test_search_finds_threads_hidden_behind_view_more(cx: &mut TestAppContext) {
-    let project = init_test_project("/my-project", cx).await;
-    let (multi_workspace, cx) =
-        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
-    let sidebar = setup_sidebar(&multi_workspace, cx);
-
-    // Create 8 threads. The oldest one has a unique name and will be
-    // behind View More (only 5 shown by default).
-    for i in 0..8u32 {
-        let title = if i == 0 {
-            "Hidden gem thread".to_string()
-        } else {
-            format!("Thread {}", i + 1)
-        };
-        save_thread_metadata(
-            acp::SessionId::new(Arc::from(format!("thread-{}", i))),
-            Some(title.into()),
-            chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, i).unwrap(),
-            None,
-            None,
-            &project,
-            cx,
-        )
-    }
-    cx.run_until_parked();
-
-    // Confirm the thread is not visible and View More is shown.
-    let entries = visible_entries_as_strings(&sidebar, cx);
-    assert!(
-        entries.iter().any(|e| e.contains("View More")),
-        "should have View More button"
-    );
-    assert!(
-        !entries.iter().any(|e| e.contains("Hidden gem")),
-        "Hidden gem should be behind View More"
-    );
-
-    // User searches for the hidden thread β€” it appears, and View More is gone.
-    type_in_search(&sidebar, "hidden gem", cx);
-    let filtered = visible_entries_as_strings(&sidebar, cx);
-    assert_eq!(
-        filtered,
-        vec![
-            //
-            "v [my-project]",
-            "  Hidden gem thread  <== selected",
-        ]
-    );
-    assert!(
-        !filtered.iter().any(|e| e.contains("View More")),
-        "View More should not appear when filtering"
-    );
-}
-
 #[gpui::test]
 async fn test_search_finds_threads_inside_collapsed_groups(cx: &mut TestAppContext) {
     let project = init_test_project("/my-project", cx).await;
@@ -2454,7 +2252,6 @@ async fn test_confirm_on_historical_thread_in_new_project_group_opens_real_threa
             key: project_b_key.clone(),
             workspaces: Vec::new(),
             expanded: true,
-            visible_thread_count: None,
         });
     });
 
@@ -4043,7 +3840,10 @@ async fn test_clicking_worktree_thread_does_not_briefly_render_as_separate_proje
                 }
                 ListEntry::Thread(thread)
                     if thread.metadata.title.as_ref().map(|t| t.as_ref()) == Some("WT Thread")
-                        && thread.worktrees.first().map(|wt| wt.name.as_ref())
+                        && thread
+                            .worktrees
+                            .first()
+                            .and_then(|wt| wt.worktree_name.as_ref().map(|n| n.as_ref()))
                             == Some("wt-feature-a") =>
                 {
                     saw_expected_thread = true;
@@ -4053,16 +3853,13 @@ async fn test_clicking_worktree_thread_does_not_briefly_render_as_separate_proje
                     let worktree_name = thread
                         .worktrees
                         .first()
-                        .map(|wt| wt.name.as_ref())
+                        .and_then(|wt| wt.worktree_name.as_ref().map(|n| n.as_ref()))
                         .unwrap_or("<none>");
                     panic!(
                         "unexpected sidebar thread while opening linked worktree thread: title=`{}`, worktree=`{}`",
                         title, worktree_name
                     );
                 }
-                ListEntry::ViewMore { .. } => {
-                    panic!("unexpected `View More` entry while opening linked worktree thread");
-                }
             }
         }
 
@@ -10635,7 +10432,7 @@ fn test_worktree_info_branch_names_for_main_worktrees() {
     assert_eq!(infos.len(), 1);
     assert_eq!(infos[0].kind, ui::WorktreeKind::Main);
     assert_eq!(infos[0].branch_name, Some(SharedString::from("feature-x")));
-    assert_eq!(infos[0].name, SharedString::from("myapp"));
+    assert_eq!(infos[0].worktree_name, Some(SharedString::from("myapp")));
 }
 
 #[test]
@@ -10672,7 +10469,7 @@ fn test_worktree_info_missing_branch_returns_none() {
     assert_eq!(infos.len(), 1);
     assert_eq!(infos[0].kind, ui::WorktreeKind::Main);
     assert_eq!(infos[0].branch_name, None);
-    assert_eq!(infos[0].name, SharedString::from("myapp"));
+    assert_eq!(infos[0].worktree_name, Some(SharedString::from("myapp")));
 }
 
 #[gpui::test]
@@ -11044,3 +10841,129 @@ async fn test_collab_guest_move_thread_paths_is_noop(cx: &mut TestAppContext) {
         );
     });
 }
+
+#[gpui::test]
+async fn test_cmd_click_project_header_returns_to_last_active_linked_worktree_workspace(
+    cx: &mut TestAppContext,
+) {
+    // Regression test for: cmd-clicking a project group header should return
+    // the user to the workspace they most recently had active in that group,
+    // including workspaces rooted at a linked worktree.
+    init_test(cx);
+    let fs = FakeFs::new(cx.executor());
+
+    fs.insert_tree(
+        "/project-a",
+        serde_json::json!({
+            ".git": {},
+            "src": {},
+        }),
+    )
+    .await;
+    fs.insert_tree("/project-b", serde_json::json!({ "src": {} }))
+        .await;
+
+    fs.add_linked_worktree_for_repo(
+        Path::new("/project-a/.git"),
+        false,
+        git::repository::Worktree {
+            path: std::path::PathBuf::from("/wt-feature-a"),
+            ref_name: Some("refs/heads/feature-a".into()),
+            sha: "aaa".into(),
+            is_main: false,
+            is_bare: false,
+        },
+    )
+    .await;
+
+    cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
+
+    let main_project_a = project::Project::test(fs.clone(), ["/project-a".as_ref()], cx).await;
+    let worktree_project_a =
+        project::Project::test(fs.clone(), ["/wt-feature-a".as_ref()], cx).await;
+    let project_b = project::Project::test(fs.clone(), ["/project-b".as_ref()], cx).await;
+
+    main_project_a
+        .update(cx, |p, cx| p.git_scans_complete(cx))
+        .await;
+    worktree_project_a
+        .update(cx, |p, cx| p.git_scans_complete(cx))
+        .await;
+
+    // The multi-workspace starts with the main-paths workspace of group A
+    // as the initially active workspace.
+    let (multi_workspace, cx) = cx
+        .add_window_view(|window, cx| MultiWorkspace::test_new(main_project_a.clone(), window, cx));
+
+    let sidebar = setup_sidebar(&multi_workspace, cx);
+
+    // Capture the initially active workspace (group A's main-paths workspace)
+    // *before* registering additional workspaces, since `workspaces()` returns
+    // retained workspaces in registration order β€” not activation order β€” and
+    // the multi-workspace's starting workspace may not be retained yet.
+    let main_workspace_a = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
+
+    // Register the linked-worktree workspace (group A) and the group-B
+    // workspace. Both get retained by the multi-workspace.
+    let worktree_workspace_a = multi_workspace.update_in(cx, |mw, window, cx| {
+        mw.test_add_workspace(worktree_project_a.clone(), window, cx)
+    });
+    let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
+        mw.test_add_workspace(project_b.clone(), window, cx)
+    });
+
+    cx.run_until_parked();
+
+    // Step 1: activate the linked-worktree workspace. The MultiWorkspace
+    // records this as the last-active workspace for group A on its
+    // ProjectGroupState. (We don't assert on the initial active workspace
+    // because `test_add_workspace` may auto-activate newly registered
+    // workspaces β€” what matters for this test is the explicit sequence of
+    // activations below.)
+    multi_workspace.update_in(cx, |mw, window, cx| {
+        mw.activate(worktree_workspace_a.clone(), window, cx);
+    });
+    cx.run_until_parked();
+    assert_eq!(
+        multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()),
+        worktree_workspace_a,
+        "linked-worktree workspace should be active after step 1"
+    );
+
+    // Step 2: switch to the workspace for group B. Group A's last-active
+    // workspace remains the linked-worktree one (group B getting activated
+    // records *its own* last-active workspace, not group A's).
+    multi_workspace.update_in(cx, |mw, window, cx| {
+        mw.activate(workspace_b.clone(), window, cx);
+    });
+    cx.run_until_parked();
+    assert_eq!(
+        multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()),
+        workspace_b,
+        "group B's workspace should be active after step 2"
+    );
+
+    // Step 3: simulate cmd-click on group A's header. The project group key
+    // for group A is derived from the *main-paths* workspace (linked-worktree
+    // workspaces share the same key because it normalizes to main-worktree
+    // paths).
+    let group_a_key = main_workspace_a.read_with(cx, |ws, cx| ws.project_group_key(cx));
+    sidebar.update_in(cx, |sidebar, window, cx| {
+        sidebar.activate_or_open_workspace_for_group(&group_a_key, window, cx);
+    });
+    cx.run_until_parked();
+
+    // Expected: we're back in the linked-worktree workspace, not the
+    // main-paths one.
+    let active_after_cmd_click = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
+    assert_eq!(
+        active_after_cmd_click, worktree_workspace_a,
+        "cmd-click on group A's header should return to the last-active \
+         linked-worktree workspace, not the main-paths workspace"
+    );
+    assert_ne!(
+        active_after_cmd_click, main_workspace_a,
+        "cmd-click must not fall back to the main-paths workspace when a \
+         linked-worktree workspace was the last-active one for the group"
+    );
+}

crates/sidebar/src/thread_switcher.rs πŸ”—

@@ -146,6 +146,15 @@ impl ThreadSwitcher {
         }
     }
 
+    fn select_index(&mut self, index: usize, cx: &mut Context<Self>) {
+        if index >= self.entries.len() || index == self.selected_index {
+            return;
+        }
+        self.selected_index = index;
+        self.emit_preview(cx);
+        cx.notify();
+    }
+
     fn cancel(&mut self, _: &menu::Cancel, _window: &mut gpui::Window, cx: &mut Context<Self>) {
         cx.emit(ThreadSwitcherEvent::Dismissed);
         cx.emit(DismissEvent);
@@ -213,37 +222,39 @@ impl Render for ThreadSwitcher {
             .children(self.entries.iter().enumerate().map(|(ix, entry)| {
                 let id = SharedString::from(format!("thread-switcher-{}", entry.session_id));
 
-                div()
-                    .id(id.clone())
+                ThreadItem::new(id, entry.title.clone())
+                    .rounded(true)
+                    .icon(entry.icon)
+                    .status(entry.status)
+                    .when_some(entry.icon_from_external_svg.clone(), |this, svg| {
+                        this.custom_icon_from_external_svg(svg)
+                    })
+                    .when_some(entry.project_name.clone(), |this, name| {
+                        this.project_name(name)
+                    })
+                    .worktrees(entry.worktrees.clone())
+                    .timestamp(entry.timestamp.clone())
+                    .title_generating(entry.is_title_generating)
+                    .notified(entry.notified)
+                    .when(entry.diff_stats.lines_added > 0, |this| {
+                        this.added(entry.diff_stats.lines_added as usize)
+                    })
+                    .when(entry.diff_stats.lines_removed > 0, |this| {
+                        this.removed(entry.diff_stats.lines_removed as usize)
+                    })
+                    .selected(ix == selected_index)
+                    .base_bg(cx.theme().colors().elevated_surface_background)
+                    .on_hover(cx.listener(move |this, hovered: &bool, _window, cx| {
+                        if *hovered {
+                            this.select_index(ix, cx);
+                        }
+                    }))
+                    // TODO: This is not properly propagating to the tread item.
                     .on_click(
                         cx.listener(move |this, _event: &gpui::ClickEvent, _window, cx| {
                             this.select_and_confirm(ix, cx);
                         }),
                     )
-                    .child(
-                        ThreadItem::new(id, entry.title.clone())
-                            .rounded(true)
-                            .icon(entry.icon)
-                            .status(entry.status)
-                            .when_some(entry.icon_from_external_svg.clone(), |this, svg| {
-                                this.custom_icon_from_external_svg(svg)
-                            })
-                            .when_some(entry.project_name.clone(), |this, name| {
-                                this.project_name(name)
-                            })
-                            .worktrees(entry.worktrees.clone())
-                            .timestamp(entry.timestamp.clone())
-                            .title_generating(entry.is_title_generating)
-                            .notified(entry.notified)
-                            .when(entry.diff_stats.lines_added > 0, |this| {
-                                this.added(entry.diff_stats.lines_added as usize)
-                            })
-                            .when(entry.diff_stats.lines_removed > 0, |this| {
-                                this.removed(entry.diff_stats.lines_removed as usize)
-                            })
-                            .selected(ix == selected_index)
-                            .base_bg(cx.theme().colors().elevated_surface_background),
-                    )
                     .into_any_element()
             }))
     }

crates/ui/src/components/ai/parallel_agents_illustration.rs πŸ”—

@@ -15,32 +15,39 @@ impl RenderOnce for ParallelAgentsIllustration {
     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
         let icon_container = || h_flex().size_4().flex_shrink_0().justify_center();
 
-        let title_bar = |id: &'static str, width: DefiniteLength, duration_ms: u64| {
+        let loading_bar = |id: &'static str, width: DefiniteLength, duration_ms: u64| {
             div()
-                .h_2()
+                .h(rems_from_px(5.))
                 .w(width)
                 .rounded_full()
-                .debug_bg_blue()
                 .bg(cx.theme().colors().element_selected)
                 .with_animation(
                     id,
                     Animation::new(Duration::from_millis(duration_ms))
                         .repeat()
-                        .with_easing(pulsating_between(0.4, 0.8)),
+                        .with_easing(pulsating_between(0.1, 0.8)),
                     |label, delta| label.opacity(delta),
                 )
         };
 
+        let skeleton_bar = |width: DefiniteLength| {
+            div().h(rems_from_px(5.)).w(width).rounded_full().bg(cx
+                .theme()
+                .colors()
+                .text_muted
+                .opacity(0.05))
+        };
+
         let time =
             |time: SharedString| Label::new(time).size(LabelSize::XSmall).color(Color::Muted);
 
         let worktree = |worktree: SharedString| {
             h_flex()
-                .gap_1()
+                .gap_0p5()
                 .child(
                     Icon::new(IconName::GitWorktree)
                         .color(Color::Muted)
-                        .size(IconSize::XSmall),
+                        .size(IconSize::Indicator),
                 )
                 .child(
                     Label::new(worktree)
@@ -56,51 +63,53 @@ impl RenderOnce for ParallelAgentsIllustration {
                 .alpha(0.5)
         };
 
-        let agent = |id: &'static str,
-                     icon: IconName,
-                     width: DefiniteLength,
-                     duration_ms: u64,
-                     data: Vec<AnyElement>| {
+        let agent = |title: SharedString, icon: IconName, selected: bool, data: Vec<AnyElement>| {
             v_flex()
-                .p_2()
+                .when(selected, |this| {
+                    this.bg(cx.theme().colors().element_active.opacity(0.2))
+                })
+                .p_1()
                 .child(
                     h_flex()
                         .w_full()
-                        .gap_2()
+                        .gap_1()
                         .child(
                             icon_container()
-                                .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)),
+                                .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted)),
                         )
-                        .child(title_bar(id, width, duration_ms)),
+                        .map(|this| {
+                            if selected {
+                                this.child(
+                                    Label::new(title)
+                                        .color(Color::Muted)
+                                        .size(LabelSize::XSmall),
+                                )
+                            } else {
+                                this.child(skeleton_bar(relative(0.7)))
+                            }
+                        }),
                 )
                 .child(
                     h_flex()
                         .opacity(0.8)
                         .w_full()
-                        .gap_2()
+                        .gap_1()
                         .child(icon_container())
                         .children(data),
                 )
         };
 
         let agents = v_flex()
-            .absolute()
-            .w(rems_from_px(380.))
-            .top_8()
-            .rounded_t_sm()
-            .border_1()
-            .border_color(cx.theme().colors().border.opacity(0.5))
+            .col_span(3)
             .bg(cx.theme().colors().elevated_surface_background)
-            .shadow_md()
             .child(agent(
-                "zed-agent-bar",
+                "Fix branch label".into(),
                 IconName::ZedAgent,
-                relative(0.7),
-                1800,
+                true,
                 vec![
-                    worktree("happy-tree".into()).into_any_element(),
+                    worktree("bug-fix".into()).into_any_element(),
                     dot_separator().into_any_element(),
-                    DiffStat::new("ds", 23, 13)
+                    DiffStat::new("ds", 5, 2)
                         .label_size(LabelSize::XSmall)
                         .into_any_element(),
                     dot_separator().into_any_element(),
@@ -109,10 +118,9 @@ impl RenderOnce for ParallelAgentsIllustration {
             ))
             .child(Divider::horizontal())
             .child(agent(
-                "claude-bar",
+                "Improve thread id".into(),
                 IconName::AiClaude,
-                relative(0.85),
-                2400,
+                false,
                 vec![
                     DiffStat::new("ds", 120, 84)
                         .label_size(LabelSize::XSmall)
@@ -123,27 +131,142 @@ impl RenderOnce for ParallelAgentsIllustration {
             ))
             .child(Divider::horizontal())
             .child(agent(
-                "openai-bar",
+                "Refactor archive view".into(),
                 IconName::AiOpenAi,
-                relative(0.4),
-                3100,
+                false,
                 vec![
                     worktree("silent-forest".into()).into_any_element(),
                     dot_separator().into_any_element(),
                     time("37m".into()).into_any_element(),
                 ],
-            ))
-            .child(Divider::horizontal());
+            ));
+
+        let thread_view = v_flex()
+            .col_span(3)
+            .h_full()
+            .flex_1()
+            .border_l_1()
+            .border_color(cx.theme().colors().border.opacity(0.5))
+            .bg(cx.theme().colors().panel_background)
+            .child(
+                h_flex()
+                    .px_1p5()
+                    .py_0p5()
+                    .w_full()
+                    .justify_between()
+                    .border_b_1()
+                    .border_color(cx.theme().colors().border.opacity(0.5))
+                    .child(
+                        Label::new("Fix branch label")
+                            .size(LabelSize::XSmall)
+                            .color(Color::Muted),
+                    )
+                    .child(
+                        Icon::new(IconName::Plus)
+                            .size(IconSize::Indicator)
+                            .color(Color::Muted),
+                    ),
+            )
+            .child(
+                div().p_1().child(
+                    v_flex()
+                        .px_1()
+                        .py_1p5()
+                        .gap_1()
+                        .border_1()
+                        .border_color(cx.theme().colors().border.opacity(0.5))
+                        .bg(cx.theme().colors().editor_background)
+                        .rounded_sm()
+                        .shadow_sm()
+                        .child(skeleton_bar(relative(0.7)))
+                        .child(skeleton_bar(relative(0.2))),
+                ),
+            )
+            .child(
+                v_flex()
+                    .p_2()
+                    .gap_1()
+                    .child(loading_bar("a", relative(0.55), 2200))
+                    .child(loading_bar("b", relative(0.75), 2000))
+                    .child(loading_bar("c", relative(0.25), 2400)),
+            );
+
+        let file_row = |indent: usize, is_folder: bool, bar_width: Rems| {
+            let indent_px = rems_from_px((indent as f32) * 4.0);
+
+            h_flex()
+                .px_2()
+                .py_px()
+                .gap_1()
+                .pl(indent_px)
+                .child(
+                    icon_container().child(
+                        Icon::new(if is_folder {
+                            IconName::FolderOpen
+                        } else {
+                            IconName::FileRust
+                        })
+                        .size(IconSize::Indicator)
+                        .color(Color::Custom(cx.theme().colors().icon_muted.opacity(0.2))),
+                    ),
+                )
+                .child(
+                    div().h_1p5().w(bar_width).rounded_sm().bg(cx
+                        .theme()
+                        .colors()
+                        .text
+                        .opacity(if is_folder { 0.15 } else { 0.1 })),
+                )
+        };
+
+        let project_panel = v_flex()
+            .col_span(1)
+            .h_full()
+            .flex_1()
+            .border_l_1()
+            .border_color(cx.theme().colors().border.opacity(0.5))
+            .bg(cx.theme().colors().panel_background)
+            .child(
+                v_flex()
+                    .child(file_row(0, true, rems_from_px(42.0)))
+                    .child(file_row(1, true, rems_from_px(28.0)))
+                    .child(file_row(2, false, rems_from_px(52.0)))
+                    .child(file_row(2, false, rems_from_px(36.0)))
+                    .child(file_row(2, false, rems_from_px(44.0)))
+                    .child(file_row(1, true, rems_from_px(34.0)))
+                    .child(file_row(2, false, rems_from_px(48.0)))
+                    .child(file_row(2, true, rems_from_px(26.0)))
+                    .child(file_row(3, false, rems_from_px(40.0)))
+                    .child(file_row(3, false, rems_from_px(56.0)))
+                    .child(file_row(1, false, rems_from_px(38.0)))
+                    .child(file_row(0, true, rems_from_px(30.0)))
+                    .child(file_row(1, false, rems_from_px(46.0)))
+                    .child(file_row(1, false, rems_from_px(32.0))),
+            );
+
+        let workspace = div()
+            .absolute()
+            .top_8()
+            .grid()
+            .grid_cols(7)
+            .w(rems_from_px(380.))
+            .rounded_t_sm()
+            .border_1()
+            .border_color(cx.theme().colors().border.opacity(0.5))
+            .shadow_md()
+            .child(agents)
+            .child(thread_view)
+            .child(project_panel);
 
         h_flex()
             .relative()
             .h(rems_from_px(180.))
-            .bg(cx.theme().colors().editor_background)
+            .bg(cx.theme().colors().editor_background.opacity(0.6))
             .justify_center()
             .items_end()
             .rounded_t_md()
             .overflow_hidden()
             .bg(gpui::black().opacity(0.2))
-            .child(agents)
+            .child(workspace)
     }
 }

crates/ui/src/components/ai/thread_item.rs πŸ”—

@@ -1,11 +1,7 @@
-use crate::{
-    CommonAnimationExt, DecoratedIcon, DiffStat, GradientFade, HighlightedLabel, IconDecoration,
-    Tooltip, prelude::*,
-};
+use crate::{CommonAnimationExt, DiffStat, GradientFade, HighlightedLabel, Tooltip, prelude::*};
 
 use gpui::{
-    Animation, AnimationExt, AnyView, ClickEvent, Hsla, MouseButton, SharedString,
-    pulsating_between,
+    Animation, AnimationExt, ClickEvent, Hsla, MouseButton, SharedString, pulsating_between,
 };
 use itertools::Itertools as _;
 use std::{path::PathBuf, sync::Arc, time::Duration};
@@ -26,13 +22,13 @@ pub enum WorktreeKind {
     Linked,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Default)]
 pub struct ThreadItemWorktreeInfo {
-    pub name: SharedString,
+    pub worktree_name: Option<SharedString>,
+    pub branch_name: Option<SharedString>,
     pub full_path: SharedString,
     pub highlight_positions: Vec<usize>,
     pub kind: WorktreeKind,
-    pub branch_name: Option<SharedString>,
 }
 
 #[derive(IntoElement, RegisterComponent)]
@@ -42,7 +38,6 @@ pub struct ThreadItem {
     icon_color: Option<Color>,
     icon_visible: bool,
     custom_icon_from_external_svg: Option<SharedString>,
-    icon_decoration: Option<IconDecoration>,
     title: SharedString,
     title_label_color: Option<Color>,
     title_generating: bool,
@@ -60,10 +55,10 @@ pub struct ThreadItem {
     project_name: Option<SharedString>,
     worktrees: Vec<ThreadItemWorktreeInfo>,
     is_remote: bool,
+    archived: bool,
     on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
     on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
     action_slot: Option<AnyElement>,
-    tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
     base_bg: Option<Hsla>,
 }
 
@@ -75,7 +70,6 @@ impl ThreadItem {
             icon_color: None,
             icon_visible: true,
             custom_icon_from_external_svg: None,
-            icon_decoration: None,
             title: title.into(),
             title_label_color: None,
             title_generating: false,
@@ -89,15 +83,14 @@ impl ThreadItem {
             rounded: false,
             added: None,
             removed: None,
-
             project_paths: None,
             project_name: None,
             worktrees: Vec::new(),
             is_remote: false,
+            archived: false,
             on_click: None,
             on_hover: Box::new(|_, _, _| {}),
             action_slot: None,
-            tooltip: None,
             base_bg: None,
         }
     }
@@ -122,11 +115,6 @@ impl ThreadItem {
         self
     }
 
-    pub fn icon_decoration(mut self, decoration: IconDecoration) -> Self {
-        self.icon_decoration = Some(decoration);
-        self
-    }
-
     pub fn custom_icon_from_external_svg(mut self, svg: impl Into<SharedString>) -> Self {
         self.custom_icon_from_external_svg = Some(svg.into());
         self
@@ -197,6 +185,11 @@ impl ThreadItem {
         self
     }
 
+    pub fn archived(mut self, archived: bool) -> Self {
+        self.archived = archived;
+        self
+    }
+
     pub fn hovered(mut self, hovered: bool) -> Self {
         self.hovered = hovered;
         self
@@ -225,11 +218,6 @@ impl ThreadItem {
         self
     }
 
-    pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
-        self.tooltip = Some(Box::new(tooltip));
-        self
-    }
-
     pub fn base_bg(mut self, color: Hsla) -> Self {
         self.base_bg = Some(color);
         self
@@ -263,11 +251,11 @@ impl RenderOnce for ThreadItem {
             .gradient_stop(0.75)
             .group_name("thread-item");
 
+        let separator_color = Color::Custom(color.text_muted.opacity(0.4));
         let dot_separator = || {
             Label::new("β€’")
                 .size(LabelSize::Small)
-                .color(Color::Muted)
-                .alpha(0.5)
+                .color(separator_color)
         };
 
         let icon_id = format!("icon-{}", self.id);
@@ -289,35 +277,26 @@ impl RenderOnce for ThreadItem {
             Icon::new(self.icon).color(icon_color).size(IconSize::Small)
         };
 
-        let (status_icon, icon_tooltip) = if self.status == AgentThreadStatus::Error {
-            (
-                Some(
-                    Icon::new(IconName::Close)
-                        .size(IconSize::Small)
-                        .color(Color::Error),
-                ),
-                Some("Thread has an Error"),
+        let status_icon = if self.status == AgentThreadStatus::Error {
+            Some(
+                Icon::new(IconName::Close)
+                    .size(IconSize::Small)
+                    .color(Color::Error),
             )
         } else if self.status == AgentThreadStatus::WaitingForConfirmation {
-            (
-                Some(
-                    Icon::new(IconName::Warning)
-                        .size(IconSize::XSmall)
-                        .color(Color::Warning),
-                ),
-                Some("Thread is Waiting for Confirmation"),
+            Some(
+                Icon::new(IconName::Warning)
+                    .size(IconSize::XSmall)
+                    .color(Color::Warning),
             )
         } else if self.notified {
-            (
-                Some(
-                    Icon::new(IconName::Circle)
-                        .size(IconSize::Small)
-                        .color(Color::Accent),
-                ),
-                Some("Thread's Generation is Complete"),
+            Some(
+                Icon::new(IconName::Circle)
+                    .size(IconSize::Small)
+                    .color(Color::Accent),
             )
         } else {
-            (None, None)
+            None
         };
 
         let icon = if self.status == AgentThreadStatus::Running {
@@ -330,16 +309,7 @@ impl RenderOnce for ThreadItem {
                 )
                 .into_any_element()
         } else if let Some(status_icon) = status_icon {
-            icon_container()
-                .child(status_icon)
-                .when_some(icon_tooltip, |icon, tooltip| {
-                    icon.tooltip(Tooltip::text(tooltip))
-                })
-                .into_any_element()
-        } else if let Some(decoration) = self.icon_decoration {
-            icon_container()
-                .child(DecoratedIcon::new(agent_icon, Some(decoration)))
-                .into_any_element()
+            icon_container().child(status_icon).into_any_element()
         } else {
             icon_container().child(agent_icon).into_any_element()
         };
@@ -392,80 +362,25 @@ impl RenderOnce for ThreadItem {
         let has_timestamp = !self.timestamp.is_empty();
         let timestamp = self.timestamp;
 
-        let linked_worktree_count = self
+        let show_tooltip = matches!(
+            self.status,
+            AgentThreadStatus::Error | AgentThreadStatus::WaitingForConfirmation
+        );
+
+        let linked_worktrees: Vec<ThreadItemWorktreeInfo> = self
             .worktrees
-            .iter()
+            .into_iter()
             .filter(|wt| wt.kind == WorktreeKind::Linked)
-            .count();
-
-        let worktree_tooltip_title = match (self.is_remote, linked_worktree_count > 1) {
-            (true, true) => "Thread Running in Remote Git Worktrees",
-            (true, false) => "Thread Running in a Remote Git Worktree",
-            (false, true) => "Thread Running in Local Git Worktrees",
-            (false, false) => "Thread Running in a Local Git Worktree",
-        };
+            .filter(|wt| wt.worktree_name.is_some() || wt.branch_name.is_some())
+            .collect();
 
-        let mut worktree_labels: Vec<AnyElement> = Vec::new();
-
-        let slash_color = Color::Custom(cx.theme().colors().text_muted.opacity(0.4));
-
-        for wt in self.worktrees {
-            match wt.kind {
-                WorktreeKind::Main => continue,
-                WorktreeKind::Linked => {
-                    let chip_index = worktree_labels.len();
-                    let tooltip_title = worktree_tooltip_title;
-                    let full_path = wt.full_path.clone();
-
-                    let label = if wt.highlight_positions.is_empty() {
-                        Label::new(wt.name)
-                            .size(LabelSize::Small)
-                            .color(Color::Muted)
-                            .truncate()
-                            .into_any_element()
-                    } else {
-                        HighlightedLabel::new(wt.name, wt.highlight_positions)
-                            .size(LabelSize::Small)
-                            .color(Color::Muted)
-                            .truncate()
-                            .into_any_element()
-                    };
-
-                    worktree_labels.push(
-                        h_flex()
-                            .id(format!("{}-worktree-{chip_index}", self.id.clone()))
-                            .min_w_0()
-                            .gap_0p5()
-                            .child(
-                                Icon::new(IconName::GitWorktree)
-                                    .size(IconSize::XSmall)
-                                    .color(Color::Muted),
-                            )
-                            .child(label)
-                            .when_some(wt.branch_name, |this, branch| {
-                                this.child(
-                                    Label::new("/")
-                                        .size(LabelSize::Small)
-                                        .color(slash_color)
-                                        .flex_shrink_0(),
-                                )
-                                .child(
-                                    Label::new(branch)
-                                        .size(LabelSize::Small)
-                                        .color(Color::Muted)
-                                        .truncate(),
-                                )
-                            })
-                            .tooltip(move |_, cx| {
-                                Tooltip::with_meta(tooltip_title, None, full_path.clone(), cx)
-                            })
-                            .into_any_element(),
-                    );
-                }
-            }
-        }
+        let has_worktree = !linked_worktrees.is_empty();
 
-        let has_worktree = !worktree_labels.is_empty();
+        let has_metadata = has_project_name
+            || has_project_paths
+            || has_worktree
+            || has_diff_stats
+            || has_timestamp;
 
         v_flex()
             .id(self.id.clone())
@@ -496,8 +411,7 @@ impl RenderOnce for ThreadItem {
                             .flex_1()
                             .gap_1p5()
                             .child(icon)
-                            .child(title_label)
-                            .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)),
+                            .child(title_label),
                     )
                     .child(gradient_overlay)
                     .when(self.hovered, |this| {
@@ -520,78 +434,154 @@ impl RenderOnce for ThreadItem {
                         })
                     }),
             )
-            .when(
-                has_project_name
-                    || has_project_paths
-                    || has_worktree
-                    || has_diff_stats
-                    || has_timestamp,
-                |this| {
-                    this.child(
-                        h_flex()
-                            .min_w_0()
-                            .gap_1p5()
-                            .child(icon_container()) // Icon Spacing
-                            .when(
-                                has_project_name || has_project_paths || has_worktree,
-                                |this| {
+            .when(has_metadata, |this| {
+                this.child(
+                    h_flex()
+                        .gap_1p5()
+                        .child(icon_container()) // Icon Spacing
+                        .when(self.archived, |this| {
+                            this.child(
+                                Icon::new(IconName::Archive).size(IconSize::XSmall).color(
+                                    Color::Custom(cx.theme().colors().icon_muted.opacity(0.5)),
+                                ),
+                            )
+                            // .child(dot_separator())
+                        })
+                        .when(
+                            has_project_name || has_project_paths || has_worktree,
+                            |this| {
+                                this.when_some(self.project_name, |this, name| {
                                     this.child(
+                                        Label::new(name).size(LabelSize::Small).color(Color::Muted),
+                                    )
+                                })
+                                .when(
+                                    has_project_name && (has_project_paths || has_worktree),
+                                    |this| this.child(dot_separator()),
+                                )
+                                .when_some(project_paths, |this, paths| {
+                                    this.child(
+                                        Label::new(paths)
+                                            .size(LabelSize::Small)
+                                            .color(Color::Muted),
+                                    )
+                                })
+                                .when(has_project_paths && has_worktree, |this| {
+                                    this.child(dot_separator())
+                                })
+                                .children(
+                                    linked_worktrees.into_iter().map(|wt| {
+                                        let worktree_label = wt.worktree_name.clone().map(|name| {
+                                            if wt.highlight_positions.is_empty() {
+                                                Label::new(name)
+                                                    .size(LabelSize::Small)
+                                                    .color(Color::Muted)
+                                                    .truncate()
+                                                    .into_any_element()
+                                            } else {
+                                                HighlightedLabel::new(
+                                                    name,
+                                                    wt.highlight_positions.clone(),
+                                                )
+                                                .size(LabelSize::Small)
+                                                .color(Color::Muted)
+                                                .truncate()
+                                                .into_any_element()
+                                            }
+                                        });
+
+                                        // When only the branch is shown, lead with a branch icon;
+                                        // otherwise keep the worktree icon (which "covers" both the
+                                        // worktree and any accompanying branch).
+                                        let chip_icon = if wt.worktree_name.is_none()
+                                            && wt.branch_name.is_some()
+                                        {
+                                            IconName::GitBranch
+                                        } else {
+                                            IconName::GitWorktree
+                                        };
+
+                                        let branch_label = wt.branch_name.map(|branch| {
+                                            Label::new(branch)
+                                                .size(LabelSize::Small)
+                                                .color(Color::Muted)
+                                                .truncate()
+                                                .into_any_element()
+                                        });
+
+                                        let show_separator =
+                                            worktree_label.is_some() && branch_label.is_some();
+
                                         h_flex()
                                             .min_w_0()
-                                            .flex_shrink()
-                                            .overflow_hidden()
-                                            .gap_1p5()
-                                            .when_some(self.project_name, |this, name| {
-                                                this.child(
-                                                    Label::new(name)
-                                                        .size(LabelSize::Small)
-                                                        .color(Color::Muted),
-                                                )
-                                            })
-                                            .when(
-                                                has_project_name
-                                                    && (has_project_paths || has_worktree),
-                                                |this| this.child(dot_separator()),
+                                            .gap_0p5()
+                                            .child(
+                                                Icon::new(chip_icon)
+                                                    .size(IconSize::XSmall)
+                                                    .color(Color::Muted),
                                             )
-                                            .when_some(project_paths, |this, paths| {
+                                            .when_some(worktree_label, |this, label| {
+                                                this.child(label)
+                                            })
+                                            .when(show_separator, |this| {
                                                 this.child(
-                                                    Label::new(paths)
+                                                    Label::new("/")
                                                         .size(LabelSize::Small)
-                                                        .color(Color::Muted)
-                                                        .into_any_element(),
+                                                        .color(separator_color)
+                                                        .flex_shrink_0(),
                                                 )
                                             })
-                                            .when(has_project_paths && has_worktree, |this| {
-                                                this.child(dot_separator())
+                                            .when_some(branch_label, |this, label| {
+                                                this.child(label)
                                             })
-                                            .children(worktree_labels),
-                                    )
-                                },
-                            )
-                            .when(
-                                (has_project_name || has_project_paths || has_worktree)
-                                    && (has_diff_stats || has_timestamp),
-                                |this| this.child(dot_separator()),
-                            )
-                            .when(has_diff_stats, |this| {
-                                this.child(
-                                    DiffStat::new(diff_stat_id, added_count, removed_count)
-                                        .tooltip("Unreviewed Changes"),
+                                    }),
                                 )
-                            })
-                            .when(has_diff_stats && has_timestamp, |this| {
-                                this.child(dot_separator())
-                            })
-                            .when(has_timestamp, |this| {
-                                this.child(
-                                    Label::new(timestamp.clone())
-                                        .size(LabelSize::Small)
-                                        .color(Color::Muted),
-                                )
-                            }),
-                    )
-                },
-            )
+                            },
+                        )
+                        .when(
+                            (has_project_name || has_project_paths || has_worktree)
+                                && (has_diff_stats || has_timestamp),
+                            |this| this.child(dot_separator()),
+                        )
+                        .when(has_diff_stats, |this| {
+                            this.child(DiffStat::new(diff_stat_id, added_count, removed_count))
+                        })
+                        .when(has_diff_stats && has_timestamp, |this| {
+                            this.child(dot_separator())
+                        })
+                        .when(has_timestamp, |this| {
+                            this.child(
+                                Label::new(timestamp.clone())
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted),
+                            )
+                        }),
+                )
+            })
+            .when(show_tooltip, |this| {
+                let status = self.status;
+                this.tooltip(Tooltip::element(move |_, _| match status {
+                    AgentThreadStatus::Error => h_flex()
+                        .gap_1()
+                        .child(
+                            Icon::new(IconName::Close)
+                                .size(IconSize::Small)
+                                .color(Color::Error),
+                        )
+                        .child(Label::new("Thread has an Error"))
+                        .into_any_element(),
+                    AgentThreadStatus::WaitingForConfirmation => h_flex()
+                        .gap_1()
+                        .child(
+                            Icon::new(IconName::Warning)
+                                .size(IconSize::Small)
+                                .color(Color::Warning),
+                        )
+                        .child(Label::new("Waiting for Confirmation"))
+                        .into_any_element(),
+                    _ => gpui::Empty.into_any_element(),
+                }))
+            })
             .when_some(self.on_click, |this, on_click| this.on_click(on_click))
     }
 }
@@ -617,7 +607,7 @@ impl Component for ThreadItem {
 
         let thread_item_examples = vec![
             single_example(
-                "Default (minutes)",
+                "Default",
                 container()
                     .child(
                         ThreadItem::new("ti-1", "Linking to the Agent Panel Depending on Settings")
@@ -626,16 +616,6 @@ impl Component for ThreadItem {
                     )
                     .into_any_element(),
             ),
-            single_example(
-                "Notified (weeks)",
-                container()
-                    .child(
-                        ThreadItem::new("ti-2", "Refine thread view scrolling behavior")
-                            .timestamp("1w")
-                            .notified(true),
-                    )
-                    .into_any_element(),
-            ),
             single_example(
                 "Waiting for Confirmation",
                 container()
@@ -675,7 +655,7 @@ impl Component for ThreadItem {
                             .icon(IconName::AiClaude)
                             .timestamp("2w")
                             .worktrees(vec![ThreadItemWorktreeInfo {
-                                name: "link-agent-panel".into(),
+                                worktree_name: Some("link-agent-panel".into()),
                                 full_path: "link-agent-panel".into(),
                                 highlight_positions: Vec::new(),
                                 kind: WorktreeKind::Linked,
@@ -685,7 +665,7 @@ impl Component for ThreadItem {
                     .into_any_element(),
             ),
             single_example(
-                "With Changes (months)",
+                "With Changes",
                 container()
                     .child(
                         ThreadItem::new("ti-5", "Managing user and project settings interactions")
@@ -703,7 +683,7 @@ impl Component for ThreadItem {
                         ThreadItem::new("ti-5b", "Full metadata example")
                             .icon(IconName::AiClaude)
                             .worktrees(vec![ThreadItemWorktreeInfo {
-                                name: "my-project".into(),
+                                worktree_name: Some("my-project".into()),
                                 full_path: "my-project".into(),
                                 highlight_positions: Vec::new(),
                                 kind: WorktreeKind::Linked,
@@ -722,7 +702,7 @@ impl Component for ThreadItem {
                         ThreadItem::new("ti-5c", "Full metadata with branch")
                             .icon(IconName::AiClaude)
                             .worktrees(vec![ThreadItemWorktreeInfo {
-                                name: "my-project".into(),
+                                worktree_name: Some("my-project".into()),
                                 full_path: "/worktrees/my-project/zed".into(),
                                 highlight_positions: Vec::new(),
                                 kind: WorktreeKind::Linked,
@@ -741,7 +721,7 @@ impl Component for ThreadItem {
                         ThreadItem::new("ti-5d", "Metadata overflow with long branch name")
                             .icon(IconName::AiClaude)
                             .worktrees(vec![ThreadItemWorktreeInfo {
-                                name: "my-project".into(),
+                                worktree_name: Some("my-project".into()),
                                 full_path: "/worktrees/my-project/zed".into(),
                                 highlight_positions: Vec::new(),
                                 kind: WorktreeKind::Linked,
@@ -760,7 +740,7 @@ impl Component for ThreadItem {
                         ThreadItem::new("ti-5e", "Main worktree branch with diff stats")
                             .icon(IconName::ZedAgent)
                             .worktrees(vec![ThreadItemWorktreeInfo {
-                                name: "zed".into(),
+                                worktree_name: Some("zed".into()),
                                 full_path: "/projects/zed".into(),
                                 highlight_positions: Vec::new(),
                                 kind: WorktreeKind::Main,
@@ -773,80 +753,176 @@ impl Component for ThreadItem {
                     .into_any_element(),
             ),
             single_example(
-                "Selected Item",
+                "Long Worktree Name (truncation)",
                 container()
                     .child(
-                        ThreadItem::new("ti-6", "Refine textarea interaction behavior")
-                            .icon(IconName::AiGemini)
-                            .timestamp("45m")
-                            .selected(true),
+                        ThreadItem::new("ti-5f", "Thread with a very long worktree name")
+                            .icon(IconName::AiClaude)
+                            .worktrees(vec![ThreadItemWorktreeInfo {
+                                worktree_name: Some(
+                                    "very-long-worktree-name-that-should-truncate".into(),
+                                ),
+                                full_path: "/worktrees/very-long-worktree-name/zed".into(),
+                                highlight_positions: Vec::new(),
+                                kind: WorktreeKind::Linked,
+                                branch_name: None,
+                            }])
+                            .timestamp("1h"),
                     )
                     .into_any_element(),
             ),
             single_example(
-                "Focused Item (Keyboard Selection)",
+                "Worktree with Search Highlights",
                 container()
                     .child(
-                        ThreadItem::new("ti-7", "Implement keyboard navigation")
+                        ThreadItem::new("ti-5g", "Filtered thread with highlighted worktree")
                             .icon(IconName::AiClaude)
-                            .timestamp("12h")
-                            .focused(true),
+                            .worktrees(vec![ThreadItemWorktreeInfo {
+                                worktree_name: Some("jade-glen".into()),
+                                full_path: "/worktrees/jade-glen/zed".into(),
+                                highlight_positions: vec![0, 1, 2, 3],
+                                kind: WorktreeKind::Linked,
+                                branch_name: Some("fix-scrolling".into()),
+                            }])
+                            .timestamp("3d"),
                     )
                     .into_any_element(),
             ),
             single_example(
-                "Selected + Focused",
+                "Multiple Worktrees (no branches)",
                 container()
                     .child(
-                        ThreadItem::new("ti-8", "Active and keyboard-focused thread")
-                            .icon(IconName::AiGemini)
-                            .timestamp("2mo")
-                            .selected(true)
-                            .focused(true),
+                        ThreadItem::new("ti-5h", "Thread spanning multiple worktrees")
+                            .icon(IconName::AiClaude)
+                            .worktrees(vec![
+                                ThreadItemWorktreeInfo {
+                                    worktree_name: Some("jade-glen".into()),
+                                    full_path: "/worktrees/jade-glen/zed".into(),
+                                    highlight_positions: Vec::new(),
+                                    kind: WorktreeKind::Linked,
+                                    branch_name: None,
+                                },
+                                ThreadItemWorktreeInfo {
+                                    worktree_name: Some("fawn-otter".into()),
+                                    full_path: "/worktrees/fawn-otter/zed-slides".into(),
+                                    highlight_positions: Vec::new(),
+                                    kind: WorktreeKind::Linked,
+                                    branch_name: None,
+                                },
+                            ])
+                            .timestamp("2h"),
                     )
                     .into_any_element(),
             ),
             single_example(
-                "Hovered with Action Slot",
+                "Multiple Worktrees with Branches",
                 container()
                     .child(
-                        ThreadItem::new("ti-9", "Hover to see action button")
-                            .icon(IconName::AiClaude)
-                            .timestamp("6h")
-                            .hovered(true)
-                            .action_slot(
-                                IconButton::new("delete", IconName::Trash)
-                                    .icon_size(IconSize::Small)
-                                    .icon_color(Color::Muted),
-                            ),
+                        ThreadItem::new("ti-5i", "Multi-root with per-worktree branches")
+                            .icon(IconName::ZedAgent)
+                            .worktrees(vec![
+                                ThreadItemWorktreeInfo {
+                                    worktree_name: Some("jade-glen".into()),
+                                    full_path: "/worktrees/jade-glen/zed".into(),
+                                    highlight_positions: Vec::new(),
+                                    kind: WorktreeKind::Linked,
+                                    branch_name: Some("fix".into()),
+                                },
+                                ThreadItemWorktreeInfo {
+                                    worktree_name: Some("fawn-otter".into()),
+                                    full_path: "/worktrees/fawn-otter/zed-slides".into(),
+                                    highlight_positions: Vec::new(),
+                                    kind: WorktreeKind::Linked,
+                                    branch_name: Some("main".into()),
+                                },
+                            ])
+                            .timestamp("15m"),
                     )
                     .into_any_element(),
             ),
             single_example(
-                "Search Highlight",
+                "Project Name + Worktree + Branch",
                 container()
                     .child(
-                        ThreadItem::new("ti-10", "Implement keyboard navigation")
+                        ThreadItem::new("ti-5j", "Thread with project context")
                             .icon(IconName::AiClaude)
-                            .timestamp("4w")
-                            .highlight_positions(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
+                            .project_name("my-remote-server")
+                            .worktrees(vec![ThreadItemWorktreeInfo {
+                                worktree_name: Some("jade-glen".into()),
+                                full_path: "/worktrees/jade-glen/zed".into(),
+                                highlight_positions: Vec::new(),
+                                kind: WorktreeKind::Linked,
+                                branch_name: Some("feature-branch".into()),
+                            }])
+                            .timestamp("1d"),
                     )
                     .into_any_element(),
             ),
             single_example(
-                "Worktree Search Highlight",
+                "Project Paths + Worktree (archive view)",
                 container()
                     .child(
-                        ThreadItem::new("ti-11", "Search in worktree name")
+                        ThreadItem::new("ti-5k", "Archived thread with folder paths")
                             .icon(IconName::AiClaude)
-                            .timestamp("3mo")
+                            .project_paths(Arc::from(vec![
+                                PathBuf::from("/projects/zed"),
+                                PathBuf::from("/projects/zed-slides"),
+                            ]))
                             .worktrees(vec![ThreadItemWorktreeInfo {
-                                name: "my-project-name".into(),
-                                full_path: "my-project-name".into(),
-                                highlight_positions: vec![3, 4, 5, 6, 7, 8, 9, 10, 11],
+                                worktree_name: Some("jade-glen".into()),
+                                full_path: "/worktrees/jade-glen/zed".into(),
+                                highlight_positions: Vec::new(),
                                 kind: WorktreeKind::Linked,
-                                branch_name: None,
-                            }]),
+                                branch_name: Some("feature".into()),
+                            }])
+                            .timestamp("2mo"),
+                    )
+                    .into_any_element(),
+            ),
+            single_example(
+                "All Metadata",
+                container()
+                    .child(
+                        ThreadItem::new("ti-5l", "Thread with every metadata field populated")
+                            .icon(IconName::ZedAgent)
+                            .project_name("remote-dev")
+                            .worktrees(vec![ThreadItemWorktreeInfo {
+                                worktree_name: Some("my-worktree".into()),
+                                full_path: "/worktrees/my-worktree/zed".into(),
+                                highlight_positions: Vec::new(),
+                                kind: WorktreeKind::Linked,
+                                branch_name: Some("main".into()),
+                            }])
+                            .added(15)
+                            .removed(4)
+                            .timestamp("8h"),
+                    )
+                    .into_any_element(),
+            ),
+            single_example(
+                "Focused Item (Keyboard Selection)",
+                container()
+                    .child(
+                        ThreadItem::new("ti-7", "Implement keyboard navigation")
+                            .icon(IconName::AiClaude)
+                            .timestamp("12h")
+                            .focused(true),
+                    )
+                    .into_any_element(),
+            ),
+            single_example(
+                "Action Slot",
+                container()
+                    .child(
+                        ThreadItem::new("ti-9", "Hover to see action button")
+                            .icon(IconName::AiClaude)
+                            .timestamp("6h")
+                            .hovered(true)
+                            .action_slot(
+                                IconButton::new("delete", IconName::Trash)
+                                    .icon_size(IconSize::Small)
+                                    .icon_color(Color::Muted),
+                            ),
                     )
                     .into_any_element(),
             ),

crates/ui/src/components/context_menu.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{
-    IconButtonShape, KeyBinding, List, ListItem, ListSeparator, ListSubHeader, Tooltip, prelude::*,
-    utils::WithRemSize,
+    ButtonCommon, ButtonStyle, IconButtonShape, KeyBinding, List, ListItem, ListSeparator,
+    ListSubHeader, Tooltip, prelude::*, utils::WithRemSize,
 };
 use gpui::{
     Action, AnyElement, App, Bounds, Corner, DismissEvent, Entity, EventEmitter, FocusHandle,
@@ -680,6 +680,17 @@ impl ContextMenu {
         self
     }
 
+    pub fn selectable(mut self, selectable: bool) -> Self {
+        if let Some(ContextMenuItem::CustomEntry {
+            selectable: entry_selectable,
+            ..
+        }) = self.items.last_mut()
+        {
+            *entry_selectable = selectable;
+        }
+        self
+    }
+
     pub fn label(mut self, label: impl Into<SharedString>) -> Self {
         self.items.push(ContextMenuItem::Label(label.into()));
         self
@@ -1968,6 +1979,7 @@ impl ContextMenu {
                             el.end_slot({
                                 let icon_button = IconButton::new("end-slot-icon", *icon)
                                     .shape(IconButtonShape::Square)
+                                    .style(ButtonStyle::Subtle)
                                     .tooltip({
                                         let action_context = self.action_context.clone();
                                         let title = title.clone();

crates/ui/src/components/icon.rs πŸ”—

@@ -113,6 +113,7 @@ impl From<IconName> for Icon {
 }
 
 /// The source of an icon.
+#[derive(Clone)]
 enum IconSource {
     /// An SVG embedded in the Zed binary.
     Embedded(SharedString),
@@ -126,7 +127,7 @@ enum IconSource {
     ExternalSvg(SharedString),
 }
 
-#[derive(IntoElement, RegisterComponent)]
+#[derive(Clone, IntoElement, RegisterComponent)]
 pub struct Icon {
     source: IconSource,
     color: Color,

crates/ui/src/components/icon/icon_decoration.rs πŸ”—

@@ -18,8 +18,6 @@ pub enum KnockoutIconName {
     DotBg,
     TriangleFg,
     TriangleBg,
-    ArchiveFg,
-    ArchiveBg,
 }
 
 impl KnockoutIconName {
@@ -35,7 +33,6 @@ pub enum IconDecorationKind {
     X,
     Dot,
     Triangle,
-    Archive,
 }
 
 impl IconDecorationKind {
@@ -44,7 +41,6 @@ impl IconDecorationKind {
             Self::X => KnockoutIconName::XFg,
             Self::Dot => KnockoutIconName::DotFg,
             Self::Triangle => KnockoutIconName::TriangleFg,
-            Self::Archive => KnockoutIconName::ArchiveFg,
         }
     }
 
@@ -53,7 +49,6 @@ impl IconDecorationKind {
             Self::X => KnockoutIconName::XBg,
             Self::Dot => KnockoutIconName::DotBg,
             Self::Triangle => KnockoutIconName::TriangleBg,
-            Self::Archive => KnockoutIconName::ArchiveBg,
         }
     }
 }

crates/workspace/src/dock.rs πŸ”—

@@ -338,6 +338,15 @@ pub struct PanelButtons {
 
 pub(crate) const PANEL_SIZE_STATE_KEY: &str = "dock_panel_size";
 
+fn panel_uses_flexible_width(
+    position: DockPosition,
+    panel: &dyn PanelHandle,
+    window: &Window,
+    cx: &App,
+) -> bool {
+    position.axis() == Axis::Horizontal && panel.has_flexible_size(window, cx)
+}
+
 fn resize_panel_entry(
     position: DockPosition,
     entry: &mut PanelEntry,
@@ -347,8 +356,8 @@ fn resize_panel_entry(
     cx: &mut App,
 ) -> (&'static str, PanelSizeState) {
     let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
-    let use_flex = entry.panel.has_flexible_size(window, cx) && position.axis() == Axis::Horizontal;
-    if use_flex {
+    let uses_flexible_width = panel_uses_flexible_width(position, entry.panel.as_ref(), window, cx);
+    if uses_flexible_width {
         entry.size_state.flex = flex;
     } else {
         entry.size_state.size = size;
@@ -960,11 +969,31 @@ impl Dock {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let size_states_to_persist: Vec<_> = self
-            .panel_entries
-            .iter_mut()
-            .map(|entry| resize_panel_entry(self.position, entry, size, flex, window, cx))
-            .collect();
+        let Some(active_panel_index) = self.active_panel_index else {
+            return;
+        };
+
+        let active_panel_uses_flexible_width = {
+            let Some(active_entry) = self.panel_entries.get(active_panel_index) else {
+                return;
+            };
+            panel_uses_flexible_width(self.position, active_entry.panel.as_ref(), window, cx)
+        };
+        let mut size_states_to_persist = Vec::new();
+        for entry in &mut self.panel_entries {
+            if panel_uses_flexible_width(self.position, entry.panel.as_ref(), window, cx)
+                == active_panel_uses_flexible_width
+            {
+                size_states_to_persist.push(resize_panel_entry(
+                    self.position,
+                    entry,
+                    size,
+                    flex,
+                    window,
+                    cx,
+                ));
+            }
+        }
 
         let workspace = self.workspace.clone();
         cx.defer(move |cx| {

crates/workspace/src/multi_workspace.rs πŸ”—

@@ -3,8 +3,8 @@ use fs::Fs;
 
 use gpui::{
     AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
-    ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, WindowId,
-    actions, deferred, px,
+    ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, WeakEntity, Window,
+    WindowId, actions, deferred, px,
 };
 pub use project::ProjectGroupKey;
 use project::{DisableAiSettings, Project};
@@ -50,10 +50,6 @@ actions!(
         NextThread,
         /// Activates the previous thread in sidebar order.
         PreviousThread,
-        /// Expands the thread list for the current project to show more threads.
-        ShowMoreThreads,
-        /// Collapses the thread list for the current project to show fewer threads.
-        ShowFewerThreads,
         /// Creates a new thread in the current workspace.
         NewThread,
         /// Moves the active project to a new window.
@@ -272,20 +268,18 @@ pub struct ProjectGroup {
     pub key: ProjectGroupKey,
     pub workspaces: Vec<Entity<Workspace>>,
     pub expanded: bool,
-    pub visible_thread_count: Option<usize>,
 }
 
 pub struct SerializedProjectGroupState {
     pub key: ProjectGroupKey,
     pub expanded: bool,
-    pub visible_thread_count: Option<usize>,
 }
 
 #[derive(Clone)]
 pub struct ProjectGroupState {
     pub key: ProjectGroupKey,
     pub expanded: bool,
-    pub visible_thread_count: Option<usize>,
+    pub last_active_workspace: Option<WeakEntity<Workspace>>,
 }
 
 pub struct MultiWorkspace {
@@ -641,7 +635,7 @@ impl MultiWorkspace {
             ProjectGroupState {
                 key,
                 expanded: true,
-                visible_thread_count: None,
+                last_active_workspace: None,
             },
         );
     }
@@ -720,6 +714,26 @@ impl MultiWorkspace {
         cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
     }
 
+    pub(crate) fn activate_provisional_workspace(
+        &mut self,
+        workspace: Entity<Workspace>,
+        provisional_key: ProjectGroupKey,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if workspace != self.active_workspace {
+            self.register_workspace(&workspace, window, cx);
+        }
+
+        self.ensure_project_group_state(provisional_key);
+        if !self.is_workspace_retained(&workspace) {
+            self.retained_workspaces.push(workspace.clone());
+        }
+
+        self.activate(workspace.clone(), window, cx);
+        cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
+    }
+
     fn register_workspace(
         &mut self,
         workspace: &Entity<Workspace>,
@@ -757,12 +771,7 @@ impl MultiWorkspace {
         _cx: &mut Context<Self>,
     ) {
         let mut restored: Vec<ProjectGroupState> = Vec::new();
-        for SerializedProjectGroupState {
-            key,
-            expanded,
-            visible_thread_count,
-        } in groups
-        {
+        for SerializedProjectGroupState { key, expanded } in groups {
             if key.path_list().paths().is_empty() {
                 continue;
             }
@@ -772,7 +781,7 @@ impl MultiWorkspace {
             restored.push(ProjectGroupState {
                 key,
                 expanded,
-                visible_thread_count,
+                last_active_workspace: None,
             });
         }
         for existing in std::mem::take(&mut self.project_groups) {
@@ -802,7 +811,6 @@ impl MultiWorkspace {
                     .cloned()
                     .collect(),
                 expanded: group.expanded,
-                visible_thread_count: group.visible_thread_count,
             })
             .collect()
     }
@@ -811,6 +819,17 @@ impl MultiWorkspace {
         self.derived_project_groups(cx)
     }
 
+    pub fn last_active_workspace_for_group(
+        &self,
+        key: &ProjectGroupKey,
+        cx: &App,
+    ) -> Option<Entity<Workspace>> {
+        let group = self.project_groups.iter().find(|g| g.key == *key)?;
+        let weak = group.last_active_workspace.as_ref()?;
+        let workspace = weak.upgrade()?;
+        (workspace.read(cx).project_group_key(cx) == *key).then_some(workspace)
+    }
+
     pub fn group_state_by_key(&self, key: &ProjectGroupKey) -> Option<&ProjectGroupState> {
         self.project_groups.iter().find(|group| group.key == *key)
     }
@@ -830,12 +849,6 @@ impl MultiWorkspace {
         }
     }
 
-    pub fn set_all_groups_visible_thread_count(&mut self, count: Option<usize>) {
-        for group in &mut self.project_groups {
-            group.visible_thread_count = count;
-        }
-    }
-
     pub fn workspaces_for_project_group(
         &self,
         key: &ProjectGroupKey,
@@ -856,6 +869,105 @@ impl MultiWorkspace {
         })
     }
 
+    pub fn close_workspace(
+        &mut self,
+        workspace: &Entity<Workspace>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<bool>> {
+        let group_key = workspace.read(cx).project_group_key(cx);
+        let excluded_workspace = workspace.clone();
+
+        self.remove(
+            [workspace.clone()],
+            move |this, window, cx| {
+                if let Some(workspace) = this
+                    .workspaces_for_project_group(&group_key, cx)
+                    .unwrap_or_default()
+                    .into_iter()
+                    .find(|candidate| candidate != &excluded_workspace)
+                {
+                    return Task::ready(Ok(workspace));
+                }
+
+                let current_group_index = this
+                    .project_groups
+                    .iter()
+                    .position(|group| group.key == group_key);
+
+                if let Some(current_group_index) = current_group_index {
+                    for distance in 1..this.project_groups.len() {
+                        for neighboring_index in [
+                            current_group_index.checked_add(distance),
+                            current_group_index.checked_sub(distance),
+                        ]
+                        .into_iter()
+                        .flatten()
+                        {
+                            let Some(neighboring_group) =
+                                this.project_groups.get(neighboring_index)
+                            else {
+                                continue;
+                            };
+
+                            if let Some(workspace) = this
+                                .last_active_workspace_for_group(&neighboring_group.key, cx)
+                                .or_else(|| {
+                                    this.workspaces_for_project_group(&neighboring_group.key, cx)
+                                        .unwrap_or_default()
+                                        .into_iter()
+                                        .find(|candidate| candidate != &excluded_workspace)
+                                })
+                            {
+                                return Task::ready(Ok(workspace));
+                            }
+                        }
+                    }
+                }
+
+                let neighboring_group_key = current_group_index.and_then(|index| {
+                    this.project_groups
+                        .get(index + 1)
+                        .or_else(|| {
+                            index
+                                .checked_sub(1)
+                                .and_then(|previous| this.project_groups.get(previous))
+                        })
+                        .map(|group| group.key.clone())
+                });
+
+                if let Some(neighboring_group_key) = neighboring_group_key {
+                    return this.find_or_create_local_workspace(
+                        neighboring_group_key.path_list().clone(),
+                        Some(neighboring_group_key),
+                        std::slice::from_ref(&excluded_workspace),
+                        None,
+                        OpenMode::Activate,
+                        window,
+                        cx,
+                    );
+                }
+
+                let app_state = this.workspace().read(cx).app_state().clone();
+                let project = Project::local(
+                    app_state.client.clone(),
+                    app_state.node_runtime.clone(),
+                    app_state.user_store.clone(),
+                    app_state.languages.clone(),
+                    app_state.fs.clone(),
+                    None,
+                    project::LocalProjectFlags::default(),
+                    cx,
+                );
+                let new_workspace =
+                    cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
+                Task::ready(Ok(new_workspace))
+            },
+            window,
+            cx,
+        )
+    }
+
     pub fn remove_project_group(
         &mut self,
         group_key: &ProjectGroupKey,
@@ -1066,13 +1178,40 @@ impl MultiWorkspace {
                 )
             });
 
+            let effective_paths_vec =
+                if let Some(project_group) = provisional_project_group_key.as_ref() {
+                    let resolve_tasks = cx.update(|cx| {
+                        let project = new_project.read(cx);
+                        paths_vec
+                            .iter()
+                            .map(|path| project.resolve_abs_path(&path.to_string_lossy(), cx))
+                            .collect::<Vec<_>>()
+                    });
+                    let resolved = futures::future::join_all(resolve_tasks).await;
+                    // `resolve_abs_path` returns `None` for both "definitely
+                    // absent" and transport errors (it swallows the error via
+                    // `log_err`). This is a weaker guarantee than the local
+                    // `Ok(None)` check, but it matches how the rest of the
+                    // codebase consumes this API.
+                    let all_paths_missing =
+                        !paths_vec.is_empty() && resolved.iter().all(|resolved| resolved.is_none());
+
+                    if all_paths_missing {
+                        project_group.path_list().paths().to_vec()
+                    } else {
+                        paths_vec
+                    }
+                } else {
+                    paths_vec
+                };
+
             let window_handle =
                 window_handle.ok_or_else(|| anyhow::anyhow!("Window is not a MultiWorkspace"))?;
 
             open_remote_project_with_existing_connection(
                 connection_options,
                 new_project,
-                paths_vec,
+                effective_paths_vec,
                 app_state,
                 window_handle,
                 provisional_project_group_key,
@@ -1238,6 +1377,11 @@ impl MultiWorkspace {
 
         self.active_workspace = workspace;
 
+        let active_key = self.active_workspace.read(cx).project_group_key(cx);
+        if let Some(group) = self.project_groups.iter_mut().find(|g| g.key == active_key) {
+            group.last_active_workspace = Some(self.active_workspace.downgrade());
+        }
+
         if !self.sidebar_open && !old_active_was_retained {
             self.detach_workspace(&old_active_workspace, cx);
         }
@@ -1288,6 +1432,17 @@ impl MultiWorkspace {
     fn detach_workspace(&mut self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
         self.retained_workspaces
             .retain(|retained| retained != workspace);
+        for group in &mut self.project_groups {
+            if group
+                .last_active_workspace
+                .as_ref()
+                .and_then(WeakEntity::upgrade)
+                .as_ref()
+                == Some(workspace)
+            {
+                group.last_active_workspace = None;
+            }
+        }
         cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
         workspace.update(cx, |workspace, _cx| {
             workspace.session_id.take();
@@ -1329,7 +1484,6 @@ impl MultiWorkspace {
                                 crate::persistence::model::SerializedProjectGroup::from_group(
                                     &group.key,
                                     group.expanded,
-                                    group.visible_thread_count,
                                 )
                             })
                             .collect::<Vec<_>>(),
@@ -1471,7 +1625,6 @@ impl MultiWorkspace {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test_expand_all_groups(&mut self) {
         self.set_all_groups_expanded(true);
-        self.set_all_groups_visible_thread_count(Some(10_000));
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -1527,7 +1680,7 @@ impl MultiWorkspace {
         self.project_groups.push(ProjectGroupState {
             key: group.key,
             expanded: group.expanded,
-            visible_thread_count: group.visible_thread_count,
+            last_active_workspace: None,
         });
     }
 
@@ -1647,13 +1800,13 @@ impl MultiWorkspace {
         let fallback_task = removing_active.then(|| fallback_workspace(self, window, cx));
 
         cx.spawn_in(window, async move |this, cx| {
-            // Prompt each workspace for unsaved changes. If any workspace
-            // has dirty buffers, save_all_internal will emit Activate to
-            // bring it into view before showing the save dialog.
+            // Run the standard workspace close lifecycle for every workspace
+            // being removed from this window. This handles save prompting and
+            // session cleanup consistently with other replace-in-window flows.
             for workspace in &workspaces {
                 let should_continue = workspace
                     .update_in(cx, |workspace, window, cx| {
-                        workspace.save_all_internal(crate::SaveIntent::Close, window, cx)
+                        workspace.prepare_to_close(CloseIntent::ReplaceWindow, window, cx)
                     })?
                     .await?;
 

crates/workspace/src/multi_workspace_tests.rs πŸ”—

@@ -524,6 +524,98 @@ async fn test_find_or_create_local_workspace_reuses_active_workspace_after_sideb
     });
 }
 
+#[gpui::test]
+async fn test_close_workspace_prefers_already_loaded_neighboring_workspace(
+    cx: &mut TestAppContext,
+) {
+    init_test(cx);
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree("/root_a", json!({ "file_a.txt": "" })).await;
+    fs.insert_tree("/root_b", json!({ "file_b.txt": "" })).await;
+    fs.insert_tree("/root_c", json!({ "file_c.txt": "" })).await;
+    let project_a = Project::test(fs.clone(), ["/root_a".as_ref()], cx).await;
+    let project_b = Project::test(fs.clone(), ["/root_b".as_ref()], cx).await;
+    let project_b_key = project_b.read_with(cx, |project, cx| project.project_group_key(cx));
+    let project_c = Project::test(fs, ["/root_c".as_ref()], cx).await;
+    let project_c_key = project_c.read_with(cx, |project, cx| project.project_group_key(cx));
+
+    let (multi_workspace, cx) =
+        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
+
+    multi_workspace.update(cx, |multi_workspace, cx| {
+        multi_workspace.open_sidebar(cx);
+    });
+    cx.run_until_parked();
+
+    let workspace_a = multi_workspace.read_with(cx, |multi_workspace, _cx| {
+        multi_workspace.workspace().clone()
+    });
+    let workspace_b = multi_workspace.update_in(cx, |multi_workspace, window, cx| {
+        multi_workspace.test_add_workspace(project_b, window, cx)
+    });
+
+    multi_workspace.update_in(cx, |multi_workspace, window, cx| {
+        multi_workspace.activate(workspace_a.clone(), window, cx);
+        multi_workspace.test_add_project_group(ProjectGroup {
+            key: project_c_key.clone(),
+            workspaces: Vec::new(),
+            expanded: true,
+        });
+    });
+
+    multi_workspace.read_with(cx, |multi_workspace, _cx| {
+        let keys = multi_workspace.project_group_keys();
+        assert_eq!(
+            keys.len(),
+            3,
+            "expected three project groups in the test setup"
+        );
+        assert_eq!(keys[0], project_b_key);
+        assert_eq!(
+            keys[1],
+            workspace_a.read_with(cx, |workspace, cx| { workspace.project_group_key(cx) })
+        );
+        assert_eq!(keys[2], project_c_key);
+        assert_eq!(
+            multi_workspace.workspace().entity_id(),
+            workspace_a.entity_id(),
+            "workspace A should be active before closing"
+        );
+    });
+
+    let closed = multi_workspace
+        .update_in(cx, |multi_workspace, window, cx| {
+            multi_workspace.close_workspace(&workspace_a, window, cx)
+        })
+        .await
+        .expect("closing the active workspace should succeed");
+
+    assert!(
+        closed,
+        "close_workspace should report that it removed a workspace"
+    );
+
+    multi_workspace.read_with(cx, |multi_workspace, cx| {
+        assert_eq!(
+            multi_workspace.workspace().entity_id(),
+            workspace_b.entity_id(),
+            "closing workspace A should activate the already-loaded workspace B instead of opening group C"
+        );
+        assert_eq!(
+            multi_workspace.workspaces().count(),
+            1,
+            "only workspace B should remain loaded after closing workspace A"
+        );
+        assert!(
+            multi_workspace
+                .workspaces_for_project_group(&project_c_key, cx)
+                .unwrap_or_default()
+                .is_empty(),
+            "the unloaded neighboring group C should remain unopened"
+        );
+    });
+}
+
 #[gpui::test]
 async fn test_switching_projects_with_sidebar_closed_detaches_old_active_workspace(
     cx: &mut TestAppContext,
@@ -575,44 +667,52 @@ async fn test_switching_projects_with_sidebar_closed_detaches_old_active_workspa
 }
 
 #[gpui::test]
-async fn test_remote_worktree_without_git_updates_project_group(cx: &mut TestAppContext) {
+async fn test_remote_project_root_dir_changes_update_groups(cx: &mut TestAppContext) {
     init_test(cx);
     let fs = FakeFs::new(cx.executor());
-    fs.insert_tree("/local", json!({ "file.txt": "" })).await;
-    let project = Project::test(fs.clone(), ["/local".as_ref()], cx).await;
+    fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
+    fs.insert_tree("/local_b", json!({ "file.txt": "" })).await;
+    let project_a = Project::test(fs.clone(), ["/root_a".as_ref()], cx).await;
+    let project_b = Project::test(fs.clone(), ["/local_b".as_ref()], cx).await;
 
     let (multi_workspace, cx) =
-        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
 
     multi_workspace.update(cx, |mw, cx| {
         mw.open_sidebar(cx);
     });
     cx.run_until_parked();
 
-    let initial_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
+    let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
+        let workspace = cx.new(|cx| Workspace::test_new(project_b.clone(), window, cx));
+        let key = workspace.read(cx).project_group_key(cx);
+        mw.activate_provisional_workspace(workspace.clone(), key, window, cx);
+        workspace
+    });
+    cx.run_until_parked();
+
+    multi_workspace.read_with(cx, |mw, _cx| {
+        assert_eq!(
+            mw.workspace().entity_id(),
+            workspace_b.entity_id(),
+            "registered workspace should become active"
+        );
+    });
+
+    let initial_key = project_b.read_with(cx, |p, cx| p.project_group_key(cx));
     multi_workspace.read_with(cx, |mw, _cx| {
         let keys = mw.project_group_keys();
-        assert_eq!(keys.len(), 1);
-        assert_eq!(keys[0], initial_key);
+        assert!(
+            keys.contains(&initial_key),
+            "project groups should contain the initial key for the registered workspace"
+        );
     });
 
-    // Add a remote worktree without git repo info.
-    let remote_worktree = project.update(cx, |project, cx| {
+    let remote_worktree = project_b.update(cx, |project, cx| {
         project.add_test_remote_worktree("/remote/project", cx)
     });
     cx.run_until_parked();
 
-    // The remote worktree has no entries yet, so project_group_key should
-    // still exclude it.
-    let key_after_add = project.read_with(cx, |p, cx| p.project_group_key(cx));
-    assert_eq!(
-        key_after_add, initial_key,
-        "remote worktree without entries should not affect the group key"
-    );
-
-    // Send an UpdateWorktree to the remote worktree with entries but no repo.
-    // This triggers UpdatedRootRepoCommonDir on the first update (the fix),
-    // which propagates through WorktreeStore β†’ Project β†’ MultiWorkspace.
     let worktree_id = remote_worktree.read_with(cx, |wt, _| wt.id().to_proto());
     remote_worktree.update(cx, |worktree, _cx| {
         worktree
@@ -649,17 +749,21 @@ async fn test_remote_worktree_without_git_updates_project_group(cx: &mut TestApp
     });
     cx.run_until_parked();
 
-    let updated_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
+    let updated_key = project_b.read_with(cx, |p, cx| p.project_group_key(cx));
     assert_ne!(
         initial_key, updated_key,
-        "adding a remote worktree should change the project group key"
+        "remote worktree update should change the project group key"
     );
 
     multi_workspace.read_with(cx, |mw, _cx| {
         let keys = mw.project_group_keys();
         assert!(
             keys.contains(&updated_key),
-            "should contain the updated key; got {keys:?}"
+            "project groups should contain the updated key after remote change; got {keys:?}"
+        );
+        assert!(
+            !keys.contains(&initial_key),
+            "project groups should no longer contain the stale initial key; got {keys:?}"
         );
     });
 }

crates/workspace/src/pane_group.rs πŸ”—

@@ -98,8 +98,8 @@ impl PaneGroup {
         }
     }
 
-    pub fn width_fraction_for_pane(&self, pane: &Entity<Pane>) -> Option<f32> {
-        self.root.width_fraction_for_pane(pane)
+    pub fn full_height_column_count(&self) -> usize {
+        self.root.full_height_column_count()
     }
 
     pub fn pane_at_pixel_position(&self, coordinate: Point<Pixels>) -> Option<&Entity<Pane>> {
@@ -307,10 +307,10 @@ impl Member {
         }
     }
 
-    fn width_fraction_for_pane(&self, pane: &Entity<Pane>) -> Option<f32> {
+    fn full_height_column_count(&self) -> usize {
         match self {
-            Member::Pane(found) => (found == pane).then_some(1.0),
-            Member::Axis(axis) => axis.width_fraction_for_pane(pane),
+            Member::Pane(_) => 1,
+            Member::Axis(axis) => axis.full_height_column_count(),
         }
     }
 }
@@ -901,38 +901,21 @@ impl PaneAxis {
         None
     }
 
-    fn width_fraction_for_pane(&self, pane: &Entity<Pane>) -> Option<f32> {
-        let flexes = self.flexes.lock();
-        let total_flex = flexes.iter().copied().sum::<f32>();
-
-        for (index, member) in self.members.iter().enumerate() {
-            let child_fraction = if total_flex > 0.0 {
-                flexes[index] / total_flex
-            } else {
-                1.0 / self.members.len() as f32
-            };
-
-            match member {
-                Member::Pane(found) => {
-                    if found == pane {
-                        return Some(match self.axis {
-                            Axis::Horizontal => child_fraction,
-                            Axis::Vertical => 1.0,
-                        });
-                    }
-                }
-                Member::Axis(axis) => {
-                    if let Some(descendant_fraction) = axis.width_fraction_for_pane(pane) {
-                        return Some(match self.axis {
-                            Axis::Horizontal => child_fraction * descendant_fraction,
-                            Axis::Vertical => descendant_fraction,
-                        });
-                    }
-                }
-            }
+    fn full_height_column_count(&self) -> usize {
+        match self.axis {
+            Axis::Horizontal => self
+                .members
+                .iter()
+                .map(Member::full_height_column_count)
+                .sum::<usize>()
+                .max(1),
+            Axis::Vertical => self
+                .members
+                .iter()
+                .map(Member::full_height_column_count)
+                .max()
+                .unwrap_or(1),
         }
-
-        None
     }
 
     fn render(

crates/workspace/src/persistence/model.rs πŸ”—

@@ -66,8 +66,6 @@ pub struct SerializedProjectGroup {
     pub(crate) location: SerializedWorkspaceLocation,
     #[serde(default = "default_expanded")]
     pub expanded: bool,
-    #[serde(default)]
-    pub visible_thread_count: Option<usize>,
 }
 
 fn default_expanded() -> bool {
@@ -75,11 +73,7 @@ fn default_expanded() -> bool {
 }
 
 impl SerializedProjectGroup {
-    pub fn from_group(
-        key: &ProjectGroupKey,
-        expanded: bool,
-        visible_thread_count: Option<usize>,
-    ) -> Self {
+    pub fn from_group(key: &ProjectGroupKey, expanded: bool) -> Self {
         Self {
             path_list: key.path_list().serialize(),
             location: match key.host() {
@@ -87,7 +81,6 @@ impl SerializedProjectGroup {
                 None => SerializedWorkspaceLocation::Local,
             },
             expanded,
-            visible_thread_count,
         }
     }
 
@@ -100,7 +93,6 @@ impl SerializedProjectGroup {
         SerializedProjectGroupState {
             key: ProjectGroupKey::new(host, path_list),
             expanded: self.expanded,
-            visible_thread_count: self.visible_thread_count,
         }
     }
 }

crates/workspace/src/toast_layer.rs πŸ”—

@@ -44,6 +44,10 @@ pub fn init(cx: &mut App) {
 
 pub trait ToastView: ManagedView {
     fn action(&self) -> Option<ToastAction>;
+
+    fn auto_dismiss(&self) -> bool {
+        true
+    }
 }
 
 #[derive(Clone)]
@@ -131,6 +135,7 @@ impl ToastLayer {
         V: ToastView,
     {
         let action = new_toast.read(cx).action();
+        let auto_dismiss = new_toast.read(cx).auto_dismiss();
         let focus_handle = cx.focus_handle();
 
         self.active_toast = Some(ActiveToast {
@@ -143,7 +148,9 @@ impl ToastLayer {
             focus_handle,
         });
 
-        self.start_dismiss_timer(DEFAULT_TOAST_DURATION, cx);
+        if auto_dismiss {
+            self.start_dismiss_timer(DEFAULT_TOAST_DURATION, cx);
+        }
 
         cx.notify();
     }

crates/workspace/src/workspace.rs πŸ”—

@@ -33,9 +33,9 @@ pub use dock::Panel;
 pub use multi_workspace::{
     CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MoveProjectToNewWindow,
     MultiWorkspace, MultiWorkspaceEvent, NewThread, NextProject, NextThread, PreviousProject,
-    PreviousThread, ProjectGroup, ProjectGroupKey, SerializedProjectGroupState, ShowFewerThreads,
-    ShowMoreThreads, Sidebar, SidebarEvent, SidebarHandle, SidebarRenderState, SidebarSide,
-    ToggleWorkspaceSidebar, sidebar_side_context_menu,
+    PreviousThread, ProjectGroup, ProjectGroupKey, SerializedProjectGroupState, Sidebar,
+    SidebarEvent, SidebarHandle, SidebarRenderState, SidebarSide, ToggleWorkspaceSidebar,
+    sidebar_side_context_menu,
 };
 pub use path_list::{PathList, SerializedPathList};
 pub use remote::{
@@ -2302,18 +2302,19 @@ impl Workspace {
                 return None;
             }
             let flex = flex.max(0.001);
+            let center_column_count = self.center_full_height_column_count();
             let opposite = self.opposite_dock_panel_and_size_state(position, window, cx);
             if let Some(opposite_flex) = opposite.as_ref().and_then(|(_, s)| s.flex) {
-                // Both docks are flex items sharing the full workspace width.
-                let total_flex = flex + 1.0 + opposite_flex;
+                let total_flex = flex + center_column_count + opposite_flex;
                 return Some((flex / total_flex * workspace_width).max(RESIZE_HANDLE_SIZE));
             } else {
-                // Opposite dock is fixed-width; flex items share (W - fixed).
                 let opposite_fixed = opposite
                     .map(|(panel, s)| s.size.unwrap_or_else(|| panel.default_size(window, cx)))
                     .unwrap_or_default();
                 let available = (workspace_width - opposite_fixed).max(RESIZE_HANDLE_SIZE);
-                return Some((flex / (flex + 1.0) * available).max(RESIZE_HANDLE_SIZE));
+                return Some(
+                    (flex / (flex + center_column_count) * available).max(RESIZE_HANDLE_SIZE),
+                );
             }
         }
 
@@ -2340,17 +2341,18 @@ impl Workspace {
             return None;
         }
 
+        let center_column_count = self.center_full_height_column_count();
         let opposite = self.opposite_dock_panel_and_size_state(position, window, cx);
         if let Some(opposite_flex) = opposite.as_ref().and_then(|(_, s)| s.flex) {
             let size = size.clamp(px(0.), workspace_width - px(1.));
-            Some((size * (1.0 + opposite_flex) / (workspace_width - size)).max(0.0))
+            Some((size * (center_column_count + opposite_flex) / (workspace_width - size)).max(0.0))
         } else {
             let opposite_width = opposite
                 .map(|(panel, s)| s.size.unwrap_or_else(|| panel.default_size(window, cx)))
                 .unwrap_or_default();
             let available = (workspace_width - opposite_width).max(RESIZE_HANDLE_SIZE);
             let remaining = (available - size).max(px(1.));
-            Some((size / remaining).max(0.0))
+            Some((size * center_column_count / remaining).max(0.0))
         }
     }
 
@@ -2377,13 +2379,16 @@ impl Workspace {
         Some((panel.clone(), size_state))
     }
 
+    fn center_full_height_column_count(&self) -> f32 {
+        self.center.full_height_column_count().max(1) as f32
+    }
+
     pub fn default_dock_flex(&self, position: DockPosition) -> Option<f32> {
         if position.axis() != Axis::Horizontal {
             return None;
         }
 
-        let pane = self.last_active_center_pane.clone()?.upgrade()?;
-        Some(self.center.width_fraction_for_pane(&pane).unwrap_or(1.0))
+        Some(1.0)
     }
 
     pub fn is_edited(&self) -> bool {
@@ -7484,7 +7489,7 @@ impl Workspace {
                     None
                 };
                 if let Some(grow) = flex_grow {
-                    let grow = grow.max(0.001);
+                    let grow = (grow / self.center_full_height_column_count()).max(0.001);
                     let style = container.style();
                     style.flex_grow = Some(grow);
                     style.flex_shrink = Some(1.0);
@@ -8802,11 +8807,7 @@ pub async fn apply_restored_multiworkspace_state(
         // stale keys from previous sessions get normalized and deduped.
         let mut resolved_groups: Vec<SerializedProjectGroupState> = Vec::new();
         for serialized in project_groups.iter().cloned() {
-            let SerializedProjectGroupState {
-                key,
-                expanded,
-                visible_thread_count,
-            } = serialized.into_restored_state();
+            let SerializedProjectGroupState { key, expanded } = serialized.into_restored_state();
             if key.path_list().paths().is_empty() {
                 continue;
             }
@@ -8827,7 +8828,6 @@ pub async fn apply_restored_multiworkspace_state(
                 resolved_groups.push(SerializedProjectGroupState {
                     key: resolved,
                     expanded,
-                    visible_thread_count,
                 });
             }
         }
@@ -9938,9 +9938,15 @@ async fn open_remote_project_inner(
         });
 
         if let Some(project_group_key) = provisional_project_group_key.clone() {
-            multi_workspace.retain_workspace(new_workspace.clone(), project_group_key, cx);
+            multi_workspace.activate_provisional_workspace(
+                new_workspace.clone(),
+                project_group_key,
+                window,
+                cx,
+            );
+        } else {
+            multi_workspace.activate(new_workspace.clone(), window, cx);
         }
-        multi_workspace.activate(new_workspace.clone(), window, cx);
         new_workspace
     })?;
 
@@ -12749,74 +12755,40 @@ mod tests {
         let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
 
         workspace.update(cx, |workspace, _cx| {
-            workspace.bounds.size.width = px(800.);
+            workspace.set_random_database_id();
         });
 
         workspace.update_in(cx, |workspace, window, cx| {
             let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
-            workspace.add_panel(panel, window, cx);
+            workspace.add_panel(panel.clone(), window, cx);
             workspace.toggle_dock(DockPosition::Right, window, cx);
-        });
-
-        let (panel, resized_width, ratio_basis_width) =
-            workspace.update_in(cx, |workspace, window, cx| {
-                let item = cx.new(|cx| {
-                    TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
-                });
-                workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
-
-                let dock = workspace.right_dock().read(cx);
-                let workspace_width = workspace.bounds.size.width;
-                let initial_width = workspace
-                    .dock_size(&dock, window, cx)
-                    .expect("flexible dock should have an initial width");
-
-                assert_eq!(initial_width, workspace_width / 2.);
 
-                workspace.resize_right_dock(px(300.), window, cx);
-
-                let dock = workspace.right_dock().read(cx);
-                let resized_width = workspace
-                    .dock_size(&dock, window, cx)
-                    .expect("flexible dock should keep its resized width");
-
-                assert_eq!(resized_width, px(300.));
-
-                let panel = workspace
-                    .right_dock()
-                    .read(cx)
-                    .visible_panel()
-                    .expect("flexible dock should have a visible panel")
-                    .panel_id();
-
-                (panel, resized_width, workspace_width)
+            let right_dock = workspace.right_dock().clone();
+            right_dock.update(cx, |dock, cx| {
+                dock.set_panel_size_state(
+                    &panel,
+                    dock::PanelSizeState {
+                        size: None,
+                        flex: Some(1.0),
+                    },
+                    cx,
+                );
             });
+        });
 
         workspace.update_in(cx, |workspace, window, cx| {
-            workspace.toggle_dock(DockPosition::Right, window, cx);
-            workspace.toggle_dock(DockPosition::Right, window, cx);
+            let item = cx.new(|cx| {
+                TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
+            });
+            workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
+            workspace.bounds.size.width = px(1920.);
 
             let dock = workspace.right_dock().read(cx);
-            let reopened_width = workspace
+            let initial_width = workspace
                 .dock_size(&dock, window, cx)
-                .expect("flexible dock should restore when reopened");
-
-            assert_eq!(reopened_width, resized_width);
+                .expect("flexible dock should have an initial width");
 
-            let right_dock = workspace.right_dock().read(cx);
-            let flexible_panel = right_dock
-                .visible_panel()
-                .expect("flexible dock should still have a visible panel");
-            assert_eq!(flexible_panel.panel_id(), panel);
-            assert_eq!(
-                right_dock
-                    .stored_panel_size_state(flexible_panel.as_ref())
-                    .and_then(|size_state| size_state.flex),
-                Some(
-                    resized_width.to_f64() as f32
-                        / (workspace.bounds.size.width - resized_width).to_f64() as f32
-                )
-            );
+            assert_eq!(initial_width, px(960.));
         });
 
         workspace.update_in(cx, |workspace, window, cx| {
@@ -12827,25 +12799,16 @@ mod tests {
                 cx,
             );
 
-            let dock = workspace.right_dock().read(cx);
-            let split_width = workspace
-                .dock_size(&dock, window, cx)
-                .expect("flexible dock should keep its user-resized proportion");
+            let center_column_count = workspace.center.full_height_column_count();
+            assert_eq!(center_column_count, 2);
 
-            assert_eq!(split_width, px(300.));
+            let dock = workspace.right_dock().read(cx);
+            assert_eq!(workspace.dock_size(&dock, window, cx).unwrap(), px(640.));
 
-            workspace.bounds.size.width = px(1600.);
+            workspace.bounds.size.width = px(2400.);
 
             let dock = workspace.right_dock().read(cx);
-            let resized_window_width = workspace
-                .dock_size(&dock, window, cx)
-                .expect("flexible dock should preserve proportional size on window resize");
-
-            assert_eq!(
-                resized_window_width,
-                workspace.bounds.size.width
-                    * (resized_width.to_f64() as f32 / ratio_basis_width.to_f64() as f32)
-            );
+            assert_eq!(workspace.dock_size(&dock, window, cx).unwrap(), px(800.));
         });
     }
 
@@ -13017,9 +12980,31 @@ mod tests {
             );
         });
 
-        // Step 2: Split the center pane vertically (top/bottom). Vertical splits do not
-        // change horizontal width fractions, so the flexible panel stays at the same
-        // width as each half of the split.
+        // Step 2: Split the center pane left/right. The flexible panel is treated as one
+        // average center column, so with two center columns it should take one third of
+        // the workspace width.
+        workspace.update_in(cx, |workspace, window, cx| {
+            workspace.split_pane(
+                workspace.active_pane().clone(),
+                SplitDirection::Right,
+                window,
+                cx,
+            );
+
+            let left_dock = workspace.left_dock().read(cx);
+            let left_width = workspace
+                .dock_size(&left_dock, window, cx)
+                .expect("left dock should still have an active panel after horizontal split");
+
+            assert_eq!(
+                left_width,
+                workspace.bounds.size.width / 3.,
+                "flexible left panel width should match the average center column width"
+            );
+        });
+
+        // Step 3: Split the active center pane vertically (top/bottom). Vertical splits do
+        // not change the number of center columns, so the flexible panel width stays the same.
         workspace.update_in(cx, |workspace, window, cx| {
             workspace.split_pane(
                 workspace.active_pane().clone(),
@@ -13035,14 +13020,14 @@ mod tests {
 
             assert_eq!(
                 left_width,
-                workspace.bounds.size.width / 2.,
-                "flexible left panel width should match each vertically-split pane"
+                workspace.bounds.size.width / 3.,
+                "flexible left panel width should still match the average center column width"
             );
         });
 
-        // Step 3: Open a fixed-width panel in the right dock. The right dock's default
-        // size reduces the available width, so the flexible left panel and the center
-        // panes all shrink proportionally to accommodate it.
+        // Step 4: Open a fixed-width panel in the right dock. The right dock's default
+        // size reduces the available width, so the flexible left panel keeps matching one
+        // average center column within the remaining space.
         workspace.update_in(cx, |workspace, window, cx| {
             let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 200, cx));
             workspace.add_panel(panel, window, cx);
@@ -13061,14 +13046,14 @@ mod tests {
             let available_width = workspace.bounds.size.width - right_width;
             assert_eq!(
                 left_width,
-                available_width / 2.,
-                "flexible left panel should shrink proportionally as the right dock takes space"
+                available_width / 3.,
+                "flexible left panel should keep matching one average center column"
             );
         });
 
-        // Step 4: Toggle the right dock's panel to flexible. Now both docks use
-        // flex sizing and the workspace width is divided among left-flex, center
-        // (implicit flex 1.0), and right-flex.
+        // Step 5: Toggle the right dock's panel to flexible. Now both docks use
+        // column-equivalent flex sizing and the workspace width is divided among
+        // left-flex, two center columns, and right-flex.
         workspace.update_in(cx, |workspace, window, cx| {
             let right_dock = workspace.right_dock().clone();
             let right_panel = right_dock
@@ -13110,8 +13095,9 @@ mod tests {
             let left_flex = workspace
                 .default_dock_flex(DockPosition::Left)
                 .expect("left dock should have a default flex");
+            let center_column_count = workspace.center.full_height_column_count() as f32;
 
-            let total_flex = left_flex + 1.0 + right_flex;
+            let total_flex = left_flex + center_column_count + right_flex;
             let expected_left = left_flex / total_flex * workspace.bounds.size.width;
             let expected_right = right_flex / total_flex * workspace.bounds.size.width;
             assert_eq!(

crates/zed/src/main.rs πŸ”—

@@ -555,6 +555,7 @@ fn main() {
         debugger_ui::init(cx);
         debugger_tools::init(cx);
         client::init(&client, cx);
+        feature_flags::FeatureFlagStore::init(cx);
 
         let system_id = cx.foreground_executor().block_on(system_id).ok();
         let installation_id = cx.foreground_executor().block_on(installation_id).ok();

crates/zed/src/visual_test_runner.rs πŸ”—

@@ -2914,7 +2914,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
                         .icon(IconName::AiClaude)
                         .timestamp("5m")
                         .worktrees(vec![ThreadItemWorktreeInfo {
-                            name: "jade-glen".into(),
+                            worktree_name: Some("jade-glen".into()),
                             full_path: "/worktrees/jade-glen/zed".into(),
                             highlight_positions: Vec::new(),
                             kind: WorktreeKind::Linked,
@@ -2931,7 +2931,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
                         .icon(IconName::AiClaude)
                         .timestamp("1h")
                         .worktrees(vec![ThreadItemWorktreeInfo {
-                            name: "focal-arrow".into(),
+                            worktree_name: Some("focal-arrow".into()),
                             full_path: "/worktrees/focal-arrow/zed".into(),
                             highlight_positions: Vec::new(),
                             kind: WorktreeKind::Linked,
@@ -2946,7 +2946,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
                         .icon(IconName::ZedAgent)
                         .timestamp("2d")
                         .worktrees(vec![ThreadItemWorktreeInfo {
-                            name: "zed".into(),
+                            worktree_name: Some("zed".into()),
                             full_path: "/projects/zed".into(),
                             highlight_positions: Vec::new(),
                             kind: WorktreeKind::Main,
@@ -2963,7 +2963,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
                         .icon(IconName::ZedAgent)
                         .timestamp("3d")
                         .worktrees(vec![ThreadItemWorktreeInfo {
-                            name: "zed".into(),
+                            worktree_name: Some("zed".into()),
                             full_path: "/projects/zed".into(),
                             highlight_positions: Vec::new(),
                             kind: WorktreeKind::Main,
@@ -2978,7 +2978,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
                         .icon(IconName::AiClaude)
                         .timestamp("6d")
                         .worktrees(vec![ThreadItemWorktreeInfo {
-                            name: "stoic-reed".into(),
+                            worktree_name: Some("stoic-reed".into()),
                             full_path: "/worktrees/stoic-reed/zed".into(),
                             highlight_positions: Vec::new(),
                             kind: WorktreeKind::Linked,
@@ -2995,7 +2995,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
                         .icon(IconName::ZedAgent)
                         .timestamp("40m")
                         .worktrees(vec![ThreadItemWorktreeInfo {
-                            name: "focal-arrow".into(),
+                            worktree_name: Some("focal-arrow".into()),
                             full_path: "/worktrees/focal-arrow/zed".into(),
                             highlight_positions: Vec::new(),
                             kind: WorktreeKind::Linked,
@@ -3014,7 +3014,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
                         .added(42)
                         .removed(17)
                         .worktrees(vec![ThreadItemWorktreeInfo {
-                            name: "jade-glen".into(),
+                            worktree_name: Some("jade-glen".into()),
                             full_path: "/worktrees/jade-glen/zed".into(),
                             highlight_positions: Vec::new(),
                             kind: WorktreeKind::Linked,
@@ -3031,7 +3031,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
                         .added(108)
                         .removed(53)
                         .worktrees(vec![ThreadItemWorktreeInfo {
-                            name: "my-project".into(),
+                            worktree_name: Some("my-project".into()),
                             full_path: "/worktrees/my-project/zed".into(),
                             highlight_positions: Vec::new(),
                             kind: WorktreeKind::Linked,
@@ -3052,7 +3052,7 @@ impl gpui::Render for ThreadItemBranchNameTestView {
                         .added(23)
                         .removed(8)
                         .worktrees(vec![ThreadItemWorktreeInfo {
-                            name: "zed".into(),
+                            worktree_name: Some("zed".into()),
                             full_path: "/projects/zed".into(),
                             highlight_positions: Vec::new(),
                             kind: WorktreeKind::Main,

crates/zed/src/zed.rs πŸ”—

@@ -159,8 +159,8 @@ pub fn init(cx: &mut App) {
 
     cx.observe_flag::<PanicFeatureFlag, _>({
         let mut added = false;
-        move |enabled, cx| {
-            if added || !enabled {
+        move |flag, cx| {
+            if added || !*flag {
                 return;
             }
             added = true;