From c486f934051453379b582eb77a93eb5e9855feb2 Mon Sep 17 00:00:00 2001 From: Atreya Srivathsan Date: Wed, 24 Dec 2025 03:18:38 +0000 Subject: [PATCH 1/4] Make tycode-cli default run command When using cargo run, the tycode-cli binary will be the default to run --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 36b4587..155e9c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = ["tycode-core", "tycode-cli", "tycode-subprocess"] +default-members = ["tycode-cli"] resolver = "2" # Note: tycode-vscode is a TypeScript/JavaScript project and not part of the Rust workspace From 8ecf328368815704c2b6661edf8f6d088db0de2f Mon Sep 17 00:00:00 2001 From: Atreya Srivathsan Date: Wed, 24 Dec 2025 03:40:47 +0000 Subject: [PATCH 2/4] Fix Shift+Enter to add new line in VSCode --- tycode-vscode/src/webview/conversationController.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tycode-vscode/src/webview/conversationController.ts b/tycode-vscode/src/webview/conversationController.ts index 1bccff8..a810b4a 100644 --- a/tycode-vscode/src/webview/conversationController.ts +++ b/tycode-vscode/src/webview/conversationController.ts @@ -1009,7 +1009,17 @@ export function createConversationController(context: WebviewContext): Conversat }); messageInput.addEventListener('keydown', (e: KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { + if (e.key === 'Enter') { + if (e.shiftKey) { + e.preventDefault(); + const start = messageInput.selectionStart; + const end = messageInput.selectionEnd; + const value = messageInput.value; + messageInput.value = value.substring(0, start) + '\n' + value.substring(end); + messageInput.selectionStart = messageInput.selectionEnd = start + 1; + messageInput.dispatchEvent(new Event('input', { bubbles: true })); + return; + } e.preventDefault(); const conversation = context.store.get(id); if (conversation && conversation.isProcessing) { From b9ec0d04febf4db66f35d7de970a7118154940c1 Mon Sep 17 00:00:00 2001 From: Atreya Srivathsan Date: Wed, 11 Feb 2026 21:27:47 +0000 Subject: [PATCH 3/4] Add support for /workspaces command This allows dynamically adding workspaces and showing the tracked workspaces --- tycode-core/src/analyzer/mod.rs | 8 +- tycode-core/src/chat/commands.rs | 130 ++++++++++++++++++ tycode-core/src/file/access.rs | 17 ++- .../src/file/modify/apply_codex_patch.rs | 7 + .../src/file/modify/cline_replace_in_file.rs | 7 + tycode-core/src/file/modify/delete_file.rs | 7 + tycode-core/src/file/modify/mod.rs | 22 ++- .../src/file/modify/replace_in_file.rs | 7 + tycode-core/src/file/modify/write_file.rs | 7 + tycode-core/src/file/read_only.rs | 33 ++++- tycode-core/src/file/resolver.rs | 44 ++++-- tycode-core/src/module.rs | 5 + tycode-core/src/modules/execution/mod.rs | 13 +- 13 files changed, 279 insertions(+), 28 deletions(-) diff --git a/tycode-core/src/analyzer/mod.rs b/tycode-core/src/analyzer/mod.rs index ccc7cde..6bf3748 100644 --- a/tycode-core/src/analyzer/mod.rs +++ b/tycode-core/src/analyzer/mod.rs @@ -2,7 +2,7 @@ pub mod get_type_docs; pub mod rust_analyzer; pub mod search_types; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::Result; @@ -108,4 +108,10 @@ impl Module for AnalyzerModule { fn session_state(&self) -> Option> { None } + + fn update_workspace_roots(&self, new_root: &Path) { + if let Err(e) = self.resolver.add_root(new_root.to_path_buf()) { + tracing::warn!(?e, "Failed to add workspace root to analyzer module"); + } + } } diff --git a/tycode-core/src/chat/commands.rs b/tycode-core/src/chat/commands.rs index 7479878..7da4ee1 100644 --- a/tycode-core/src/chat/commands.rs +++ b/tycode-core/src/chat/commands.rs @@ -21,11 +21,13 @@ use serde_json::json; use std::collections::HashMap; use std::fs; use std::iter::Peekable; +use std::path::PathBuf; use std::str::Chars; use std::sync::Arc; use toml; use crate::persistence::storage; +use crate::steering::SteeringDocuments; fn handle_escape_sequence(chars: &mut Peekable, current: &mut String, c: char) { let Some(&next) = chars.peek() else { @@ -119,6 +121,7 @@ pub async fn process_command(state: &mut ActorState, command: &str) -> Vec handle_provider_command(state, &parts_refs).await, "profile" => handle_profile_command(state, &parts_refs).await, "sessions" => handle_sessions_command(state, &parts_refs).await, + "workspaces" => handle_workspace_command(state, &parts_refs).await, "debug_ui" => handle_debug_ui_command(state).await, _ => vec![create_message( format!("Unknown command: /{}", command_name), @@ -225,6 +228,12 @@ fn get_core_commands() -> Vec { hidden: false, }, + CommandInfo { + name: "workspaces".to_string(), + description: "Show or add workspace roots".to_string(), + usage: "/workspaces [show|add-root ]".to_string(), + hidden: false, + }, CommandInfo { name: "quit".to_string(), description: "Exit the application".to_string(), @@ -1762,6 +1771,127 @@ async fn handle_sessions_delete_command(state: &ActorState, parts: &[&str]) -> V } } +async fn handle_workspace_command(state: &mut ActorState, parts: &[&str]) -> Vec { + let subcommand = parts.get(1).copied().unwrap_or("show"); + + match subcommand { + "show" => handle_workspace_show(state), + "add-root" => handle_workspace_add(state, parts), + _ => vec![create_message( + "Usage: /workspaces [show|add-root ]".to_string(), + MessageSender::Error, + )], + } +} + +fn expand_tilde(path_str: &str) -> PathBuf { + if path_str == "~" { + return dirs::home_dir().unwrap_or_else(|| PathBuf::from(path_str)); + } + if let Some(rest) = path_str.strip_prefix("~/") { + if let Some(home) = dirs::home_dir() { + return home.join(rest); + } + } + PathBuf::from(path_str) +} + +fn handle_workspace_show(state: &ActorState) -> Vec { + if state.workspace_roots.is_empty() { + return vec![create_message( + "No workspace roots configured.".to_string(), + MessageSender::System, + )]; + } + + let mut message = String::from("=== Workspace Roots ===\n\n"); + for root in &state.workspace_roots { + let resolved = root.canonicalize().unwrap_or_else(|_| root.clone()); + let name = resolved + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown"); + message.push_str(&format!(" /{} -> {}\n", name, resolved.display())); + } + vec![create_message(message, MessageSender::System)] +} + +fn handle_workspace_add(state: &mut ActorState, parts: &[&str]) -> Vec { + let Some(path_str) = parts.get(2) else { + return vec![create_message( + "Usage: /workspaces add-root ".to_string(), + MessageSender::Error, + )]; + }; + + let path = expand_tilde(path_str); + if !path.exists() { + return vec![create_message( + format!("Path does not exist: {}", path.display()), + MessageSender::Error, + )]; + } + if !path.is_dir() { + return vec![create_message( + format!("Path is not a directory: {}", path.display()), + MessageSender::Error, + )]; + } + + let canonical = match path.canonicalize() { + Ok(p) => p, + Err(e) => { + return vec![create_message( + format!("Failed to canonicalize path: {e:?}"), + MessageSender::Error, + )]; + } + }; + + if state.workspace_roots.contains(&canonical) { + let name = canonical + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown"); + return vec![create_message( + format!("Workspace root already exists: /{} -> {}", name, canonical.display()), + MessageSender::System, + )]; + } + + state.workspace_roots.push(canonical.clone()); + + for module in &state.modules { + module.update_workspace_roots(&canonical); + } + + let home_dir = match dirs::home_dir() { + Some(h) => h, + None => { + return vec![create_message( + "Failed to get home directory.".to_string(), + MessageSender::Error, + )]; + } + }; + let tone = state.settings.settings().communication_tone; + state.steering = SteeringDocuments::new( + state.workspace_roots.clone(), + home_dir, + tone, + ); + + let name = canonical + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown"); + + vec![create_message( + format!("Added workspace root: /{} -> {}", name, canonical.display()), + MessageSender::System, + )] +} + async fn handle_sessions_gc_command(state: &ActorState, parts: &[&str]) -> Vec { let days = if parts.len() >= 3 { match parts[2].parse::() { diff --git a/tycode-core/src/file/access.rs b/tycode-core/src/file/access.rs index f8f223d..dc3ab18 100644 --- a/tycode-core/src/file/access.rs +++ b/tycode-core/src/file/access.rs @@ -6,16 +6,25 @@ use tokio::fs; #[derive(Clone)] pub struct FileAccessManager { - pub roots: Vec, resolver: Resolver, } impl FileAccessManager { pub fn new(workspace_roots: Vec) -> anyhow::Result { let resolver = Resolver::new(workspace_roots)?; - let roots = resolver.roots(); + Ok(Self { resolver }) + } + + pub fn from_resolver(resolver: Resolver) -> Self { + Self { resolver } + } + + pub fn resolver(&self) -> &Resolver { + &self.resolver + } - Ok(Self { resolver, roots }) + pub fn roots(&self) -> Vec { + self.resolver.roots() } pub async fn read_file(&self, file_path: &str) -> Result { @@ -226,7 +235,7 @@ mod tests { async fn test_new() { let roots = vec![std::env::current_dir().unwrap()]; let manager = FileAccessManager::new(roots.clone()).unwrap(); - assert_eq!(manager.roots.len(), 1); + assert_eq!(manager.roots().len(), 1); } #[tokio::test] diff --git a/tycode-core/src/file/modify/apply_codex_patch.rs b/tycode-core/src/file/modify/apply_codex_patch.rs index 5acb70f..6963aca 100644 --- a/tycode-core/src/file/modify/apply_codex_patch.rs +++ b/tycode-core/src/file/modify/apply_codex_patch.rs @@ -2,6 +2,7 @@ use crate::chat::events::{ToolExecutionResult, ToolRequest as ToolRequestEvent, use crate::file::access::FileAccessManager; use crate::file::find::find_closest_match; use crate::file::manager::FileModificationManager; +use crate::file::resolver::Resolver; use crate::tools::r#trait::{ ContinuationPreference, FileModification, FileOperation, ToolCallHandle, ToolCategory, ToolExecutor, ToolOutput, ToolRequest, @@ -54,6 +55,12 @@ impl ApplyCodexPatchTool { Ok(Self { file_manager }) } + pub fn from_resolver(resolver: Resolver) -> Self { + Self { + file_manager: FileAccessManager::from_resolver(resolver), + } + } + /// Strip leading and trailing @@ markers from a hunk string. fn strip_leading_trailing_markers(&self, hunk_str: &str) -> String { let lines: Vec<&str> = hunk_str.lines().collect(); diff --git a/tycode-core/src/file/modify/cline_replace_in_file.rs b/tycode-core/src/file/modify/cline_replace_in_file.rs index d6defff..c29a719 100644 --- a/tycode-core/src/file/modify/cline_replace_in_file.rs +++ b/tycode-core/src/file/modify/cline_replace_in_file.rs @@ -2,6 +2,7 @@ use crate::chat::events::{ToolExecutionResult, ToolRequest as ToolRequestEvent, use crate::file::access::FileAccessManager; use crate::file::find::{self, find_closest_match}; use crate::file::manager::FileModificationManager; +use crate::file::resolver::Resolver; use crate::tools::r#trait::{ ContinuationPreference, FileModification, FileOperation, ToolCallHandle, ToolCategory, ToolExecutor, ToolOutput, ToolRequest, @@ -118,6 +119,12 @@ impl ClineReplaceInFileTool { Ok(Self { file_manager }) } + pub fn from_resolver(resolver: Resolver) -> Self { + Self { + file_manager: FileAccessManager::from_resolver(resolver), + } + } + fn parse_diff_blocks(diff: &str) -> Result> { let mut blocks = Vec::new(); let lines: Vec<&str> = diff.lines().collect(); diff --git a/tycode-core/src/file/modify/delete_file.rs b/tycode-core/src/file/modify/delete_file.rs index 7ce7cbe..2b5898a 100644 --- a/tycode-core/src/file/modify/delete_file.rs +++ b/tycode-core/src/file/modify/delete_file.rs @@ -1,6 +1,7 @@ use crate::chat::events::{ToolExecutionResult, ToolRequest as ToolRequestEvent, ToolRequestType}; use crate::file::access::FileAccessManager; use crate::file::manager::FileModificationManager; +use crate::file::resolver::Resolver; use crate::tools::r#trait::{ ContinuationPreference, FileModification, FileOperation, ToolCallHandle, ToolCategory, ToolExecutor, ToolOutput, ToolRequest, @@ -24,6 +25,12 @@ impl DeleteFileTool { let file_manager = FileAccessManager::new(workspace_roots)?; Ok(Self { file_manager }) } + + pub fn from_resolver(resolver: Resolver) -> Self { + Self { + file_manager: FileAccessManager::from_resolver(resolver), + } + } } struct DeleteFileHandle { diff --git a/tycode-core/src/file/modify/mod.rs b/tycode-core/src/file/modify/mod.rs index 9000c8e..c44a16a 100644 --- a/tycode-core/src/file/modify/mod.rs +++ b/tycode-core/src/file/modify/mod.rs @@ -10,12 +10,13 @@ pub mod delete_file; pub mod replace_in_file; pub mod write_file; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::Result; use crate::file::config::File; +use crate::file::resolver::Resolver; use crate::module::ContextComponent; use crate::module::Module; use crate::module::PromptComponent; @@ -39,6 +40,7 @@ use write_file::WriteFileTool; /// - DeleteFileTool: Delete files or empty directories /// - modify_file tool: Selected based on FileModificationApi setting (late bound) pub struct FileModifyModule { + resolver: Resolver, write_file: Arc, delete_file: Arc, apply_codex_patch: Arc, @@ -49,12 +51,14 @@ pub struct FileModifyModule { impl FileModifyModule { pub fn new(workspace_roots: Vec, settings: SettingsManager) -> Result { + let resolver = Resolver::new(workspace_roots)?; Ok(Self { - write_file: Arc::new(WriteFileTool::new(workspace_roots.clone())?), - delete_file: Arc::new(DeleteFileTool::new(workspace_roots.clone())?), - apply_codex_patch: Arc::new(ApplyCodexPatchTool::new(workspace_roots.clone())?), - replace_in_file: Arc::new(ReplaceInFileTool::new(workspace_roots.clone())?), - cline_replace_in_file: Arc::new(ClineReplaceInFileTool::new(workspace_roots)?), + resolver: resolver.clone(), + write_file: Arc::new(WriteFileTool::from_resolver(resolver.clone())), + delete_file: Arc::new(DeleteFileTool::from_resolver(resolver.clone())), + apply_codex_patch: Arc::new(ApplyCodexPatchTool::from_resolver(resolver.clone())), + replace_in_file: Arc::new(ReplaceInFileTool::from_resolver(resolver.clone())), + cline_replace_in_file: Arc::new(ClineReplaceInFileTool::from_resolver(resolver)), settings, }) } @@ -92,4 +96,10 @@ impl Module for FileModifyModule { modify_file, ] } + + fn update_workspace_roots(&self, new_root: &Path) { + if let Err(e) = self.resolver.add_root(new_root.to_path_buf()) { + tracing::warn!(?e, "Failed to add workspace root to file modify module"); + } + } } diff --git a/tycode-core/src/file/modify/replace_in_file.rs b/tycode-core/src/file/modify/replace_in_file.rs index 2e2c909..1ca2396 100644 --- a/tycode-core/src/file/modify/replace_in_file.rs +++ b/tycode-core/src/file/modify/replace_in_file.rs @@ -2,6 +2,7 @@ use crate::chat::events::{ToolExecutionResult, ToolRequest as ToolRequestEvent, use crate::file::access::FileAccessManager; use crate::file::find::{self, find_closest_match}; use crate::file::manager::FileModificationManager; +use crate::file::resolver::Resolver; use crate::tools::r#trait::{ ContinuationPreference, FileModification, FileOperation, ToolCallHandle, ToolCategory, ToolExecutor, ToolOutput, ToolRequest, @@ -34,6 +35,12 @@ impl ReplaceInFileTool { Ok(Self { file_manager }) } + pub fn from_resolver(resolver: Resolver) -> Self { + Self { + file_manager: FileAccessManager::from_resolver(resolver), + } + } + /// Apply replacements to content fn apply_replacements( &self, diff --git a/tycode-core/src/file/modify/write_file.rs b/tycode-core/src/file/modify/write_file.rs index 93f9aa7..deaaffb 100644 --- a/tycode-core/src/file/modify/write_file.rs +++ b/tycode-core/src/file/modify/write_file.rs @@ -1,6 +1,7 @@ use crate::chat::events::{ToolExecutionResult, ToolRequest as ToolRequestEvent, ToolRequestType}; use crate::file::access::FileAccessManager; use crate::file::manager::FileModificationManager; +use crate::file::resolver::Resolver; use crate::tools::r#trait::{ ContinuationPreference, FileModification, FileOperation, ToolCallHandle, ToolCategory, ToolExecutor, ToolOutput, ToolRequest, @@ -24,6 +25,12 @@ impl WriteFileTool { let file_manager = FileAccessManager::new(workspace_roots)?; Ok(Self { file_manager }) } + + pub fn from_resolver(resolver: Resolver) -> Self { + Self { + file_manager: FileAccessManager::from_resolver(resolver), + } + } } struct WriteFileHandle { diff --git a/tycode-core/src/file/read_only.rs b/tycode-core/src/file/read_only.rs index 3cbea58..e73515f 100644 --- a/tycode-core/src/file/read_only.rs +++ b/tycode-core/src/file/read_only.rs @@ -4,7 +4,7 @@ //! plus the set_tracked_files tool for managing which files appear in context. use std::collections::{BTreeMap, BTreeSet}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use anyhow::{bail, Result}; @@ -41,18 +41,18 @@ pub const TRACKED_FILES_ID: ContextComponentId = ContextComponentId("tracked_fil /// - FileTreeManager: Shows project file structure in context /// - TrackedFilesManager: Displays tracked file contents in context and exposes set_tracked_files tool pub struct ReadOnlyFileModule { + resolver: Resolver, tracked_files: Arc, file_tree: Arc, } impl ReadOnlyFileModule { pub fn new(workspace_roots: Vec, settings: SettingsManager) -> Result { - let tracked_files = Arc::new(TrackedFilesManager::new( - workspace_roots.clone(), - settings.clone(), - )?); - let file_tree = Arc::new(FileTreeManager::new(workspace_roots, settings)?); + let resolver = Resolver::new(workspace_roots)?; + let tracked_files = Arc::new(TrackedFilesManager::from_resolver(resolver.clone(), settings.clone())); + let file_tree = Arc::new(FileTreeManager::from_resolver(resolver.clone(), settings)); Ok(Self { + resolver, tracked_files, file_tree, }) @@ -93,6 +93,12 @@ impl Module for ReadOnlyFileModule { fn settings_json_schema(&self) -> Option { Some(schemars::schema_for!(File)) } + + fn update_workspace_roots(&self, new_root: &Path) { + if let Err(e) = self.resolver.add_root(new_root.to_path_buf()) { + tracing::warn!(?e, "Failed to add workspace root to read-only file module"); + } + } } /// Manages file tree state and renders project structure to context. @@ -107,6 +113,10 @@ impl FileTreeManager { Ok(Self { resolver, settings }) } + pub fn from_resolver(resolver: Resolver, settings: SettingsManager) -> Self { + Self { resolver, settings } + } + pub(crate) fn list_files(&self) -> Vec { let mut all_files = Vec::new(); @@ -231,6 +241,17 @@ impl TrackedFilesManager { }) } + pub fn from_resolver(resolver: Resolver, settings: SettingsManager) -> Self { + Self { + inner: Arc::new(RwLock::new(TrackedFilesInner { + ai_tracked: BTreeSet::new(), + user_pinned: BTreeSet::new(), + })), + file_manager: FileAccessManager::from_resolver(resolver), + settings, + } + } + pub fn get_tracked_files(&self) -> Vec { let inner = self.inner.read().expect("lock poisoned"); inner diff --git a/tycode-core/src/file/resolver.rs b/tycode-core/src/file/resolver.rs index 95a100a..e41e51b 100644 --- a/tycode-core/src/file/resolver.rs +++ b/tycode-core/src/file/resolver.rs @@ -2,6 +2,7 @@ use std::{ collections::HashMap, ffi::OsString, path::{Component, Path, PathBuf}, + sync::{Arc, RwLock}, }; use anyhow::bail; @@ -17,7 +18,7 @@ pub struct ResolvedPath { /// AI agents where each workspace is in a root file system. #[derive(Debug, Clone)] pub struct Resolver { - workspaces: HashMap, + workspaces: Arc>>, } impl Resolver { @@ -43,7 +44,9 @@ impl Resolver { let name = os_to_string(name.to_os_string())?; workspaces.insert(name, workspace_root); } - Ok(Self { workspaces }) + Ok(Self { + workspaces: Arc::new(RwLock::new(workspaces)), + }) } /// Resolves a path in the virtual file system to the real path on disk @@ -51,8 +54,9 @@ impl Resolver { let virtual_path = PathBuf::from(path_str); let root = root(&virtual_path)?; let relative = remaining(&virtual_path); + let workspaces = self.workspaces.read().expect("resolver lock poisoned"); - if let Some(workspace) = self.workspaces.get(&root) { + if let Some(workspace) = workspaces.get(&root) { let virtual_path = PathBuf::from("/").join(&root).join(&relative); let real_path = workspace.join(relative); return Ok(ResolvedPath { @@ -62,8 +66,8 @@ impl Resolver { }); } - if self.workspaces.len() == 1 { - let (ws_name, ws_path) = self.workspaces.iter().next().unwrap(); + if workspaces.len() == 1 { + let (ws_name, ws_path) = workspaces.iter().next().unwrap(); let trimmed = path_str.trim_start_matches('/').trim_start_matches("./"); let full_relative = PathBuf::from(trimmed); let virtual_path = PathBuf::from("/").join(ws_name).join(&full_relative); @@ -77,14 +81,15 @@ impl Resolver { bail!( "No root directory: {root} (known: {:?}). Be sure to use absolute paths!", - self.workspaces.keys() + workspaces.keys() ); } /// Converts a real on disk path to the virtual file system path pub fn canonicalize(&self, path: &Path) -> anyhow::Result { let real_path = path.canonicalize()?; - for (name, root) in &self.workspaces { + let workspaces = self.workspaces.read().expect("resolver lock poisoned"); + for (name, root) in workspaces.iter() { let Ok(path) = real_path.strip_prefix(root) else { continue; }; @@ -98,11 +103,32 @@ impl Resolver { } pub fn root(&self, workspace: &str) -> Option { - self.workspaces.get(workspace).cloned() + let workspaces = self.workspaces.read().expect("resolver lock poisoned"); + workspaces.get(workspace).cloned() } pub fn roots(&self) -> Vec { - self.workspaces.keys().cloned().collect() + let workspaces = self.workspaces.read().expect("resolver lock poisoned"); + workspaces.keys().cloned().collect() + } + + pub fn workspaces(&self) -> HashMap { + let workspaces = self.workspaces.read().expect("resolver lock poisoned"); + workspaces.clone() + } + + pub fn add_root(&self, path: PathBuf) -> anyhow::Result<()> { + if !path.exists() { + bail!("Workspace root does not exist: {}", path.display()); + } + let path = path.canonicalize()?; + let Some(name) = path.file_name() else { + bail!("Cannot get workspace name for {path:?}"); + }; + let name = os_to_string(name.to_os_string())?; + let mut workspaces = self.workspaces.write().expect("resolver lock poisoned"); + workspaces.insert(name, path); + Ok(()) } } diff --git a/tycode-core/src/module.rs b/tycode-core/src/module.rs index 4b02877..5bc9722 100644 --- a/tycode-core/src/module.rs +++ b/tycode-core/src/module.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::path::Path; use std::sync::Arc; use anyhow::Result; @@ -142,6 +143,10 @@ pub trait Module: Send + Sync { fn on_agent_pushed(&self, _agent: &ActiveAgent, _params: HashMap) {} fn on_agent_popped(&self, _agent: &ActiveAgent) {} + + /// Called when a new workspace root is added at runtime. + /// Modules that hold a Resolver should call `resolver.add_root()` here. + fn update_workspace_roots(&self, _new_root: &Path) {} } /// Encapsulates prompt component management and builds the combined prompt. diff --git a/tycode-core/src/modules/execution/mod.rs b/tycode-core/src/modules/execution/mod.rs index 7b55571..0798ba5 100644 --- a/tycode-core/src/modules/execution/mod.rs +++ b/tycode-core/src/modules/execution/mod.rs @@ -14,6 +14,7 @@ use std::collections::VecDeque; use crate::chat::events::{ToolExecutionResult, ToolRequest as ToolRequestEvent, ToolRequestType}; use crate::file::access::FileAccessManager; +use crate::file::resolver::Resolver; use crate::module::Module; use crate::module::PromptComponent; use crate::module::{ContextComponent, ContextComponentId}; @@ -181,6 +182,7 @@ pub async fn run_cmd( } pub struct ExecutionModule { + resolver: Resolver, inner: Arc, } @@ -192,12 +194,13 @@ struct ExecutionModuleInner { impl ExecutionModule { pub fn new(workspace_roots: Vec, settings: SettingsManager) -> Result { + let resolver = Resolver::new(workspace_roots)?; let inner = Arc::new(ExecutionModuleInner { command_outputs_manager: Arc::new(CommandOutputsManager::new(10)), - access: FileAccessManager::new(workspace_roots)?, + access: FileAccessManager::from_resolver(resolver.clone()), settings, }); - Ok(Self { inner }) + Ok(Self { resolver, inner }) } } @@ -227,6 +230,12 @@ impl Module for ExecutionModule { fn settings_json_schema(&self) -> Option { Some(schemars::schema_for!(ExecutionConfig)) } + + fn update_workspace_roots(&self, new_root: &Path) { + if let Err(e) = self.resolver.add_root(new_root.to_path_buf()) { + tracing::warn!(?e, "Failed to add workspace root to execution module"); + } + } } pub struct RunBuildTestTool { From 3966faebca2c980fdeb9116a3108a4063874e08e Mon Sep 17 00:00:00 2001 From: Atreya Srivathsan Date: Wed, 4 Mar 2026 16:09:10 +0000 Subject: [PATCH 4/4] Bring compatibility to sessions --- tycode-core/src/ai/bedrock.rs | 3 +++ tycode-core/src/ai/types.rs | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tycode-core/src/ai/bedrock.rs b/tycode-core/src/ai/bedrock.rs index 42e7dcf..f5c6941 100644 --- a/tycode-core/src/ai/bedrock.rs +++ b/tycode-core/src/ai/bedrock.rs @@ -78,6 +78,9 @@ impl BedrockProvider { } } ContentBlock::ReasoningContent(reasoning) => { + if reasoning.signature.is_none() && reasoning.blob.is_none() { + continue; + } let reasoning_content = if let Some(blob) = &reasoning.blob { ReasoningContentBlock::RedactedContent(Blob::new(blob.clone())) } else { diff --git a/tycode-core/src/ai/types.rs b/tycode-core/src/ai/types.rs index 73afcb5..a893858 100644 --- a/tycode-core/src/ai/types.rs +++ b/tycode-core/src/ai/types.rs @@ -297,11 +297,13 @@ pub struct ModelConfig { /// Breakdown of context usage by category. /// Byte sizes are measured before sending; actual input_tokens come from the API response. /// Per-category token estimates are derived by applying byte proportions to actual input_tokens. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] pub struct ContextBreakdown { pub context_window: u32, pub input_tokens: u32, pub system_prompt_bytes: usize, + #[serde(alias = "tool_definitions_bytes")] pub tool_io_bytes: usize, pub conversation_history_bytes: usize, pub reasoning_bytes: usize,