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:
asepharyana
2026-07-11 13:16:10 +07:00
parent 7cb4ae6708
commit cc03bd79b6
131 changed files with 10994 additions and 0 deletions
+263
View File
@@ -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;
}
}
}