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
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ jsonschema = "0.30.0"
zip = "2.2.0"
rmcp = { version = "0.8.0", features = ["client", "transport-sse-client-reqwest", "reqwest", "transport-streamable-http-client-reqwest", "transport-child-process", "tower", "auth"] }
chat-cli-ui = { path = "crates/chat-cli-ui" }
serde_yaml = "0.9"

[workspace.lints.rust]
future_incompatible = "warn"
Expand Down
1 change: 1 addition & 0 deletions crates/chat-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ jsonschema.workspace = true
zip.workspace = true
rmcp.workspace = true
chat-cli-ui.workspace = true
serde_yaml.workspace = true

[target.'cfg(unix)'.dependencies]
nix.workspace = true
Expand Down
4 changes: 4 additions & 0 deletions crates/chat-cli/src/api_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ impl ApiClient {
conversation_id,
user_input_message,
history,
agent_continuation_id,
} = conversation;

let model_id_opt: Option<String> = user_input_message.model_id.clone();
Expand All @@ -400,6 +401,8 @@ impl ApiClient {
.map(|v| v.into_iter().map(|i| i.try_into()).collect::<Result<Vec<_>, _>>())
.transpose()?,
)
.set_agent_continuation_id(agent_continuation_id)
.agent_task_type(amzn_codewhisperer_streaming_client::types::AgentTaskType::Vibe)
.build()
.expect("building conversation should not fail");

Expand Down Expand Up @@ -744,6 +747,7 @@ mod tests {
model_id: Some("model".to_owned()),
},
history: None,
agent_continuation_id: None,
})
.await
.unwrap();
Expand Down
47 changes: 42 additions & 5 deletions crates/chat-cli/src/api_client/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ pub struct ConversationState {
pub conversation_id: Option<String>,
pub user_input_message: UserInputMessage,
pub history: Option<Vec<ChatMessage>>,
pub agent_continuation_id: Option<String>,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -542,7 +543,7 @@ impl TryFrom<AssistantResponseMessage> for amzn_qdeveloper_streaming_client::typ
}

