Skip to content

Commit c09fcfb

Browse files
committed
Replace mdsh with esh, add cursor style indicator
esh template processing: - Vendor esh script (MIT licensed) into lib/esh/ - Remove mdsh dependency from Cargo.toml - Update prompts.rs to use esh instead of mdsh - Update README with esh syntax examples Cursor style indicator: - Dimmed cursor (dark gray bg) when agent is busy - Bright cursor (white bg) when ready for input
1 parent d55f7a3 commit c09fcfb

4 files changed

Lines changed: 80 additions & 37 deletions

File tree

src/app.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ impl App {
413413
&self.config.agents.foreground.model,
414414
context_tokens,
415415
self.tool_executor.running_background_count(),
416+
self.input_mode != InputMode::Normal,
416417
);
417418
let alert = self.alert.clone();
418419

src/prompts.rs

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
//! This module contains all system prompts used throughout the application,
44
//! as well as the `SystemPrompt` struct for building dynamic prompts.
55
6+
use std::fs;
67
use std::path::{Path, PathBuf};
7-
8-
use mdsh::Processor;
8+
use std::process::Command;
99

1010
use crate::config::{Config, CODEY_DIR};
1111

12+
/// Embedded esh script for template processing
13+
const ESH_SCRIPT: &str = include_str!("../lib/esh/esh");
14+
1215
/// Filename for custom system prompt additions
1316
pub const SYSTEM_MD_FILENAME: &str = "SYSTEM.md";
1417

@@ -49,7 +52,6 @@ You have access to the following tools:
4952
- Prefer `read_file` over `cat`, `head`, `tail`
5053
- Use `ls` for directory exploration
5154
- Use `grep` or `rg` for searching code
52-
- Run `pwd` early in your session to confirm your working directory
5355
- Prefer relative paths over absolute paths when possible - tool approval configs often allow execution from the current working directory but restrict access to system-wide paths
5456
- Avoid using `cd` to change directories before commands; instead, use relative paths from your working directory (e.g., `./src/main.rs` or `src/main.rs`)
5557
@@ -104,15 +106,15 @@ You have read-only access to:
104106
- If you need to suggest changes, describe them clearly for the primary agent to implement
105107
"#;
106108

