Implement chat and markdown views, enhance status bar, and add workflow panel
- Added `chat.rs` for rendering chat messages with timestamps and roles. - Introduced `markdown.rs` for rendering markdown content with styling. - Created `status.rs` to display the application status bar with session and message counts. - Developed `workflow.rs` to show the current workflow status, including tool calls and active jobs. - Established a `theme.rs` for centralized color management across the UI. - Updated `mod.rs` to include new modules and manage rendering logic.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
use super::super::resolve_path;
|
||||
use super::helpers::arg_str;
|
||||
|
||||
pub struct Edit;
|
||||
|
||||
impl Tool for Edit {
|
||||
fn name(&self) -> &'static str {
|
||||
"edit"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Replace a string in a file with a new string. The old string must be unique unless replace_all is true."
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the file to edit (relative to workspace root)"
|
||||
},
|
||||
"old": {
|
||||
"type": "string",
|
||||
"description": "The exact text to replace"
|
||||
},
|
||||
"new": {
|
||||
"type": "string",
|
||||
"description": "The replacement text"
|
||||
},
|
||||
"replace_all": {
|
||||
"type": "boolean",
|
||||
"description": "Replace all occurrences instead of requiring uniqueness"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "Reason for the change (must be non-empty)"
|
||||
}
|
||||
},
|
||||
"required": ["path", "old", "new", "reason"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = arg_str(args, "path")?;
|
||||
let old = arg_str(args, "old")?;
|
||||
let new_str = arg_str(args, "new")?;
|
||||
let reason = arg_str(args, "reason")?;
|
||||
if reason.trim().is_empty() {
|
||||
anyhow::bail!("reason must be a non-empty string");
|
||||
}
|
||||
let replace_all = args.get("replace_all").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let path: PathBuf = resolve_path(&ctx.workspaces, &rel)?;
|
||||
if !path.exists() {
|
||||
anyhow::bail!("file '{}' does not exist at resolved path {}", rel, path.display());
|
||||
}
|
||||
if path.is_dir() {
|
||||
anyhow::bail!("'{}' is a directory, not a file", rel);
|
||||
}
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|e| anyhow!("failed to read '{}': {}", rel, e))?;
|
||||
if !content.contains(&old) {
|
||||
anyhow::bail!("old string not found in '{}'", rel);
|
||||
}
|
||||
if !replace_all {
|
||||
let count = content.matches(&old).count();
|
||||
if count > 1 {
|
||||
anyhow::bail!(
|
||||
"old string appears {} times in '{}'. Set replace_all=true to replace all occurrences, or provide a more specific match.",
|
||||
count, rel
|
||||
);
|
||||
}
|
||||
}
|
||||
let new_content = if replace_all {
|
||||
content.replace(&old, &new_str)
|
||||
} else {
|
||||
content.replacen(&old, &new_str, 1)
|
||||
};
|
||||
fs::write(&path, &new_content)
|
||||
.map_err(|e| anyhow!("failed to write '{}': {}", rel, e))?;
|
||||
let bytes_diff = if new_content.len() > content.len() {
|
||||
new_content.len() - content.len()
|
||||
} else {
|
||||
content.len() - new_content.len()
|
||||
};
|
||||
Ok(format!("edited {} ({} byte delta)", rel, bytes_diff as isize))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
use std::path::Path;
|
||||
use serde_json::Value;
|
||||
use anyhow::{Result, anyhow};
|
||||
|
||||
pub fn arg_str(args: &Value, name: &str) -> Result<String> {
|
||||
args.get(name)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| anyhow!("missing required argument: {}", name))
|
||||
}
|
||||
|
||||
pub fn not_found_help(ctx: &super::super::ToolCtx, path: &Path, rel: &str) -> String {
|
||||
let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
let in_ws = ctx.workspaces.iter().any(|w| {
|
||||
let wc = w.canonicalize().unwrap_or_else(|_| w.to_path_buf());
|
||||
canon.starts_with(&wc)
|
||||
});
|
||||
if !in_ws {
|
||||
format!(
|
||||
"path '{}' is outside all workspace roots. Workspace roots: {}",
|
||||
rel,
|
||||
ctx.workspaces.iter().map(|w| w.display().to_string()).collect::<Vec<_>>().join(", ")
|
||||
)
|
||||
} else {
|
||||
format!("path '{}' does not exist (resolved to {})", rel, canon.display())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod edit;
|
||||
pub mod helpers;
|
||||
pub mod read;
|
||||
pub mod write;
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
use super::super::resolve_path;
|
||||
use super::helpers::{arg_str, not_found_help};
|
||||
|
||||
pub struct Read;
|
||||
|
||||
impl Tool for Read {
|
||||
fn name(&self) -> &'static str {
|
||||
"read"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Read the contents of a file and display it with line numbers"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the file to read (relative to workspace root, or [N]prefix for other workspaces)"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of lines to return (optional)"
|
||||
}
|
||||
},
|
||||
"required": ["path"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = arg_str(args, "path")?;
|
||||
let limit = args.get("limit").and_then(|v| v.as_u64()).map(|v| v as usize);
|
||||
let path: PathBuf = match resolve_path(&ctx.workspaces, &rel) {
|
||||
Ok(p) => p,
|
||||
Err(_e) => return Ok(not_found_help(ctx, &PathBuf::from(&rel), &rel)),
|
||||
};
|
||||
if !path.exists() {
|
||||
return Ok(not_found_help(ctx, &path, &rel));
|
||||
}
|
||||
if path.is_dir() {
|
||||
return Ok(format!("'{}' is a directory, not a file. Use ls or glob to list directory contents.", rel));
|
||||
}
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|e| anyhow!("failed to read '{}': {}", rel, e))?;
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
let total = lines.len();
|
||||
let take = limit.unwrap_or(total).min(total);
|
||||
let result: String = lines[..take]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, line)| format!("{}\t{}", i + 1, line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
if take < total {
|
||||
Ok(format!("{}\n... ({} more lines, total {})", result, total - take, total))
|
||||
} else if total == 0 {
|
||||
Ok(String::new())
|
||||
} else {
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use std::fs;
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
use super::super::resolve_path;
|
||||
use super::helpers::arg_str;
|
||||
|
||||
pub struct Write;
|
||||
|
||||
impl Tool for Write {
|
||||
fn name(&self) -> &'static str {
|
||||
"write"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Write content to a file, creating parent directories as needed"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the file to write (relative to workspace root)"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Content to write to the file"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "Reason for the change (must be non-empty)"
|
||||
}
|
||||
},
|
||||
"required": ["path", "content", "reason"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = arg_str(args, "path")?;
|
||||
let content = arg_str(args, "content")?;
|
||||
let reason = arg_str(args, "reason")?;
|
||||
if reason.trim().is_empty() {
|
||||
anyhow::bail!("reason must be a non-empty string");
|
||||
}
|
||||
let path = resolve_path(&ctx.workspaces, &rel)?;
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| anyhow!("failed to create parent directories for '{}': {}", rel, e))?;
|
||||
}
|
||||
fs::write(&path, &content)
|
||||
.map_err(|e| anyhow!("failed to write '{}': {}", rel, e))?;
|
||||
Ok(format!("wrote {} bytes to {}", content.len(), rel))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user