acp-threads-in-sidebar-plan.md

  1# Plan: Show ACP Threads in the Sidebar (Revised)
  2
  3## Problem
  4
  5The sidebar currently only shows **Zed-native agent threads** (from `ThreadStore`/`ThreadsDatabase`). ACP threads (Claude Code, Codex, Gemini, etc.) are invisible in the sidebar once they're no longer live.
  6
  7## Root Cause
  8
  9`ThreadStore` and `ThreadsDatabase` only persist metadata for native threads. When `rebuild_contents` populates the sidebar, it reads from `ThreadStore` for historical threads and overlays live info from the `AgentPanel` — but non-native threads never get written to `ThreadStore`, so once they stop being live, they disappear.
 10
 11## Solution Overview (Revised)
 12
 13**Key change from the original plan:** We completely remove the sidebar's dependency on `ThreadStore`. Instead, the `Sidebar` itself owns a **single, unified persistence layer** — a new `SidebarDb` domain stored in the workspace DB — that tracks metadata for _all_ thread types (native and ACP). The sidebar becomes the single source of truth for what threads appear in the list.
 14
 15### Why Remove the ThreadStore Dependency?
 16
 171. **Single responsibility** — The sidebar is the only consumer of "which threads to show in the list." Having it depend on `ThreadStore` (which exists primarily for native agent save/load) creates an indirect coupling that makes ACP integration awkward.
 182. **No merge logic** — The original plan required merging native `ThreadStore` data with a separate `AcpThreadMetadataDb` in `ThreadStore::reload`. By moving all sidebar metadata into one place, there's nothing to merge.
 193. **Simpler data flow** — Writers (native agent, ACP connections) push metadata to the sidebar DB. The sidebar reads from one table. No cross-crate coordination needed.
 204. **ThreadStore stays focused**`ThreadStore` continues to manage native thread blob storage (save/load message data) without being polluted with sidebar display concerns.
 21
 22### Architecture
 23
 24```
 25  ┌─────────────────────┐      ┌─────────────────────────┐
 26  │    NativeAgent      │      │   ACP Connections       │
 27  │  (on save_thread)   │      │ (on create/update/list) │
 28  └──────────┬──────────┘      └──────────┬──────────────┘
 29             │                            │
 30             │   save_sidebar_thread()    │
 31             └──────────┬─────────────────┘
 32 33              ┌───────────────────┐
 34              │   SidebarDb       │
 35              │  (workspace DB)   │
 36              │  sidebar_threads  │
 37              └────────┬──────────┘
 38 39 40              ┌───────────────────┐
 41              │     Sidebar       │
 42              │ rebuild_contents  │
 43              └───────────────────┘
 44```
 45
 46---
 47
 48## Step 1: Create `SidebarDb` Domain in `sidebar.rs`
 49
 50**File:** `crates/agent_ui/src/sidebar.rs`
 51
 52Add a `SidebarDb` domain using `db::static_connection!`, co-located in the sidebar module (or a small `persistence` submodule within `sidebar.rs` if it helps organization, but keeping it in the same file is fine for now).
 53
 54### Schema
 55
 56```rust
 57use db::{
 58    sqlez::{
 59        bindable::Column, domain::Domain, statement::Statement,
 60        thread_safe_connection::ThreadSafeConnection,
 61    },
 62    sqlez_macros::sql,
 63};
 64
 65/// Lightweight metadata for any thread (native or ACP), enough to populate
 66/// the sidebar list and route to the correct load path when clicked.
 67#[derive(Debug, Clone)]
 68pub struct SidebarThreadRow {
 69    pub session_id: acp::SessionId,
 70    /// `None` for native Zed threads, `Some("claude-code")` etc. for ACP agents.
 71    pub agent_name: Option<String>,
 72    pub title: SharedString,
 73    pub updated_at: DateTime<Utc>,
 74    pub created_at: Option<DateTime<Utc>>,
 75    pub folder_paths: PathList,
 76}
 77
 78pub struct SidebarDb(ThreadSafeConnection);
 79
 80impl Domain for SidebarDb {
 81    const NAME: &str = stringify!(SidebarDb);
 82
 83    const MIGRATIONS: &[&str] = &[sql!(
 84        CREATE TABLE IF NOT EXISTS sidebar_threads(
 85            session_id TEXT PRIMARY KEY,
 86            agent_name TEXT,
 87            title TEXT NOT NULL,
 88            updated_at TEXT NOT NULL,
 89            created_at TEXT,
 90            folder_paths TEXT,
 91            folder_paths_order TEXT
 92        ) STRICT;
 93    )];
 94}
 95
 96db::static_connection!(SIDEBAR_DB, SidebarDb, []);
 97```
 98
 99### CRUD Methods
