返回 CodeWhale
sandbox.rs
根目录 / crates / tui / src / repl / sandbox.rs
1 //! REPL fence-extraction utilities.
2 //!
3 //! The agent's main loop scans assistant text for ` ```repl ` fenced blocks
4 //! and feeds them to a [`crate::repl::runtime::PythonRuntime`]. Capturing
5 //! `FINAL(...)` and routing sub-LLM RPCs are handled inside the runtime via
6 //! a stdin/stdout protocol — no scraping required here.
7
8 const REPL_FENCE: &str = "```repl";
9
10 /// Byte offset of the first opening `` ```repl `` fence at or after `from`.
11 ///
12 /// A fence must open its own line (up to three spaces of indent, as in
13 /// Markdown) and carry no other info string. Prose that mentions the fence
14 /// mid-line, or a `` ```repl-output `` block, is not code to execute.
15 fn next_repl_fence(text: &str, from: usize) -> Option<usize> {
16 let mut line_start = if from == 0 || text.as_bytes().get(from - 1) == Some(&b'\n') {
17 from
18 } else {
19 from + text[from..].find('\n')? + 1
20 };
21 loop {
22 let line_end = text[line_start..]
23 .find('\n')
24 .map_or(text.len(), |offset| line_start + offset);
25 let line = &text[line_start..line_end];
26 let indent = line.len() - line.trim_start_matches(' ').len();
27 if indent <= 3
28 && line[indent..]
29 .strip_prefix(REPL_FENCE)
30 .is_some_and(|info| info.trim().is_empty())
31 {
32 return Some(line_start + indent);
33 }
34 if line_end >= text.len() {
35 return None;
36 }
37 line_start = line_end + 1;
38 }
39 }
40
41 /// Check if a string contains a `` ```repl `` fence that opens its own line.
42 pub fn has_repl_block(text: &str) -> bool {
43 next_repl_fence(text, 0).is_some()
44 }
45
46 /// Extract every line-anchored `` ```repl `` block from `text` with byte offsets.
47 pub fn extract_repl_blocks(text: &str) -> Vec<ReplBlock> {
48 let mut blocks = Vec::new();
49 let mut search_from = 0;
50
51 while let Some(start) = next_repl_fence(text, search_from) {
52 let after_fence = &text[start..];
53 let code_start = after_fence.find('\n').unwrap_or(after_fence.len());
54 let code_region = &after_fence[code_start..];
55 let Some(end_offset) = code_region.find("\n```") else {
56 break;
57 };
58 blocks.push(ReplBlock {
59 code: code_region[..end_offset].to_string(),
60 start_offset: start,
61 end_offset: start + code_start + end_offset + 3,
62 });
63 search_from = start + code_start + end_offset + 4;
64 }
65
66 blocks
67 }
68
69 /// A `` ```repl `` code block with byte-offset position info.
70 #[derive(Debug, Clone)]
71 pub struct ReplBlock {
72 pub code: String,
73 pub start_offset: usize,
74 pub end_offset: usize,
75 }
76
77 #[cfg(test)]
78 mod tests {
79 use super::*;
80
81 #[test]
82 fn has_repl_block_detects_fence() {
83 assert!(has_repl_block("some text\n```repl\ncode\n``` more"));
84 assert!(has_repl_block(" ```repl \r\ncode\n```"));
85 assert!(!has_repl_block("no repl here ```python\ncode\n```"));
86 assert!(!has_repl_block("just text"));
87 }
88
89 #[test]
90 fn extract_repl_blocks_single() {
91 let text = "before\n```repl\nprint('hello')\n```\nafter";
92 let blocks = extract_repl_blocks(text);
93 assert_eq!(blocks.len(), 1);
94 assert_eq!(blocks[0].code.trim(), "print('hello')");
95 }
96
97 #[test]
98 fn extract_repl_blocks_multiple() {
99 let text = "```repl\ncode1\n```\nmid\n```repl\ncode2\n```\nend";
100 let blocks = extract_repl_blocks(text);
101 assert_eq!(blocks.len(), 2);
102 assert_eq!(blocks[0].code.trim(), "code1");
103 assert_eq!(blocks[1].code.trim(), "code2");
104 }
105
106 #[test]
107 fn extract_repl_blocks_empty_when_none() {
108 let blocks = extract_repl_blocks("no blocks here");
109 assert!(blocks.is_empty());
110 }
111
112 #[test]
113 fn mid_line_mention_is_not_a_fence() {
114 let text = "a ```repl mention\n```\nx\n```";
115 assert!(!has_repl_block(text));
116 assert!(extract_repl_blocks(text).is_empty());
117 assert!(!has_repl_block("```repl-output\nx\n```"));
118 assert!(!has_repl_block(" ```repl\nindented code block\n```"));
119 }
120
121 #[test]
122 fn mid_line_mention_does_not_hide_a_later_fence() {
123 let text = "see ```repl here\n```repl\nprint(1)\n```";
124 let blocks = extract_repl_blocks(text);
125 assert_eq!(blocks.len(), 1);
126 assert_eq!(blocks[0].code.trim(), "print(1)");
127 assert_eq!(&text[blocks[0].start_offset..][..7], "```repl");
128 }
129 }
130
130 lines RUST