Skip to content

Commit 44ab086

Browse files
committed
feat: Add syntax highlighting for code blocks in conversation messages and introduce agent indexing with configuration updates for tool execution.
1 parent 09c87ec commit 44ab086

13 files changed

Lines changed: 2080 additions & 166 deletions

.agent_index.json

Lines changed: 1021 additions & 0 deletions
Large diffs are not rendered by default.

Cargo.lock

Lines changed: 552 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rust_tui_coder"
3-
version = "0.2.4"
3+
version = "1.0.0"
44
edition = "2021"
55
description = "AI-powered terminal coding assistant with interactive TUI, supporting multiple LLMs and comprehensive development tools"
66
license = "MIT OR Apache-2.0"
@@ -28,10 +28,14 @@ path = "src/main.rs"
2828
ratatui = { version = "0.26.0", features = ["all-widgets"] }
2929
crossterm = "0.27.0"
3030
tokio = { version = "1.35.1", features = ["full"] }
31-
reqwest = { version = "0.11.23", features = ["json", "stream"] }
31+
reqwest = { version = "0.11.23", features = ["json", "stream", "blocking"] }
32+
html2text = "0.11"
3233
serde = { version = "1.0.195", features = ["derive"] }
3334
serde_json = "1.0.111"
3435
toml = "0.8.8"
3536
futures-util = "0.3.30"
3637
chrono = "0.4"
3738
regex = "1.12.2"
39+
urlencoding = "2.1.3"
40+
syntect = { version = "5.3.0", features = ["default-fancy"] }
41+
once_cell = "1.21.3"

src/agent.rs

Lines changed: 195 additions & 25 deletions
Large diffs are not rendered by default.

src/config.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ use std::io;
55
#[derive(Deserialize, Debug, Clone)]
66
pub struct Config {
77
pub llm: LlmConfig,
8+
#[serde(default)]
9+
pub web: WebConfig,
10+
}
11+
12+
#[derive(Deserialize, Debug, Clone, Default)]
13+
pub struct WebConfig {
14+
#[serde(default = "default_provider")]
15+
pub provider: String,
16+
#[allow(dead_code)]
17+
pub api_key: Option<String>,
18+
}
19+
20+
fn default_provider() -> String {
21+
"duckduckgo".to_string()
822
}
923

1024
#[derive(Deserialize, Debug, Clone)]

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ async fn run_app<B: Backend>(
279279
// Run the agent with access to the shared app state
280280
// The agent will handle its own locking/unlocking to allow UI updates
281281
let result = agent_clone
282-
.run(&config_clone.llm, user_input_clone, app_clone)
282+
.run(&config_clone, user_input_clone, app_clone)
283283
.await;
284284
result
285285
}));

src/ui.rs

Lines changed: 104 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,93 @@
11
use crate::app::App;
2+
use once_cell::sync::Lazy;
23
use ratatui::{
34
layout::{Constraint, Direction, Layout},
45
style::{Color, Style, Stylize},
56
text::{Line, Span},
67
widgets::{Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
78
Frame,
89
};
10+
use syntect::easy::HighlightLines;
11+
use syntect::highlighting::{Style as SyntectStyle, ThemeSet};
12+
use syntect::parsing::SyntaxSet;
13+
14+
// Global syntax highlighting resources
15+
static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
16+
static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);
17+
18+
// Convert syntect color to ratatui color
19+
fn syntect_to_ratatui_color(color: syntect::highlighting::Color) -> Color {
20+
Color::Rgb(color.r, color.g, color.b)
21+
}
22+
23+
// Parse a message and extract code blocks with syntax highlighting
24+
fn parse_message_with_highlighting(text: &str, max_width: usize) -> Vec<Line<'static>> {
25+
let mut lines = Vec::new();
26+
let mut in_code_block = false;
27+
let mut code_buffer: Vec<String> = Vec::new();
28+
let mut code_language = String::new();
29+
30+
for line in text.lines() {
31+
if line.starts_with("```") {
32+
if in_code_block {
33+
// End of code block - highlight and flush
34+
let theme = &THEME_SET.themes["base16-ocean.dark"];
35+
let syntax = SYNTAX_SET
36+
.find_syntax_by_token(&code_language)
37+
.unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
38+
39+
let mut highlighter = HighlightLines::new(syntax, theme);
40+
41+
for code_line in &code_buffer {
42+
let ranges: Vec<(SyntectStyle, &str)> = highlighter
43+
.highlight_line(code_line, &SYNTAX_SET)
44+
.unwrap_or_default();
45+
46+
let mut spans = Vec::new();
47+
for (style, text) in ranges {
48+
spans.push(Span::styled(
49+
text.to_string(),
50+
Style::default().fg(syntect_to_ratatui_color(style.foreground)),
51+
));
52+
}
53+
lines.push(Line::from(spans));
54+
}
55+
56+
code_buffer.clear();
57+
in_code_block = false;
58+
} else {
59+
// Start of code block
60+
in_code_block = true;
61+
code_language = line.trim_start_matches("```").trim().to_string();
62+
if code_language.is_empty() {
63+
code_language = "txt".to_string();
64+
}
65+
}
66+
} else if in_code_block {
67+
code_buffer.push(line.to_string());
68+
} else {
69+
// Regular text - wrap it
70+
for wrapped_line in wrap_text(line, max_width) {
71+
lines.push(Line::from(vec![Span::styled(
72+
wrapped_line,
73+
Style::default().fg(Color::White),
74+
)]));
75+
}
76+
}
77+
}
78+
79+
// Handle unclosed code block
80+
if in_code_block && !code_buffer.is_empty() {
81+
for code_line in &code_buffer {
82+
lines.push(Line::from(vec![Span::styled(
83+
code_line.clone(),
84+
Style::default().fg(Color::Gray),
85+
)]));
86+
}
87+
}
88+
89+
lines
90+
}
991

