diff --git a/src/app/runtime/actions/mod.rs b/src/app/runtime/actions/mod.rs index 1ea5ac3..b121a5e 100644 --- a/src/app/runtime/actions/mod.rs +++ b/src/app/runtime/actions/mod.rs @@ -81,9 +81,7 @@ pub enum Action { ModelList, AbortTurn, Compact, - RunWorkflow { - script: String, - }, + } /// Apply an `Action` to the application state. @@ -394,11 +392,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { state.workflow_engine.agents.clear(); state.workflow_engine.findings.clear(); } - if message.to_lowercase().contains("complete") - && state.misc.overlay == Overlay::Workflow - { - state.misc.overlay = Overlay::None; - } + // popup removed, no overlay to reset state.push_toast(Toast { kind: ToastKind::Info, message: message.clone(), @@ -435,9 +429,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { crate::dto::chat::message::Role::System, format!("✓ {message}"), )); - if state.misc.overlay == Overlay::Workflow { - state.misc.overlay = Overlay::None; - } + // overlay removed state.dirty = true; } else if kind == "workflow_error" { state.push_toast(Toast { @@ -450,9 +442,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { crate::dto::chat::message::Role::System, format!("✗ {message}"), )); - if state.misc.overlay == Overlay::Workflow { - state.misc.overlay = Overlay::None; - } + // overlay removed state.dirty = true; } else { state.push_toast(Toast::new(ToastKind::Info, message)); @@ -537,18 +527,14 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { status, }); } - if state.misc.overlay != Overlay::Workflow { - state.misc.overlay = Overlay::Workflow; - } + // popup removed state.dirty = true; } } } if turn_finished { maybe_trigger_review(state); - if state.misc.overlay == Overlay::Workflow { - state.misc.overlay = Overlay::None; - } + } if turn_finished || state.dirty { state.dirty = true; @@ -610,89 +596,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { state.dirty = true; } - Action::RunWorkflow { script } => { - // Open the Workflow overlay so the user can see progress. - state.misc.overlay = Overlay::Workflow; - state.dirty = true; - // Reset engine state before starting. - state.workflow_engine.agents.clear(); - state.workflow_engine.findings.clear(); - - let turn_events = state.turn_events.clone(); - let turn_events_live = state.turn_events.clone(); - - state.push_toast(Toast::new( - ToastKind::Info, - format!("Starting workflow: {}…", script.chars().take(40).collect::()), - )); - - let session_dir = state.session_dir.clone(); - let workspace_roots = state.workspace_roots.clone(); - - std::thread::spawn(move || { - use std::collections::HashMap; - use std::sync::Arc; - use crate::app::workflow::script::{ScriptPrimitive, ScriptOptions, WorkflowScript}; - use crate::app::workflow::engine::{LiveStateFn, AgentStatus}; - - // Parse the script string: - // "prompt1 | prompt2 | prompt3" → Parallel of 3 agents - // "prompt1 -> prompt2" → Pipeline of 2 stages - // "prompt" → single Agent - let parts_pipe: Vec<&str> = script.split('|').map(str::trim).collect(); - let parts_arrow: Vec<&str> = script.split("->").map(str::trim).collect(); - - let primitive = if parts_pipe.len() > 1 { - ScriptPrimitive::Parallel( - parts_pipe.iter().map(|p| ScriptPrimitive::Agent(p.to_string())).collect() - ) - } else if parts_arrow.len() > 1 { - ScriptPrimitive::Pipeline( - parts_arrow.iter().map(|p| ScriptPrimitive::Agent(p.to_string())).collect() - ) - } else { - ScriptPrimitive::Agent(script.clone()) - }; - - let wf = WorkflowScript { - name: script.chars().take(40).collect(), - description: script.clone(), - script: primitive, - options: ScriptOptions::default(), - }; - - // Build a live-state callback that pushes WorkflowAgentUpdate events - // into the turn_events queue so the TUI panel updates in real time. - let live: LiveStateFn = Arc::new(move |agent_id: String, agent_name: String, status: AgentStatus| { - if let Ok(mut q) = turn_events_live.lock() { - q.push_back(crate::app::state::runtime::TurnEvent::WorkflowAgentUpdate { - agent_id: agent_id.clone(), - agent_name, - status, - }); - } - }); - - let args: HashMap = HashMap::new(); - let no_abort: Option> = None; - let result = crate::app::workflow::engine::run_workflow_tracked( - &wf, &args, &no_abort, Some(&live), &session_dir, &workspace_roots, - ); - - let (kind, message) = match result { - Ok(summary) => ("workflow_done".to_string(), summary), - Err(e) => ("workflow_error".to_string(), format!("Workflow failed: {e}")), - }; - - if let Ok(mut q) = turn_events.lock() { - q.push_back(crate::app::state::runtime::TurnEvent::SystemNote { - kind, - message, - }); - } - }); - } } } diff --git a/src/app/runtime/commands.rs b/src/app/runtime/commands.rs index 6ce6b70..ecb036f 100644 --- a/src/app/runtime/commands.rs +++ b/src/app/runtime/commands.rs @@ -60,12 +60,7 @@ pub fn apply_command(command: Command) -> Vec { Command::Compact => { vec![Action::Compact] } - Command::WorkflowOpen => { - vec![Action::OpenOverlay(Overlay::Workflow)] - } - Command::WorkflowRun { script } => { - vec![Action::RunWorkflow { script }] - } + Command::TodoOpen => { vec![Action::OpenOverlay(Overlay::Todo)] } diff --git a/src/app/state/misc.rs b/src/app/state/misc.rs index 748f9db..921281e 100644 --- a/src/app/state/misc.rs +++ b/src/app/state/misc.rs @@ -87,8 +87,7 @@ const COMMANDS: &[&str] = &[ "/model", "/model ls", "/model add", - "/workflow", - "/workflow run", + "/todo", "/usage", "/compact", diff --git a/src/app/state/types.rs b/src/app/state/types.rs index 0d1573e..aae52a0 100644 --- a/src/app/state/types.rs +++ b/src/app/state/types.rs @@ -50,7 +50,6 @@ pub enum Overlay { Settings, Bash, QuitConfirm, - Workflow, KeyInput, Editor, diff --git a/src/controller/command.rs b/src/controller/command.rs index 0b3a793..a4816f9 100644 --- a/src/controller/command.rs +++ b/src/controller/command.rs @@ -17,10 +17,6 @@ pub enum Command { }, ModelList, Compact, - WorkflowOpen, - WorkflowRun { - script: String, - }, TodoOpen, UsageOpen, Unknown(String), @@ -67,14 +63,6 @@ pub fn parse_command(text: &str) -> Command { } "/model" => Command::ModelList, "/compact" => Command::Compact, - "/workflow" if arg1.is_empty() => Command::WorkflowOpen, - "/workflow" if arg1 == "run" && !arg2.is_empty() => Command::WorkflowRun { - script: arg2.to_string(), - }, - "/workflow" if arg1 == "run" => Command::WorkflowOpen, - "/workflow" => Command::WorkflowRun { - script: arg1.to_string(), - }, "/todo" => Command::TodoOpen, "/usage" => Command::UsageOpen, _ => Command::Unknown(cmd.to_string()), diff --git a/src/main.rs b/src/main.rs index 58ec383..85b3bc1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -281,7 +281,7 @@ fn apply_client_update( Some("Bash") => Overlay::Bash, Some("QuitConfirm") => Overlay::QuitConfirm, - Some("Workflow") => Overlay::Workflow, + Some("KeyInput") => Overlay::KeyInput, Some("Editor") => Overlay::Editor, diff --git a/src/resources.rs b/src/resources.rs index 2b3b8a7..dfb8473 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -32,9 +32,7 @@ Input: /help Show help /clear Clear screen /model Select AI model provider - /workflow Open workflow panel - /workflow run

