first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
@@ -5,12 +5,13 @@ use galaxy_completer::ParsedTokensSnapshot;
use itertools::Itertools as _;
use natural_language_detection::natural_language_words_score;
use crate::parser::parse_query_into_tokens;
use crate::util::{
is_installed_binary, is_likely_shell_command, is_one_off_natural_language_word_or_prefix,
};
use crate::{
ClassificationResult, Context, InputClassifier, InputType,
parser::parse_query_into_tokens,
util::{
is_installed_binary, is_likely_shell_command, is_one_off_natural_language_word_or_prefix,
},
ClassificationResult, Context, InputClassificationResult, InputClassifier,
InputClassifierDecisionSource, InputType,
};
/// Minimum number of characters the input buffer must have before classification runs.
@@ -32,7 +33,11 @@ pub struct HeuristicClassifier;
#[cfg_attr(not(target_family = "wasm"), async_trait)]
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
impl InputClassifier for HeuristicClassifier {
async fn detect_input_type(&self, input: ParsedTokensSnapshot, context: &Context) -> InputType {
async fn detect_input_type(
&self,
input: ParsedTokensSnapshot,
context: &Context,
) -> InputClassificationResult {
let word_tokens = parse_query_into_tokens(input.buffer_text.as_str());
let total_word_token_count = word_tokens.len();
@@ -46,23 +51,27 @@ impl InputClassifier for HeuristicClassifier {
if total_word_token_count == 1
&& is_one_off_natural_language_word_or_prefix(&word_tokens[0].to_lowercase())
{
log::info!("[input-classifier] → one-off NL word, returning AI");
return InputType::AI;
return InputClassificationResult::new(
InputType::AI,
InputClassifierDecisionSource::NaturalLanguageOneOffAllowlist,
);
}
if is_likely_shell_command(&input, total_word_token_count).await {
log::info!("[input-classifier] → is_likely_shell_command=true, returning Shell");
return InputType::Shell;
return InputClassificationResult::new(
InputType::Shell,
InputClassifierDecisionSource::ShellHeuristic,
);
}
let result = self
.classify_input(input, context)
.await
.map(|result| result.to_input_type())
.unwrap_or(context.current_input_type);
log::info!("[input-classifier] → classify_input returned {:?}", result);
result
.map(|result| InputClassificationResult::new(result.to_input_type(), result.source))
.unwrap_or(InputClassificationResult::new(
context.current_input_type,
InputClassifierDecisionSource::InputClassifierFallbackHeuristic,
))
}
async fn classify_input(
@@ -105,10 +114,17 @@ async fn natural_language_detection_heuristic(
_current_input_type: InputType,
include_last_token: bool,
) -> ClassificationResult {
let _word_tokens_count = word_tokens.len();
let source = InputClassifierDecisionSource::InputClassifierFallbackHeuristic;
let word_tokens_count = word_tokens.len();
if input.buffer_text.len() < MINIMUM_CLASSIFICATION_CHAR_LENGTH {
return ClassificationResult::pure_shell();
let min_token_length = if matches!(current_input_type, InputType::AI) {
MINIMUM_COMMAND_DETECTION_TOKEN_LENGTH
} else {
MINIMUM_NATURAL_LANGUAGE_DETECTION_TOKEN_LENGTH
};
if min_token_length > word_tokens_count as u8 {
return ClassificationResult::pure_shell(source);
}
let mut word_tokens = word_tokens.into_iter().map(Cow::Owned).collect_vec();
@@ -136,10 +152,10 @@ async fn natural_language_detection_heuristic(
};
if likely_english_token_count >= (updated_word_token_count as f32 * threshold) as usize {
return ClassificationResult::pure_ai();
return ClassificationResult::pure_ai(source);
}
ClassificationResult::pure_shell()
ClassificationResult::pure_shell(source)
}
#[cfg(test)]
@@ -1,14 +1,52 @@
use galaxy_completer::meta::SpannedItem;
use galaxy_completer::util::parse_current_commands_and_tokens;
use crate::{Context, test_utils::CompletionContext};
use galaxy_completer::{ParsedTokenData, ParsedTokensSnapshot};
use super::*;
use crate::Context;
use crate::test_utils::CompletionContext;
async fn mock_parsed_input_token(buffer_text: String) -> ParsedTokensSnapshot {
warp_features::mark_initialized();
let completion_context = CompletionContext::new();
parse_current_commands_and_tokens(buffer_text, &completion_context).await
}
fn mock_parsed_input_token_without_descriptions(buffer_text: &str) -> ParsedTokensSnapshot {
let mut next_search_start = 0;
let parsed_tokens = buffer_text
.split_whitespace()
.enumerate()
.map(|(token_index, token)| {
let token_start =
buffer_text[next_search_start..].find(token).unwrap() + next_search_start;
let token_end = token_start + token.len();
next_search_start = token_end;
ParsedTokenData {
token: token.to_string().spanned((token_start, token_end)),
token_index,
token_description: None,
}
})
.collect();
ParsedTokensSnapshot {
buffer_text: buffer_text.to_string(),
parsed_tokens,
}
}
async fn detected_input_type(
classifier: &HeuristicClassifier,
input: ParsedTokensSnapshot,
context: &Context,
) -> InputType {
classifier
.detect_input_type(input, context)
.await
.input_type
}
#[test]
fn test_input_detection() {
futures::executor::block_on(async move {
@@ -21,7 +59,7 @@ fn test_input_detection() {
let token = mock_parsed_input_token("cargo --version".to_string()).await;
assert_eq!(
classifier.detect_input_type(token, &context).await,
detected_input_type(&classifier, token, &context).await,
InputType::Shell
);
@@ -32,14 +70,14 @@ fn test_input_detection() {
let mut token = mock_parsed_input_token("cargo --version".to_string()).await;
token.parsed_tokens[0].token_description = None;
assert_eq!(
classifier.detect_input_type(token, &context).await,
detected_input_type(&classifier, token, &context).await,
InputType::Shell
);
let mut token = mock_parsed_input_token("rvm install 3.3".to_string()).await;
token.parsed_tokens[0].token_description = None;
assert_eq!(
classifier.detect_input_type(token, &context).await,
detected_input_type(&classifier, token, &context).await,
InputType::Shell
);
@@ -47,7 +85,7 @@ fn test_input_detection() {
let mut token = mock_parsed_input_token("Explain this".to_string()).await;
token.parsed_tokens[0].token_description = None;
assert_eq!(
classifier.detect_input_type(token.clone(), &context).await,
detected_input_type(&classifier, token.clone(), &context).await,
InputType::AI
);
@@ -57,21 +95,21 @@ fn test_input_detection() {
let mut token = mock_parsed_input_token("fix this".to_string()).await;
token.parsed_tokens[0].token_description = None;
assert_eq!(
classifier.detect_input_type(token, &context).await,
detected_input_type(&classifier, token, &context).await,
InputType::AI,
);
// Short queries with punctuation should be parsed as AI input.
let token = mock_parsed_input_token("What went wrong?".to_string()).await;
assert_eq!(
classifier.detect_input_type(token, &context).await,
detected_input_type(&classifier, token, &context).await,
InputType::AI
);
// Short queries with contractions should be parsed as AI input.
let mut token = mock_parsed_input_token("What's the reason".to_string()).await;
token.parsed_tokens[0].token_description = None;
assert_eq!(
classifier.detect_input_type(token, &context).await,
detected_input_type(&classifier, token, &context).await,
InputType::AI
);
@@ -80,7 +118,7 @@ fn test_input_detection() {
mock_parsed_input_token("The message is \"utils::future ... ok\"".to_string()).await;
token.parsed_tokens[0].token_description = None;
assert_eq!(
classifier.detect_input_type(token, &context).await,
detected_input_type(&classifier, token, &context).await,
InputType::AI
);
@@ -88,8 +126,47 @@ fn test_input_detection() {
let mut token = mock_parsed_input_token("The type is \"<>\"".to_string()).await;
token.parsed_tokens[0].token_description = None;
assert_eq!(
classifier.detect_input_type(token, &context).await,
detected_input_type(&classifier, token, &context).await,
InputType::AI
);
});
}
#[test]
fn test_input_detection_sources() {
futures::executor::block_on(async move {
let classifier = HeuristicClassifier;
let context = Context {
current_input_type: InputType::Shell,
is_agent_follow_up: false,
};
let token = mock_parsed_input_token_without_descriptions("echo hello");
let decision = classifier.detect_input_type(token, &context).await;
assert_eq!(
decision,
InputClassificationResult::new(
InputType::Shell,
InputClassifierDecisionSource::ShellHeuristic,
)
);
let token = mock_parsed_input_token_without_descriptions("explain");
let decision = classifier.detect_input_type(token, &context).await;
assert_eq!(
decision,
InputClassificationResult::new(
InputType::AI,
InputClassifierDecisionSource::NaturalLanguageOneOffAllowlist,
)
);
let token = mock_parsed_input_token_without_descriptions("fix this");
let decision = classifier.detect_input_type(token, &context).await;
assert_eq!(
decision,
InputClassificationResult::new(
InputType::AI,
InputClassifierDecisionSource::InputClassifierFallbackHeuristic,
)
);
});
}