diff --git a/Cargo.lock b/Cargo.lock
index 75bdf2ca..0205ea43 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1357,7 +1357,6 @@ dependencies = [
"sandbox",
"serde",
"serde_json",
- "shell-words",
"similar",
"smallvec",
"tempfile",
@@ -6772,12 +6771,6 @@ dependencies = [
"lazy_static",
]
-[[package]]
-name = "shell-words"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
-
[[package]]
name = "shlex"
version = "1.3.0"
diff --git a/crates/code_assistant/Cargo.toml b/crates/code_assistant/Cargo.toml
index af5aac1f..712692dd 100644
--- a/crates/code_assistant/Cargo.toml
+++ b/crates/code_assistant/Cargo.toml
@@ -51,7 +51,6 @@ async-trait = "0.1"
dotenv = "0.15"
dirs = "5.0"
md5 = "0.7.0"
-shell-words = "1.1"
# Date and time handling
chrono = { version = "0.4", features = ["serde"] }
diff --git a/crates/code_assistant/assets/icons/panel_left_close.svg b/crates/code_assistant/assets/icons/panel_left_close.svg
new file mode 100644
index 00000000..c5bda10c
--- /dev/null
+++ b/crates/code_assistant/assets/icons/panel_left_close.svg
@@ -0,0 +1 @@
+
diff --git a/crates/code_assistant/assets/icons/panel_left_open.svg b/crates/code_assistant/assets/icons/panel_left_open.svg
new file mode 100644
index 00000000..e10acec9
--- /dev/null
+++ b/crates/code_assistant/assets/icons/panel_left_open.svg
@@ -0,0 +1 @@
+
diff --git a/crates/code_assistant/src/acp/terminal_executor.rs b/crates/code_assistant/src/acp/terminal_executor.rs
index f0016237..f61ce3f9 100644
--- a/crates/code_assistant/src/acp/terminal_executor.rs
+++ b/crates/code_assistant/src/acp/terminal_executor.rs
@@ -1,7 +1,6 @@
use agent_client_protocol::{self as acp, Client};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
-use shell_words::split as split_command_line;
use std::path::PathBuf;
use std::sync::{Arc, OnceLock};
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
@@ -58,18 +57,6 @@ impl ACPTerminalCommandExecutor {
default_timeout: DEFAULT_TIMEOUT,
}
}
-
- fn parse_command_line(command_line: &str) -> Result<(String, Vec)> {
- let mut parts = split_command_line(command_line)
- .map_err(|e| anyhow!("Failed to parse command line '{command_line}': {e}"))?
- .into_iter();
-
- let command = parts
- .next()
- .ok_or_else(|| anyhow!("Command line is empty"))?;
- let args = parts.collect();
- Ok((command, args))
- }
}
#[async_trait]
@@ -91,11 +78,6 @@ impl CommandExecutor for ACPTerminalCommandExecutor {
callback: Option<&dyn StreamingCallback>,
sandbox_request: Option<&SandboxCommandRequest>,
) -> Result {
- let (command, args) = match Self::parse_command_line(command_line) {
- Ok(parsed) => parsed,
- Err(err) => return Err(err),
- };
-
let sender = match terminal_worker_sender() {
Some(sender) => sender,
None => {
@@ -109,8 +91,7 @@ impl CommandExecutor for ACPTerminalCommandExecutor {
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
let request = TerminalExecuteRequest {
session_id: self.session_id.clone(),
- command,
- args,
+ command_line: command_line.to_string(),
cwd: working_dir.cloned(),
timeout: self.default_timeout,
streaming: callback.is_some(),
@@ -157,8 +138,7 @@ impl CommandExecutor for ACPTerminalCommandExecutor {
#[derive(Debug)]
struct TerminalExecuteRequest {
session_id: acp::SessionId,
- command: String,
- args: Vec,
+ command_line: String,
cwd: Option,
timeout: Duration,
streaming: bool,
@@ -204,18 +184,19 @@ async fn run_command(
) -> Result {
let TerminalExecuteRequest {
session_id,
- command,
- args,
+ command_line,
cwd,
timeout,
streaming,
..
} = request;
+ // Pass the complete command line as the command parameter with empty args.
+ // This avoids escaping issues on the Zed side when args are passed separately.
let create_request = acp::CreateTerminalRequest {
session_id: session_id.clone(),
- command,
- args,
+ command: command_line,
+ args: Vec::new(),
env: Vec::new(),
cwd,
output_byte_limit: Some(OUTPUT_BYTE_LIMIT),
@@ -369,23 +350,3 @@ async fn wait_for_terminal_completion(
output: output_response.output,
})
}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn parse_command_line_handles_quotes() {
- let (command, args) =
- ACPTerminalCommandExecutor::parse_command_line("cargo test --package \"my crate\"")
- .unwrap();
- assert_eq!(command, "cargo");
- assert_eq!(args, vec!["test", "--package", "my crate"]);
- }
-
- #[test]
- fn parse_command_line_errors_on_empty_input() {
- let err = ACPTerminalCommandExecutor::parse_command_line("").unwrap_err();
- assert!(err.to_string().contains("empty"));
- }
-}
diff --git a/crates/code_assistant/src/acp/ui.rs b/crates/code_assistant/src/acp/ui.rs
index e9da2562..d4d2fb7a 100644
--- a/crates/code_assistant/src/acp/ui.rs
+++ b/crates/code_assistant/src/acp/ui.rs
@@ -668,9 +668,39 @@ impl UserInterface for ACPUserUI {
);
}
+ // Resource events - could be used for "follow mode" in ACP
+ UiEvent::ResourceLoaded { project, path } => {
+ tracing::trace!(
+ "ACPUserUI: ResourceLoaded - project: {}, path: {}",
+ project,
+ path.display()
+ );
+ // TODO: Could emit follow mode updates here
+ }
+ UiEvent::ResourceWritten { project, path } => {
+ tracing::trace!(
+ "ACPUserUI: ResourceWritten - project: {}, path: {}",
+ project,
+ path.display()
+ );
+ }
+ UiEvent::DirectoryListed { project, path } => {
+ tracing::trace!(
+ "ACPUserUI: DirectoryListed - project: {}, path: {}",
+ project,
+ path.display()
+ );
+ }
+ UiEvent::ResourceDeleted { project, path } => {
+ tracing::trace!(
+ "ACPUserUI: ResourceDeleted - project: {}, path: {}",
+ project,
+ path.display()
+ );
+ }
+
// Events that don't translate to ACP
- UiEvent::UpdateMemory { .. }
- | UiEvent::SetMessages { .. }
+ UiEvent::SetMessages { .. }
| UiEvent::DisplayCompactionSummary { .. }
| UiEvent::StreamingStarted(_)
| UiEvent::StreamingStopped { .. }
diff --git a/crates/code_assistant/src/agent/runner.rs b/crates/code_assistant/src/agent/runner.rs
index 8c803e5d..96fae5d8 100644
--- a/crates/code_assistant/src/agent/runner.rs
+++ b/crates/code_assistant/src/agent/runner.rs
@@ -45,7 +45,6 @@ enum LoopFlow {
}
pub struct Agent {
- working_memory: WorkingMemory,
plan: PlanState,
llm_provider: Box,
session_config: SessionConfig,
@@ -77,6 +76,10 @@ pub struct Agent {
enable_naming_reminders: bool,
// Shared pending message with SessionInstance
pending_message_ref: Option>>>,
+ // File trees for projects (used in system prompt)
+ file_trees: HashMap,
+ // Available project names (used in system prompt)
+ available_projects: Vec,
}
const CONTEXT_USAGE_THRESHOLD: f32 = 0.8;
@@ -111,7 +114,6 @@ impl Agent {
} = components;
let mut this = Self {
- working_memory: WorkingMemory::default(),
plan: PlanState::default(),
llm_provider,
session_config,
@@ -132,6 +134,8 @@ impl Agent {
enable_naming_reminders: true, // Enabled by default
pending_message_ref: None,
model_hint: None,
+ file_trees: HashMap::new(),
+ available_projects: Vec::new(),
};
if this.session_config.use_diff_blocks {
this.tool_scope = ToolScope::AgentWithDiffBlocks;
@@ -267,13 +271,21 @@ impl Agent {
"saving {} messages to persistence",
self.message_history.len()
);
+
if let Some(session_id) = &self.session_id {
+ // Create a WorkingMemory for backward-compatible persistence
+ // File trees are regenerated on load, so we only persist available_projects
+ let working_memory = WorkingMemory {
+ available_projects: self.available_projects.clone(),
+ ..Default::default()
+ };
+
let session_state = crate::session::SessionState {
session_id: session_id.clone(),
name: self.session_name.clone(),
messages: self.message_history.clone(),
tool_executions: self.tool_executions.clone(),
- working_memory: self.working_memory.clone(),
+ working_memory,
plan: self.plan.clone(),
config: self.session_config.clone(),
next_request_id: Some(self.next_request_id),
@@ -370,19 +382,14 @@ impl Agent {
debug!("Agent iteration complete - waiting for next user message");
return Ok(());
}
+
LoopFlow::Continue => {
if !tool_requests.is_empty() {
// Tools were requested, manage their execution
let flow = self.manage_tool_execution(&tool_requests).await?;
- // Save state and update memory after tool executions
+ // Save state after tool executions
self.save_state()?;
- let _ = self
- .ui
- .send_event(UiEvent::UpdateMemory {
- memory: self.working_memory.clone(),
- })
- .await;
match flow {
LoopFlow::Continue => { /* Continue to the next iteration */ }
@@ -425,7 +432,6 @@ impl Agent {
self.message_history.len()
);
self.tool_executions = session_state.tool_executions;
- self.working_memory = session_state.working_memory;
self.plan = session_state.plan.clone();
self.session_config = session_state.config;
if self.session_config.use_diff_blocks {
@@ -458,15 +464,12 @@ impl Agent {
+ 1
});
- // Restore working memory file trees and project state
- self.init_working_memory_projects()?;
+ // Restore available projects from persisted working memory
+ // (backward compatibility with existing sessions)
+ self.available_projects = session_state.working_memory.available_projects.clone();
- let _ = self
- .ui
- .send_event(UiEvent::UpdateMemory {
- memory: self.working_memory.clone(),
- })
- .await;
+ // Refresh project information from project manager (regenerates file trees)
+ self.init_projects()?;
let _ = self
.ui
@@ -539,18 +542,18 @@ impl Agent {
}
#[allow(dead_code)]
- pub fn init_working_memory(&mut self) -> Result<()> {
+ pub fn init_project_context(&mut self) -> Result<()> {
// Initialize empty structures for multi-project support
- self.working_memory.file_trees = HashMap::new();
- self.working_memory.available_projects = Vec::new();
+ self.file_trees = HashMap::new();
+ self.available_projects = Vec::new();
// Reset the initial project
self.session_config.initial_project = String::new();
- self.init_working_memory_projects()
+ self.init_projects()
}
- fn init_working_memory_projects(&mut self) -> Result<()> {
+ fn init_projects(&mut self) -> Result<()> {
// If a path was provided in args, add it as a temporary project
if let Some(path) = &self.session_config.init_path {
// Add as temporary project and get its name
@@ -565,23 +568,16 @@ impl Agent {
.get_explorer_for_project(&project_name)?;
let tree = explorer.create_initial_tree(2)?; // Limited depth for initial tree
- // Store in working memory
- self.working_memory
- .file_trees
- .insert(project_name.clone(), tree);
+ // Store file tree as string for system prompt
+ self.file_trees
+ .insert(project_name.clone(), tree.to_string());
}
// Load all available projects
let all_projects = self.project_manager.get_projects()?;
for project_name in all_projects.keys() {
- if !self
- .working_memory
- .available_projects
- .contains(project_name)
- {
- self.working_memory
- .available_projects
- .push(project_name.clone());
+ if !self.available_projects.contains(project_name) {
+ self.available_projects.push(project_name.clone());
}
}
@@ -690,7 +686,7 @@ impl Agent {
pub async fn start_with_task(&mut self, task: String) -> Result<()> {
debug!("Starting agent with task: {}", task);
- self.init_working_memory()?;
+ self.init_project_context()?;
self.message_history.clear();
self.ui
@@ -703,14 +699,6 @@ impl Agent {
// Create the initial user message
self.append_message(Message::new_user(task.clone()))?;
- // Notify UI of initial working memory
- let _ = self
- .ui
- .send_event(UiEvent::UpdateMemory {
- memory: self.working_memory.clone(),
- })
- .await;
-
self.run_single_iteration().await
}
@@ -745,20 +733,16 @@ impl Agent {
));
// Add file tree for the initial project if available
- if let Some(tree) = self
- .working_memory
- .file_trees
- .get(&self.session_config.initial_project)
- {
+ if let Some(tree) = self.file_trees.get(&self.session_config.initial_project) {
project_info.push_str("### File Structure:\n");
project_info.push_str(&format!("```\n{tree}\n```\n\n"));
}
}
// Add information about available projects
- if !self.working_memory.available_projects.is_empty() {
+ if !self.available_projects.is_empty() {
project_info.push_str("## Available Projects:\n");
- for project in &self.working_memory.available_projects {
+ for project in &self.available_projects {
project_info.push_str(&format!("- {project}\n"));
}
}
@@ -1440,7 +1424,6 @@ impl Agent {
let mut context = ToolContext {
project_manager: self.project_manager.as_ref(),
command_executor: self.command_executor.as_ref(),
- working_memory: Some(&mut self.working_memory),
plan: Some(&mut self.plan),
ui: Some(self.ui.as_ref()),
tool_id: Some(tool_request.id.clone()),
diff --git a/crates/code_assistant/src/mcp/handler.rs b/crates/code_assistant/src/mcp/handler.rs
index 34f809aa..8a1ad144 100644
--- a/crates/code_assistant/src/mcp/handler.rs
+++ b/crates/code_assistant/src/mcp/handler.rs
@@ -265,7 +265,6 @@ impl MessageHandler {
let mut context = crate::tools::core::ToolContext {
project_manager: self.project_manager.as_ref(),
command_executor: self.command_executor.as_ref(),
- working_memory: None,
plan: None,
ui: None,
tool_id: None,
diff --git a/crates/code_assistant/src/session/instance.rs b/crates/code_assistant/src/session/instance.rs
index 662903ce..2bbed095 100644
--- a/crates/code_assistant/src/session/instance.rs
+++ b/crates/code_assistant/src/session/instance.rs
@@ -228,10 +228,6 @@ impl SessionInstance {
tool_results,
});
- events.push(UiEvent::UpdateMemory {
- memory: self.session.working_memory.clone(),
- });
-
events.push(UiEvent::UpdatePlan {
plan: self.session.plan.clone(),
});
diff --git a/crates/code_assistant/src/tests/format_on_save_tests.rs b/crates/code_assistant/src/tests/format_on_save_tests.rs
index 1694be30..d3952d21 100644
--- a/crates/code_assistant/src/tests/format_on_save_tests.rs
+++ b/crates/code_assistant/src/tests/format_on_save_tests.rs
@@ -3,7 +3,7 @@ use crate::tools::core::{Tool, ToolContext};
use crate::tools::impls::edit::{EditInput, EditTool};
use crate::tools::impls::replace_in_file::{ReplaceInFileInput, ReplaceInFileTool};
use crate::tools::impls::write_file::{WriteFileInput, WriteFileTool};
-use crate::types::{LoadedResource, Project, WorkingMemory};
+use crate::types::Project;
use anyhow::Result;
use command_executor::CommandOutput;
use fs_explorer::file_updater::{
@@ -191,11 +191,9 @@ async fn test_edit_tool_parameter_update_after_formatting() -> Result<()> {
Box::new(explorer),
));
- let mut working_memory = WorkingMemory::default();
let mut context = ToolContext {
project_manager: project_manager.as_ref(),
command_executor: &command_executor,
- working_memory: Some(&mut working_memory),
plan: None,
ui: None,
tool_id: None,
@@ -233,14 +231,6 @@ async fn test_edit_tool_parameter_update_after_formatting() -> Result<()> {
assert_eq!(input.old_text, "const y = 2;"); // search unchanged
assert_eq!(input.new_text, "const y = 42;"); // replacement reflects formatted code
- // Verify that working memory contains the formatted full file content
- let key = ("test-project".to_string(), PathBuf::from("test.js"));
- if let Some(LoadedResource::File(content)) = working_memory.loaded_resources.get(&key) {
- assert_eq!(content, "const x = 1;const y = 42;const z = 3;");
- } else {
- panic!("Expected file in working memory");
- }
-
Ok(())
}
@@ -274,11 +264,9 @@ async fn test_write_file_with_format_on_save() -> Result<()> {
Box::new(explorer),
));
- let mut working_memory = WorkingMemory::default();
let mut context = ToolContext {
project_manager: project_manager.as_ref(),
command_executor: &command_executor,
- working_memory: Some(&mut working_memory),
plan: None,
ui: None,
tool_id: None,
@@ -348,11 +336,9 @@ async fn test_replace_in_file_with_format_on_save() -> Result<()> {
Box::new(explorer),
));
- let mut working_memory = WorkingMemory::default();
let mut context = ToolContext {
project_manager: project_manager.as_ref(),
command_executor: &command_executor,
- working_memory: Some(&mut working_memory),
plan: None,
ui: None,
tool_id: None,
@@ -396,15 +382,6 @@ async fn test_replace_in_file_with_format_on_save() -> Result<()> {
assert!(input.diff.contains("version = \"0.2.0\""));
assert!(input.diff.contains("serde = \"2.0\""));
- // Verify that working memory contains the final formatted content
- let key = ("test-project".to_string(), PathBuf::from("config.toml"));
- if let Some(LoadedResource::File(content)) = working_memory.loaded_resources.get(&key) {
- assert!(content.contains("version = \"0.2.0\""));
- assert!(content.contains("serde = \"2.0\""));
- } else {
- panic!("Expected file in working memory");
- }
-
Ok(())
}
@@ -433,11 +410,9 @@ async fn test_no_format_when_pattern_doesnt_match() -> Result<()> {
Box::new(explorer),
));
- let mut working_memory = WorkingMemory::default();
let mut context = ToolContext {
project_manager: project_manager.as_ref(),
command_executor: &command_executor,
- working_memory: Some(&mut working_memory),
plan: None,
ui: None,
tool_id: None,
@@ -511,11 +486,9 @@ async fn test_format_on_save_multiple_patterns() -> Result<()> {
Box::new(explorer),
));
- let mut working_memory = WorkingMemory::default();
let mut context = ToolContext {
project_manager: project_manager.as_ref(),
command_executor: &command_executor,
- working_memory: Some(&mut working_memory),
plan: None,
ui: None,
tool_id: None,
@@ -609,11 +582,9 @@ async fn test_format_on_save_glob_patterns() -> Result<()> {
Box::new(explorer),
));
- let mut working_memory = WorkingMemory::default();
let mut context = ToolContext {
project_manager: project_manager.as_ref(),
command_executor: &command_executor,
- working_memory: Some(&mut working_memory),
plan: None,
ui: None,
tool_id: None,
@@ -700,11 +671,9 @@ async fn test_format_on_save_with_conflicting_matches() -> Result<()> {
Box::new(explorer),
));
- let mut working_memory = WorkingMemory::default();
let mut context = ToolContext {
project_manager: project_manager.as_ref(),
command_executor: &command_executor,
- working_memory: Some(&mut working_memory),
plan: None,
ui: None,
tool_id: None,
diff --git a/crates/code_assistant/src/tests/mocks.rs b/crates/code_assistant/src/tests/mocks.rs
index 37095a4e..b081109f 100644
--- a/crates/code_assistant/src/tests/mocks.rs
+++ b/crates/code_assistant/src/tests/mocks.rs
@@ -220,7 +220,6 @@ pub fn create_failed_command_executor_mock() -> MockCommandExecutor {
pub fn create_test_tool_context<'a>(
project_manager: &'a dyn crate::config::ProjectManager,
command_executor: &'a dyn CommandExecutor,
- working_memory: Option<&'a mut crate::types::WorkingMemory>,
plan: Option<&'a mut crate::types::PlanState>,
ui: Option<&'a dyn crate::ui::UserInterface>,
tool_id: Option,
@@ -228,7 +227,6 @@ pub fn create_test_tool_context<'a>(
crate::tools::core::ToolContext {
project_manager,
command_executor,
- working_memory,
plan,
ui,
tool_id,
@@ -987,7 +985,6 @@ impl ProjectManager for MockProjectManager {
pub struct ToolTestFixture {
project_manager: MockProjectManager,
command_executor: MockCommandExecutor,
- working_memory: Option,
plan: Option,
ui: Option,
tool_id: Option,
@@ -1000,7 +997,6 @@ impl ToolTestFixture {
Self {
project_manager: MockProjectManager::new(),
command_executor: MockCommandExecutor::new(vec![]),
- working_memory: None,
plan: None,
ui: None,
tool_id: None,
@@ -1066,7 +1062,6 @@ impl ToolTestFixture {
Self {
project_manager,
command_executor: MockCommandExecutor::new(vec![]),
- working_memory: None,
plan: None,
ui: None,
tool_id: None,
@@ -1079,7 +1074,6 @@ impl ToolTestFixture {
Self {
project_manager: MockProjectManager::new(),
command_executor: MockCommandExecutor::new(responses),
- working_memory: None,
plan: None,
ui: None,
tool_id: None,
@@ -1098,12 +1092,6 @@ impl ToolTestFixture {
fixture
}
- /// Enable working memory for this fixture
- pub fn with_working_memory(mut self) -> Self {
- self.working_memory = Some(WorkingMemory::default());
- self
- }
-
/// Enable plan state for this fixture
pub fn with_plan(mut self) -> Self {
self.plan = Some(PlanState::default());
@@ -1136,7 +1124,6 @@ impl ToolTestFixture {
ToolContext {
project_manager: &self.project_manager,
command_executor: &self.command_executor,
- working_memory: self.working_memory.as_mut(),
plan: self.plan.as_mut(),
ui: self.ui.as_ref().map(|ui| ui as &dyn UserInterface),
tool_id: self.tool_id.clone(),
@@ -1155,16 +1142,6 @@ impl ToolTestFixture {
&self.project_manager
}
- /// Get a reference to the working memory for assertions
- pub fn working_memory(&self) -> Option<&WorkingMemory> {
- self.working_memory.as_ref()
- }
-
- /// Get a mutable reference to the working memory for modifications
- pub fn working_memory_mut(&mut self) -> Option<&mut WorkingMemory> {
- self.working_memory.as_mut()
- }
-
/// Get a reference to the plan state for assertions
pub fn plan(&self) -> Option<&PlanState> {
self.plan.as_ref()
diff --git a/crates/code_assistant/src/tools/core/tool.rs b/crates/code_assistant/src/tools/core/tool.rs
index 927ea513..8236958e 100644
--- a/crates/code_assistant/src/tools/core/tool.rs
+++ b/crates/code_assistant/src/tools/core/tool.rs
@@ -2,7 +2,7 @@ use super::render::Render;
use super::result::ToolResult;
use super::spec::ToolSpec;
use crate::permissions::PermissionMediator;
-use crate::types::{PlanState, WorkingMemory};
+use crate::types::PlanState;
use anyhow::{anyhow, Result};
use command_executor::CommandExecutor;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
@@ -13,8 +13,6 @@ pub struct ToolContext<'a> {
pub project_manager: &'a dyn crate::config::ProjectManager,
/// Command executor for running shell commands
pub command_executor: &'a dyn CommandExecutor,
- /// Optional working memory (available in WorkingMemoryAgent mode)
- pub working_memory: Option<&'a mut WorkingMemory>,
/// Optional plan state reference for plan-related tools
pub plan: Option<&'a mut PlanState>,
/// Optional UI instance for streaming output
diff --git a/crates/code_assistant/src/tools/impls/delete_files.rs b/crates/code_assistant/src/tools/impls/delete_files.rs
index d71badfa..ab15bbb1 100644
--- a/crates/code_assistant/src/tools/impls/delete_files.rs
+++ b/crates/code_assistant/src/tools/impls/delete_files.rs
@@ -149,12 +149,14 @@ impl Tool for DeleteFilesTool {
Ok(_) => {
deleted.push(path.clone());
- // If we have a working memory reference, remove the deleted file
- if let Some(working_memory) = &mut context.working_memory {
- // Remove from loaded resources
- working_memory
- .loaded_resources
- .remove(&(input.project.clone(), path.clone()));
+ // Emit resource event
+ if let Some(ui) = context.ui {
+ let _ = ui
+ .send_event(crate::ui::UiEvent::ResourceDeleted {
+ project: input.project.clone(),
+ path: path.clone(),
+ })
+ .await;
}
}
Err(e) => {
@@ -200,26 +202,13 @@ mod tests {
}
#[tokio::test]
- async fn test_delete_files_working_memory_update() -> Result<()> {
- // Create test fixture with working memory
+ async fn test_delete_files_emits_resource_deleted_event() -> Result<()> {
+ // Create test fixture with UI
let mut fixture = ToolTestFixture::with_files(vec![
("file1.txt".to_string(), "File 1 content".to_string()),
("file2.txt".to_string(), "File 2 content".to_string()),
])
- .with_working_memory();
-
- // Pre-populate working memory with files
- {
- let working_memory = fixture.working_memory_mut().unwrap();
- working_memory.loaded_resources.insert(
- ("test-project".to_string(), PathBuf::from("file1.txt")),
- crate::types::LoadedResource::File("File 1 content".to_string()),
- );
- working_memory.loaded_resources.insert(
- ("test-project".to_string(), PathBuf::from("file2.txt")),
- crate::types::LoadedResource::File("File 2 content".to_string()),
- );
- }
+ .with_ui();
let mut context = fixture.context();
@@ -238,15 +227,16 @@ mod tests {
assert_eq!(result.deleted[0], PathBuf::from("file1.txt"));
assert!(result.failed.is_empty());
- // Verify working memory updates
- let working_memory = fixture.working_memory().unwrap();
- assert_eq!(working_memory.loaded_resources.len(), 1);
- assert!(!working_memory
- .loaded_resources
- .contains_key(&("test-project".to_string(), PathBuf::from("file1.txt"))));
- assert!(working_memory
- .loaded_resources
- .contains_key(&("test-project".to_string(), PathBuf::from("file2.txt"))));
+ // Drop context to release borrow
+ drop(context);
+
+ // Verify ResourceDeleted event was emitted
+ let events = fixture.ui().unwrap().events();
+ assert!(events.iter().any(|e| matches!(
+ e,
+ crate::ui::UiEvent::ResourceDeleted { project, path }
+ if project == "test-project" && path == &PathBuf::from("file1.txt")
+ )));
Ok(())
}
diff --git a/crates/code_assistant/src/tools/impls/edit.rs b/crates/code_assistant/src/tools/impls/edit.rs
index bc0910e1..4ea1e1c8 100644
--- a/crates/code_assistant/src/tools/impls/edit.rs
+++ b/crates/code_assistant/src/tools/impls/edit.rs
@@ -1,7 +1,6 @@
use crate::tools::core::{
Render, ResourcesTracker, Tool, ToolContext, ToolResult, ToolScope, ToolSpec,
};
-use crate::types::LoadedResource;
use anyhow::{anyhow, Result};
use fs_explorer::{FileReplacement, FileUpdaterError};
use serde::{Deserialize, Serialize};
@@ -190,7 +189,7 @@ impl Tool for EditTool {
};
match format_result {
- Ok((new_content, updated_replacements)) => {
+ Ok((_new_content, updated_replacements)) => {
// If formatting updated the replacement parameters, update our input
if let Some(updated) = updated_replacements {
if let Some(updated_replacement) = updated.first() {
@@ -200,13 +199,14 @@ impl Tool for EditTool {
}
}
- // If we have a working memory reference, update it with the modified file
- if let Some(working_memory) = &mut context.working_memory {
- // Add the file with new content to working memory
- working_memory.loaded_resources.insert(
- (input.project.clone(), path.clone()),
- LoadedResource::File(new_content.clone()),
- );
+ // Emit resource event
+ if let Some(ui) = context.ui {
+ let _ = ui
+ .send_event(crate::ui::UiEvent::ResourceWritten {
+ project: input.project.clone(),
+ path: path.clone(),
+ })
+ .await;
}
Ok(EditOutput {
@@ -285,12 +285,12 @@ mod tests {
#[tokio::test]
async fn test_edit_basic_replacement() -> Result<()> {
- // Create test fixture with working memory
+ // Create test fixture with UI for event capture
let mut fixture = ToolTestFixture::with_files(vec![(
"test.rs".to_string(),
"fn original() {\n println!(\"Original\");\n}".to_string(),
)])
- .with_working_memory();
+ .with_ui();
let mut context = fixture.context();
// Create input for a valid replacement
@@ -309,30 +309,28 @@ mod tests {
// Verify the result
assert!(result.error.is_none());
- // Verify that working memory was updated
- let working_memory = fixture.working_memory().unwrap();
- assert_eq!(working_memory.loaded_resources.len(), 1);
+ // Drop context to release borrow
+ drop(context);
- // Verify the content in working memory
- let key = ("test-project".to_string(), PathBuf::from("test.rs"));
- if let Some(LoadedResource::File(content)) = working_memory.loaded_resources.get(&key) {
- assert!(content.contains("fn renamed()"));
- assert!(content.contains("println!(\"Updated\")"));
- } else {
- panic!("File not found in working memory or wrong resource type");
- }
+ // Verify that ResourceWritten event was emitted
+ let events = fixture.ui().unwrap().events();
+ assert!(events.iter().any(|e| matches!(
+ e,
+ crate::ui::UiEvent::ResourceWritten { project, path }
+ if project == "test-project" && path == &PathBuf::from("test.rs")
+ )));
Ok(())
}
#[tokio::test]
async fn test_edit_replace_all() -> Result<()> {
- // Create test fixture with working memory
+ // Create test fixture with UI for event capture
let mut fixture = ToolTestFixture::with_files(vec![(
"test.js".to_string(),
"console.log('test1');\nconsole.log('test2');\nconsole.log('test3');".to_string(),
)])
- .with_working_memory();
+ .with_ui();
let mut context = fixture.context();
// Create input for replace all
@@ -351,18 +349,6 @@ mod tests {
// Verify the result
assert!(result.error.is_none());
- // Verify the content in working memory
- let working_memory = fixture.working_memory().unwrap();
- let key = ("test-project".to_string(), PathBuf::from("test.js"));
- if let Some(LoadedResource::File(content)) = working_memory.loaded_resources.get(&key) {
- assert!(content.contains("logger.debug('test1')"));
- assert!(content.contains("logger.debug('test2')"));
- assert!(content.contains("logger.debug('test3')"));
- assert!(!content.contains("console.log"));
- } else {
- panic!("File not found in working memory or wrong resource type");
- }
-
Ok(())
}
@@ -426,7 +412,7 @@ mod tests {
"fn test() {\n // TODO: Remove this comment\n println!(\"Hello\");\n}"
.to_string(),
)])
- .with_working_memory();
+ .with_ui();
let mut context = fixture.context();
// Delete the TODO comment
@@ -444,17 +430,6 @@ mod tests {
// Verify the result
assert!(result.error.is_none());
- // Verify the content in working memory
- let working_memory = fixture.working_memory().unwrap();
- let key = ("test-project".to_string(), PathBuf::from("test.rs"));
- if let Some(LoadedResource::File(content)) = working_memory.loaded_resources.get(&key) {
- assert!(!content.contains("TODO"));
- assert!(content.contains("fn test() {"));
- assert!(content.contains("println!(\"Hello\");"));
- } else {
- panic!("File not found in working memory or wrong resource type");
- }
-
Ok(())
}
@@ -467,7 +442,7 @@ mod tests {
"function test() {\r\n console.log('test');\r\n}".to_string(),
), // CRLF endings
])
- .with_working_memory();
+ .with_ui();
let mut context = fixture.context();
// Use LF endings in search text, should still match CRLF in file
@@ -485,17 +460,6 @@ mod tests {
// Verify the result
assert!(result.error.is_none());
- // Verify the content in working memory
- let working_memory = fixture.working_memory().unwrap();
- let key = ("test-project".to_string(), PathBuf::from("test.rs"));
- if let Some(LoadedResource::File(content)) = working_memory.loaded_resources.get(&key) {
- assert!(content.contains("function answer()"));
- assert!(content.contains("return 42;"));
- assert!(!content.contains("console.log"));
- } else {
- panic!("File not found in working memory or wrong resource type");
- }
-
Ok(())
}
}
diff --git a/crates/code_assistant/src/tools/impls/list_files.rs b/crates/code_assistant/src/tools/impls/list_files.rs
index ab879913..3178e359 100644
--- a/crates/code_assistant/src/tools/impls/list_files.rs
+++ b/crates/code_assistant/src/tools/impls/list_files.rs
@@ -2,7 +2,7 @@ use crate::tools::core::{
Render, ResourcesTracker, Tool, ToolContext, ToolResult, ToolScope, ToolSpec,
};
use anyhow::Result;
-use fs_explorer::{FileSystemEntryType, FileTreeEntry};
+use fs_explorer::FileTreeEntry;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::path::PathBuf;
@@ -23,50 +23,6 @@ pub struct ListFilesOutput {
pub failed_paths: Vec<(String, String)>,
}
-// Helper function to update file tree in working memory
-fn update_tree_entry(
- parent: &mut FileTreeEntry,
- path: PathBuf,
- entry: FileTreeEntry,
-) -> Result<()> {
- let components: Vec<_> = path.components().collect();
- if components.is_empty() {
- // Replace current node with new entry
- *parent = entry;
- return Ok(());
- }
-
- // Process path components
- let first = components[0].as_os_str().to_string_lossy().to_string();
- let remaining = if components.len() > 1 {
- let mut new_path = PathBuf::new();
- for component in &components[1..] {
- new_path.push(component);
- }
- Some(new_path)
- } else {
- None
- };
-
- // Insert or update the child node
- if let Some(remaining_path) = remaining {
- let child = parent
- .children
- .entry(first.clone())
- .or_insert_with(|| FileTreeEntry {
- name: first.clone(),
- entry_type: FileSystemEntryType::Directory,
- children: std::collections::HashMap::new(),
- is_expanded: true,
- });
- update_tree_entry(child, remaining_path, entry)?;
- } else {
- parent.children.insert(first, entry);
- }
-
- Ok(())
-}
-
// Render implementation for output formatting
impl Render for ListFilesOutput {
fn status(&self) -> String {
@@ -217,45 +173,15 @@ impl Tool for ListFilesTool {
}
}
- // If we have a working memory reference, update it with the expanded paths
- if let Some(working_memory) = &mut context.working_memory {
- // Create file tree for this project if it doesn't exist yet
- let file_tree = working_memory
- .file_trees
- .entry(input.project.clone())
- .or_insert_with(|| FileTreeEntry {
- name: input.project.clone(),
- entry_type: FileSystemEntryType::Directory,
- children: std::collections::HashMap::new(),
- is_expanded: true,
- });
-
- // Update file tree with each entry
- for (path, entry) in &expanded_paths {
- if let Err(e) = update_tree_entry(file_tree, path.clone(), entry.clone()) {
- eprintln!("Error updating tree entry: {e}");
- // Continue with other entries even if one fails
- }
- }
-
- // Store expanded directories for this project
- let project_paths = working_memory
- .expanded_directories
- .entry(input.project.clone())
- .or_insert_with(Vec::new);
-
- // Add all paths that were listed for this project
+ // Emit directory listed events for each successful path
+ if let Some(ui) = context.ui {
for (path, _) in &expanded_paths {
- if !project_paths.contains(path) {
- project_paths.push(path.clone());
- }
- }
-
- // Make sure project is in available_projects list
- if !working_memory.available_projects.contains(&input.project) {
- working_memory
- .available_projects
- .push(input.project.clone());
+ let _ = ui
+ .send_event(crate::ui::UiEvent::DirectoryListed {
+ project: input.project.clone(),
+ path: path.clone(),
+ })
+ .await;
}
}
diff --git a/crates/code_assistant/src/tools/impls/perplexity_ask.rs b/crates/code_assistant/src/tools/impls/perplexity_ask.rs
index 6414d5a9..94f68810 100644
--- a/crates/code_assistant/src/tools/impls/perplexity_ask.rs
+++ b/crates/code_assistant/src/tools/impls/perplexity_ask.rs
@@ -4,7 +4,6 @@ use crate::tools::core::{
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::json;
-use std::path::PathBuf;
use web::{PerplexityCitation, PerplexityClient, PerplexityMessage};
// Input type for the perplexity_ask tool
@@ -122,7 +121,7 @@ impl Tool for PerplexityAskTool {
async fn execute<'a>(
&self,
- context: &mut ToolContext<'a>,
+ _context: &mut ToolContext<'a>,
input: &mut Self::Input,
) -> Result {
// Check if the API key exists
@@ -142,48 +141,12 @@ impl Tool for PerplexityAskTool {
// Call Perplexity API
match client.ask(&input.messages, None).await {
- Ok(response) => {
- // Update working memory if available
- if let Some(working_memory) = &mut context.working_memory {
- // Store the result as a resource with a synthetic path
- let path = PathBuf::from(format!(
- "perplexity-ask-{}",
- percent_encoding::utf8_percent_encode(
- &query,
- percent_encoding::NON_ALPHANUMERIC
- )
- ));
-
- // Format the answer with citations
- let mut full_answer = response.content.clone();
- if !response.citations.is_empty() {
- full_answer.push_str("\n\nCitations:\n");
- for (i, citation) in response.citations.iter().enumerate() {
- full_answer.push_str(&format!(
- "[{}] {}: {}\n",
- i + 1,
- citation.text,
- citation.url
- ));
- }
- }
-
- // Store as a file resource
- let project = "perplexity".to_string();
- working_memory.add_resource(
- project,
- path,
- crate::types::LoadedResource::File(full_answer),
- );
- }
-
- Ok(PerplexityAskOutput {
- query,
- answer: response.content,
- citations: response.citations,
- error: None,
- })
- }
+ Ok(response) => Ok(PerplexityAskOutput {
+ query,
+ answer: response.content,
+ citations: response.citations,
+ error: None,
+ }),
Err(e) => Ok(PerplexityAskOutput {
query,
answer: String::new(),
diff --git a/crates/code_assistant/src/tools/impls/read_files.rs b/crates/code_assistant/src/tools/impls/read_files.rs
index 612850b5..046c8c6f 100644
--- a/crates/code_assistant/src/tools/impls/read_files.rs
+++ b/crates/code_assistant/src/tools/impls/read_files.rs
@@ -2,7 +2,6 @@ use crate::tools::core::{
Render, ResourcesTracker, Tool, ToolContext, ToolResult, ToolScope, ToolSpec,
};
use crate::tools::parse::PathWithLineRange;
-use crate::types::LoadedResource;
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -209,10 +208,9 @@ impl Tool for ReadFilesTool {
}
}
- // If we have a working memory reference, update it with the loaded files
- if let Some(working_memory) = &mut context.working_memory {
- // Store successfully loaded files in working memory
- for (path, content) in &loaded_files {
+ // Emit resource events for loaded files
+ if let Some(ui) = context.ui {
+ for path in loaded_files.keys() {
// Get the base path without any line range information
let base_path =
if let Ok(parsed) = PathWithLineRange::parse(path.to_str().unwrap_or("")) {
@@ -220,10 +218,12 @@ impl Tool for ReadFilesTool {
} else {
path.clone()
};
- working_memory.loaded_resources.insert(
- (input.project.clone(), base_path.clone()),
- LoadedResource::File(content.clone()),
- );
+ let _ = ui
+ .send_event(crate::ui::UiEvent::ResourceLoaded {
+ project: input.project.clone(),
+ path: base_path,
+ })
+ .await;
}
}
@@ -266,7 +266,9 @@ mod tests {
}
#[tokio::test]
- async fn test_read_files_updates_memory() -> Result<()> {
+ async fn test_read_files_emits_resource_loaded_events() -> Result<()> {
+ use crate::ui::UiEvent;
+
// Create a tool registry
let registry = ToolRegistry::global();
@@ -275,7 +277,7 @@ mod tests {
.get("read_files")
.expect("read_files tool should be registered");
- // Create test fixture with files and working memory
+ // Create test fixture with files and UI for event capture
let mut fixture = ToolTestFixture::with_files(vec![
(
"test.txt".to_string(),
@@ -283,7 +285,7 @@ mod tests {
),
("test2.txt".to_string(), "Another file content".to_string()),
])
- .with_working_memory();
+ .with_ui();
let mut context = fixture.context();
// Parameters for read_files
@@ -302,17 +304,30 @@ mod tests {
// Check the output
assert!(output.contains("Successfully loaded"));
- // Verify that the files were added to working memory
- let working_memory = fixture.working_memory().unwrap();
- assert_eq!(working_memory.loaded_resources.len(), 2);
-
- // Check that both files are in the working memory
- assert!(working_memory
- .loaded_resources
- .contains_key(&("test-project".to_string(), "test.txt".into())));
- assert!(working_memory
- .loaded_resources
- .contains_key(&("test-project".to_string(), "test2.txt".into())));
+ // Drop context to release borrow before checking events
+ drop(context);
+
+ // Verify ResourceLoaded events were emitted
+ let events = fixture.ui().unwrap().events();
+ let resource_events: Vec<_> = events
+ .iter()
+ .filter(|e| matches!(e, UiEvent::ResourceLoaded { .. }))
+ .collect();
+
+ assert_eq!(resource_events.len(), 2, "Expected 2 ResourceLoaded events");
+
+ // Check that both files have events
+ let has_test_txt = events.iter().any(|e| {
+ matches!(e, UiEvent::ResourceLoaded { project, path }
+ if project == "test-project" && path == &PathBuf::from("test.txt"))
+ });
+ let has_test2_txt = events.iter().any(|e| {
+ matches!(e, UiEvent::ResourceLoaded { project, path }
+ if project == "test-project" && path == &PathBuf::from("test2.txt"))
+ });
+
+ assert!(has_test_txt, "Expected ResourceLoaded event for test.txt");
+ assert!(has_test2_txt, "Expected ResourceLoaded event for test2.txt");
Ok(())
}
diff --git a/crates/code_assistant/src/tools/impls/replace_in_file.rs b/crates/code_assistant/src/tools/impls/replace_in_file.rs
index 4582fbac..0741174b 100644
--- a/crates/code_assistant/src/tools/impls/replace_in_file.rs
+++ b/crates/code_assistant/src/tools/impls/replace_in_file.rs
@@ -2,7 +2,6 @@ use crate::tools::core::{
Render, ResourcesTracker, Tool, ToolContext, ToolResult, ToolScope, ToolSpec,
};
use crate::tools::parse::parse_search_replace_blocks;
-use crate::types::LoadedResource;
use anyhow::{anyhow, Result};
use fs_explorer::{FileReplacement, FileUpdaterError};
use serde::{Deserialize, Serialize};
@@ -220,17 +219,20 @@ impl Tool for ReplaceInFileTool {
};
match result {
- Ok((new_content, updated_replacements)) => {
+ Ok((_new_content, updated_replacements)) => {
// If we have updated replacements (after formatting), update the diff text
if let Some(updated) = updated_replacements {
input.diff = render_diff_from_replacements(&updated);
}
- if let Some(working_memory) = &mut context.working_memory {
- working_memory.loaded_resources.insert(
- (input.project.clone(), path.clone()),
- LoadedResource::File(new_content.clone()),
- );
+ // Emit resource event
+ if let Some(ui) = context.ui {
+ let _ = ui
+ .send_event(crate::ui::UiEvent::ResourceWritten {
+ project: input.project.clone(),
+ path: path.clone(),
+ })
+ .await;
}
Ok(ReplaceInFileOutput {
@@ -305,13 +307,14 @@ mod tests {
}
#[tokio::test]
- async fn test_replace_in_file_working_memory_update() -> Result<()> {
- // Create test fixture with working memory
+
+ async fn test_replace_in_file_emits_resource_event() -> Result<()> {
+ // Create test fixture with UI for event capture
let mut fixture = ToolTestFixture::with_files(vec![(
"test.rs".to_string(),
"fn original() {\n println!(\"Original\");\n}".to_string(),
)])
- .with_working_memory();
+ .with_ui();
let mut context = fixture.context();
// Create input for a valid replacement
@@ -328,18 +331,16 @@ mod tests {
// Verify the result
assert!(result.error.is_none());
- // Verify that working memory was updated
- let working_memory = fixture.working_memory().unwrap();
- assert_eq!(working_memory.loaded_resources.len(), 1);
+ // Drop context to release borrow
+ drop(context);
- // Verify the content in working memory
- let key = ("test-project".to_string(), PathBuf::from("test.rs"));
- if let Some(LoadedResource::File(content)) = working_memory.loaded_resources.get(&key) {
- assert!(content.contains("fn renamed()"));
- assert!(content.contains("println!(\"Updated\")"));
- } else {
- panic!("File not found in working memory or wrong resource type");
- }
+ // Verify that ResourceWritten event was emitted
+ let events = fixture.ui().unwrap().events();
+ assert!(events.iter().any(|e| matches!(
+ e,
+ crate::ui::UiEvent::ResourceWritten { project, path }
+ if project == "test-project" && path == &PathBuf::from("test.rs")
+ )));
Ok(())
}
diff --git a/crates/code_assistant/src/tools/impls/web_fetch.rs b/crates/code_assistant/src/tools/impls/web_fetch.rs
index a7b3bb36..8b6c5d12 100644
--- a/crates/code_assistant/src/tools/impls/web_fetch.rs
+++ b/crates/code_assistant/src/tools/impls/web_fetch.rs
@@ -103,7 +103,7 @@ impl Tool for WebFetchTool {
async fn execute<'a>(
&self,
- context: &mut ToolContext<'a>,
+ _context: &mut ToolContext<'a>,
input: &mut Self::Input,
) -> Result {
// Create new client for each request
@@ -119,26 +119,7 @@ impl Tool for WebFetchTool {
// Fetch the page
match client.fetch(&input.url).await {
- Ok(page) => {
- // Update working memory if available
- if let Some(working_memory) = &mut context.working_memory {
- // Use the URL as path (normalized)
- let path =
- std::path::PathBuf::from(page.url.replace([':', '/', '?', '#'], "_"));
-
- // Use "web" as the project name for web resources
- let project = "web".to_string();
-
- // Store in working memory
- working_memory.add_resource(
- project,
- path,
- crate::types::LoadedResource::WebPage(page.clone()),
- );
- }
-
- Ok(WebFetchOutput { page, error: None })
- }
+ Ok(page) => Ok(WebFetchOutput { page, error: None }),
Err(e) => Ok(WebFetchOutput {
page: WebPage::default(),
error: Some(e.to_string()),
diff --git a/crates/code_assistant/src/tools/impls/write_file.rs b/crates/code_assistant/src/tools/impls/write_file.rs
index 9cecb0dc..9782934d 100644
--- a/crates/code_assistant/src/tools/impls/write_file.rs
+++ b/crates/code_assistant/src/tools/impls/write_file.rs
@@ -1,7 +1,6 @@
use crate::tools::core::{
Render, ResourcesTracker, Tool, ToolContext, ToolResult, ToolScope, ToolSpec,
};
-use crate::types::LoadedResource;
use anyhow::Result;
use command_executor::SandboxCommandRequest;
use serde::{Deserialize, Serialize};
@@ -185,12 +184,14 @@ impl Tool for WriteFileTool {
}
}
- // Update working memory if present
- if let Some(working_memory) = &mut context.working_memory {
- working_memory.loaded_resources.insert(
- (input.project.clone(), path.clone()),
- LoadedResource::File(full_content.clone()),
- );
+ // Emit resource event
+ if let Some(ui) = context.ui {
+ let _ = ui
+ .send_event(crate::ui::UiEvent::ResourceWritten {
+ project: input.project.clone(),
+ path: path.clone(),
+ })
+ .await;
}
Ok(WriteFileOutput {
@@ -246,7 +247,8 @@ mod tests {
let write_file_tool = WriteFileTool;
// Create test fixture with working memory
- let mut fixture = ToolTestFixture::new().with_working_memory();
+
+ let mut fixture = ToolTestFixture::new().with_ui();
let mut context = fixture.context();
// Parameters for write_file
@@ -263,37 +265,30 @@ mod tests {
// Check the result
assert!(result.error.is_none());
- // Verify that the file was added to working memory
- let working_memory = fixture.working_memory().unwrap();
- assert_eq!(working_memory.loaded_resources.len(), 1);
-
- // Check that the file is in the working memory
- let resource_key = ("test".to_string(), PathBuf::from("test.txt"));
- assert!(working_memory.loaded_resources.contains_key(&resource_key));
+ // Drop context to release borrow
+ drop(context);
- // Check that the content matches
- if let Some(LoadedResource::File(content)) =
- working_memory.loaded_resources.get(&resource_key)
- {
- assert_eq!(content, "Test content");
- } else {
- panic!("Expected file resource in working memory");
- }
+ // Verify that ResourceWritten event was emitted
+ let events = fixture.ui().unwrap().events();
+ assert!(events.iter().any(|e| matches!(
+ e,
+ crate::ui::UiEvent::ResourceWritten { project, path }
+ if project == "test" && path == &PathBuf::from("test.txt")
+ )));
Ok(())
}
#[tokio::test]
- async fn test_write_file_append_has_memory_update() -> Result<()> {
- // Create a tool registry (not needed for this test but kept for consistency)
+ async fn test_write_file_append_emits_event() -> Result<()> {
let write_file_tool = WriteFileTool;
- // Create test fixture with existing file and working memory
+ // Create test fixture with existing file and UI
let mut fixture = ToolTestFixture::with_files(vec![(
"test.txt".to_string(),
"Initial content".to_string(),
)])
- .with_working_memory();
+ .with_ui();
let mut context = fixture.context();
// Parameters for write_file with append=true
@@ -310,23 +305,16 @@ mod tests {
// Check the result
assert!(result.error.is_none());
- // Verify that the file WAS added to working memory (we fixed the behavior)
- let working_memory = fixture.working_memory().unwrap();
- assert_eq!(working_memory.loaded_resources.len(), 1);
-
- // Check that the file is in the working memory
- let resource_key = ("test-project".to_string(), PathBuf::from("test.txt"));
- assert!(working_memory.loaded_resources.contains_key(&resource_key));
+ // Drop context to release borrow
+ drop(context);
- // Check that the content is the combined content (initial + appended)
- if let Some(LoadedResource::File(content)) =
- working_memory.loaded_resources.get(&resource_key)
- {
- assert!(content.contains("Initial content"));
- assert!(content.contains("Test content"));
- } else {
- panic!("Expected file resource in working memory");
- }
+ // Verify that ResourceWritten event was emitted
+ let events = fixture.ui().unwrap().events();
+ assert!(events.iter().any(|e| matches!(
+ e,
+ crate::ui::UiEvent::ResourceWritten { project, path }
+ if project == "test-project" && path == &PathBuf::from("test.txt")
+ )));
Ok(())
}
diff --git a/crates/code_assistant/src/types.rs b/crates/code_assistant/src/types.rs
index cb890e8f..325e9c50 100644
--- a/crates/code_assistant/src/types.rs
+++ b/crates/code_assistant/src/types.rs
@@ -178,13 +178,6 @@ impl std::fmt::Display for LoadedResource {
}
}
-impl WorkingMemory {
- /// Add a new resource to working memory
- pub fn add_resource(&mut self, project: String, path: PathBuf, resource: LoadedResource) {
- self.loaded_resources.insert((project, path), resource);
- }
-}
-
/// Tool description for LLM
#[derive(Debug, thiserror::Error)]
pub enum ToolError {
diff --git a/crates/code_assistant/src/ui/gpui/chat_sidebar.rs b/crates/code_assistant/src/ui/gpui/chat_sidebar.rs
index 429e6eba..d5ba0525 100644
--- a/crates/code_assistant/src/ui/gpui/chat_sidebar.rs
+++ b/crates/code_assistant/src/ui/gpui/chat_sidebar.rs
@@ -7,7 +7,8 @@ use gpui::{
Styled, Subscription, Window,
};
use gpui_component::scroll::ScrollbarAxis;
-use gpui_component::{tooltip::Tooltip, ActiveTheme, Icon, StyledExt};
+
+use gpui_component::{tooltip::Tooltip, ActiveTheme, Icon, Sizable, Size, StyledExt};
use std::time::SystemTime;
use tracing::debug;
@@ -464,7 +465,12 @@ impl ChatSidebar {
cx: &mut Context,
) {
debug!("New chat button clicked");
- // Emit event to parent to create a new chat session
+ self.request_new_session(cx);
+ }
+
+ /// Request creation of a new chat session
+ pub fn request_new_session(&mut self, cx: &mut Context) {
+ debug!("Requesting new chat session");
cx.emit(ChatSidebarEvent::NewSessionRequested { name: None });
}
@@ -501,7 +507,7 @@ impl Focusable for ChatSidebar {
impl Render for ChatSidebar {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement {
if self.is_collapsed {
- // Collapsed view - narrow bar with toggle button
+ // Collapsed view - narrow bar with new chat button
div()
.id("collapsed-chat-sidebar")
.flex_none()
@@ -517,17 +523,20 @@ impl Render for ChatSidebar {
.py_2()
.child(
div()
- .size(px(24.))
- .rounded_full()
+ .size(px(28.))
+ .rounded_sm()
.flex()
.items_center()
.justify_center()
- .child(file_icons::render_icon(
- &file_icons::get().get_type_icon(file_icons::MESSAGE_BUBBLES),
- 16.0,
- cx.theme().muted_foreground,
- "💬",
- )),
+ .cursor_pointer()
+ .hover(|s| s.bg(cx.theme().muted))
+ .child(
+ Icon::default()
+ .path(SharedString::from("icons/plus.svg"))
+ .with_size(Size::Small)
+ .text_color(cx.theme().muted_foreground),
+ )
+ .on_mouse_up(MouseButton::Left, cx.listener(Self::on_new_chat_click)),
)
} else {
// Full sidebar view
diff --git a/crates/code_assistant/src/ui/gpui/file_icons.rs b/crates/code_assistant/src/ui/gpui/file_icons.rs
index e43d05db..09fcc219 100644
--- a/crates/code_assistant/src/ui/gpui/file_icons.rs
+++ b/crates/code_assistant/src/ui/gpui/file_icons.rs
@@ -1,12 +1,9 @@
use gpui::{div, px, svg, App, AssetSource, IntoElement, ParentElement, SharedString, Styled};
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
-use std::path::Path;
use std::sync::{Arc, Mutex, OnceLock};
use tracing::{debug, trace, warn};
-use crate::ui::gpui::path_util::PathExt;
-
/// Represents icon information for different file types
#[derive(Deserialize, Debug)]
struct TypeConfig {
@@ -25,31 +22,14 @@ struct FileTypesConfig {
pub struct FileIcons {
/// The loaded configuration from file_types.json
config: FileTypesConfig,
- /// Fallback emoji icons for when SVGs aren't available
- fallback_stems: HashMap,
- fallback_suffixes: HashMap,
/// Set of already logged missing icon paths to avoid duplicate warnings
logged_missing_icons: Mutex>,
}
-// Public icon type constants that already exist in file_types.json
-pub const DIRECTORY_COLLAPSED: &str = "collapsed_folder"; // folder.svg
-pub const DIRECTORY_EXPANDED: &str = "expanded_folder"; // folder_open.svg
- // pub const CHEVRON_LEFT: &str = "chevron_left"; // chevron_left.svg
- // pub const CHEVRON_RIGHT: &str = "chevron_right"; // chevron_right.svg
+// Public icon type constants
pub const CHEVRON_DOWN: &str = "chevron_down"; // chevron_down.svg
pub const CHEVRON_UP: &str = "chevron_up"; // chevron_up.svg
pub const WORKING_MEMORY: &str = "brain"; // brain.svg
-pub const LIBRARY: &str = "library"; // library.svg
-pub const FILE_TREE: &str = "file_tree"; // file_tree.svg
-pub const MAGNIFYING_GLASS: &str = "magnifying_glass"; // magnifying_glass.svg
-pub const HTML: &str = "template"; // html.svg
-pub const DEFAULT: &str = "default"; // file.svg
-
-pub const PANEL_RIGHT_CLOSE: &str = "panel_right_close"; // panel_right_close.svg
-pub const PANEL_RIGHT_OPEN: &str = "panel_right_open"; // panel_right_open.svg
-pub const THEME_DARK: &str = "theme_dark"; // theme_dark.svg
-pub const THEME_LIGHT: &str = "theme_light"; // theme_light.svg
pub const SEND: &str = "send"; // send.svg
pub const STOP: &str = "stop"; // circle_stop.svg
@@ -57,7 +37,6 @@ pub const MESSAGE_BUBBLES: &str = "message_bubbles"; // message_bubbles.svg
pub const PLUS: &str = "plus"; // plus.svg
// Tool-specific icon mappings to actual SVG files
-// These are direct constants defining the paths to SVG icons or existing types
pub const TOOL_READ_FILES: &str = "search_code"; // search_code.svg
pub const TOOL_LIST_FILES: &str = "reveal"; // reveal.svg
pub const TOOL_EXECUTE_COMMAND: &str = "terminal"; // terminal.svg
@@ -83,32 +62,8 @@ impl FileIcons {
// Load the configuration from the JSON file
let config = Self::load_config(&assets);
- // Initialize fallback emoji mappings
- let mut fallback_stems = HashMap::new();
- let mut fallback_suffixes = HashMap::new();
-
- // Initialize with common file types as fallbacks
- fallback_suffixes.insert("rs".to_string(), "🦀".to_string());
- fallback_suffixes.insert("js".to_string(), "📜".to_string());
- fallback_suffixes.insert("jsx".to_string(), "⚛️".to_string());
- fallback_suffixes.insert("ts".to_string(), "📘".to_string());
- fallback_suffixes.insert("tsx".to_string(), "⚛️".to_string());
- fallback_suffixes.insert("py".to_string(), "🐍".to_string());
- fallback_suffixes.insert("html".to_string(), "🌐".to_string());
- fallback_suffixes.insert("css".to_string(), "🎨".to_string());
- fallback_suffixes.insert("json".to_string(), "📋".to_string());
- fallback_suffixes.insert("md".to_string(), "📝".to_string());
-
- // Special file stems
- fallback_stems.insert("Cargo.toml".to_string(), "📦".to_string());
- fallback_stems.insert("package.json".to_string(), "📦".to_string());
- fallback_stems.insert("Dockerfile".to_string(), "🐳".to_string());
- fallback_stems.insert("README.md".to_string(), "📚".to_string());
-
Self {
config,
- fallback_stems,
- fallback_suffixes,
logged_missing_icons: Mutex::new(HashSet::new()),
}
}
@@ -154,69 +109,6 @@ impl FileIcons {
}
}
- /// Get the appropriate icon for a file path
- pub fn get_icon(&self, path: &Path) -> Option {
- // Extract the stem or suffix from the path
- let suffix = match path.icon_stem_or_suffix() {
- Some(s) => s,
- None => {
- let path_str = path.to_string_lossy().to_string();
- self.log_missing_icon(
- &format!("[FileIcons]: No suffix found for path: {path:?}"),
- &format!("no_suffix:{path_str}"),
- );
- return self.get_type_icon(DEFAULT);
- }
- };
-
- // First check if we have a match in the stems mapping
- if let Some(type_str) = self.config.stems.get(suffix) {
- trace!(
- "[FileIcons]: Found stem match: '{}' -> '{}'",
- suffix,
- type_str
- );
- return self.get_type_icon(type_str);
- }
-
- // Then check if we have a match in the suffixes mapping
- if let Some(type_str) = self.config.suffixes.get(suffix) {
- trace!(
- "[FileIcons]: Found suffix match: '{}' -> '{}'",
- suffix,
- type_str
- );
- return self.get_type_icon(type_str);
- }
-
- // Try fallback stems for specific filenames
- if let Some(filename) = path.file_name() {
- if let Some(filename_str) = filename.to_str() {
- if let Some(icon) = self.fallback_stems.get(filename_str) {
- debug!(
- "[FileIcons]: Using fallback stem icon for: '{}'",
- filename_str
- );
- return Some(SharedString::from(icon.clone()));
- }
- }
- }
-
- // Try fallback suffixes for extensions
- if let Some(fallback) = self.fallback_suffixes.get(suffix) {
- debug!("[FileIcons]: Using fallback suffix icon for: '{}'", suffix);
- return Some(SharedString::from(fallback.clone()));
- }
-
- // Default icon
- let path_str = path.to_string_lossy().to_string();
- self.log_missing_icon(
- &format!("[FileIcons]: Using default icon for: {path:?}"),
- &format!("default_icon:{path_str}"),
- );
- self.get_type_icon(DEFAULT)
- }
-
/// Get icon based on type name - this is the core method that all icon lookups use
pub fn get_type_icon(&self, typ: &str) -> Option {
// First check if the type exists in the config
diff --git a/crates/code_assistant/src/ui/gpui/memory.rs b/crates/code_assistant/src/ui/gpui/memory.rs
deleted file mode 100644
index d4747ee5..00000000
--- a/crates/code_assistant/src/ui/gpui/memory.rs
+++ /dev/null
@@ -1,423 +0,0 @@
-use std::path::Path;
-use std::sync::{Arc, Mutex};
-
-use gpui::{div, prelude::*, px, App, Axis, Context, Entity, FocusHandle, Focusable, Window};
-use gpui_component::{ActiveTheme, StyledExt};
-
-use crate::types::{LoadedResource, WorkingMemory};
-use crate::ui::gpui::file_icons;
-use fs_explorer::{FileSystemEntryType, FileTreeEntry};
-
-// Entity for displaying loaded resources
-pub struct LoadedResourcesView {
- memory: Arc>>,
- focus_handle: FocusHandle,
-}
-
-impl LoadedResourcesView {
- pub fn new(memory: Arc>>, cx: &mut Context) -> Self {
- Self {
- memory,
- focus_handle: cx.focus_handle(),
- }
- }
-}
-
-impl Focusable for LoadedResourcesView {
- fn focus_handle(&self, _: &App) -> FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-impl Render for LoadedResourcesView {
- fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
- let memory_lock = self.memory.lock().unwrap();
-
- // Resources header (shared between both branches)
- let header = div()
- .id("resources-header")
- .flex_none()
- .text_sm()
- .w_full()
- .px_2()
- .bg(cx.theme().popover)
- .flex()
- .items_center()
- .justify_between()
- .text_color(cx.theme().foreground)
- .child(
- div()
- .flex()
- .items_center()
- .gap_2()
- .child(file_icons::render_icon(
- &file_icons::get().get_type_icon(file_icons::LIBRARY),
- 16.0,
- cx.theme().muted_foreground,
- "📚",
- ))
- .child("Loaded Resources"),
- );
-
- // Container
- let mut container = div()
- .id("resources-section")
- .flex_none()
- .bg(cx.theme().sidebar)
- .border_b_1()
- .border_color(cx.theme().sidebar_border)
- .flex()
- .flex_col();
-
- if let Some(memory) = &*memory_lock {
- // Add resources count to header
- let header_with_count = header.child(
- div()
- .text_xs()
- .text_color(cx.theme().muted_foreground)
- .child(format!("({})", memory.loaded_resources.len())),
- );
-
- // Resources content with scrollbar using .scrollable()
- let resources_content = div()
- .id("resources-content")
- .scrollable(Axis::Vertical)
- .flex()
- .flex_col()
- .p_1()
- .gap_1()
- .children(
- memory
- .loaded_resources
- .iter()
- .map(|((project, path), resource)| {
- // Get appropriate icon for resource type
- let icon = match resource {
- LoadedResource::File(_) => file_icons::get().get_icon(path),
- LoadedResource::WebSearch { .. } => {
- file_icons::get().get_type_icon(file_icons::MAGNIFYING_GLASS)
- }
- LoadedResource::WebPage(_) => {
- file_icons::get().get_type_icon(file_icons::HTML)
- }
- };
-
- div()
- .px_2()
- .w_full()
- .flex()
- .items_center()
- .justify_between()
- .gap_2()
- .child(file_icons::render_icon_container(
- &icon,
- 14.0,
- cx.theme().muted_foreground,
- "📄",
- ))
- .child(
- div()
- .text_color(cx.theme().foreground)
- .text_xs()
- .truncate()
- .flex_grow()
- .child(format!("{}/{}", project, path.to_string_lossy())),
- )
- }),
- );
-
- // Update container with the resources content
- container = container.child(header_with_count).child(
- div()
- .id("resources-list-container")
- .max_h(px(300.))
- .flex_grow()
- .child(resources_content),
- );
- } else {
- // Add empty message with header
- container = container.child(header).child(
- div()
- .p_2()
- .text_center()
- .text_color(cx.theme().muted_foreground)
- .child("No resources available"),
- );
- }
-
- container
- }
-}
-
-// Entity for displaying file tree
-pub struct FileTreeView {
- memory: Arc>>,
- focus_handle: FocusHandle,
-}
-
-impl FileTreeView {
- pub fn new(memory: Arc>>, cx: &mut Context) -> Self {
- Self {
- memory,
- focus_handle: cx.focus_handle(),
- }
- }
-
- // Render a single file tree entry item
- fn render_entry_item(
- &self,
- entry: &FileTreeEntry,
- indent_level: usize,
- cx: &Context,
- ) -> gpui::Div {
- // Get appropriate icon based on type and name
- let icon = match entry.entry_type {
- FileSystemEntryType::Directory => {
- // Get folder icon based on expanded state
- let icon_type = if entry.is_expanded {
- file_icons::DIRECTORY_EXPANDED
- } else {
- file_icons::DIRECTORY_COLLAPSED
- };
- file_icons::get().get_type_icon(icon_type)
- }
- FileSystemEntryType::File => {
- // Get file icon based on file extension
- let path = Path::new(&entry.name);
- file_icons::get().get_icon(path)
- }
- };
-
- let icon_color = cx.theme().muted_foreground;
-
- // Create the single item row
- div()
- .py(px(2.))
- .pl(px(indent_level as f32 * 16.0)) // Use 16px indentation per level
- .flex()
- .items_center()
- .gap_2()
- .w_full() // Ensure entry takes full width to prevent wrapping
- .flex_none() // Prevent item from growing or shrinking
- .child(file_icons::render_icon_container(
- &icon,
- 16.0,
- icon_color,
- match entry.entry_type {
- FileSystemEntryType::Directory => "📁",
- FileSystemEntryType::File => "📄",
- },
- ))
- .child(
- div()
- .text_xs()
- .font_weight(gpui::FontWeight(400.))
- .text_color(cx.theme().foreground)
- .child(entry.name.clone()),
- )
- }
-
- // Generate a flat list of all file tree entries with proper indentation
- fn generate_file_tree(
- &self,
- entry: &FileTreeEntry,
- indent_level: usize,
- cx: &Context,
- ) -> Vec {
- let mut result = Vec::new();
-
- // Add current entry
- result.push(self.render_entry_item(entry, indent_level, cx));
-
- // Add children if expanded
- if entry.is_expanded && !entry.children.is_empty() {
- // Sort children: directories first, then files, both alphabetically
- let mut children: Vec<&FileTreeEntry> = entry.children.values().collect();
- children.sort_by_key(|entry| {
- (
- // First sort criterion: directories before files
- matches!(entry.entry_type, FileSystemEntryType::File),
- // Second sort criterion: alphabetical by name (case insensitive)
- entry.name.to_lowercase(),
- )
- });
-
- // Process each child
- for child in children {
- // Recursively add this child and its children
- let child_items = self.generate_file_tree(child, indent_level + 1, cx);
- result.extend(child_items);
- }
- }
-
- result
- }
-}
-
-impl Focusable for FileTreeView {
- fn focus_handle(&self, _: &App) -> FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-impl Render for FileTreeView {
- fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
- let memory_lock = self.memory.lock().unwrap();
-
- // File tree header (shared between both branches)
- let header = div()
- .id("file-tree-header")
- .flex_none()
- .text_sm()
- .w_full()
- .px_2()
- .bg(cx.theme().popover)
- .flex()
- .items_center()
- .justify_between()
- .text_color(cx.theme().foreground)
- .child(
- div()
- .flex()
- .items_center()
- .gap_2()
- .child(file_icons::render_icon(
- &file_icons::get().get_type_icon(file_icons::FILE_TREE),
- 16.0,
- cx.theme().muted_foreground,
- "🌲",
- ))
- .child("File Tree"),
- );
-
- // Container for file tree section
- let mut container = div()
- .id("file-tree-section")
- .flex_1() // Take remaining space in parent container
- .min_h(px(100.)) // Minimum height to ensure scrolling works
- .flex()
- .flex_col();
-
- if let Some(memory) = &*memory_lock {
- // File tree content - generate a flat list of items
- if !memory.file_trees.is_empty() {
- let mut all_entries = Vec::new();
- for root_entry in memory.file_trees.values() {
- // Generate flat list of all entries for this project
- // The root entry is already the project name
- let entries = self.generate_file_tree(root_entry, 0, cx);
- all_entries.extend(entries);
- }
-
- let file_tree_content = div().flex().flex_col().w_full().children(all_entries);
-
- // Add the file tree container with scrollable content
- container = container.child(header).child(
- div()
- .id("file-tree-container")
- .flex_1() // Take remaining space in the parent container
- .min_h(px(100.)) // Minimum height to ensure scrolling works
- .child(
- div()
- .id("file-tree")
- .size_full()
- .scrollable(Axis::Vertical)
- .p_1()
- .child(file_tree_content),
- ),
- );
- } else {
- // No file trees
- container = container.child(header).child(
- div()
- .p_2()
- .text_color(cx.theme().muted_foreground)
- .text_center()
- .child("No file trees available"),
- );
- }
- } else {
- // No memory data
- container = container.child(header).child(
- div()
- .p_2()
- .text_center()
- .text_color(cx.theme().muted_foreground)
- .child("No file tree available"),
- );
- }
-
- container
- }
-}
-
-// Memory sidebar component
-pub struct MemoryView {
- focus_handle: FocusHandle,
- resources_view: Entity,
- file_tree_view: Entity,
-}
-
-impl MemoryView {
- pub fn new(memory: Arc>>, cx: &mut Context) -> Self {
- // Create sub-entities with the same memory
- let resources_view = cx.new(|cx| LoadedResourcesView::new(memory.clone(), cx));
- let file_tree_view = cx.new(|cx| FileTreeView::new(memory.clone(), cx));
-
- Self {
- focus_handle: cx.focus_handle(),
- resources_view,
- file_tree_view,
- }
- }
-}
-
-impl Focusable for MemoryView {
- fn focus_handle(&self, _: &App) -> FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-impl Render for MemoryView {
- fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
- let title_view = div()
- .id("title-view")
- .flex_none()
- .h(px(36.))
- .w_full()
- .flex()
- .items_center()
- .px_2()
- .justify_between()
- .bg(cx.theme().popover)
- .text_color(cx.theme().foreground)
- .child(
- div()
- .flex()
- .items_center()
- .gap_2()
- .child(file_icons::render_icon(
- &file_icons::get().get_type_icon(file_icons::WORKING_MEMORY),
- 16.0,
- cx.theme().muted_foreground,
- "🧠",
- ))
- .child("Working Memory"),
- );
-
- // Build main container with the title and child entities
- div()
- .id("memory-sidebar")
- .track_focus(&self.focus_handle(cx))
- .flex_none()
- .w(px(260.))
- .h_full()
- .bg(cx.theme().sidebar)
- .overflow_hidden() // Prevent content from overflowing
- .flex()
- .flex_col()
- .child(title_view)
- .child(self.resources_view.clone())
- .child(self.file_tree_view.clone())
- }
-}
diff --git a/crates/code_assistant/src/ui/gpui/mod.rs b/crates/code_assistant/src/ui/gpui/mod.rs
index 735f47d2..3d527ac0 100644
--- a/crates/code_assistant/src/ui/gpui/mod.rs
+++ b/crates/code_assistant/src/ui/gpui/mod.rs
@@ -9,11 +9,9 @@ pub mod elements;
pub mod file_icons;
pub mod image;
pub mod input_area;
-mod memory;
mod messages;
pub mod model_selector;
pub mod parameter_renderers;
-mod path_util;
mod plan_banner;
mod root;
pub mod sandbox_selector;
@@ -21,7 +19,7 @@ pub mod simple_renderers;
pub mod theme;
use crate::persistence::{ChatMetadata, DraftStorage};
-use crate::types::{PlanState, WorkingMemory};
+use crate::types::PlanState;
use crate::ui::gpui::{
content_renderer::ContentRenderer,
diff_renderer::DiffParameterRenderer,
@@ -39,7 +37,6 @@ use gpui::{
SharedString,
};
use gpui_component::Root;
-pub use memory::MemoryView;
pub use messages::MessagesView;
pub use root::RootView;
use sandbox::SandboxPolicy;
@@ -65,7 +62,6 @@ pub use crate::ui::backend::{BackendEvent, BackendResponse};
#[derive(Clone)]
pub struct Gpui {
message_queue: Arc>>>,
- working_memory: Arc>>,
plan_state: Arc>>,
event_sender: Arc>>,
event_receiver: Arc>>,
@@ -212,7 +208,6 @@ impl Gpui {
pub fn new() -> Self {
let message_queue = Arc::new(Mutex::new(Vec::new()));
- let working_memory = Arc::new(Mutex::new(None));
let plan_state = Arc::new(Mutex::new(None));
let event_task = Arc::new(Mutex::new(None::>));
let session_event_task = Arc::new(Mutex::new(None::>));
@@ -267,7 +262,6 @@ impl Gpui {
Self {
message_queue,
- working_memory,
plan_state,
event_sender,
event_receiver,
@@ -303,7 +297,6 @@ impl Gpui {
// Run the application
pub fn run_app(&self) {
let message_queue = self.message_queue.clone();
- let working_memory = self.working_memory.clone();
let gpui_clone = self.clone();
// Initialize app with assets
@@ -401,12 +394,9 @@ impl Gpui {
*task_guard = Some(chat_response_task);
}
- // Create memory view with our shared working memory
- let memory_view = cx.new(|cx| MemoryView::new(working_memory.clone(), cx));
-
- // Create window with larger size to accommodate chat sidebar, messages, and memory view
+ // Create window with larger size to accommodate chat sidebar and messages
let bounds =
- gpui::Bounds::centered(None, gpui::size(gpui::px(1400.0), gpui::px(700.0)), cx);
+ gpui::Bounds::centered(None, gpui::size(gpui::px(1100.0), gpui::px(700.0)), cx);
// Open window with titlebar
let window = cx
.open_window(
@@ -439,13 +429,7 @@ impl Gpui {
// Create RootView
let root_view = cx.new(|cx| {
- RootView::new(
- memory_view.clone(),
- messages_view,
- chat_sidebar.clone(),
- window,
- cx,
- )
+ RootView::new(messages_view, chat_sidebar.clone(), window, cx)
});
// Wrap in Root component
@@ -579,12 +563,7 @@ impl Gpui {
message_container.end_tool_use(&id, cx);
});
}
- UiEvent::UpdateMemory { memory } => {
- if let Ok(mut memory_guard) = self.working_memory.lock() {
- *memory_guard = Some(memory);
- }
- cx.refresh().expect("Failed to refresh windows");
- }
+
UiEvent::UpdatePlan { plan } => {
if let Ok(mut plan_guard) = self.plan_state.lock() {
*plan_guard = Some(plan);
@@ -992,6 +971,36 @@ impl Gpui {
*self.current_sandbox_policy.lock().unwrap() = Some(policy.clone());
cx.refresh().expect("Failed to refresh windows");
}
+
+ // Resource events - logged for now, can be extended for features like "follow mode"
+ UiEvent::ResourceLoaded { project, path } => {
+ trace!(
+ "UI: ResourceLoaded event - project: {}, path: {}",
+ project,
+ path.display()
+ );
+ }
+ UiEvent::ResourceWritten { project, path } => {
+ trace!(
+ "UI: ResourceWritten event - project: {}, path: {}",
+ project,
+ path.display()
+ );
+ }
+ UiEvent::DirectoryListed { project, path } => {
+ trace!(
+ "UI: DirectoryListed event - project: {}, path: {}",
+ project,
+ path.display()
+ );
+ }
+ UiEvent::ResourceDeleted { project, path } => {
+ trace!(
+ "UI: ResourceDeleted event - project: {}, path: {}",
+ project,
+ path.display()
+ );
+ }
}
}
diff --git a/crates/code_assistant/src/ui/gpui/path_util.rs b/crates/code_assistant/src/ui/gpui/path_util.rs
deleted file mode 100644
index 99993fc1..00000000
--- a/crates/code_assistant/src/ui/gpui/path_util.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-use std::path::Path;
-
-/// Helper trait to extend Path with icon-related functionality.
-pub trait PathExt {
- /// Returns either the suffix if available, or the file stem otherwise to determine
- /// which file icon to use.
- fn icon_stem_or_suffix(&self) -> Option<&str>;
-}
-
-impl> PathExt for T {
- fn icon_stem_or_suffix(&self) -> Option<&str> {
- let path = self.as_ref();
- let file_name = path.file_name()?.to_str()?;
-
- // For hidden files (Unix style), return the name without the leading dot
- if file_name.starts_with('.') {
- return file_name.strip_prefix('.');
- }
-
- // Try to get extension, or fall back to file stem
- path.extension()
- .and_then(|e| e.to_str())
- .or_else(|| path.file_stem()?.to_str())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_icon_stem_or_suffix() {
- // No dots in name
- let path = Path::new("/a/b/c/file_name.rs");
- assert_eq!(path.icon_stem_or_suffix(), Some("rs"));
-
- // Single dot in name
- let path = Path::new("/a/b/c/file.name.rs");
- assert_eq!(path.icon_stem_or_suffix(), Some("rs"));
-
- // No suffix
- let path = Path::new("/a/b/c/file");
- assert_eq!(path.icon_stem_or_suffix(), Some("file"));
-
- // Multiple dots in name
- let path = Path::new("/a/b/c/long.file.name.rs");
- assert_eq!(path.icon_stem_or_suffix(), Some("rs"));
-
- // Hidden file, no extension
- let path = Path::new("/a/b/c/.gitignore");
- assert_eq!(path.icon_stem_or_suffix(), Some("gitignore"));
-
- // Hidden file, with extension
- let path = Path::new("/a/b/c/.eslintrc.js");
- assert_eq!(path.icon_stem_or_suffix(), Some("eslintrc.js"));
- }
-}
diff --git a/crates/code_assistant/src/ui/gpui/plan_banner.rs b/crates/code_assistant/src/ui/gpui/plan_banner.rs
index 5a1351c0..d1e2eff8 100644
--- a/crates/code_assistant/src/ui/gpui/plan_banner.rs
+++ b/crates/code_assistant/src/ui/gpui/plan_banner.rs
@@ -108,19 +108,18 @@ impl Render for PlanBanner {
.border_t_1()
.border_color(cx.theme().border)
.px_4()
- .py_3()
+ .py(px(6.))
.gap_2()
.text_size(px(11.))
.line_height(px(15.))
.child(header);
if self.collapsed {
- let summary_color = if highlight_summary {
- cx.theme().info
- } else {
- cx.theme().muted_foreground
- };
- container = container.child(div().text_color(summary_color).child(summary_text));
+ // Only show summary line if there's something meaningful (e.g., in-progress item)
+ // The header already shows the item count, so don't repeat it
+ if highlight_summary {
+ container = container.child(div().text_color(cx.theme().info).child(summary_text));
+ }
} else {
let markdown = build_plan_markdown(plan);
if !markdown.is_empty() {
diff --git a/crates/code_assistant/src/ui/gpui/root.rs b/crates/code_assistant/src/ui/gpui/root.rs
index be5b127c..0166283f 100644
--- a/crates/code_assistant/src/ui/gpui/root.rs
+++ b/crates/code_assistant/src/ui/gpui/root.rs
@@ -1,8 +1,7 @@
use super::auto_scroll::AutoScrollContainer;
use super::chat_sidebar::{ChatSidebar, ChatSidebarEvent};
-use super::file_icons;
+
use super::input_area::{InputArea, InputAreaEvent};
-use super::memory::MemoryView;
use super::messages::MessagesView;
use super::plan_banner;
use super::theme;
@@ -15,21 +14,19 @@ use gpui::{
Context, Entity, FocusHandle, Focusable, MouseButton, MouseUpEvent, SharedString, Subscription,
Transformation,
};
-use gpui_component::ActiveTheme;
+
+use gpui_component::{ActiveTheme, Icon, Sizable, Size};
use std::collections::HashMap;
use tracing::{debug, error, trace, warn};
// Root View - handles overall layout and coordination
pub struct RootView {
input_area: Entity,
- memory_view: Entity,
chat_sidebar: Entity,
auto_scroll_container: Entity>,
plan_banner: Entity,
recent_keystrokes: Vec,
focus_handle: FocusHandle,
- // Memory view state
- memory_collapsed: bool,
// Chat sidebar state
chat_collapsed: bool,
current_session_id: Option,
@@ -44,7 +41,6 @@ pub struct RootView {
impl RootView {
pub fn new(
- memory_view: Entity,
messages_view: Entity,
chat_sidebar: Entity,
window: &mut gpui::Window,
@@ -74,13 +70,11 @@ impl RootView {
let mut root_view = Self {
input_area,
- memory_view,
chat_sidebar,
auto_scroll_container,
plan_banner,
recent_keystrokes: vec![],
focus_handle: cx.focus_handle(),
- memory_collapsed: false,
chat_collapsed: false, // Chat sidebar is visible by default
current_session_id: None,
chat_sessions: Vec::new(),
@@ -97,16 +91,6 @@ impl RootView {
root_view
}
- pub fn on_toggle_memory(
- &mut self,
- _: &MouseUpEvent,
- _window: &mut gpui::Window,
- cx: &mut Context,
- ) {
- self.memory_collapsed = !self.memory_collapsed;
- cx.notify();
- }
-
pub fn on_toggle_chat_sidebar(
&mut self,
_: &MouseUpEvent,
@@ -749,41 +733,35 @@ impl Render for RootView {
.flex()
.flex_row()
.items_center()
- .justify_between()
- .px_4()
- // Left side - title
+ .justify_start()
+ // Left padding for macOS traffic lights (doubled for more space)
+ .pl(px(86.))
+ // Left side - controls
.child(
div()
.flex()
.items_center()
- .text_color(cx.theme().muted_foreground)
- .gap_2()
- .pl(px(80.))
- .child("Code Assistant"),
- )
- // Right side - controls
- .child(
- div()
- .flex()
- .items_center()
- .gap_2()
+ .gap_1()
// Chat sidebar toggle button
.child(
div()
- .size(px(32.))
+ .size(px(28.))
.rounded_sm()
.flex()
.items_center()
.justify_center()
.cursor_pointer()
.hover(|s| s.bg(cx.theme().muted))
- .child(file_icons::render_icon(
- &file_icons::get()
- .get_type_icon(file_icons::MESSAGE_BUBBLES),
- 18.0,
- cx.theme().muted_foreground,
- "💬",
- ))
+ .child(
+ Icon::default()
+ .path(SharedString::from(if self.chat_collapsed {
+ "icons/panel_left_open.svg"
+ } else {
+ "icons/panel_left_close.svg"
+ }))
+ .with_size(Size::Small)
+ .text_color(cx.theme().muted_foreground),
+ )
.on_mouse_up(
MouseButton::Left,
cx.listener(Self::on_toggle_chat_sidebar),
@@ -792,68 +770,41 @@ impl Render for RootView {
// Theme toggle button
.child(
div()
- .size(px(32.))
+ .size(px(28.))
.rounded_sm()
.flex()
.items_center()
.justify_center()
.cursor_pointer()
.hover(|s| s.bg(cx.theme().muted))
- .child(file_icons::render_icon(
- &file_icons::get().get_type_icon(if cx.theme().is_dark() {
- file_icons::THEME_LIGHT
- } else {
- file_icons::THEME_DARK
- }),
- 18.0,
- cx.theme().muted_foreground,
- if cx.theme().is_dark() { "*" } else { "c" },
- ))
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(Self::on_toggle_theme),
- ),
- )
- // Memory toggle button
- .child(
- div()
- .size(px(32.))
- .rounded_sm()
- .flex()
- .items_center()
- .justify_center()
- .cursor_pointer()
- .hover(|s| s.bg(cx.theme().muted))
- .child(file_icons::render_icon(
- &file_icons::get().get_type_icon(
- if self.memory_collapsed {
- file_icons::PANEL_RIGHT_OPEN
+ .child(
+ Icon::default()
+ .path(SharedString::from(if cx.theme().is_dark() {
+ "icons/theme_light.svg"
} else {
- file_icons::PANEL_RIGHT_CLOSE
- },
- ),
- 18.0,
- cx.theme().muted_foreground,
- "<>",
- ))
+ "icons/theme_dark.svg"
+ }))
+ .with_size(Size::Small)
+ .text_color(cx.theme().muted_foreground),
+ )
.on_mouse_up(
MouseButton::Left,
- cx.listener(Self::on_toggle_memory),
+ cx.listener(Self::on_toggle_theme),
),
),
),
)
- // Main content area with chat sidebar, messages+input, and memory sidebar (3-column layout)
+ // Main content area with chat sidebar and messages+input (2-column layout)
.child(
div()
.size_full()
.min_h_0()
.flex()
- .flex_row() // 3-column layout: chat | messages+input | memory
+ .flex_row() // 2-column layout: chat | messages+input
// Left sidebar: Chat sessions
.child(self.chat_sidebar.clone())
.child(
- // Center: Messages and input (content area) with floating popover
+ // Messages and input (content area) with floating popover
div()
.relative() // For popover positioning
.bg(cx.theme().popover)
@@ -879,57 +830,7 @@ impl Render for RootView {
.border_color(cx.theme().border)
.child(self.input_area.clone()),
),
- )
- // Right sidebar with memory view - only show if not collapsed
- .when(!self.memory_collapsed, |s| {
- s.child(
- div()
- .id("memory-sidebar")
- .flex_none()
- .w(px(260.))
- .h_full()
- .bg(cx.theme().sidebar)
- .border_l_1()
- .border_color(cx.theme().sidebar_border)
- .overflow_hidden()
- .flex()
- .flex_col()
- .child(self.memory_view.clone()),
- )
- })
- // When memory view is collapsed, show only a narrow bar
- .when(self.memory_collapsed, |s| {
- s.child(
- div()
- .id("collapsed-memory-sidebar")
- .flex_none()
- .w(px(40.))
- .h_full()
- .bg(cx.theme().sidebar)
- .border_l_1()
- .border_color(cx.theme().sidebar_border)
- .flex()
- .flex_col()
- .items_center()
- .gap_2()
- .py_2()
- .child(
- div()
- .size(px(24.))
- .rounded_full()
- .flex()
- .items_center()
- .justify_center()
- .child(file_icons::render_icon(
- &file_icons::get()
- .get_type_icon(file_icons::WORKING_MEMORY),
- 16.0,
- cx.theme().muted_foreground,
- "🧠",
- )),
- ),
- )
- }),
+ ),
)
}
}
diff --git a/crates/code_assistant/src/ui/terminal/state.rs b/crates/code_assistant/src/ui/terminal/state.rs
index 05cc3986..40499f3c 100644
--- a/crates/code_assistant/src/ui/terminal/state.rs
+++ b/crates/code_assistant/src/ui/terminal/state.rs
@@ -1,11 +1,10 @@
use crate::persistence::ChatMetadata;
use crate::session::instance::SessionActivityState;
-use crate::types::{PlanState, WorkingMemory};
+use crate::types::PlanState;
use sandbox::SandboxPolicy;
use std::collections::HashMap;
pub struct AppState {
- pub working_memory: Option,
pub plan: Option,
pub plan_expanded: bool,
pub plan_dirty: bool,
@@ -23,7 +22,6 @@ pub struct AppState {
impl AppState {
pub fn new() -> Self {
Self {
- working_memory: None,
plan: None,
plan_expanded: false,
plan_dirty: true,
diff --git a/crates/code_assistant/src/ui/terminal/ui.rs b/crates/code_assistant/src/ui/terminal/ui.rs
index 4e1cfece..79fa5555 100644
--- a/crates/code_assistant/src/ui/terminal/ui.rs
+++ b/crates/code_assistant/src/ui/terminal/ui.rs
@@ -100,10 +100,7 @@ impl UserInterface for TerminalTuiUI {
.insert(tool_result.tool_id, tool_result.status);
}
}
- UiEvent::UpdateMemory { memory } => {
- debug!("Updating memory");
- state.working_memory = Some(memory);
- }
+
UiEvent::UpdatePlan { plan } => {
debug!("Updating plan");
let plan_clone = plan.clone();
@@ -310,6 +307,35 @@ impl UserInterface for TerminalTuiUI {
renderer_guard.clear_error();
}
}
+ // Resource events - logged for debugging, can be extended for features like "follow mode"
+ UiEvent::ResourceLoaded { project, path } => {
+ tracing::trace!(
+ "ResourceLoaded event - project: {}, path: {}",
+ project,
+ path.display()
+ );
+ }
+ UiEvent::ResourceWritten { project, path } => {
+ tracing::trace!(
+ "ResourceWritten event - project: {}, path: {}",
+ project,
+ path.display()
+ );
+ }
+ UiEvent::DirectoryListed { project, path } => {
+ tracing::trace!(
+ "DirectoryListed event - project: {}, path: {}",
+ project,
+ path.display()
+ );
+ }
+ UiEvent::ResourceDeleted { project, path } => {
+ tracing::trace!(
+ "ResourceDeleted event - project: {}, path: {}",
+ project,
+ path.display()
+ );
+ }
_ => {
// For other events, just log them
debug!("Unhandled event: {:?}", event);
diff --git a/crates/code_assistant/src/ui/ui_events.rs b/crates/code_assistant/src/ui/ui_events.rs
index 18a3e3b4..b63bd0ae 100644
--- a/crates/code_assistant/src/ui/ui_events.rs
+++ b/crates/code_assistant/src/ui/ui_events.rs
@@ -1,9 +1,10 @@
use crate::persistence::{ChatMetadata, DraftAttachment};
use crate::session::instance::SessionActivityState;
-use crate::types::{PlanState, WorkingMemory};
+use crate::types::PlanState;
use crate::ui::gpui::elements::MessageRole;
use crate::ui::{DisplayFragment, ToolStatus};
use sandbox::SandboxPolicy;
+use std::path::PathBuf;
/// Data for a complete message with its display fragments
#[derive(Debug, Clone)]
@@ -56,8 +57,6 @@ pub enum UiEvent {
AddImage { media_type: String, data: String },
/// Append streaming tool output
AppendToolOutput { tool_id: String, chunk: String },
- /// Update the working memory view
- UpdateMemory { memory: WorkingMemory },
/// Update the session plan display
UpdatePlan { plan: PlanState },
/// Set all messages at once (for session loading, clears existing)
@@ -119,4 +118,14 @@ pub enum UiEvent {
UpdateCurrentModel { model_name: String },
/// Update the current sandbox selection in the UI
UpdateSandboxPolicy { policy: SandboxPolicy },
+
+ // === Resource Events (for tool operations) ===
+ /// A file was loaded/read by a tool
+ ResourceLoaded { project: String, path: PathBuf },
+ /// A file was written/modified by a tool
+ ResourceWritten { project: String, path: PathBuf },
+ /// A directory was listed by a tool
+ DirectoryListed { project: String, path: PathBuf },
+ /// A file was deleted by a tool
+ ResourceDeleted { project: String, path: PathBuf },
}
diff --git a/crates/fs_explorer/src/explorer.rs b/crates/fs_explorer/src/explorer.rs
index 7fadbdba..212122a3 100644
--- a/crates/fs_explorer/src/explorer.rs
+++ b/crates/fs_explorer/src/explorer.rs
@@ -7,7 +7,7 @@ use command_executor::CommandExecutor;
use ignore::WalkBuilder;
use path_clean::PathClean;
use regex::RegexBuilder;
-use std::collections::{HashMap, HashSet};
+use std::collections::HashMap;
use std::ffi::OsString;
use std::fmt;
use std::path::{Path, PathBuf};
@@ -72,11 +72,10 @@ struct SearchSection {
}
/// Handles file system operations for code exploration
+
#[derive(Clone)]
pub struct Explorer {
root_dir: PathBuf,
- // Track which paths were explicitly listed
- expanded_paths: HashSet,
// Track which files had which encoding
file_encodings: Arc>>,
// Track file format information (encoding + line ending)
@@ -157,9 +156,9 @@ impl Explorer {
/// * `root_dir` - The root directory to explore
pub fn new(root_dir: PathBuf) -> Self {
let canonical_root = root_dir.canonicalize().unwrap_or_else(|_| root_dir.clean());
+
Self {
root_dir: canonical_root,
- expanded_paths: HashSet::new(),
file_encodings: Arc::new(RwLock::new(HashMap::new())),
file_formats: Arc::new(RwLock::new(HashMap::new())),
}
@@ -214,18 +213,13 @@ impl Explorer {
}
fn expand_directory(
- &mut self,
path: &Path,
entry: &mut FileTreeEntry,
current_depth: usize,
max_depth: usize,
) -> Result<()> {
- // Expand if either:
- // - Within max_depth during initial load
- // - The path was explicitly listed before
- let should_expand = current_depth < max_depth || self.expanded_paths.contains(path);
-
- if !should_expand {
+ // Expand if within max_depth
+ if current_depth >= max_depth {
entry.is_expanded = false;
return Ok(());
}
@@ -273,7 +267,7 @@ impl Explorer {
};
if is_dir {
- self.expand_directory(entry_path, &mut child_entry, current_depth + 1, max_depth)?;
+ Self::expand_directory(entry_path, &mut child_entry, current_depth + 1, max_depth)?;
}
entry.children.insert(child_entry.name.clone(), child_entry);
@@ -433,7 +427,7 @@ impl CodeExplorer for Explorer {
};
let root_dir = &self.root_dir.clone();
- self.expand_directory(root_dir, &mut root, 0, max_depth)?;
+ Self::expand_directory(root_dir, &mut root, 0, max_depth)?;
Ok(root)
}
@@ -547,14 +541,12 @@ impl CodeExplorer for Explorer {
async fn list_files(&mut self, path: &Path, max_depth: Option) -> Result {
let resolved = self.resolve_path(path)?;
let path = resolved.as_path();
+
// Check if the path exists before proceeding
if !path.exists() {
return Err(anyhow::anyhow!("Path not found"));
}
- // Remember that this path was explicitly listed
- self.expanded_paths.insert(resolved.clone());
-
let mut entry = FileTreeEntry {
name: path
.file_name()
@@ -571,7 +563,7 @@ impl CodeExplorer for Explorer {
};
if path.is_dir() {
- self.expand_directory(path, &mut entry, 0, max_depth.unwrap_or(usize::MAX))?;
+ Self::expand_directory(path, &mut entry, 0, max_depth.unwrap_or(usize::MAX))?;
}
Ok(entry)
diff --git a/docs/remove-working-memory-ui-plan.md b/docs/remove-working-memory-ui-plan.md
new file mode 100644
index 00000000..6d8050a1
--- /dev/null
+++ b/docs/remove-working-memory-ui-plan.md
@@ -0,0 +1,302 @@
+# Remove Working Memory UI Implementation Plan
+
+## Overview
+
+This plan outlines the removal of the Working Memory UI from the GPUI version of the application, while preserving the event-driven notification system for file and directory operations. The goal is to reduce coupling between the tool registry/execution module and the agent code, enabling the tools module to eventually be extracted into its own crate.
+
+## Current State Analysis
+
+### WorkingMemory Usage
+
+The `WorkingMemory` struct currently serves multiple purposes:
+
+1. **UI Display**: Showing loaded resources and file trees in the GPUI sidebar
+2. **Session Persistence**: Stored in `SessionState` and `ChatSession` for session recovery
+3. **System Prompt Generation**: Provides `file_trees` and `available_projects` for the system prompt
+4. **Resource Tracking in Tools**: Tools update `loaded_resources` when files are read/written
+
+### Components Using WorkingMemory
+
+| Component | Location | Usage |
+|-----------|----------|-------|
+| `MemoryView` | `ui/gpui/memory.rs` | UI display (to be removed) |
+| `Agent` | `agent/runner.rs` | System prompt generation, memory management |
+| `SessionState` | `session/mod.rs` | Persistence structure |
+| `ChatSession` | `persistence.rs` | Persistence to disk |
+| `ToolContext` | `tools/core/tool.rs` | Passed to tools for resource tracking |
+| Tools | `tools/impls/*.rs` | Update `loaded_resources`, `file_trees`, `expanded_directories` |
+| `UiEvent::UpdateMemory` | `ui/ui_events.rs` | Event to update UI (to be removed) |
+| `AppState` | `ui/terminal/state.rs` | Terminal UI state (keeps field, unused) |
+
+## Design Goals
+
+1. **Remove the Working Memory UI panel** from GPUI entirely
+2. **Preserve event emission** when files are loaded or directories listed
+3. **Stop tracking visited directories** in Explorer instances
+4. **Remove the `WorkingMemory` struct** and its usage across the codebase
+5. **Reduce coupling** so tools module could become an independent crate
+
+## Implementation Phases
+
+### Phase 1: Add Resource Events to UiEvent (Low Risk)
+
+**Goal**: Extend the existing `UiEvent` enum to include resource operation notifications. This follows the established pattern where `UiEvent` carries state updates to the UI.
+
+#### Design Rationale
+
+The codebase already has two event mechanisms:
+- `DisplayFragment` - for real-time streaming content (text, tool invocations)
+- `UiEvent` - for discrete state updates (session changes, tool status, memory updates)
+
+Resource operations are discrete state changes, so they belong in `UiEvent`. This avoids introducing new traits or coupling mechanisms.
+
+#### Tasks
+
+1. **Add new variants to `UiEvent`** in `ui/ui_events.rs`:
+ ```rust
+ /// A file was loaded/read by a tool
+ ResourceLoaded {
+ project: String,
+ path: PathBuf,
+ },
+ /// A file was written/modified by a tool
+ ResourceWritten {
+ project: String,
+ path: PathBuf,
+ },
+ /// A directory was listed by a tool
+ DirectoryListed {
+ project: String,
+ path: PathBuf,
+ },
+ /// A file was deleted by a tool
+ ResourceDeleted {
+ project: String,
+ path: PathBuf,
+ },
+ ```
+
+2. **Handle new events in GPUI** (`ui/gpui/mod.rs`):
+ - Add match arms that log the events (for now)
+ - These can be extended later for features like "follow mode"
+
+3. **Handle new events in Terminal UI** (`ui/terminal/ui.rs`):
+ - Add match arms that log the events
+
+### Phase 2: Update Tool Implementations (Medium Risk)
+
+**Goal**: Modify tools to emit `UiEvent`s via `ToolContext.ui` instead of updating WorkingMemory directly.
+
+#### Tasks per Tool
+
+| Tool | Current WorkingMemory Usage | New UiEvent |
+|------|---------------------------|-------------|
+| `read_files` | Insert into `loaded_resources` | `ResourceLoaded` |
+| `write_file` | Insert into `loaded_resources` | `ResourceWritten` |
+| `edit` | Insert into `loaded_resources` | `ResourceWritten` |
+| `replace_in_file` | Insert into `loaded_resources` | `ResourceWritten` |
+| `delete_files` | Could remove from `loaded_resources` | `ResourceDeleted` |
+| `list_files` | Update `file_trees`, `expanded_directories`, `available_projects` | `DirectoryListed` |
+
+#### Detailed Changes
+
+1. **read_files.rs** (~5 lines):
+ - Remove: `working_memory.loaded_resources.insert(...)`
+ - Add: Send `UiEvent::ResourceLoaded` via `context.ui`
+
+2. **write_file.rs** (~5 lines):
+ - Remove: `working_memory.loaded_resources.insert(...)`
+ - Add: Send `UiEvent::ResourceWritten` via `context.ui`
+
+3. **edit.rs** (~5 lines):
+ - Same pattern as write_file
+
+4. **replace_in_file.rs** (~5 lines):
+ - Same pattern as edit
+
+5. **delete_files.rs** (~5 lines):
+ - Add: Send `UiEvent::ResourceDeleted` via `context.ui`
+ - (Currently doesn't update WorkingMemory)
+
+6. **list_files.rs** (~20 lines):
+ - Remove: All `file_trees`, `expanded_directories`, `available_projects` updates
+ - Add: Send `UiEvent::DirectoryListed` via `context.ui`
+
+#### Note on Event Emission
+
+Tools already have access to `context.ui: Option<&'a dyn UserInterface>` for streaming output. We'll reuse this for resource events. The pattern is:
+
+```rust
+if let Some(ui) = context.ui {
+ let _ = ui.send_event(UiEvent::ResourceLoaded {
+ project: input.project.clone(),
+ path: path.clone(),
+ }).await;
+}
+```
+
+Since `send_event` is async, we'll need to handle this appropriately (tools are already async).
+
+### Phase 3: Update ToolContext and Agent (Medium Risk)
+
+**Goal**: Remove `working_memory` from `ToolContext` and `Agent`. Tools already have `ui` access for events.
+
+#### Tasks
+
+1. **Update `ToolContext`** in `tools/core/tool.rs`:
+ - Remove: `working_memory: Option<&'a mut WorkingMemory>`
+ - The `ui` field already exists and will be used for events
+
+2. **Update `execute_tool()`** in `agent/runner.rs`:
+ - Remove: `working_memory: Some(&mut self.working_memory)` from context construction
+
+3. **Remove `working_memory` field** from `Agent` struct
+
+4. **Update `init_working_memory()`** and `init_working_memory_projects()`:
+ - These methods currently build file trees for system prompt
+ - Keep the file tree building logic but store trees differently (see Phase 4)
+
+### Phase 4: Preserve System Prompt Information (Medium Risk)
+
+**Goal**: Keep the ability to show available projects and initial file tree in system prompts without WorkingMemory.
+
+#### Tasks
+
+1. **Add dedicated fields to `Agent`** for system prompt data:
+ ```rust
+ struct Agent {
+ // ... existing fields ...
+ available_projects: Vec,
+ initial_file_tree: Option, // For initial project only
+ initial_project: String,
+ }
+ ```
+
+2. **Update `get_system_prompt()`** to use these fields instead of `self.working_memory.available_projects` etc.
+
+3. **Update `init_working_memory_projects()`** → rename to `init_projects()`:
+ - Still queries ProjectManager for available projects
+ - Still creates initial file tree for the initial project
+ - Stores in Agent fields instead of WorkingMemory
+
+### Phase 5: Remove GPUI Working Memory UI (Low Risk)
+
+**Goal**: Remove all UI components related to Working Memory display.
+
+#### Tasks
+
+1. **Delete `ui/gpui/memory.rs`** entirely
+
+2. **Update `ui/gpui/mod.rs`**:
+ - Remove `mod memory;`
+ - Remove `pub use memory::MemoryView;`
+ - Remove `working_memory` field from `Gpui` struct
+ - Remove `UiEvent::UpdateMemory` handling
+
+3. **Update `ui/gpui/root.rs`**:
+ - Remove `memory_view: Entity` field
+ - Remove memory sidebar toggle button
+ - Remove memory sidebar rendering (both expanded and collapsed states)
+ - Remove `memory_collapsed` state field
+
+4. **Update `ui/ui_events.rs`**:
+ - Remove `UpdateMemory { memory: WorkingMemory }` variant
+
+5. **Update `ui/terminal/state.rs`**:
+ - Remove `working_memory: Option` field (or keep as dead code if minimal impact)
+
+### Phase 6: Remove WorkingMemory from Persistence (Medium Risk)
+
+**Goal**: Stop persisting WorkingMemory in sessions since it's no longer used.
+
+#### Tasks
+
+1. **Update `SessionState` struct** in `session/mod.rs`:
+ - Remove `working_memory: WorkingMemory` field
+
+2. **Update `ChatSession` struct** in `persistence.rs`:
+ - Remove `working_memory: WorkingMemory` field
+ - Note: Existing session files will have this field; handle gracefully during deserialization
+
+3. **Update `FileStatePersistence::save_agent_state()`** in `agent/persistence.rs`:
+ - Remove working_memory from saved state
+
+4. **Update `Agent::load_from_session_state()`**:
+ - Remove `self.working_memory = session_state.working_memory;`
+
+5. **Update test fixtures** in `tests/mocks.rs` and other test files
+
+### Phase 7: Remove Explorer Directory Tracking (Low Risk)
+
+**Goal**: Remove `expanded_paths` from Explorer since it's no longer used for WorkingMemory.
+
+The `expanded_paths` field is a remnant from an earlier version where the full directory tree (with expanded/collapsed state) was shown on every turn. Now that each `list_files` call returns exactly what the model requested, this tracking is unnecessary.
+
+#### Tasks
+
+1. **Update `Explorer` struct** in `fs_explorer/src/explorer.rs`:
+ - Remove: `expanded_paths: HashSet` field
+ - Simplify `expand_directory()` to only consider `max_depth`, not `expanded_paths`
+ - The condition `current_depth < max_depth || self.expanded_paths.contains(path)` becomes just `current_depth < max_depth`
+
+### Phase 8: Delete WorkingMemory Type (Low Risk)
+
+**Goal**: Final cleanup - remove the WorkingMemory type definition.
+
+#### Tasks
+
+1. **Update `types.rs`**:
+ - Remove `WorkingMemory` struct
+ - Remove `LoadedResource` enum (unless used elsewhere)
+ - Remove `tuple_key_map` module
+
+2. **Search and fix any remaining references**
+
+## Migration Notes
+
+### Backward Compatibility
+
+- Existing session files contain `working_memory` field
+- Use `#[serde(default)]` on new structs to allow loading old sessions
+- Old `working_memory` data in session files will be ignored
+
+### Test Updates Required
+
+- `tests/format_on_save_tests.rs`: Update tests that verify WorkingMemory updates
+- `tests/mocks.rs`: Remove `ToolTestFixture::with_working_memory()` method
+- `agent/tests.rs`: Update agent tests that check WorkingMemory state
+- Tool-specific tests: Update to verify events instead of WorkingMemory mutations
+
+## Resolved Questions
+
+1. **Event mechanism**: Use existing `UiEvent` enum with new variants. This follows the established pattern and avoids new traits/coupling.
+
+2. **Explorer expanded_paths**: Remove. It's a remnant of an earlier architecture where file trees accumulated across turns.
+
+3. **Terminal UI working_memory field**: Remove for cleanliness.
+
+4. **Event granularity**: Just path in events. The full tree/content is in tool output already.
+
+5. **Events to UI**: Events flow to UI for future extensibility (e.g., ACP follow mode).
+
+## Risk Assessment
+
+| Phase | Risk Level | Reason |
+|-------|------------|--------|
+| 1 | Low | Adding new types, no breaking changes |
+| 2 | Medium | Modifying tool implementations |
+| 3 | Medium | Agent structure changes |
+| 4 | Medium | System prompt generation changes |
+| 5 | Low | UI removal, isolated changes |
+| 6 | Medium | Persistence schema changes |
+| 7 | Low | Internal Explorer cleanup |
+| 8 | Low | Final type deletion |
+
+## Success Criteria
+
+1. GPUI application runs without Working Memory sidebar
+2. All existing tests pass (with necessary updates)
+3. Session persistence works (loading old sessions, saving new sessions)
+4. System prompt still contains project information and initial file tree
+5. Tools emit appropriate events when operating on files
+6. No references to `WorkingMemory` remain in codebase