diff --git a/.beans/claudbg-9c8r--tui-sub-agents-panel-in-transcript-view.md b/.beans/claudbg-9c8r--tui-sub-agents-panel-in-transcript-view.md index 6a0156d..356df0f 100644 --- a/.beans/claudbg-9c8r--tui-sub-agents-panel-in-transcript-view.md +++ b/.beans/claudbg-9c8r--tui-sub-agents-panel-in-transcript-view.md @@ -1,11 +1,11 @@ --- # claudbg-9c8r title: 'TUI: sub-agents panel in transcript view' -status: todo +status: in-progress type: feature priority: normal created_at: 2026-03-30T04:47:15Z -updated_at: 2026-03-30T04:49:03Z +updated_at: 2026-03-30T16:59:55Z parent: claudbg-i6l2 blocked_by: - claudbg-rudq diff --git a/src/tui/screens/transcript.rs b/src/tui/screens/transcript.rs index f363906..361df3c 100644 --- a/src/tui/screens/transcript.rs +++ b/src/tui/screens/transcript.rs @@ -6,7 +6,7 @@ use ratatui::Frame; use ratatui::crossterm::event::{Event, KeyCode, KeyEventKind}; -use ratatui::layout::{Constraint, Layout, Rect}; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span, Text}; use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; @@ -248,10 +248,16 @@ pub fn render_transcript(f: &mut Frame, area: Rect, state: &AppState) { .wrap(Wrap { trim: false }); f.render_widget(header_paragraph, chunks[0]); + // ── Chat log + sub-agents panel (horizontal split) ────────────────────── + let body_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Min(0), Constraint::Length(30)]) + .split(chunks[1]); + // ── Chat log ───────────────────────────────────────────────────────────── let chat_lines = build_chat_lines(&state.transcript_entries); - let border_style = if state.focus == Focus::ChatLog { + let chat_border_style = if state.focus == Focus::ChatLog { Style::default().fg(Color::Yellow) } else { Style::default() @@ -260,13 +266,60 @@ pub fn render_transcript(f: &mut Frame, area: Rect, state: &AppState) { let chat_block = Block::default() .title(" Transcript [↑/↓ scroll · ←/→ h-scroll · Tab focus · Esc back · ? help] ") .borders(Borders::ALL) - .border_style(border_style); + .border_style(chat_border_style); let chat_paragraph = Paragraph::new(Text::from(chat_lines)) .block(chat_block) .scroll((state.transcript_scroll as u16, state.transcript_h_scroll as u16)); - f.render_widget(chat_paragraph, chunks[1]); + f.render_widget(chat_paragraph, body_chunks[0]); + + // ── Sub-agents panel ───────────────────────────────────────────────────── + let subagents_border_style = if state.focus == Focus::SubagentsPanel { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }; + + let subagents_block = Block::default() + .title(" Sub-agents ") + .borders(Borders::ALL) + .border_style(subagents_border_style); + + let subagent_lines: Vec> = if state.subagents.is_empty() { + vec![Line::from(" No sub-agents")] + } else { + state + .subagents + .iter() + .enumerate() + .map(|(i, agent)| { + let short_id = if agent.agent_id.len() >= 8 { + agent.agent_id[..8].to_string() + } else { + agent.agent_id.clone() + }; + let agent_type = agent + .agent_type + .as_deref() + .unwrap_or("agent") + .to_string(); + if i == state.subagent_selected && state.focus == Focus::SubagentsPanel { + Line::from(Span::styled( + format!("> {short_id} {agent_type}"), + Style::default().fg(Color::Yellow), + )) + } else { + Line::from(format!(" {short_id} {agent_type}")) + } + }) + .collect() + }; + + let subagents_paragraph = Paragraph::new(Text::from(subagent_lines)) + .block(subagents_block); + + f.render_widget(subagents_paragraph, body_chunks[1]); } // --------------------------------------------------------------------------- @@ -285,24 +338,6 @@ pub fn handle_transcript_event(event: Event, state: &mut AppState) -> bool { } match key.code { - // Vertical scroll. - KeyCode::Up | KeyCode::Char('k') => { - state.transcript_scroll = state.transcript_scroll.saturating_sub(1); - true - } - KeyCode::Down | KeyCode::Char('j') => { - state.transcript_scroll = state.transcript_scroll.saturating_add(1); - true - } - // Horizontal scroll. - KeyCode::Left | KeyCode::Char('h') => { - state.transcript_h_scroll = state.transcript_h_scroll.saturating_sub(1); - true - } - KeyCode::Right | KeyCode::Char('l') => { - state.transcript_h_scroll = state.transcript_h_scroll.saturating_add(1); - true - } // Toggle focus between ChatLog and SubagentsPanel. KeyCode::Tab => { state.focus = match state.focus { @@ -326,6 +361,56 @@ pub fn handle_transcript_event(event: Event, state: &mut AppState) -> bool { state.show_help = true; true } + // Directional keys are routed based on the focused panel. + KeyCode::Up | KeyCode::Char('k') => match state.focus { + Focus::SubagentsPanel => { + state.subagent_selected = state.subagent_selected.saturating_sub(1); + true + } + Focus::ChatLog => { + state.transcript_scroll = state.transcript_scroll.saturating_sub(1); + true + } + }, + KeyCode::Down | KeyCode::Char('j') => match state.focus { + Focus::SubagentsPanel => { + if !state.subagents.is_empty() { + let max = state.subagents.len() - 1; + state.subagent_selected = + (state.subagent_selected + 1).min(max); + } + true + } + Focus::ChatLog => { + state.transcript_scroll = state.transcript_scroll.saturating_add(1); + true + } + }, + // Horizontal scroll — only meaningful in ChatLog. + KeyCode::Left | KeyCode::Char('h') => { + state.transcript_h_scroll = state.transcript_h_scroll.saturating_sub(1); + true + } + KeyCode::Right | KeyCode::Char('l') => { + state.transcript_h_scroll = state.transcript_h_scroll.saturating_add(1); + true + } + // Enter on a selected sub-agent — navigate into its transcript. + KeyCode::Enter => { + if state.focus == Focus::SubagentsPanel && !state.subagents.is_empty() { + let agent = state.subagents[state.subagent_selected].clone(); + let parent_session_id = agent.session_id.clone(); + let agent_id = agent.agent_id.clone(); + state.enter_subagent_transcript(parent_session_id, agent_id); + // Load the transcript for the selected agent. + let agent_id_for_load = agent.agent_id.clone(); + let parent_for_load = agent.session_id.clone(); + load_transcript_for_agent(&parent_for_load, &agent_id_for_load, state); + true + } else { + false + } + } _ => false, } }