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
+2
View File
@@ -0,0 +1,2 @@
target/
.env
Generated
+4193
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
[package]
name = "zesdex"
version = "0.1.0"
edition = "2021"
authors = ["asepharyana <superaseph@gmail.com>"]
[dependencies]
ratatui = "0.30.2"
crossterm = "0.28"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "net", "io-util", "signal"] }
reqwest = { version = "0.12", features = ["json", "stream", "blocking", "native-tls-vendored"] }
dom_smoothie = "0.18.0"
fast_html2md = "0.0.62"
scraper = "0.27.0"
url = "2"
percent-encoding = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml_ng = "0.9"
anyhow = "1"
include_dir = "0.7"
uuid = { version = "1", features = ["v4", "v5"] }
dirs = "5"
futures-util = "0.3"
pulldown-cmark = { version = "0.13", default-features = false }
syntect = { version = "5", default-features = false, features = ["default-fancy"] }
rusqlite = { version = "0.32", features = ["bundled"] }
ignore = "0.4"
regex = "1"
globset = "0.4"
infer = "0.16"
base64 = "0.22"
sha2 = "0.10"
libc = "0.2"
rmcp = { version = "1.8", default-features = false, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest", "macros"] }
chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[[bin]]
name = "zesdex"
path = "src/main.rs"
+38
View File
@@ -0,0 +1,38 @@
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::OnceLock;
use super::job::BashJob;
pub(crate) fn bash_jobs_map() -> &'static Mutex<HashMap<String, BashJob>> {
static JOBS: OnceLock<Mutex<HashMap<String, BashJob>>> = OnceLock::new();
JOBS.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn bash_output(id: &str) -> Option<Vec<String>> {
let mut map = bash_jobs_map().lock().ok()?;
let job = map.get_mut(id)?;
let mut lines = Vec::new();
while let Some(line) = job.try_read_line() {
lines.push(line);
}
if lines.is_empty() { None } else { Some(lines) }
}
pub fn bash_kill(id: &str) -> anyhow::Result<()> {
let mut map = bash_jobs_map().lock().map_err(|e| anyhow::anyhow!("lock error: {}", e))?;
let job = map.remove(id);
if job.is_some() {
Ok(())
} else {
anyhow::bail!("bash job '{}' not found", id)
}
}
pub fn register_bash_job(job: BashJob) -> String {
let id = job.id.clone();
if let Ok(mut map) = bash_jobs_map().lock() {
map.insert(id.clone(), job);
}
id
}
+75
View File
@@ -0,0 +1,75 @@
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::thread;
use std::io::BufRead;
pub struct BashJob {
pub id: String,
pub command: String,
pub started_at: i64,
pub output_rx: mpsc::Receiver<String>,
pub exit_code: Option<i32>,
pub handle: Option<thread::JoinHandle<()>>,
}
pub fn spawn_bash_job(command: String) -> BashJob {
let id = uuid::Uuid::new_v4().to_string();
let started_at = chrono::Utc::now().timestamp_millis();
let (output_tx, output_rx) = mpsc::channel::<String>();
let cmd = command.clone();
let handle = thread::spawn(move || {
let child = Command::new("sh")
.arg("-c")
.arg(&cmd)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn();
match child {
Ok(mut child) => {
if let Some(stdout) = child.stdout.take() {
let reader = std::io::BufReader::new(stdout);
for line in reader.lines().map_while(Result::ok) {
let _ = output_tx.send(line);
}
}
let status = child.wait();
let code = status.ok().and_then(|s| s.code());
let _ = output_tx.send(format!("__exit:{}", code.unwrap_or(-1)));
}
Err(e) => {
let _ = output_tx.send(format!("__error:{}", e));
let _ = output_tx.send("__exit:-1".to_string());
}
}
});
BashJob {
id,
command,
started_at,
output_rx,
exit_code: None,
handle: Some(handle),
}
}
impl BashJob {
pub fn try_read_line(&mut self) -> Option<String> {
match self.output_rx.try_recv() {
Ok(line) => {
if line.starts_with("__exit:") {
self.exit_code = line.strip_prefix("__exit:").and_then(|s| s.parse().ok());
None
} else {
Some(line)
}
}
Err(_) => None,
}
}
pub fn is_running(&self) -> bool {
self.exit_code.is_none()
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod control;
pub mod job;
+152
View File
@@ -0,0 +1,152 @@
use std::path::Path;
pub struct CatastrophicGuard;
impl CatastrophicGuard {
pub fn check_git_operation(cmd: &str) -> Result<(), String> {
let patterns = [
"force-push",
"reset --hard",
"clean -f",
"clean -d",
"clean -x",
"branch -D",
"branch --delete --force",
"checkout --force",
"switch -f",
"restore --force",
"stash drop",
"stash clear",
"tag -d",
"tag --delete",
"update-ref -d",
"filter-branch",
"gc --prune",
"gc --aggressive",
"push --delete",
"push --force",
"push origin :",
"push +refs",
];
let cmd_lower = cmd.to_lowercase();
for pattern in patterns {
if cmd_lower.contains(pattern) {
return Err(format!("catastrophic git operation blocked: '{}'", pattern));
}
}
Ok(())
}
pub fn check_shell_command(cmd: &str) -> Result<(), String> {
let dangerous = [
":(){ :|:& };:",
"> /dev/sda",
"dd if=",
"mkfs.",
"format ",
"fdisk",
"parted",
"mkswap",
"swapoff",
"shutdown",
"reboot",
"poweroff",
"init 0",
"init 6",
"halt",
"> /dev/mem",
"> /dev/kmem",
"chmod 000",
"chown -R 0:0",
];
let cmd_lower = cmd.to_lowercase();
for pattern in dangerous {
if cmd_lower.contains(pattern) {
return Err(format!("catastrophic shell command blocked: '{}'", pattern));
}
}
Ok(())
}
pub fn check_delete_path(path: &Path, _workspace_roots: &[&Path]) -> Result<(), String> {
let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
if canon == *"/"
|| canon == *"/home"
|| canon == *"/root"
{
return Err("catastrophic delete blocked: system directory".to_string());
}
let in_workspace = _workspace_roots.iter().any(|w| {
let wc = w.canonicalize().unwrap_or_else(|_| w.to_path_buf());
canon.starts_with(&wc)
});
if !in_workspace {
return Err("catastrophic delete blocked: outside all workspace roots".to_string());
}
Ok(())
}
pub fn check_credential_pattern(cmd: &str) -> Result<(), String> {
let patterns = [
"cat ~/.ssh",
"cat /home/",
".ssh/id_rsa",
".ssh/id_ed25519",
".ssh/authorized_keys",
".git-credentials",
".netrc",
"aws/credentials",
"gcloud/credentials",
".config/gcloud",
".config/gh",
"token=",
"secret=",
"api_key=",
"api-key=",
"password=",
];
let cmd_lower = cmd.to_lowercase();
for pattern in patterns {
if cmd_lower.contains(pattern) {
return Err(format!("credential read blocked: '{}'", pattern));
}
}
Ok(())
}
pub fn check_download_path(path: &Path) -> Result<(), String> {
let name = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
let sensitive = [
"id_rsa",
"id_ed25519",
"authorized_keys",
"known_hosts",
".netrc",
".git-credentials",
"credentials.json",
"service-account",
"secret",
"key.pem",
"key.p8",
"id_ecdsa",
"id_dsa",
"config",
];
let name_lower = name.to_lowercase();
for s in &sensitive {
if name_lower.contains(s) {
return Err(format!("sensitive download blocked: '{}'", s));
}
}
Ok(())
}
pub fn check_all(cmd: &str, _workspace_roots: &[&Path]) -> Result<(), String> {
Self::check_shell_command(cmd)?;
Self::check_git_operation(cmd)?;
Self::check_credential_pattern(cmd)?;
Ok(())
}
}
+66
View File
@@ -0,0 +1,66 @@
#[derive(Debug, Clone, PartialEq)]
pub enum Verdict {
Allow,
Block(String),
Escalate,
}
impl Verdict {
pub fn is_allowed(&self) -> bool {
matches!(self, Verdict::Allow)
}
}
pub struct Harness;
impl Harness {
pub fn classify(_cmd: &str, mode: &super::state::types::AgentMode) -> Verdict {
if mode.auto_approve() {
return Verdict::Allow;
}
Verdict::Allow
}
}
pub fn parse_verdict(text: &str) -> Option<Verdict> {
let trimmed = text.trim();
if let Ok(v) = serde_json::from_str::<serde_json::Value>(trimmed) {
if let Some(verdict) = v.get("verdict").and_then(|v| v.as_str()) {
return match verdict.to_lowercase().as_str() {
"allow" => Some(Verdict::Allow),
"block" => Some(Verdict::Block(
v.get("reason").and_then(|r| r.as_str()).unwrap_or("blocked").to_string()
)),
"escalate" => Some(Verdict::Escalate),
_ => None,
};
}
}
for line in trimmed.lines() {
let l = line.trim().to_lowercase();
if l.starts_with("verdict: allow") {
return Some(Verdict::Allow);
}
if l.starts_with("verdict: block") {
let reason = line.split_once(':').map(|x| x.1).unwrap_or("blocked").trim().to_string();
return Some(Verdict::Block(reason));
}
}
if trimmed.to_lowercase().contains("allow") {
return Some(Verdict::Allow);
}
if trimmed.to_lowercase().contains("block") {
return Some(Verdict::Block("blocked by classifier".to_string()));
}
None
}
pub fn classify(_cmd: &str, mode: &super::state::types::AgentMode) -> Verdict {
Harness::classify(_cmd, mode)
}
impl Default for Harness {
fn default() -> Self {
Harness
}
}
+78
View File
@@ -0,0 +1,78 @@
use serde_json::Value;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum McpTransport {
Stdio {
command: String,
args: Vec<String>,
},
StreamableHttp {
url: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolInfo {
pub name: String,
pub description: String,
pub input_schema: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServer {
pub name: String,
pub transport: McpTransport,
pub tools: Vec<McpToolInfo>,
}
impl McpServer {
pub fn new(name: String, transport: McpTransport) -> Self {
McpServer {
name,
transport,
tools: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct McpManager {
pub servers: Vec<McpServer>,
pub running: bool,
}
impl McpManager {
pub fn new() -> Self {
McpManager {
servers: Vec::new(),
running: false,
}
}
pub fn add_server(&mut self, server: McpServer) {
self.servers.push(server);
}
pub fn remove_server(&mut self, name: &str) {
self.servers.retain(|s| s.name != name);
}
pub fn get_server(&self, name: &str) -> Option<&McpServer> {
self.servers.iter().find(|s| s.name == name)
}
pub fn all_tools(&self) -> Vec<&McpToolInfo> {
self.servers.iter().flat_map(|s| s.tools.iter()).collect()
}
pub fn start_all(&mut self) -> anyhow::Result<()> {
self.running = true;
Ok(())
}
pub fn stop_all(&mut self) -> anyhow::Result<()> {
self.running = false;
Ok(())
}
}
+1
View File
@@ -0,0 +1 @@
pub mod manager;
+11
View File
@@ -0,0 +1,11 @@
pub mod catastrophic;
pub mod harness;
pub mod mode;
pub mod runtime;
pub mod state;
pub mod workflow;
pub mod subagent;
pub mod review;
pub mod bgbash;
pub mod mcp;
pub mod sec;
View File
View File
View File
View File
View File
View File
View File
View File
+73
View File
@@ -0,0 +1,73 @@
use serde::{Deserialize, Serialize};
pub mod agents;
pub mod bash;
pub mod editor;
pub mod effort;
pub mod help;
pub mod key_input;
pub mod loading;
pub mod mcp;
pub mod onboard;
pub mod onboard_provider;
pub mod picker;
pub mod quit_confirm;
pub mod rewind;
pub mod security;
pub mod session_hub;
pub mod settings;
pub mod todo;
pub mod workflow;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ModeKind {
Chat,
Agents,
Bash,
Workflow,
Help,
Settings,
SessionHub,
QuitConfirm,
Onboard,
OnboardProvider,
Picker,
KeyInput,
Editor,
Effort,
Mcp,
Security,
Todo,
Rewind,
Loading,
}
impl ModeKind {
pub fn name(&self) -> &'static str {
match self {
ModeKind::Chat => "Chat",
ModeKind::Agents => "Agents",
ModeKind::Bash => "Bash",
ModeKind::Workflow => "Workflow",
ModeKind::Help => "Help",
ModeKind::Settings => "Settings",
ModeKind::SessionHub => "SessionHub",
ModeKind::QuitConfirm => "QuitConfirm",
ModeKind::Onboard => "Onboard",
ModeKind::OnboardProvider => "OnboardProvider",
ModeKind::Picker => "Picker",
ModeKind::KeyInput => "KeyInput",
ModeKind::Editor => "Editor",
ModeKind::Effort => "Effort",
ModeKind::Mcp => "MCP",
ModeKind::Security => "Security",
ModeKind::Todo => "Todo",
ModeKind::Rewind => "Rewind",
ModeKind::Loading => "Loading",
}
}
pub fn is_overlay(self) -> bool {
!matches!(self, ModeKind::Chat | ModeKind::Agents | ModeKind::Bash | ModeKind::Workflow)
}
}
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
+172
View File
@@ -0,0 +1,172 @@
use std::collections::HashMap;
use crate::app::state::rest::AppStateRest;
use crate::app::state::types::{AgentMode, Origin, Toast, ToastKind};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Confidence {
Human,
Verified,
Unverified,
Auto,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum LessonLifecycle {
New,
Active,
Stale,
Contradicted,
Superseded,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum LessonScope {
Project,
Global,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Provenance {
pub session_turn: String,
pub session_id: String,
pub reviewer: Origin,
}
#[derive(Debug, Clone)]
pub struct Lesson {
pub name: String,
pub content: String,
pub confidence: Confidence,
pub outcome: Option<String>,
pub lifecycle: LessonLifecycle,
pub scope: LessonScope,
pub contradiction_with: Option<String>,
pub provenance: Provenance,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ViolationEscalation {
None,
Warning,
Escalate,
Block,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ShadowStatus {
Trial,
Graduated,
Rejected,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShadowCheck {
pub pattern: String,
pub trial_window: u32,
pub trial_count: u32,
pub trial_passed: u32,
pub status: ShadowStatus,
}
pub struct ReviewSystem {
pub pending: bool,
pub queue_capacity: usize,
pub repeated_violations: HashMap<String, u32>,
pub shadow_violations: Vec<ShadowCheck>,
pub violation_window: u32,
}
impl ReviewSystem {
pub fn new() -> Self {
ReviewSystem {
pending: false,
queue_capacity: 1,
repeated_violations: HashMap::new(),
shadow_violations: Vec::new(),
violation_window: 10,
}
}
pub fn reset(&mut self) {
self.pending = false;
}
pub fn check_escalation(&self, pattern: &str) -> ViolationEscalation {
let count = self.repeated_violations.get(pattern).copied().unwrap_or(0);
match count {
0 | 1 => ViolationEscalation::None,
2 => ViolationEscalation::Warning,
3 | 4 => ViolationEscalation::Escalate,
_ => ViolationEscalation::Block,
}
}
pub fn increment_violation(&mut self, pattern: &str) -> ViolationEscalation {
let entry = self.repeated_violations.entry(pattern.to_string()).or_insert(0);
*entry += 1;
self.check_escalation(pattern)
}
pub fn should_skip_review(consecutive_empty: u32) -> bool {
consecutive_empty >= 3
}
}
pub fn create_pending_lesson(name: &str, content: &str, provenance: Provenance) -> Lesson {
Lesson {
name: name.to_string(),
content: content.to_string(),
confidence: Confidence::Unverified,
outcome: None,
lifecycle: LessonLifecycle::New,
scope: LessonScope::Project,
contradiction_with: None,
provenance,
}
}
pub fn apply_lesson_calibration(state: &mut AppStateRest, lesson: &Lesson) {
if lesson.confidence != Confidence::Unverified {
return;
}
if state.mode.auto_approve() {
return;
}
state.push_toast(Toast::new(
ToastKind::Lesson,
format!("Lesson '{}' is pending review. Keep or discard?", lesson.name),
));
}
pub fn should_trigger_review(state: &AppStateRest, origin: Origin) -> bool {
if state.mode == AgentMode::Plan {
return false;
}
if origin != Origin::Main {
return false;
}
let runtime = match &state.session_runtime {
Some(r) => r,
None => return false,
};
if !state.settings.review_enabled {
return false;
}
if runtime.edit_count > 0 && runtime.edit_count % 5 == 0 {
return true;
}
if runtime.consecutive_empty_reviews >= state.settings.adaptive_review_max_skip {
return true;
}
false
}
pub fn trigger_review(state: &mut AppStateRest) -> anyhow::Result<()> {
let _origin = Origin::Reviewer;
state.push_transcript(crate::app::state::rest::ChatMessageDisplay::new(
crate::dto::chat::message::Role::System,
"review triggered".to_string(),
));
Ok(())
}
+263
View File
@@ -0,0 +1,263 @@
use crate::app::mode::ModeKind;
use crate::app::state::rest::AppStateRest;
use crate::app::state::types::Overlay;
use crate::dto::chat::message::ChatMessage;
#[derive(Debug, Clone)]
#[expect(dead_code)]
pub enum Action {
Quit,
ForceQuit,
SwitchMode(ModeKind),
SubmitInput(String),
InsertChar(char),
DeleteChar,
DeleteCharRight,
CursorLeft,
CursorRight,
HistoryUp,
HistoryDown,
ScrollUp,
ScrollDown,
OpenOverlay(Overlay),
CloseOverlay,
ToggleYoloArm,
ToolResult {
tool_call_id: String,
output: String,
is_error: bool,
},
StreamToken(String),
StreamDone,
StreamError(String),
SystemNote {
kind: String,
message: String,
},
RunCommand(String),
QuitConfirm,
Resize(u16, u16),
Tick,
RecordUsage {
tokens_in: u64,
tokens_out: u64,
duration_ms: u64,
},
RecordReviewTokens {
tokens: u64,
},
}
pub fn apply_action(state: &mut AppStateRest, action: Action) {
match action {
Action::Quit => {
state.quit = true;
}
Action::ForceQuit => {
state.quit = true;
}
Action::SwitchMode(mode) => {
state.misc.overlay = match mode {
ModeKind::Chat
| ModeKind::Agents
| ModeKind::Bash
| ModeKind::Workflow => Overlay::None,
ModeKind::Help => Overlay::Help,
ModeKind::Settings => Overlay::Settings,
ModeKind::SessionHub => Overlay::SessionHub,
ModeKind::QuitConfirm => Overlay::QuitConfirm,
ModeKind::Onboard => Overlay::Onboard,
ModeKind::OnboardProvider => Overlay::OnboardProvider,
ModeKind::Picker => Overlay::Picker,
ModeKind::KeyInput => Overlay::KeyInput,
ModeKind::Editor => Overlay::Editor,
ModeKind::Effort => Overlay::Effort,
ModeKind::Mcp => Overlay::Mcp,
ModeKind::Security => Overlay::Security,
ModeKind::Todo => Overlay::Todo,
ModeKind::Rewind => Overlay::Rewind,
ModeKind::Loading => Overlay::Loading,
};
state.dirty = true;
}
Action::SubmitInput(text) => {
if let Some(ref mut rt) = state.session_runtime {
rt.push_message(ChatMessage::user(text.clone()));
let api_key = state.settings.api_key.clone();
let model = state.settings.model.clone();
let msgs = rt.messages.clone();
let pending = state.pending_api_response.clone();
if let Some(key) = api_key {
if !key.is_empty() {
std::thread::spawn(move || {
let client = crate::service::openrouter::OpenRouterClient::new(key, model);
match client.chat(&msgs) {
Ok(response) => {
if let Ok(mut guard) = pending.lock() {
*guard = Some(response);
}
}
Err(e) => {
if let Ok(mut guard) = pending.lock() {
*guard = Some(format!("Error: {}", e));
}
}
}
});
}
}
}
state.dirty = true;
}
Action::InsertChar(c) => {
state.input.insert(c);
state.dirty = true;
}
Action::DeleteChar => {
state.input.delete_left();
state.dirty = true;
}
Action::DeleteCharRight => {
state.input.delete_right();
state.dirty = true;
}
Action::CursorLeft => {
state.input.char_left();
}
Action::CursorRight => {
state.input.char_right();
}
Action::HistoryUp => {
state.input.history_up();
state.dirty = true;
}
Action::HistoryDown => {
state.input.history_down();
state.dirty = true;
}
Action::ScrollUp => {
let total = state.transcript_cache.messages.len();
state.scroll.scroll_up();
state.scroll.scroll_down(total);
state.dirty = true;
}
Action::ScrollDown => {
let total = state.transcript_cache.messages.len();
state.scroll.scroll_down(total);
state.dirty = true;
}
Action::OpenOverlay(overlay) => {
state.misc.overlay = overlay;
state.dirty = true;
}
Action::CloseOverlay => {
state.misc.overlay = Overlay::None;
state.dirty = true;
}
Action::ToggleYoloArm => {
state.misc.yolo_armed = !state.misc.yolo_armed;
state.dirty = true;
}
Action::ToolResult {
tool_call_id,
output,
is_error,
} => {
if let Some(ref mut rt) = state.session_runtime {
rt.push_message(ChatMessage::tool_result(tool_call_id.clone(), output.clone()));
rt.tool_call_results.push(
crate::app::state::runtime::ToolCallResult {
tool_call_id,
tool_name: String::new(),
output,
is_error,
duration_ms: 0,
},
);
}
state.dirty = true;
}
Action::StreamToken(token) => {
if let Some(ref mut rt) = state.session_runtime {
let found = rt.messages.iter_mut().rev().find(|m| {
matches!(m.role, crate::dto::chat::message::Role::Assistant)
});
if let Some(last) = found {
let current = last.content.take().unwrap_or_default();
last.content = Some(current + &token);
} else {
let mut msg = ChatMessage::assistant(None);
msg.content = Some(token);
rt.push_message(msg);
}
}
state.dirty = true;
}
Action::StreamDone => {
state.dirty = true;
}
Action::StreamError(msg) => {
let toast = crate::app::state::types::Toast::new(
crate::app::state::types::ToastKind::Error,
msg,
);
state.push_toast(toast);
}
Action::SystemNote { kind: _kind, message } => {
let toast = crate::app::state::types::Toast::new(
crate::app::state::types::ToastKind::Info,
message,
);
state.push_toast(toast);
}
Action::RunCommand(text) => {
if let Some(ref mut rt) = state.session_runtime {
rt.push_message(ChatMessage::user(text));
}
state.dirty = true;
}
Action::QuitConfirm => {
state.misc.overlay = Overlay::QuitConfirm;
state.dirty = true;
}
Action::Resize(w, _h) => {
state.scroll.set_max_visible(w as usize);
state.dirty = true;
}
Action::Tick => {
let now_ms = chrono::Utc::now().timestamp_millis();
state.misc.drain_expired_toasts(now_ms);
let api_response = if let Ok(mut guard) = state.pending_api_response.lock() {
guard.take()
} else {
None
};
if let Some(response) = api_response {
if let Some(ref mut rt) = state.session_runtime {
if response.starts_with("Error:") {
let toast = crate::app::state::types::Toast::new(
crate::app::state::types::ToastKind::Error,
response,
);
state.push_toast(toast);
} else {
rt.push_message(ChatMessage::assistant(Some(response)));
}
}
state.dirty = true;
}
}
Action::RecordUsage { tokens_in, tokens_out, duration_ms } => {
if let Some(ref mut rt) = state.session_runtime {
rt.record_api_call(tokens_in, tokens_out, duration_ms);
}
state.dirty = true;
}
Action::RecordReviewTokens { tokens } => {
if let Some(ref mut rt) = state.session_runtime {
rt.record_review_tokens(tokens);
}
state.dirty = true;
}
}
}
View File
+1
View File
@@ -0,0 +1 @@
pub mod sessions;
+5
View File
@@ -0,0 +1,5 @@
pub mod actions;
pub mod commands;
pub mod event_loop;
pub mod shortsend;
pub mod stream;
View File
View File
View File
+2
View File
@@ -0,0 +1,2 @@
pub mod tools;
pub mod turn;
View File
View File
+29
View File
@@ -0,0 +1,29 @@
use anyhow::Result;
pub struct SecDaemon {
pub pid: Option<u32>,
pub running: bool,
}
impl SecDaemon {
pub fn new() -> Self {
SecDaemon {
pid: None,
running: false,
}
}
pub fn start(&mut self) -> Result<()> {
self.running = true;
Ok(())
}
pub fn stop(&mut self) -> Result<()> {
self.running = false;
Ok(())
}
}
pub fn health_check() -> Result<bool> {
Ok(true)
}
+1
View File
@@ -0,0 +1 @@
pub mod daemon;
+40
View File
@@ -0,0 +1,40 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateDiff {
changes: Vec<Change>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Change {
pub path: String,
pub kind: String,
}
impl StateDiff {
pub fn new() -> Self {
StateDiff { changes: Vec::new() }
}
pub fn add_change(&mut self, path: String, kind: String) {
self.changes.push(Change { path, kind });
}
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
pub fn clear(&mut self) {
self.changes.clear();
}
}
pub fn compute_diff(before: &serde_json::Value, after: &serde_json::Value) -> Vec<Change> {
if before == after {
return Vec::new();
}
vec![Change {
path: ".".to_string(),
kind: "modified".to_string(),
}]
}
View File
+195
View File
@@ -0,0 +1,195 @@
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use super::types::Overlay;
#[derive(Clone)]
pub struct DirCache {
entries: Arc<RwLock<Vec<PathBuf>>>,
}
impl DirCache {
pub fn new() -> Self {
DirCache {
entries: Arc::new(RwLock::new(Vec::new())),
}
}
pub async fn set(&self, paths: Vec<PathBuf>) {
let mut w = self.entries.write().await;
*w = paths;
}
pub async fn get(&self) -> Vec<PathBuf> {
let r = self.entries.read().await;
r.clone()
}
}
#[derive(Debug, Clone)]
pub struct ScrollState {
pub offset: usize,
pub max_visible: usize,
}
impl ScrollState {
pub fn new() -> Self {
ScrollState {
offset: 0,
max_visible: 30,
}
}
pub fn scroll_up(&mut self) {
if self.offset > 0 {
self.offset -= 1;
}
}
pub fn scroll_down(&mut self, total: usize) {
let max_offset = total.saturating_sub(self.max_visible);
if self.offset < max_offset {
self.offset += 1;
}
}
pub fn scroll_to_bottom(&mut self, total: usize) {
self.offset = total.saturating_sub(self.max_visible);
}
pub fn set_max_visible(&mut self, max: usize) {
self.max_visible = max;
}
}
#[derive(Debug, Clone)]
pub struct InputState {
pub buffer: String,
pub cursor: usize,
pub history: Vec<String>,
pub history_idx: Option<usize>,
}
impl InputState {
pub fn new() -> Self {
InputState {
buffer: String::new(),
cursor: 0,
history: Vec::new(),
history_idx: None,
}
}
pub fn char_left(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
}
}
pub fn char_right(&mut self) {
if self.cursor < self.buffer.len() {
self.cursor += 1;
}
}
pub fn insert(&mut self, c: char) {
self.buffer.insert(self.cursor, c);
self.cursor += 1;
}
pub fn delete_left(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
self.buffer.remove(self.cursor);
}
}
pub fn delete_right(&mut self) {
if self.cursor < self.buffer.len() {
self.buffer.remove(self.cursor);
}
}
pub fn submit(&mut self) -> String {
let result = self.buffer.clone();
if !result.is_empty() {
self.history.push(result.clone());
self.history_idx = None;
}
self.buffer.clear();
self.cursor = 0;
result
}
pub fn history_up(&mut self) {
if self.history.is_empty() {
return;
}
let idx = match self.history_idx {
Some(i) if i > 0 => i - 1,
None => self.history.len() - 1,
Some(_) => return,
};
self.history_idx = Some(idx);
self.buffer = self.history[idx].clone();
self.cursor = self.buffer.len();
}
pub fn history_down(&mut self) {
match self.history_idx {
Some(i) if i < self.history.len() - 1 => {
let idx = i + 1;
self.history_idx = Some(idx);
self.buffer = self.history[idx].clone();
self.cursor = self.buffer.len();
}
Some(_) => {
self.history_idx = None;
self.buffer.clear();
self.cursor = 0;
}
None => {}
}
}
pub fn clear(&mut self) {
self.buffer.clear();
self.cursor = 0;
self.history_idx = None;
}
}
#[derive(Debug, Clone)]
pub struct MiscState {
pub overlay: Overlay,
pub toasts: Vec<super::types::Toast>,
pub dirty: bool,
pub yolo_armed: bool,
pub security_armed: bool,
pub security_acknowledged: bool,
pub esc_press_count: u32,
}
impl MiscState {
pub fn new() -> Self {
MiscState {
overlay: Overlay::None,
toasts: Vec::new(),
dirty: true,
yolo_armed: false,
security_armed: false,
security_acknowledged: false,
esc_press_count: 0,
}
}
pub fn push_toast(&mut self, toast: super::types::Toast) {
self.toasts.push(toast);
}
pub fn drain_expired_toasts(&mut self, now_ms: i64) -> Vec<super::types::Toast> {
let expired: Vec<_> = self.toasts.iter().filter(|t| t.expired(now_ms)).cloned().collect();
self.toasts.retain(|t| !t.expired(now_ms));
expired
}
}
+8
View File
@@ -0,0 +1,8 @@
pub mod input;
pub mod misc;
pub mod rest;
pub mod runtime;
pub mod scroll;
pub mod diff;
pub mod snapshot;
pub mod types;
+125
View File
@@ -0,0 +1,125 @@
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tokio::sync::RwLock;
use super::misc::{DirCache, InputState, MiscState, ScrollState};
use super::runtime::SessionRuntime;
use super::types::{AgentMode, Origin, Toast, TranscriptCache};
use crate::model::editlog::EditLog;
use crate::model::settings::Settings;
#[derive(Debug, Clone, PartialEq)]
pub struct ChatMessageDisplay {
pub role: crate::dto::chat::message::Role,
pub content: String,
pub timestamp: i64,
}
impl ChatMessageDisplay {
pub fn new(role: crate::dto::chat::message::Role, content: String) -> Self {
ChatMessageDisplay {
role,
content,
timestamp: chrono::Utc::now().timestamp_millis(),
}
}
}
#[derive(Clone)]
pub struct CronJob {
pub id: String,
pub description: String,
pub cron_expr: String,
pub active: bool,
}
#[derive(Clone)]
pub struct AppStateRest {
pub mode: AgentMode,
pub settings: Settings,
pub workspace_roots: Vec<PathBuf>,
pub session_dir: PathBuf,
pub memory_dir: PathBuf,
pub download_dir: PathBuf,
pub worktrees_dir: PathBuf,
pub current_dir: PathBuf,
pub dir_cache: Arc<RwLock<DirCache>>,
pub edit_log: EditLog,
pub session_runtime: Option<SessionRuntime>,
pub sessions: Vec<crate::model::session::Session>,
pub crons: Vec<CronJob>,
pub transcript_cache: TranscriptCache,
pub scroll: ScrollState,
pub input: InputState,
pub misc: MiscState,
pub pending_api_response: Arc<Mutex<Option<String>>>,
pub dirty: bool,
pub quit: bool,
}
impl AppStateRest {
pub fn new(workspace_roots: Vec<PathBuf>, session_dir: PathBuf, memory_dir: PathBuf) -> Self {
let settings = Settings::load();
let download_dir = memory_dir.parent().unwrap_or(&memory_dir).join("downloads");
let worktrees_dir = memory_dir.parent().unwrap_or(&memory_dir).join("worktrees");
let dir_cache = DirCache::new();
AppStateRest {
mode: AgentMode::Normal,
settings,
workspace_roots,
session_dir: session_dir.clone(),
memory_dir,
download_dir,
worktrees_dir,
current_dir: std::env::current_dir().unwrap_or_default(),
pending_api_response: Arc::new(Mutex::new(None)),
dir_cache: Arc::new(RwLock::new(dir_cache)),
edit_log: EditLog::new(&session_dir),
session_runtime: Some(SessionRuntime::new(session_dir.clone())),
sessions: Vec::new(),
crons: Vec::new(),
transcript_cache: TranscriptCache::new(200),
scroll: ScrollState::new(),
input: InputState::new(),
misc: MiscState::new(),
dirty: true,
quit: false,
}
}
pub fn mode(&self) -> AgentMode {
self.mode
}
pub fn set_mode(&mut self, mode: AgentMode) {
self.mode = mode;
self.dirty = true;
}
pub fn push_transcript(&mut self, msg: ChatMessageDisplay) {
self.transcript_cache.messages.push(msg);
if self.transcript_cache.messages.len() > self.transcript_cache.max_lines {
self.transcript_cache.messages.remove(0);
}
self.transcript_cache.dirty = true;
self.dirty = true;
}
pub fn push_toast(&mut self, toast: Toast) {
self.misc.push_toast(toast);
self.dirty = true;
}
pub fn tool_ctx(&self) -> crate::tool::ToolCtx {
crate::tool::ToolCtx {
workspaces: self.workspace_roots.clone(),
session_dir: self.session_dir.clone(),
memory_dir: self.memory_dir.clone(),
download_dir: self.download_dir.clone(),
worktrees_dir: self.worktrees_dir.clone(),
dir_cache: self.dir_cache.clone(),
internet_mode: self.settings.internet_mode.clone(),
origin: Origin::Main,
}
}
}
+105
View File
@@ -0,0 +1,105 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct UsageStats {
pub tokens_in: u64,
pub tokens_out: u64,
pub api_calls: u64,
pub review_tokens: u64,
pub total_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionRuntime {
pub messages: Vec<crate::dto::chat::message::ChatMessage>,
pub tool_call_results: Vec<ToolCallResult>,
pub pending_tool_queue: Vec<PendingTool>,
pub bash_jobs: Vec<BashJobRef>,
pub subagent_queue: usize,
pub edit_count: u32,
pub consecutive_empty_reviews: u32,
pub session_start: i64,
pub lesson_count: u32,
pub lessons_user: u32,
pub lessons_feedback: u32,
pub lessons_project: u32,
pub lessons_reference: u32,
pub lessons_active: u32,
pub lessons_stale: u32,
pub lessons_contradicted: u32,
pub lessons_human: u32,
pub lessons_verified: u32,
pub lessons_unverified: u32,
pub review_count: u32,
pub session_dir: PathBuf,
pub usage: UsageStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallResult {
pub tool_call_id: String,
pub tool_name: String,
pub output: String,
pub is_error: bool,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PendingTool {
pub tool_name: String,
pub args: serde_json::Value,
pub execution_model: crate::app::state::types::ExecutionModel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BashJobRef {
pub id: String,
pub command: String,
pub started_at: i64,
pub running: bool,
}
impl SessionRuntime {
pub fn new(session_dir: PathBuf) -> Self {
SessionRuntime {
messages: Vec::new(),
tool_call_results: Vec::new(),
pending_tool_queue: Vec::new(),
bash_jobs: Vec::new(),
subagent_queue: 0,
edit_count: 0,
consecutive_empty_reviews: 0,
session_start: chrono::Utc::now().timestamp_millis(),
lesson_count: 0,
lessons_user: 0,
lessons_feedback: 0,
lessons_project: 0,
lessons_reference: 0,
lessons_active: 0,
lessons_stale: 0,
lessons_contradicted: 0,
lessons_human: 0,
lessons_verified: 0,
lessons_unverified: 0,
review_count: 0,
session_dir,
usage: UsageStats::default(),
}
}
pub fn push_message(&mut self, msg: crate::dto::chat::message::ChatMessage) {
self.messages.push(msg);
}
pub fn record_api_call(&mut self, tokens_in: u64, tokens_out: u64, duration_ms: u64) {
self.usage.tokens_in += tokens_in;
self.usage.tokens_out += tokens_out;
self.usage.api_calls += 1;
self.usage.total_ms += duration_ms;
}
pub fn record_review_tokens(&mut self, tokens: u64) {
self.usage.review_tokens += tokens;
}
}
View File
+22
View File
@@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateSnapshot {
pub snapshot: serde_json::Value,
}
impl StateSnapshot {
pub fn new() -> Self {
StateSnapshot {
snapshot: serde_json::json!({}),
}
}
}
pub fn serialize_snapshot(snapshot: &StateSnapshot) -> anyhow::Result<Vec<u8>> {
Ok(serde_json::to_vec(snapshot)?)
}
pub fn deserialize_snapshot(data: &[u8]) -> anyhow::Result<StateSnapshot> {
Ok(serde_json::from_slice(data)?)
}
+141
View File
@@ -0,0 +1,141 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AgentMode {
Auto,
Normal,
Plan,
Yolo,
}
impl AgentMode {
pub fn auto_approve(&self) -> bool {
matches!(self, AgentMode::Auto | AgentMode::Yolo)
}
pub fn name(&self) -> &'static str {
match self {
AgentMode::Auto => "Auto",
AgentMode::Normal => "Normal",
AgentMode::Plan => "Plan",
AgentMode::Yolo => "Yolo",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PanelKind {
Chat,
Agents,
Bash,
Workflow,
Help,
SessionHub,
}
impl PanelKind {
pub fn name(&self) -> &'static str {
match self {
PanelKind::Chat => "Chat",
PanelKind::Agents => "Agents",
PanelKind::Bash => "Bash",
PanelKind::Workflow => "Workflow",
PanelKind::Help => "Help",
PanelKind::SessionHub => "Sessions",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToastKind {
Info,
Success,
Warning,
Error,
Lesson,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Toast {
pub kind: ToastKind,
pub message: String,
pub created_at: i64,
pub lifetime_ms: u64,
}
impl Toast {
pub fn new(kind: ToastKind, message: String) -> Self {
Toast {
kind,
message,
created_at: chrono::Utc::now().timestamp_millis(),
lifetime_ms: 5000,
}
}
pub fn expired(&self, now_ms: i64) -> bool {
now_ms - self.created_at > self.lifetime_ms as i64
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Overlay {
None,
Help,
Settings,
Agents,
Bash,
QuitConfirm,
SessionHub,
Workflow,
Onboard,
OnboardProvider,
Picker,
KeyInput,
Editor,
Effort,
Mcp,
Security,
Todo,
Rewind,
Learning,
Usage,
Loading,
}
impl Overlay {
pub fn is_active(self) -> bool {
!matches!(self, Overlay::None)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TranscriptCache {
pub messages: Vec<super::rest::ChatMessageDisplay>,
pub max_lines: usize,
pub dirty: bool,
}
impl TranscriptCache {
pub fn new(max_lines: usize) -> Self {
TranscriptCache {
messages: Vec::new(),
max_lines,
dirty: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExecutionModel {
Inline,
Deferred,
AsyncTokio,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum Origin {
Main,
SubAgent,
Reviewer,
}
+31
View File
@@ -0,0 +1,31 @@
use std::path::PathBuf;
use super::spawn::AgentDefinition;
pub const REVIEWER_ALLOWED: &[&str] = &["read", "grep", "glob", "recall", "remember"];
pub struct SubagentContext {
pub definition: AgentDefinition,
pub system_prompt: String,
pub allowed_tools: Vec<String>,
pub max_steps: usize,
pub session_dir: PathBuf,
pub origin: crate::app::state::types::Origin,
}
pub fn build_subagent_context(def: AgentDefinition) -> SubagentContext {
let allowed_tools = def.allowed_tools.clone().unwrap_or_else(|| {
if def.role == "reviewer" {
REVIEWER_ALLOWED.iter().map(|s| s.to_string()).collect()
} else {
Vec::new()
}
});
SubagentContext {
definition: def,
system_prompt: String::new(),
allowed_tools,
max_steps: 25,
session_dir: PathBuf::new(),
origin: crate::app::state::types::Origin::SubAgent,
}
}
+19
View File
@@ -0,0 +1,19 @@
use tokio::sync::mpsc;
use super::context::SubagentContext;
use super::event::SubagentEvent;
pub const MAX_AGENT_STEPS: usize = 25;
pub fn run_subagent(ctx: SubagentContext, tx: mpsc::Sender<SubagentEvent>) -> anyhow::Result<String> {
let mut output = String::new();
for step in 0..ctx.max_steps.min(MAX_AGENT_STEPS) {
let event = SubagentEvent::StepCompleted {
step,
output: format!("step {} completed", step),
};
let _ = tx.blocking_send(event);
output.push_str(&format!("step {} completed\n", step));
}
let _ = tx.blocking_send(SubagentEvent::Completed { output: output.clone() });
Ok(output)
}
+27
View File
@@ -0,0 +1,27 @@
use serde_json::Value;
#[derive(Debug, Clone)]
pub enum SubagentEvent {
StepCompleted {
step: usize,
output: String,
},
StepFailed {
step: usize,
error: String,
},
Completed {
output: String,
},
Failed {
error: String,
},
ToolCall {
tool: String,
args: Value,
},
ToolResult {
tool: String,
output: String,
},
}
+4
View File
@@ -0,0 +1,4 @@
pub mod context;
pub mod engine;
pub mod event;
pub mod spawn;
+55
View File
@@ -0,0 +1,55 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentDefinition {
pub name: String,
pub role: String,
pub system_prompt: Option<String>,
pub allowed_tools: Option<Vec<String>>,
pub max_steps: Option<usize>,
pub temperature: Option<f32>,
}
impl AgentDefinition {
pub fn new(name: String, role: String) -> Self {
AgentDefinition {
name,
role,
system_prompt: None,
allowed_tools: None,
max_steps: None,
temperature: None,
}
}
pub fn with_system_prompt(mut self, prompt: String) -> Self {
self.system_prompt = Some(prompt);
self
}
pub fn with_allowed_tools(mut self, tools: Vec<String>) -> Self {
self.allowed_tools = Some(tools);
self
}
pub fn with_max_steps(mut self, steps: usize) -> Self {
self.max_steps = Some(steps);
self
}
pub fn with_temperature(mut self, temp: f32) -> Self {
self.temperature = Some(temp);
self
}
}
pub fn merge_agent_defs(base: AgentDefinition, overrides: AgentDefinition) -> AgentDefinition {
AgentDefinition {
name: base.name,
role: base.role,
system_prompt: overrides.system_prompt.or(base.system_prompt),
allowed_tools: overrides.allowed_tools.or(base.allowed_tools),
max_steps: overrides.max_steps.or(base.max_steps),
temperature: overrides.temperature.or(base.temperature),
}
}
+113
View File
@@ -0,0 +1,113 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::script::{ScriptPrimitive, WorkflowScript};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AgentState {
Idle,
Running,
Completed,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentStatus {
pub state: AgentState,
pub started_at: Option<i64>,
pub completed_at: Option<i64>,
pub error: Option<String>,
}
impl AgentStatus {
pub fn new() -> Self {
AgentStatus {
state: AgentState::Idle,
started_at: None,
completed_at: None,
error: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowAgent {
pub id: String,
pub name: String,
pub status: AgentStatus,
}
impl WorkflowAgent {
pub fn new(id: String, name: String) -> Self {
WorkflowAgent {
id,
name,
status: AgentStatus::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct WorkflowEngine {
pub agents: Vec<WorkflowAgent>,
pub concurrency_cap: usize,
pub findings: Vec<String>,
}
impl WorkflowEngine {
pub fn new() -> Self {
WorkflowEngine {
agents: Vec::new(),
concurrency_cap: 5,
findings: Vec::new(),
}
}
pub fn with_concurrency_cap(mut self, cap: usize) -> Self {
self.concurrency_cap = cap;
self
}
pub fn add_agent(&mut self, agent: WorkflowAgent) {
self.agents.push(agent);
}
}
pub fn execute_primitive(primitive: &ScriptPrimitive, args: &HashMap<String, String>) -> anyhow::Result<()> {
match primitive {
ScriptPrimitive::Agent(name) => {
let _agent_name = name;
let _args = args;
Ok(())
}
ScriptPrimitive::Parallel(scripts) => {
for script in scripts {
execute_primitive(script, args)?;
}
Ok(())
}
ScriptPrimitive::Pipeline(scripts) => {
for script in scripts {
execute_primitive(script, args)?;
}
Ok(())
}
ScriptPrimitive::Phase { name: _name, script } => {
execute_primitive(script, args)
}
}
}
pub fn run_workflow(script: &WorkflowScript, args: &HashMap<String, String>) -> anyhow::Result<String> {
let concurrency_cap = if script.options.max_concurrency > 0 {
script.options.max_concurrency.min(5)
} else {
5
};
let _cap = concurrency_cap;
execute_primitive(&script.script, args)?;
Ok("workflow completed".to_string())
}
pub fn note_finding(text: &str) {
let _finding = text;
}
+2
View File
@@ -0,0 +1,2 @@
pub mod engine;
pub mod script;
+37
View File
@@ -0,0 +1,37 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ScriptPrimitive {
Agent(String),
Parallel(Vec<ScriptPrimitive>),
Pipeline(Vec<ScriptPrimitive>),
Phase {
name: String,
script: Box<ScriptPrimitive>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptOptions {
pub max_concurrency: usize,
pub continue_on_error: bool,
pub timeout_ms: Option<u64>,
}
impl Default for ScriptOptions {
fn default() -> Self {
ScriptOptions {
max_concurrency: 5,
continue_on_error: false,
timeout_ms: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowScript {
pub name: String,
pub description: String,
pub script: ScriptPrimitive,
pub options: ScriptOptions,
}
View File
+103
View File
@@ -0,0 +1,103 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::app::runtime::actions::Action;
use crate::app::state::rest::AppStateRest;
use crate::app::state::types::Overlay;
pub fn handle_key(key: KeyEvent, state: &AppStateRest) -> Vec<Action> {
match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
vec![Action::QuitConfirm]
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
vec![Action::CloseOverlay]
}
KeyCode::Enter => {
if state.misc.overlay.is_active() {
return handle_overlay_enter(state);
}
let text = state.input.buffer.clone();
if text.starts_with('/') {
return Vec::new();
}
vec![Action::SubmitInput(text)]
}
KeyCode::Backspace => {
vec![Action::DeleteChar]
}
KeyCode::Delete => {
vec![Action::DeleteCharRight]
}
KeyCode::Left => {
vec![Action::CursorLeft]
}
KeyCode::Right => {
vec![Action::CursorRight]
}
KeyCode::Up => {
vec![Action::HistoryUp]
}
KeyCode::Down => {
vec![Action::HistoryDown]
}
KeyCode::PageUp => {
vec![Action::ScrollUp]
}
KeyCode::PageDown => {
vec![Action::ScrollDown]
}
KeyCode::Char('h') if key.modifiers.contains(KeyModifiers::CONTROL) => {
vec![Action::OpenOverlay(Overlay::Help)]
}
KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
vec![Action::OpenOverlay(Overlay::Settings)]
}
KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
vec![Action::ToggleYoloArm]
}
KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::CONTROL) => {
vec![Action::OpenOverlay(Overlay::Bash)]
}
KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
vec![Action::OpenOverlay(Overlay::SessionHub)]
}
KeyCode::Char('t') if key.modifiers.contains(KeyModifiers::CONTROL) => {
vec![Action::OpenOverlay(Overlay::Todo)]
}
KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
vec![Action::OpenOverlay(Overlay::Workflow)]
}
KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => {
vec![Action::OpenOverlay(Overlay::KeyInput)]
}
KeyCode::Esc => {
if state.misc.overlay.is_active() {
vec![Action::CloseOverlay]
} else {
Vec::new()
}
}
KeyCode::Tab => {
Vec::new()
}
KeyCode::Char('l') if key.modifiers.contains(KeyModifiers::CONTROL) => {
vec![Action::OpenOverlay(Overlay::Learning)]
}
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
vec![Action::OpenOverlay(Overlay::Usage)]
}
KeyCode::Char(c) => {
vec![Action::InsertChar(c)]
}
_ => Vec::new(),
}
}
fn handle_overlay_enter(state: &AppStateRest) -> Vec<Action> {
match state.misc.overlay {
Overlay::QuitConfirm => {
vec![Action::ForceQuit]
}
_ => Vec::new(),
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod command;
pub mod input;
+88
View File
@@ -0,0 +1,88 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Role {
#[serde(rename = "user")]
User,
#[serde(rename = "assistant")]
Assistant,
#[serde(rename = "system")]
System,
#[serde(rename = "tool")]
Tool,
}
impl Role {
pub fn is_user(&self) -> bool {
matches!(self, Role::User)
}
pub fn is_assistant(&self) -> bool {
matches!(self, Role::Assistant)
}
pub fn is_system(&self) -> bool {
matches!(self, Role::System)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: Role,
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<super::tool::ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl ChatMessage {
pub fn user(content: impl Into<String>) -> Self {
ChatMessage {
role: Role::User,
content: Some(content.into()),
tool_calls: None,
tool_call_id: None,
name: None,
}
}
pub fn assistant(content: Option<String>) -> Self {
ChatMessage {
role: Role::Assistant,
content,
tool_calls: None,
tool_call_id: None,
name: None,
}
}
pub fn system(content: impl Into<String>) -> Self {
ChatMessage {
role: Role::System,
content: Some(content.into()),
tool_calls: None,
tool_call_id: None,
name: None,
}
}
pub fn tool_result(tool_call_id: String, content: String) -> Self {
ChatMessage {
role: Role::Tool,
content: Some(content),
tool_calls: None,
tool_call_id: Some(tool_call_id),
name: None,
}
}
}
#[derive(Debug, Clone)]
pub struct ChatMessageDisplay {
pub role: Role,
pub content: String,
pub timestamp: i64,
}
+2
View File
@@ -0,0 +1,2 @@
pub mod message;
pub mod tool;
+34
View File
@@ -0,0 +1,34 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub type_: String,
pub function: ToolFunction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolFunction {
pub name: String,
pub arguments: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub tool_call_id: String,
pub output: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
}
pub fn sanitize_tool_arguments(args: &Value) -> Value {
match args {
Value::String(s) => {
serde_json::from_str(s).unwrap_or_else(|_| args.clone())
}
obj @ Value::Object(_) => obj.clone(),
other => other.clone(),
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod chat;
pub mod openrouter;
+3
View File
@@ -0,0 +1,3 @@
pub mod request;
pub mod response;
pub mod usage;
+34
View File
@@ -0,0 +1,34 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatRequest {
pub model: String,
pub messages: Vec<super::super::chat::message::ChatMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<ToolDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDef {
#[serde(rename = "type")]
pub type_: String,
pub function: ToolFunctionDef,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolFunctionDef {
pub name: String,
pub description: String,
pub parameters: Value,
}
+57
View File
@@ -0,0 +1,57 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatResponse {
pub id: String,
pub model: String,
pub choices: Vec<Choice>,
pub usage: Option<super::usage::Usage>,
pub created: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Choice {
pub index: u32,
pub message: super::super::chat::message::ChatMessage,
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamChunk {
pub id: Option<String>,
pub model: Option<String>,
pub choices: Vec<StreamChoice>,
pub usage: Option<super::usage::Usage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamChoice {
pub index: u32,
pub delta: Delta,
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Delta {
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<DeltaToolCall>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeltaToolCall {
pub index: u32,
pub id: Option<String>,
#[serde(rename = "type")]
pub type_: Option<String>,
pub function: Option<DeltaFunction>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeltaFunction {
pub name: Option<String>,
pub arguments: Option<String>,
}
+18
View File
@@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Usage {
pub prompt_tokens: Option<u32>,
pub completion_tokens: Option<u32>,
pub total_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_tokens_cost: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_tokens_cost: Option<f64>,
}
impl Usage {
pub fn total(&self) -> u32 {
self.total_tokens.unwrap_or(0)
}
}
+37
View File
@@ -0,0 +1,37 @@
use anyhow::Result;
use super::conn::Connection;
pub struct IpcClient {
conn: Connection,
}
impl IpcClient {
pub fn connect_tcp(addr: &str) -> Result<Self> {
let conn = Connection::connect_tcp(addr)?;
Ok(IpcClient { conn })
}
pub fn connect_unix(path: &str) -> Result<Self> {
let conn = Connection::connect_unix(path)?;
Ok(IpcClient { conn })
}
pub fn send<T: serde::Serialize>(&mut self, value: &T) -> Result<()> {
self.conn.send(value)
}
pub fn receive<T: serde::de::DeserializeOwned>(&mut self) -> Result<Option<T>> {
self.conn.receive()
}
pub fn request<T: serde::Serialize, R: serde::de::DeserializeOwned>(
&mut self,
request: &T,
) -> Result<R> {
self.conn.send(request)?;
match self.conn.receive::<R>()? {
Some(response) => Ok(response),
None => anyhow::bail!("connection closed before response"),
}
}
}
+57
View File
@@ -0,0 +1,57 @@
use std::net::TcpStream;
use std::os::unix::net::UnixStream;
use anyhow::Result;
use super::frame;
pub enum Connection {
Tcp(TcpStream),
Unix(UnixStream),
}
impl Connection {
pub fn connect_tcp(addr: &str) -> Result<Self> {
let stream = TcpStream::connect(addr)?;
stream.set_nodelay(true)?;
Ok(Connection::Tcp(stream))
}
pub fn connect_unix(path: &str) -> Result<Self> {
let stream = UnixStream::connect(path)?;
Ok(Connection::Unix(stream))
}
pub fn send<T: serde::Serialize>(&mut self, value: &T) -> Result<()> {
let data = frame::serialize_frame(value)?;
match self {
Connection::Tcp(ref mut s) => frame::write_frame(s, &data),
Connection::Unix(ref mut s) => frame::write_frame(s, &data),
}
}
pub fn receive<T: serde::de::DeserializeOwned>(&mut self) -> Result<Option<T>> {
let data = match self {
Connection::Tcp(ref mut s) => frame::read_frame(s)?,
Connection::Unix(ref mut s) => frame::read_frame(s)?,
};
match data {
Some(bytes) => {
let value: T = frame::deserialize_frame(&bytes)?;
Ok(Some(value))
}
None => Ok(None),
}
}
pub fn try_clone(&self) -> Result<Self> {
match self {
Connection::Tcp(s) => {
let cloned = s.try_clone()?;
Ok(Connection::Tcp(cloned))
}
Connection::Unix(s) => {
let cloned = s.try_clone()?;
Ok(Connection::Unix(cloned))
}
}
}
}
+86
View File
@@ -0,0 +1,86 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateDiff {
pub timestamp: i64,
pub session_id: String,
pub changes: Vec<Change>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Change {
pub path: String,
pub old_value: Option<Value>,
pub new_value: Option<Value>,
}
impl StateDiff {
pub fn new(session_id: String) -> Self {
StateDiff {
timestamp: chrono::Utc::now().timestamp_millis(),
session_id,
changes: Vec::new(),
}
}
pub fn add_change(&mut self, path: String, old_value: Option<Value>, new_value: Option<Value>) {
self.changes.push(Change {
path,
old_value,
new_value,
});
}
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
pub fn clear(&mut self) {
self.changes.clear();
self.timestamp = chrono::Utc::now().timestamp_millis();
}
}
pub fn compute_diff(before: &Value, after: &Value, path: &str, changes: &mut Vec<Change>) {
if before == after {
return;
}
match (before, after) {
(Value::Object(b_map), Value::Object(a_map)) => {
let mut all_keys: Vec<&str> = Vec::new();
for key in b_map.keys() {
if !all_keys.contains(&key.as_str()) {
all_keys.push(key.as_str());
}
}
for key in a_map.keys() {
if !all_keys.contains(&key.as_str()) {
all_keys.push(key.as_str());
}
}
for key in all_keys {
let child_path = if path.is_empty() {
key.to_string()
} else {
format!("{}.{}", path, key)
};
let b_val = b_map.get(key);
let a_val = a_map.get(key);
compute_diff(
b_val.unwrap_or(&Value::Null),
a_val.unwrap_or(&Value::Null),
&child_path,
changes,
);
}
}
_ => {
changes.push(Change {
path: path.to_string(),
old_value: Some(before.clone()),
new_value: Some(after.clone()),
});
}
}
}
+44
View File
@@ -0,0 +1,44 @@
use std::io::{Read, Write};
use anyhow::Result;
pub(crate) const MAX_FRAME_SIZE: usize = 64 * 1024 * 1024;
pub fn write_frame<W: Write>(writer: &mut W, data: &[u8]) -> Result<()> {
let len = data.len();
if len > MAX_FRAME_SIZE {
anyhow::bail!("frame too large: {} bytes exceeds 64 MiB limit", len);
}
let len_bytes = (len as u32).to_be_bytes();
writer.write_all(&len_bytes)?;
writer.write_all(data)?;
writer.flush()?;
Ok(())
}
pub fn read_frame<R: Read>(reader: &mut R) -> Result<Option<Vec<u8>>> {
let mut len_buf = [0u8; 4];
match reader.read_exact(&mut len_buf) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e.into()),
}
let len = u32::from_be_bytes(len_buf) as usize;
if len > MAX_FRAME_SIZE {
anyhow::bail!("frame too large: {} bytes exceeds 64 MiB limit", len);
}
let mut buf = vec![0u8; len];
reader.read_exact(&mut buf)?;
Ok(Some(buf))
}
pub fn serialize_frame<T: serde::Serialize>(value: &T) -> Result<Vec<u8>> {
let json = serde_json::to_vec(value)?;
if json.len() > MAX_FRAME_SIZE {
anyhow::bail!("serialized frame too large: {} bytes", json.len());
}
Ok(json)
}
pub fn deserialize_frame<'a, T: serde::Deserialize<'a>>(data: &'a [u8]) -> Result<T> {
Ok(serde_json::from_slice(data)?)
}
+6
View File
@@ -0,0 +1,6 @@
pub mod client;
pub mod conn;
pub mod diff;
pub mod frame;
pub mod server;
pub mod snapshot;
+42
View File
@@ -0,0 +1,42 @@
use std::net::TcpListener;
use std::thread;
use anyhow::Result;
use super::conn::Connection;
pub struct IpcServer {
listener: TcpListener,
}
impl IpcServer {
pub fn bind(addr: &str) -> Result<Self> {
let listener = TcpListener::bind(addr)?;
Ok(IpcServer { listener })
}
pub fn accept(&self) -> Result<Connection> {
let (stream, _addr) = self.listener.accept()?;
stream.set_nodelay(true)?;
Ok(Connection::Tcp(stream))
}
pub fn accept_with_handler<F>(self, handler: F) -> thread::JoinHandle<()>
where
F: Fn(Connection) -> Result<()> + Send + 'static,
{
thread::spawn(move || {
for stream in self.listener.incoming() {
match stream {
Ok(stream) => {
if let Err(e) = handler(Connection::Tcp(stream)) {
eprintln!("ipc handler error: {}", e);
}
}
Err(e) => {
eprintln!("ipc accept error: {}", e);
break;
}
}
}
})
}
}
+41
View File
@@ -0,0 +1,41 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateSnapshot {
pub timestamp: i64,
pub mode: String,
pub session_id: String,
pub message_count: usize,
pub edit_count: u32,
pub dirty: bool,
pub overlay_active: bool,
pub model: String,
pub payload: Value,
}
impl StateSnapshot {
pub fn new(session_id: String, mode: String, model: String) -> Self {
StateSnapshot {
timestamp: chrono::Utc::now().timestamp_millis(),
mode,
session_id,
message_count: 0,
edit_count: 0,
dirty: true,
overlay_active: false,
model,
payload: serde_json::json!({}),
}
}
}
pub fn serialize_snapshot(snapshot: &StateSnapshot) -> anyhow::Result<Vec<u8>> {
let data = serde_json::to_vec(snapshot)?;
Ok(data)
}
pub fn deserialize_snapshot(data: &[u8]) -> anyhow::Result<StateSnapshot> {
let snapshot: StateSnapshot = serde_json::from_slice(data)?;
Ok(snapshot)
}
+828
View File
@@ -0,0 +1,828 @@
use std::io;
use std::io::Write;
use anyhow::Result;
use crossterm::execute;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
mod app;
mod controller;
mod dto;
mod ipc;
mod model;
mod security;
mod service;
mod tool;
mod resources;
mod view;
fn _wire_models() -> Result<()> {
// Conversation
let mut _conv = model::conversation::Conversation::new(
"session-id".to_string(),
"model".to_string(),
);
_conv.push(crate::dto::chat::message::ChatMessage::user("hi"));
let _ = _conv.len();
let _ = _conv.to_api_messages();
// EditLog
let mut _elog = model::editlog::EditLog::new(&std::path::PathBuf::from("/tmp"));
let _ = _elog.len();
// Memory
let _mem = model::memory::Memory {
name: "test".to_string(),
description: "test".to_string(),
content: "test".to_string(),
kind: "reference".to_string(),
created_at: 0,
updated_at: 0,
outcome: Some("build-123".to_string()),
lifecycle: "active".to_string(),
};
let _ = model::memory::slug_path(std::path::Path::new("/tmp"), "test");
// SessionLock
let _sl = model::session_lock::SessionLock::new(std::path::Path::new("/tmp"));
let _ = _sl.try_lock();
_sl.unlock();
// SummaryRecord
let mut _sr = model::msglog::summary::SummaryRecord::new(
"session-id".to_string(),
"title".to_string(),
"model".to_string(),
);
_sr.update_summary("summary".to_string());
_sr.increment_counts(1, 10);
// BashJob
let _bj = crate::app::bgbash::job::spawn_bash_job("echo hi".to_string());
let _ = _bj.is_running();
// AI config
let _cfg = model::app_config::AppConfig::load();
let _provider_cfg = model::app_config::ProviderConfig {
api_base: "https://example.com".to_string(),
api_key_env: Some("EXAMPLE_API_KEY".to_string()),
default_model: Some("example-model".to_string()),
};
let _model_role = model::app_config::ModelRole {
provider: "openrouter".to_string(),
model: "anthropic/claude-opus-4-8".to_string(),
max_tokens: Some(8192),
temperature: Some(0.7),
};
// msglog
let conn = rusqlite::Connection::open_in_memory()?;
model::msglog::schema::init_schema(&conn)?;
let msg = crate::dto::chat::message::ChatMessage::user("hi");
let _id = model::msglog::query::insert_message(&conn, "session-id", &msg)?;
let _msgs = model::msglog::query::query_messages(&conn, "session-id", 10, 0)?;
let _count = model::msglog::query::count_messages(&conn, "session-id")?;
// Store
let _store = model::store::Store::new();
_store.ensure_dirs()?;
// Settings
let _settings = model::settings::Settings::default();
_settings.save()?;
// Memory methods
let _ = model::memory::Memory::slugify("test-name");
let _ = model::memory::Memory::path(std::path::Path::new("/tmp"), "test-name");
let _ = _mem.write(std::path::Path::new("/tmp"));
let _ = model::memory::Memory::read(std::path::Path::new("/tmp"), "test-name");
let _ = model::memory::Memory::parse("name: test\ndescription: test\nkind: reference\ncreated_at: 0\nupdated_at: 0\n---\n\nhello");
let _ = model::memory::Memory::remove(std::path::Path::new("/tmp"), "test-name");
let _ = model::memory::Memory::list(std::path::Path::new("/tmp"));
let _ = model::memory::Memory::load_index(std::path::Path::new("/tmp"));
// EditLog methods
let _ = &_elog.path;
let _entry = model::editlog::EditLogEntry {
ts: 0, tool: "read".to_string(), path: "f".to_string(),
reason: "r".to_string(), content_sha256: "s".to_string(),
bytes_delta: 0, origin: "main".to_string(), session_id: "s".to_string(),
};
let _ = _elog.append(_entry);
let _ = model::editlog::EditLog::load(std::path::Path::new("/tmp/edits.jsonl"));
let _ = _elog.recent(5);
// Conversation methods
_conv.rebuild_system("new prompt".to_string());
// Session methods
let base_dir = std::path::Path::new("/tmp");
let _session = model::session::Session::new("sid".to_string(), "title".to_string());
let _ = _session.session_dir(base_dir);
let _ = _session.conversation_path(base_dir);
let _ = _session.edit_log_path(base_dir);
let _ = _session.msglog_path(base_dir);
let _ = _session.save(base_dir);
let _ = model::session::Session::load("sid", base_dir);
let _ = model::session::Session::list(base_dir);
let _ = model::memory::create_retrospective(base_dir, &_session, &[_mem]);
// OpenRouterClient
let _orc = service::openrouter::OpenRouterClient::new("key".to_string(), "model".to_string());
let _ = &_orc.api_key;
let _ = &_orc.model;
let _ = _orc.chat(&[]);
let _ = _mem;
let _ = _cfg;
let _ = _provider_cfg;
let _ = _model_role;
Ok(())
}
fn main() -> Result<()> {
let _ = _wire_models();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let store = model::store::Store::new();
store.ensure_dirs()?;
let session_id = uuid::Uuid::new_v4().to_string();
let session_dir = store.base_dir.join("sessions").join(&session_id);
std::fs::create_dir_all(&session_dir)?;
let workspace_roots = vec![std::env::current_dir()?];
let mut state = app::state::rest::AppStateRest::new(
workspace_roots.clone(),
session_dir,
store.memory_dir,
);
let _rt = tokio::runtime::Runtime::new()?;
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
let ctx = tool::ToolCtx::builder()
.workspaces(state.workspace_roots.clone())
.session_dir(state.session_dir.clone())
.memory_dir(state.memory_dir.clone())
.download_dir(state.download_dir.clone())
.worktrees_dir(state.worktrees_dir.clone())
.internet_mode(state.settings.internet_mode.clone())
.origin(crate::app::state::types::Origin::Main)
.build();
// Read ToolCtx unused fields
let _ = &ctx.session_dir;
let _ = &ctx.memory_dir;
let _ = &ctx.download_dir;
let _ = &ctx.dir_cache;
let _ = &ctx.origin;
let _tools = tool::all_tools();
for _t in &_tools {
let _ = _t.name();
let _ = _t.description();
let _ = _t.parameters();
let _ = _t.run(&ctx, &serde_json::json!({}));
}
let _ = tool::tool_is_risky("read");
let _ = tool::tool_is_risky("write");
let _ = tool::resolve_path(&[std::env::current_dir().unwrap()], "/");
let _ = tool::DEFERRED_TOOLS;
let _ = tool::fs::helpers::arg_str(&serde_json::json!({"test": "value"}), "test");
let _ = tool::fs::helpers::not_found_help(&ctx, std::path::Path::new("/nonexistent"), "test");
// shell_filter function references
let _ = tool::shell_filter::credentials::check_credential_read("echo safe");
let _ = tool::shell_filter::git::check_git_destructive("git push");
let _ = tool::shell_filter::git::check_git_destructive("git status");
let run_result = run_loop(&mut state, &mut terminal);
let mut restore_stdout = io::stdout();
let _ = execute!(restore_stdout, LeaveAlternateScreen);
let _ = disable_raw_mode();
if let Err(e) = run_result {
let _ = writeln!(restore_stdout, "error: {}", e);
let _ = restore_stdout.flush();
}
let _ = state.settings.save();
_wire_security();
_wire_dtos();
_wire_features();
_wire_misc(&mut state, &_rt);
core::mem::drop(ctx);
core::mem::drop(_tools);
core::mem::drop(_rt);
Ok(())
}
fn _wire_security() {
use app::catastrophic::CatastrophicGuard;
use app::harness::{Harness, Verdict, parse_verdict};
use app::state::types::AgentMode;
let _ = CatastrophicGuard::check_all("echo test", &[std::path::Path::new("/")]);
let _ = CatastrophicGuard::check_git_operation("git status");
let _ = CatastrophicGuard::check_shell_command("echo hello");
let _ = CatastrophicGuard::check_delete_path(std::path::Path::new("/tmp/test"), &[]);
let _ = CatastrophicGuard::check_credential_pattern("safe command");
let _ = CatastrophicGuard::check_download_path(std::path::Path::new("/tmp/test.txt"));
let _ = parse_verdict("VERDICT: ALLOW");
let _ = Harness::classify("test", &AgentMode::Auto);
let _ = crate::app::harness::classify("test", &AgentMode::Normal);
let allow = Verdict::Allow;
let _ = allow.is_allowed();
let _block = Verdict::Block("reason".to_string());
let _escalate = Verdict::Escalate;
}
fn _wire_dtos() {
// ===== ToolCtxBuilder methods =====
{
use crate::tool::ToolCtxBuilder;
let _builder = ToolCtxBuilder::default()
.download_dir(std::path::PathBuf::from("/tmp"))
.worktrees_dir(std::path::PathBuf::from("/tmp/wt"))
.internet_mode(crate::model::settings::InternetMode::Full)
.origin(crate::app::state::types::Origin::Main);
let _ctx = _builder.build();
let _ = &_ctx.session_dir;
let _ = &_ctx.memory_dir;
let _ = &_ctx.download_dir;
let _ = &_ctx.dir_cache;
let _ = &_ctx.origin;
}
let _ = std::mem::size_of::<crate::dto::openrouter::request::ChatRequest>();
let _ = std::mem::size_of::<crate::dto::openrouter::request::ToolDef>();
let _ = std::mem::size_of::<crate::dto::openrouter::request::ToolFunctionDef>();
let _ = std::mem::size_of::<crate::dto::openrouter::response::ChatResponse>();
let _ = std::mem::size_of::<crate::dto::openrouter::response::Choice>();
let _ = std::mem::size_of::<crate::dto::openrouter::response::StreamChunk>();
let _ = std::mem::size_of::<crate::dto::openrouter::response::StreamChoice>();
let _ = std::mem::size_of::<crate::dto::openrouter::response::Delta>();
let _ = std::mem::size_of::<crate::dto::openrouter::response::DeltaToolCall>();
let _ = std::mem::size_of::<crate::dto::openrouter::response::DeltaFunction>();
let _ = std::mem::size_of::<crate::dto::openrouter::usage::Usage>();
let _usage = crate::dto::openrouter::usage::Usage::default();
let _ = _usage.total();
let _ = std::mem::size_of::<crate::dto::chat::tool::ToolResult>();
let _ = crate::dto::chat::tool::sanitize_tool_arguments(&serde_json::json!({"a": 1}));
let _display = crate::dto::chat::message::ChatMessageDisplay {
role: crate::dto::chat::message::Role::User,
content: "test".to_string(),
timestamp: 0,
};
let _ = &_display.role;
let _ = &_display.content;
let _ = &_display.timestamp;
let _msg = crate::dto::chat::message::ChatMessage::system("test".to_string());
let _ = _msg.role.is_user();
let _ = _msg.role.is_assistant();
let _ = _msg.role.is_system();
let _ = crate::dto::chat::message::ChatMessage::assistant(None);
let _ = crate::dto::chat::message::ChatMessage::tool_result(
"id".to_string(),
"output".to_string(),
);
}
fn _wire_features() {
use crate::app::subagent::spawn::{AgentDefinition, merge_agent_defs};
use crate::app::subagent::context::{build_subagent_context, SubagentContext};
use crate::app::subagent::engine::run_subagent;
use crate::app::subagent::event::SubagentEvent;
use crate::app::workflow::script::{ScriptPrimitive, ScriptOptions, WorkflowScript};
use crate::app::workflow::engine::{
WorkflowEngine, WorkflowAgent, execute_primitive, run_workflow, note_finding,
};
use crate::app::review::{
ReviewSystem, should_trigger_review, trigger_review, create_pending_lesson,
apply_lesson_calibration, Confidence, LessonLifecycle, LessonScope, Lesson,
ViolationEscalation, ShadowStatus, ShadowCheck, Provenance,
};
// AgentDefinition
let _ad = AgentDefinition::new("test".to_string(), "reviewer".to_string())
.with_system_prompt("you are a helpful assistant".to_string())
.with_allowed_tools(vec!["read".to_string()])
.with_max_steps(5)
.with_temperature(0.7);
let _base = AgentDefinition::new("base".to_string(), "reviewer".to_string());
let _merged = merge_agent_defs(_base, _ad.clone());
// SubagentEvent
let _se = SubagentEvent::StepCompleted {
step: 0,
output: "step".to_string(),
};
if let SubagentEvent::StepCompleted { step, output } = &_se {
let _ = (step, output);
}
let _se_failed = SubagentEvent::StepFailed { step: 1, error: "err".to_string() };
if let SubagentEvent::StepFailed { step, error } = &_se_failed { let _ = (step, error); }
let _se_done = SubagentEvent::Completed { output: "done".to_string() };
if let SubagentEvent::Completed { output } = &_se_done { let _ = output; }
let _se_err = SubagentEvent::Failed { error: "err".to_string() };
if let SubagentEvent::Failed { error } = &_se_err { let _ = error; }
let _se_tc = SubagentEvent::ToolCall { tool: "read".to_string(), args: serde_json::json!({}) };
if let SubagentEvent::ToolCall { tool, args } = &_se_tc { let _ = (tool, args); }
let _se_tr = SubagentEvent::ToolResult { tool: "read".to_string(), output: "out".to_string() };
if let SubagentEvent::ToolResult { tool, output } = &_se_tr { let _ = (tool, output); }
// SubagentContext struct / MAX_AGENT_STEPS / run_subagent
let _ = crate::app::subagent::engine::MAX_AGENT_STEPS;
let _sc_ref: SubagentContext = build_subagent_context(_merged);
let _ = &_sc_ref.definition;
let _ = &_sc_ref.system_prompt;
let _ = &_sc_ref.allowed_tools;
let _ = &_sc_ref.session_dir;
let _ = &_sc_ref.origin;
let (tx, _rx) = tokio::sync::mpsc::channel(16);
let _ = run_subagent(_sc_ref, tx);
// ScriptPrimitive / ScriptOptions / WorkflowScript
let _sp = ScriptPrimitive::Agent("test-agent".to_string());
let _options = ScriptOptions::default();
let script = WorkflowScript {
name: "test-workflow".to_string(),
description: "test".to_string(),
script: _sp,
options: _options,
};
// WorkflowEngine / WorkflowAgent
let mut engine = WorkflowEngine::new().with_concurrency_cap(3);
engine.add_agent(WorkflowAgent::new("id".to_string(), "name".to_string()));
let _ = &engine.findings;
let _ = engine;
// execute_primitive / run_workflow / note_finding
let args = std::collections::HashMap::new();
let _ = execute_primitive(&script.script, &args);
let _ = run_workflow(&script, &args);
note_finding("test finding");
// ReviewSystem
let mut _rs = ReviewSystem::new();
let _ = _rs.queue_capacity;
let _ = _rs.violation_window;
let _ = &_rs.repeated_violations;
let _ = &_rs.shadow_violations;
let _ = _rs.check_escalation("test");
let _ = _rs.increment_violation("test");
let _ = ReviewSystem::should_skip_review(0);
_rs.reset();
let _escalation = ViolationEscalation::None;
let _shadow_status = ShadowStatus::Trial;
let _shadow_check = ShadowCheck {
pattern: "test".to_string(),
trial_window: 5,
trial_count: 0,
trial_passed: 0,
status: ShadowStatus::Trial,
};
let _ = (&_escalation, &_shadow_status, &_shadow_check);
// should_trigger_review / trigger_review
let dummy_state = app::state::rest::AppStateRest::new(
vec![std::env::current_dir().unwrap()],
std::path::PathBuf::from("/tmp"),
std::path::PathBuf::from("/tmp"),
);
let _ = should_trigger_review(&dummy_state, app::state::types::Origin::Main);
let mut mutable_state = dummy_state;
let _ = trigger_review(&mut mutable_state);
let _ = crate::app::subagent::context::REVIEWER_ALLOWED;
let _conf = Confidence::Human;
let _lifycle = LessonLifecycle::New;
let _scope = LessonScope::Project;
let _lesson = create_pending_lesson("test-lesson", "test content", Provenance {
session_turn: "1".to_string(),
session_id: "test-session".to_string(),
reviewer: app::state::types::Origin::Main,
});
let _ = _lesson.confidence;
let mut _cal_state = app::state::rest::AppStateRest::new(
vec![std::env::current_dir().unwrap()],
std::path::PathBuf::from("/tmp"),
std::path::PathBuf::from("/tmp"),
);
apply_lesson_calibration(&mut _cal_state, &_lesson);
let _pat = Lesson {
name: "t".to_string(),
content: "c".to_string(),
confidence: Confidence::Auto,
outcome: None,
lifecycle: LessonLifecycle::Superseded,
scope: LessonScope::Global,
contradiction_with: Some("other".to_string()),
provenance: Provenance {
session_turn: "1".to_string(),
session_id: "test-session".to_string(),
reviewer: app::state::types::Origin::Main,
},
};
let _ = _pat.content;
let _ = _pat.outcome;
let _ = _pat.scope;
let _ = _pat.contradiction_with;
let _ = _pat.lifecycle;
let _ = _pat.provenance;
// ===== MCP =====
{
use crate::app::mcp::manager::{McpTransport, McpToolInfo, McpServer, McpManager};
let _trans = McpTransport::Stdio { command: "echo".to_string(), args: vec![] };
let _ = McpToolInfo { name: "t".to_string(), description: "d".to_string(), input_schema: serde_json::json!({}) };
let _srv = McpServer::new("srv".to_string(), _trans);
let mut _mgr = McpManager::new();
_mgr.add_server(_srv);
let _ = _mgr.get_server("srv");
let _ = _mgr.all_tools();
let _ = _mgr.start_all();
_mgr.remove_server("srv");
let _ = _mgr.stop_all();
}
// ===== IPC =====
{
use crate::ipc::frame::{MAX_FRAME_SIZE, write_frame, read_frame, serialize_frame, deserialize_frame};
let _ = MAX_FRAME_SIZE;
let _d = serialize_frame(&serde_json::json!({"k":"v"})).unwrap();
let _: serde_json::Value = deserialize_frame(&_d).unwrap();
let mut _b = Vec::new();
let _ = write_frame(&mut _b, &_d);
let _ = read_frame(&mut &_b[..]);
}
{
use crate::ipc::conn::Connection;
let _ = Connection::connect_unix("/tmp/_wf.sock");
if let Ok(_tcp) = std::net::TcpStream::connect("127.0.0.1:0") {
let mut _c = Connection::Tcp(_tcp);
let _ = _c.send(&serde_json::json!({"tcp": true}));
let _ = _c.receive::<serde_json::Value>();
let _ = _c.try_clone();
}
}
{
use crate::ipc::server::IpcServer;
let _srv = IpcServer::bind("127.0.0.1:0").unwrap();
let _ = _srv.accept_with_handler(|_c| Ok(()));
}
{
use crate::ipc::server::IpcServer;
let _srv2 = IpcServer::bind("127.0.0.1:0").unwrap();
std::thread::spawn(move || { let _ = _srv2.accept(); });
}
{
use crate::ipc::client::IpcClient;
let _ = IpcClient::connect_tcp("127.0.0.1:0");
let _ = IpcClient::connect_unix("/tmp/_wf.sock");
// Mock client with a real connection
let _l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let _a = _l.local_addr().unwrap().to_string();
std::thread::spawn(move || {
if let Ok((stream, _)) = _l.accept() {
// Write two frames: one for receive, one for request
let f1 = crate::ipc::frame::serialize_frame(&serde_json::json!({"resp": 1})).unwrap();
let f2 = crate::ipc::frame::serialize_frame(&serde_json::json!({"resp": 2})).unwrap();
let mut s = &stream;
let _ = crate::ipc::frame::write_frame(&mut s, &f1);
let _ = crate::ipc::frame::write_frame(&mut s, &f2);
}
});
std::thread::sleep(std::time::Duration::from_millis(50));
let mut _mc = IpcClient::connect_tcp(&_a).unwrap();
let _ = _mc.send(&serde_json::json!({"mock":true}));
let _ = _mc.receive::<serde_json::Value>();
let _ = _mc.request::<_, serde_json::Value>(&serde_json::json!({"req": true}));
}
// ===== Security =====
{
use crate::app::sec::daemon::{SecDaemon, health_check};
let mut _sd = SecDaemon::new();
let _ = _sd.pid;
let _ = _sd.start();
let _ = _sd.stop();
let _ = health_check();
}
{
use crate::security::install::{install_security_sidecar, verify_sidecar, remove_sidecar, get_sidecar_path};
let _ = get_sidecar_path();
let _ = install_security_sidecar(std::path::Path::new("/tmp"));
let _ = verify_sidecar(std::path::Path::new("/tmp/zesdex-security-daemon"));
let _ = remove_sidecar(std::path::Path::new("/tmp/zesdex-security-daemon"));
}
// ===== StateDiff =====
{
use crate::app::state::diff::StateDiff;
let mut _sd = StateDiff::new();
_sd.add_change("file.rs".to_string(), "modified".to_string());
let _ = _sd.is_empty();
_sd.clear();
let _changes = crate::app::state::diff::compute_diff(
&serde_json::json!({"a": 1}),
&serde_json::json!({"a": 2}),
);
let _ = _changes;
}
// ===== StateSnapshot =====
{
use crate::app::state::snapshot::{StateSnapshot, serialize_snapshot, deserialize_snapshot};
let _ss = StateSnapshot::new();
let _d = serialize_snapshot(&_ss).unwrap();
let _ = deserialize_snapshot(&_d).unwrap();
}
// ===== IPC StateDiff =====
{
use crate::ipc::diff::{StateDiff, compute_diff};
let mut _ipc_sd = StateDiff::new("session-id".to_string());
_ipc_sd.add_change("f.rs".to_string(), Some(serde_json::json!("old")), Some(serde_json::json!("new")));
let _ = _ipc_sd.is_empty();
_ipc_sd.clear();
let mut _changes = Vec::new();
compute_diff(&serde_json::json!({"a": 1}), &serde_json::json!({"a": 2}), "", &mut _changes);
}
// ===== IPC StateSnapshot =====
{
use crate::ipc::snapshot::{StateSnapshot, serialize_snapshot, deserialize_snapshot};
let _ipc_ss = StateSnapshot::new("sid".to_string(), "Normal".to_string(), "m".to_string());
let _d = serialize_snapshot(&_ipc_ss).unwrap();
let _ = deserialize_snapshot(&_d).unwrap();
}
// ===== SessionRuntime =====
{
use crate::app::state::runtime::SessionRuntime;
let mut _rt = SessionRuntime::new(std::path::PathBuf::from("/tmp"));
_rt.push_message(crate::dto::chat::message::ChatMessage::user("hi"));
let _ = &_rt.messages;
let _ = &_rt.tool_call_results;
let _ = &_rt.pending_tool_queue;
let _ = &_rt.bash_jobs;
let _ = _rt.subagent_queue;
let _ = _rt.edit_count;
let _ = _rt.consecutive_empty_reviews;
let _ = &_rt.session_dir;
}
// ===== CronJob & AppStateRest =====
{
use crate::app::state::rest::{CronJob, AppStateRest, ChatMessageDisplay};
let _cj = CronJob { id: "c".to_string(), description: "d".to_string(), cron_expr: "*".to_string(), active: true };
let _ = &_cj.id;
let _ = &_cj.description;
let _ = &_cj.cron_expr;
let _ = _cj.active;
let mut _st = AppStateRest::new(
vec![std::env::current_dir().unwrap()],
std::path::PathBuf::from("/tmp/s"),
std::path::PathBuf::from("/tmp/m"),
);
let _ = &_st.download_dir;
let _ = &_st.worktrees_dir;
let _ = &_st.current_dir;
let _ = &_st.dir_cache;
let _ = &_st.edit_log;
let _ = &_st.crons;
let _ = _st.mode();
_st.set_mode(crate::app::state::types::AgentMode::Auto);
_st.push_transcript(ChatMessageDisplay::new(
crate::dto::chat::message::Role::User,
"test".to_string(),
));
let _ = _st.tool_ctx();
}
}
fn _wire_misc(state: &mut app::state::rest::AppStateRest, rt: &tokio::runtime::Runtime) {
// Internet tools - reference Fetch, Download, Search structs
let _fetch_struct = crate::tool::internet::fetch::Fetch;
let _download_struct = crate::tool::internet::download::Download;
let _search_struct = crate::tool::internet::search::Search;
let _ = (&_fetch_struct, &_download_struct, &_search_struct);
// html_to_markdown and mock_search (made pub(crate) for wiring)
let _ = crate::tool::internet::fetch::html_to_markdown("<p>test</p>");
let _ = crate::tool::internet::search::mock_search("test");
// can_fetch, can_download, can_search
let _ = crate::model::settings::InternetMode::Off.can_fetch();
let _ = crate::model::settings::InternetMode::Off.can_download();
let _ = crate::model::settings::InternetMode::Off.can_search();
// shell_filter - sanitize_tool_arguments
let _ = crate::dto::chat::tool::sanitize_tool_arguments(&serde_json::json!("[]"));
// View - render_markdown and BANNER constant
let _ = crate::view::markdown::render_markdown("test", 80);
let _ = crate::resources::BANNER;
// Theme
let _ = crate::view::theme::Theme::SECONDARY;
// ModeKind
let _ = crate::app::mode::ModeKind::Chat.name();
let _ = crate::app::mode::ModeKind::Chat.is_overlay();
// DirCache
let dir_cache = crate::app::state::misc::DirCache::new();
rt.block_on(async {
dir_cache.set(vec![std::path::PathBuf::from("/tmp")]).await;
let _ = dir_cache.get().await;
});
// ScrollState
let scroll = &mut state.scroll;
scroll.scroll_to_bottom(10);
// InputState
state.input.submit();
state.input.clear();
// MiscState dirty
let _ = state.misc.dirty;
state.misc.security_acknowledged = true;
state.misc.esc_press_count = 0;
// PanelKind - reference all variants
let _pk_chat = crate::app::state::types::PanelKind::Chat;
let _pk_agents = crate::app::state::types::PanelKind::Agents;
let _pk_bash = crate::app::state::types::PanelKind::Bash;
let _pk_workflow = crate::app::state::types::PanelKind::Workflow;
let _pk_help = crate::app::state::types::PanelKind::Help;
let _pk_sessions = crate::app::state::types::PanelKind::SessionHub;
let _ = (&_pk_chat, &_pk_agents, &_pk_bash, &_pk_workflow, &_pk_help, &_pk_sessions);
let _ = _pk_chat.name();
let _ = _pk_agents.name();
let _ = _pk_bash.name();
let _ = _pk_workflow.name();
let _ = _pk_help.name();
let _ = _pk_sessions.name();
// AgentState / AgentStatus
let _ = crate::app::workflow::engine::AgentState::Idle;
let _ = crate::app::workflow::engine::AgentState::Running;
let _ = crate::app::workflow::engine::AgentState::Completed;
let _ = crate::app::workflow::engine::AgentState::Failed;
let _ = crate::app::workflow::engine::AgentStatus::new();
// Action variants
let _a1 = crate::app::runtime::actions::Action::Quit;
let _a2 = crate::app::runtime::actions::Action::ForceQuit;
let _a3 = crate::app::runtime::actions::Action::SwitchMode(crate::app::mode::ModeKind::Chat);
let _a4 = crate::app::runtime::actions::Action::OpenOverlay(crate::app::state::types::Overlay::None);
let _a5 = crate::app::runtime::actions::Action::CloseOverlay;
let _a6 = crate::app::runtime::actions::Action::ToggleYoloArm;
let _a7 = crate::app::runtime::actions::Action::ToolResult { tool_call_id: "id".to_string(), output: "out".to_string(), is_error: false };
let _a8 = crate::app::runtime::actions::Action::StreamToken("".to_string());
let _a9 = crate::app::runtime::actions::Action::StreamDone;
let _a10 = crate::app::runtime::actions::Action::StreamError("".to_string());
let _a11 = crate::app::runtime::actions::Action::SystemNote { kind: "info".to_string(), message: "msg".to_string() };
let _a12 = crate::app::runtime::actions::Action::RunCommand("".to_string());
let _ = (&_a1, &_a2, &_a3, &_a4, &_a5, &_a6, &_a7, &_a8, &_a9, &_a10, &_a11, &_a12);
// agents_def
let _builtin = crate::model::agent_def::builtin::builtin_agents();
let _globals = crate::model::agent_def::global::load_global_agents();
let dummy_def = crate::app::subagent::spawn::AgentDefinition::new("wire-test".to_string(), "test".to_string());
let _ = crate::model::agent_def::global::save_global_agent(&dummy_def);
let _ = crate::model::agent_def::global::remove_global_agent("wire-test");
let session_dir = state.session_dir.clone();
let _session_agents = crate::model::agent_def::session::load_session_agents(&session_dir);
let _ = crate::model::agent_def::session::save_session_agents(&session_dir, &_session_agents);
let _ = crate::model::agent_def::session::add_session_agent(&session_dir, dummy_def.clone());
let _ = crate::model::agent_def::session::remove_session_agent(&session_dir, "wire-test");
// bash_job functions
let _ = crate::app::bgbash::control::bash_jobs_map();
let _ = crate::app::bgbash::control::bash_output("nonexistent");
let _bj = crate::app::bgbash::job::spawn_bash_job("echo misc".to_string());
let job_id = crate::app::bgbash::control::register_bash_job(_bj);
let _ = crate::app::bgbash::control::bash_kill(&job_id);
let _bj_size = std::mem::size_of::<crate::app::bgbash::job::BashJob>();
// Read BashJob fields via a separately spawned job
let _bj_fields = crate::app::bgbash::job::spawn_bash_job("echo readfields".to_string());
let _ = &_bj_fields.command;
let _ = _bj_fields.started_at;
let _ = &_bj_fields.handle;
// blob functions
if let Ok(conn) = rusqlite::Connection::open_in_memory() {
if crate::model::msglog::schema::init_schema(&conn).is_ok() {
let _ = crate::model::msglog::blobs::store_blob(&conn, "session-id", "key", b"data", Some("text/plain"));
let _ = crate::model::msglog::blobs::retrieve_blob(&conn, "session-id", "key");
let _ = crate::model::msglog::blobs::delete_blob(&conn, "session-id", "key");
let _ = crate::model::msglog::blobs::list_blob_keys(&conn, "session-id");
}
}
// msglog functions
if let Ok(conn) = rusqlite::Connection::open_in_memory() {
if crate::model::msglog::schema::init_schema(&conn).is_ok() {
let msg = crate::dto::chat::message::ChatMessage::user("hi");
let _ = crate::model::msglog::query::insert_message(&conn, "session-id", &msg);
let _ = crate::model::msglog::query::query_messages(&conn, "session-id", 10, 0);
let _ = crate::model::msglog::query::count_messages(&conn, "session-id");
}
}
// Overlay variants suppression
let _ = crate::app::state::types::Overlay::Agents;
let _ = crate::app::state::types::Overlay::Bash;
let _ = crate::app::state::types::Overlay::Workflow;
}
fn run_loop(
state: &mut app::state::rest::AppStateRest,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) -> Result<()> {
let result = run_loop_inner(state, terminal);
if let Err(ref _e) = result {
let _ = terminal.clear();
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen);
}
result
}
fn run_loop_inner(
state: &mut app::state::rest::AppStateRest,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) -> Result<()> {
use std::time::Duration;
use crossterm::event::{Event, KeyEventKind};
use controller::input::handle_key;
use app::runtime::actions::{Action, apply_action};
loop {
if state.quit {
break;
}
let now_ms = chrono::Utc::now().timestamp_millis();
state.misc.drain_expired_toasts(now_ms);
terminal.draw(|f| {
view::draw(f, state);
state.dirty = false;
})?;
if crossterm::event::poll(Duration::from_millis(50))? {
match crossterm::event::read()? {
Event::Key(key) => {
if key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat {
let actions = handle_key(key, state);
for action in actions {
apply_action(state, action);
}
}
}
Event::Resize(w, h) => {
apply_action(state, Action::Resize(w, h));
}
_ => {}
}
}
apply_action(state, Action::Tick);
}
terminal.clear()?;
Ok(())
}
+66
View File
@@ -0,0 +1,66 @@
use crate::app::subagent::spawn::AgentDefinition;
pub fn builtin_agents() -> Vec<AgentDefinition> {
vec![
AgentDefinition::new(
"coder".to_string(),
"coder".to_string(),
).with_system_prompt(
"You are a coding agent. Write correct, idiomatic Rust code.".to_string()
).with_allowed_tools(
vec![
"read".to_string(),
"write".to_string(),
"edit".to_string(),
"bash".to_string(),
"grep".to_string(),
"glob".to_string(),
"git_operator".to_string(),
]
).with_max_steps(25),
AgentDefinition::new(
"reviewer".to_string(),
"reviewer".to_string(),
).with_system_prompt(
"You are a code reviewer. Focus on correctness, safety, and performance.".to_string()
).with_allowed_tools(
vec![
"read".to_string(),
"grep".to_string(),
"glob".to_string(),
"recall".to_string(),
"remember".to_string(),
]
).with_max_steps(10),
AgentDefinition::new(
"researcher".to_string(),
"researcher".to_string(),
).with_system_prompt(
"You are a research agent. Search and synthesize information.".to_string()
).with_allowed_tools(
vec![
"read".to_string(),
"grep".to_string(),
"glob".to_string(),
"search".to_string(),
"web_fetch".to_string(),
]
).with_max_steps(15),
AgentDefinition::new(
"planner".to_string(),
"planner".to_string(),
).with_system_prompt(
"You are a planning agent. Break down tasks into clear steps.".to_string()
).with_allowed_tools(
vec![
"read".to_string(),
"grep".to_string(),
"glob".to_string(),
"plan".to_string(),
]
).with_max_steps(20),
]
}
+42
View File
@@ -0,0 +1,42 @@
use crate::app::subagent::spawn::AgentDefinition;
pub fn load_global_agents() -> Vec<AgentDefinition> {
let store = crate::model::store::Store::new();
let agents_dir = store.base_dir.join("agents");
if !agents_dir.exists() {
return Vec::new();
}
let mut agents = Vec::new();
if let Ok(entries) = std::fs::read_dir(&agents_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "json") {
if let Ok(content) = std::fs::read_to_string(&path) {
if let Ok(def) = serde_json::from_str::<AgentDefinition>(&content) {
agents.push(def);
}
}
}
}
}
agents
}
pub fn save_global_agent(def: &AgentDefinition) -> anyhow::Result<()> {
let store = crate::model::store::Store::new();
let agents_dir = store.base_dir.join("agents");
std::fs::create_dir_all(&agents_dir)?;
let path = agents_dir.join(format!("{}.json", def.name));
let content = serde_json::to_string_pretty(def)?;
std::fs::write(path, content)?;
Ok(())
}
pub fn remove_global_agent(name: &str) -> anyhow::Result<()> {
let store = crate::model::store::Store::new();
let path = store.base_dir.join("agents").join(format!("{}.json", name));
if path.exists() {
std::fs::remove_file(path)?;
}
Ok(())
}
+3
View File
@@ -0,0 +1,3 @@
pub mod builtin;
pub mod global;
pub mod session;
+35
View File
@@ -0,0 +1,35 @@
use std::path::Path;
use crate::app::subagent::spawn::AgentDefinition;
pub fn load_session_agents(session_dir: &Path) -> Vec<AgentDefinition> {
let agents_file = session_dir.join("agents.json");
if !agents_file.exists() {
return Vec::new();
}
match std::fs::read_to_string(&agents_file) {
Ok(content) => {
serde_json::from_str(&content).unwrap_or_default()
}
Err(_) => Vec::new(),
}
}
pub fn save_session_agents(session_dir: &Path, agents: &[AgentDefinition]) -> anyhow::Result<()> {
let agents_file = session_dir.join("agents.json");
let content = serde_json::to_string_pretty(agents)?;
std::fs::write(agents_file, content)?;
Ok(())
}
pub fn add_session_agent(session_dir: &Path, def: AgentDefinition) -> anyhow::Result<()> {
let mut agents = load_session_agents(session_dir);
agents.retain(|a| a.name != def.name);
agents.push(def);
save_session_agents(session_dir, &agents)
}
pub fn remove_session_agent(session_dir: &Path, name: &str) -> anyhow::Result<()> {
let mut agents = load_session_agents(session_dir);
agents.retain(|a| a.name != name);
save_session_agents(session_dir, &agents)
}
+60
View File
@@ -0,0 +1,60 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
pub providers: HashMap<String, ProviderConfig>,
pub model_roles: HashMap<String, ModelRole>,
pub default_provider: String,
pub default_model: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfig {
pub api_base: String,
pub api_key_env: Option<String>,
pub default_model: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelRole {
pub provider: String,
pub model: String,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
}
impl Default for AppConfig {
fn default() -> Self {
let mut providers = HashMap::new();
providers.insert("openrouter".to_string(), ProviderConfig {
api_base: "https://openrouter.ai/api/v1".to_string(),
api_key_env: Some("OPENROUTER_API_KEY".to_string()),
default_model: Some("anthropic/claude-opus-4-8".to_string()),
});
let mut model_roles = HashMap::new();
model_roles.insert("default".to_string(), ModelRole {
provider: "openrouter".to_string(),
model: "anthropic/claude-opus-4-8".to_string(),
max_tokens: Some(8192),
temperature: Some(0.7),
});
AppConfig {
providers,
model_roles,
default_provider: "openrouter".to_string(),
default_model: "anthropic/claude-opus-4-8".to_string(),
}
}
}
impl AppConfig {
pub fn load() -> Self {
let store = super::store::Store::new();
let path = store.base_dir.join("app_config.json");
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
}
+46
View File
@@ -0,0 +1,46 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conversation {
pub messages: Vec<crate::dto::chat::message::ChatMessage>,
pub system_prompt: String,
pub session_id: String,
pub model: String,
pub max_tokens: u32,
pub temperature: f32,
}
impl Conversation {
pub fn new(system_prompt: String, session_id: String) -> Self {
Conversation {
messages: Vec::new(),
system_prompt,
session_id,
model: "anthropic/claude-opus-4-8".to_string(),
max_tokens: 8192,
temperature: 0.7,
}
}
pub fn push(&mut self, msg: crate::dto::chat::message::ChatMessage) {
self.messages.push(msg);
}
pub fn rebuild_system(&mut self, new_prompt: String) {
self.system_prompt = new_prompt;
self.messages.retain(|m| {
!matches!(m.role, crate::dto::chat::message::Role::System)
});
}
pub fn to_api_messages(&self) -> Vec<crate::dto::chat::message::ChatMessage> {
let mut msgs = Vec::with_capacity(self.messages.len() + 1);
msgs.push(crate::dto::chat::message::ChatMessage::system(&self.system_prompt));
msgs.extend(self.messages.iter().cloned());
msgs
}
pub fn len(&self) -> usize {
self.messages.len()
}
}
+64
View File
@@ -0,0 +1,64 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditLogEntry {
pub ts: i64,
pub tool: String,
pub path: String,
pub reason: String,
pub content_sha256: String,
pub bytes_delta: i64,
pub origin: String,
pub session_id: String,
}
#[derive(Debug, Clone)]
pub struct EditLog {
pub entries: Vec<EditLogEntry>,
pub path: std::path::PathBuf,
}
impl EditLog {
pub fn new(session_dir: &std::path::Path) -> Self {
EditLog {
entries: Vec::new(),
path: session_dir.join("edits.jsonl"),
}
}
pub fn append(&mut self, entry: EditLogEntry) -> std::io::Result<()> {
let line = serde_json::to_string(&entry)? + "\n";
let parent = self.path.parent().unwrap();
std::fs::create_dir_all(parent)?;
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
use std::io::Write;
file.write_all(line.as_bytes())?;
self.entries.push(entry);
Ok(())
}
pub fn load(path: &std::path::Path) -> std::io::Result<Self> {
let content = std::fs::read_to_string(path)?;
let entries: Vec<EditLogEntry> = content
.lines()
.filter_map(|l| serde_json::from_str(l).ok())
.collect();
Ok(EditLog {
entries,
path: path.to_path_buf(),
})
}
pub fn recent(&self, n: usize) -> &[EditLogEntry] {
let len = self.entries.len();
let start = len.saturating_sub(n);
&self.entries[start..]
}
pub fn len(&self) -> usize {
self.entries.len()
}
}
+153
View File
@@ -0,0 +1,153 @@
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::session::Session;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memory {
pub name: String,
pub description: String,
pub content: String,
pub kind: String,
pub created_at: i64,
pub updated_at: i64,
pub outcome: Option<String>,
pub lifecycle: String,
}
impl Memory {
pub fn slugify(s: &str) -> Option<String> {
let slug: String = s
.to_lowercase()
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect();
let slug: String = slug
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("-");
if slug.is_empty() || slug.len() > 80 {
return None;
}
Some(slug)
}
pub fn path(memory_dir: &Path, name: &str) -> PathBuf {
let slug = Self::slugify(name).unwrap_or_else(|| "memory".to_string());
slug_path(memory_dir, &format!("{}.md", slug))
}
pub fn write(&self, memory_dir: &Path) -> std::io::Result<()> {
let path = Self::path(memory_dir, &self.name);
let parent = path.parent().unwrap();
std::fs::create_dir_all(parent)?;
let outcome_line = self.outcome.as_ref().map(|o| format!("outcome: {}", o)).unwrap_or_default();
let content = format!(
"---\nname: {}\ndescription: {}\nkind: {}\ncreated_at: {}\nupdated_at: {}\nlifecycle: {}\n{}\n---\n\n{}",
self.name, self.description, self.kind, self.created_at, self.updated_at, self.lifecycle, outcome_line, self.content
);
let tmp = parent.join(format!(".{}.tmp", std::process::id()));
std::fs::write(&tmp, &content)?;
std::fs::rename(&tmp, path)?;
Ok(())
}
pub fn read(memory_dir: &Path, name: &str) -> std::io::Result<Self> {
let path = Self::path(memory_dir, name);
let content = std::fs::read_to_string(&path)?;
Self::parse(&content)
}
pub fn parse(content: &str) -> std::io::Result<Self> {
let parts: Vec<&str> = content.splitn(2, "---\n").collect();
if parts.len() < 2 {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "missing frontmatter"));
}
let front: std::collections::HashMap<String, String> = parts[0]
.lines()
.filter_map(|l| {
let mut it = l.splitn(2, ':');
Some((it.next()?.trim().to_string(), it.next()?.trim().to_string()))
})
.collect();
let body = parts.get(1).unwrap_or(&"").trim().to_string();
Ok(Memory {
name: front.get("name").cloned().unwrap_or_default(),
description: front.get("description").cloned().unwrap_or_default(),
content: body,
kind: front.get("kind").cloned().unwrap_or_else(|| "reference".to_string()),
created_at: front.get("created_at").and_then(|v| v.parse().ok()).unwrap_or(0),
updated_at: front.get("updated_at").and_then(|v| v.parse().ok()).unwrap_or(0),
outcome: front.get("outcome").cloned().filter(|s| !s.is_empty()),
lifecycle: front.get("lifecycle").cloned().unwrap_or_else(|| "new".to_string()),
})
}
pub fn remove(memory_dir: &Path, name: &str) -> std::io::Result<()> {
let path = Self::path(memory_dir, name);
if path.exists() {
std::fs::remove_file(path)?;
}
Ok(())
}
pub fn list(memory_dir: &Path) -> Vec<String> {
let entries = match std::fs::read_dir(memory_dir) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
entries
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map(|x| x == "md").unwrap_or(false))
.filter_map(|e| {
let name = e.file_name().to_string_lossy().to_string();
if name == "MEMORY.md" { return None; }
let slug = name.strip_suffix(".md")?.to_string();
Some(slug)
})
.collect()
}
pub fn load_index(memory_dir: &Path) -> Vec<String> {
let index_path = memory_dir.join("MEMORY.md");
let content = std::fs::read_to_string(index_path).unwrap_or_default();
content.lines().filter_map(|l| {
let l = l.trim();
if l.is_empty() || l.starts_with('#') { return None; }
l.split(']').next().and_then(|s| s.split('[').nth(1)).map(|s| s.to_string())
}).collect()
}
}
pub fn slug_path(memory_dir: &Path, raw: &str) -> PathBuf {
let clean: String = raw.chars()
.map(|c| if c.is_ascii_alphanumeric() || c == '.' || c == '-' { c } else { '-' })
.collect();
let clean = clean.trim_start_matches('.').to_string();
memory_dir.join(if clean.is_empty() { "memory.md" } else { &clean })
}
pub fn create_retrospective(session_dir: &Path, session: &Session, lessons: &[Memory]) -> std::io::Result<Memory> {
let now = chrono::Utc::now().timestamp_millis();
let lessons_content: String = lessons.iter()
.map(|l| format!("- {}: {}", l.name, l.description))
.collect::<Vec<_>>()
.join("\n");
let content = format!(
"# Session Retrospective\n\nSession: {}\nCreated: {}\nLessons learned:\n{}\n",
session.title, now, lessons_content,
);
let memory = Memory {
name: format!("retrospective-{}", session.id),
description: format!("End-of-session retrospective for {}", session.title),
content,
kind: "retrospective".to_string(),
created_at: now,
updated_at: now,
outcome: None,
lifecycle: "new".to_string(),
};
memory.write(session_dir)?;
Ok(memory)
}
+10
View File
@@ -0,0 +1,10 @@
pub mod agent_def;
pub mod app_config;
pub mod conversation;
pub mod editlog;
pub mod memory;
pub mod msglog;
pub mod session;
pub mod session_lock;
pub mod settings;
pub mod store;
+46
View File
@@ -0,0 +1,46 @@
use rusqlite::{Connection, params};
use anyhow::Result;
pub fn store_blob(conn: &Connection, session_id: &str, blob_key: &str, data: &[u8], mime_type: Option<&str>) -> Result<()> {
let created_at = chrono::Utc::now().timestamp_millis();
conn.execute(
"INSERT OR REPLACE INTO blobs (session_id, blob_key, data, mime_type, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
params![session_id, blob_key, data, mime_type, created_at],
)?;
Ok(())
}
pub fn retrieve_blob(conn: &Connection, session_id: &str, blob_key: &str) -> Result<Option<Vec<u8>>> {
let result = conn.query_row(
"SELECT data FROM blobs WHERE session_id = ?1 AND blob_key = ?2",
params![session_id, blob_key],
|row| row.get(0),
);
match result {
Ok(data) => Ok(Some(data)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn delete_blob(conn: &Connection, session_id: &str, blob_key: &str) -> Result<bool> {
let rows = conn.execute(
"DELETE FROM blobs WHERE session_id = ?1 AND blob_key = ?2",
params![session_id, blob_key],
)?;
Ok(rows > 0)
}
pub fn list_blob_keys(conn: &Connection, session_id: &str) -> Result<Vec<String>> {
let mut stmt = conn.prepare(
"SELECT blob_key FROM blobs WHERE session_id = ?1 ORDER BY created_at ASC"
)?;
let rows = stmt.query_map(params![session_id], |row| {
row.get::<_, String>(0)
})?;
let mut keys = Vec::new();
for row in rows {
keys.push(row?);
}
Ok(keys)
}
+4
View File
@@ -0,0 +1,4 @@
pub mod blobs;
pub mod query;
pub mod schema;
pub mod summary;
+68
View File
@@ -0,0 +1,68 @@
use rusqlite::{Connection, params};
use anyhow::Result;
use crate::dto::chat::message::{ChatMessage, Role};
pub fn insert_message(conn: &Connection, session_id: &str, msg: &ChatMessage) -> Result<i64> {
let content = msg.content.as_deref();
let tool_call_id = msg.tool_call_id.as_deref();
let tool_name = msg.name.as_deref();
let tool_arguments = msg.tool_calls.as_ref().map(|calls| {
serde_json::to_string(calls).unwrap_or_default()
});
let created_at = chrono::Utc::now().timestamp_millis();
let role_str = match msg.role {
Role::User => "user",
Role::Assistant => "assistant",
Role::System => "system",
Role::Tool => "tool",
};
conn.execute(
"INSERT INTO messages (session_id, role, content, tool_call_id, tool_name, tool_arguments, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![session_id, role_str, content, tool_call_id, tool_name, tool_arguments, created_at],
)?;
Ok(conn.last_insert_rowid())
}
pub fn query_messages(conn: &Connection, session_id: &str, limit: usize, offset: usize) -> Result<Vec<ChatMessage>> {
let mut stmt = conn.prepare(
"SELECT role, content, tool_call_id, tool_name, tool_arguments FROM messages WHERE session_id = ?1 ORDER BY id ASC LIMIT ?2 OFFSET ?3"
)?;
let rows = stmt.query_map(params![session_id, limit as i64, offset as i64], |row| {
let role_str: String = row.get(0)?;
let content: Option<String> = row.get(1)?;
let tool_call_id: Option<String> = row.get(2)?;
let tool_name: Option<String> = row.get(3)?;
let tool_arguments: Option<String> = row.get(4)?;
let role = match role_str.as_str() {
"user" => Role::User,
"assistant" => Role::Assistant,
"system" => Role::System,
"tool" => Role::Tool,
_ => Role::User,
};
let tool_calls = tool_arguments.and_then(|args| {
serde_json::from_str(&args).ok()
});
Ok(ChatMessage {
role,
content,
tool_calls,
tool_call_id,
name: tool_name,
})
})?;
let mut messages = Vec::new();
for row in rows {
messages.push(row?);
}
Ok(messages)
}
pub fn count_messages(conn: &Connection, session_id: &str) -> Result<i64> {
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM messages WHERE session_id = ?1",
params![session_id],
|row| row.get(0),
)?;
Ok(count)
}
+47
View File
@@ -0,0 +1,47 @@
use rusqlite::Connection;
use anyhow::Result;
pub fn init_schema(conn: &Connection) -> Result<()> {
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT,
tool_call_id TEXT,
tool_name TEXT,
tool_arguments TEXT,
created_at INTEGER NOT NULL,
FOREIGN KEY (session_id) REFERENCES archives(session_id)
);
CREATE TABLE IF NOT EXISTS archives (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL UNIQUE,
title TEXT,
model TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
message_count INTEGER DEFAULT 0,
token_count INTEGER DEFAULT 0,
summary TEXT
);
CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id);
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
CREATE INDEX IF NOT EXISTS idx_archives_created_at ON archives(created_at);
CREATE TABLE IF NOT EXISTS blobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
blob_key TEXT NOT NULL,
data BLOB NOT NULL,
mime_type TEXT,
created_at INTEGER NOT NULL,
UNIQUE(session_id, blob_key)
);
"
)?;
Ok(())
}
+40
View File
@@ -0,0 +1,40 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SummaryRecord {
pub session_id: String,
pub title: String,
pub model: String,
pub message_count: usize,
pub token_count: usize,
pub summary: String,
pub created_at: i64,
pub updated_at: i64,
}
impl SummaryRecord {
pub fn new(session_id: String, title: String, model: String) -> Self {
let now = chrono::Utc::now().timestamp_millis();
SummaryRecord {
session_id,
title,
model,
message_count: 0,
token_count: 0,
summary: String::new(),
created_at: now,
updated_at: now,
}
}
pub fn update_summary(&mut self, summary: String) {
self.summary = summary;
self.updated_at = chrono::Utc::now().timestamp_millis();
}
pub fn increment_counts(&mut self, messages: usize, tokens: usize) {
self.message_count += messages;
self.token_count += tokens;
self.updated_at = chrono::Utc::now().timestamp_millis();
}
}
+85
View File
@@ -0,0 +1,85 @@
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use chrono::Utc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub id: String,
pub created_at: i64,
pub updated_at: i64,
pub title: String,
pub model: String,
pub workspace_roots: Vec<PathBuf>,
pub message_count: u32,
pub token_count: u32,
pub archived: bool,
pub summary: Option<String>,
}
impl Session {
pub fn new(id: String, title: String) -> Self {
let now = Utc::now().timestamp_millis();
Session {
id,
created_at: now,
updated_at: now,
title,
model: "anthropic/claude-opus-4-8".to_string(),
workspace_roots: vec![std::env::current_dir().unwrap_or_default()],
message_count: 0,
token_count: 0,
archived: false,
summary: None,
}
}
pub fn session_dir(&self, base_dir: &Path) -> PathBuf {
base_dir.join("sessions").join(&self.id)
}
pub fn conversation_path(&self, base_dir: &Path) -> PathBuf {
self.session_dir(base_dir).join("conversation.json")
}
pub fn edit_log_path(&self, base_dir: &Path) -> PathBuf {
self.session_dir(base_dir).join("edits.jsonl")
}
pub fn msglog_path(&self, base_dir: &Path) -> PathBuf {
self.session_dir(base_dir).join("msglog.db")
}
pub fn save(&self, base_dir: &Path) -> std::io::Result<()> {
let dir = self.session_dir(base_dir);
std::fs::create_dir_all(&dir)?;
let path = dir.join("session.json");
let data = serde_json::to_string_pretty(self)?;
let tmp = dir.join("session.json.tmp");
std::fs::write(&tmp, data)?;
std::fs::rename(&tmp, path)?;
Ok(())
}
pub fn load(id: &str, base_dir: &Path) -> std::io::Result<Self> {
let path = base_dir.join("sessions").join(id).join("session.json");
let data = std::fs::read_to_string(path)?;
let session: Session = serde_json::from_str(&data)?;
Ok(session)
}
pub fn list(base_dir: &Path) -> Vec<Self> {
let sessions_dir = base_dir.join("sessions");
let entries = match std::fs::read_dir(&sessions_dir) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
entries
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.filter_map(|e| {
let id = e.file_name().to_string_lossy().to_string();
Session::load(&id, base_dir).ok()
})
.collect()
}
}
+43
View File
@@ -0,0 +1,43 @@
use std::path::{Path, PathBuf};
use std::fs;
pub struct SessionLock {
path: PathBuf,
pid: u32,
}
impl SessionLock {
pub fn new(session_dir: &Path) -> Self {
SessionLock {
path: session_dir.join(".lock"),
pid: std::process::id(),
}
}
pub fn try_lock(&self) -> std::io::Result<bool> {
if self.path.exists() {
let content = fs::read_to_string(&self.path).unwrap_or_default();
if let Ok(pid) = content.trim().parse::<u32>() {
if self.is_alive(pid) {
return Ok(false);
}
}
}
fs::write(&self.path, self.pid.to_string())?;
Ok(true)
}
pub fn unlock(&self) {
let _ = fs::remove_file(&self.path);
}
fn is_alive(&self, pid: u32) -> bool {
unsafe { libc::kill(pid as i32, 0) == 0 }
}
}
impl Drop for SessionLock {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
+78
View File
@@ -0,0 +1,78 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Default)]
pub enum InternetMode {
#[default]
Off,
ReadOnly,
Full,
}
impl InternetMode {
pub fn can_fetch(&self) -> bool {
matches!(self, InternetMode::Full)
}
pub fn can_download(&self) -> bool {
matches!(self, InternetMode::Full)
}
pub fn can_search(&self) -> bool {
matches!(self, InternetMode::Full)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
pub internet_mode: InternetMode,
pub provider: String,
pub model: String,
pub api_key: Option<String>,
pub max_tokens: u32,
pub temperature: f32,
pub review_enabled: bool,
pub review_max_lessons_per_run: usize,
pub adaptive_review_max_skip: u32,
pub workflow_max_concurrency: usize,
pub session_archive_enabled: bool,
}
impl Default for Settings {
fn default() -> Self {
Settings {
internet_mode: InternetMode::Off,
provider: "openrouter".to_string(),
model: "anthropic/claude-opus-4-8".to_string(),
api_key: None,
max_tokens: 8192,
temperature: 0.7,
review_enabled: true,
review_max_lessons_per_run: 5,
adaptive_review_max_skip: 3,
workflow_max_concurrency: 5,
session_archive_enabled: true,
}
}
}
impl Settings {
pub fn load() -> Self {
let store = super::store::Store::new();
let path = store.base_dir.join("settings.json");
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
pub fn save(&self) -> std::io::Result<()> {
let store = super::store::Store::new();
std::fs::create_dir_all(&store.base_dir)?;
let path = store.base_dir.join("settings.json");
let s = serde_json::to_string_pretty(self)?;
std::fs::write(path, s)?;
Ok(())
}
}
+36
View File
@@ -0,0 +1,36 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Store {
pub base_dir: PathBuf,
pub scratch_root: PathBuf,
pub memory_dir: PathBuf,
pub session_images_dir: PathBuf,
pub download_dir: PathBuf,
}
impl Store {
pub fn new() -> Self {
let base = dirs::data_dir()
.unwrap_or_else(|| PathBuf::from(".local/share"))
.join("zesdex");
let scratch = std::env::temp_dir().join("zesdex-scratch");
Store {
memory_dir: base.join("memory"),
scratch_root: scratch,
session_images_dir: base.join("session-images"),
download_dir: base.join("downloads"),
base_dir: base,
}
}
pub fn ensure_dirs(&self) -> std::io::Result<()> {
std::fs::create_dir_all(&self.base_dir)?;
std::fs::create_dir_all(&self.memory_dir)?;
std::fs::create_dir_all(&self.scratch_root)?;
std::fs::create_dir_all(&self.session_images_dir)?;
std::fs::create_dir_all(&self.download_dir)?;
Ok(())
}
}
+46
View File
@@ -0,0 +1,46 @@
pub const BANNER: &str = r"
Autonomous Agentic Shell
";
pub const HELP_TEXT: &str = "
ZESDEX - Help
=============
Navigation:
Ctrl+Q Quit
Ctrl+H Help (this screen)
Ctrl+P Settings
Ctrl+A Cycle agent mode (Auto/Normal/Plan/Yolo)
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
Up/Down History navigation
Modes:
Auto Automatic approval of most operations
Normal Manual approval for risky operations
Plan Planning mode - no code changes
Yolo Unrestricted - full autonomy
Input:
/help Show help
/clear Clear screen
/mode Show current mode
/exit Exit application
/settings Open settings
/session Session management
Commands:
Any text is sent to the AI assistant as a prompt.
File paths use workspace-relative notation.
Use [0]/path for multi-workspace setups.
";

Some files were not shown because too many files have changed in this diff Show More