vtcode-core 0.103.1

Core library for VT Code - a Rust-based terminal coding agent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
//! Entity resolution for vibe coding support
//!
//! This module provides fuzzy entity matching to resolve vague terms like
//! "the sidebar" or "that button" to actual workspace entities (files, components, etc.)

use anyhow::{Context, Result};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use vtcode_commons::fs::{read_file_with_context, write_file_with_context};
use vtcode_commons::utils::current_timestamp;

/// Maximum number of entity matches to return
const MAX_ENTITY_MATCHES: usize = 5;

/// Maximum number of recent edits to track
const MAX_RECENT_EDITS: usize = 50;

/// Location of an entity within a file
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileLocation {
    pub path: PathBuf,
    pub line_start: usize,
    pub line_end: usize,
    pub content_preview: String,
}

/// A matched entity with confidence score
#[derive(Debug, Clone)]
pub struct EntityMatch {
    pub entity: String,
    pub locations: Vec<FileLocation>,
    pub confidence: f32,
    pub recency_score: f32,
    pub mention_score: f32,
    pub proximity_score: f32,
}

impl EntityMatch {
    /// Calculate total score for ranking
    pub fn total_score(&self) -> f32 {
        self.confidence + self.recency_score + self.mention_score + self.proximity_score
    }
}

/// Reference to an entity that was recently edited
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityReference {
    pub entity: String,
    pub file: PathBuf,
    pub timestamp: u64,
}

/// Value found in a style file (CSS, SCSS, etc.)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StyleValue {
    pub property: String,
    pub value: String,
    pub file: PathBuf,
    pub line: usize,
}

/// Index of entities in the workspace
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityIndex {
    /// UI components (Sidebar, Button, etc.)
    pub ui_components: HashMap<String, Vec<FileLocation>>,

    /// Functions and methods
    pub functions: HashMap<String, Vec<FileLocation>>,

    /// Classes and structs
    pub classes: HashMap<String, Vec<FileLocation>>,

    /// Style properties (padding, color, etc.)
    pub style_properties: HashMap<String, Vec<StyleValue>>,

    /// Config keys
    pub config_keys: HashMap<String, Vec<FileLocation>>,

    /// Recent edits for recency ranking
    pub recent_edits: VecDeque<EntityReference>,

    /// Last mentioned entities with timestamps
    pub last_mentioned: HashMap<String, u64>,

    /// Last update timestamp
    pub last_updated: u64,
}

impl Default for EntityIndex {
    fn default() -> Self {
        Self {
            ui_components: HashMap::new(),
            functions: HashMap::new(),
            classes: HashMap::new(),
            style_properties: HashMap::new(),
            config_keys: HashMap::new(),
            recent_edits: VecDeque::with_capacity(MAX_RECENT_EDITS),
            last_mentioned: HashMap::new(),
            last_updated: current_timestamp(),
        }
    }
}

impl EntityIndex {
    /// Record an entity mention for recency tracking
    pub fn record_mention(&mut self, entity: &str) {
        self.last_mentioned
            .insert(entity.to_lowercase(), current_timestamp());
    }

    /// Record a recent edit
    pub fn record_edit(&mut self, entity: String, file: PathBuf) {
        let reference = EntityReference {
            entity,
            file,
            timestamp: current_timestamp(),
        };

        self.recent_edits.push_back(reference);

        // Keep bounded
        while self.recent_edits.len() > MAX_RECENT_EDITS {
            self.recent_edits.pop_front();
        }
    }

    /// Check if file was recently edited
    pub fn was_recently_edited(&self, file: &Path, within_seconds: u64) -> bool {
        let cutoff = current_timestamp().saturating_sub(within_seconds);
        self.recent_edits
            .iter()
            .any(|r| r.file == file && r.timestamp >= cutoff)
    }
}

/// Entity resolver for fuzzy matching
pub struct EntityResolver {
    /// The entity index
    index: EntityIndex,

    /// Workspace root for path resolution
    #[allow(dead_code)]
    workspace_root: PathBuf,

    /// Cache file path
    cache_path: Option<PathBuf>,
}

impl EntityResolver {
    /// Create a new entity resolver
    pub fn new(workspace_root: PathBuf) -> Self {
        Self {
            index: EntityIndex::default(),
            workspace_root,
            cache_path: None,
        }
    }

