Refactor verdict handling and editor state management
- Removed the Escalate variant from the Verdict enum and associated parsing logic. - Cleaned up the EditorState struct by removing unused fields and methods. - Simplified MCP connection functions by removing unnecessary disconnect and handle dismiss functions. - Added tick count to MiscState for managing UI updates during processing. - Updated chat and status views to display a spinner during AI processing using the tick count. - Created a README.md file to document the project, its features, architecture, usage, key bindings, agent modes, security measures, installation instructions, and configuration details.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
|||||||
|
# Zesdex
|
||||||
|
|
||||||
|
Autonomous AI coding and security agent running in a terminal-based TUI environment.
|
||||||
|
|
||||||
|
Zesdex is a Rust-powered AI assistant that operates directly in your terminal via a rich TUI interface. It combines large language model intelligence with a comprehensive set of tools to explore, understand, and modify codebases autonomously.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **TUI Interface** — Full-screen terminal UI with chat panel, input bar, and status bar built with ratatui and crossterm.
|
||||||
|
- **Multi-Agent Modes** — Auto (full autonomy), Normal (review risky ops), Plan (no mutations), Yolo (unrestricted).
|
||||||
|
- **Rich Tool System** — 20+ built-in tools for file operations, searching, bash execution, git operations, web access, memory management, and workflow orchestration.
|
||||||
|
- **Daemon Architecture** — Run as a background daemon with client attach/detach via Unix domain sockets.
|
||||||
|
- **IPC Protocol** — Bidirectional state synchronization between daemon and client processes.
|
||||||
|
- **MCP Support** — Model Context Protocol integration for connecting to external AI servers.
|
||||||
|
- **Self-Learning** — Persistent memory system that stores lessons, references, and project knowledge.
|
||||||
|
- **Workflow Engine** — Orchestrate complex multi-step tasks with parallel sub-agents, pipelines, and phased execution.
|
||||||
|
- **Security Guardrails** — Catastrophic operation detection, credential exfiltration monitoring, graduated safety checks.
|
||||||
|
- **Security Sidecar** — Python-based security analysis daemon for vulnerability scanning.
|
||||||
|
- **Provider Agnostic** — Configurable AI model providers with dynamic model selection.
|
||||||
|
- **Session Management** — Multiple sessions with history, rewind, and transcript persistence.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.rs # Entry point, single/daemon/attach modes
|
||||||
|
├── app/ # Application core
|
||||||
|
│ ├── state/ # State management (AppStateRest, types, misc)
|
||||||
|
│ ├── runtime/ # Action dispatch and event loop
|
||||||
|
│ ├── mode/ # Agent mode definitions (Auto/Normal/Plan/Yolo)
|
||||||
|
│ ├── harness.rs # Tool harness for agent execution
|
||||||
|
│ ├── workflow/ # Workflow engine (script, engine)
|
||||||
|
│ ├── mcp/ # Model Context Protocol manager
|
||||||
|
│ ├── sec/ # Security daemon integration
|
||||||
|
│ ├── subagent/ # Sub-agent orchestration
|
||||||
|
│ ├── bgbash/ # Background bash job management
|
||||||
|
│ └── review/ # Self-review quality system
|
||||||
|
├── controller/ # Input handling and command dispatch
|
||||||
|
│ ├── input.rs # Key event processing
|
||||||
|
│ └── command.rs # Slash command parser
|
||||||
|
├── dto/ # Data transfer objects
|
||||||
|
│ ├── chat/ # Message types
|
||||||
|
│ └── provider/ # AI provider request/response types
|
||||||
|
├── ipc/ # Inter-process communication
|
||||||
|
│ ├── protocol.rs # Message protocol definition
|
||||||
|
│ ├── server.rs # Unix socket server
|
||||||
|
│ ├── client.rs # Unix socket client
|
||||||
|
│ ├── conn.rs # Connection framing
|
||||||
|
│ ├── frame.rs # Frame encoding/decoding
|
||||||
|
│ ├── snapshot.rs # State snapshot
|
||||||
|
│ └── diff.rs # State diffing
|
||||||
|
├── model/ # Data models
|
||||||
|
│ ├── store.rs # File-based storage
|
||||||
|
│ ├── session.rs # Session management
|
||||||
|
│ ├── settings.rs # User settings
|
||||||
|
│ ├── app_config.rs # Provider configuration
|
||||||
|
│ ├── memory.rs # Persistent memory
|
||||||
|
│ ├── editlog.rs # Edit history log
|
||||||
|
│ ├── msglog/ # Message log persistence
|
||||||
|
│ ├── agent_def/ # Agent definitions
|
||||||
|
│ └── session_lock.rs # Session locking
|
||||||
|
├── security/ # Security installation utilities
|
||||||
|
│ └── install.rs # Sidecar binary management
|
||||||
|
├── service/ # External service integrations
|
||||||
|
│ ├── provider.rs # AI provider abstraction
|
||||||
|
│ └── oauth/ # OAuth authentication
|
||||||
|
├── tool/ # Tool implementations
|
||||||
|
│ ├── fs/ # read, write, edit, delete
|
||||||
|
│ ├── search.rs # grep, glob
|
||||||
|
│ ├── shell.rs # bash execution
|
||||||
|
│ ├── bash_tools.rs # bash_output, bash_kill
|
||||||
|
│ ├── git_operator.rs # git operations
|
||||||
|
│ ├── git_worktree.rs # git worktree management
|
||||||
|
│ ├── git_cred.rs # git credential management
|
||||||
|
│ ├── internet/ # fetch, download, search
|
||||||
|
│ ├── memory/ # remember, forget, recall
|
||||||
|
│ ├── plan.rs # plan_enter, plan_ready
|
||||||
|
│ ├── seqthink.rs # Sequential thinking
|
||||||
|
│ ├── workflow.rs # workflow_run, note_finding
|
||||||
|
│ ├── utility/ # cd, dir_list, dir_cache_update, pong, todowrite
|
||||||
|
│ └── shell_filter/ # Shell output filtering
|
||||||
|
├── view/ # TUI rendering
|
||||||
|
│ ├── chat.rs # Chat transcript panel
|
||||||
|
│ ├── markdown.rs # Markdown rendering
|
||||||
|
│ ├── status.rs # Status bar
|
||||||
|
│ ├── theme.rs # Color scheme
|
||||||
|
│ └── workflow.rs # Workflow visualization
|
||||||
|
└── resources.rs # Embedded resources (help text, prompts)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run in single-process mode (default)
|
||||||
|
zesdex
|
||||||
|
|
||||||
|
# Run as a background daemon
|
||||||
|
zesdex --daemon
|
||||||
|
|
||||||
|
# Attach to a running daemon session
|
||||||
|
zesdex --attach <session-id>
|
||||||
|
|
||||||
|
# Set log level
|
||||||
|
RUST_LOG=debug zesdex
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Bindings
|
||||||
|
|
||||||
|
| Binding | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| `Ctrl+Q` | Quit |
|
||||||
|
| `Ctrl+H` | Help overlay |
|
||||||
|
| `Ctrl+P` | Settings overlay |
|
||||||
|
| `Ctrl+A` | Cycle agent mode |
|
||||||
|
| `Ctrl+B` | Bash panel |
|
||||||
|
| `Ctrl+S` | Session hub |
|
||||||
|
| `Ctrl+T` | Task list |
|
||||||
|
| `Ctrl+W` | Workflow view |
|
||||||
|
| `Ctrl+K` | Key input mode |
|
||||||
|
| `Esc` | Cancel / back |
|
||||||
|
| `Tab` | Autocomplete |
|
||||||
|
| `↑/↓` | History / navigation |
|
||||||
|
|
||||||
|
### Slash Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/help` | Show help |
|
||||||
|
| `/clear` | Clear transcript |
|
||||||
|
| `/model` | Select AI model provider |
|
||||||
|
| `/exit` | Exit application |
|
||||||
|
| `/settings` | Open settings |
|
||||||
|
| `/session` | Session management |
|
||||||
|
|
||||||
|
## Agent Modes
|
||||||
|
|
||||||
|
- **Auto** — Full autonomy. The agent can read, write, edit files, run bash commands, and execute git operations without confirmation.
|
||||||
|
- **Normal** — Risky operations (write, delete, edit, bash, destructive git) require manual approval.
|
||||||
|
- **Plan** — Planning mode. The agent can analyze and propose changes but cannot execute mutations.
|
||||||
|
- **Yolo** — Unrestricted mode. Full autonomy with no confirmation prompts.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Zesdex includes multiple layers of security:
|
||||||
|
|
||||||
|
- **Catastrophic Guard** — Detects and blocks destructive operations (rm -rf, force push, credential exfiltration) across all modes.
|
||||||
|
- **Graduated Checks** — Content-aware pattern matching for common danger zones (API keys, passwords, git credentials).
|
||||||
|
- **Security Sidecar** — Optional Python daemon for deep vulnerability scanning.
|
||||||
|
- **Session Locking** — Prevents multiple processes from operating on the same session directory.
|
||||||
|
- **Workspace Isolation** — All file operations are validated against workspace roots.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Rust 2021 edition toolchain
|
||||||
|
- (Optional) Python 3 for the security sidecar
|
||||||
|
|
||||||
|
### Build from source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd zesdex
|
||||||
|
cargo build --release
|
||||||
|
./target/release/zesdex
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security sidecar (optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r security-sidecar/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configuration is stored in `~/.config/zesdex/` (or platform equivalent). Key files:
|
||||||
|
|
||||||
|
- `config.yaml` — Provider settings, default model, temperature, max tokens
|
||||||
|
- `app_config.yaml` — AI provider definitions (name, URL, auth type)
|
||||||
|
- `memory/` — Persistent lesson and reference storage
|
||||||
|
- `sessions/` — Per-session transcripts and activity logs
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
pub enum Verdict {
|
pub enum Verdict {
|
||||||
Allow,
|
Allow,
|
||||||
Block(String),
|
Block(String),
|
||||||
Escalate,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Harness;
|
pub struct Harness;
|
||||||
@@ -80,7 +79,6 @@ mod tests {
|
|||||||
"block" => Some(Verdict::Block(
|
"block" => Some(Verdict::Block(
|
||||||
v.get("reason").and_then(|r| r.as_str()).unwrap_or("blocked").to_string()
|
v.get("reason").and_then(|r| r.as_str()).unwrap_or("blocked").to_string()
|
||||||
)),
|
)),
|
||||||
"escalate" => Some(Verdict::Escalate),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -153,12 +151,6 @@ mod tests {
|
|||||||
assert_eq!(v, Some(Verdict::Block("dangerous operation".to_string())));
|
assert_eq!(v, Some(Verdict::Block("dangerous operation".to_string())));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_verdict_json_escalate() {
|
|
||||||
let v = parse_verdict(r#"{"verdict": "escalate"}"#);
|
|
||||||
assert_eq!(v, Some(Verdict::Escalate));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_verdict_text_allow() {
|
fn test_parse_verdict_text_allow() {
|
||||||
let v = parse_verdict("Verdict: Allow");
|
let v = parse_verdict("Verdict: Allow");
|
||||||
|
|||||||
@@ -8,16 +8,6 @@ pub struct EditorState {
|
|||||||
pub undo_stack: Vec<Vec<String>>,
|
pub undo_stack: Vec<Vec<String>>,
|
||||||
pub cursor_line: usize,
|
pub cursor_line: usize,
|
||||||
pub cursor_col: usize,
|
pub cursor_col: usize,
|
||||||
pub scroll_offset: usize,
|
|
||||||
pub active: bool,
|
|
||||||
pub mode: EditorMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum EditorMode {
|
|
||||||
Normal,
|
|
||||||
Insert,
|
|
||||||
Visual,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for EditorState {
|
impl Default for EditorState {
|
||||||
@@ -28,9 +18,6 @@ impl Default for EditorState {
|
|||||||
undo_stack: Vec::new(),
|
undo_stack: Vec::new(),
|
||||||
cursor_line: 0,
|
cursor_line: 0,
|
||||||
cursor_col: 0,
|
cursor_col: 0,
|
||||||
scroll_offset: 0,
|
|
||||||
active: false,
|
|
||||||
mode: EditorMode::Normal,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +28,6 @@ impl EditorState {
|
|||||||
EditorState {
|
EditorState {
|
||||||
path,
|
path,
|
||||||
content,
|
content,
|
||||||
active: true,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,29 +146,6 @@ impl EditorState {
|
|||||||
pub fn as_string(&self) -> String {
|
pub fn as_string(&self) -> String {
|
||||||
self.content.join("\n")
|
self.content.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close(&mut self) {
|
|
||||||
self.active = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_mode(&mut self) {
|
|
||||||
self.mode = match self.mode {
|
|
||||||
EditorMode::Normal => EditorMode::Insert,
|
|
||||||
EditorMode::Insert => EditorMode::Normal,
|
|
||||||
EditorMode::Visual => EditorMode::Normal,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AppEditorState {
|
|
||||||
pub editor: Option<EditorState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppEditorState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
AppEditorState { editor: None }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_editor_input(state: &mut AppStateRest, text: String) {
|
pub fn handle_editor_input(state: &mut AppStateRest, text: String) {
|
||||||
|
|||||||
@@ -1,17 +1,6 @@
|
|||||||
use crate::app::state::rest::AppStateRest;
|
use crate::app::state::rest::AppStateRest;
|
||||||
use crate::app::state::types::Overlay;
|
|
||||||
|
|
||||||
pub fn connect_mcp(state: &mut AppStateRest, server_name: &str) {
|
pub fn connect_mcp(state: &mut AppStateRest, server_name: &str) {
|
||||||
let _ = server_name;
|
let _ = server_name;
|
||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnect_mcp(state: &mut AppStateRest, server_name: &str) {
|
|
||||||
let _ = server_name;
|
|
||||||
state.dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_mcp_dismiss(state: &mut AppStateRest) {
|
|
||||||
state.misc.overlay = Overlay::None;
|
|
||||||
state.dirty = true;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
|||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
}
|
}
|
||||||
Action::Tick => {
|
Action::Tick => {
|
||||||
|
state.misc.tick_count = state.misc.tick_count.wrapping_add(1);
|
||||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||||
state.misc.drain_expired_toasts(now_ms);
|
state.misc.drain_expired_toasts(now_ms);
|
||||||
crate::app::review::maybe_run_staleness_sweep(state);
|
crate::app::review::maybe_run_staleness_sweep(state);
|
||||||
@@ -693,12 +694,6 @@ fn run_agent_turn(
|
|||||||
Err(e) => (e.to_string(), true, false),
|
Err(e) => (e.to_string(), true, false),
|
||||||
},
|
},
|
||||||
Verdict::Block(reason) => (format!("Blocked: {}", reason), true, false),
|
Verdict::Block(reason) => (format!("Blocked: {}", reason), true, false),
|
||||||
Verdict::Escalate => (
|
|
||||||
"Tool requires approval. Provide explicit approval."
|
|
||||||
.to_string(),
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_edit {
|
if is_edit {
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ pub struct MiscState {
|
|||||||
pub selected_index: usize,
|
pub selected_index: usize,
|
||||||
pub editor: Option<super::super::mode::editor::EditorState>,
|
pub editor: Option<super::super::mode::editor::EditorState>,
|
||||||
pub api_connected: bool,
|
pub api_connected: bool,
|
||||||
|
pub tick_count: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MiscState {
|
impl MiscState {
|
||||||
@@ -252,6 +253,7 @@ impl MiscState {
|
|||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
editor: None,
|
editor: None,
|
||||||
api_connected: false,
|
api_connected: false,
|
||||||
|
tick_count: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -83,10 +83,12 @@ pub fn draw_chat(frame: &mut Frame, area: Rect, state: &crate::app::state::rest:
|
|||||||
display_lines.push(Line::from(Span::raw("")));
|
display_lines.push(Line::from(Span::raw("")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.misc.thinking {
|
if state.turn_in_flight() {
|
||||||
|
let spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||||
|
let frame = spinner_frames[(state.misc.tick_count as usize / 2) % spinner_frames.len()];
|
||||||
display_lines.push(Line::from(vec![
|
display_lines.push(Line::from(vec![
|
||||||
Span::styled(" AI ", Style::default().fg(Theme::BG).bg(Theme::ROLE_ASSISTANT).add_modifier(Modifier::BOLD)),
|
Span::styled(" AI ", Style::default().fg(Theme::BG).bg(Theme::ROLE_ASSISTANT).add_modifier(Modifier::BOLD)),
|
||||||
Span::styled(" Thinking...", Style::default().fg(Theme::DIM)),
|
Span::styled(format!(" {} Generating...", frame), Style::default().fg(Theme::DIM)),
|
||||||
]));
|
]));
|
||||||
display_lines.push(Line::from(Span::raw("")));
|
display_lines.push(Line::from(Span::raw("")));
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -10,12 +10,14 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state:
|
|||||||
// PROG → turn is in flight
|
// PROG → turn is in flight
|
||||||
// READY → connected and ready
|
// READY → connected and ready
|
||||||
// NOAPI → disconnected
|
// NOAPI → disconnected
|
||||||
|
let spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||||
let (agent_status, conn_color) = if state.turn_in_flight() {
|
let (agent_status, conn_color) = if state.turn_in_flight() {
|
||||||
("PROG", Theme::MODE_YOLO)
|
let frame = spinner_frames[(state.misc.tick_count as usize / 2) % spinner_frames.len()];
|
||||||
|
(format!("{} PROG", frame), Theme::MODE_YOLO)
|
||||||
} else if state.misc.api_connected {
|
} else if state.misc.api_connected {
|
||||||
("READY", Theme::MODE_AUTO)
|
("READY".to_string(), Theme::MODE_AUTO)
|
||||||
} else {
|
} else {
|
||||||
("NOAPI", Theme::DIM)
|
("NOAPI".to_string(), Theme::DIM)
|
||||||
};
|
};
|
||||||
let status = Span::styled(
|
let status = Span::styled(
|
||||||
format!(" {} ", agent_status),
|
format!(" {} ", agent_status),
|
||||||
|
|||||||
Reference in New Issue
Block a user