Refactor IPC and DTO structures; remove unused code and streamline message handling

- Removed unused structs and methods from `response.rs`, `usage.rs`, and `client.rs`.
- Simplified `Connection` handling in `conn.rs` to only support Unix sockets.
- Updated `IpcServer` to exclusively use Unix sockets and removed TCP handling.
- Cleaned up `editlog.rs` by removing loading and recent entry methods.
- Refactored `memory.rs` to eliminate unused functions related to lesson promotion and retrospective creation.
- Enhanced `search.rs` to support multiple search providers and improved error handling.
- Updated chat view logic to simplify message display and improve user experience.
- Removed deprecated modules and constants from various files to streamline the codebase.
This commit is contained in:
asepharyana
2026-07-11 23:45:13 +07:00
parent 93d1bbb7c1
commit fcef85a327
51 changed files with 1431 additions and 1246 deletions
+139 -115
View File
@@ -15,7 +15,6 @@ const MAX_AGENT_STEPS: usize = 40;
#[derive(Debug, Clone)]
pub enum Action {
Quit,
ForceQuit,
SwitchMode(ModeKind),
SubmitInput(String),
@@ -32,19 +31,10 @@ pub enum Action {
CloseOverlay,
ToggleYoloArm,
CycleAgentMode,
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,
@@ -60,15 +50,13 @@ pub enum Action {
LessonReject {
name: String,
},
StartOAuth {
provider: String,
},
}
pub fn apply_action(state: &mut AppStateRest, action: Action) {
match action {
Action::Quit => {
save_current_session(state);
auto_create_retrospective(state);
state.quit = true;
}
Action::ForceQuit => {
save_current_session(state);
state.quit = true;
@@ -106,8 +94,8 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
if let Some(ref mut rt) = state.session_runtime {
rt.push_message(ChatMessage::user(text));
}
state.misc.thinking = true;
spawn_turn(state);
state.push_transcript(ChatMessageDisplay::new(Role::Assistant, "Thinking...".to_string()));
state.dirty = true;
}
Action::InsertChar(c) => {
@@ -137,9 +125,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
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 => {
@@ -166,51 +152,6 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
state.push_toast(toast);
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,
@@ -218,12 +159,6 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
);
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;
@@ -271,6 +206,26 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
}
state.dirty = true;
}
Action::StartOAuth { provider } => {
let turn_events = state.turn_events.clone();
let provider_clone = provider.clone();
std::thread::spawn(move || {
let result = run_oauth_flow(&provider_clone);
let message = match result {
Ok(msg) => msg,
Err(e) => format!("OAuth login failed: {}", e),
};
if let Ok(mut q) = turn_events.lock() {
q.push_back(TurnEvent::SystemNote {
kind: "oauth".to_string(),
message,
});
}
});
let toast = Toast::new(ToastKind::Info, format!("Opening browser for {} login...", provider));
state.push_toast(toast);
state.dirty = true;
}
Action::Tick => {
let now_ms = chrono::Utc::now().timestamp_millis();
state.misc.drain_expired_toasts(now_ms);
@@ -290,27 +245,17 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
for event in events {
match event {
TurnEvent::AssistantMessage(msg) => {
state.misc.thinking = false;
let display_content = msg.content.clone().unwrap_or_default();
if !display_content.is_empty() {
let replaced = if let Some(last) = state.transcript_cache.messages.last_mut() {
if last.role == Role::Assistant && last.content == "Thinking..." {
last.content = display_content.clone();
true
} else {
false
}
} else {
false
};
if !replaced {
state.push_transcript(ChatMessageDisplay::new(Role::Assistant, display_content));
}
state.push_transcript(ChatMessageDisplay::new(Role::Assistant, display_content));
}
if let Some(ref mut rt) = state.session_runtime {
rt.push_message(msg);
}
}
TurnEvent::ToolResult { tool_call_id, tool_name, output, is_error, path } => {
state.misc.thinking = false;
let display_path = path.unwrap_or_default();
let display = if tool_name == "read" {
let line_count = output.lines().count();
@@ -385,6 +330,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
turn_finished = true;
}
TurnEvent::Done => {
state.misc.thinking = false;
turn_finished = true;
}
}
@@ -454,6 +400,9 @@ fn spawn_turn(state: &AppStateRest) {
let events_q = turn_events.clone();
std::thread::spawn(move || {
let db = crate::model::msglog::open_or_create(&edit_session_dir)
.ok()
.map(|c| std::sync::Arc::new(std::sync::Mutex::new(c)));
let tc = TurnCtx {
client: crate::service::provider::LlmClient::new(api_key, model),
tdefs: tool_defs,
@@ -463,6 +412,7 @@ fn spawn_turn(state: &AppStateRest) {
workspace_roots,
edit_log_session_dir: edit_session_dir,
session_id,
db,
};
let result = run_agent_turn(tc, &messages, &events_q);
if let Err(e) = result {
@@ -485,6 +435,15 @@ struct TurnCtx {
workspace_roots: Vec<std::path::PathBuf>,
edit_log_session_dir: std::path::PathBuf,
session_id: String,
db: Option<std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>>,
}
fn archive_message(db: &Option<std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>>, session_id: &str, msg: &ChatMessage) {
if let Some(ref arc) = db {
if let Ok(conn) = arc.lock() {
let _ = crate::model::msglog::insert_message(&conn, session_id, msg);
}
}
}
fn run_agent_turn(
@@ -495,6 +454,7 @@ fn run_agent_turn(
let mut msgs = messages.to_vec();
let mut edits_this_turn = 0u32;
let mut tool_only_rounds = 0usize;
let mut prev_shaped = false;
let system_text = format!(
"{}\n\n{}",
@@ -502,20 +462,37 @@ fn run_agent_turn(
crate::resources::SYSTEM_TOOLS,
);
if !msgs.iter().any(|m| matches!(m.role, crate::dto::chat::message::Role::System)) {
msgs.insert(0, ChatMessage::system(system_text));
let sys = ChatMessage::system(system_text);
archive_message(&tc.db, &tc.session_id, &sys);
msgs.insert(0, sys);
}
for _step in 0..MAX_AGENT_STEPS {
if tool_only_rounds >= MAX_TOOL_ONLY_TURNS {
msgs.push(ChatMessage::user(
let stop_msg = ChatMessage::user(
"Stop calling tools. Respond naturally now.".to_string(),
));
);
archive_message(&tc.db, &tc.session_id, &stop_msg);
msgs.push(stop_msg);
tool_only_rounds = 0;
}
let wire_msgs = if crate::app::runtime::shortsend::should_shape(msgs.len(), prev_shaped) {
let total_chars: usize = msgs.iter()
.filter_map(|m| m.content.as_deref())
.map(|c| c.len())
.sum();
let token_estimate = total_chars / 4;
prev_shaped = true;
crate::app::runtime::shortsend::shape_messages(&msgs, token_estimate)
} else {
prev_shaped = false;
msgs.clone()
};
let response = tc
.client
.chat_with_tools(&msgs, Some(tc.tdefs.clone()))?;
.chat_with_tools(&wire_msgs, Some(tc.tdefs.clone()))?;
let has_tool_calls = response.tool_calls.is_some()
&& response.tool_calls.as_ref().is_some_and(|tc| !tc.is_empty());
@@ -524,6 +501,7 @@ fn run_agent_turn(
if has_tool_calls {
tool_only_rounds += 1;
let tool_calls = response.tool_calls.clone().unwrap_or_default();
archive_message(&tc.db, &tc.session_id, &response);
msgs.push(response);
for tool_call in tool_calls {
let tool_name = tool_call.function.name.clone();
@@ -581,10 +559,12 @@ fn run_agent_turn(
}
let tool_msg = ChatMessage::tool_result(tool_call.id.clone(), output);
archive_message(&tc.db, &tc.session_id, &tool_msg);
msgs.push(tool_msg);
}
} else {
if !content.is_empty() {
archive_message(&tc.db, &tc.session_id, &response);
if let Ok(mut q) = events_q.lock() {
q.push_back(TurnEvent::AssistantMessage(response));
}
@@ -698,36 +678,80 @@ fn save_current_session(state: &AppStateRest) {
}
}
fn auto_create_retrospective(state: &mut AppStateRest) {
if state.session_runtime.is_none() {
return;
}
let session = crate::model::session::Session::new(
state.session_id.clone(),
"session".to_string(),
);
match crate::model::memory::auto_create_retrospective(&state.session_dir, &session) {
Ok(Some(retro)) => {
state.push_toast(crate::app::state::types::Toast::new(
crate::app::state::types::ToastKind::Info,
format!("Retrospective created: {}", retro.name),
));
}
Ok(None) => {}
Err(e) => {
let _ = e;
}
}
let lessons: Vec<crate::model::memory::Memory> = crate::model::memory::Memory::list(&state.memory_dir)
.iter()
.filter_map(|n| crate::model::memory::Memory::read(&state.memory_dir, n).ok())
.filter(|m| m.kind == "lesson")
.collect();
if let Some(global_dir) = dirs::data_dir().map(|d| d.join("zesdex")) {
for lesson in &lessons {
if lesson.scope.as_deref() != Some("global") {
let _ = crate::model::memory::promote_with_consensus(&global_dir, lesson);
fn run_oauth_flow(provider: &str) -> anyhow::Result<String> {
use crate::service::oauth::manager::{OAuthConfig, OAuthManager};
use crate::service::oauth::loopback::LoopbackServer;
use crate::service::oauth::pkce::CodeVerifier;
let config = match provider {
"zen" | "opencode" => OAuthConfig {
auth_url: "https://opencode.ai/zen/oauth/authorize".to_string(),
token_url: "https://opencode.ai/zen/oauth/token".to_string(),
client_id: std::env::var("ZEN_CLIENT_ID")
.unwrap_or_else(|_| "zesdex".to_string()),
client_secret: std::env::var("ZEN_CLIENT_SECRET").ok(),
scopes: vec!["openid".to_string(), "profile".to_string(), "email".to_string()],
},
"openai" => OAuthConfig {
auth_url: "https://auth0.openai.com/authorize".to_string(),
token_url: "https://auth0.openai.com/oauth/token".to_string(),
client_id: std::env::var("OPENAI_CLIENT_ID")
.unwrap_or_else(|_| "zesdex".to_string()),
client_secret: std::env::var("OPENAI_CLIENT_SECRET").ok(),
scopes: vec!["openid".to_string(), "profile".to_string(), "email".to_string()],
},
other => {
let auth_url = std::env::var(format!("{}_AUTH_URL", other.to_uppercase()))
.map_err(|_| anyhow::anyhow!("unknown provider '{}'. Set {}_AUTH_URL env var.", other, other.to_uppercase()))?;
let token_url = std::env::var(format!("{}_TOKEN_URL", other.to_uppercase()))
.map_err(|_| anyhow::anyhow!("{}_TOKEN_URL not set", other.to_uppercase()))?;
let client_id = std::env::var(format!("{}_CLIENT_ID", other.to_uppercase()))
.unwrap_or_else(|_| "zesdex".to_string());
OAuthConfig {
auth_url,
token_url,
client_id,
client_secret: std::env::var(format!("{}_CLIENT_SECRET", other.to_uppercase())).ok(),
scopes: vec!["openid".to_string(), "profile".to_string(), "email".to_string()],
}
}
};
let server = LoopbackServer::bind()?;
let redirect_uri = server.redirect_uri();
let verifier = CodeVerifier::new();
let challenge = verifier.challenge();
let state_token = format!("{:x}", sha2::Sha256::digest(rand_bytes(16)));
let mut manager = OAuthManager::new(config.clone());
let auth_url = manager.build_auth_url(&redirect_uri, &state_token, challenge.as_str());
let _ = webbrowser::open(&auth_url);
let code = server.wait_for_code(120_000)?;
manager.exchange_code(&code, &redirect_uri, verifier.as_str())
.map_err(|e| anyhow::anyhow!("{}", e))?;
if let Some(ref token) = manager.token {
let token_path = dirs::config_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join("zesdex")
.join(format!("oauth_{}.json", provider));
if let Some(parent) = token_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(&token_path, serde_json::to_string_pretty(token).unwrap_or_default());
}
Ok(format!("Successfully authenticated with {}.", provider))
}
fn rand_bytes(n: usize) -> Vec<u8> {
use std::time::{SystemTime, UNIX_EPOCH};
let seed = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().subsec_nanos();
(0..n).map(|i| ((seed >> (i % 4 * 8)) ^ (i as u32 * 2654435761)) as u8).collect()
}