@ -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 < Line < ' static > > = 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 ,
}
}