aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-ai/src/user_context/mod.rs
blob: fdeb890bce250ed75d644e8dfa18469c15cd2e01 (plain) (blame)
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
//! User-authored context files (`TERMINAL.md`).
//!
//! Context files are markdown documents that can embed shell commands for
//! dynamic content. Before each API request, context files are discovered
//! by walking the filesystem, commands are executed, and the interpolated
//! content is sent to the server as `config.user_contexts`.

pub(crate) mod interpolate;
mod walker;

use std::path::Path;

pub(crate) use walker::global_context_path;

/// A fully resolved user context, ready to include in an API request.
#[derive(Debug, Clone, serde::Serialize)]
pub(crate) struct UserContext {
    /// The path to the context file on disk.
    pub path: String,
    /// The interpolated content.
    pub data: String,
}

/// Discover context files and interpolate embedded commands.
///
/// Walks from `start` up to the filesystem root looking for
/// `.atuin/ai-context.md`, then checks `global_path`. Returns contexts
/// ordered from most general (global/root) to most specific (deepest).
pub(crate) async fn gather(
    start: &Path,
    global_path: Option<&Path>,
    shell: &str,
) -> Vec<UserContext> {
    let raw_files = match walker::walk(start, global_path).await {
        Ok(files) => files,
        Err(e) => {
            tracing::warn!("Failed to walk for context files: {e}");
            return Vec::new();
        }
    };

    if raw_files.is_empty() {
        return Vec::new();
    }

    // Interpolate all files in parallel.
    let mut handles = Vec::with_capacity(raw_files.len());
    for file in raw_files {
        let shell = shell.to_string();
        handles.push(tokio::spawn(async move {
            let data = interpolate::interpolate(&file.content, &shell).await;
            UserContext {
                path: file.path.to_string_lossy().to_string(),
                data,
            }
        }));
    }

    let mut contexts = Vec::with_capacity(handles.len());
    for handle in handles {
        match handle.await {
            Ok(ctx) => contexts.push(ctx),
            Err(e) => tracing::warn!("Context interpolation task failed: {e}"),
        }
    }

    contexts
}