From aa523273452918ffd7fb2fafdd3567994cd55c1c Mon Sep 17 00:00:00 2001 From: syui Date: Tue, 24 Mar 2026 16:05:38 +0900 Subject: [PATCH] feat(agent): inject user/bot identity and recent chat context into agent and TUI sessions --- src/agent.rs | 26 +++++++++++++++--- src/tui.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index 73eb861..1882272 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -73,13 +73,13 @@ impl Agent { let (child, mut stdin, stdout) = claude::spawn_claude(Some(cwd))?; let pid = child.id(); - // Inject self-awareness + git context + // Inject self-awareness + user/bot context + let user_ctx = load_user_context(); let identity = format!( "[system]\n\ You are agent '{name}' running inside aishell.\n\ - aishell is a multi-agent development tool. You are one of potentially several agents working in parallel.\n\ - If you find issues or have follow-up work, you can propose: @agent-name task -c dir\n\ - Your feedback about aishell itself is also valuable.\n" + {user_ctx}\ + If you find issues or have follow-up work, propose: @agent-name task -c dir\n" ); let git_ctx = git_context(cwd) .map(|ctx| format!("\n[git context]\n{ctx}")) @@ -290,6 +290,24 @@ impl Drop for Agent { } } +/// Load user/bot identity from shared config. +fn load_user_context() -> String { + let home = std::env::var("HOME").unwrap_or_default(); + let path = if cfg!(target_os = "macos") { + format!("{home}/Library/Application Support/ai.syui.log/config.json") + } else { + format!("{home}/.config/ai.syui.log/config.json") + }; + let config: serde_json::Value = std::fs::read_to_string(&path).ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); + + let user = config["handle"].as_str().unwrap_or(""); + let bot = config["bot"]["handle"].as_str().unwrap_or(""); + if user.is_empty() { return String::new(); } + format!("user: {user}, ai: {bot}\n") +} + /// Load last 3 session summaries for agent context. fn recent_session_summary() -> String { let dir = if cfg!(target_os = "macos") { diff --git a/src/tui.rs b/src/tui.rs index eeac064..2520de7 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -92,13 +92,14 @@ impl App { should_quit: false, }; - // Send protocol + project context + // Send protocol + identity + project context if let Some(ref mut claude) = app.claude { let cwd = std::env::current_dir() .map(|p| p.display().to_string()) .unwrap_or_else(|_| ".".to_string()); - let ctx = crate::agent::git_context(&cwd).unwrap_or_default(); - let msg = format!("{AI_IDENTITY}\n\n[project context]\n{ctx}"); + let git_ctx = crate::agent::git_context(&cwd).unwrap_or_default(); + let identity_ctx = load_identity_context(); + let msg = format!("{AI_IDENTITY}\n\n{identity_ctx}[project]\n{git_ctx}"); claude.send(&msg); } @@ -661,6 +662,74 @@ fn parse_agent_commands(text: &str) -> Vec<(String, String, String)> { .collect() } +/// Load identity context from atproto config + recent chat. +fn load_identity_context() -> String { + let home = std::env::var("HOME").unwrap_or_default(); + let config_path = if cfg!(target_os = "macos") { + format!("{home}/Library/Application Support/ai.syui.log/config.json") + } else { + format!("{home}/.config/ai.syui.log/config.json") + }; + + let config: serde_json::Value = match std::fs::read_to_string(&config_path) { + Ok(s) => serde_json::from_str(&s).unwrap_or_default(), + Err(_) => return String::new(), + }; + + let mut ctx = String::new(); + + // User & bot identity + let user_handle = config["handle"].as_str().unwrap_or("?"); + let bot_handle = config["bot"]["handle"].as_str().unwrap_or("?"); + let bot_did = config["bot"]["did"].as_str().unwrap_or("?"); + let network = config["network"].as_str().unwrap_or("?"); + + ctx.push_str(&format!( + "[identity]\nuser: {user_handle}\nyou: {bot_handle} ({bot_did})\nnetwork: {network}\n\n" + )); + + // Recent chat (last 2 entries for context) + let bot_path = config["bot"]["path"].as_str().unwrap_or(""); + let expanded = expand_tilde(bot_path); + let chat_dir = format!( + "{}/{}/ai.syui.log.chat", + if expanded.is_empty() { format!("{home}/.config/ai.syui.log/at") } else { expanded.clone() }, + config["bot"]["did"].as_str().unwrap_or("did") + ); + + // Also check user's chat dir + let user_did = config["did"].as_str().unwrap_or(""); + let user_chat_dir = format!("{}/{}/ai.syui.log.chat", + if expanded.is_empty() { format!("{home}/.config/ai.syui.log/at") } else { expanded }, + user_did + ); + + for dir in [&chat_dir, &user_chat_dir] { + if let Ok(mut entries) = std::fs::read_dir(dir) { + let mut files: Vec<_> = entries + .flatten() + .filter(|e| e.path().extension().is_some_and(|x| x == "json")) + .collect(); + files.sort_by_key(|e| std::cmp::Reverse(e.file_name())); + + for f in files.iter().take(1) { + if let Ok(content) = std::fs::read_to_string(f.path()) { + if let Ok(record) = serde_json::from_str::(&content) { + let text = record["value"]["content"]["text"].as_str().unwrap_or(""); + if !text.is_empty() { + let short: String = text.chars().take(200).collect(); + ctx.push_str(&format!("[recent chat]\n{short}\n\n")); + } + } + } + } + break; // Use first available dir + } + } + + ctx +} + fn expand_tilde(path: &str) -> String { if let Some(rest) = path.strip_prefix('~') { if let Ok(home) = std::env::var("HOME") {