2
0

feat(tui): show @handle in AI title and user input from oauth/token

This commit is contained in:
2026-03-24 16:54:07 +09:00
parent a1e8267d58
commit b8649b00f2

View File

@@ -41,6 +41,8 @@ pub struct App {
ai_status: String,
ai_scroll: u16,
ai_phase: AiPhase,
ai_handle: String,
user_handle: String,
agents: Vec<Agent>,
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<String> {
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("") },