diff --git a/src/app/mode/help.rs b/src/app/mode/help.rs index 67a64a7..a6a0d58 100644 --- a/src/app/mode/help.rs +++ b/src/app/mode/help.rs @@ -9,7 +9,6 @@ Keybindings: Ctrl+P Settings Ctrl+A Toggle yolo arm Ctrl+B Bash panel - Ctrl+S Session hub Ctrl+T Todo panel Ctrl+W Workflow panel Ctrl+K Key input @@ -21,14 +20,12 @@ Keybindings: Slash commands: /help Show this help /quit Quit session - /resume Resume from overlay /mode Switch mode (chat, agents, bash, workflow) /lesson Create a lesson /lesson list List lessons /lesson export Export lessons /lesson import Import lessons - /clear Clear transcript - /save Save session"; + /clear Clear transcript"; pub fn handle_help_action(action: &Action) -> Action { match action { diff --git a/src/app/mode/mod.rs b/src/app/mode/mod.rs index fc99434..164da59 100644 --- a/src/app/mode/mod.rs +++ b/src/app/mode/mod.rs @@ -19,14 +19,10 @@ pub mod mcp; pub mod onboard; #[expect(dead_code)] pub mod onboard_provider; -#[expect(dead_code)] -pub mod picker; pub mod quit_confirm; #[expect(dead_code)] pub mod rewind; pub mod security; -#[expect(dead_code)] -pub mod session_hub; pub mod settings; pub mod todo; #[expect(dead_code)] @@ -40,11 +36,9 @@ pub enum ModeKind { Workflow, Help, Settings, - SessionHub, QuitConfirm, Onboard, OnboardProvider, - Picker, KeyInput, Editor, Effort, @@ -64,11 +58,9 @@ impl ModeKind { ModeKind::Workflow => "Workflow", ModeKind::Help => "Help", ModeKind::Settings => "Settings", - ModeKind::SessionHub => "SessionHub", ModeKind::QuitConfirm => "QuitConfirm", ModeKind::Onboard => "Onboard", ModeKind::OnboardProvider => "OnboardProvider", - ModeKind::Picker => "Picker", ModeKind::KeyInput => "KeyInput", ModeKind::Editor => "Editor", ModeKind::Effort => "Effort", diff --git a/src/app/runtime/actions/mod.rs b/src/app/runtime/actions/mod.rs index 73d3b5b..afa18d6 100644 --- a/src/app/runtime/actions/mod.rs +++ b/src/app/runtime/actions/mod.rs @@ -9,6 +9,8 @@ use crate::app::state::runtime::TurnEvent; use crate::app::state::types::{AgentMode, Origin, Overlay, Toast, ToastKind}; use crate::dto::chat::message::{ChatMessage, Role}; +const MAX_TOOL_ONLY_TURNS: usize = 6; + const MAX_AGENT_STEPS: usize = 40; #[derive(Debug, Clone)] @@ -58,9 +60,6 @@ pub enum Action { LessonReject { name: String, }, - SaveSession, - ResumeSession, - RefreshSessions, } pub fn apply_action(state: &mut AppStateRest, action: Action) { @@ -82,11 +81,9 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { | ModeKind::Workflow => Overlay::None, ModeKind::Help => Overlay::Help, ModeKind::Settings => Overlay::Settings, - ModeKind::SessionHub => Overlay::SessionHub, ModeKind::QuitConfirm => Overlay::QuitConfirm, ModeKind::Onboard => Overlay::Onboard, ModeKind::OnboardProvider => Overlay::OnboardProvider, - ModeKind::Picker => Overlay::Picker, ModeKind::KeyInput => Overlay::KeyInput, ModeKind::Editor => Overlay::Editor, ModeKind::Effort => Overlay::Effort, @@ -110,6 +107,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { rt.push_message(ChatMessage::user(text)); } spawn_turn(state); + state.push_transcript(ChatMessageDisplay::new(Role::Assistant, "Thinking...".to_string())); state.dirty = true; } Action::InsertChar(c) => { @@ -273,53 +271,6 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { } state.dirty = true; } - Action::SaveSession => { - save_current_session(state); - state.push_toast(Toast::new(ToastKind::Success, "session saved".to_string())); - state.dirty = true; - } - Action::ResumeSession => { - let base = state.store_base_dir(); - let sessions = crate::model::session::Session::list(&base); - let target = sessions.into_iter() - .filter(|s| s.id != state.session_id) - .max_by_key(|s| s.updated_at); - if let Some(session) = target { - let conv_path = session.conversation_path(&base); - let loaded_msgs: Vec = - std::fs::read_to_string(&conv_path) - .ok() - .and_then(|data| serde_json::from_str(&data).ok()) - .unwrap_or_default(); - state.session_id = session.id.clone(); - state.session_dir = session.session_dir(&base); - state.session_runtime = Some(crate::app::state::runtime::SessionRuntime::new( - state.session_dir.clone(), - )); - state.transcript_cache.messages.clear(); - if let Some(ref mut rt) = state.session_runtime { - for msg in loaded_msgs { - let display = ChatMessageDisplay::new( - msg.role.clone(), - msg.content.clone().unwrap_or_default(), - ); - state.transcript_cache.messages.push(display); - rt.push_message(msg); - } - } - state.push_toast(Toast::new(ToastKind::Success, - format!("resumed session: {}", session.title))); - } else { - state.push_toast(Toast::new(ToastKind::Info, - "no other sessions to resume".to_string())); - } - state.dirty = true; - } - Action::RefreshSessions => { - let base = state.store_base_dir(); - state.sessions = crate::model::session::Session::list(&base); - state.dirty = true; - } Action::Tick => { let now_ms = chrono::Utc::now().timestamp_millis(); state.misc.drain_expired_toasts(now_ms); @@ -341,16 +292,39 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { TurnEvent::AssistantMessage(msg) => { let display_content = msg.content.clone().unwrap_or_default(); if !display_content.is_empty() { - state.push_transcript(ChatMessageDisplay::new(Role::Assistant, display_content)); + let replaced = if let Some(last) = state.transcript_cache.messages.last_mut() { + if last.role == Role::Assistant && last.content == "Thinking..." { + last.content = display_content.clone(); + true + } else { + false + } + } else { + false + }; + if !replaced { + state.push_transcript(ChatMessageDisplay::new(Role::Assistant, display_content)); + } } if let Some(ref mut rt) = state.session_runtime { rt.push_message(msg); } } - TurnEvent::ToolResult { tool_call_id, tool_name, output, is_error } => { + TurnEvent::ToolResult { tool_call_id, tool_name, output, is_error, path } => { + let display_path = path.unwrap_or_default(); + let display = if tool_name == "read" { + let line_count = output.lines().count(); + if !display_path.is_empty() { + format!("read: {} ({} lines)", display_path, line_count) + } else { + format!("read: {} line(s)", line_count) + } + } else { + format!("{}: {}", tool_name, output) + }; state.push_transcript(ChatMessageDisplay::new( Role::Tool, - format!("{}: {}", tool_name, output), + display, )); if let Some(ref mut rt) = state.session_runtime { rt.push_message(ChatMessage::tool_result(tool_call_id.clone(), output.clone())); @@ -520,6 +494,7 @@ fn run_agent_turn( ) -> anyhow::Result<()> { let mut msgs = messages.to_vec(); let mut edits_this_turn = 0u32; + let mut tool_only_rounds = 0usize; let system_text = format!( "{}\n\n{}", @@ -531,6 +506,13 @@ fn run_agent_turn( } for _step in 0..MAX_AGENT_STEPS { + if tool_only_rounds >= MAX_TOOL_ONLY_TURNS { + msgs.push(ChatMessage::user( + "Stop calling tools. Respond naturally now.".to_string(), + )); + tool_only_rounds = 0; + } + let response = tc .client .chat_with_tools(&msgs, Some(tc.tdefs.clone()))?; @@ -540,6 +522,7 @@ fn run_agent_turn( let content = response.content.clone().unwrap_or_default(); if has_tool_calls { + tool_only_rounds += 1; let tool_calls = response.tool_calls.clone().unwrap_or_default(); msgs.push(response); for tool_call in tool_calls { @@ -583,6 +566,8 @@ fn run_agent_turn( edits_this_turn += 1; } + let tool_path = args.get("path").and_then(|v| v.as_str()).map(|s| s.to_string()); + { if let Ok(mut q) = events_q.lock() { q.push_back(TurnEvent::ToolResult { @@ -590,6 +575,7 @@ fn run_agent_turn( tool_name: tool_name.clone(), output: output.clone(), is_error, + path: tool_path, }); } } diff --git a/src/app/runtime/commands.rs b/src/app/runtime/commands.rs index a615113..039235c 100644 --- a/src/app/runtime/commands.rs +++ b/src/app/runtime/commands.rs @@ -10,9 +10,6 @@ pub fn apply_command(command: Command) -> Vec { Command::Quit => { vec![Action::QuitConfirm] } - Command::Resume => { - vec![Action::SaveSession, Action::ResumeSession, Action::CloseOverlay] - } Command::LessonCreate(text) => { vec![Action::SystemNote { kind: "lesson".to_string(), @@ -43,9 +40,6 @@ pub fn apply_command(command: Command) -> Vec { message: "transcript cleared".to_string(), }] } - Command::Save => { - vec![Action::SaveSession] - } Command::Login { provider } => { vec![Action::SystemNote { kind: "oauth".to_string(), diff --git a/src/app/state/misc.rs b/src/app/state/misc.rs index 1344fcd..111c6b0 100644 --- a/src/app/state/misc.rs +++ b/src/app/state/misc.rs @@ -76,9 +76,7 @@ pub struct InputState { const COMMANDS: &[&str] = &[ "/help", "/quit", - "/resume", "/clear", - "/save", "/lesson", "/lesson ls", "/lesson export", diff --git a/src/app/state/runtime.rs b/src/app/state/runtime.rs index 4d807b0..0078e15 100644 --- a/src/app/state/runtime.rs +++ b/src/app/state/runtime.rs @@ -68,6 +68,7 @@ pub enum TurnEvent { tool_name: String, output: String, is_error: bool, + path: Option, }, SystemNote { kind: String, diff --git a/src/app/state/types.rs b/src/app/state/types.rs index cb05441..f3f08df 100644 --- a/src/app/state/types.rs +++ b/src/app/state/types.rs @@ -39,7 +39,6 @@ pub enum PanelKind { Bash, Workflow, Help, - SessionHub, } impl PanelKind { @@ -50,7 +49,6 @@ impl PanelKind { PanelKind::Bash => "Bash", PanelKind::Workflow => "Workflow", PanelKind::Help => "Help", - PanelKind::SessionHub => "Sessions", } } } @@ -95,11 +93,9 @@ pub enum Overlay { Agents, Bash, QuitConfirm, - SessionHub, Workflow, Onboard, OnboardProvider, - Picker, KeyInput, Editor, Effort, diff --git a/src/controller/command.rs b/src/controller/command.rs index 8f77e03..06c231d 100644 --- a/src/controller/command.rs +++ b/src/controller/command.rs @@ -4,16 +4,14 @@ use crate::app::mode::ModeKind; pub enum Command { Help, Quit, - Resume, LessonCreate(String), LessonExport(String), LessonImport(String), LessonAccept(String), LessonReject(String), + LessonList, Mode(ModeKind), Clear, - Save, - LessonList, Login { provider: String }, Unknown(String), } @@ -30,9 +28,7 @@ pub fn parse_command(text: &str) -> Command { match cmd { "/help" => Command::Help, "/quit" => Command::Quit, - "/resume" => Command::Resume, "/clear" => Command::Clear, - "/save" => Command::Save, "/mode" => { let mode = match arg1 { "chat" | "c" => ModeKind::Chat, diff --git a/src/controller/input.rs b/src/controller/input.rs index 1b63e5a..6e20d1f 100644 --- a/src/controller/input.rs +++ b/src/controller/input.rs @@ -72,11 +72,6 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec { KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::CONTROL) => { vec![Action::OpenOverlay(Overlay::Bash)] } - KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) - && !key.modifiers.contains(KeyModifiers::SHIFT) => - { - vec![Action::RefreshSessions, Action::OpenOverlay(Overlay::SessionHub)] - } KeyCode::Char('S') if key.modifiers.contains(KeyModifiers::CONTROL) => { vec![Action::OpenOverlay(Overlay::Security)] } diff --git a/src/main.rs b/src/main.rs index d8fd2fb..3b12c33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -211,11 +211,9 @@ fn apply_client_update( Some("Agents") => Overlay::Agents, Some("Bash") => Overlay::Bash, Some("QuitConfirm") => Overlay::QuitConfirm, - Some("SessionHub") => Overlay::SessionHub, Some("Workflow") => Overlay::Workflow, Some("Onboard") => Overlay::Onboard, Some("OnboardProvider") => Overlay::OnboardProvider, - Some("Picker") => Overlay::Picker, Some("KeyInput") => Overlay::KeyInput, Some("Editor") => Overlay::Editor, Some("Effort") => Overlay::Effort, diff --git a/src/view/mod.rs b/src/view/mod.rs index 94f4140..fb578f7 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -156,24 +156,6 @@ fn render_overlay(frame: &mut Frame, area: Rect, overlay: crate::app::state::typ let paragraph = Paragraph::new(lines).block(block); frame.render_widget(paragraph, overlay_area); } - crate::app::state::types::Overlay::SessionHub => { - let block = block.title(" Sessions "); - let lines: Vec = state.sessions.iter().map(|s| { - Line::from(Span::styled( - format!("{} | {}", s.title, s.model), - Style::default().fg(Theme::TEXT), - )) - }).collect(); - let paragraph = if lines.is_empty() { - Paragraph::new(Line::from(Span::styled( - "No sessions", - Style::default().fg(Theme::DIM), - ))).block(block) - } else { - Paragraph::new(lines).block(block) - }; - frame.render_widget(paragraph, overlay_area); - } crate::app::state::types::Overlay::Workflow => { workflow::draw_workflow_panel(frame, overlay_area, state); } @@ -228,37 +210,6 @@ fn render_overlay(frame: &mut Frame, area: Rect, overlay: crate::app::state::typ let paragraph = Paragraph::new(lines).block(block); frame.render_widget(paragraph, overlay_area); } - crate::app::state::types::Overlay::Picker => { - let block = block.title(" Select "); - let lines = vec![ - Line::from(Span::styled( - "Selection Picker", - Style::default().fg(Theme::TEXT).add_modifier(Modifier::BOLD), - )), - Line::from(Span::styled( - "", - Style::default(), - )), - Line::from(Span::styled( - format!("Filter: {}", state.input.buffer), - Style::default().fg(Theme::DIM), - )), - Line::from(Span::styled( - "", - Style::default(), - )), - Line::from(Span::styled( - "Press Enter to select, Esc to cancel.", - Style::default().fg(Theme::DIM), - )), - Line::from(Span::styled( - "Type to filter available options.", - Style::default().fg(Theme::DIM), - )), - ]; - let paragraph = Paragraph::new(lines).block(block); - frame.render_widget(paragraph, overlay_area); - } crate::app::state::types::Overlay::KeyInput => { let block = block.title(" Input "); let input_text = &state.input.buffer;