#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub enum ChatResponseStream {
AssistantResponseEvent {
content: String,
Expand All @@ -563,6 +564,16 @@ pub enum ChatResponseStream {
conversation_id: Option<String>,
utterance_id: Option<String>,
},
MetadataEvent {
total_tokens: Option<i32>,
uncached_input_tokens: Option<i32>,
output_tokens: Option<i32>,
cache_read_input_tokens: Option<i32>,
cache_write_input_tokens: Option<i32>,
},
MeteringEvent {
usage: Option<f64>,
},
SupplementaryWebLinksEvent(()),
ToolUseEvent {
tool_use_id: String,
Expand Down Expand Up @@ -609,6 +620,18 @@ impl From<amzn_codewhisperer_streaming_client::types::ChatResponseStream> for Ch
conversation_id,
utterance_id,
},
amzn_codewhisperer_streaming_client::types::ChatResponseStream::MetadataEvent(
amzn_codewhisperer_streaming_client::types::MetadataEvent { token_usage, .. },
) => ChatResponseStream::MetadataEvent {
total_tokens: token_usage.as_ref().map(|t| t.total_tokens),
uncached_input_tokens: token_usage.as_ref().map(|t| t.uncached_input_tokens),
output_tokens: token_usage.as_ref().map(|t| t.output_tokens),
cache_read_input_tokens: token_usage.as_ref().and_then(|t| t.cache_read_input_tokens),
cache_write_input_tokens: token_usage.as_ref().and_then(|t| t.cache_write_input_tokens),
},
amzn_codewhisperer_streaming_client::types::ChatResponseStream::MeteringEvent(
amzn_codewhisperer_streaming_client::types::MeteringEvent { usage, .. },
) => ChatResponseStream::MeteringEvent { usage },
amzn_codewhisperer_streaming_client::types::ChatResponseStream::ToolUseEvent(
amzn_codewhisperer_streaming_client::types::ToolUseEvent {
tool_use_id,
Expand All @@ -626,7 +649,7 @@ impl From<amzn_codewhisperer_streaming_client::types::ChatResponseStream> for Ch
amzn_codewhisperer_streaming_client::types::ChatResponseStream::SupplementaryWebLinksEvent(_) => {
ChatResponseStream::SupplementaryWebLinksEvent(())
},
_ => ChatResponseStream::Unknown,
_other => ChatResponseStream::Unknown,
}
}
}
Expand Down Expand Up @@ -665,6 +688,18 @@ impl From<amzn_qdeveloper_streaming_client::types::ChatResponseStream> for ChatR
conversation_id,
utterance_id,
},
amzn_qdeveloper_streaming_client::types::ChatResponseStream::MetadataEvent(
amzn_qdeveloper_streaming_client::types::MetadataEvent { token_usage, .. },
) => ChatResponseStream::MetadataEvent {
total_tokens: token_usage.as_ref().map(|t| t.total_tokens),
uncached_input_tokens: token_usage.as_ref().map(|t| t.uncached_input_tokens),
output_tokens: token_usage.as_ref().map(|t| t.output_tokens),
cache_read_input_tokens: token_usage.as_ref().and_then(|t| t.cache_read_input_tokens),
cache_write_input_tokens: token_usage.as_ref().and_then(|t| t.cache_write_input_tokens),
},
amzn_qdeveloper_streaming_client::types::ChatResponseStream::MeteringEvent(
amzn_qdeveloper_streaming_client::types::MeteringEvent { usage, .. },
) => ChatResponseStream::MeteringEvent { usage },
amzn_qdeveloper_streaming_client::types::ChatResponseStream::ToolUseEvent(
amzn_qdeveloper_streaming_client::types::ToolUseEvent {
tool_use_id,
Expand All @@ -682,7 +717,7 @@ impl From<amzn_qdeveloper_streaming_client::types::ChatResponseStream> for ChatR
amzn_qdeveloper_streaming_client::types::ChatResponseStream::SupplementaryWebLinksEvent(_) => {
ChatResponseStream::SupplementaryWebLinksEvent(())
},
_ => ChatResponseStream::Unknown,
_other => ChatResponseStream::Unknown,
}
}
}
Expand Down Expand Up @@ -870,7 +905,8 @@ impl From<UserInputMessage> for amzn_codewhisperer_streaming_client::types::User
.set_user_input_message_context(value.user_input_message_context.map(Into::into))
.set_user_intent(value.user_intent.map(Into::into))
.set_model_id(value.model_id)
.origin(amzn_codewhisperer_streaming_client::types::Origin::Cli)
//TODO: Setup new origin.
.origin(amzn_codewhisperer_streaming_client::types::Origin::AiEditor)
.build()
.expect("Failed to build UserInputMessage")
}
Expand All @@ -884,7 +920,8 @@ impl From<UserInputMessage> for amzn_qdeveloper_streaming_client::types::UserInp
.set_user_input_message_context(value.user_input_message_context.map(Into::into))
.set_user_intent(value.user_intent.map(Into::into))
.set_model_id(value.model_id)
.origin(amzn_qdeveloper_streaming_client::types::Origin::Cli)
//TODO: Setup new origin.
.origin(amzn_qdeveloper_streaming_client::types::Origin::AiEditor)
.build()
.expect("Failed to build UserInputMessage")
}
Expand Down
1 change: 1 addition & 0 deletions crates/chat-cli/src/cli/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ impl Default for Agent {
"file://AGENTS.md",
"file://README.md",
"file://.amazonq/rules/**/*.md",
"file://.kiro/steering/**/*.md",
]
.into_iter()
.map(Into::into)
Expand Down
93 changes: 92 additions & 1 deletion crates/chat-cli/src/cli/chat/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,8 @@ async fn process_path(
///
/// This method:
/// 1. Reads the content of the file
/// 2. Adds the (filename, content) pair to the context collection
/// 2. Checks front matter inclusion rules for steering files
/// 3. Adds the (filename, content) pair to the context collection if allowed
///
/// # Arguments
/// * `path` - The path to the file
Expand All @@ -377,10 +378,69 @@ async fn process_path(
async fn add_file_to_context(os: &Os, path: &Path, context_files: &mut Vec<(String, String)>) -> Result<()> {
let filename = path.to_string_lossy().to_string();
let content = os.fs.read_to_string(path).await?;

// Check if this is a steering file that needs front matter filtering
if filename.contains(".kiro/steering") && filename.ends_with(".md") && !should_include_steering_file(&content)? {
return Ok(());
}

context_files.push((filename, content));
Ok(())
}

#[derive(Debug, Deserialize)]
struct FrontMatter {
inclusion: Option<String>,
}

/// Check if a steering file should be included based on its front matter
fn should_include_steering_file(content: &str) -> Result<bool> {
// Check if file has YAML front matter
if !content.starts_with("---\n") {
// No front matter - include the file
return Ok(true);
}

// Find the end of the front matter
let lines: Vec<&str> = content.lines().collect();
let mut end_index = None;

for (i, line) in lines.iter().enumerate().skip(1) {
if line.trim() == "---" {
end_index = Some(i);
break;
}
}

let end_index = match end_index {
Some(idx) => idx,
None => {
// Malformed front matter - include the file
return Ok(true);
},
};

// Extract and parse the front matter
let front_matter_lines = &lines[1..end_index];
let front_matter_yaml = front_matter_lines.join("\n");

match serde_yaml::from_str::<FrontMatter>(&front_matter_yaml) {
Ok(front_matter) => {
match front_matter.inclusion.as_deref() {
Some("always") => Ok(true),
Some("fileMatch") => Ok(false), // Exclude fileMatch files
Some("manual") => Ok(false), // Exclude manual files
None => Ok(true), // No inclusion field - include
Some(_) => Ok(true), // Unknown inclusion value - include
}
},
Err(_) => {
// Failed to parse front matter - include the file
Ok(true)
},
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -458,4 +518,35 @@ mod tests {
96_000
);
}

#[test]
fn test_should_include_steering_file() {
// Test file without front matter - should be included
let content_no_frontmatter = "# Regular markdown file\nSome content here.";
assert!(should_include_steering_file(content_no_frontmatter).unwrap());

// Test file with inclusion: always - should be included
let content_always = "---\ninclusion: always\n---\n# Always included\nContent here.";
assert!(should_include_steering_file(content_always).unwrap());

// Test file with inclusion: fileMatch - should be excluded
let content_filematch = "---\ninclusion: fileMatch\n---\n# File match only\nContent here.";
assert!(!should_include_steering_file(content_filematch).unwrap());

// Test file with inclusion: manual - should be excluded
let content_manual = "---\ninclusion: manual\n---\n# Manual only\nContent here.";
assert!(!should_include_steering_file(content_manual).unwrap());

// Test file with no inclusion field - should be included
let content_no_inclusion = "---\ntitle: Some Title\n---\n# No inclusion field\nContent here.";
assert!(should_include_steering_file(content_no_inclusion).unwrap());

// Test file with malformed front matter - should be included
let content_malformed = "---\ninvalid yaml: [\n---\n# Malformed\nContent here.";
assert!(should_include_steering_file(content_malformed).unwrap());

// Test file with incomplete front matter - should be included
let content_incomplete = "---\ninclusion: always\n# Missing closing ---\nContent here.";
assert!(should_include_steering_file(content_incomplete).unwrap());
}
}
Loading
Loading