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,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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)?)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod client;
|
||||
pub mod conn;
|
||||
pub mod diff;
|
||||
pub mod frame;
|
||||
pub mod server;
|
||||
pub mod snapshot;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user