diff --git a/docs/CODEMAPS/architecture.md b/docs/CODEMAPS/architecture.md deleted file mode 100644 index 615fd33..0000000 --- a/docs/CODEMAPS/architecture.md +++ /dev/null @@ -1,61 +0,0 @@ - - -# Architecture - -Zesdex is a single-process terminal AI coding agent with optional daemon/client split. - -## System Layout - -``` -┌──────────────────────────────────────────────────────┐ -│ main.rs │ -│ single-process ─┬── daemon ── Unix socket ── client │ -│ └── attach (TUI-only client) │ -└──────────────────────┬───────────────────────────────┘ - │ -┌──────────────────────▼───────────────────────────────┐ -│ Event Loop │ -│ ┌────────┐ ┌───────────┐ ┌──────┐ ┌────────┐ │ -│ │Input │──▶│ Actions │──▶│State │──▶│ TUI │ │ -│ │Handler │ │ (dispatch)│ │ │ │ Render │ │ -│ └────────┘ └─────┬─────┘ └──────┘ └────────┘ │ -│ │ │ -│ ┌──────▼──────┐ │ -│ │ LLM Stream │ │ -│ │ + Tool Exec │ │ -│ └──────┬──────┘ │ -│ ┌────┴────┐ │ -│ │ │ │ -│ ┌─────▼──┐ ┌───▼────┐ │ -│ │ Tools │ │Sub- │ │ -│ │ (37) │ │agents │ │ -│ └────────┘ └────────┘ │ -└───────────────────────────────────────────────────────┘ -``` - -## Data Flow - -``` -User keystroke → Controller (KeyEvent → Action) - → apply_action() mutates AppStateRest - → TUI redraws (ratatui Frame) - → On submit: LLM request → SSE stream → tool calls → tool results → more LLM - → Session persisted to disk (editlog, msglog, memory) -``` - -## Process Modes - -| Mode | Impl | Process | IPC | -|------|------|---------|-----| -| Single | `run_single_process()` | One | No | -| Daemon | `run_daemon()` | Server | `ipc/server.rs` | -| Attach | `run_attach()` | Client | `ipc/client.rs` | - -## Key Files - -| File | Lines | Role | -|------|-------|------| -| `src/main.rs` | 647 | Entry, TUI setup, daemon loop, attach loop | -| `src/app/runtime/actions/mod.rs` | 1815 | Action dispatch + LLM stream loop + tool execution | -| `src/controller/input.rs` | 365 | Key event → Action mapping | -| `src/view/mod.rs` | 975 | TUI rendering (ratatui) | diff --git a/docs/CODEMAPS/backend.md b/docs/CODEMAPS/backend.md deleted file mode 100644 index b59d912..0000000 --- a/docs/CODEMAPS/backend.md +++ /dev/null @@ -1,75 +0,0 @@ - - -# Backend / Service Layer - -## AI Provider - -`src/service/provider.rs` (310 lines) -- `LlmClient::new(api_key, model, base_url)` — constructs blocking reqwest client -- `chat_with_tools()` — non-streaming with tool definitions -- `chat_stream()` — SSE streaming, returns `SseParser` yielding `StreamEvent` -- Retry logic: up to 3 attempts on transient errors, exponential backoff - -## OAuth - -`src/service/oauth/manager.rs` (113 lines) + `loopback.rs` + `pkce.rs` -- PKCE flow: `CodeVerifier` → challenge → browser auth → loopback server → token exchange -- Configurable via `app_config.json` provider definitions (auth URL, token URL, scopes) - -## IPC / Daemon - -`src/ipc/` (7 files, ~350 lines total) -- Unix domain socket, length-prefixed JSON frames -- Daemon sends `DaemonFrame` (state payload, stream tokens, system notes) -- Clients send `ClientRequest` (key presses, resize, submit, scroll) -- State sync uses full-state push from daemon to client after each action - -## Workflow Engine - -`src/app/workflow/engine.rs` (648 lines) + `script.rs` -- Inline JS-style DSL executed by a lightweight runtime -- `agent()`, `parallel()`, `pipeline()`, `phase()`, `log()` — spawns sub-agents -- Max concurrency configurable via `workflow_max_concurrency` setting -- Hive-mind orchestrator in `hive_mind.rs`: Core Intelligence compiles a `CognitiveCyclePlan` per task — cycle count and nodes-per-cycle are decided fresh each time based on what the task actually needs - -## Sub-Agent System - -`src/app/subagent/` (6 files: `spawn.rs`, `engine.rs`, `context.rs`, `event.rs`, `division.rs`, `auto.rs`, ~450 lines total) -- `run_subagent()` — spawns independent agent with its own tool set and context -- Communicates via `mpsc` channel (tool calls, results, completion) -- Uses `LlmClient` (same as main agent) with tool-use API -- Auto-healing: on build/test failure, spawns auto-fix sub-agent -- Node access tiers (`division.rs`'s `tool_scope` module): `read`, `write`, `full` — granted per node by the Core Intelligence based on what its directive needs - -## MCP Client - -`src/app/mcp/manager.rs` (441+ lines) -- Stdio transport: spawns child process, JSON-RPC via stdin/stdout -- HTTP transport: streaming HTTP with JSON-RPC -- Dynamic tool list refresh and error recovery -- Persistent child handle for stdio (reuses connection across calls) - -## Self-Review - -`src/app/review/mod.rs` (495 lines) -- Post-tool execution quality check against learned lessons -- Invokes `run_subagent()` with reviewer prompt -- Staleness detection: skips review after N consecutive empty results -- Three review types: code quality, architecture, security - -## Background Bash - -`src/app/bgbash/` (2 files: `job.rs`, `control.rs`) -- `spawn_bash_job()` — runs `sh -c` in a thread, collects stdout line-by-line -- Channels: output via `mpsc`, PID via `mpsc` -- Killable via PID (SIGTERM) -- Output buffering capped at 10,000 lines to prevent memory issues - -## Gate Guard / Harness - -`src/app/harness.rs` (495 lines) -- `Harness::gate_tool_call()` — verdict-based tool gating (allow/block) -- Path traversal, credential read, and destructive command detection -- Pattern detection for stub code, denial language, and assumptions in write/edit content -- Reason validation for mutating tools (minimum 8 characters, rejects generic non-answers) -- Includes 8 unit tests for verdict parsing formats diff --git a/docs/CODEMAPS/data.md b/docs/CODEMAPS/data.md deleted file mode 100644 index 8590b0b..0000000 --- a/docs/CODEMAPS/data.md +++ /dev/null @@ -1,51 +0,0 @@ - - -# Data / Persistence Layer - -## Storage Overview - -Base directory: `~/.config/zesdex/` (via `dirs::data_dir()`) - -``` -~/.config/zesdex/ -├── settings.json # User preferences (provider, model, tokens) -├── app_config.json # Provider definitions (API base, auth, models) -├── agents/ # Global agent definitions -│ └── *.json -├── memory/ # Persistent lesson/reference store -│ └── *.md # Markdown with YAML frontmatter -├── sessions/ # Per-session data -│ └── / -│ ├── edits.jsonl # Edit history (JSONL, append-only) -│ ├── msglog.db # SQLite message log -│ ├── transcript.json # Chat transcript -│ ├── session.json # Session metadata -│ ├── agents.json # Session-local agent defs -│ └── snapshot.dat # State snapshot (daemon mode) -├── run/ # Unix domain sockets -│ └── zesdex-*.sock -└── store.json # Legacy session index -``` - -## Key Files - -| File | Lines | Role | -|------|-------|------| -| `src/model/store.rs` | ~50 | File-system storage (ensure_dirs, base_dir resolution) | -| `src/model/settings.rs` | ~60 | `Settings` — load/save JSON, API keys map | -| `src/model/app_config.rs` | ~80 | `AppConfig` — provider definitions, model roles, auth | -| `src/model/memory.rs` | 440 | Memory CRUD — markdown files with frontmatter | -| `src/model/editlog.rs` | 161 | Edit log — append-only JSONL (not JSON array) | -| `src/model/msglog/` | 4 files | SQLite-backed message log (schema, query, blobs) | -| `src/model/session.rs` | ~60 | Session CRUD, listing, archival | -| `src/model/session_lock.rs` | ~50 | flock-based session lock | -| `src/model/agent_def/` | 3 files | Agent definitions (builtin, global, session-local) | - -## Key Patterns - -- **No ORM** — raw JSON files + SQLite via rusqlite -- **settings.json** — loaded at startup, saved on quit / mode switches -- **Memory format** — Markdown files with YAML frontmatter (`---\nname: ...\ndescription: ...\n---\ncontent`) -- **Edit log** — append-only, stores `(file, old, new, timestamp, tool)` -- **Session locking** — flock-based, prevents concurrent access to same session dir -- **Message log** — SQLite with attached blobs for tool arguments/outputs diff --git a/docs/CODEMAPS/dependencies.md b/docs/CODEMAPS/dependencies.md deleted file mode 100644 index cf38157..0000000 --- a/docs/CODEMAPS/dependencies.md +++ /dev/null @@ -1,41 +0,0 @@ - - -# Dependencies - -## Rust Crates (Cargo.toml) - -| Crate | Version | Purpose | -|-------|---------|---------| -| ratatui | 0.30 | TUI framework (tui-rs successor) | -| crossterm | 0.29 | Terminal manipulation (raw mode, alt screen) | -| tokio | 1 | Async runtime (daemon, OAuth loopback) | -| reqwest | 0.12 | HTTP client (blocking + streaming, vendored native-tls) | -| serde / serde_json | 1 | JSON serialization (state, DTOs, IPC, config) | -| serde_yaml_ng | 0.10 | YAML frontmatter parsing (memory files) | -| anyhow | 1 | Error handling (no custom error types) | -| tracing / tracing-subscriber | 0.1/0.3 | Structured logging → file | -| rusqlite | 0.40 | SQLite (bundled, for message log) | -| pulldown-cmark | 0.13 | Markdown → HTML (chat rendering) | -| syntect | 5 | Syntax highlighting (code blocks in chat) | -| sha2 | 0.10 | SHA-256 for PKCE challenge | -| base64 | 0.22 | URL-safe base64 for PKCE | -| libc | 0.2 | daemon PID file locking | -| rmcp | 2.2 | MCP client (stdio + HTTP transports) | -| uuid | 1 | Session IDs, job IDs | -| chrono | 0.4 | Timestamps (ISO 8601, millis) | -| dirs | 6 | Platform data directories | -| dom_smoothie | 0.18 | HTML → plain text (web scraping) | -| scraper | 0.27 | HTML parsing (web scraping) | -| ignore | 0.4 | .gitignore-aware file walking (glob tool) | -| regex / globset | 0.4 | Pattern matching (grep/glob tools) | -| url / percent-encoding | 2 | URL parsing + encoding (OAuth) | - -## External Services - -| Service | Integration | Notes | -|---------|-------------|-------| -| **LLM providers** | HTTP API (OpenAI-compatible) | Configurable via app_config.json | -| **MCP servers** | stdio or HTTP | Model Context Protocol | -| **git** | CLI (spawns `git`) | Via git_operator/git_worktree/git_cred tools | -| **sh** | CLI (spawns `sh`) | Via bash tool | -| **webbrowser** | opens URL | OAuth browser flow | diff --git a/docs/CODEMAPS/frontend.md b/docs/CODEMAPS/frontend.md deleted file mode 100644 index 24ba518..0000000 --- a/docs/CODEMAPS/frontend.md +++ /dev/null @@ -1,64 +0,0 @@ - - -# Frontend / TUI - -## Render Pipeline - -``` -ratatui::Terminal::draw(|frame|) - → view::draw(frame, AppStateRest) - → render_main_panel / render_overlay (based on overlay state) - → render_input_bar - → draw_status_bar - → render_toasts (top-right floating notifications) -``` - -## Layout - -``` -┌──────────────────────────────────────────────┐ -│ Chat Panel (main_area: Min 3) │ -│ ┌────────────────────────────────────────┐ │ -│ │ User: Hello │ │ -│ │ Agent: Hi there, how can I help? │ │ -│ │ │ │ -│ │ Toast notifications (top-right) │ │ -│ └────────────────────────────────────────┘ │ -├──────────────────────────────────────────────┤ -│ Input Bar (3 lines) │ -│ > Some text... │ -├──────────────────────────────────────────────┤ -│ Status Bar (1 line) │ -│ ┌ Provider │ Model │ Tokens │ Mode │ Quit ─┤ -└──────────────────────────────────────────────┘ -``` - -## Key Files - -| File | Lines | Purpose | -|------|-------|---------| -| `src/view/mod.rs` | 623 | Frame draw, overlays (16 types), input bar, toasts | -| `src/view/chat.rs` | 155 | Chat transcript rendering with markdown | -| `src/view/markdown.rs` | 144 | Markdown → ratatui `Span` rendering (pulldown-cmark + syntect) | -| `src/view/status.rs` | ~50 | Status bar with provider/model/tokens | -| `src/view/workflow.rs` | 88 | Workflow progress visualization | -| `src/view/theme.rs` | 23 | Color palette (23 named colors) | -| `src/controller/input.rs` | 281 | Key event → Action mapping | - -## Overlays (16 types) - -`Overlay::Help | Settings | Bash | QuitConfirm | Workflow | KeyInput | Editor | Effort | Mcp | Todo | Rewind | Learning | Usage | Loading | ModelSelector | ClearConfirm` - -Each overlay renders a centered popup via `render_overlay()`. - -## State Mutations - -State is mutated in-place from two locations: -- `src/controller/input.rs` — keyboard shortcuts and overlay interactions -- `src/app/runtime/actions/mod.rs` — `apply_action()` reducer for all programmatic actions - -## Toast Notifications - -`render_toasts()` — floating stack at top-right, color-coded by severity: -- Info: blue, Success: green, Warning: yellow, Error: red, Lesson: cyan -- Max 4 visible, auto-expire after 5s lifetime diff --git a/src/view/markdown.rs b/src/view/markdown.rs index 441975d..1d5a0c2 100644 --- a/src/view/markdown.rs +++ b/src/view/markdown.rs @@ -37,7 +37,6 @@ pub fn render_markdown(text: &str, width: u16) -> Vec> { let mut in_heading = false; let mut heading_level = 0; - let mut in_table = false; let mut in_table_cell = false; let mut table_rows: Vec>>> = Vec::new(); let mut current_row: Vec>> = Vec::new(); @@ -99,7 +98,6 @@ pub fn render_markdown(text: &str, width: u16) -> Vec> { )); } pulldown_cmark::Tag::Table(_) => { - in_table = true; table_rows.clear(); } pulldown_cmark::Tag::TableHead | pulldown_cmark::Tag::TableRow => { @@ -141,38 +139,68 @@ pub fn render_markdown(text: &str, width: u16) -> Vec> { table_rows.push(std::mem::take(&mut current_row)); } pulldown_cmark::TagEnd::Table => { - in_table = false; - - let mut col_widths = Vec::new(); + let cols_count = table_rows.first().map(|r| r.len()).unwrap_or(0); + if cols_count == 0 { + continue; + } + + let mut col_widths = vec![0; cols_count]; for row in &table_rows { for (i, cell) in row.iter().enumerate() { - let width: usize = cell.iter().map(|s| s.content.chars().count()).sum(); - if i >= col_widths.len() { - col_widths.push(width); - } else if width > col_widths[i] { - col_widths[i] = width; + if i < cols_count { + let cell_width: usize = cell.iter().map(|s| s.content.chars().count()).sum(); + if cell_width > col_widths[i] { + col_widths[i] = cell_width; + } } } } + + let effective_width = if width > 0 { (width as usize).saturating_sub(2) } else { 0 }; + let border_overhead = cols_count * 3 + 4; + let available_width = effective_width.saturating_sub(border_overhead); + let mut total_width: usize = col_widths.iter().sum(); + + if width > 0 && total_width > available_width && available_width > 0 { + while total_width > available_width { + let max_idx = col_widths.iter().enumerate().max_by_key(|&(_, &w)| w).map(|(i, _)| i).unwrap(); + if col_widths[max_idx] <= 3 { break; } + col_widths[max_idx] -= 1; + total_width -= 1; + } + } spans.push(Span::raw("\n")); for (r, row) in table_rows.iter().enumerate() { - spans.push(Span::styled(" | ", Style::default().fg(Theme::BORDER))); + let mut cell_lines = Vec::new(); for (i, cell) in row.iter().enumerate() { - let width: usize = cell.iter().map(|s| s.content.chars().count()).sum(); - let pad = col_widths.get(i).copied().unwrap_or(0).saturating_sub(width); - for span in cell { - spans.push(span.clone()); + if i < cols_count { + cell_lines.push(wrap_spans_to_lines(cell, col_widths[i])); } - spans.push(Span::raw(" ".repeat(pad))); - spans.push(Span::styled(" | ", Style::default().fg(Theme::BORDER))); } - spans.push(Span::raw("\n")); + + let max_height = cell_lines.iter().map(|cl| cl.len()).max().unwrap_or(1); + + for y in 0..max_height { + spans.push(Span::styled(" | ", Style::default().fg(Theme::BORDER))); + for (i, cl) in cell_lines.iter().enumerate() { + let line_spans = if y < cl.len() { &cl[y] } else { [].as_slice() }; + let mut line_width = 0; + for span in line_spans { + line_width += span.content.chars().count(); + spans.push(span.clone()); + } + let pad = col_widths[i].saturating_sub(line_width); + spans.push(Span::raw(" ".repeat(pad))); + spans.push(Span::styled(" | ", Style::default().fg(Theme::BORDER))); + } + spans.push(Span::raw("\n")); + } if r == 0 { spans.push(Span::styled(" |", Style::default().fg(Theme::BORDER))); - for width in &col_widths { - spans.push(Span::styled(format!("{}-|", "-".repeat(*width + 2)), Style::default().fg(Theme::BORDER))); + for w in &col_widths { + spans.push(Span::styled(format!("{}-|", "-".repeat(*w + 2)), Style::default().fg(Theme::BORDER))); } spans.push(Span::raw("\n")); } @@ -291,3 +319,61 @@ pub fn render_markdown(text: &str, width: u16) -> Vec> { spans } + +fn wrap_spans_to_lines(spans: &[Span<'static>], target_width: usize) -> Vec>> { + let mut lines = Vec::new(); + let mut current_line = Vec::new(); + let mut line_len = 0; + + for span in spans { + let style = span.style; + let text = span.content.as_ref(); + let mut current_word = String::new(); + let mut tokens = Vec::new(); + + for c in text.chars() { + if c == ' ' { + if !current_word.is_empty() { tokens.push(current_word.clone()); current_word.clear(); } + tokens.push(" ".to_string()); + } else { + current_word.push(c); + } + } + if !current_word.is_empty() { tokens.push(current_word); } + + for token in tokens { + if token == " " { + if line_len > 0 && line_len < target_width { + current_line.push(Span::styled(" ", style)); + line_len += 1; + } + } else { + let token_len = token.chars().count(); + if line_len + token_len > target_width && line_len > 0 { + lines.push(std::mem::take(&mut current_line)); + line_len = 0; + } + if token_len > target_width { + for c in token.chars() { + if target_width > 0 && line_len >= target_width { + lines.push(std::mem::take(&mut current_line)); + line_len = 0; + } + current_line.push(Span::styled(c.to_string(), style)); + line_len += 1; + } + } else { + current_line.push(Span::styled(token, style)); + line_len += token_len; + } + } + } + } + if !current_line.is_empty() { + lines.push(current_line); + } + if lines.is_empty() { + lines.push(vec![]); + } + lines +} diff --git a/test_mouse.rs b/test_mouse.rs deleted file mode 100644 index 0275cfb..0000000 --- a/test_mouse.rs +++ /dev/null @@ -1,2 +0,0 @@ -use crossterm::event::{EnableMouseCapture, EnableMouseScrollCapture}; -fn main() {}