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,40 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StateDiff {
|
||||
changes: Vec<Change>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Change {
|
||||
pub path: String,
|
||||
pub kind: String,
|
||||
}
|
||||
|
||||
impl StateDiff {
|
||||
pub fn new() -> Self {
|
||||
StateDiff { changes: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn add_change(&mut self, path: String, kind: String) {
|
||||
self.changes.push(Change { path, kind });
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.changes.is_empty()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.changes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_diff(before: &serde_json::Value, after: &serde_json::Value) -> Vec<Change> {
|
||||
if before == after {
|
||||
return Vec::new();
|
||||
}
|
||||
vec![Change {
|
||||
path: ".".to_string(),
|
||||
kind: "modified".to_string(),
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use super::types::Overlay;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DirCache {
|
||||
entries: Arc<RwLock<Vec<PathBuf>>>,
|
||||
}
|
||||
|
||||
impl DirCache {
|
||||
pub fn new() -> Self {
|
||||
DirCache {
|
||||
entries: Arc::new(RwLock::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set(&self, paths: Vec<PathBuf>) {
|
||||
let mut w = self.entries.write().await;
|
||||
*w = paths;
|
||||
}
|
||||
|
||||
pub async fn get(&self) -> Vec<PathBuf> {
|
||||
let r = self.entries.read().await;
|
||||
r.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScrollState {
|
||||
pub offset: usize,
|
||||
pub max_visible: usize,
|
||||
}
|
||||
|
||||
impl ScrollState {
|
||||
pub fn new() -> Self {
|
||||
ScrollState {
|
||||
offset: 0,
|
||||
max_visible: 30,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_up(&mut self) {
|
||||
if self.offset > 0 {
|
||||
self.offset -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_down(&mut self, total: usize) {
|
||||
let max_offset = total.saturating_sub(self.max_visible);
|
||||
if self.offset < max_offset {
|
||||
self.offset += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_to_bottom(&mut self, total: usize) {
|
||||
self.offset = total.saturating_sub(self.max_visible);
|
||||
}
|
||||
|
||||
pub fn set_max_visible(&mut self, max: usize) {
|
||||
self.max_visible = max;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InputState {
|
||||
pub buffer: String,
|
||||
pub cursor: usize,
|
||||
pub history: Vec<String>,
|
||||
pub history_idx: Option<usize>,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub fn new() -> Self {
|
||||
InputState {
|
||||
buffer: String::new(),
|
||||
cursor: 0,
|
||||
history: Vec::new(),
|
||||
history_idx: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn char_left(&mut self) {
|
||||
if self.cursor > 0 {
|
||||
self.cursor -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn char_right(&mut self) {
|
||||
if self.cursor < self.buffer.len() {
|
||||
self.cursor += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, c: char) {
|
||||
self.buffer.insert(self.cursor, c);
|
||||
self.cursor += 1;
|
||||
}
|
||||
|
||||
pub fn delete_left(&mut self) {
|
||||
if self.cursor > 0 {
|
||||
self.cursor -= 1;
|
||||
self.buffer.remove(self.cursor);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_right(&mut self) {
|
||||
if self.cursor < self.buffer.len() {
|
||||
self.buffer.remove(self.cursor);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn submit(&mut self) -> String {
|
||||
let result = self.buffer.clone();
|
||||
if !result.is_empty() {
|
||||
self.history.push(result.clone());
|
||||
self.history_idx = None;
|
||||
}
|
||||
self.buffer.clear();
|
||||
self.cursor = 0;
|
||||
result
|
||||
}
|
||||
|
||||
pub fn history_up(&mut self) {
|
||||
if self.history.is_empty() {
|
||||
return;
|
||||
}
|
||||
let idx = match self.history_idx {
|
||||
Some(i) if i > 0 => i - 1,
|
||||
None => self.history.len() - 1,
|
||||
Some(_) => return,
|
||||
};
|
||||
self.history_idx = Some(idx);
|
||||
self.buffer = self.history[idx].clone();
|
||||
self.cursor = self.buffer.len();
|
||||
}
|
||||
|
||||
pub fn history_down(&mut self) {
|
||||
match self.history_idx {
|
||||
Some(i) if i < self.history.len() - 1 => {
|
||||
let idx = i + 1;
|
||||
self.history_idx = Some(idx);
|
||||
self.buffer = self.history[idx].clone();
|
||||
self.cursor = self.buffer.len();
|
||||
}
|
||||
Some(_) => {
|
||||
self.history_idx = None;
|
||||
self.buffer.clear();
|
||||
self.cursor = 0;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.buffer.clear();
|
||||
self.cursor = 0;
|
||||
self.history_idx = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MiscState {
|
||||
pub overlay: Overlay,
|
||||
pub toasts: Vec<super::types::Toast>,
|
||||
pub dirty: bool,
|
||||
pub yolo_armed: bool,
|
||||
pub security_armed: bool,
|
||||
pub security_acknowledged: bool,
|
||||
pub esc_press_count: u32,
|
||||
}
|
||||
|
||||
impl MiscState {
|
||||
pub fn new() -> Self {
|
||||
MiscState {
|
||||
overlay: Overlay::None,
|
||||
toasts: Vec::new(),
|
||||
dirty: true,
|
||||
yolo_armed: false,
|
||||
security_armed: false,
|
||||
security_acknowledged: false,
|
||||
esc_press_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_toast(&mut self, toast: super::types::Toast) {
|
||||
self.toasts.push(toast);
|
||||
}
|
||||
|
||||
pub fn drain_expired_toasts(&mut self, now_ms: i64) -> Vec<super::types::Toast> {
|
||||
let expired: Vec<_> = self.toasts.iter().filter(|t| t.expired(now_ms)).cloned().collect();
|
||||
self.toasts.retain(|t| !t.expired(now_ms));
|
||||
expired
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod input;
|
||||
pub mod misc;
|
||||
pub mod rest;
|
||||
pub mod runtime;
|
||||
pub mod scroll;
|
||||
pub mod diff;
|
||||
pub mod snapshot;
|
||||
pub mod types;
|
||||
@@ -0,0 +1,125 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::misc::{DirCache, InputState, MiscState, ScrollState};
|
||||
use super::runtime::SessionRuntime;
|
||||
use super::types::{AgentMode, Origin, Toast, TranscriptCache};
|
||||
use crate::model::editlog::EditLog;
|
||||
use crate::model::settings::Settings;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ChatMessageDisplay {
|
||||
pub role: crate::dto::chat::message::Role,
|
||||
pub content: String,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
impl ChatMessageDisplay {
|
||||
pub fn new(role: crate::dto::chat::message::Role, content: String) -> Self {
|
||||
ChatMessageDisplay {
|
||||
role,
|
||||
content,
|
||||
timestamp: chrono::Utc::now().timestamp_millis(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CronJob {
|
||||
pub id: String,
|
||||
pub description: String,
|
||||
pub cron_expr: String,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppStateRest {
|
||||
pub mode: AgentMode,
|
||||
pub settings: Settings,
|
||||
pub workspace_roots: Vec<PathBuf>,
|
||||
pub session_dir: PathBuf,
|
||||
pub memory_dir: PathBuf,
|
||||
pub download_dir: PathBuf,
|
||||
pub worktrees_dir: PathBuf,
|
||||
pub current_dir: PathBuf,
|
||||
pub dir_cache: Arc<RwLock<DirCache>>,
|
||||
pub edit_log: EditLog,
|
||||
pub session_runtime: Option<SessionRuntime>,
|
||||
pub sessions: Vec<crate::model::session::Session>,
|
||||
pub crons: Vec<CronJob>,
|
||||
pub transcript_cache: TranscriptCache,
|
||||
pub scroll: ScrollState,
|
||||
pub input: InputState,
|
||||
pub misc: MiscState,
|
||||
pub pending_api_response: Arc<Mutex<Option<String>>>,
|
||||
pub dirty: bool,
|
||||
pub quit: bool,
|
||||
}
|
||||
|
||||
impl AppStateRest {
|
||||
pub fn new(workspace_roots: Vec<PathBuf>, session_dir: PathBuf, memory_dir: PathBuf) -> Self {
|
||||
let settings = Settings::load();
|
||||
let download_dir = memory_dir.parent().unwrap_or(&memory_dir).join("downloads");
|
||||
let worktrees_dir = memory_dir.parent().unwrap_or(&memory_dir).join("worktrees");
|
||||
let dir_cache = DirCache::new();
|
||||
AppStateRest {
|
||||
mode: AgentMode::Normal,
|
||||
settings,
|
||||
workspace_roots,
|
||||
session_dir: session_dir.clone(),
|
||||
memory_dir,
|
||||
download_dir,
|
||||
worktrees_dir,
|
||||
current_dir: std::env::current_dir().unwrap_or_default(),
|
||||
pending_api_response: Arc::new(Mutex::new(None)),
|
||||
dir_cache: Arc::new(RwLock::new(dir_cache)),
|
||||
edit_log: EditLog::new(&session_dir),
|
||||
session_runtime: Some(SessionRuntime::new(session_dir.clone())),
|
||||
sessions: Vec::new(),
|
||||
crons: Vec::new(),
|
||||
transcript_cache: TranscriptCache::new(200),
|
||||
scroll: ScrollState::new(),
|
||||
input: InputState::new(),
|
||||
misc: MiscState::new(),
|
||||
dirty: true,
|
||||
quit: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> AgentMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, mode: AgentMode) {
|
||||
self.mode = mode;
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
pub fn push_transcript(&mut self, msg: ChatMessageDisplay) {
|
||||
self.transcript_cache.messages.push(msg);
|
||||
if self.transcript_cache.messages.len() > self.transcript_cache.max_lines {
|
||||
self.transcript_cache.messages.remove(0);
|
||||
}
|
||||
self.transcript_cache.dirty = true;
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
pub fn push_toast(&mut self, toast: Toast) {
|
||||
self.misc.push_toast(toast);
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
pub fn tool_ctx(&self) -> crate::tool::ToolCtx {
|
||||
crate::tool::ToolCtx {
|
||||
workspaces: self.workspace_roots.clone(),
|
||||
session_dir: self.session_dir.clone(),
|
||||
memory_dir: self.memory_dir.clone(),
|
||||
download_dir: self.download_dir.clone(),
|
||||
worktrees_dir: self.worktrees_dir.clone(),
|
||||
dir_cache: self.dir_cache.clone(),
|
||||
internet_mode: self.settings.internet_mode.clone(),
|
||||
origin: Origin::Main,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
use std::path::PathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
|
||||
pub struct UsageStats {
|
||||
pub tokens_in: u64,
|
||||
pub tokens_out: u64,
|
||||
pub api_calls: u64,
|
||||
pub review_tokens: u64,
|
||||
pub total_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionRuntime {
|
||||
pub messages: Vec<crate::dto::chat::message::ChatMessage>,
|
||||
pub tool_call_results: Vec<ToolCallResult>,
|
||||
pub pending_tool_queue: Vec<PendingTool>,
|
||||
pub bash_jobs: Vec<BashJobRef>,
|
||||
pub subagent_queue: usize,
|
||||
pub edit_count: u32,
|
||||
pub consecutive_empty_reviews: u32,
|
||||
pub session_start: i64,
|
||||
pub lesson_count: u32,
|
||||
pub lessons_user: u32,
|
||||
pub lessons_feedback: u32,
|
||||
pub lessons_project: u32,
|
||||
pub lessons_reference: u32,
|
||||
pub lessons_active: u32,
|
||||
pub lessons_stale: u32,
|
||||
pub lessons_contradicted: u32,
|
||||
pub lessons_human: u32,
|
||||
pub lessons_verified: u32,
|
||||
pub lessons_unverified: u32,
|
||||
pub review_count: u32,
|
||||
pub session_dir: PathBuf,
|
||||
pub usage: UsageStats,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolCallResult {
|
||||
pub tool_call_id: String,
|
||||
pub tool_name: String,
|
||||
pub output: String,
|
||||
pub is_error: bool,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PendingTool {
|
||||
pub tool_name: String,
|
||||
pub args: serde_json::Value,
|
||||
pub execution_model: crate::app::state::types::ExecutionModel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BashJobRef {
|
||||
pub id: String,
|
||||
pub command: String,
|
||||
pub started_at: i64,
|
||||
pub running: bool,
|
||||
}
|
||||
|
||||
impl SessionRuntime {
|
||||
pub fn new(session_dir: PathBuf) -> Self {
|
||||
SessionRuntime {
|
||||
messages: Vec::new(),
|
||||
tool_call_results: Vec::new(),
|
||||
pending_tool_queue: Vec::new(),
|
||||
bash_jobs: Vec::new(),
|
||||
subagent_queue: 0,
|
||||
edit_count: 0,
|
||||
consecutive_empty_reviews: 0,
|
||||
session_start: chrono::Utc::now().timestamp_millis(),
|
||||
lesson_count: 0,
|
||||
lessons_user: 0,
|
||||
lessons_feedback: 0,
|
||||
lessons_project: 0,
|
||||
lessons_reference: 0,
|
||||
lessons_active: 0,
|
||||
lessons_stale: 0,
|
||||
lessons_contradicted: 0,
|
||||
lessons_human: 0,
|
||||
lessons_verified: 0,
|
||||
lessons_unverified: 0,
|
||||
review_count: 0,
|
||||
session_dir,
|
||||
usage: UsageStats::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_message(&mut self, msg: crate::dto::chat::message::ChatMessage) {
|
||||
self.messages.push(msg);
|
||||
}
|
||||
|
||||
pub fn record_api_call(&mut self, tokens_in: u64, tokens_out: u64, duration_ms: u64) {
|
||||
self.usage.tokens_in += tokens_in;
|
||||
self.usage.tokens_out += tokens_out;
|
||||
self.usage.api_calls += 1;
|
||||
self.usage.total_ms += duration_ms;
|
||||
}
|
||||
|
||||
pub fn record_review_tokens(&mut self, tokens: u64) {
|
||||
self.usage.review_tokens += tokens;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StateSnapshot {
|
||||
pub snapshot: serde_json::Value,
|
||||
}
|
||||
|
||||
impl StateSnapshot {
|
||||
pub fn new() -> Self {
|
||||
StateSnapshot {
|
||||
snapshot: serde_json::json!({}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_snapshot(snapshot: &StateSnapshot) -> anyhow::Result<Vec<u8>> {
|
||||
Ok(serde_json::to_vec(snapshot)?)
|
||||
}
|
||||
|
||||
pub fn deserialize_snapshot(data: &[u8]) -> anyhow::Result<StateSnapshot> {
|
||||
Ok(serde_json::from_slice(data)?)
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AgentMode {
|
||||
Auto,
|
||||
Normal,
|
||||
Plan,
|
||||
Yolo,
|
||||
}
|
||||
|
||||
impl AgentMode {
|
||||
pub fn auto_approve(&self) -> bool {
|
||||
matches!(self, AgentMode::Auto | AgentMode::Yolo)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
AgentMode::Auto => "Auto",
|
||||
AgentMode::Normal => "Normal",
|
||||
AgentMode::Plan => "Plan",
|
||||
AgentMode::Yolo => "Yolo",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PanelKind {
|
||||
Chat,
|
||||
Agents,
|
||||
Bash,
|
||||
Workflow,
|
||||
Help,
|
||||
SessionHub,
|
||||
}
|
||||
|
||||
impl PanelKind {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
PanelKind::Chat => "Chat",
|
||||
PanelKind::Agents => "Agents",
|
||||
PanelKind::Bash => "Bash",
|
||||
PanelKind::Workflow => "Workflow",
|
||||
PanelKind::Help => "Help",
|
||||
PanelKind::SessionHub => "Sessions",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ToastKind {
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
Lesson,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Toast {
|
||||
pub kind: ToastKind,
|
||||
pub message: String,
|
||||
pub created_at: i64,
|
||||
pub lifetime_ms: u64,
|
||||
}
|
||||
|
||||
impl Toast {
|
||||
pub fn new(kind: ToastKind, message: String) -> Self {
|
||||
Toast {
|
||||
kind,
|
||||
message,
|
||||
created_at: chrono::Utc::now().timestamp_millis(),
|
||||
lifetime_ms: 5000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expired(&self, now_ms: i64) -> bool {
|
||||
now_ms - self.created_at > self.lifetime_ms as i64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Overlay {
|
||||
None,
|
||||
Help,
|
||||
Settings,
|
||||
Agents,
|
||||
Bash,
|
||||
QuitConfirm,
|
||||
SessionHub,
|
||||
Workflow,
|
||||
Onboard,
|
||||
OnboardProvider,
|
||||
Picker,
|
||||
KeyInput,
|
||||
Editor,
|
||||
Effort,
|
||||
Mcp,
|
||||
Security,
|
||||
Todo,
|
||||
Rewind,
|
||||
Learning,
|
||||
Usage,
|
||||
Loading,
|
||||
}
|
||||
|
||||
impl Overlay {
|
||||
pub fn is_active(self) -> bool {
|
||||
!matches!(self, Overlay::None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TranscriptCache {
|
||||
pub messages: Vec<super::rest::ChatMessageDisplay>,
|
||||
pub max_lines: usize,
|
||||
pub dirty: bool,
|
||||
}
|
||||
|
||||
impl TranscriptCache {
|
||||
pub fn new(max_lines: usize) -> Self {
|
||||
TranscriptCache {
|
||||
messages: Vec::new(),
|
||||
max_lines,
|
||||
dirty: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ExecutionModel {
|
||||
Inline,
|
||||
Deferred,
|
||||
AsyncTokio,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub enum Origin {
|
||||
Main,
|
||||
SubAgent,
|
||||
Reviewer,
|
||||
}
|
||||
Reference in New Issue
Block a user