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:
asepharyana
2026-07-11 13:16:10 +07:00
parent 7cb4ae6708
commit cc03bd79b6
131 changed files with 10994 additions and 0 deletions
+94
View File
@@ -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))
}
}
+27
View File
@@ -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())
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod edit;
pub mod helpers;
pub mod read;
pub mod write;
+70
View File
@@ -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)
}
}
}
+57
View File
@@ -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))
}
}