Implement chat and markdown views, enhance status bar, and add workflow panel
- Added `chat.rs` for rendering chat messages with timestamps and roles. - Introduced `markdown.rs` for rendering markdown content with styling. - Created `status.rs` to display the application status bar with session and message counts. - Developed `workflow.rs` to show the current workflow status, including tool calls and active jobs. - Established a `theme.rs` for centralized color management across the UI. - Updated `mod.rs` to include new modules and manage rendering logic.
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
use crate::app::mode::ModeKind;
|
||||
use crate::app::state::rest::AppStateRest;
|
||||
use crate::app::state::types::Overlay;
|
||||
use crate::dto::chat::message::ChatMessage;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[expect(dead_code)]
|
||||
pub enum Action {
|
||||
Quit,
|
||||
ForceQuit,
|
||||
SwitchMode(ModeKind),
|
||||
SubmitInput(String),
|
||||
InsertChar(char),
|
||||
DeleteChar,
|
||||
DeleteCharRight,
|
||||
CursorLeft,
|
||||
CursorRight,
|
||||
HistoryUp,
|
||||
HistoryDown,
|
||||
ScrollUp,
|
||||
ScrollDown,
|
||||
OpenOverlay(Overlay),
|
||||
CloseOverlay,
|
||||
ToggleYoloArm,
|
||||
ToolResult {
|
||||
tool_call_id: String,
|
||||
output: String,
|
||||
is_error: bool,
|
||||
},
|
||||
StreamToken(String),
|
||||
StreamDone,
|
||||
StreamError(String),
|
||||
SystemNote {
|
||||
kind: String,
|
||||
message: String,
|
||||
},
|
||||
RunCommand(String),
|
||||
QuitConfirm,
|
||||
Resize(u16, u16),
|
||||
Tick,
|
||||
RecordUsage {
|
||||
tokens_in: u64,
|
||||
tokens_out: u64,
|
||||
duration_ms: u64,
|
||||
},
|
||||
RecordReviewTokens {
|
||||
tokens: u64,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
match action {
|
||||
Action::Quit => {
|
||||
state.quit = true;
|
||||
}
|
||||
Action::ForceQuit => {
|
||||
state.quit = true;
|
||||
}
|
||||
Action::SwitchMode(mode) => {
|
||||
state.misc.overlay = match mode {
|
||||
ModeKind::Chat
|
||||
| ModeKind::Agents
|
||||
| ModeKind::Bash
|
||||
| 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,
|
||||
ModeKind::Mcp => Overlay::Mcp,
|
||||
ModeKind::Security => Overlay::Security,
|
||||
ModeKind::Todo => Overlay::Todo,
|
||||
ModeKind::Rewind => Overlay::Rewind,
|
||||
ModeKind::Loading => Overlay::Loading,
|
||||
};
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::SubmitInput(text) => {
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
rt.push_message(ChatMessage::user(text.clone()));
|
||||
let api_key = state.settings.api_key.clone();
|
||||
let model = state.settings.model.clone();
|
||||
let msgs = rt.messages.clone();
|
||||
let pending = state.pending_api_response.clone();
|
||||
if let Some(key) = api_key {
|
||||
if !key.is_empty() {
|
||||
std::thread::spawn(move || {
|
||||
let client = crate::service::openrouter::OpenRouterClient::new(key, model);
|
||||
match client.chat(&msgs) {
|
||||
Ok(response) => {
|
||||
if let Ok(mut guard) = pending.lock() {
|
||||
*guard = Some(response);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if let Ok(mut guard) = pending.lock() {
|
||||
*guard = Some(format!("Error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::InsertChar(c) => {
|
||||
state.input.insert(c);
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::DeleteChar => {
|
||||
state.input.delete_left();
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::DeleteCharRight => {
|
||||
state.input.delete_right();
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::CursorLeft => {
|
||||
state.input.char_left();
|
||||
}
|
||||
Action::CursorRight => {
|
||||
state.input.char_right();
|
||||
}
|
||||
Action::HistoryUp => {
|
||||
state.input.history_up();
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::HistoryDown => {
|
||||
state.input.history_down();
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::ScrollUp => {
|
||||
let total = state.transcript_cache.messages.len();
|
||||
state.scroll.scroll_up();
|
||||
state.scroll.scroll_down(total);
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::ScrollDown => {
|
||||
let total = state.transcript_cache.messages.len();
|
||||
state.scroll.scroll_down(total);
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::OpenOverlay(overlay) => {
|
||||
state.misc.overlay = overlay;
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::CloseOverlay => {
|
||||
state.misc.overlay = Overlay::None;
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::ToggleYoloArm => {
|
||||
state.misc.yolo_armed = !state.misc.yolo_armed;
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::ToolResult {
|
||||
tool_call_id,
|
||||
output,
|
||||
is_error,
|
||||
} => {
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
rt.push_message(ChatMessage::tool_result(tool_call_id.clone(), output.clone()));
|
||||
rt.tool_call_results.push(
|
||||
crate::app::state::runtime::ToolCallResult {
|
||||
tool_call_id,
|
||||
tool_name: String::new(),
|
||||
output,
|
||||
is_error,
|
||||
duration_ms: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::StreamToken(token) => {
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
let found = rt.messages.iter_mut().rev().find(|m| {
|
||||
matches!(m.role, crate::dto::chat::message::Role::Assistant)
|
||||
});
|
||||
if let Some(last) = found {
|
||||
let current = last.content.take().unwrap_or_default();
|
||||
last.content = Some(current + &token);
|
||||
} else {
|
||||
let mut msg = ChatMessage::assistant(None);
|
||||
msg.content = Some(token);
|
||||
rt.push_message(msg);
|
||||
}
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::StreamDone => {
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::StreamError(msg) => {
|
||||
let toast = crate::app::state::types::Toast::new(
|
||||
crate::app::state::types::ToastKind::Error,
|
||||
msg,
|
||||
);
|
||||
state.push_toast(toast);
|
||||
}
|
||||
Action::SystemNote { kind: _kind, message } => {
|
||||
let toast = crate::app::state::types::Toast::new(
|
||||
crate::app::state::types::ToastKind::Info,
|
||||
message,
|
||||
);
|
||||
state.push_toast(toast);
|
||||
}
|
||||
Action::RunCommand(text) => {
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
rt.push_message(ChatMessage::user(text));
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::QuitConfirm => {
|
||||
state.misc.overlay = Overlay::QuitConfirm;
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::Resize(w, _h) => {
|
||||
state.scroll.set_max_visible(w as usize);
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::Tick => {
|
||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||
state.misc.drain_expired_toasts(now_ms);
|
||||
let api_response = if let Ok(mut guard) = state.pending_api_response.lock() {
|
||||
guard.take()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(response) = api_response {
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
if response.starts_with("Error:") {
|
||||
let toast = crate::app::state::types::Toast::new(
|
||||
crate::app::state::types::ToastKind::Error,
|
||||
response,
|
||||
);
|
||||
state.push_toast(toast);
|
||||
} else {
|
||||
rt.push_message(ChatMessage::assistant(Some(response)));
|
||||
}
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
}
|
||||
Action::RecordUsage { tokens_in, tokens_out, duration_ms } => {
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
rt.record_api_call(tokens_in, tokens_out, duration_ms);
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::RecordReviewTokens { tokens } => {
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
rt.record_review_tokens(tokens);
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user