Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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

Expand Down
3 changes: 3 additions & 0 deletions tycode-core/src/ai/bedrock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion tycode-core/src/ai/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion tycode-core/src/analyzer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -108,4 +108,10 @@ impl Module for AnalyzerModule {
fn session_state(&self) -> Option<Arc<dyn SessionStateComponent>> {
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");
}
}
}
130 changes: 130 additions & 0 deletions tycode-core/src/chat/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Chars>, current: &mut String, c: char) {
let Some(&next) = chars.peek() else {
Expand Down Expand Up @@ -119,6 +121,7 @@ pub async fn process_command(state: &mut ActorState, command: &str) -> Vec<ChatM
"provider" => 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),
Expand Down Expand Up @@ -225,6 +228,12 @@ fn get_core_commands() -> Vec<CommandInfo> {
hidden: false,
},

CommandInfo {
name: "workspaces".to_string(),
description: "Show or add workspace roots".to_string(),
usage: "/workspaces [show|add-root <path>]".to_string(),
hidden: false,
},
CommandInfo {
name: "quit".to_string(),
description: "Exit the application".to_string(),
Expand Down Expand Up @@ -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<ChatMessage> {
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 <path>]".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<ChatMessage> {
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<ChatMessage> {
let Some(path_str) = parts.get(2) else {
return vec![create_message(
"Usage: /workspaces add-root <path>".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<ChatMessage> {
let days = if parts.len() >= 3 {
match parts[2].parse::<u64>() {
Expand Down
17 changes: 13 additions & 4 deletions tycode-core/src/file/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,25 @@ use tokio::fs;

#[derive(Clone)]
pub struct FileAccessManager {
pub roots: Vec<String>,
resolver: Resolver,
}

impl FileAccessManager {
pub fn new(workspace_roots: Vec<PathBuf>) -> anyhow::Result<Self> {
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<String> {
self.resolver.roots()
}

pub async fn read_file(&self, file_path: &str) -> Result<String> {
Expand Down Expand Up @@ -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]
Expand Down
7 changes: 7 additions & 0 deletions tycode-core/src/file/modify/apply_codex_patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
7 changes: 7 additions & 0 deletions tycode-core/src/file/modify/cline_replace_in_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Vec<SearchReplaceBlock>> {
let mut blocks = Vec::new();
let lines: Vec<&str> = diff.lines().collect();
Expand Down
7 changes: 7 additions & 0 deletions tycode-core/src/file/modify/delete_file.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand Down
22 changes: 16 additions & 6 deletions tycode-core/src/file/modify/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<WriteFileTool>,
delete_file: Arc<DeleteFileTool>,
apply_codex_patch: Arc<ApplyCodexPatchTool>,
Expand All @@ -49,12 +51,14 @@ pub struct FileModifyModule {

impl FileModifyModule {
pub fn new(workspace_roots: Vec<PathBuf>, settings: SettingsManager) -> Result<Self> {
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,
})
}
Expand Down Expand Up @@ -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");
}
}
}
7 changes: 7 additions & 0 deletions tycode-core/src/file/modify/replace_in_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading