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) |