100
101```rust
102impl SidebarDb {
103    /// Upsert metadata for a thread (native or ACP).
104    pub async fn save(&self, row: &SidebarThreadRow) -> Result<()> {
105        let id = row.session_id.0.clone();
106        let agent_name = row.agent_name.clone();
107        let title = row.title.to_string();
108        let updated_at = row.updated_at.to_rfc3339();
109        let created_at = row.created_at.map(|dt| dt.to_rfc3339());
110        let serialized = row.folder_paths.serialize();
111        let (fp, fpo) = if row.folder_paths.is_empty() {
112            (None, None)
113        } else {
114            (Some(serialized.paths), Some(serialized.order))
115        };
116
117        self.write(move |conn| {
118            let mut stmt = Statement::prepare(
119                conn,
120                "INSERT INTO sidebar_threads(session_id, agent_name, title, updated_at, created_at, folder_paths, folder_paths_order)
121                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
122                 ON CONFLICT(session_id) DO UPDATE SET
123                     agent_name = excluded.agent_name,
124                     title = excluded.title,
125                     updated_at = excluded.updated_at,
126                     folder_paths = excluded.folder_paths,
127                     folder_paths_order = excluded.folder_paths_order",
128            )?;
129            let mut i = stmt.bind(&id, 1)?;
130            i = stmt.bind(&agent_name, i)?;
131            i = stmt.bind(&title, i)?;
132            i = stmt.bind(&updated_at, i)?;
133            i = stmt.bind(&created_at, i)?;
134            i = stmt.bind(&fp, i)?;
135            stmt.bind(&fpo, i)?;
136            stmt.exec()
137        })
138        .await
139    }
140
141    /// List all sidebar thread metadata, ordered by updated_at descending.
142    pub fn list(&self) -> Result<Vec<SidebarThreadRow>> {
143        self.select::<SidebarThreadRow>(
144            "SELECT session_id, agent_name, title, updated_at, created_at, folder_paths, folder_paths_order
145             FROM sidebar_threads
146             ORDER BY updated_at DESC"
147        )?(())
148    }
149
150    /// List threads for a specific folder path set.
151    pub fn list_for_paths(&self, paths: &PathList) -> Result<Vec<SidebarThreadRow>> {
152        let serialized = paths.serialize();
153        self.select_bound::<String, SidebarThreadRow>(sql!(
154            SELECT session_id, agent_name, title, updated_at, created_at, folder_paths, folder_paths_order
155            FROM sidebar_threads
156            WHERE folder_paths = ?
157            ORDER BY updated_at DESC
158        ))?(serialized.paths)
159    }
160
161    /// Look up a single thread by session ID.
162    pub fn get(&self, session_id: &acp::SessionId) -> Result<Option<SidebarThreadRow>> {
163        let id = session_id.0.clone();
164        self.select_row_bound::<Arc<str>, SidebarThreadRow>(sql!(
165            SELECT session_id, agent_name, title, updated_at, created_at, folder_paths, folder_paths_order
166            FROM sidebar_threads
167            WHERE session_id = ?
168        ))?(id)
169    }
170
171    /// Return the total number of rows in the table.
172    pub fn count(&self) -> Result<usize> {
173        let count: (i32, i32) = self.select_row(sql!(
174            SELECT COUNT(*) FROM sidebar_threads
175        ))?(())?.unwrap_or_default();
176        Ok(count.0 as usize)
177    }
178
179    /// Delete metadata for a single thread.
180    pub async fn delete(&self, session_id: acp::SessionId) -> Result<()> {
181        let id = session_id.0;
182        self.write(move |conn| {
183            let mut stmt = Statement::prepare(
184                conn,
185                "DELETE FROM sidebar_threads WHERE session_id = ?",
186            )?;
187            stmt.bind(&id, 1)?;
188            stmt.exec()
189        })
190        .await
191    }
192
193    /// Delete all thread metadata.
194    pub async fn delete_all(&self) -> Result<()> {
195        self.write(move |conn| {
196            let mut stmt = Statement::prepare(
197                conn,
198                "DELETE FROM sidebar_threads",
199            )?;
200            stmt.exec()
201        })
202        .await
203    }
204}
205```
206
207### `Column` Implementation
208
209```rust
210impl Column for SidebarThreadRow {
211    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
212        let (id, next): (Arc<str>, i32) = Column::column(statement, start_index)?;
213        let (agent_name, next): (Option<String>, i32) = Column::column(statement, next)?;
214        let (title, next): (String, i32) = Column::column(statement, next)?;
215        let (updated_at_str, next): (String, i32) = Column::column(statement, next)?;
216        let (created_at_str, next): (Option<String>, i32) = Column::column(statement, next)?;
217        let (folder_paths_str, next): (Option<String>, i32) = Column::column(statement, next)?;
218        let (folder_paths_order_str, next): (Option<String>, i32) = Column::column(statement, next)?;
219
220        let updated_at = DateTime::parse_from_rfc3339(&updated_at_str)?.with_timezone(&Utc);
221        let created_at = created_at_str
222            .as_deref()
223            .map(DateTime::parse_from_rfc3339)
224            .transpose()?
225            .map(|dt| dt.with_timezone(&Utc));
226
227        let folder_paths = folder_paths_str
228            .map(|paths| {
229                PathList::deserialize(&util::path_list::SerializedPathList {
230                    paths,
231                    order: folder_paths_order_str.unwrap_or_default(),
232                })
233            })
234            .unwrap_or_default();
235
236        Ok((
237            SidebarThreadRow {
238                session_id: acp::SessionId::new(id),
239                agent_name,
240                title: title.into(),
241                updated_at,
242                created_at,
243                folder_paths,
244            },
245            next,
246        ))
247    }
248}
249```
250
251**Key points:**
252
253- `SIDEBAR_DB` is a `LazyLock` static — initialized on first use, no manual connection management.
254- The `agent_name` column is `NULL` for native Zed threads and a string like `"claude-code"` for ACP agents. This replaces the `agent_type` field from the original plan.
255- The DB file lives alongside other `static_connection!` databases.
256- `ThreadsDatabase` and `ThreadStore` are **completely unchanged** by this step.
257
258---
259
260## Step 2: Replace `ThreadStore` Reads in `rebuild_contents` with `SidebarDb` Reads
261
262**File:** `crates/agent_ui/src/sidebar.rs`
263
264### Remove `ThreadStore` Dependency
265
2661. **Remove** `ThreadStore::global(cx)` and `ThreadStore::try_global(cx)` from `Sidebar::new` and `rebuild_contents`.
2672. **Remove** the `cx.observe_in(&thread_store, ...)` subscription that triggers `update_entries` when `ThreadStore` changes.
2683. **Replace** `thread_store.read(cx).threads_for_paths(&path_list)` calls with `SIDEBAR_DB.list_for_paths(&path_list)` (or read all rows once at the top of `rebuild_contents` and index them in memory, which is simpler and avoids repeated DB calls).
269
270### New Data Flow in `rebuild_contents`
271
272```rust
273fn rebuild_contents(&mut self, cx: &App) {
274    // ... existing workspace iteration setup ...
275
276    // Read ALL sidebar thread metadata once, index by folder_paths.
277    let all_sidebar_threads = SIDEBAR_DB.list().unwrap_or_default();
278    let mut threads_by_paths: HashMap<PathList, Vec<SidebarThreadRow>> = HashMap::new();
279    for row in all_sidebar_threads {
280        threads_by_paths
281            .entry(row.folder_paths.clone())
282            .or_default()
283            .push(row);
284    }
285
286    for (ws_index, workspace) in workspaces.iter().enumerate() {
287        // ... existing absorbed-workspace logic ...
288
289        let path_list = workspace_path_list(workspace, cx);
290
291        if should_load_threads {
292            let mut seen_session_ids: HashSet<acp::SessionId> = HashSet::new();
293
294            // Read from SidebarDb instead of ThreadStore
295            if let Some(rows) = threads_by_paths.get(&path_list) {
296                for row in rows {
297                    seen_session_ids.insert(row.session_id.clone());
298                    let (agent, icon) = match &row.agent_name {
299                        None => (Agent::NativeAgent, IconName::ZedAgent),
300                        Some(name) => (
301                            Agent::Custom { name: name.clone().into() },
302                            IconName::ZedAgent, // placeholder, resolved in Step 5
303                        ),
304                    };
305                    threads.push(ThreadEntry {
306                        agent,
307                        session_info: AgentSessionInfo {
308                            session_id: row.session_id.clone(),
309                            cwd: None,
310                            title: Some(row.title.clone()),
311                            updated_at: Some(row.updated_at),
312                            created_at: row.created_at,
313                            meta: None,
314                        },
315                        icon,
316                        icon_from_external_svg: None,
317                        status: AgentThreadStatus::default(),
318                        workspace: ThreadEntryWorkspace::Open(workspace.clone()),
319                        is_live: false,
320                        is_background: false,
321                        highlight_positions: Vec::new(),
322                        worktree_name: None,
323                        worktree_highlight_positions: Vec::new(),
324                        diff_stats: DiffStats::default(),
325                    });
326                }
327            }
328
329            // ... existing linked git worktree logic, also reading from threads_by_paths ...
330            // ... existing live thread overlay logic (unchanged) ...
331        }
332    }
333}
334```
335
336### What Changes
337
338- `rebuild_contents` reads from `SIDEBAR_DB` instead of `ThreadStore`.
339- The `ThreadEntry.agent` field now carries `Agent::Custom { name }` for ACP threads, enabling correct routing in `activate_thread`.
340- The live thread overlay logic (from `all_thread_infos_for_workspace`) is **unchanged** — it still reads from `AgentPanel` to get real-time status of running threads.
341
342### What Stays the Same
343
344- The entire workspace/absorbed-workspace/git-worktree structure.
345- The live thread overlay pass.
346- The notification tracking logic.
347- The search/filter logic.
348
349---
350
351## Step 3: Write Native Thread Metadata to `SidebarDb`
352
353**File:** `crates/agent_ui/src/sidebar.rs` and/or `crates/agent_ui/src/agent_panel.rs`
354
355When a native thread is saved (after conversation, on title update, etc.), we also write its metadata to `SidebarDb`. There are two approaches:
356
357### Option A: Subscribe to `ThreadStore` Changes (Recommended)
358
359Keep a one-directional sync: when `ThreadStore` finishes a `save_thread` or `reload`, the sidebar syncs the metadata to `SidebarDb`. This can be done in the sidebar's workspace subscription or by observing `ThreadStore` changes purely for the purpose of syncing (not for reading).
360
361```rust
362// In Sidebar::subscribe_to_workspace or a dedicated sync method:
363fn sync_native_threads_to_sidebar_db(&self, cx: &App) {
364    if let Some(thread_store) = ThreadStore::try_global(cx) {
365        let entries: Vec<_> = thread_store.read(cx).entries().collect();
366        cx.background_spawn(async move {
367            for meta in entries {
368                SIDEBAR_DB.save(&SidebarThreadRow {
369                    session_id: meta.id,
370                    agent_name: None, // native
371                    title: meta.title,
372                    updated_at: meta.updated_at,
373                    created_at: meta.created_at,
374                    folder_paths: meta.folder_paths,
375                }).await.log_err();
376            }
377        }).detach();
378    }
379}
380```
381
382### Option B: Write at the Point of Save
383
384In `AgentPanel` or wherever `thread_store.save_thread()` is called, also call `SIDEBAR_DB.save(...)`. This is more direct but requires touching more call sites.
385
386**Recommendation:** Option A is simpler for the initial implementation. We observe `ThreadStore` changes, diff against `SidebarDb`, and sync. Later, if we want to remove `ThreadStore` entirely from the write path for native threads, we can switch to Option B.
387
388---
389
390## Step 4: Write ACP Thread Metadata to `SidebarDb`
391
392**File:** `crates/agent_ui/src/connection_view.rs` (or `agent_panel.rs`)
393
394When ACP sessions are created, updated, or listed, write metadata directly to `SidebarDb`:
395
396- **On new session creation:** After `connection.new_session()` returns the `AcpThread`, call `SIDEBAR_DB.save(...)`.
397- **On title update:** ACP threads receive title updates via `SessionInfoUpdate`. When these come in, call `SIDEBAR_DB.save(...)` with the new title and updated timestamp.
398- **On session list refresh:** When `AgentSessionList::list_sessions` returns for an ACP agent, bulk-sync the metadata into `SidebarDb`.
399
400After any write, call `cx.notify()` on the `Sidebar` entity (or use a channel/event) to trigger a `rebuild_contents`.
401
402### Triggering Sidebar Refresh
403
404Since the sidebar no longer observes `ThreadStore`, we need a mechanism to trigger `rebuild_contents` after DB writes. Options:
405
4061. **Emit an event from `AgentPanel`** — The sidebar already subscribes to `AgentPanelEvent`. Add a new variant like `AgentPanelEvent::ThreadMetadataChanged` and emit it after saving to `SidebarDb`.
4072. **Use `cx.notify()` directly** — If the save happens within a `Sidebar` method, just call `self.update_entries(cx)`.
4083. **Observe a lightweight signal entity** — A simple `Entity<()>` that gets notified after DB writes.
409
410**Recommendation:** Option 1 (emit from `AgentPanel`) is cleanest since the sidebar already subscribes to panel events.
411
412---
413
414## Step 5: Handle Agent Icon Resolution for ACP Threads
415
416**File:** `crates/agent_ui/src/sidebar.rs`
417
418For ACP threads in the sidebar, we need the correct agent icon. The `agent_name` string stored in `SidebarDb` maps to an agent in the `AgentServerStore`, which has icon info.
419
420In `rebuild_contents`, after building the initial thread list from `SidebarDb`, resolve icons for ACP threads:
421
422```rust
423// For ACP threads, look up the icon from the agent server store
424if let Some(name) = &row.agent_name {
425    if let Some(agent_server_store) = /* get from workspace */ {
426        // resolve icon from agent_server_store using name
427    }
428}
429```
430
431---
432
433## Step 6: Handle Delete Operations Correctly
434
435**File:** `crates/agent_ui/src/sidebar.rs`
436
437When the user deletes a thread from the sidebar:
438
439- **All threads** → Delete from `SidebarDb` via `SIDEBAR_DB.delete(session_id)`.
440- **Native threads** → _Also_ delete from `ThreadStore`/`ThreadsDatabase` (to clean up the blob data).
441- **ACP threads** → Optionally notify the ACP server via `AgentSessionList::delete_session`.
442
443The `agent_name` field on `SidebarThreadRow` (or the `Agent` enum on `ThreadEntry`) tells us which path to take.
444
445When the user clears all history:
446
447```rust
448// Delete all sidebar metadata
449SIDEBAR_DB.delete_all().await?;
450// Also clear native thread blobs
451thread_store.delete_threads(cx);
452// Optionally notify ACP servers
453```
454
455---
456
457## Step 7: Handle `activate_thread` Routing
458
459**File:** `crates/agent_ui/src/sidebar.rs`, `crates/agent_ui/src/agent_panel.rs`
460
461In `activate_thread`, branch on the `Agent` variant:
462
463- `Agent::NativeAgent` → Call `panel.load_agent_thread(Agent::NativeAgent, session_id, ...)` (current behavior).
464- `Agent::Custom { name }` → Call `panel.load_agent_thread(Agent::Custom { name }, session_id, ...)` so it routes to the correct `AgentConnection::load_session`.
465
466This is already partially set up — `activate_thread` takes an `Agent` parameter. The key change is that `ThreadEntry` now carries the correct `Agent` variant based on `SidebarThreadRow.agent_name`.
467
468---
469
470## Step 8: Handle `activate_archived_thread` Without ThreadStore
471
472**File:** `crates/agent_ui/src/sidebar.rs`
473
474Currently, `activate_archived_thread` looks up `saved_path_list` from `ThreadStore`:
475
476```rust
477let saved_path_list = ThreadStore::try_global(cx).and_then(|thread_store| {
478    thread_store
479        .read(cx)
480        .thread_from_session_id(&session_info.session_id)
481        .map(|thread| thread.folder_paths.clone())
482});
483```
484
485Replace this with a targeted `SidebarDb::get` lookup (single-row SELECT, no full table scan):
486
487```rust
488let saved_path_list = SIDEBAR_DB
489    .get(&session_info.session_id)
490    .ok()
491    .flatten()
492    .map(|row| row.folder_paths);
493```
494
495---
496
497## Step 9: Error Handling for Offline Agents
498
499When an ACP thread is clicked but the agent server is not running:
500
501- Show a toast/notification explaining the agent is offline.
502- Keep the metadata in the sidebar (don't remove it).
503- Optionally offer to start the agent server.
504
505---
506
507## Step 10: Migration — Backfill Existing Native Threads
508
509On first launch after this change, the `SidebarDb` will be empty while `ThreadsDatabase` has existing native threads. We need a one-time backfill:
510
511```rust
512// In Sidebar::new or a dedicated init method:
513fn backfill_native_threads_if_needed(cx: &App) {
514    if SIDEBAR_DB.count()  > 0 {
515        return; // Already populated
516    }
517
518    if let Some(thread_store) = ThreadStore::try_global(cx) {
519        let entries: Vec<_> = thread_store.read(cx).entries().collect();
520        cx.background_spawn(async move {
521            for meta in entries {
522                SIDEBAR_DB.save(&SidebarThreadRow {
523                    session_id: meta.id,
524                    agent_name: None,
525                    title: meta.title,
526                    updated_at: meta.updated_at,
527                    created_at: meta.created_at,
528                    folder_paths: meta.folder_paths,
529                }).await.log_err();
530            }
531        }).detach();
532    }
533}
534```
535
536---
537
538## Summary of Files to Change
539
540| File                                     | Changes                                                                                                                                                                                                                                                        |
541| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
542| `crates/agent_ui/Cargo.toml`             | Add `db.workspace = true`, `sqlez.workspace = true`, `sqlez_macros.workspace = true`, `chrono.workspace = true` dependencies                                                                                                                                   |
543| `crates/agent_ui/src/sidebar.rs`         | **Main changes.** Add `SidebarDb` domain + `SIDEBAR_DB` static + `SidebarThreadRow`. Replace all `ThreadStore` reads in `rebuild_contents` with `SidebarDb` reads. Update `activate_archived_thread`. Add native thread sync logic. Add backfill on first run. |
544| `crates/agent_ui/src/agent_panel.rs`     | Emit `AgentPanelEvent::ThreadMetadataChanged` after thread saves. Potentially write ACP metadata to `SidebarDb` here.                                                                                                                                          |
545| `crates/agent_ui/src/connection_view.rs` | Write ACP metadata to `SidebarDb` on session creation, title updates, and session list refreshes.                                                                                                                                                              |
546
547## What Is NOT Changed
548
549| File / Area                                | Why                                                                                                                          |
550| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
551| `threads` table schema                     | No migration needed — native blob persistence is completely untouched                                                        |
552| `ThreadsDatabase` methods                  | `save_thread_sync`, `load_thread`, `list_threads`, `delete_thread`, `delete_threads` — all unchanged                         |
553| `ThreadStore` struct/methods               | Stays exactly as-is. It's still used for native thread blob save/load. The sidebar just no longer reads from it for display. |
554| `NativeAgent::load_thread` / `open_thread` | These deserialize `DbThread` blobs — completely unaffected                                                                   |
555| `crates/acp_thread/`                       | No new persistence module needed there (unlike the original plan)                                                            |
556| `crates/agent/src/db.rs`                   | `DbThreadMetadata` is unchanged — no `agent_type` field added                                                                |
557
558## Execution Order
559
5601. **SidebarDb domain** (Step 1) — Create `SidebarDb`, `SidebarThreadRow`, `SIDEBAR_DB` static, CRUD methods in `sidebar.rs`.
5612. **Replace reads** (Step 2) — Swap `ThreadStore` reads in `rebuild_contents` for `SidebarDb` reads.
5623. **Native write path** (Step 3) — Sync native thread metadata from `ThreadStore` into `SidebarDb`.
5634. **ACP write path** (Step 4) — Write ACP thread metadata to `SidebarDb` from connection views.
5645. **Icon resolution** (Step 5) — Resolve ACP agent icons in the sidebar.
5656. **Delete path** (Step 6) — Route deletes to `SidebarDb` + native blob cleanup + ACP server notification.
5667. **Activate routing** (Step 7) — Ensure `activate_thread` routes correctly based on `Agent` variant.
5678. **Archive fix** (Step 8) — Update `activate_archived_thread` to use `SidebarDb`.
5689. **Migration** (Step 10) — Backfill existing native threads on first run.
56910. **Polish** (Step 9) — Error handling for offline agents.
570
571## Key Differences from Original Plan
572
573| Aspect                               | Original Plan                                                                              | Revised Plan                                                                    |
574| ------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
575| **Where ACP metadata lives**         | New `AcpThreadMetadataDb` in `crates/acp_thread/`                                          | `SidebarDb` in `crates/agent_ui/src/sidebar.rs`                                 |
576| **Where sidebar reads from**         | `ThreadStore` (which merges native + ACP)                                                  | `SidebarDb` directly (single source)                                            |
577| **ThreadStore changes**              | Added `agent_type` to `DbThreadMetadata`, merge logic in `reload`, new save/delete methods | **None** — ThreadStore is untouched                                             |
578| **`crates/agent/src/db.rs` changes** | Added `agent_type: Option<String>` to `DbThreadMetadata`                                   | **None**                                                                        |
579| **Merge complexity**                 | Two data sources merged in `ThreadStore::reload`                                           | No merge — one table, one read                                                  |
580| **Crate dependencies**               | `acp_thread` gains `db` dependency                                                         | `agent_ui` gains `db` dependency (more natural — it's a UI persistence concern) |