feat(protocol): add Paste request type for bracketed-paste events

feat(engine): summarize agent completion progress in UI notifications

feat(main): enable and disable bracketed paste support in terminal

fix(app_config): store API key from file as fallback for Claude provider

refactor(workflow): remove unnecessary card separator in workflow panel
This commit is contained in:
asepharyana
2026-07-15 01:54:36 +07:00
parent e13f040833
commit b92dab97e6
5 changed files with 64 additions and 17 deletions
+22 -11
View File
@@ -278,18 +278,29 @@ fn spawn_single_agent(
// Notify UI: agent completed or failed // Notify UI: agent completed or failed
if let Some(f) = live { if let Some(f) = live {
let summary_from = |text: &str| {
text.lines()
.next()
.unwrap_or(text)
.chars()
.take(80)
.collect::<String>()
};
match &result { match &result {
Ok(_) => f( Ok(text) => {
agent_id.to_string(), let summary = summary_from(text);
agent_name.to_string(), f(
AgentStatus { agent_id.to_string(),
state: AgentState::Completed, agent_name.to_string(),
started_at: Some(started_at), AgentStatus {
completed_at: Some(completed_at), state: AgentState::Completed,
error: None, started_at: Some(started_at),
progress: None, completed_at: Some(completed_at),
}, error: None,
), progress: Some(summary),
},
)
}
Err(e) => f( Err(e) => f(
agent_id.to_string(), agent_id.to_string(),
agent_name.to_string(), agent_name.to_string(),
+2
View File
@@ -45,6 +45,8 @@ pub enum ClientRequest {
shift: bool, shift: bool,
}, },
Submit(String), Submit(String),
/// Bulk-pasted text from a bracketed-paste event.
Paste(String),
Resize(u16, u16), Resize(u16, u16),
Close, Close,
ScrollUp, ScrollUp,
+29 -1
View File
@@ -118,6 +118,7 @@ fn run_single_process() -> Result<()> {
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = io::stdout(); let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?; execute!(stdout, EnterAlternateScreen)?;
execute!(stdout, crossterm::event::EnableBracketedPaste)?;
let backend = CrosstermBackend::new(stdout); let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
terminal.clear()?; terminal.clear()?;
@@ -125,6 +126,7 @@ fn run_single_process() -> Result<()> {
let run_result = run_loop(&mut state, &mut terminal); let run_result = run_loop(&mut state, &mut terminal);
let mut restore_stdout = io::stdout(); let mut restore_stdout = io::stdout();
let _ = execute!(restore_stdout, crossterm::event::DisableBracketedPaste);
let _ = execute!(restore_stdout, LeaveAlternateScreen); let _ = execute!(restore_stdout, LeaveAlternateScreen);
let _ = disable_raw_mode(); let _ = disable_raw_mode();
@@ -408,6 +410,12 @@ fn run_daemon() -> Result<()> {
} }
apply_action(&mut state, Action::Tick); 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) => { ClientRequest::Resize(w, h) => {
apply_action(&mut state, Action::Resize(w, h)); apply_action(&mut state, Action::Resize(w, h));
apply_action(&mut state, Action::Tick); apply_action(&mut state, Action::Tick);
@@ -468,6 +476,7 @@ fn run_attach(session_id: &str) -> Result<()> {
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = io::stdout(); let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?; execute!(stdout, EnterAlternateScreen)?;
execute!(stdout, crossterm::event::EnableBracketedPaste)?;
let backend = CrosstermBackend::new(stdout); let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
terminal.clear()?; 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) => { Event::Resize(w, h) => {
client.send(&ClientRequest::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 _ = execute!(io::stdout(), LeaveAlternateScreen);
let _ = disable_raw_mode(); let _ = disable_raw_mode();
@@ -578,8 +591,9 @@ fn run_loop(
let result = run_loop_inner(state, terminal); let result = run_loop_inner(state, terminal);
if let Err(ref _e) = result { if let Err(ref _e) = result {
let _ = terminal.clear(); let _ = terminal.clear();
let _ = disable_raw_mode(); let _ = disable_raw_mode();
let _ = execute!(io::stdout(), crossterm::event::DisableBracketedPaste);
let _ = execute!(io::stdout(), LeaveAlternateScreen); let _ = execute!(io::stdout(), LeaveAlternateScreen);
} }
result 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) => { Event::Resize(w, h) => {
apply_action(state, Action::Resize(w, h)); apply_action(state, Action::Resize(w, h));
} }
+11 -2
View File
@@ -154,13 +154,22 @@ struct ClaudeSettings {
/// than through its settings file, so reading only the file misses them. /// than through its settings file, so reading only the file misses them.
fn detect_claude_settings_provider() -> Option<ProviderConfig> { fn detect_claude_settings_provider() -> Option<ProviderConfig> {
// Prefer the file, then fall back to env vars. // 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)?; .or_else(claude_credentials_from_env)?;
Some(ProviderConfig { Some(ProviderConfig {
api_base: base_url, api_base: base_url,
// Keep the env-var name so runtime env overrides still work.
api_key_env: Some("ANTHROPIC_API_KEY".to_string()), api_key_env: Some("ANTHROPIC_API_KEY".to_string()),
default_model: None, 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),
}) })
} }
-3
View File
@@ -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)), 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); let list = Paragraph::new(card_lines);