Run a workflow with prompt

- /mode workflow Open workflow panel + /todo Open task list /usage Open usage details /compact Compact conversation history diff --git a/src/view/mod.rs b/src/view/mod.rs index 3f54d17..595366b 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -36,11 +36,13 @@ pub fn draw(frame: &mut Frame, state: &crate::app::state::rest::AppStateRest) { // since this sidebar holds three stacked widgets, not one. let show_sidebar = area.width > SIDEBAR_MIN_WIDTH; let (main_area, sidebar_area) = if show_sidebar { + let has_workflow = !state.workflow_engine.agents.is_empty(); + let sidebar_width = if has_workflow { 48 } else { 30 }; let h_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Min(40), - Constraint::Length(30), + Constraint::Length(sidebar_width), ]) .split(area); (h_chunks[0], Some(h_chunks[1])) @@ -224,10 +226,7 @@ fn render_overlay( frame.render_widget(paragraph, overlay_area); } - // ── Workflow ────────────────────────────────────────────────── - crate::app::state::types::Overlay::Workflow => { - workflow::draw_workflow_panel(frame, overlay_area, state); - } + // ── Key Input ───────────────────────────────────────────────── crate::app::state::types::Overlay::KeyInput => { diff --git a/src/view/sidebar.rs b/src/view/sidebar.rs index 7948744..641414c 100644 --- a/src/view/sidebar.rs +++ b/src/view/sidebar.rs @@ -13,16 +13,28 @@ use super::theme::Theme; /// Render the persistent right-hand dashboard: Workflow, Tasks, and Usage /// widgets stacked in three roughly-equal vertical thirds. pub fn draw_sidebar(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) { + let has_workflow = !state.workflow_engine.agents.is_empty(); + + let constraints = if has_workflow { + vec![ + Constraint::Ratio(1, 2), + Constraint::Ratio(1, 4), + Constraint::Ratio(1, 4), + ] + } else { + vec![ + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + ] + }; + let chunks = Layout::default() .direction(Direction::Vertical) - .constraints([ - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - ]) + .constraints(constraints) .split(area); - super::workflow::draw_workflow_widget(frame, chunks[0], state); + super::workflow::draw_workflow_panel(frame, chunks[0], state); draw_tasks_widget(frame, chunks[1], state); draw_usage_widget(frame, chunks[2], state); } diff --git a/src/view/workflow.rs b/src/view/workflow.rs index adf3fb7..fa7cbcf 100644 --- a/src/view/workflow.rs +++ b/src/view/workflow.rs @@ -14,7 +14,7 @@ use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; use ratatui::Frame; use super::theme::Theme; -use crate::app::workflow::engine::{AgentState, WorkflowAgent}; +use crate::app::workflow::engine::AgentState; /// Icons for agent states. fn state_icon(state: AgentState) -> &'static str { @@ -74,7 +74,6 @@ pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::st header_lines.push(Line::from(vec![ Span::styled("/workflow run ", Style::default().fg(Theme::PRIMARY).add_modifier(Modifier::BOLD)), Span::styled("", Style::default().fg(Theme::TEXT_DIM)), - Span::styled(" · Esc to close", Style::default().fg(Theme::TEXT_DIM)), ])); header_lines.push(Line::from(vec![ Span::styled("Status: ", Style::default().fg(Theme::TEXT_DIM)), @@ -207,47 +206,4 @@ fn build_session_lines(state: &crate::app::state::rest::AppStateRest) -> Vec = if agents.is_empty() { - vec![Line::from(Span::styled( - " No workflow running.", - Style::default().fg(Theme::TEXT_DIM), - ))] - } else { - let show_hint = agents.len() > budget; - let item_budget = if show_hint { budget.saturating_sub(1).max(1) } else { budget }; - let (visible, hidden) = super::split_for_display(agents.as_slice(), item_budget); - let mut lines: Vec = visible.iter().map(workflow_agent_line).collect(); - if show_hint { - lines.push(super::overflow_hint_line(hidden, "/workflow")); - } - lines - }; - - let paragraph = Paragraph::new(lines).block(block); - frame.render_widget(paragraph, area); -} - -/// One compact line for a single agent: state icon + name, state-colored. -fn workflow_agent_line(agent: &WorkflowAgent) -> Line<'static> { - let color = state_color(agent.status.state); - let icon = state_icon(agent.status.state); - Line::from(vec![ - Span::styled(format!(" {icon} "), Style::default().fg(color).add_modifier(Modifier::BOLD)), - Span::styled(agent.name.clone(), Style::default().fg(Theme::TEXT)), - ]) -}