107-
/// A system prompt builder that supports dynamic content via mdsh.
109+
/// A system prompt builder that supports dynamic content via esh templates.
108110
///
109111
/// The prompt is composed of:
110112
/// 1. The base system prompt (static)
111113
/// 2. User SYSTEM.md from ~/.config/codey/ (optional, dynamic)
112114
/// 3. Project SYSTEM.md from .codey/ (optional, dynamic)
113115
///
114-
/// SYSTEM.md files are processed through [mdsh](https://github.com/zimbatm/mdsh),
115-
/// allowing embedded shell commands to be executed and their output included.
116+
/// SYSTEM.md files are processed through [esh](https://github.com/jirutka/esh),
117+
/// allowing embedded shell commands using `<%= command %>` syntax.
116118
#[derive(Clone)]
117119
pub struct SystemPrompt {
118120
user_path: Option<PathBuf>,
@@ -134,7 +136,7 @@ impl SystemPrompt {
134136
/// Build the complete system prompt.
135137
///
136138
/// This reads and processes all SYSTEM.md files, executing any embedded
137-
/// shell commands via mdsh. The result is the concatenation of:
139+
/// shell commands via esh (`<%= command %>`). The result is the concatenation of:
138140
/// - Base system prompt
139141
/// - User SYSTEM.md content (if exists)
140142
/// - Project SYSTEM.md content (if exists)
@@ -143,41 +145,71 @@ impl SystemPrompt {
143145

144146
// Append user SYSTEM.md if it exists
145147
if let Some(ref user_path) = self.user_path {
146-
if let Ok(content) = std::fs::read_to_string(user_path) {
147-
let processed = self.process_mdsh(&content, user_path);
148-
tracing::debug!("Appending user SYSTEM.md from {:?}:\n{}", user_path, processed);
148+
if let Some(content) = self.load_system_md(user_path) {
149149
prompt.push_str("\n\n");
150-
prompt.push_str(&processed);
150+
prompt.push_str(&content);
151151
}
152152
}
153153

154154
// Append project SYSTEM.md if it exists
155-
if let Ok(content) = std::fs::read_to_string(&self.project_path) {
156-
let processed = self.process_mdsh(&content, &self.project_path);
157-
tracing::debug!("Appending project SYSTEM.md from {:?}:\n{}", self.project_path, processed);
155+
if let Some(content) = self.load_system_md(&self.project_path) {
158156
prompt.push_str("\n\n");
159-
prompt.push_str(&processed);
157+
prompt.push_str(&content);
160158
}
161159

162160
prompt
163161
}
164162

165-
/// Process content through mdsh, executing embedded shell commands.
166-
fn process_mdsh(&self, content: &str, path: &Path) -> String {
167-
let workdir = path
168-
.parent()
169-
.map(|p| p.as_os_str())
170-
.unwrap_or_else(|| std::ffi::OsStr::new("."));
171-
172-
let mut output = Vec::new();
173-
let mut processor = mdsh::executor::TheProcessor::new(workdir, &mut output);
163+
/// Load and process a SYSTEM.md file through esh, falling back to raw content.
164+
fn load_system_md(&self, path: &Path) -> Option<String> {
165+
if !path.exists() {
166+
return None;
167+
}
168+
self.process_esh(path)
169+
.or_else(|| fs::read_to_string(path).ok())
170+
.filter(|s| !s.is_empty())
171+
}
174172

175-
if let Err(e) = processor.process(content, &mdsh::cli::FileArg::StdHandle) {
176-
tracing::warn!("Failed to process mdsh content from {:?}: {}", path, e);
177-
return content.to_string();
173+
/// Ensure the esh script is available in the cache directory.
174+
/// Returns the path to the esh executable.
175+
// TODO: Use system esh if installed (e.g., `which esh`) before falling back to vendored version.
176+
fn ensure_esh() -> Option<PathBuf> {
177+
let cache_dir = dirs::cache_dir()?.join("codey");
178+
let esh_path = cache_dir.join("esh");
179+
180+
if !esh_path.exists() {
181+
fs::create_dir_all(&cache_dir).ok()?;
182+
fs::write(&esh_path, ESH_SCRIPT).ok()?;
183+
#[cfg(unix)]
184+
{
185+
use std::os::unix::fs::PermissionsExt;
186+
fs::set_permissions(&esh_path, fs::Permissions::from_mode(0o755)).ok()?;
187+
}
178188
}
179189

180-
String::from_utf8(output).unwrap_or_else(|_| content.to_string())
190+
Some(esh_path)
191+
}
192+
193+
/// Process content through esh, executing embedded shell commands.
194+
/// Uses `<%= $(command) %>` syntax for command substitution.
195+
fn process_esh(&self, path: &Path) -> Option<String> {
196+
let esh_path = Self::ensure_esh()?;
197+
let workdir = path.parent().unwrap_or(Path::new("."));
198+
let filename = path.file_name()?;
199+
200+
let output = Command::new(&esh_path)
201+
.arg(filename)
202+
.current_dir(workdir)
203+
.output()
204+
.ok()?;
205+
206+
if output.status.success() {
207+
String::from_utf8(output.stdout).ok()
208+
} else {
209+
let stderr = String::from_utf8_lossy(&output.stderr);
210+
tracing::warn!("esh failed for {:?}: {}", path, stderr);
211+
None
212+
}
181213
}
182214
}
183215

src/ui/input.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,12 +573,14 @@ impl InputBox {
573573
model: &'a str,
574574
context_tokens: u32,
575575
background_tasks: usize,
576+
agent_active: bool,
576577
) -> InputBoxWidget<'a> {
577578
InputBoxWidget {
578579
state: self,
579580
model,
580581
context_tokens,
581582
background_tasks,
583+
agent_active,
582584
}
583585
}
584586
}
@@ -597,6 +599,8 @@ pub struct InputBoxWidget<'a> {
597599
context_tokens: u32,
598600
/// Number of running background tasks
599601
background_tasks: usize,
602+
/// Whether the agent is actively processing (streaming, tool execution)
603+
agent_active: bool,
600604
}
601605

602606
impl Widget for InputBoxWidget<'_> {
@@ -707,7 +711,13 @@ impl Widget for InputBoxWidget<'_> {
707711
let y = inner.y + cursor_y as u16;
708712

709713
if x < inner.x + inner.width && y < inner.y + inner.height {
710-
buf[(x, y)].set_style(Style::default().bg(Color::White).fg(Color::Black));
714+
if self.agent_active {
715+
// Dimmed reversed cursor when agent is busy
716+
buf[(x, y)].set_style(Style::default().bg(Color::DarkGray).fg(Color::Black));
717+
} else {
718+
// Bright reversed cursor when ready for input
719+
buf[(x, y)].set_style(Style::default().bg(Color::White).fg(Color::Black));
720+
}
711721
}
712722
}
713723
}