    /// Create with cache file path
    pub fn with_cache(workspace_root: PathBuf, cache_path: PathBuf) -> Self {
        Self {
            index: EntityIndex::default(),
            workspace_root,
            cache_path: Some(cache_path),
        }
    }

    /// Load index from cache file
    pub async fn load_cache(&mut self) -> Result<()> {
        if let Some(cache_path) = &self.cache_path
            && cache_path.exists()
        {
            let content = read_file_with_context(cache_path, "entity cache")
                .await
                .with_context(|| format!("Failed to read entity cache at {:?}", cache_path))?;

            self.index = serde_json::from_str(&content)
                .with_context(|| "Failed to deserialize entity cache")?;
        }
        Ok(())
    }

    /// Save index to cache file
    pub async fn save_cache(&self) -> Result<()> {
        if let Some(cache_path) = &self.cache_path {
            let content = serde_json::to_string_pretty(&self.index)
                .with_context(|| "Failed to serialize entity cache")?;

            write_file_with_context(cache_path, &content, "entity cache")
                .await
                .with_context(|| format!("Failed to write entity cache to {:?}", cache_path))?;
        }
        Ok(())
    }

    /// Check if the entity index is empty
    pub fn index_is_empty(&self) -> bool {
        self.index.ui_components.is_empty()
            && self.index.functions.is_empty()
            && self.index.classes.is_empty()
    }

    /// Resolve a vague term to entity matches
    pub fn resolve(&self, term: &str) -> Option<EntityMatch> {
        let matches = self.find_entity_fuzzy(term);

        // Return best match if any
        matches.into_iter().max_by(|a, b| {
            a.total_score()
                .partial_cmp(&b.total_score())
                .unwrap_or(std::cmp::Ordering::Equal)
        })
    }

