diff --git a/src/app/workflow/engine.rs b/src/app/workflow/engine.rs index 23222cb..7c303fa 100644 --- a/src/app/workflow/engine.rs +++ b/src/app/workflow/engine.rs @@ -278,18 +278,29 @@ fn spawn_single_agent( // Notify UI: agent completed or failed if let Some(f) = live { + let summary_from = |text: &str| { + text.lines() + .next() + .unwrap_or(text) + .chars() + .take(80) + .collect::() + }; match &result { - Ok(_) => f( - agent_id.to_string(), - agent_name.to_string(), - AgentStatus { - state: AgentState::Completed, - started_at: Some(started_at), - completed_at: Some(completed_at), - error: None, - progress: None, - }, - ), + Ok(text) => { + let summary = summary_from(text); + f( + agent_id.to_string(), + agent_name.to_string(), + AgentStatus { + state: AgentState::Completed, + started_at: Some(started_at), + completed_at: Some(completed_at), + error: None, + progress: Some(summary), + }, + ) + } Err(e) => f( agent_id.to_string(), agent_name.to_string(), diff --git a/src/ipc/protocol.rs b/src/ipc/protocol.rs index 7a67bdf..242ab93 100644 --- a/src/ipc/protocol.rs +++ b/src/ipc/protocol.rs @@ -45,6 +45,8 @@ pub enum ClientRequest { shift: bool, }, Submit(String), + /// Bulk-pasted text from a bracketed-paste event. + Paste(String), Resize(u16, u16), Close, ScrollUp, diff --git a/src/main.rs b/src/main.rs index 839835b..36f6b51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -118,6 +118,7 @@ fn run_single_process() -> Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen)?; + execute!(stdout, crossterm::event::EnableBracketedPaste)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; terminal.clear()?; @@ -125,6 +126,7 @@ fn run_single_process() -> Result<()> { let run_result = run_loop(&mut state, &mut terminal); let mut restore_stdout = io::stdout(); + let _ = execute!(restore_stdout, crossterm::event::DisableBracketedPaste); let _ = execute!(restore_stdout, LeaveAlternateScreen); let _ = disable_raw_mode(); @@ -408,6 +410,12 @@ fn run_daemon() -> Result<()> { } apply_action(&mut state, Action::Tick); } + ClientRequest::Paste(text) => { + state.input.buffer.insert_str(state.input.cursor, &text); + state.input.cursor += text.len(); + state.dirty = true; + apply_action(&mut state, Action::Tick); + } ClientRequest::Resize(w, h) => { apply_action(&mut state, Action::Resize(w, h)); apply_action(&mut state, Action::Tick); @@ -468,6 +476,7 @@ fn run_attach(session_id: &str) -> Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen)?; + execute!(stdout, crossterm::event::EnableBracketedPaste)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; terminal.clear()?; @@ -516,6 +525,9 @@ fn run_attach(session_id: &str) -> Result<()> { } } } + Event::Paste(text) => { + client.send(&ClientRequest::Paste(text))?; + } Event::Resize(w, h) => { client.send(&ClientRequest::Resize(w, h))?; } @@ -555,6 +567,7 @@ fn run_attach(session_id: &str) -> Result<()> { })?; } + let _ = execute!(io::stdout(), crossterm::event::DisableBracketedPaste); let _ = execute!(io::stdout(), LeaveAlternateScreen); let _ = disable_raw_mode(); @@ -578,8 +591,9 @@ fn run_loop( let result = run_loop_inner(state, terminal); if let Err(ref _e) = result { let _ = terminal.clear(); - + let _ = disable_raw_mode(); + let _ = execute!(io::stdout(), crossterm::event::DisableBracketedPaste); let _ = execute!(io::stdout(), LeaveAlternateScreen); } result @@ -625,6 +639,20 @@ fn run_loop_inner( } } } + Event::Paste(text) => { + // Insert pasted text as a single bulk operation instead of + // character-by-character, avoiding O(n^2) String::insert() + // and preventing stray newline/control-byte misinterpretation. + if state.input.autocomplete_visible { + state.input.close_autocomplete(); + } + state.input.buffer.insert_str(state.input.cursor, &text); + state.input.cursor += text.len(); + if state.input.buffer.starts_with('/') { + state.input.open_autocomplete(); + } + state.dirty = true; + } Event::Resize(w, h) => { apply_action(state, Action::Resize(w, h)); } diff --git a/src/model/app_config.rs b/src/model/app_config.rs index fc3fd78..4ca00d9 100644 --- a/src/model/app_config.rs +++ b/src/model/app_config.rs @@ -154,13 +154,22 @@ struct ClaudeSettings { /// than through its settings file, so reading only the file misses them. fn detect_claude_settings_provider() -> Option { // Prefer the file, then fall back to env vars. - let (base_url, _key) = claude_credentials_from_file() + let (base_url, key) = claude_credentials_from_file() .or_else(claude_credentials_from_env)?; Some(ProviderConfig { api_base: base_url, + // Keep the env-var name so runtime env overrides still work. api_key_env: Some("ANTHROPIC_API_KEY".to_string()), default_model: None, - default_api_key: None, + // Store the key read from the file as a direct fallback. + // Without this, subagent/engine.rs resolve_provider_config() falls + // through to std::env::var("ANTHROPIC_API_KEY") which is only + // injected into the Claude Code process — not into zesdex. Workflow + // nodes therefore got an empty key and failed with + // "no API key configured for provider 'claude'", even though the + // main agent succeeded (it has a DEFAULT_API_KEY fallback that + // subagents intentionally do not have). + default_api_key: Some(key), }) } diff --git a/src/view/workflow.rs b/src/view/workflow.rs index 4dfa2e5..455d30a 100644 --- a/src/view/workflow.rs +++ b/src/view/workflow.rs @@ -146,9 +146,6 @@ pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::st Span::styled(prog.clone(), Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC)), ])); } - - // Card separator - card_lines.push(Line::from(Span::raw(""))); } let list = Paragraph::new(card_lines);