mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
docs: Clean up old plan document (#51926)
## Context Shouldn't have been committed in the first place. ## 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
This commit is contained in:
parent
99d51a4f3c
commit
6758ac3590
1 changed files with 0 additions and 580 deletions
|
|
@ -1,580 +0,0 @@
|
|||
# Plan: Show ACP Threads in the Sidebar (Revised)
|
||||
|
||||
## Problem
|
||||
|
||||
The 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.
|
||||
|
||||
## Root Cause
|
||||
|
||||
`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.
|
||||
|
||||
## Solution Overview (Revised)
|
||||
|
||||
**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.
|
||||
|
||||
### Why Remove the ThreadStore Dependency?
|
||||
|
||||
1. **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.
|
||||
2. **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.
|
||||
3. **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.
|
||||
4. **ThreadStore stays focused** — `ThreadStore` continues to manage native thread blob storage (save/load message data) without being polluted with sidebar display concerns.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────┐ ┌─────────────────────────┐
|
||||
│ NativeAgent │ │ ACP Connections │
|
||||
│ (on save_thread) │ │ (on create/update/list) │
|
||||
└──────────┬──────────┘ └──────────┬──────────────┘
|
||||
│ │
|
||||
│ save_sidebar_thread() │
|
||||
└──────────┬─────────────────┘
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ SidebarDb │
|
||||
│ (workspace DB) │
|
||||
│ sidebar_threads │
|
||||
└────────┬──────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ Sidebar │
|
||||
│ rebuild_contents │
|
||||
└───────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create `SidebarDb` Domain in `sidebar.rs`
|
||||
|
||||
**File:** `crates/agent_ui/src/sidebar.rs`
|
||||
|
||||
Add 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).
|
||||
|
||||
### Schema
|
||||
|
||||
```rust
|
||||
use db::{
|
||||
sqlez::{
|
||||
bindable::Column, domain::Domain, statement::Statement,
|
||||
thread_safe_connection::ThreadSafeConnection,
|
||||
},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
|
||||
/// Lightweight metadata for any thread (native or ACP), enough to populate
|
||||
/// the sidebar list and route to the correct load path when clicked.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SidebarThreadRow {
|
||||
pub session_id: acp::SessionId,
|
||||
/// `None` for native Zed threads, `Some("claude-code")` etc. for ACP agents.
|
||||
pub agent_name: Option<String>,
|
||||
pub title: SharedString,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub folder_paths: PathList,
|
||||
}
|
||||
|
||||
pub struct SidebarDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for SidebarDb {
|
||||
const NAME: &str = stringify!(SidebarDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE IF NOT EXISTS sidebar_threads(
|
||||
session_id TEXT PRIMARY KEY,
|
||||
agent_name TEXT,
|
||||
title TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
created_at TEXT,
|
||||
folder_paths TEXT,
|
||||
folder_paths_order TEXT
|
||||
) STRICT;
|
||||
)];
|
||||
}
|
||||
|
||||
db::static_connection!(SIDEBAR_DB, SidebarDb, []);
|
||||
```
|
||||
|
||||
### CRUD Methods
|
||||
|
||||
```rust
|
||||
impl SidebarDb {
|
||||
/// Upsert metadata for a thread (native or ACP).
|
||||
pub async fn save(&self, row: &SidebarThreadRow) -> Result<()> {
|
||||
let id = row.session_id.0.clone();
|
||||
let agent_name = row.agent_name.clone();
|
||||
let title = row.title.to_string();
|
||||
let updated_at = row.updated_at.to_rfc3339();
|
||||
let created_at = row.created_at.map(|dt| dt.to_rfc3339());
|
||||
let serialized = row.folder_paths.serialize();
|
||||
let (fp, fpo) = if row.folder_paths.is_empty() {
|
||||
(None, None)
|
||||
} else {
|
||||
(Some(serialized.paths), Some(serialized.order))
|
||||
};
|
||||
|
||||
self.write(move |conn| {
|
||||
let mut stmt = Statement::prepare(
|
||||
conn,
|
||||
"INSERT INTO sidebar_threads(session_id, agent_name, title, updated_at, created_at, folder_paths, folder_paths_order)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||
ON CONFLICT(session_id) DO UPDATE SET
|
||||
agent_name = excluded.agent_name,
|
||||
title = excluded.title,
|
||||
updated_at = excluded.updated_at,
|
||||
folder_paths = excluded.folder_paths,
|
||||
folder_paths_order = excluded.folder_paths_order",
|
||||
)?;
|
||||
let mut i = stmt.bind(&id, 1)?;
|
||||
i = stmt.bind(&agent_name, i)?;
|
||||
i = stmt.bind(&title, i)?;
|
||||
i = stmt.bind(&updated_at, i)?;
|
||||
i = stmt.bind(&created_at, i)?;
|
||||
i = stmt.bind(&fp, i)?;
|
||||
stmt.bind(&fpo, i)?;
|
||||
stmt.exec()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// List all sidebar thread metadata, ordered by updated_at descending.
|
||||
pub fn list(&self) -> Result<Vec<SidebarThreadRow>> {
|
||||
self.select::<SidebarThreadRow>(
|
||||
"SELECT session_id, agent_name, title, updated_at, created_at, folder_paths, folder_paths_order
|
||||
FROM sidebar_threads
|
||||
ORDER BY updated_at DESC"
|
||||
)?(())
|
||||
}
|
||||
|
||||
/// List threads for a specific folder path set.
|
||||
pub fn list_for_paths(&self, paths: &PathList) -> Result<Vec<SidebarThreadRow>> {
|
||||
let serialized = paths.serialize();
|
||||
self.select_bound::<String, SidebarThreadRow>(sql!(
|
||||
SELECT session_id, agent_name, title, updated_at, created_at, folder_paths, folder_paths_order
|
||||
FROM sidebar_threads
|
||||
WHERE folder_paths = ?
|
||||
ORDER BY updated_at DESC
|
||||
))?(serialized.paths)
|
||||
}
|
||||
|
||||
/// Look up a single thread by session ID.
|
||||
pub fn get(&self, session_id: &acp::SessionId) -> Result<Option<SidebarThreadRow>> {
|
||||
let id = session_id.0.clone();
|
||||
self.select_row_bound::<Arc<str>, SidebarThreadRow>(sql!(
|
||||
SELECT session_id, agent_name, title, updated_at, created_at, folder_paths, folder_paths_order
|
||||
FROM sidebar_threads
|
||||
WHERE session_id = ?
|
||||
))?(id)
|
||||
}
|
||||
|
||||
/// Return the total number of rows in the table.
|
||||
pub fn count(&self) -> Result<usize> {
|
||||
let count: (i32, i32) = self.select_row(sql!(
|
||||
SELECT COUNT(*) FROM sidebar_threads
|
||||
))?(())?.unwrap_or_default();
|
||||
Ok(count.0 as usize)
|
||||
}
|
||||
|
||||
/// Delete metadata for a single thread.
|
||||
pub async fn delete(&self, session_id: acp::SessionId) -> Result<()> {
|
||||
let id = session_id.0;
|
||||
self.write(move |conn| {
|
||||
let mut stmt = Statement::prepare(
|
||||
conn,
|
||||
"DELETE FROM sidebar_threads WHERE session_id = ?",
|
||||
)?;
|
||||
stmt.bind(&id, 1)?;
|
||||
stmt.exec()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Delete all thread metadata.
|
||||
pub async fn delete_all(&self) -> Result<()> {
|
||||
self.write(move |conn| {
|
||||
let mut stmt = Statement::prepare(
|
||||
conn,
|
||||
"DELETE FROM sidebar_threads",
|
||||
)?;
|
||||
stmt.exec()
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `Column` Implementation
|
||||
|
||||
```rust
|
||||
impl Column for SidebarThreadRow {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let (id, next): (Arc<str>, i32) = Column::column(statement, start_index)?;
|
||||
let (agent_name, next): (Option<String>, i32) = Column::column(statement, next)?;
|
||||
let (title, next): (String, i32) = Column::column(statement, next)?;
|
||||
let (updated_at_str, next): (String, i32) = Column::column(statement, next)?;
|
||||
let (created_at_str, next): (Option<String>, i32) = Column::column(statement, next)?;
|
||||
let (folder_paths_str, next): (Option<String>, i32) = Column::column(statement, next)?;
|
||||
let (folder_paths_order_str, next): (Option<String>, i32) = Column::column(statement, next)?;
|
||||
|
||||
let updated_at = DateTime::parse_from_rfc3339(&updated_at_str)?.with_timezone(&Utc);
|
||||
let created_at = created_at_str
|
||||
.as_deref()
|
||||
.map(DateTime::parse_from_rfc3339)
|
||||
.transpose()?
|
||||
.map(|dt| dt.with_timezone(&Utc));
|
||||
|
||||
let folder_paths = folder_paths_str
|
||||
.map(|paths| {
|
||||
PathList::deserialize(&util::path_list::SerializedPathList {
|
||||
paths,
|
||||
order: folder_paths_order_str.unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok((
|
||||
SidebarThreadRow {
|
||||
session_id: acp::SessionId::new(id),
|
||||
agent_name,
|
||||
title: title.into(),
|
||||
updated_at,
|
||||
created_at,
|
||||
folder_paths,
|
||||
},
|
||||
next,
|
||||
))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
|
||||
- `SIDEBAR_DB` is a `LazyLock` static — initialized on first use, no manual connection management.
|
||||
- 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.
|
||||
- The DB file lives alongside other `static_connection!` databases.
|
||||
- `ThreadsDatabase` and `ThreadStore` are **completely unchanged** by this step.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Replace `ThreadStore` Reads in `rebuild_contents` with `SidebarDb` Reads
|
||||
|
||||
**File:** `crates/agent_ui/src/sidebar.rs`
|
||||
|
||||
### Remove `ThreadStore` Dependency
|
||||
|
||||
1. **Remove** `ThreadStore::global(cx)` and `ThreadStore::try_global(cx)` from `Sidebar::new` and `rebuild_contents`.
|
||||
2. **Remove** the `cx.observe_in(&thread_store, ...)` subscription that triggers `update_entries` when `ThreadStore` changes.
|
||||
3. **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).
|
||||
|
||||
### New Data Flow in `rebuild_contents`
|
||||
|
||||
```rust
|
||||
fn rebuild_contents(&mut self, cx: &App) {
|
||||
// ... existing workspace iteration setup ...
|
||||
|
||||
// Read ALL sidebar thread metadata once, index by folder_paths.
|
||||
let all_sidebar_threads = SIDEBAR_DB.list().unwrap_or_default();
|
||||
let mut threads_by_paths: HashMap<PathList, Vec<SidebarThreadRow>> = HashMap::new();
|
||||
for row in all_sidebar_threads {
|
||||
threads_by_paths
|
||||
.entry(row.folder_paths.clone())
|
||||
.or_default()
|
||||
.push(row);
|
||||
}
|
||||
|
||||
for (ws_index, workspace) in workspaces.iter().enumerate() {
|
||||
// ... existing absorbed-workspace logic ...
|
||||
|
||||
let path_list = workspace_path_list(workspace, cx);
|
||||
|
||||
if should_load_threads {
|
||||
let mut seen_session_ids: HashSet<acp::SessionId> = HashSet::new();
|
||||
|
||||
// Read from SidebarDb instead of ThreadStore
|
||||
if let Some(rows) = threads_by_paths.get(&path_list) {
|
||||
for row in rows {
|
||||
seen_session_ids.insert(row.session_id.clone());
|
||||
let (agent, icon) = match &row.agent_name {
|
||||
None => (Agent::NativeAgent, IconName::ZedAgent),
|
||||
Some(name) => (
|
||||
Agent::Custom { name: name.clone().into() },
|
||||
IconName::ZedAgent, // placeholder, resolved in Step 5
|
||||
),
|
||||
};
|
||||
threads.push(ThreadEntry {
|
||||
agent,
|
||||
session_info: AgentSessionInfo {
|
||||
session_id: row.session_id.clone(),
|
||||
cwd: None,
|
||||
title: Some(row.title.clone()),
|
||||
updated_at: Some(row.updated_at),
|
||||
created_at: row.created_at,
|
||||
meta: None,
|
||||
},
|
||||
icon,
|
||||
icon_from_external_svg: None,
|
||||
status: AgentThreadStatus::default(),
|
||||
workspace: ThreadEntryWorkspace::Open(workspace.clone()),
|
||||
is_live: false,
|
||||
is_background: false,
|
||||
highlight_positions: Vec::new(),
|
||||
worktree_name: None,
|
||||
worktree_highlight_positions: Vec::new(),
|
||||
diff_stats: DiffStats::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ... existing linked git worktree logic, also reading from threads_by_paths ...
|
||||
// ... existing live thread overlay logic (unchanged) ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### What Changes
|
||||
|
||||
- `rebuild_contents` reads from `SIDEBAR_DB` instead of `ThreadStore`.
|
||||
- The `ThreadEntry.agent` field now carries `Agent::Custom { name }` for ACP threads, enabling correct routing in `activate_thread`.
|
||||
- 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.
|
||||
|
||||
### What Stays the Same
|
||||
|
||||
- The entire workspace/absorbed-workspace/git-worktree structure.
|
||||
- The live thread overlay pass.
|
||||
- The notification tracking logic.
|
||||
- The search/filter logic.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Write Native Thread Metadata to `SidebarDb`
|
||||
|
||||
**File:** `crates/agent_ui/src/sidebar.rs` and/or `crates/agent_ui/src/agent_panel.rs`
|
||||
|
||||
When a native thread is saved (after conversation, on title update, etc.), we also write its metadata to `SidebarDb`. There are two approaches:
|
||||
|
||||
### Option A: Subscribe to `ThreadStore` Changes (Recommended)
|
||||
|
||||
Keep 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).
|
||||
|
||||
```rust
|
||||
// In Sidebar::subscribe_to_workspace or a dedicated sync method:
|
||||
fn sync_native_threads_to_sidebar_db(&self, cx: &App) {
|
||||
if let Some(thread_store) = ThreadStore::try_global(cx) {
|
||||
let entries: Vec<_> = thread_store.read(cx).entries().collect();
|
||||
cx.background_spawn(async move {
|
||||
for meta in entries {
|
||||
SIDEBAR_DB.save(&SidebarThreadRow {
|
||||
session_id: meta.id,
|
||||
agent_name: None, // native
|
||||
title: meta.title,
|
||||
updated_at: meta.updated_at,
|
||||
created_at: meta.created_at,
|
||||
folder_paths: meta.folder_paths,
|
||||
}).await.log_err();
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option B: Write at the Point of Save
|
||||
|
||||
In `AgentPanel` or wherever `thread_store.save_thread()` is called, also call `SIDEBAR_DB.save(...)`. This is more direct but requires touching more call sites.
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Write ACP Thread Metadata to `SidebarDb`
|
||||
|
||||
**File:** `crates/agent_ui/src/connection_view.rs` (or `agent_panel.rs`)
|
||||
|
||||
When ACP sessions are created, updated, or listed, write metadata directly to `SidebarDb`:
|
||||
|
||||
- **On new session creation:** After `connection.new_session()` returns the `AcpThread`, call `SIDEBAR_DB.save(...)`.
|
||||
- **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.
|
||||
- **On session list refresh:** When `AgentSessionList::list_sessions` returns for an ACP agent, bulk-sync the metadata into `SidebarDb`.
|
||||
|
||||
After any write, call `cx.notify()` on the `Sidebar` entity (or use a channel/event) to trigger a `rebuild_contents`.
|
||||
|
||||
### Triggering Sidebar Refresh
|
||||
|
||||
Since the sidebar no longer observes `ThreadStore`, we need a mechanism to trigger `rebuild_contents` after DB writes. Options:
|
||||
|
||||
1. **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`.
|
||||
2. **Use `cx.notify()` directly** — If the save happens within a `Sidebar` method, just call `self.update_entries(cx)`.
|
||||
3. **Observe a lightweight signal entity** — A simple `Entity<()>` that gets notified after DB writes.
|
||||
|
||||
**Recommendation:** Option 1 (emit from `AgentPanel`) is cleanest since the sidebar already subscribes to panel events.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Handle Agent Icon Resolution for ACP Threads
|
||||
|
||||
**File:** `crates/agent_ui/src/sidebar.rs`
|
||||
|
||||
For 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.
|
||||
|
||||
In `rebuild_contents`, after building the initial thread list from `SidebarDb`, resolve icons for ACP threads:
|
||||
|
||||
```rust
|
||||
// For ACP threads, look up the icon from the agent server store
|
||||
if let Some(name) = &row.agent_name {
|
||||
if let Some(agent_server_store) = /* get from workspace */ {
|
||||
// resolve icon from agent_server_store using name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Handle Delete Operations Correctly
|
||||
|
||||
**File:** `crates/agent_ui/src/sidebar.rs`
|
||||
|
||||
When the user deletes a thread from the sidebar:
|
||||
|
||||
- **All threads** → Delete from `SidebarDb` via `SIDEBAR_DB.delete(session_id)`.
|
||||
- **Native threads** → _Also_ delete from `ThreadStore`/`ThreadsDatabase` (to clean up the blob data).
|
||||
- **ACP threads** → Optionally notify the ACP server via `AgentSessionList::delete_session`.
|
||||
|
||||
The `agent_name` field on `SidebarThreadRow` (or the `Agent` enum on `ThreadEntry`) tells us which path to take.
|
||||
|
||||
When the user clears all history:
|
||||
|
||||
```rust
|
||||
// Delete all sidebar metadata
|
||||
SIDEBAR_DB.delete_all().await?;
|
||||
// Also clear native thread blobs
|
||||
thread_store.delete_threads(cx);
|
||||
// Optionally notify ACP servers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Handle `activate_thread` Routing
|
||||
|
||||
**File:** `crates/agent_ui/src/sidebar.rs`, `crates/agent_ui/src/agent_panel.rs`
|
||||
|
||||
In `activate_thread`, branch on the `Agent` variant:
|
||||
|
||||
- `Agent::NativeAgent` → Call `panel.load_agent_thread(Agent::NativeAgent, session_id, ...)` (current behavior).
|
||||
- `Agent::Custom { name }` → Call `panel.load_agent_thread(Agent::Custom { name }, session_id, ...)` so it routes to the correct `AgentConnection::load_session`.
|
||||
|
||||
This 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`.
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Handle `activate_archived_thread` Without ThreadStore
|
||||
|
||||
**File:** `crates/agent_ui/src/sidebar.rs`
|
||||
|
||||
Currently, `activate_archived_thread` looks up `saved_path_list` from `ThreadStore`:
|
||||
|
||||
```rust
|
||||
let saved_path_list = ThreadStore::try_global(cx).and_then(|thread_store| {
|
||||
thread_store
|
||||
.read(cx)
|
||||
.thread_from_session_id(&session_info.session_id)
|
||||
.map(|thread| thread.folder_paths.clone())
|
||||
});
|
||||
```
|
||||
|
||||
Replace this with a targeted `SidebarDb::get` lookup (single-row SELECT, no full table scan):
|
||||
|
||||
```rust
|
||||
let saved_path_list = SIDEBAR_DB
|
||||
.get(&session_info.session_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|row| row.folder_paths);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 9: Error Handling for Offline Agents
|
||||
|
||||
When an ACP thread is clicked but the agent server is not running:
|
||||
|
||||
- Show a toast/notification explaining the agent is offline.
|
||||
- Keep the metadata in the sidebar (don't remove it).
|
||||
- Optionally offer to start the agent server.
|
||||
|
||||
---
|
||||
|
||||
## Step 10: Migration — Backfill Existing Native Threads
|
||||
|
||||
On first launch after this change, the `SidebarDb` will be empty while `ThreadsDatabase` has existing native threads. We need a one-time backfill:
|
||||
|
||||
```rust
|
||||
// In Sidebar::new or a dedicated init method:
|
||||
fn backfill_native_threads_if_needed(cx: &App) {
|
||||
if SIDEBAR_DB.count() > 0 {
|
||||
return; // Already populated
|
||||
}
|
||||
|
||||
if let Some(thread_store) = ThreadStore::try_global(cx) {
|
||||
let entries: Vec<_> = thread_store.read(cx).entries().collect();
|
||||
cx.background_spawn(async move {
|
||||
for meta in entries {
|
||||
SIDEBAR_DB.save(&SidebarThreadRow {
|
||||
session_id: meta.id,
|
||||
agent_name: None,
|
||||
title: meta.title,
|
||||
updated_at: meta.updated_at,
|
||||
created_at: meta.created_at,
|
||||
folder_paths: meta.folder_paths,
|
||||
}).await.log_err();
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary of Files to Change
|
||||
|
||||
| File | Changes |
|
||||
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `crates/agent_ui/Cargo.toml` | Add `db.workspace = true`, `sqlez.workspace = true`, `sqlez_macros.workspace = true`, `chrono.workspace = true` dependencies |
|
||||
| `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. |
|
||||
| `crates/agent_ui/src/agent_panel.rs` | Emit `AgentPanelEvent::ThreadMetadataChanged` after thread saves. Potentially write ACP metadata to `SidebarDb` here. |
|
||||
| `crates/agent_ui/src/connection_view.rs` | Write ACP metadata to `SidebarDb` on session creation, title updates, and session list refreshes. |
|
||||
|
||||
## What Is NOT Changed
|
||||
|
||||
| File / Area | Why |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `threads` table schema | No migration needed — native blob persistence is completely untouched |
|
||||
| `ThreadsDatabase` methods | `save_thread_sync`, `load_thread`, `list_threads`, `delete_thread`, `delete_threads` — all unchanged |
|
||||
| `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. |
|
||||
| `NativeAgent::load_thread` / `open_thread` | These deserialize `DbThread` blobs — completely unaffected |
|
||||
| `crates/acp_thread/` | No new persistence module needed there (unlike the original plan) |
|
||||
| `crates/agent/src/db.rs` | `DbThreadMetadata` is unchanged — no `agent_type` field added |
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. **SidebarDb domain** (Step 1) — Create `SidebarDb`, `SidebarThreadRow`, `SIDEBAR_DB` static, CRUD methods in `sidebar.rs`.
|
||||
2. **Replace reads** (Step 2) — Swap `ThreadStore` reads in `rebuild_contents` for `SidebarDb` reads.
|
||||
3. **Native write path** (Step 3) — Sync native thread metadata from `ThreadStore` into `SidebarDb`.
|
||||
4. **ACP write path** (Step 4) — Write ACP thread metadata to `SidebarDb` from connection views.
|
||||
5. **Icon resolution** (Step 5) — Resolve ACP agent icons in the sidebar.
|
||||
6. **Delete path** (Step 6) — Route deletes to `SidebarDb` + native blob cleanup + ACP server notification.
|
||||
7. **Activate routing** (Step 7) — Ensure `activate_thread` routes correctly based on `Agent` variant.
|
||||
8. **Archive fix** (Step 8) — Update `activate_archived_thread` to use `SidebarDb`.
|
||||
9. **Migration** (Step 10) — Backfill existing native threads on first run.
|
||||
10. **Polish** (Step 9) — Error handling for offline agents.
|
||||
|
||||
## Key Differences from Original Plan
|
||||
|
||||
| Aspect | Original Plan | Revised Plan |
|
||||
| ------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
|
||||
| **Where ACP metadata lives** | New `AcpThreadMetadataDb` in `crates/acp_thread/` | `SidebarDb` in `crates/agent_ui/src/sidebar.rs` |
|
||||
| **Where sidebar reads from** | `ThreadStore` (which merges native + ACP) | `SidebarDb` directly (single source) |
|
||||
| **ThreadStore changes** | Added `agent_type` to `DbThreadMetadata`, merge logic in `reload`, new save/delete methods | **None** — ThreadStore is untouched |
|
||||
| **`crates/agent/src/db.rs` changes** | Added `agent_type: Option<String>` to `DbThreadMetadata` | **None** |
|
||||
| **Merge complexity** | Two data sources merged in `ThreadStore::reload` | No merge — one table, one read |
|
||||
| **Crate dependencies** | `acp_thread` gains `db` dependency | `agent_ui` gains `db` dependency (more natural — it's a UI persistence concern) |
|
||||
Loading…
Reference in a new issue