返回 CodeWhale
context_reference.rs
根目录 / crates / core / src / context_reference.rs
1 //! Durable context reference records and media attachment parsing.
2 //!
3 //! Separated from composer completion and terminal-only UI so session persistence,
4 //! image attachment, and engine history can reference context and attachment items
5 //! without depending on `codewhale-tui`.
6
7 use serde::{Deserialize, Serialize};
8
9 /// The transcript keeps the user's compact text (`@path` or `[Attached ...]`)
10 /// readable. This record preserves the exact target and inclusion state for
11 /// the context inspector and for session resume without leaking raw metadata
12 /// into the visible history cell.
13 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14 pub struct ContextReference {
15 pub kind: ContextReferenceKind,
16 pub source: ContextReferenceSource,
17 /// Short badge for terminal display, e.g. `file`, `dir`, `image`.
18 pub badge: String,
19 /// Compact display label from the transcript, without the leading `@`.
20 pub label: String,
21 /// Resolved target path or URI-equivalent string.
22 pub target: String,
23 pub included: bool,
24 pub expanded: bool,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub detail: Option<String>,
27 }
28
29 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30 #[serde(rename_all = "snake_case")]
31 pub enum ContextReferenceKind {
32 File,
33 Directory,
34 Missing,
35 Unsupported,
36 MediaMention,
37 MediaAttachment,
38 /// `@git` / `@diff` — curated git context rather than a path (#4067).
39 GitContext,
40 }
41
42 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43 #[serde(rename_all = "snake_case")]
44 pub enum ContextReferenceSource {
45 AtMention,
46 Attachment,
47 }
48
49 #[derive(Debug, Clone, PartialEq, Eq)]
50 pub struct MediaAttachmentReference {
51 pub kind: String,
52 pub path: String,
53 pub start_byte: usize,
54 pub end_byte: usize,
55 }
56
57 /// Extract media attachment references from text formatted as `[Attached <kind>: <path>]`.
58 #[must_use]
59 pub fn media_attachment_references(input: &str) -> Vec<MediaAttachmentReference> {
60 let mut out = Vec::new();
61 let mut offset = 0usize;
62 for line in input.split_inclusive('\n') {
63 let start_byte = offset;
64 let end_byte = offset + line.len();
65 offset = end_byte;
66 let trimmed = line.trim();
67 let Some(body) = trimmed
68 .strip_prefix("[Attached ")
69 .and_then(|value| value.strip_suffix(']'))
70 else {
71 continue;
72 };
73 let Some((kind, rest)) = body.split_once(": ") else {
74 continue;
75 };
76 let path = rest
77 .rsplit_once(" at ")
78 .map_or(rest, |(_, path)| path)
79 .trim();
80 if !path.is_empty() {
81 out.push(MediaAttachmentReference {
82 kind: kind.trim().to_string(),
83 path: path.to_string(),
84 start_byte,
85 end_byte,
86 });
87 }
88 }
89 out
90 }
91
92 #[cfg(test)]
93 mod tests {
94 use super::*;
95
96 #[test]
97 fn serialization_roundtrip() {
98 let reference = ContextReference {
99 kind: ContextReferenceKind::File,
100 source: ContextReferenceSource::AtMention,
101 badge: "file".to_string(),
102 label: "test.rs".to_string(),
103 target: "/path/to/test.rs".to_string(),
104 included: true,
105 expanded: false,
106 detail: Some("included".to_string()),
107 };
108 let json = serde_json::to_string(&reference).unwrap();
109 let deserialized: ContextReference = serde_json::from_str(&json).unwrap();
110 assert_eq!(reference, deserialized);
111 }
112
113 #[test]
114 fn parses_media_attachments() {
115 let input = "Here is the screenshot:\n[Attached image: 100x100 at /tmp/shot.png]\nPlease analyze it.";
116 let refs = media_attachment_references(input);
117 assert_eq!(refs.len(), 1);
118 assert_eq!(refs[0].kind, "image");
119 assert_eq!(refs[0].path, "/tmp/shot.png");
120 }
121 }
122
122 lines RUST