feat: add editing and MCP command handling, enhance SSE streaming with usage tracking

- Implemented `Edit` and `McpAdd` commands in the command parser and handler.
- Added a new `stream` module to the runtime for handling streaming events.
- Enhanced `SseParser` to parse usage information from SSE events.
- Introduced `ToolCallAccumulator` for tracking tool calls independently.
- Updated `AppStateRest` to include `app_config` and `MiscState` to track `effort_level` and `selected_index`.
- Modified `LlmClient` to support streaming responses with usage tracking.
- Improved error handling and retry logic in the streaming API calls.
- Added tests for new features and improved markdown rendering in the chat view.
This commit is contained in:
asepharyana
2026-07-12 01:25:52 +07:00
parent fcef85a327
commit 18a41aad48
28 changed files with 838 additions and 98 deletions
+117
View File
@@ -8,6 +8,53 @@ use crate::app::state::types::Overlay;
use crate::controller::command::parse_command;
pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
// While Editor overlay is active, route input directly to the editor handler
if state.misc.overlay == Overlay::Editor {
match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
return vec![Action::QuitConfirm];
}
KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if let Some(ref ed) = state.misc.editor.clone() {
let content = ed.as_string();
if let Err(e) = std::fs::write(&ed.path, &content) {
state.push_toast(crate::app::state::types::Toast::new(
crate::app::state::types::ToastKind::Error,
format!("Save failed: {}", e),
));
} else {
state.push_toast(crate::app::state::types::Toast::new(
crate::app::state::types::ToastKind::Success,
format!("Saved {}", ed.path),
));
}
state.dirty = true;
}
return vec![];
}
KeyCode::Esc => {
crate::app::mode::editor::handle_editor_dismiss(state);
return vec![];
}
KeyCode::Backspace => {
if let Some(ref mut ed) = state.misc.editor {
ed.delete_left();
state.dirty = true;
}
return vec![];
}
KeyCode::Enter => {
crate::app::mode::editor::handle_editor_input(state, "\n".to_string());
return vec![];
}
KeyCode::Char(c) => {
crate::app::mode::editor::handle_editor_input(state, c.to_string());
return vec![];
}
_ => return vec![],
}
}
match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
vec![Action::QuitConfirm]
@@ -40,6 +87,19 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
KeyCode::Up => {
if key.modifiers.contains(KeyModifiers::CONTROL) {
vec![Action::HistoryUp]
} else if state.misc.overlay == Overlay::Effort {
mode::effort::cycle_effort(state);
Vec::new()
} else if state.misc.overlay == Overlay::OnboardProvider {
let n = mode::onboard_provider::PROVIDERS.len();
state.misc.selected_index = if state.misc.selected_index == 0 { n - 1 } else { state.misc.selected_index - 1 };
state.dirty = true;
Vec::new()
} else if state.misc.overlay == Overlay::Rewind {
let n = mode::rewind::rewind_count(state);
state.misc.selected_index = if state.misc.selected_index == 0 { n.saturating_sub(1) } else { state.misc.selected_index - 1 };
state.dirty = true;
Vec::new()
} else {
vec![Action::ScrollUp]
}
@@ -47,6 +107,19 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
KeyCode::Down => {
if key.modifiers.contains(KeyModifiers::CONTROL) {
vec![Action::HistoryDown]
} else if state.misc.overlay == Overlay::Effort {
mode::effort::cycle_effort(state);
Vec::new()
} else if state.misc.overlay == Overlay::OnboardProvider {
let n = mode::onboard_provider::PROVIDERS.len();
state.misc.selected_index = (state.misc.selected_index + 1) % n;
state.dirty = true;
Vec::new()
} else if state.misc.overlay == Overlay::Rewind {
let n = mode::rewind::rewind_count(state);
state.misc.selected_index = if n == 0 { 0 } else { (state.misc.selected_index + 1) % n };
state.dirty = true;
Vec::new()
} else {
vec![Action::ScrollDown]
}
@@ -134,6 +207,50 @@ fn handle_overlay_enter(state: &mut AppStateRest) -> Vec<Action> {
Overlay::QuitConfirm => {
vec![mode::quit_confirm::handle_quit_confirm(true)]
}
Overlay::KeyInput => {
let text = state.input.buffer.clone();
mode::key_input::handle_key_text(state, text.clone());
state.settings.api_key = if text.is_empty() { None } else { Some(text) };
let _ = state.settings.save();
state.input.buffer.clear();
state.input.cursor = 0;
state.misc.overlay = Overlay::None;
state.push_toast(crate::app::state::types::Toast::new(
crate::app::state::types::ToastKind::Success,
"API key saved".to_string(),
));
state.dirty = true;
Vec::new()
}
Overlay::Onboard => {
state.misc.overlay = Overlay::OnboardProvider;
state.misc.selected_index = 0;
state.dirty = true;
Vec::new()
}
Overlay::OnboardProvider => {
let idx = state.misc.selected_index.min(mode::onboard_provider::PROVIDERS.len() - 1);
let provider = mode::onboard_provider::PROVIDERS[idx];
mode::onboard_provider::set_provider(&mut state.settings, provider);
state.push_toast(crate::app::state::types::Toast::new(
crate::app::state::types::ToastKind::Info,
format!("Provider set to {}", provider),
));
state.misc.overlay = Overlay::KeyInput;
state.input.buffer.clear();
state.input.cursor = 0;
state.dirty = true;
Vec::new()
}
Overlay::Mcp => {
mode::mcp::connect_mcp(state, "");
Vec::new()
}
Overlay::Rewind => {
let idx = state.misc.selected_index;
mode::rewind::rewind_to(state, idx);
Vec::new()
}
_ => Vec::new(),
}
}