src/ui/input_tests.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ fn render_input_box(input: &InputBox, width: u16, height: u16) -> String {
1212
let mut terminal = Terminal::new(backend).unwrap();
1313

1414
terminal.draw(|frame| {
15-
let widget = input.widget("test-model", 1000, 0);
15+
let widget = input.widget("test-model", 1000, 0, false);
1616
frame.render_widget(widget, frame.area());
1717
}).unwrap();
1818

@@ -35,7 +35,7 @@ fn render_input_content(input: &InputBox, width: u16, height: u16) -> String {
3535
let mut terminal = Terminal::new(backend).unwrap();
3636

3737
terminal.draw(|frame| {
38-
let widget = input.widget("test-model", 1000, 0);
38+
let widget = input.widget("test-model", 1000, 0, false);
3939
frame.render_widget(widget, frame.area());
4040
}).unwrap();
4141

@@ -162,7 +162,7 @@ fn test_render_cursor_position_at_end() {
162162
let mut terminal = Terminal::new(backend).unwrap();
163163

164164
terminal.draw(|frame| {
165-
let widget = input.widget("model", 1000, 0);
165+
let widget = input.widget("model", 1000, 0, false);
166166
frame.render_widget(widget, frame.area());
167167
}).unwrap();
168168

@@ -194,7 +194,7 @@ fn test_render_cursor_position_middle() {
194194
let mut terminal = Terminal::new(backend).unwrap();
195195

196196
terminal.draw(|frame| {
197-
let widget = input.widget("model", 1000, 0);
197+
let widget = input.widget("model", 1000, 0, false);
198198
frame.render_widget(widget, frame.area());
199199
}).unwrap();
200200

@@ -282,7 +282,7 @@ fn test_render_token_count_display() {
282282
let mut terminal = Terminal::new(backend).unwrap();
283283

284284
terminal.draw(|frame| {
285-
let widget = input.widget("model", 5000, 0);
285+
let widget = input.widget("model", 5000, 0, false);
286286
frame.render_widget(widget, frame.area());
287287
}).unwrap();
288288

@@ -308,7 +308,7 @@ fn test_render_background_tasks_indicator() {
308308
let mut terminal = Terminal::new(backend).unwrap();
309309

310310
terminal.draw(|frame| {
311-
let widget = input.widget("model", 1000, 0);
311+
let widget = input.widget("model", 1000, 0, false);
312312
frame.render_widget(widget, frame.area());
313313
}).unwrap();
314314

@@ -332,7 +332,7 @@ fn test_render_background_tasks_indicator() {
332332
let mut terminal = Terminal::new(backend).unwrap();
333333

334334
terminal.draw(|frame| {
335-
let widget = input.widget("model", 1000, 2);
335+
let widget = input.widget("model", 1000, 2, false);
336336
frame.render_widget(widget, frame.area());
337337
}).unwrap();
338338

@@ -629,7 +629,7 @@ fn test_cursor_position_after_complex_edits() {
629629
let backend = TestBackend::new(40, 5);
630630
let mut terminal = Terminal::new(backend).unwrap();
631631
terminal.draw(|f| {
632-
f.render_widget(input.widget("m", 0, 0), f.area());
632+
f.render_widget(input.widget("m", 0, 0, false), f.area());
633633
}).unwrap();
634634

635635
let buffer = terminal.backend().buffer();

0 commit comments

Comments
 (0)