use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub fn normalize_thread_id(thread_id: &str) -> String {
thread_id.chars().filter(|c| c.is_alphanumeric() || *c == '_').collect()
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum InitializeType {
FirstCall,
UserAskedModeChange,
ResetShell,
UserAskedChangeWorkspace,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ModeName {
Wcgw,
Architect,
CodeWriter,
}
impl Serialize for ModeName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
ModeName::Wcgw => serializer.serialize_str("wcgw"),
ModeName::Architect => serializer.serialize_str("architect"),
ModeName::CodeWriter => serializer.serialize_str("code_writer"),
}
}
}
impl<'de> Deserialize<'de> for ModeName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"wcgw" => Ok(ModeName::Wcgw),
"architect" => Ok(ModeName::Architect),
"code_writer" | "code_write" | "code-writer" => Ok(ModeName::CodeWriter),
_ => Err(serde::de::Error::custom(format!("Unknown mode name: {s}"))),
}
}
}
impl JsonSchema for ModeName {
fn schema_name() -> std::borrow::Cow<'static, str> {
"ModeName".into()
}
fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::Schema::new_ref("#/definitions/ModeName".to_string())
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq, Default)]
pub struct CodeWriterConfig {
#[serde(default)]
pub allowed_globs: AllowedGlobs,
#[serde(default)]
pub allowed_commands: AllowedCommands,
}
impl CodeWriterConfig {
pub fn update_relative_globs(&mut self, workspace_root: &str) {
if let AllowedGlobs::List(globs) = &self.allowed_globs {
let updated_globs = globs
.iter()
.map(|glob| {
if std::path::Path::new(glob).is_absolute() {
glob.clone()
} else {
format!("{workspace_root}/{glob}")
}
})
.collect();
self.allowed_globs = AllowedGlobs::List(updated_globs);
}
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq)]
#[serde(untagged)]
pub enum AllowedGlobs {
All(String),
List(Vec<String>),
}
impl Default for AllowedGlobs {
fn default() -> Self {
AllowedGlobs::All("all".to_string())
}
}
impl AllowedGlobs {
#[allow(dead_code)]
pub fn is_allowed(&self, path: &str) -> bool {
match self {
AllowedGlobs::All(s) if s == "all" => true,
AllowedGlobs::List(globs) => globs.iter().any(|g| match glob::Pattern::new(g) {
Ok(pattern) => pattern.matches(path),
Err(_) => false,
}),
AllowedGlobs::All(_) => false,
}
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq)]
#[serde(untagged)]
pub enum AllowedCommands {
All(String),
List(Vec<String>),
}
impl Default for AllowedCommands {
fn default() -> Self {
AllowedCommands::All("all".to_string())
}
}
impl AllowedCommands {
#[allow(dead_code)]
pub fn is_allowed(&self, command_line: &str) -> bool {
match self {
AllowedCommands::All(s) if s == "all" => true,
AllowedCommands::List(commands) => {
let cmd_prog = command_line.split_whitespace().next().unwrap_or("");
commands.iter().any(|c| cmd_prog == c)
}
AllowedCommands::All(_) => false,
}
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
pub struct Initialize {
#[serde(rename = "type")]
#[serde(default = "default_init_type")]
pub init_type: InitializeType,
pub any_workspace_path: String,
#[serde(default)]
pub initial_files_to_read: Vec<String>,
#[serde(default = "String::new")]
#[serde(deserialize_with = "deserialize_string_or_null")]
pub task_id_to_resume: String,
#[serde(default = "default_mode_name")]
pub mode_name: ModeName,
#[serde(default)]
#[serde(deserialize_with = "deserialize_string_or_null")]
pub thread_id: String,
#[serde(default)]
#[serde(deserialize_with = "deserialize_code_writer_config")]
pub code_writer_config: Option<CodeWriterConfig>,
}
fn deserialize_string_or_null<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
let result = serde_json::Value::deserialize(deserializer)?;
match result {
serde_json::Value::Null => Ok(String::new()),
serde_json::Value::String(s) => {
if s == "null" {
Ok(String::new())
} else {
Ok(s)
}
}
_ => match serde_json::to_string(&result) {
Ok(s) => Ok(s),
Err(_) => Ok(String::new()),
},
}
}
fn deserialize_code_writer_config<'de, D>(
deserializer: D,
) -> Result<Option<CodeWriterConfig>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::Null => Ok(None),
serde_json::Value::String(s) if s == "null" => Ok(None),
_ => {
match serde_json::from_value::<CodeWriterConfig>(value.clone()) {
Ok(config) => {
tracing::debug!("Successfully parsed CodeWriterConfig: {:?}", config);
Ok(Some(config))
}
Err(e) => {
tracing::error!("Failed to parse CodeWriterConfig: {}. Value: {}", e, value);
Ok(None) }
}
}
}
}
fn default_mode_name() -> ModeName {
ModeName::Wcgw
}
fn default_init_type() -> InitializeType {
InitializeType::FirstCall
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Modes {
Wcgw,
Architect,
CodeWriter,
}
impl std::fmt::Display for Modes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Modes::Wcgw => write!(f, "wcgw"),
Modes::Architect => write!(f, "architect"),
Modes::CodeWriter => write!(f, "code_writer"),
}
}
}
impl JsonSchema for Modes {
fn schema_name() -> std::borrow::Cow<'static, str> {
"Modes".into()
}
fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::Schema::new_ref("#/definitions/Modes".to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum SpecialKey {
Enter,
#[serde(rename = "Key-up")]
KeyUp,
#[serde(rename = "Key-down")]
KeyDown,
#[serde(rename = "Key-left")]
KeyLeft,
#[serde(rename = "Key-right")]
KeyRight,
#[serde(rename = "Ctrl-c")]
CtrlC,
#[serde(rename = "Ctrl-d")]
CtrlD,
#[serde(rename = "Ctrl-z")]
CtrlZ,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct ReadFiles {
pub file_paths: Vec<String>,
#[serde(skip)]
#[schemars(skip)]
pub start_line_nums: Vec<Option<usize>>,
#[serde(skip)]
#[schemars(skip)]
pub end_line_nums: Vec<Option<usize>>,
}
impl<'de> Deserialize<'de> for ReadFiles {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct ReadFilesHelper {
file_paths: Option<Vec<String>>,
}
let input = serde_json::Value::deserialize(deserializer)?;
if !input.is_object() {
if input.is_null() {
return Err(serde::de::Error::custom("Cannot convert null to ReadFiles object."));
}
return Err(serde::de::Error::custom(format!("Expected object, got {input}")));
}
let helper: ReadFilesHelper = serde_json::from_value(input.clone())
.map_err(|e| serde::de::Error::custom(format!("Failed to parse ReadFiles: {e}")))?;
let file_paths = match helper.file_paths {
Some(paths) if !paths.is_empty() => paths,
Some(_) => return Err(serde::de::Error::custom("file_paths must not be empty.")),
None => return Err(serde::de::Error::custom("file_paths is required.")),
};
let mut clean_file_paths = Vec::with_capacity(file_paths.len());
let mut start_line_nums = Vec::with_capacity(file_paths.len());
let mut end_line_nums = Vec::with_capacity(file_paths.len());
for path in file_paths {
let (clean_path, start, end) = parse_file_path_with_line_range(&path);
clean_file_paths.push(clean_path);
start_line_nums.push(start);
end_line_nums.push(end);
}
Ok(ReadFiles { file_paths: clean_file_paths, start_line_nums, end_line_nums })
}
}
fn parse_file_path_with_line_range(path: &str) -> (String, Option<usize>, Option<usize>) {
let Some((potential_path, line_spec)) = path.rsplit_once(':') else {
return (path.to_string(), None, None);
};
let Some((start, end)) = parse_line_spec(line_spec) else {
return (path.to_string(), None, None);
};
(potential_path.to_string(), start, end)
}
fn parse_line_spec(line_spec: &str) -> Option<(Option<usize>, Option<usize>)> {
if line_spec.chars().all(|c| c.is_ascii_digit()) {
return line_spec.parse().ok().map(|line| (Some(line), None));
}
let (start, end) = line_spec.split_once('-')?;
if start.is_empty() && !end.is_empty() && end.chars().all(|c| c.is_ascii_digit()) {
return end.parse().ok().map(|line| (None, Some(line)));
}
if !start.is_empty()
&& start.chars().all(|c| c.is_ascii_digit())
&& (end.is_empty() || end.chars().all(|c| c.is_ascii_digit()))
{
let start = start.parse().ok()?;
let end = if end.is_empty() { None } else { Some(end.parse().ok()?) };
return Some((Some(start), end));
}
None
}
impl ReadFiles {
pub fn show_line_numbers(&self) -> bool {
true
}
pub fn get_clean_path(&self, index: usize) -> String {
parse_file_path_with_line_range(&self.file_paths[index]).0
}
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum BashCommandAction {
Command {
command: String,
#[serde(default)]
is_background: bool,
},
StatusCheck {
#[serde(default = "default_true")]
status_check: bool,
bg_command_id: Option<String>,
},
SendText {
send_text: String,
bg_command_id: Option<String>,
#[serde(default)]
submit: bool,
},
SendSpecials {
send_specials: Vec<SpecialKey>,
bg_command_id: Option<String>,
#[serde(default)]
submit: bool,
},
SendAscii {
send_ascii: Vec<u8>,
bg_command_id: Option<String>,
#[serde(default)]
submit: bool,
},
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct BashCommand {
pub action_json: BashCommandAction,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub wait_for_seconds: Option<f32>,
#[serde(default)]
pub thread_id: String,
}
impl<'de> Deserialize<'de> for BashCommand {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct BashCommandHelper {
action_json: serde_json::Value,
#[serde(default)]
wait_for_seconds: Option<f32>,
#[serde(default)]
#[serde(deserialize_with = "deserialize_string_or_null")]
thread_id: String,
}
let helper = BashCommandHelper::deserialize(deserializer)?;
let action_json = match helper.action_json {
serde_json::Value::String(s) => {
let sanitized = s.replace('\n', " ");
match serde_json::from_str(&sanitized) {
Ok(json) => json,
Err(e) => {
tracing::warn!(
"Failed to parse action_json as JSON, trying fallback: {}",
e
);
if s.contains("command") && s.contains('{') && s.contains('}') {
tracing::debug!("JSON parse error on: {}", s);
let re_sanitized = s
.replace('\n', "\\n") .replace('\r', "\\r") .replace('\t', "\\t");
let re_sanitized = if !s.contains('"') && s.contains(':') {
tracing::debug!("Attempting to fix unquoted JSON keys/values");
re_sanitized
} else {
re_sanitized
};
match serde_json::from_str(&re_sanitized) {
Ok(json) => json,
Err(err) => {
tracing::error!("Secondary JSON parse error: {}", err);
serde_json::json!({"type": "command", "command": s})
}
}
} else {
tracing::info!("Treating as simple command: {}", s);
serde_json::json!({"type": "command", "command": s})
}
}
}
}
value => value,
};
let mut action: BashCommandAction =
serde_json::from_value(action_json.clone()).map_err(|e| {
tracing::error!(
"Failed to deserialize action_json to BashCommandAction: {}\nProblematic JSON: {}",
e,
action_json
);
let err_str = e.to_string();
if err_str.contains("unexpected token") || err_str.contains("Unexpected token") {
return serde::de::Error::custom(format!(
"JSON syntax error: {e}. Please check your JSON structure. Each field name should be in quotes, and string values should be in quotes."
));
}
serde::de::Error::custom(format!("Invalid action_json: {e}. Please ensure your JSON is properly formatted."))
})?;
Ok(BashCommand {
action_json: action,
wait_for_seconds: helper.wait_for_seconds,
thread_id: normalize_thread_id(&helper.thread_id),
})
}
}
#[derive(Debug, Clone, JsonSchema, PartialEq)]
pub struct BashCommandMode {
pub bash_mode: BashMode,
pub allowed_commands: AllowedCommands,
}
#[derive(Debug, Clone, Copy, JsonSchema, PartialEq)]
pub enum BashMode {
NormalMode,
RestrictedMode,
}
#[derive(Debug, Clone, JsonSchema, PartialEq)]
pub struct FileEditMode {
pub allowed_globs: AllowedGlobs,
}
#[derive(Debug, Clone, JsonSchema, PartialEq)]
pub struct WriteIfEmptyMode {
pub allowed_globs: AllowedGlobs,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct FileWriteOrEdit {
pub file_path: String,
pub percentage_to_change: u32,
pub text_or_search_replace_blocks: String,
pub thread_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ContextSave {
pub id: String,
pub project_root_path: String,
pub description: String,
pub relevant_file_globs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ReadImage {
pub file_path: String,
}