On Linux with client-side decorations, opening the workspace sidebar
squared off the window corners on the sidebar's side while the rest of
the window stayed rounded:
Before:
<img width="366" height="239" alt="image"
src="https://github.com/user-attachments/assets/e0ee8d28-6143-42ea-bfd2-10d069df5492"
/>
After:
<img width="426" height="219" alt="image"
src="https://github.com/user-attachments/assets/51d2979d-0103-4742-90bd-a97f5837d99b"
/>
Previously, `client_side_decorations` took a `border_radius_tiling`
override that `MultiWorkspace` used to deliberately square the window
shape on the sidebar's side, since the sidebar painted a square
background that would otherwise poke out past the rounded window border.
The title bar and status bar already skip rounding their corners on the
sidebar side, implying the sidebar is expected to own those window
corners — it just never rounded them.
This PR makes the sidebar round its outer corners the same way the title
bar and status bar do (including overlapping the 1px window border on
untiled edges to avoid a transparent gap in the rounded corners), and
removes the now-unneeded `border_radius_tiling` parameter so the window
border and backdrop round purely based on actual tiling state.
Only applies when the window uses client-side decorations, so macOS and
Windows are unaffected.
Fixes#54724
Release Notes:
- Fixed square window corners on the sidebar side of client-decorated
(Linux) windows when the workspace sidebar is open.
---------
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Mermaid diagrams with long node labels rendered as an unreadable jumble:
each label was painted as one long line over a box sized for wrapped
text, overflowing into neighboring nodes. With this change, labels wrap
to the box width the layout computed.
**Root cause:** mermaid's default `htmlLabels: true` makes merman emit
labels as HTML inside `<foreignObject>`, which resvg cannot rasterize.
merman's resvg-safe pipeline replaces each one with a single-line
`<text>` fallback that only honors explicit `<br>` breaks, so
soft-wrapped labels collapse onto one long line — while the node boxes
were laid out for the wrapped text.
**Fix:** disable HTML labels in the site config (both the top-level key,
which merman reads for node labels, and `flowchart.htmlLabels`, which it
reads for edge labels). merman then emits native SVG `<text>`/`<tspan>`
labels wrapped to the same measured width the layout used, and the
foreignObject fallback never runs for these diagrams.
One behavior preserved deliberately: a literal `\n` in label text used
to become a line break as a side effect of the foreignObject fallback
(`fallback.rs` converts `\n` when flattening HTML labels).
`render_mermaid` now normalizes `\n` to `<br/>` before rendering so the
native path keeps that behavior; the existing
`backslash_n_converted_to_line_break` test covers it.
Side effect: class diagram member text also switches from the lossy
fallback to native SVG text; the accent-class test was updated
accordingly (accent classes now land directly on `text`/`tspan`
elements).
Verified with `cargo nextest run -p mermaid_render`, `-p markdown`,
`script/clippy -p mermaid_render`, and `cargo fmt --check`.
Release Notes:
- Fixed mermaid diagram labels overflowing and overlapping nodes when
long labels wrap
---------
Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Claude Fable 5 always thinks and cannot honor a request with thinking
disabled, but the cloud models listing gives clients no way to tell it
apart from models where thinking is optional (e.g. Claude Opus 4.6):
both report `supports_thinking: true` plus the same adaptive effort
levels. As a result, the agent panel shows a thinking toggle for Fable
even though turning it off isn't actually supported.
zed-industries/cloud#2789 adds a `supports_disabling_thinking` field to
the models listing. This PR mirrors it through
`cloud_llm_client::LanguageModel` (serde-defaulted to `false`, so a
server without the field is treated as "don't claim thinking can be
turned off") and exposes it as
`LanguageModel::supports_disabling_thinking()`, forwarded from the
listing by `CloudLanguageModel`. The agent panel now hides the thinking
toggle for models that report `false`, showing only the effort selector.
The trait default is `true`: every non-cloud provider in the tree treats
thinking as toggleable today, and only the cloud listing knows about
always-thinking models.
Draft until zed-industries/cloud#2789 lands and deploys.
Release Notes:
- Fixed the agent panel offering a thinking toggle for models that
cannot run with thinking disabled.
Adds two built-in agent tools, behind the staff-only
`create-thread-tool` feature flag, that let the agent spawn independent
sibling threads:
- **`create_thread`** creates a new thread with a title and initial
prompt. It runs on the user's behalf and shows up in the agent sidebar
like a thread the user started themselves, but it runs in the background
— the active thread is never interrupted — and it does **not** report
results back to the caller. (Use `spawn_agent` when you want the
results.)
- **`list_agents_and_models`** enumerates the configured agents and, for
the native agent, the available models, so the agent can pick a
cheaper/faster model for bulk work. The motivating stress test was
"spawn 100 threads that each write a haiku, using Haiku instead of
Opus."
By default the sibling shares the parent's project and worktree. If
`use_new_worktree` is set, `create_thread` instead opens a new
background workspace backed by fresh linked git worktrees of the
project's repos — the same flow you get from the worktree picker's
"Create worktree" — and starts the thread there. The new workspace opens
as a background tab so it doesn't pull you away from what you're doing.
A few notes on the design:
- The `agent` crate can't depend on the UI, so there's a
`SiblingThreadHost` trait that `agent_ui::AgentPanel` implements and
installs on the `NativeAgent`. Tool calls in a native-agent thread route
through it.
- Sibling threads go through the same `create_agent_thread` →
`ConversationView` path as the user-facing "new thread" action, so
eviction, persistence, and sidebar rendering match user-created threads.
- An explicit `agent`/`model` override doesn't clobber the panel's
selected agent or the last-used-agent preference — those are snapshotted
and restored around the call.
- The worktree path reuses the existing "Create worktree" machinery
rather than reimplementing it; the agent flow just opens the new
workspace in the background instead of activating it.
A couple of things I left out or want to call out:
- There's no tool to operate on an already-created sibling by session
id. The success payload deliberately omits it, since the session is
populated asynchronously and nothing consumes it yet.
- When a project has multiple worktrees that are linked worktrees of the
*same* underlying repo, they collapse into a single new worktree (they
resolve to the same target path). The tool returns a warning in that
case rather than failing — and this also fixes a latent collision the
manual "Create worktree" flow would hit in the same situation.
Release Notes:
- N/A
Fixes#54618.
## Summary
After upgrading from `v0.232.x` to `v0.233.5+`, many users reported that
most
of their agent threads "disappeared" from the sidebar. The threads are
still
on disk in the legacy `threads.db` — but they never make it into
`sidebar_threads`, which is the only table the new sidebar UI reads
from, so
they're unreachable from the UI.
The root cause is a race between `ThreadStore::reload` and
`migrate_thread_metadata`:
- `ThreadStore::new` constructs with an empty in-memory `Vec` and kicks
off
`reload()` as a fire-and-forget task.
- `migrate_thread_metadata` runs during `agent_ui::init` and reads
`ThreadStore::global(cx).read(cx).entries()` synchronously, without
awaiting that reload.
- On cold boot the migration observes an empty iterator, early-returns
on
`to_migrate.is_empty()`, and never populates `sidebar_threads`. The
migration runs every launch, so it keeps losing the race forever.
Empirically, the only rows that *did* end up in `sidebar_threads` for
affected users came from `handle_conversation_event` writing rows when
the
user actively interacted with a thread. That's why users would typically
see
a handful of recently-touched threads rather than their full history.
## Fix
1. `ThreadStore` now tracks its current reload as a `Shared<Task<()>>`
and
exposes it via `reload_task()`. `migrate_thread_metadata` awaits this
before reading `entries()`.
2. Move the top-5-per-project unarchive pass out from under the
`is_first_migration` guard. Previously the rescue only ran when
`sidebar_threads` was empty, which meant any user who had even one
pre-existing row (e.g. from interacting with a thread on a prior launch)
got every subsequent batch of migrated threads archived with no rescue.
Running the rescue per-batch is stateless and idempotent — a user who
bounces between older releases and newer ones still gets their top 5 per
project surfaced each time a new batch is migrated.
No new KVP flag or one-shot backfill is needed: because the migration
already dedupes by session_id and runs on every launch, the next cold
boot
on a fixed build picks up any legacy threads that earlier launches
missed.
## Structure
The PR is split into two commits to make the diff easy to verify:
1. **Regression test** — fails on `main`, reproduces the cold-boot race
by
seeding the legacy DB, reinitializing `ThreadStore` to get a fresh empty
cache, and running the migration without parking first.
2. **The fix** — gates entries reads on `reload_task().await`, and
adjusts
the per-batch rescue policy. Updates one existing test's assertions to
match the new per-batch rescue policy.
## Validation
- The regression test fails on `main`, passes after the fix.
- All `agent_ui`, `agent::thread_store`, and `sidebar` lib tests pass.
- `./script/clippy -p agent -p agent_ui` is clean.
- End-to-end repro with a real on-disk data dir: launched `v0.230.2 →
v0.231.2 → v0.232.3 → v0.233.8` as a non-staff user, creating 15 threads
and interacting with one, reproduced the reported state (1 thread in
sidebar, 14 missing). Launched a debug build of this branch against the
same data dir: `Migrating 14 thread store entries` in the log, 15 rows
in
`sidebar_threads` with 6 unarchived (the interacted-with thread plus the
5 most recent), 9 archived and reachable from the archive view.
Release Notes:
- Fixed agent threads appearing to be missing after upgrading to the
parallel-agents sidebar. The thread-metadata migration was racing
against the on-disk thread store's async reload and skipping itself on
every launch. Upgrading to this build runs the migration successfully;
previously-missing threads are surfaced either in the sidebar (5 most
recent per project) or in the archive view.
Starting with Claude Opus 4.7, Anthropic omits thinking content from
responses by default; callers must pass `display: "summarized"` to keep
seeing thinking summaries. Without opting in, the agent UI shows a long
pause with no visible thinking, and users get no progress indication
during extended reasoning.
This extends the adaptive-thinking wire type with an optional `display`
field and requests `Summarized` from every call site that builds an
adaptive thinking request (direct Anthropic, Copilot Chat proxy, Zed
Cloud, and Bedrock).
## Notes
- Applied at the adaptive-thinking layer rather than special-casing Opus
4.7. The `display` parameter is accepted by every
adaptive-thinking-capable model, and the previous behavior (visible
summaries) is what users already see on Opus 4.6 / Sonnet 4.6, so there
is no behavior change for those models.
Release Notes:
- Restored thinking summaries for Claude Opus 4.7.
When soft-wrapping text, `LineWrapper::is_word_char` determines which
characters stick to the preceding word at wrap boundaries. The list
already includes trailing punctuation like `,`, `.`, and `:`, but was
missing `;`. As a result, a message like "we won't pass it on; no big
deal" could wrap with `;` isolated at the start of the next line (`on` /
`; no`).
This was most visible in the agent panel message editor but affects any
surface that uses soft-wrapping.
Also adds `assert_word("on;")` to `test_is_word_char` to guard against
regressions.
Release Notes:
- Fixed trailing `;` being wrapped to a new line instead of staying
attached to the preceding word.
Part of #54531.
Previously the log tap started disabled and only flipped on when someone
opened the ACP logs panel for the first time on a given connection. That
meant the common *'I noticed something is wrong with my agent, let me go
look at the logs'* workflow started with an empty pane \u2014 all the
traffic that led up to the problem was already gone.
This removes the `enabled` flag on `AcpLogTap` so capture is
unconditional. The backlog ring is still bounded at
`MAX_BACKLOG_MESSAGES` (2000), so memory is capped. The steady-state
cost is one channel send per transport/stderr line, which is negligible
compared to the JSON-RPC serialization that already happened to produce
the line.
Net diff is ~40 lines deleted from `acp_tools.rs` \u2014 the flag, its
accessors, the early-return in `emit`, and the `active_tap` bookkeeping
needed to enable it on first subscribe.
I confirmed with the author of the original opt-in design before making
this change.
Release Notes:
- N/A
Fixes a user-reported bug where a thread could be missing from the
sidebar even though it was still present in the metadata store and still
visible in Thread History. The thread reappears in the sidebar only
after the user sends a message.
### Scenario
A single multi-root workspace whose roots are `[/cloud,
/worktrees/zed/wt_a/zed]`, where:
- `/cloud` is a standalone git repo (main == folder).
- `/worktrees/zed/wt_a/zed` is a linked worktree of a separate `/zed`
repo.
The project group normalizes to `main_worktree_paths = [/cloud, /zed]`.
A thread created in this workspace is written with `main=[/cloud, /zed],
folder=[/cloud, /worktrees/zed/wt_a/zed]` and the sidebar finds it via
`entries_for_main_worktree_path`.
If the thread's stored `main_worktree_paths` ever drifts from the group
key — e.g. a stale row loaded from the store on startup, a legacy write,
or a row persisted with `main == folder` — all three existing lookups in
`Sidebar::rebuild_contents` miss:
1. `entries_for_main_worktree_path([/cloud, /zed])` — the thread's
stored main doesn't equal the group key.
2. `entries_for_path([/cloud, /zed])` — the thread's folder paths don't
equal the group key either.
3. The linked-worktree fallback iterates the group workspaces'
`linked_worktrees()` snapshots. Those yield *sibling* linked worktrees
of the repo, not the workspace's own roots, so `/worktrees/zed/wt_a/zed`
doesn't match.
The row falls out of the sidebar entirely even though the metadata is
intact and the thread's folder paths exactly equal the open workspace's
roots. The store heals the stored row on the next `RootThreadUpdated`
event, which is why sending a message makes the row reappear — but until
then the sidebar misrepresents the state.
### Fix
Add a fourth lookup to `Sidebar::rebuild_contents`: for each open
workspace in the group, query the store by the workspace's own root
paths. Any thread whose `folder_paths` matches an open workspace's roots
belongs under that group, regardless of what its `main_worktree_paths`
say.
This covers the gap between stale-row load and store self-heal, matches
the principle that the sidebar should reflect state that exists in a
reasonable way, and is symmetric with the existing lookups (same store
API, one more iterator).
### Commits
1. `sidebar: Add failing repro for thread disappearing from sidebar` —
adds `test_sidebar_keeps_multi_root_thread_with_stale_main_paths` which
reproduces the bug. Sets up the multi-root + linked-worktree layout,
persists a thread in the stale shape, and asserts the row is still
visible in the sidebar.
2. `sidebar: Show threads whose folder paths match an open workspace` —
the fourth-lookup fix. 31 lines in `crates/sidebar/src/sidebar.rs`, no
deletions.
### Verification
- `cargo test -p sidebar`: 103 passed, 0 failed (the new test was
failing before commit 2 and is passing after).
- `./script/clippy -p sidebar`: clean.
### Follow-ups
The three existing lookups in `rebuild_contents` are each covering a
different historical shape the store can be in. A real cleanup — do a
one-shot migration on reload that fills in `main_worktree_paths` for any
row missing it, then retire the two legacy-shape lookups — is worth
doing as a separate PR, but out of scope here.
Release Notes:
- Fixed an issue where agent threads could go missing from the sidebar.
## Summary
Fixes a crash I hit running Zed Preview against a checkout with many
linked git worktrees. The panic was:
```
thread '<unnamed>' panicked at crates/worktree/src/worktree.rs:5334:25:
update_git_repositories: .git path outside worktree root is not a gitfile: "/Users/eric/repo/zed/.git"
```
The `debug_assert!` in `update_git_repositories` was added in #53443 to
catch the case where a `.git` path outside the worktree root is not a
gitfile. The comment there explained that a `.git` outside the root
should always be a gitfile (as in a linked worktree or submodule), so
the assertion was meant to flag "should never happen" paths.
But there's a second legitimate case: after a linked worktree's
repository has been unregistered from `git_repositories` (for example
because its gitfile was removed, or because the filesystem watcher for
the common git dir lost sync and rescan-driven cleanup dropped the
entry), a subsequent rescan event on the main repo's `.git` directory
arrives at the linked worktree's scanner with the common git dir as the
`dot_git_dir`. That path:
- is outside the linked worktree's root (so it doesn't strip-prefix
cleanly), and
- is a real directory (not a gitfile), because it's the main repo's
`.git`.
So the assertion fires, but `continue` is already the right thing to do
— there's simply nothing left for this scanner to do with a path that
isn't its repository anymore.
On macOS, the trigger in practice is the FSEvents API setting
`MustScanSubDirs` / `UserDropped` / `KernelDropped` on an event (which
`notify` surfaces as `need_rescan()`), which our `FsWatcher` converts
into a `PathEvent { path: <watched path>, kind: Rescan }`. Because every
linked worktree registers a watcher on the same shared common git dir,
one kernel drop fans out into many rescan callbacks, and any one of them
hitting a worktree whose repo was just unregistered triggers the panic.
## Changes
- `crates/worktree/src/worktree.rs` — drop the `debug_assert!`, broaden
the comment to cover both cases.
- `crates/worktree/tests/integration/main.rs` — add a failing regression
test that drives the exact sequence (repo unregistered, then a Rescan
event on the common git dir) and asserts it doesn't panic.
The two commits are split so the test commit reproduces the panic on its
own, and the fix commit on top makes it pass.
Release Notes:
- N/A
Adds instrumentation to track input-to-frame latency in GPUI windows,
helping diagnose input responsiveness issues.
## What this does
- Records the time between when an input event is dispatched and when
the resulting frame is presented, capturing worst-case latency when
multiple events are coalesced into a single frame.
- Tracks how many input events get coalesced per rendered frame.
- Both metrics are stored in
[HdrHistogram](https://docs.rs/hdrhistogram) instances with 3
significant digits of precision.
- Latency is only recorded when the input event actually causes a redraw
(i.e. marks the window dirty), so idle mouse moves and other no-op
events don't skew the data.
- Adds a `Dump Input Latency Histogram` command that opens a buffer with
a formatted report including percentile breakdowns and visual
distribution bars.
## Example output
The report shows percentile latencies, a bucketed distribution with bar
charts, and a per-frame event coalescing breakdown.
Release Notes:
- N/A
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Cole Miller <cole@zed.dev>
## Problem
When creating a new agent thread from the sidebar's **+** button (or via
the `NewThread` / `NewThreadInGroup` actions), the panel always returned
the cached draft thread — even if the user had since switched to a
different agent. This meant the new thread would use the wrong agent
(typically the default Zed Agent instead of the last-used external
agent).
The root cause was in `ensure_draft()`: it cached the draft and returned
it unconditionally on subsequent calls without checking whether its
agent
matched the current `selected_agent`.
Meanwhile, **cmd-n from within the agent panel** dispatched
`NewExternalAgentThread`, which calls `create_thread()` — this always
creates a fresh thread with the correct `selected_agent`, so it worked
fine.
## Fix
`ensure_draft()` now compares the cached draft's agent against the
desired agent. When they differ and the draft's editor is empty, the
stale draft is discarded and a new one is created with the correct
agent.
If the draft has user-typed content, it is preserved to avoid losing
work.
## Test
Added `test_draft_replaced_when_selected_agent_changes` which verifies:
1. A draft created with `NativeAgent` gets replaced when
`selected_agent`
changes to a custom agent
2. The replacement draft uses the correct agent
3. Calling `activate_draft` again with the same agent reuses the cached
draft (no unnecessary replacement)
The test fails without the fix (returns the stale `NativeAgent` draft)
and passes with it.
Release Notes:
- N/A
The cross-channel import tests from #54002 hardcoded
`ReleaseChannel::Preview` and `ReleaseChannel::Nightly` as source
channels. When those tests run on a release branch (where the current
channel is Preview or Nightly), the import skips the current channel,
causing test failures.
This adds a `foreign_channels` helper that dynamically picks two
channels that are neither the current one nor Dev, making the tests work
on main, preview, nightly, or any other branch.
Discovered while cherry-picking #54002 to v0.233.x (#54021).
Release Notes:
- N/A
Users have been testing the threads sidebar on Nightly and Preview but
once we release the feature we expect many of these users to move back
to Stable. As it stands right now, they would lose the threads they'd
been working on. This PR adds a way to import these threads.
- [x] Add `agent: import threads from other channels` action to the
command palette.
- This can be always available for users that switch between channels
regularly.
- [x] Add tests for import behavior
- [x] Add a button that disappears after using it to make importing more
discoverable for when users first switch.
Release Notes:
- Added an action to the command palette that imports threads from other
Zed release channels into the current one.
---------
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
The `cli_default_open_behavior` setting controls how `zed <path>` opens
directories, but the underlying behavior is gated behind the
`AgentV2FeatureFlag` in `open_listener.rs`. Without the flag enabled,
the setting has no effect — Zed always falls back to `NewWindow`
behavior.
This change gates the settings UI entry behind the same feature flag so
it only appears when the feature is actually active.
**Changes:**
- Converted `general_settings_section()` from a fixed-size array to a
`Vec` so the CLI setting can be conditionally appended
- Added `feature_flags` as a dependency to `settings_ui`
Release Notes:
- N/A
This fixes a regression where `zed -n .` in a subdirectory of an
already-open
project would redirect to the parent window instead of creating a new
one.
The root cause was that commit b3566fcf55 ("Adjust `zed -n` behavior")
made
`-n` run the worktree matching loop with subdirectory matching enabled,
when
previously `-n` skipped matching entirely.
## Changes
### Bug fix
- **Restore `-n` to always create a new window.** No worktree matching,
no
exceptions. This matches the behavior from when `-n` was first
introduced.
### New `--classic` flag
- Adds a hidden `--classic` CLI flag that explicitly selects the
pre-sidebar
default behavior: new window for directories, reuse existing window for
files already in an open worktree.
- The `cli_default_open_behavior` setting now toggles between `-e` (add
to
sidebar) and `--classic` behavior. When set to `new_window`, the classic
logic is used instead of unconditionally opening a new window.
### Refactor CLI open options
Replaces the old grab-bag of `open_new_workspace: Option<bool>`,
`force_existing_window: bool`, `classic: bool`, and `reuse: bool` with:
- **`cli::CliOpenBehavior` enum** — a single enum on the IPC boundary
with
variants `Default`, `AlwaysNew`, `Add`, `ExistingWindow`, `Classic`, and
`Reuse`.
- **`workspace::WorkspaceMatching` enum** — describes how to match paths
against existing worktrees (`None`, `MatchExact`, `MatchSubdirectory`).
- **`workspace::OpenOptions`** — uses `WorkspaceMatching` plus a simple
`add_dirs_to_sidebar: bool` instead of overlapping boolean flags.
The translation from CLI enum to workspace options happens in
`open_listener.rs`, keeping both layers clean and independent.
Release Notes:
- N/A
Soft-deprecates the `-r`/`--reuse` CLI flag by hiding it from `--help`
output. The flag remains fully functional for backward compatibility,
but new users will discover `-e`/`--existing` instead.
The change is a single `hide = true` addition to the clap arg attribute.
Release Notes:
- N/A
On macOS, the CLI uses `LSOpenFromURLSpec` to deliver the `zed-cli://`
URL to the running Zed app. With `kLSLaunchDefaults`, Launch Services
activates (brings to foreground) the Zed app as a side effect of URL
delivery. This happens before the IPC handshake completes and before the
user can answer the terminal prompt asking whether to open in a new
window or existing workspace, forcing them to manually switch back to
the terminal.
## Fix
**CLI side** (`crates/cli/src/main.rs`): Add `kLSLaunchDontSwitch` to
the launch flags so macOS delivers the URL without activating the app.
**App side** (`crates/zed/src/zed/open_listener.rs`): Add
`cx.activate(true)` in `handle_cli_connection` after the prompt resolves
(or immediately for the URL-only path), so the app comes to the
foreground at the right time.
Linux and Windows are unaffected — they use Unix sockets and named pipes
respectively, which don't have activation side effects.
Release Notes:
- Fixed the Zed CLI activating the app window before the user answers
the open-behavior prompt in the terminal.
Gates the CLI first-run prompt (from #53663) on the `agent-v2` feature
flag, giving us an escape hatch if something goes wrong.
## What this does
**Commit 1: Gate on agent-v2 flag**
- Adds an `AgentV2FeatureFlag` check at the top of the open behavior
resolution. When the flag is off, the prompt is skipped.
- Refactors `maybe_prompt_open_behavior` → `resolve_open_behavior`:
moves the explicit-flags guard (`-n`, `-e`, `--reuse`) to the caller and
drops three parameters from the function signature, since the function
was only inspecting those values to decide whether to bail early.
**Commit 2: Default to new-window when flag is disabled**
- When the flag is off, falls back to the pre-#53254 behavior of opening
projects in a new window, rather than silently adding to the sidebar.
Release Notes:
- N/A
The `user_message_id` parameter on `AgentConnection::prompt` was
`Option<UserMessageId>`, but `NativeAgentConnection::prompt` immediately
called `.expect()` on it, crashing when the value was `None`.
Because the `expect` message said `UserMessageId` was required, this PR
makes the parameter not optional which makes the panic impossible.
This means we also need to unconditionally generate an ID in `send`,
where before we gated this on whether `truncate` returned `Some`.
Generating IDs unconditionally should be find, especially because the
prompt requires them.
Release Notes:
- N/A
This PR adds the `cli_default_open_behavior` setting and a first-run TUI
prompt
that appears when `zed <path>` is invoked without flags while existing
windows are
open and the setting hasn't been configured yet.
## What it does
### Setting and prompt
- Adds a new `cli_default_open_behavior` workspace setting with two
values:
`existing_window` (default) and `new_window`.
- When the user runs `zed <path>` for the first time with existing Zed
windows
open, a `dialoguer::Select` prompt in the CLI asks them to choose their
preferred behavior. The choice is persisted to `settings.json`.
- The prompt is skipped when:
- An explicit flag (`-n`, `-e`, `-a`) is given
- No existing Zed windows are open
- The setting is already configured in `settings.json`
- The paths being opened are already contained in an existing workspace
### IPC transport abstraction
- Introduces a `CliResponseSink` trait in the `cli` crate that abstracts
`IpcSender<CliResponse>`, with an implementation for the real IPC
sender.
- Replaces `IpcSender<CliResponse>` with `Box<dyn CliResponseSink>` /
`&dyn CliResponseSink` across all signatures in `open_listener.rs`:
`OpenRequestKind::CliConnection`, `handle_cli_connection`,
`maybe_prompt_open_behavior`, `open_workspaces`, `open_local_workspace`.
- Extracts the inline CLI response loop from `main.rs` into a testable
`cli::run_cli_response_loop` function.
- Switches the request channel from bounded `mpsc::channel(16)` to
`mpsc::unbounded()`, eliminating `smol::block_on` in the bridge thread.
### End-to-end tests
Seven new tests exercise both the CLI-side response loop and the
Zed-side
handler connected through in-memory channels, using `allow_parking()` so
the
real `cli::run_cli_response_loop` runs on an OS thread while the GPUI
executor
drives the Zed handler:
- No flags, no windows → no prompt, opens new window
- No flags, existing windows, user picks "existing window" → prompt,
setting persisted
- No flags, existing windows, user picks "new window" → prompt, setting
persisted
- Setting already configured → no prompt
- Paths already in existing workspace → no prompt
- Explicit `-e` flag → no prompt
- Explicit `-n` flag → no prompt
Existing tests that previously used `ipc::channel()` now use a
`DiscardResponseSink`, removing OS-level IPC from all tests.
Release Notes:
- Added a first-run prompt when using `zed <path>` to choose between
opening
in an existing window or a new window. The choice is saved to settings
and
can be changed later via the `cli_default_open_behavior` setting.
---------
Co-authored-by: Nathan Sobo <nathan@zed.dev>
When the sidebar opens a remote SSH project, it shows a
`RemoteConnectionModal` on the currently active (local) workspace. After
the connection succeeds and a new remote workspace is created and
activated, the modal on the local workspace was never dismissed.
Switching back to the local workspace (e.g. by activating a thread)
would re-render the local workspace's modal layer, revealing the stale
"Starting proxy..." modal.
Other code paths that show this modal (`recent_projects`, `git_ui`)
already call `modal.finished(cx)` after the connection completes. The
sidebar and agent panel paths were missing this cleanup.
## Changes
- **`remote_connection`**: Added `dismiss_connection_modal()`, a public
utility that finds and dismisses any active `RemoteConnectionModal` on a
given workspace.
- **`sidebar`**: Fixed two call sites (`open_workspace_for_group` and
`open_workspace_and_activate_thread`) to dismiss the modal after the
connection task completes, regardless of success or failure.
- **`agent_ui`**: Fixed `open_worktree_workspace_and_start_thread` to
dismiss the modal after workspace creation completes.
Release Notes:
- (Preview only) Fixed a spurious "Starting proxy..." modal appearing
and hanging when switching back to a local project after opening a
remote SSH project in a multi-project workspace.
Also indicates if the multiworkspace and the worspace disagree on the
right key.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
This PR greatly improves our handling of remote threads in the sidebar.
One primary issue was that many parts of the sidebar were only looking
at a thread's path list and not its remote connection information. The
fix here is to use `ProjectGroupKey` more consistently throughout the
sidebar which also includes remote connection information.
The second major change is to extend the MultiWorkspace with the ability
to initiate the creation of remote workspaces when needed. This involved
refactoring a lot of our remote workspace creation paths to share a
single code path for better consistency.
Release Notes:
- (Preview only) Fixed remote project threads appearing as a separate
local project in the sidebar
---------
Co-authored-by: Anthony Eid <anthony@zed.dev>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
When there's already a workspace that matches the paths provided,
activate that workspace, otherwise create a new window. This is
different from the default behavior of always adding to the sidebar.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
When a project mixes main repo worktrees (e.g. `extensions`) with linked
git worktrees (e.g. `zed4`, a linked worktree of `zed`), two things were
broken:
1. **Threads appeared under the wrong sidebar heading.** A thread
created in a workspace with both `extensions` and `zed4` would show
under the "zed" group instead of the "extensions, zed" group. This
happened because `main_worktree_paths` in `ThreadMetadata` was only
populated with paths from linked worktrees, omitting regular repos
entirely. The fix uses `project_group_key()` to normalize all visible
worktrees to their main repo paths — the same normalization the sidebar
uses for group headers.
2. **The worktree chip tooltip only showed linked worktree paths**,
missing main repo paths like `extensions`. This happened because
`worktree_info_from_thread_paths` filtered out main worktree paths
entirely. The fix introduces a `WorktreeKind` enum (`Main` / `Linked`)
on `ThreadItemWorktreeInfo`, so all worktrees are included in the data
model. Chips still only render for `Linked` worktrees (main worktrees
are redundant with the group header), but the tooltip now shows all
paths.
Release Notes:
- N/A
Changes the default CLI behavior when opening directories:
- `zed foo/` now adds `foo/` as a new workspace in the active window's
MultiWorkspace and opens the sidebar, instead of creating a new window.
If `bar/` was previously open, both `foo/` and `bar/` are retained as
separate workspaces in the sidebar.
- `zed -n foo/` continues to open a new window without the sidebar
(unchanged behavior).
- `zed -a foo/` continues to add to the existing workspace (unchanged
behavior).
The new behavior is gated on `multi_workspace_enabled` (the `agent-v2`
feature flag). Without the flag, directories open in a new window as
before, avoiding a situation where the workspace gets replaced with no
sidebar to switch back.
The change is contained to `open_paths()` in
`crates/workspace/src/workspace.rs`. When no CLI flag is specified and
no existing workspace matches the paths, we now look for the active
window and set `requesting_window` so that `Workspace::new_local` adds
the new workspace to it rather than creating a new window.
Release Notes:
- N/A
This PR adds tracking of project groups to the MultiWorkspace and
serialization/restoration of them. This will later be used by the
sidebar to provide reliable reloading of threads across Zed reloads.
Release Notes:
- N/A
---------
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Splits the tests out of `multi_workspace.rs` into a dedicated
`multi_workspace_tests.rs` file for better organization.
Release Notes:
- N/A
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
When reopening an ACP agent thread (e.g. after restarting Zed) that has
no saved scroll position, the conversation now starts scrolled to the
bottom so the user sees the latest messages instead of starting at the
top.
Uses `set_follow_tail(true)` rather than a one-shot `scroll_to_end()` so
the scroll position is re-anchored on every layout pass — ensuring it
lands at the true bottom even when list items haven't been measured yet.
The follow-tail mode automatically disengages as soon as the user
scrolls up.
Release Notes:
- Fixed agent threads starting scrolled to the top when reopened after
restarting Zed.
Add a debug-only (`cfg(debug_assertions)`) action `DumpWorkspaceInfo`
that opens a read-only buffer with a dump of all workspace state. This
is useful for debugging the sidebar's view of workspaces and threads,
since the sidebar is currently the only way to see workspace-related
state.
For each workspace in the MultiWorkspace it shows:
- **Workspace DB ID** — for cross-referencing with persistence
- **All worktrees** with their paths, branches, and visibility
- **Whether each worktree is a git linked worktree** and where it links
to
- **Active agent thread** with title, session ID, status, and entry
count
- **Background agent threads** with the same detail
- **A warning** if the agent panel's workspace ID doesn't match the
workspace
Example output:
```
MultiWorkspace: 3 workspace(s)
Active workspace index: 1
--- Workspace 0 ---
Workspace DB ID: WorkspaceId(42)
Worktrees:
- /Users/eric/repo/scratch3 [branch: refs/heads/scratch3] [linked worktree -> /Users/eric/repo/scratch]
Active thread: Git Worktree Path Consistency Check (session: 575b4349-...) [idle, 42 entries]
--- Workspace 1 (active) ---
Workspace DB ID: WorkspaceId(57)
Worktrees:
- /Users/eric/repo/worktrees/zed/my-branch/zed [branch: refs/heads/my-branch] [linked worktree -> /Users/eric/repo/zed]
Active thread: Sidebar Not Displaying Git Worktree (session: 8f337c5c-...) [generating, 17 entries, awaiting confirmation]
Background threads (1):
- Previous Investigation (session: abc12345-...) [idle, 83 entries]
--- Workspace 2 ---
Workspace DB ID: WorkspaceId(63)
Worktrees:
- /Users/eric/repo/ex [branch: refs/heads/main]
Active thread: (none)
```
### Implementation
The action and handler live in the `sidebar` crate (close to what they
debug), following the same pattern as `language_tools` owning
`OpenLanguageServerLogs` and `debugger_tools` owning
`OpenDebugAdapterLogs`. The `zed` crate has only a one-line
registration.
Two small public accessors were added to `AgentPanel`:
- `workspace_id()` — exposes the panel's workspace ID for mismatch
detection
- `background_threads()` — exposes retained background conversation
views
Release Notes:
- N/A
Workspaces with no threads weren't being shown in the sidebar which can
make it easy to lose your sense of place. This PR works to address that
by showing these workspaces as a "New Thread" button which also shows
the work trees associated with that workspace.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
Thinking more on #52596, I realized the `entry` method needs a key by
value, which meant we were always cloning a path list even if it was
already in the map. This PR adds an `entry_ref` that takes the key by
reference and delays cloning the key until we know we're going to be
inserting.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
## Summary
When a thread was created in a workspace whose roots span different git
worktrees (e.g. the `olivetti` branch of project_a and the `selectric`
branch of project_b), the sidebar now shows a worktree chip for each
distinct branch name — like `{olivetti} {selectric}` — each with its own
git worktree icon. Same-named worktrees are collapsed into a single
chip. The tooltip on any chip shows the full list of worktree paths.
Previously, only one worktree chip was ever shown (from the first path).
### Implementation
- Introduces `WorktreeInfo` struct replacing the flat
`worktree_name`/`worktree_full_path`/`worktree_highlight_positions`
fields on `ThreadEntry` with `worktrees: Vec<WorktreeInfo>`
- Adds `worktree_info_from_thread_paths` which derives worktree display
info from a thread's own stored `folder_paths` metadata rather than from
the workspace path
- Updates `ThreadItem` in the UI crate to accept
`Vec<ThreadItemWorktree>` and render multiple chips, deduplicating by
name
- Updates fuzzy search to match against all worktree names
- Adds tests for multiple worktree chips and same-name deduplication
Release Notes:
- N/A
This adds a collection that has a map-like API but is backed by vectors.
For small collections, this often outperforms a HashMap because you
don't have the overhead of hashing things and everything is guaranteed
to be contiguous in memory.
I hand-rolled one of these in `ProjectGroupBuilder` but this factors it
into its own collection. This implements the API that
`ProjectGroupBuilder` needed, but if this becomes more widely used we
can expand to include more of the `HashMap` API.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
## Context
With the new sidebar, we are having some bugs around multi-root projects
combined with git work trees that can cause threads to be visible in the
agent panel but not have an entry in the sidebar.
## How to Review
This PR takes a step towards resolving these issue by adding a
`ProjectGroupBuilder` which is responsible for gathering the set of
projects groups from the open workspaces and then helping to discover
threads and map them into this set.
## Self-Review Checklist
<!-- Check before requesting review: -->
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
---------
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Max Brunsfeld <max@zed.dev>
`PathList` stores both sorted paths and the original insertion order
(for display in the project panel). Previously, `PartialEq`, `Eq`, and
`Hash` were derived, which meant two `PathList` values with the same
paths but different display orderings were considered unequal.
This change replaces the derived impls with manual ones that only
compare
the sorted `paths` field, matching the semantic intent: a `PathList`
identifies a set of directories, and the display order is not part of
that identity.
Release Notes:
- N/A
Follow-up to the terminal permission params refactor, addressing
@benbrandt's [review
feedback](https://github.com/zed-industries/zed/pull/51782#pullrequestreview-2926899804)
about string formatting leaking into the UI layer.
## Changes
### Outcome construction moved to acp_thread
- Added `PermissionOptionChoice::build_outcome(is_allow)` — builds a
`SelectedPermissionOutcome` from a choice, attaching
`SelectedPermissionParams::Terminal` when the choice carries
`sub_patterns`.
- Added
`PermissionOptions::build_outcome_for_checked_patterns(checked_indices,
is_allow)` — handles the `DropdownWithPatterns` per-command checklist
flow, returning `None` when zero patterns are checked (degrading to
once).
The UI's `authorize_with_granularity` no longer does any
`format!("always_allow:{}",...)` string formatting or
`SelectedPermissionParams` construction.
### `option_kind` folded into `SelectedPermissionOutcome`
`SelectedPermissionOutcome` now carries `option_kind:
acp::PermissionOptionKind`, eliminating the separate parameter that was
threaded through the entire `authorize_tool_call` chain:
```
ThreadView::authorize_tool_call
→ Conversation::authorize_tool_call
→ AcpThread::authorize_tool_call
```
Every signature in this chain dropped one parameter.
### `SelectedPermissionParams` removed from UI imports
The type is now only referenced by `acp_thread` (construction) and
`agent` (consumption). The UI passes `SelectedPermissionOutcome`
opaquely.
Release Notes:
- N/A
## Summary
Adds a new permission UI for terminal pipeline commands (e.g. `cargo
test | tail`) that lets users selectively always-allow individual
commands in the pipeline, rather than only offering a blanket
always-allow for the first command.
## Screenshot
<img width="464" height="293" alt="Screenshot 2026-03-18 at 3 27 48 PM"
src="https://github.com/user-attachments/assets/e027eeec-c2b3-4f73-a596-95f42a9adad2"
/>
Release notes:
- The terminal permissions UI now allows you to select individual
subcommands independently.
---------
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
When a git worktree is in a detached HEAD state (e.g. after `git
checkout --detach`), the workspace for that worktree would disappear
from the sidebar UI, and if the workspace was currently open your UI
would just disappear.
This happened because `parse_worktrees_from_str` silently dropped any
worktree entries without a `branch` line in the porcelain output, which
cascaded through the `linked_worktrees` list and caused
`prune_stale_worktree_workspaces` to remove the workspace.
This PR:
- Makes `Worktree::ref_name` an `Option<SharedString>` so detached
worktrees can be represented
- Renames `Worktree::branch()` to `Worktree::display_name()`, which
returns the branch name when available or the short SHA as a fallback
(mirroring how the title bar already handles detached HEAD for
repositories)
- Updates `parse_worktrees_from_str` to include detached and bare
worktree entries instead of dropping them
- Filters detached worktrees out of the worktree picker UI, preserving
the existing product decision to only show branch-based worktrees there
## Self-Review Checklist
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
The line wrapper's `is_word_char` function included the ASCII apostrophe
(U+0027) but not the typographic right single quotation mark `'`
(U+2019) or left single quotation mark `'` (U+2018). When Markdown
rendering produces curly quotes, words like `won't` were being split at
the apostrophe during line wrapping, producing `won` at the end of one
line and `'t` at the start of the next.
This adds both typographic quote characters to `is_word_char` so they
are treated the same as the ASCII apostrophe.
Release Notes:
- Fixed line wrapping splitting words at typographic apostrophes (e.g.
"won't" breaking as "won" / "'t").
Before you mark this PR as ready for review, make sure that you have:
- [X] Added a solid test coverage and/or screenshots from doing manual
testing
- [X] Done a self-review taking into account security and performance
aspects
- [X] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
Release Notes:
- N/A
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Add the existing `PasteRaw` action to the right-click context menu in
the message editor, making it more discoverable. Previously it was only
available via the keyboard shortcut (Cmd+Shift+V / Ctrl+Shift+V) or
the command palette.
Release Notes:
- Added "Paste as Plain Text" to the agent panel message editor
right-click menu
Store the user's unsent message editor text in DbThread so it survives
quitting and reloading Zed. The draft flows through Thread → AcpThread →
AcpThreadView on load, and back via a debounced observer on the message
editor for saves.
Currently works for native Zed agents only; external ACP agents will
pick this up once general ACP history persistence lands.
## Changes
- **`DbThread`** / **`Thread`**: New `draft_prompt: Option<String>`
field, included in `to_db()`/`from_db()`
- **`AcpThread`**: Bridge field with getter/setter, populated during
`register_session()`
- **`NativeAgent::save_thread()`**: Copies draft from `AcpThread` →
`Thread` before persisting
- **`AcpThreadView`**: Restores draft into `MessageEditor` on load;
syncs editor text → `AcpThread` via observer; debounced (500ms) Thread
notify triggers DB save
Co-authored-by: Anthony Eid <anthony@zed.dev>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
When a subagent title is long, the header buttons (expand/collapse,
maximize, stop) were pushed out of view because the title area grew
unconstrained.
## Fix
- Add `min_w_0()` and `overflow_hidden()` to the title container so it
can shrink below its content width
- Add `truncate()` to the title label so it shows an ellipsis when
clipped
- Add `flex_shrink_0()` to the buttons container so it never gets
squeezed out
- Add a tooltip on the title area showing the full text on hover
Release Notes:
- N/A
Repro steps:
1. Make a Rust file and check it in to git
2. Delete a lot of the file
3. Expand all the diffs (cmd+")
4. Select larger syntax node (ctrl+cmd+right)
5. Collapse all the diffs
6. Select smaller syntax node (ctrl+cmd+left)
7. Panic!
The problem was we were using byte offsets instead of anchors so after
we shrank the buffer by collapsing diffs we'd be pointing past the end
of it.
Release notes:
- N/A
When clipboard data was produced by the editor's copy/cut with multiple
entire-line selections, vim's paste would panic with `byte index N is
out of bounds`.
The editor's `do_copy` and `cut_common` skip the `\n` separator between
clipboard selections when the previous selection was an entire-line
selection (because the text already ends with `\n`). However, vim's
paste code unconditionally did `start_offset = end_offset + 1`, always
assuming a `\n` separator exists between every pair of selections. This
caused the accumulated offset to exceed the text length, resulting in a
string slicing panic.
The fix checks `clipboard_selection.is_entire_line` to decide whether to
skip the separator, matching the behavior of the editor's own `do_paste`
method. The same fix is applied to both the vim and helix paste
implementations.
Release Notes:
- Fixed a crash when using vim paste on clipboard data copied with the
editor's copy command containing multiple entire-line selections.
Previously, clicking "Always allow for `cargo` commands" after running
`cargo build --release` would also silently permit `cargo run`
(arbitrary code execution), `cargo publish`, and any other cargo
subcommand. This was overly broad and did not match user intent.
Now the extracted pattern includes the subcommand when present, so the
button reads "Always allow for `cargo build` commands" and the pattern
`^cargo\s+build\b` only matches `cargo build` invocations — not `cargo
test`, `cargo run`, etc.
### How it works
- The second token is included in the pattern when it looks like a
subcommand (alphanumeric, hyphens, underscores, no leading `-`).
- When the second token is a flag (e.g. `ls -la`), only the command name
is used — the user sees "Always allow for `ls` commands".
- Single-word commands and path-like commands behave the same as before.
### Examples
| Command | Pattern | Button label |
|---|---|---|
| `cargo build --release` | `^cargo\s+build\b` | Always for `cargo
build` commands |
| `cargo test -p search` | `^cargo\s+test\b` | Always for `cargo test`
commands |
| `npm install` | `^npm\s+install\b` | Always for `npm install` commands
|
| `ls -la` | `^ls\b` | Always for `ls` commands |
| `ls` | `^ls\b` | Always for `ls` commands |
| `./script.sh` | *(rejected)* | *(no pattern button)* |
Release Notes:
- Agent: "Always allow" suggestions for terminal commands are now
subcommand-specific (e.g. "Always allow for `cargo build` commands"
instead of "Always allow for `cargo` commands").
Replace the generic folder icon next to thread entries in the workspace
sidebar with the actual agent icon (Claude, Zed Agent, Gemini, etc.) for
the active thread.
Release Notes:
- N/A
Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
## Bug
**Sentry:**
[ZED-3W3](https://sentry.io/organizations/zed-dev/issues/7085300207/)
(13 events, first seen 2025-12-03)
`summaries_for_anchors` panics with "cannot seek backward" when
iterating selection anchors whose excerpt locators are no longer in
ascending order after an `update_path_excerpts` call.
### Root cause
`update_path_excerpts` merges new excerpt ranges with existing ones.
When an existing excerpt has no overlap with any new range (e.g.
`existing_range.end < new.context.start`), it is removed from the
excerpts tree but is **not** recorded in `replaced_excerpts`. Its stale
locator persists in the `excerpt_ids` tree.
Later, when a new excerpt is inserted for a different path whose locator
falls between the stale locator and another surviving excerpt,
`summaries_for_anchors` can encounter anchors in an order where it needs
to seek the cursor *backward* — violating the forward-only cursor
invariant.
Concretely, the scenario requires:
1. A path with ≥3 excerpts (E_B1, E_B2, E_B3) with anchors in E_B2 and
E_B3.
2. An `update_path_excerpts` call that keeps E_B1, **removes E_B2** (no
overlap → no `replaced_excerpts` entry), and replaces E_B3 with a new
excerpt N.
3. A new path D inserted between paths B and C, whose excerpt E_D gets a
locator between N and E_B2's stale locator.
4. Resolving the old anchors: E_B2's stale locator seeks past E_D, then
E_B3→N tries to seek backward to N — **panic**.
## Fix
In the two branches of `update_path_excerpts` where existing excerpts
are removed without being absorbed into a new range, map the removed
excerpt to the nearest surviving neighbor (the last kept or inserted
excerpt) in `replaced_excerpts`. This ensures stale anchors always remap
to a valid, correctly-ordered locator.
The two branches are:
- `existing_range.end < new.context.start` — existing excerpt falls
entirely before the next new range
- `(None, Some(...))` — no more new ranges for remaining existing
excerpts
## Verification
- A reproduction test
(`test_cannot_seek_backward_after_excerpt_replacement`) that constructs
the exact crash scenario now passes.
- Full `multi_buffer` test suite (48 tests) passes with no regressions.
- Clippy is clean.
Release Notes:
- Fixed a possible crash when updating excerpts in multibuffers
These prompts can be used to automatically diagnose and fix crashes
report in Sentry.
Usage:
1. Find a crash in Sentry. It will have an ID like ZED-123
2. In an agent, do a prompt like `Follow the instructions in
@investigate.md to investigate ZED-123`
3. Once the agent finds a repro, fix it in a new thread by saying
`Follow the instructions in @fix.md`
Release Notes:
- N/A
---------
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>