1092
// Enhanced helper function to wrap text to fit within a given width
1193
fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
@@ -116,36 +198,30 @@ pub fn ui(f: &mut Frame, app: &App) {
116198
// Add all conversation messages
117199
for message in &app.conversation {
118200
if let Some(content) = message.strip_prefix("User: ") {
119-
let wrapped_content = wrap_text(content, max_width.saturating_sub(6)); // Account for "User: " prefix
120-
121-
for (i, line) in wrapped_content.iter().enumerate() {
122-
if i == 0 {
123-
conversation_lines.push(Line::from(vec![
124-
Span::styled("User: ", Style::default().fg(Color::Blue).bold()),
125-
Span::styled(line.clone(), Style::default().fg(Color::White)),
126-
]));
127-
} else {
128-
conversation_lines.push(Line::from(vec![
129-
Span::styled(" ", Style::default().fg(Color::Blue)), // Indent continuation
130-
Span::styled(line.clone(), Style::default().fg(Color::White)),
131-
]));
132-
}
201+
// Add user prefix
202+
conversation_lines.push(Line::from(vec![Span::styled(
203+
"User: ",
204+
Style::default().fg(Color::Blue).bold(),
205+
)]));
206+
207+
// Parse and highlight the content
208+
let content_lines =
209+
parse_message_with_highlighting(content, max_width.saturating_sub(6));
210+
for line in content_lines {
211+
conversation_lines.push(line);
133212
}
134213
} else if let Some(content) = message.strip_prefix("Agent: ") {
135-
let wrapped_content = wrap_text(content, max_width.saturating_sub(7)); // Account for "Agent: " prefix
136-
137-
for (i, line) in wrapped_content.iter().enumerate() {
138-
if i == 0 {
139-
conversation_lines.push(Line::from(vec![
140-
Span::styled("Agent: ", Style::default().fg(Color::Green).bold()),
141-
Span::styled(line.clone(), Style::default().fg(Color::White)),
142-
]));
143-
} else {
144-
conversation_lines.push(Line::from(vec![
145-
Span::styled(" ", Style::default().fg(Color::Green)), // Indent continuation
146-
Span::styled(line.clone(), Style::default().fg(Color::White)),
147-
]));
148-
}
214+
// Add agent prefix
215+
conversation_lines.push(Line::from(vec![Span::styled(
216+
"Agent: ",
217+
Style::default().fg(Color::Green).bold(),
218+
)]));
219+
220+
// Parse and highlight the content
221+
let content_lines =
222+
parse_message_with_highlighting(content, max_width.saturating_sub(7));
223+
for line in content_lines {
224+
conversation_lines.push(line);
149225
}
150226
} else {
151227
let wrapped_message = wrap_text(message, max_width);

tests/agent_tests.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ fn test_tool_read_file() {
2323
path: test_file.to_string(),
2424
};
2525

26-
let result = tool.execute();
26+
let result = tool.execute(&rust_tui_coder::config::WebConfig::default());
2727
assert!(result.is_ok());
2828
assert!(result.unwrap().contains("Test content"));
2929

@@ -39,7 +39,7 @@ fn test_tool_write_file() {
3939
content: "Hello World!".to_string(),
4040
};
4141

42-
let result = tool.execute();
42+
let result = tool.execute(&rust_tui_coder::config::WebConfig::default());
4343
assert!(result.is_ok());
4444
assert!(Path::new(test_file).exists());
4545

@@ -59,7 +59,7 @@ fn test_tool_append_file() {
5959
content: "Appended content".to_string(),
6060
};
6161

62-
let result = tool.execute();
62+
let result = tool.execute(&rust_tui_coder::config::WebConfig::default());
6363
assert!(result.is_ok());
6464

6565
let content = fs::read_to_string(test_file).unwrap();
@@ -80,7 +80,7 @@ fn test_tool_search_replace() {
8080
new_string: "Rust".to_string(),
8181
};
8282

83-
let result = tool.execute();
83+
let result = tool.execute(&rust_tui_coder::config::WebConfig::default());
8484
assert!(result.is_ok());
8585

8686
let content = fs::read_to_string(test_file).unwrap();
@@ -98,7 +98,7 @@ fn test_tool_delete_file() {
9898
path: test_file.to_string(),
9999
};
100100

101-
let result = tool.execute();
101+
let result = tool.execute(&rust_tui_coder::config::WebConfig::default());
102102
assert!(result.is_ok());
103103
assert!(!Path::new(test_file).exists());
104104
}
@@ -111,7 +111,7 @@ fn test_tool_create_directory() {
111111
path: test_dir.to_string(),
112112
};
113113

114-
let result = tool.execute();
114+
let result = tool.execute(&rust_tui_coder::config::WebConfig::default());
115115
assert!(result.is_ok());
116116
assert!(Path::new(test_dir).is_dir());
117117

@@ -130,7 +130,7 @@ fn test_tool_list_files() {
130130
path: test_dir.to_string(),
131131
};
132132

133-
let result = tool.execute();
133+
let result = tool.execute(&rust_tui_coder::config::WebConfig::default());
134134
assert!(result.is_ok());
135135
let output = result.unwrap();
136136
assert!(output.contains("file1.txt"));
@@ -151,7 +151,7 @@ fn test_tool_list_files_recursive() {
151151
path: test_dir.to_string(),
152152
};
153153

154-
let result = tool.execute();
154+
let result = tool.execute(&rust_tui_coder::config::WebConfig::default());
155155
assert!(result.is_ok());
156156
let output = result.unwrap();
157157
assert!(output.contains("file1.txt"));
@@ -166,7 +166,7 @@ fn test_tool_run_command() {
166166
command: "echo 'Hello from test'".to_string(),
167167
};
168168

169-
let result = tool.execute();
169+
let result = tool.execute(&rust_tui_coder::config::WebConfig::default());
170170
assert!(result.is_ok());
171171
assert!(result.unwrap().contains("Hello from test"));
172172
}
@@ -178,7 +178,7 @@ fn test_tool_execute_code_python() {
178178
code: "print('Python test')".to_string(),
179179
};
180180

181-
let result = tool.execute();
181+
let result = tool.execute(&rust_tui_coder::config::WebConfig::default());
182182
// Python might not be available in all test environments
183183
if result.is_ok() {
184184
assert!(result.unwrap().contains("Python test"));
@@ -192,7 +192,7 @@ fn test_tool_execute_code_bash() {
192192
code: "echo 'Bash test'".to_string(),
193193
};
194194

195-
let result = tool.execute();
195+
let result = tool.execute(&rust_tui_coder::config::WebConfig::default());
196196
assert!(result.is_ok());
197197
assert!(result.unwrap().contains("Bash test"));
198198
}
@@ -202,7 +202,7 @@ fn test_tool_execute_code_bash() {
202202
#[test]
203203
fn test_tool_git_status() {
204204
let tool = Tool::GitStatus;
205-
let result = tool.execute();
205+
let result = tool.execute(&rust_tui_coder::config::WebConfig::default());
206206
// Git might not be available or this might not be a git repo
207207
assert!(result.is_ok());
208208
}

0 commit comments

Comments
 (0)