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
+37
View File
@@ -0,0 +1,37 @@
use anyhow::Result;
use super::conn::Connection;
pub struct IpcClient {
conn: Connection,
}
impl IpcClient {
pub fn connect_tcp(addr: &str) -> Result<Self> {
let conn = Connection::connect_tcp(addr)?;
Ok(IpcClient { conn })
}
pub fn connect_unix(path: &str) -> Result<Self> {
let conn = Connection::connect_unix(path)?;
Ok(IpcClient { conn })
}
pub fn send<T: serde::Serialize>(&mut self, value: &T) -> Result<()> {
self.conn.send(value)
}
pub fn receive<T: serde::de::DeserializeOwned>(&mut self) -> Result<Option<T>> {
self.conn.receive()
}
pub fn request<T: serde::Serialize, R: serde::de::DeserializeOwned>(
&mut self,
request: &T,
) -> Result<R> {
self.conn.send(request)?;
match self.conn.receive::<R>()? {
Some(response) => Ok(response),
None => anyhow::bail!("connection closed before response"),
}
}
}
+57
View File
@@ -0,0 +1,57 @@
use std::net::TcpStream;
use std::os::unix::net::UnixStream;
use anyhow::Result;
use super::frame;
pub enum Connection {
Tcp(TcpStream),
Unix(UnixStream),
}
impl Connection {
pub fn connect_tcp(addr: &str) -> Result<Self> {
let stream = TcpStream::connect(addr)?;
stream.set_nodelay(true)?;
Ok(Connection::Tcp(stream))
}
pub fn connect_unix(path: &str) -> Result<Self> {
let stream = UnixStream::connect(path)?;
Ok(Connection::Unix(stream))
}
pub fn send<T: serde::Serialize>(&mut self, value: &T) -> Result<()> {
let data = frame::serialize_frame(value)?;
match self {
Connection::Tcp(ref mut s) => frame::write_frame(s, &data),
Connection::Unix(ref mut s) => frame::write_frame(s, &data),
}
}
pub fn receive<T: serde::de::DeserializeOwned>(&mut self) -> Result<Option<T>> {
let data = match self {
Connection::Tcp(ref mut s) => frame::read_frame(s)?,
Connection::Unix(ref mut s) => frame::read_frame(s)?,
};
match data {
Some(bytes) => {
let value: T = frame::deserialize_frame(&bytes)?;
Ok(Some(value))
}
None => Ok(None),
}
}
pub fn try_clone(&self) -> Result<Self> {
match self {
Connection::Tcp(s) => {
let cloned = s.try_clone()?;
Ok(Connection::Tcp(cloned))
}
Connection::Unix(s) => {
let cloned = s.try_clone()?;
Ok(Connection::Unix(cloned))
}
}
}
}
+86
View File
@@ -0,0 +1,86 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateDiff {
pub timestamp: i64,
pub session_id: String,
pub changes: Vec<Change>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Change {
pub path: String,
pub old_value: Option<Value>,
pub new_value: Option<Value>,
}
impl StateDiff {
pub fn new(session_id: String) -> Self {
StateDiff {
timestamp: chrono::Utc::now().timestamp_millis(),
session_id,
changes: Vec::new(),
}
}
pub fn add_change(&mut self, path: String, old_value: Option<Value>, new_value: Option<Value>) {
self.changes.push(Change {
path,
old_value,
new_value,
});
}
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
pub fn clear(&mut self) {
self.changes.clear();
self.timestamp = chrono::Utc::now().timestamp_millis();
}
}
pub fn compute_diff(before: &Value, after: &Value, path: &str, changes: &mut Vec<Change>) {
if before == after {
return;
}
match (before, after) {
(Value::Object(b_map), Value::Object(a_map)) => {
let mut all_keys: Vec<&str> = Vec::new();
for key in b_map.keys() {
if !all_keys.contains(&key.as_str()) {
all_keys.push(key.as_str());
}
}
for key in a_map.keys() {
if !all_keys.contains(&key.as_str()) {
all_keys.push(key.as_str());
}
}
for key in all_keys {
let child_path = if path.is_empty() {
key.to_string()
} else {
format!("{}.{}", path, key)
};
let b_val = b_map.get(key);
let a_val = a_map.get(key);
compute_diff(
b_val.unwrap_or(&Value::Null),
a_val.unwrap_or(&Value::Null),
&child_path,
changes,
);
}
}
_ => {
changes.push(Change {
path: path.to_string(),
old_value: Some(before.clone()),
new_value: Some(after.clone()),
});
}
}
}
+44
View File
@@ -0,0 +1,44 @@
use std::io::{Read, Write};
use anyhow::Result;
pub(crate) const MAX_FRAME_SIZE: usize = 64 * 1024 * 1024;
pub fn write_frame<W: Write>(writer: &mut W, data: &[u8]) -> Result<()> {
let len = data.len();
if len > MAX_FRAME_SIZE {
anyhow::bail!("frame too large: {} bytes exceeds 64 MiB limit", len);
}
let len_bytes = (len as u32).to_be_bytes();
writer.write_all(&len_bytes)?;
writer.write_all(data)?;
writer.flush()?;
Ok(())
}
pub fn read_frame<R: Read>(reader: &mut R) -> Result<Option<Vec<u8>>> {
let mut len_buf = [0u8; 4];
match reader.read_exact(&mut len_buf) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e.into()),
}
let len = u32::from_be_bytes(len_buf) as usize;
if len > MAX_FRAME_SIZE {
anyhow::bail!("frame too large: {} bytes exceeds 64 MiB limit", len);
}
let mut buf = vec![0u8; len];
reader.read_exact(&mut buf)?;
Ok(Some(buf))
}
pub fn serialize_frame<T: serde::Serialize>(value: &T) -> Result<Vec<u8>> {
let json = serde_json::to_vec(value)?;
if json.len() > MAX_FRAME_SIZE {
anyhow::bail!("serialized frame too large: {} bytes", json.len());
}
Ok(json)
}
pub fn deserialize_frame<'a, T: serde::Deserialize<'a>>(data: &'a [u8]) -> Result<T> {
Ok(serde_json::from_slice(data)?)
}
+6
View File
@@ -0,0 +1,6 @@
pub mod client;
pub mod conn;
pub mod diff;
pub mod frame;
pub mod server;
pub mod snapshot;
+42
View File
@@ -0,0 +1,42 @@
use std::net::TcpListener;
use std::thread;
use anyhow::Result;
use super::conn::Connection;
pub struct IpcServer {
listener: TcpListener,
}
impl IpcServer {
pub fn bind(addr: &str) -> Result<Self> {
let listener = TcpListener::bind(addr)?;
Ok(IpcServer { listener })
}
pub fn accept(&self) -> Result<Connection> {
let (stream, _addr) = self.listener.accept()?;
stream.set_nodelay(true)?;
Ok(Connection::Tcp(stream))
}
pub fn accept_with_handler<F>(self, handler: F) -> thread::JoinHandle<()>
where
F: Fn(Connection) -> Result<()> + Send + 'static,
{
thread::spawn(move || {
for stream in self.listener.incoming() {
match stream {
Ok(stream) => {
if let Err(e) = handler(Connection::Tcp(stream)) {
eprintln!("ipc handler error: {}", e);
}
}
Err(e) => {
eprintln!("ipc accept error: {}", e);
break;
}
}
}
})
}
}
+41
View File
@@ -0,0 +1,41 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateSnapshot {
pub timestamp: i64,
pub mode: String,
pub session_id: String,
pub message_count: usize,
pub edit_count: u32,
pub dirty: bool,
pub overlay_active: bool,
pub model: String,
pub payload: Value,
}
impl StateSnapshot {
pub fn new(session_id: String, mode: String, model: String) -> Self {
StateSnapshot {
timestamp: chrono::Utc::now().timestamp_millis(),
mode,
session_id,
message_count: 0,
edit_count: 0,
dirty: true,
overlay_active: false,
model,
payload: serde_json::json!({}),
}
}
}
pub fn serialize_snapshot(snapshot: &StateSnapshot) -> anyhow::Result<Vec<u8>> {
let data = serde_json::to_vec(snapshot)?;
Ok(data)
}
pub fn deserialize_snapshot(data: &[u8]) -> anyhow::Result<StateSnapshot> {
let snapshot: StateSnapshot = serde_json::from_slice(data)?;
Ok(snapshot)
}