From b8649b00f23418f49fcfa6b450dd4a6bcac623de Mon Sep 17 00:00:00 2001 From: syui Date: Tue, 24 Mar 2026 16:54:07 +0900 Subject: [PATCH] feat(tui): show @handle in AI title and user input from oauth/token --- src/tui.rs | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/tui.rs b/src/tui.rs index 4f182ac..3157009 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -41,6 +41,8 @@ pub struct App { ai_status: String, ai_scroll: u16, ai_phase: AiPhase, + ai_handle: String, + user_handle: String, agents: Vec, selected: usize, @@ -72,6 +74,8 @@ impl App { let claude = ClaudeManager::spawn().ok(); let ai_status = if claude.is_some() { "starting..." } else { "not available" }; + let (user_handle, ai_handle) = load_handles(); + let mut app = Self { claude, ai_output: String::new(), @@ -79,6 +83,8 @@ impl App { ai_status: ai_status.to_string(), ai_scroll: 0, ai_phase: AiPhase::Idle, + ai_handle, + user_handle, agents: Vec::new(), selected: 0, next_id: 1, @@ -663,6 +669,30 @@ fn parse_agent_commands(text: &str) -> Vec<(String, String, String)> { } /// Load identity context from atproto config + recent chat. +/// Load user and AI handles. Priority: oauth > token/bot > config.json +fn load_handles() -> (String, String) { + let cfg_dir = crate::config::config_dir(); + let base = format!("{cfg_dir}/ai.syui.log"); + + let read_handle = |path: &str| -> Option { + let content = std::fs::read_to_string(path).ok()?; + let v: serde_json::Value = serde_json::from_str(&content).ok()?; + v["handle"].as_str().map(|s| s.to_string()) + }; + + // User: oauth_session > token + let user = read_handle(&format!("{base}/oauth_session.json")) + .or_else(|| read_handle(&format!("{base}/token.json"))) + .unwrap_or_default(); + + // Bot: oauth_bot_session > bot + let bot = read_handle(&format!("{base}/oauth_bot_session.json")) + .or_else(|| read_handle(&format!("{base}/bot.json"))) + .unwrap_or_default(); + + (user, bot) +} + fn load_identity_context() -> String { let config_path = crate::config::config_path(); @@ -800,10 +830,12 @@ fn render_ai_section(frame: &mut Frame, app: &App, area: Rect) { let border_style = Style::default().fg(ai_color); let (ai_icon, ai_label_color) = ai_status_icon(app.ai_status.as_str()); + let ai_name = if app.ai_handle.is_empty() { "AI".to_string() } + else { format!("@{}", app.ai_handle) }; let title_spans = vec![ Span::raw(" "), Span::styled(format!("{ai_icon} "), Style::default().fg(ai_label_color)), - Span::styled("AI", Style::default().fg(ai_color).add_modifier(Modifier::BOLD)), + Span::styled(&ai_name, Style::default().fg(ai_color).add_modifier(Modifier::BOLD)), Span::styled(format!(" {} ", app.ai_status), Style::default().fg(ai_label_color)), ]; @@ -829,8 +861,10 @@ fn render_ai_section(frame: &mut Frame, app: &App, area: Rect) { .scroll((scroll, 0)); frame.render_widget(output, layout[0]); + let user_label = if app.user_handle.is_empty() { ">".to_string() } + else { format!("@{}", app.user_handle) }; let input_line = Line::from(vec![ - Span::styled(" > ", Style::default().fg(ai_color)), + Span::styled(format!(" {user_label} "), Style::default().fg(Color::DarkGray)), Span::raw(&app.ai_input), if ai_focused { Span::styled("_", Style::default().fg(ai_color)) } else { Span::raw("") },