    /// Find entities using fuzzy matching
    fn find_entity_fuzzy(&self, term: &str) -> Vec<EntityMatch> {
        let term_lower = term.to_lowercase();
        let mut matches = Vec::new();

        // Search UI components
        self.search_hashmap(&self.index.ui_components, &term_lower, &mut matches);

        // Search functions
        self.search_hashmap(&self.index.functions, &term_lower, &mut matches);

        // Search classes
        self.search_hashmap(&self.index.classes, &term_lower, &mut matches);

        // Sort by total score and limit
        matches.sort_by(|a, b| {
            b.total_score()
                .partial_cmp(&a.total_score())
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        matches.truncate(MAX_ENTITY_MATCHES);

        matches
    }

    /// Search a hashmap for matching entities
    fn search_hashmap(
        &self,
        map: &HashMap<String, Vec<FileLocation>>,
        term: &str,
        matches: &mut Vec<EntityMatch>,
    ) {
        for (entity, locations) in map {
            let entity_lower = entity.to_lowercase();

            // Exact match
            if entity_lower == term {
                matches.push(EntityMatch {
                    entity: entity.clone(),
                    locations: locations.clone(),
                    confidence: 1.0,
                    recency_score: self.calculate_recency_score(&entity_lower),
                    mention_score: self.calculate_mention_score(&entity_lower),
                    proximity_score: 0.0,
                });
                continue;
            }

            // Case-insensitive substring match
            if entity_lower.contains(term) {
                let confidence = term.len() as f32 / entity_lower.len() as f32;
                matches.push(EntityMatch {
                    entity: entity.clone(),
                    locations: locations.clone(),
                    confidence: confidence * 0.8, // Slightly lower than exact
                    recency_score: self.calculate_recency_score(&entity_lower),
                    mention_score: self.calculate_mention_score(&entity_lower),
                    proximity_score: 0.0,
                });
                continue;
            }

            // Fuzzy match using Levenshtein distance
            let distance = levenshtein_distance(term, &entity_lower);
            if distance <= 2 {
                let confidence =
                    1.0 - (distance as f32 / term.len().max(entity_lower.len()) as f32);
                matches.push(EntityMatch {
                    entity: entity.clone(),
                    locations: locations.clone(),
                    confidence: confidence * 0.6, // Lower confidence for fuzzy
                    recency_score: self.calculate_recency_score(&entity_lower),
                    mention_score: self.calculate_mention_score(&entity_lower),
                    proximity_score: 0.0,
                });
            }
        }
    }

    /// Calculate recency score based on recent edits
    fn calculate_recency_score(&self, entity: &str) -> f32 {
        let now = current_timestamp();

        // Check if entity was recently edited (within 5 minutes)
        if let Some(edit) = self
            .index
            .recent_edits
            .iter()
            .rev()
            .find(|e| e.entity.to_lowercase() == entity)
        {
            let age_seconds = now.saturating_sub(edit.timestamp);
            if age_seconds < 300 {
                // Score decays over 5 minutes
                return 0.3 * (1.0 - (age_seconds as f32 / 300.0));
            }
        }

        0.0
    }

    /// Calculate mention score based on conversation history
    fn calculate_mention_score(&self, entity: &str) -> f32 {
        if let Some(&timestamp) = self.index.last_mentioned.get(entity) {
            let now = current_timestamp();
            let age_seconds = now.saturating_sub(timestamp);

            // Score decays over 10 minutes
            if age_seconds < 600 {
                return 0.2 * (1.0 - (age_seconds as f32 / 600.0));
            }
        }

        0.0
    }

    /// Record a mention for recency tracking
    pub fn record_mention(&mut self, entity: &str) {
        self.index.record_mention(entity);
    }

    /// Record an edit for recency tracking
    pub fn record_edit(&mut self, entity: String, file: PathBuf) {
        self.index.record_edit(entity, file);
    }

    /// Get mutable access to the index for building
    pub fn index_mut(&mut self) -> &mut EntityIndex {
        &mut self.index
    }

    /// Get read-only access to the index
    pub fn index(&self) -> &EntityIndex {
        &self.index
    }
}

/// Calculate Levenshtein distance between two strings
fn levenshtein_distance(a: &str, b: &str) -> usize {
    let a_chars: Vec<char> = a.chars().collect();
    let b_chars: Vec<char> = b.chars().collect();
    let a_len = a_chars.len();
    let b_len = b_chars.len();

    if a_len == 0 {
        return b_len;
    }
    if b_len == 0 {
        return a_len;
    }

    // Keep only two rows to reduce memory traffic and allocations.
    let mut prev_row: Vec<usize> = (0..=b_len).collect();
    let mut curr_row = vec![0usize; b_len + 1];

    for (i, a_char) in a_chars.iter().enumerate() {
        curr_row[0] = i + 1;

        for (j, b_char) in b_chars.iter().enumerate() {
            let cost = usize::from(a_char != b_char);
            curr_row[j + 1] = (prev_row[j + 1] + 1)
                .min(curr_row[j] + 1)
                .min(prev_row[j] + cost);
        }

        std::mem::swap(&mut prev_row, &mut curr_row);
    }

    prev_row[b_len]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_levenshtein_distance() {
        assert_eq!(levenshtein_distance("", ""), 0);
        assert_eq!(levenshtein_distance("hello", "hello"), 0);
        assert_eq!(levenshtein_distance("hello", "hallo"), 1);
        assert_eq!(levenshtein_distance("sidebar", "sidbar"), 1);
        assert_eq!(levenshtein_distance("button", "btn"), 3);
    }

    #[test]
    fn test_entity_match_scoring() {
        let match1 = EntityMatch {
            entity: "Sidebar".to_string(),
            locations: vec![],
            confidence: 1.0,
            recency_score: 0.3,
            mention_score: 0.2,
            proximity_score: 0.0,
        };

        assert_eq!(match1.total_score(), 1.5);
    }

    #[tokio::test]
    async fn test_entity_resolver_exact_match() {
        let mut resolver = EntityResolver::new(PathBuf::from("/test"));

        resolver.index_mut().ui_components.insert(
            "Sidebar".to_string(),
            vec![FileLocation {
                path: PathBuf::from("src/Sidebar.tsx"),
                line_start: 1,
                line_end: 50,
                content_preview: "export const Sidebar = () => {}".to_string(),
            }],
        );

        let result = resolver.resolve("sidebar");
        assert!(result.is_some());

        let matched = result.unwrap();
        assert_eq!(matched.entity, "Sidebar");
        assert_eq!(matched.confidence, 1.0);
    }
}