From 90c5ecabc979108d61cf26173004b4157ddc98ba Mon Sep 17 00:00:00 2001 From: echobt Date: Sun, 1 Feb 2026 20:46:01 +0000 Subject: [PATCH 1/3] feat(cortex-tui): add subagent conversation navigation - Add SubagentConversation(String) variant to AppView enum for viewing subagent conversations - Add viewing_subagent: Option field to AppState to track current subagent - Add view_subagent_conversation(), return_to_main_conversation(), is_viewing_subagent(), get_viewing_subagent() methods - Add SubagentView context to HintContext with hints for Esc (back to main) and scroll - Add render_back_to_main_hint() function for displaying back navigation hint - Update match expressions to handle new SubagentConversation variant --- src/cortex-tui/src/app/methods.rs | 22 +++++++++++++++++++ src/cortex-tui/src/app/state.rs | 5 ++++- src/cortex-tui/src/app/types.rs | 4 +++- .../src/runner/event_loop/rendering.rs | 20 ++++++++++++----- .../src/views/minimal_session/rendering.rs | 9 ++++++++ src/cortex-tui/src/widgets/key_hints.rs | 6 +++++ 6 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/cortex-tui/src/app/methods.rs b/src/cortex-tui/src/app/methods.rs index ab1c3693..a7143fc0 100644 --- a/src/cortex-tui/src/app/methods.rs +++ b/src/cortex-tui/src/app/methods.rs @@ -457,6 +457,28 @@ impl AppState { .filter(|t| !t.status.is_terminal()) .count() } + + /// Enter subagent conversation view + pub fn view_subagent_conversation(&mut self, session_id: String) { + self.viewing_subagent = Some(session_id.clone()); + self.set_view(AppView::SubagentConversation(session_id)); + } + + /// Return to main conversation from subagent view + pub fn return_to_main_conversation(&mut self) { + self.viewing_subagent = None; + self.set_view(AppView::Session); + } + + /// Check if viewing a subagent conversation + pub fn is_viewing_subagent(&self) -> bool { + self.viewing_subagent.is_some() + } + + /// Get the currently viewed subagent session ID + pub fn get_viewing_subagent(&self) -> Option<&String> { + self.viewing_subagent.as_ref() + } } // ============================================================================ diff --git a/src/cortex-tui/src/app/state.rs b/src/cortex-tui/src/app/state.rs index 26374f0e..4a4d0b71 100644 --- a/src/cortex-tui/src/app/state.rs +++ b/src/cortex-tui/src/app/state.rs @@ -88,6 +88,8 @@ pub struct AppState { pub message_queue: VecDeque, /// Active subagent tasks being displayed. pub active_subagents: Vec, + /// Currently viewed subagent session ID (for SubagentConversation view) + pub viewing_subagent: Option, /// MCP servers list for management pub mcp_servers: Vec, /// Context files added to the session @@ -220,6 +222,7 @@ impl AppState { question_hovered_tab: None, message_queue: VecDeque::new(), active_subagents: Vec::new(), + viewing_subagent: None, mcp_servers: Vec::new(), context_files: Vec::new(), log_level: String::from("info"), @@ -299,7 +302,7 @@ impl Default for AppState { impl AppState { /// Set the current view pub fn set_view(&mut self, view: AppView) { - self.previous_view = Some(self.view); + self.previous_view = Some(self.view.clone()); self.view = view; } diff --git a/src/cortex-tui/src/app/types.rs b/src/cortex-tui/src/app/types.rs index f5c77234..270ee784 100644 --- a/src/cortex-tui/src/app/types.rs +++ b/src/cortex-tui/src/app/types.rs @@ -5,7 +5,7 @@ use cortex_core::widgets::DisplayMode; pub type OperationMode = DisplayMode; /// The current view/screen being displayed -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub enum AppView { #[default] Session, @@ -13,6 +13,8 @@ pub enum AppView { Questions, Settings, Help, + /// Viewing a subagent's conversation (stores the subagent session_id) + SubagentConversation(String), } /// Which UI element currently has focus diff --git a/src/cortex-tui/src/runner/event_loop/rendering.rs b/src/cortex-tui/src/runner/event_loop/rendering.rs index 9422a3da..8da2ce07 100644 --- a/src/cortex-tui/src/runner/event_loop/rendering.rs +++ b/src/cortex-tui/src/runner/event_loop/rendering.rs @@ -35,7 +35,7 @@ impl EventLoop { terminal.draw(|frame| { let area = frame.area(); - match self.app_state.view { + match &self.app_state.view { AppView::Session => { let view = crate::views::MinimalSessionView::new(&self.app_state); frame.render_widget(view, area); @@ -63,6 +63,12 @@ impl EventLoop { let view = crate::views::MinimalSessionView::new(&self.app_state); frame.render_widget(view, area); } + + AppView::SubagentConversation(_session_id) => { + // Render the subagent conversation view (same as session for now) + let view = crate::views::MinimalSessionView::new(&self.app_state); + frame.render_widget(view, area); + } } // Render modal overlays (legacy) @@ -162,12 +168,13 @@ impl EventLoop { let (width, height) = self.app_state.terminal_size; let area = Rect::new(0, 0, width, height); - match self.app_state.view { + match &self.app_state.view { AppView::Session | AppView::Approval | AppView::Questions | AppView::Settings - | AppView::Help => { + | AppView::Help + | AppView::SubagentConversation(_) => { let input_height: u16 = 1; let hints_height: u16 = 1; let status_height: u16 = if self.app_state.streaming.is_streaming { @@ -263,7 +270,6 @@ impl EventLoop { use std::cell::RefCell; let selected_lines: RefCell> = RefCell::new(Vec::new()); - let view = self.app_state.view; let active_modal = self.app_state.active_modal.clone(); let card_active = self.card_handler.is_active(); @@ -273,7 +279,7 @@ impl EventLoop { let screen_height = area.height; // Full render - match view { + match &self.app_state.view { AppView::Session => { let widget = crate::views::MinimalSessionView::new(&self.app_state); frame.render_widget(widget, area); @@ -298,6 +304,10 @@ impl EventLoop { let widget = crate::views::MinimalSessionView::new(&self.app_state); frame.render_widget(widget, area); } + AppView::SubagentConversation(_session_id) => { + let widget = crate::views::MinimalSessionView::new(&self.app_state); + frame.render_widget(widget, area); + } } // Render modals diff --git a/src/cortex-tui/src/views/minimal_session/rendering.rs b/src/cortex-tui/src/views/minimal_session/rendering.rs index eb263301..fa2c8d9a 100644 --- a/src/cortex-tui/src/views/minimal_session/rendering.rs +++ b/src/cortex-tui/src/views/minimal_session/rendering.rs @@ -20,6 +20,15 @@ use crate::views::tool_call::{ContentSegment, ToolCallDisplay, ToolStatus}; use super::text_utils::wrap_text; use super::VERSION; +/// Renders the "← Back to main conversation" hint when viewing a subagent. +/// Displays in the top-left area of the screen. +pub fn render_back_to_main_hint(area: Rect, buf: &mut Buffer, colors: &AdaptiveColors) { + let hint = "← Back to main (Esc)"; + let style = Style::default().fg(colors.text_dim); + // Render at the start of the area with 1 character padding + buf.set_string(area.x + 1, area.y, hint, style); +} + /// Renders a single message to lines. pub fn render_message( msg: &Message, diff --git a/src/cortex-tui/src/widgets/key_hints.rs b/src/cortex-tui/src/widgets/key_hints.rs index 0f521f33..1aa85ceb 100644 --- a/src/cortex-tui/src/widgets/key_hints.rs +++ b/src/cortex-tui/src/widgets/key_hints.rs @@ -30,6 +30,8 @@ pub enum HintContext { Approval, /// Selection list is focused Selection, + /// Viewing a subagent's conversation + SubagentView, } impl HintContext { @@ -53,6 +55,10 @@ impl HintContext { ("Esc", "close"), ("/", "filter"), ], + HintContext::SubagentView => vec![ + ("Esc", "back to main"), + ("↑/↓", "scroll"), + ], } } } From 76363f595372ea30f1b3913f3e9d037908cef390 Mon Sep 17 00:00:00 2001 From: echobt Date: Sun, 1 Feb 2026 20:59:55 +0000 Subject: [PATCH 2/3] fix(cortex-tui): improve tool result summary formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use '↳' prefix consistently across all tool summaries - LS tool shows 'items' instead of generic 'lines' - Glob/Grep show 'Found N files/matches' for clarity - Add specific handling for Create, MultiEdit, TodoWrite, Task tools - Default fallback shows 'items' instead of 'lines' - Add unit tests for new summary formats --- src/cortex-tui/src/views/tool_call.rs | 86 +++++++++++++++++++++------ 1 file changed, 67 insertions(+), 19 deletions(-) diff --git a/src/cortex-tui/src/views/tool_call.rs b/src/cortex-tui/src/views/tool_call.rs index 1fb100f0..e69516e0 100644 --- a/src/cortex-tui/src/views/tool_call.rs +++ b/src/cortex-tui/src/views/tool_call.rs @@ -182,54 +182,74 @@ pub fn format_tool_summary(name: &str, args: &Value) -> String { /// Format a result summary based on tool name and output /// -/// Returns a short summary like "Read 450 lines" or "Error: file not found" +/// Returns a short summary like "↳ Read 450 lines" or "↳ Error: file not found" +/// Uses '↳' prefix consistently across all tools for visual consistency. pub fn format_result_summary(name: &str, output: &str, success: bool) -> String { if !success { // Extract first line of error, truncated let first_line = output.lines().next().unwrap_or("unknown error"); let truncated = truncate_str(first_line, 50); - return format!("Error: {truncated}"); + return format!("↳ Error: {truncated}"); } match name.to_lowercase().as_str() { "read" => { let line_count = output.lines().count(); - format!("Read {line_count} lines") + format!("↳ Read {line_count} lines") } - "edit" => "Applied edit".to_string(), + "edit" | "multiedit" => "↳ Applied edit".to_string(), + "create" => "↳ File created".to_string(), "execute" | "bash" => { let line_count = output.lines().count(); if line_count == 0 { - "Completed".to_string() + "↳ Completed".to_string() } else if line_count == 1 { - truncate_str(output.trim(), 60) + format!("↳ {}", truncate_str(output.trim(), 60)) } else { - format!("{line_count} lines of output") + format!("↳ {line_count} lines of output") } } "glob" => { let file_count = output.lines().count(); match file_count { - 0 => "No matches".to_string(), - 1 => "1 file".to_string(), - n => format!("{n} files"), + 0 => "↳ No matches".to_string(), + 1 => "↳ Found 1 file".to_string(), + n => format!("↳ Found {n} files"), } } - "websearch" | "codesearch" => { + "grep" => { + let match_count = output.lines().count(); + match match_count { + 0 => "↳ No matches".to_string(), + 1 => "↳ Found 1 match".to_string(), + n => format!("↳ Found {n} matches"), + } + } + "ls" => { + let item_count = output.lines().count(); + match item_count { + 0 => "↳ Empty directory".to_string(), + 1 => "↳ Listed 1 item".to_string(), + n => format!("↳ Listed {n} items"), + } + } + "websearch" | "codesearch" | "fetchurl" => { let char_count = output.len(); if char_count > 1000 { - format!("Retrieved ~{} chars", char_count) + format!("↳ Retrieved ~{} chars", char_count) } else { - "Retrieved results".to_string() + "↳ Retrieved results".to_string() } } - "write" => "File written".to_string(), + "write" => "↳ File written".to_string(), + "todowrite" => "↳ Todos updated".to_string(), + "task" => "↳ Task completed".to_string(), _ => { let line_count = output.lines().count(); if line_count == 0 { - "Completed".to_string() + "↳ Completed".to_string() } else { - format!("{line_count} lines") + format!("↳ {line_count} items") } } } @@ -376,21 +396,49 @@ mod tests { fn test_format_result_summary_read() { let output = "line1\nline2\nline3\nline4\nline5"; let summary = format_result_summary("read", output, true); - assert_eq!(summary, "Read 5 lines"); + assert_eq!(summary, "↳ Read 5 lines"); } #[test] fn test_format_result_summary_error() { let output = "file not found"; let summary = format_result_summary("read", output, false); - assert_eq!(summary, "Error: file not found"); + assert_eq!(summary, "↳ Error: file not found"); } #[test] fn test_format_result_summary_glob() { let output = "file1.rs\nfile2.rs\nfile3.rs"; let summary = format_result_summary("glob", output, true); - assert_eq!(summary, "3 files"); + assert_eq!(summary, "↳ Found 3 files"); + } + + #[test] + fn test_format_result_summary_ls() { + let output = "file1.rs\nfile2.rs\ndir1"; + let summary = format_result_summary("ls", output, true); + assert_eq!(summary, "↳ Listed 3 items"); + } + + #[test] + fn test_format_result_summary_grep() { + let output = "match1\nmatch2"; + let summary = format_result_summary("grep", output, true); + assert_eq!(summary, "↳ Found 2 matches"); + } + + #[test] + fn test_format_result_summary_execute_multiline() { + let output = "line1\nline2\nline3"; + let summary = format_result_summary("execute", output, true); + assert_eq!(summary, "↳ 3 lines of output"); + } + + #[test] + fn test_format_result_summary_default() { + let output = "item1\nitem2\nitem3\nitem4"; + let summary = format_result_summary("unknown_tool", output, true); + assert_eq!(summary, "↳ 4 items"); } #[test] From de6ae1ea4b5e9008aed848a8bfc05d873079dc6a Mon Sep 17 00:00:00 2001 From: echobt Date: Sun, 1 Feb 2026 21:00:01 +0000 Subject: [PATCH 3/3] feat(cortex-tui): add ESC key handler for subagent view navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ESC key returns from subagent conversation to main conversation - Display '← Back to main (Esc)' hint when viewing subagent - Add SubagentView context for appropriate key hints --- src/cortex-tui/src/runner/event_loop/input.rs | 7 +++++++ src/cortex-tui/src/views/minimal_session/view.rs | 13 ++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/cortex-tui/src/runner/event_loop/input.rs b/src/cortex-tui/src/runner/event_loop/input.rs index 63f1b3e2..ed649b22 100644 --- a/src/cortex-tui/src/runner/event_loop/input.rs +++ b/src/cortex-tui/src/runner/event_loop/input.rs @@ -367,6 +367,13 @@ impl EventLoop { /// Handle ESC key with double-tap to quit fn handle_esc(&mut self, terminal: &mut CortexTerminal) -> Result<()> { + // Priority 1: If viewing a subagent conversation, return to main conversation + if self.app_state.is_viewing_subagent() { + self.app_state.return_to_main_conversation(); + self.render(terminal)?; + return Ok(()); + } + // Check if app is idle (nothing active that ESC should cancel) let is_idle = !self.app_state.streaming.is_streaming && self.app_state.pending_approval.is_none() diff --git a/src/cortex-tui/src/views/minimal_session/view.rs b/src/cortex-tui/src/views/minimal_session/view.rs index 2ab6ae21..37c2cc84 100644 --- a/src/cortex-tui/src/views/minimal_session/view.rs +++ b/src/cortex-tui/src/views/minimal_session/view.rs @@ -630,7 +630,9 @@ impl<'a> Widget for MinimalSessionView<'a> { // 8. Key hints - only show when NOT in interactive mode if !self.app_state.is_interactive_mode() { let hints_area = Rect::new(area.x, next_y, area.width, hints_height); - let context = if is_task_running { + let context = if self.app_state.is_viewing_subagent() { + HintContext::SubagentView + } else if is_task_running { HintContext::TaskRunning } else { HintContext::Idle @@ -642,6 +644,15 @@ impl<'a> Widget for MinimalSessionView<'a> { hints = hints.with_thinking_budget(budget); } hints.render(hints_area, buf); + + // Render "← Back to main (Esc)" hint when viewing a subagent + if self.app_state.is_viewing_subagent() { + crate::views::minimal_session::rendering::render_back_to_main_hint( + hints_area, + buf, + &self.colors, + ); + } } } }