Enhance tool documentation and add new features
- Added module-level documentation for memory tools (`remember`, `recall`, `forget`) to clarify their purpose. - Improved documentation in `recall.rs` and `remember.rs` to describe the functionality and flow of memory entry operations. - Updated `mod.rs` to include descriptions for the tool trait and execution context. - Enhanced `plan.rs` with detailed comments on plan-mode signaling tools. - Documented text search tools in `search.rs` to explain their functionality. - Improved sequential-thinking tool documentation in `seqthink.rs`. - Added safety filter documentation in `shell_filter` for credential and git operations. - Enhanced utility tools documentation, including `cd`, `dir_cache_update`, and `todowrite`. - Improved rendering documentation in view modules (`chat`, `markdown`, `status`, `workflow`) to clarify rendering flows and purposes.
This commit is contained in:
@@ -1,20 +1,37 @@
|
||||
//! Unix-socket client used by the `--attach` process to talk to a
|
||||
//! running `--daemon`.
|
||||
//!
|
||||
//! Flow: `IpcClient::connect_unix` opens a `Connection` (see `conn.rs`)
|
||||
//! to the daemon's socket path → `send`/`receive` exchange framed JSON
|
||||
//! messages (typically `ClientRequest`/`DaemonFrame` from `protocol.rs`).
|
||||
|
||||
use anyhow::Result;
|
||||
use super::conn::Connection;
|
||||
|
||||
/// Client-side handle for the `--attach` process: wraps a `Connection`
|
||||
/// to a daemon's Unix socket.
|
||||
pub struct IpcClient {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl IpcClient {
|
||||
/// Connect to a daemon listening on the given Unix socket path.
|
||||
///
|
||||
/// Return: `Ok(IpcClient)` on success, or an error if the socket is
|
||||
/// missing or the daemon isn't accepting connections.
|
||||
pub fn connect_unix(path: &str) -> Result<Self> {
|
||||
let conn = Connection::connect_unix(path)?;
|
||||
Ok(IpcClient { conn })
|
||||
}
|
||||
|
||||
/// Serialize and send a value to the daemon (see `frame::write_frame`).
|
||||
pub fn send<T: serde::Serialize>(&mut self, value: &T) -> Result<()> {
|
||||
self.conn.send(value)
|
||||
}
|
||||
|
||||
/// Read and deserialize the next frame from the daemon.
|
||||
///
|
||||
/// Return: `Ok(None)` if the daemon closed the connection cleanly.
|
||||
pub fn receive<T: serde::de::DeserializeOwned>(&mut self) -> Result<Option<T>> {
|
||||
self.conn.receive()
|
||||
}
|
||||
|
||||
@@ -1,26 +1,43 @@
|
||||
//! Framed Unix-socket connection shared by both the server (`server.rs`)
|
||||
//! and client (`client.rs`) sides of the IPC layer.
|
||||
//!
|
||||
//! Flow: `Connection` wraps a `UnixStream` (either accepted by the server
|
||||
//! or dialed by the client) → `send` serializes a value to JSON and
|
||||
//! writes it as one length-prefixed frame (`frame::write_frame`) →
|
||||
//! `receive` reads one frame and deserializes it back to the caller's
|
||||
//! type, propagating a clean peer-close as `Ok(None)`.
|
||||
|
||||
use std::os::unix::net::UnixStream;
|
||||
use anyhow::Result;
|
||||
use super::frame;
|
||||
|
||||
/// A framed Unix-socket connection shared by client and server sides of
|
||||
/// the IPC layer; each `send`/`receive` moves one length-prefixed JSON frame.
|
||||
pub struct Connection {
|
||||
inner: UnixStream,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
/// Wrap an already-connected/accepted `UnixStream`.
|
||||
pub fn from_stream(stream: UnixStream) -> Result<Self> {
|
||||
Ok(Connection { inner: stream })
|
||||
}
|
||||
|
||||
/// Open a new Unix-socket connection to `path`.
|
||||
pub fn connect_unix(path: &str) -> Result<Self> {
|
||||
let stream = UnixStream::connect(path)?;
|
||||
Ok(Connection { inner: stream })
|
||||
}
|
||||
|
||||
/// Serialize `value` to JSON and write it as one length-prefixed frame.
|
||||
pub fn send<T: serde::Serialize>(&mut self, value: &T) -> Result<()> {
|
||||
let data = frame::serialize_frame(value)?;
|
||||
frame::write_frame(&mut self.inner, &data)
|
||||
}
|
||||
|
||||
/// Read one length-prefixed frame and deserialize it as `T`.
|
||||
///
|
||||
/// Return: `Ok(None)` on clean EOF (peer closed the connection).
|
||||
pub fn receive<T: serde::de::DeserializeOwned>(&mut self) -> Result<Option<T>> {
|
||||
let data = frame::read_frame(&mut self.inner)?;
|
||||
match data {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
//! Field-level diffing of JSON app-state snapshots, for sending only
|
||||
//! incremental changes over IPC instead of a full `StateSnapshot`.
|
||||
//!
|
||||
//! Flow: `compute_diff` recursively walks two JSON `Value`s (before/after)
|
||||
//! → for objects, recurses per key building a dotted path string; any
|
||||
//! other mismatch is recorded wholesale → results accumulate into a
|
||||
//! `StateDiff`'s `Vec<Change>`, built via `StateDiff::new`/`add_change`
|
||||
//! and reset via `clear`.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// A timestamped batch of field-level changes to app state, keyed by
|
||||
/// dotted JSON path, for a given session.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StateDiff {
|
||||
pub timestamp: i64,
|
||||
@@ -8,6 +19,7 @@ pub struct StateDiff {
|
||||
pub changes: Vec<Change>,
|
||||
}
|
||||
|
||||
/// A single field change: the JSON path and its old/new values.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Change {
|
||||
pub path: String,
|
||||
@@ -16,6 +28,7 @@ pub struct Change {
|
||||
}
|
||||
|
||||
impl StateDiff {
|
||||
/// Create an empty diff for `session_id`, timestamped at creation.
|
||||
pub fn new(session_id: String) -> Self {
|
||||
StateDiff {
|
||||
timestamp: chrono::Utc::now().timestamp_millis(),
|
||||
@@ -24,6 +37,7 @@ impl StateDiff {
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a single field change to the diff.
|
||||
pub fn add_change(&mut self, path: String, old_value: Option<Value>, new_value: Option<Value>) {
|
||||
self.changes.push(Change {
|
||||
path,
|
||||
@@ -32,16 +46,28 @@ impl StateDiff {
|
||||
});
|
||||
}
|
||||
|
||||
/// Whether the diff has no recorded changes.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.changes.is_empty()
|
||||
}
|
||||
|
||||
/// Drop all changes and refresh the timestamp.
|
||||
pub fn clear(&mut self) {
|
||||
self.changes.clear();
|
||||
self.timestamp = chrono::Utc::now().timestamp_millis();
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively diff two JSON values, appending field-level `Change`s.
|
||||
///
|
||||
/// Flow: equal values short-circuit → for two objects, recurse per key
|
||||
/// (union of both maps' keys, missing side treated as `Null`) building
|
||||
/// a dotted `path` → any other value-type mismatch (or non-object diff)
|
||||
/// is recorded as one `Change` at the current `path`.
|
||||
///
|
||||
/// Why: only objects are diffed structurally; arrays and scalars are
|
||||
/// compared wholesale so a change anywhere inside them replaces the
|
||||
/// whole value rather than producing an index-level diff.
|
||||
pub fn compute_diff(before: &Value, after: &Value, path: &str, changes: &mut Vec<Change>) {
|
||||
if before == after {
|
||||
return;
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
//! Length-prefixed binary framing and JSON (de)serialization helpers for
|
||||
//! the IPC wire protocol.
|
||||
//!
|
||||
//! Flow: `write_frame`/`read_frame` handle the raw byte-level framing
|
||||
//! (4-byte big-endian length header + payload) over any `Read`/`Write`;
|
||||
//! `serialize_frame`/`deserialize_frame` handle the JSON layer on top.
|
||||
//! `Connection` (see `conn.rs`) composes both layers for a full send/receive.
|
||||
//!
|
||||
//! Why: a fixed-size length prefix lets the reader know exactly how many
|
||||
//! bytes to pull before attempting to parse, avoiding partial-JSON reads
|
||||
//! over a stream socket.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use anyhow::Result;
|
||||
|
||||
/// Upper bound on a single frame's byte size (64 MiB), enforced on both
|
||||
/// the write and read paths to bound memory use and reject malformed or
|
||||
/// malicious oversized length headers.
|
||||
pub(crate) const MAX_FRAME_SIZE: usize = 64 * 1024 * 1024;
|
||||
|
||||
/// Write `data` as a length-prefixed frame: 4-byte big-endian length
|
||||
/// followed by the raw bytes, then flush.
|
||||
///
|
||||
/// Why: rejects frames over `MAX_FRAME_SIZE` to bound memory use on the
|
||||
/// reading side before any bytes are read.
|
||||
pub fn write_frame<W: Write>(writer: &mut W, data: &[u8]) -> Result<()> {
|
||||
let len = data.len();
|
||||
if len > MAX_FRAME_SIZE {
|
||||
@@ -15,6 +35,14 @@ pub fn write_frame<W: Write>(writer: &mut W, data: &[u8]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read one length-prefixed frame written by `write_frame`.
|
||||
///
|
||||
/// Flow: read 4-byte length header → on clean EOF before any bytes,
|
||||
/// return `Ok(None)` (peer closed) → validate against `MAX_FRAME_SIZE`
|
||||
/// → read the payload.
|
||||
///
|
||||
/// Return: `Ok(None)` signals a graceful connection close, distinct
|
||||
/// from an `Err` mid-frame I/O failure.
|
||||
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) {
|
||||
@@ -31,6 +59,7 @@ pub fn read_frame<R: Read>(reader: &mut R) -> Result<Option<Vec<u8>>> {
|
||||
Ok(Some(buf))
|
||||
}
|
||||
|
||||
/// Serialize `value` to JSON bytes, rejecting output over `MAX_FRAME_SIZE`.
|
||||
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 {
|
||||
@@ -39,6 +68,7 @@ pub fn serialize_frame<T: serde::Serialize>(value: &T) -> Result<Vec<u8>> {
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Deserialize a frame's raw JSON bytes into `T`.
|
||||
pub fn deserialize_frame<'a, T: serde::Deserialize<'a>>(data: &'a [u8]) -> Result<T> {
|
||||
Ok(serde_json::from_slice(data)?)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! Unix-socket IPC layer used to connect a `--attach` TUI client to a
|
||||
//! `--daemon` process: length-prefixed framing, connection wrapper,
|
||||
//! client/server handles, and the wire protocol types.
|
||||
|
||||
pub mod client;
|
||||
pub mod conn;
|
||||
pub mod frame;
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
//! Wire message types exchanged between an attached client and the
|
||||
//! daemon over the `Connection`/framing layer (`conn.rs`, `frame.rs`).
|
||||
//!
|
||||
//! Flow: client input events are captured as `KeyAction`/`ClientRequest`
|
||||
//! and sent to the daemon → the daemon applies them to its `AppStateRest`
|
||||
//! and replies with `DaemonFrame` variants (a flattened `StatePayload`
|
||||
//! for redraw, streamed tokens, system notes, or a close signal).
|
||||
//!
|
||||
//! Why: `StatePayload`/`MessageEntry`/`ToastEntry` are deliberately flat,
|
||||
//! serializable projections of daemon-side state so the client can
|
||||
//! redraw its TUI without sharing any in-process state with the daemon.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Wire-serializable subset of `crossterm::event::KeyCode`, sent from
|
||||
/// an attached client to the daemon over IPC.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum KeyAction {
|
||||
Char(char),
|
||||
@@ -19,6 +33,8 @@ pub enum KeyAction {
|
||||
Function(u8),
|
||||
}
|
||||
|
||||
/// Messages an attached client sends to the daemon: input events, a
|
||||
/// full-line submit, terminal resize, and connection lifecycle.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ClientRequest {
|
||||
Tick,
|
||||
@@ -33,6 +49,7 @@ pub enum ClientRequest {
|
||||
Close,
|
||||
}
|
||||
|
||||
/// Flattened chat message sent from daemon to client for transcript display.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MessageEntry {
|
||||
pub role: String,
|
||||
@@ -40,6 +57,7 @@ pub struct MessageEntry {
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
/// Flattened toast notification sent from daemon to client for rendering.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToastEntry {
|
||||
pub kind: String,
|
||||
@@ -48,6 +66,8 @@ pub struct ToastEntry {
|
||||
pub lifetime_ms: u64,
|
||||
}
|
||||
|
||||
/// Snapshot of daemon-side `AppStateRest` sent to the client after every
|
||||
/// action, enough for the client to redraw its TUI without shared state.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StatePayload {
|
||||
pub session_id: String,
|
||||
@@ -61,6 +81,7 @@ pub struct StatePayload {
|
||||
pub input_cursor: usize,
|
||||
}
|
||||
|
||||
/// Messages the daemon sends back to an attached client.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum DaemonFrame {
|
||||
StateUpdate(Box<StatePayload>),
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
//! Unix-socket listener for the `--daemon` process.
|
||||
//!
|
||||
//! Flow: `IpcServer::bind_unix` opens/binds a Unix socket at a well-known
|
||||
//! path (clearing any stale file left by a crashed prior daemon) →
|
||||
//! `accept` blocks for the next client and wraps it as a `Connection`
|
||||
//! (see `conn.rs`) for framed request/response traffic.
|
||||
|
||||
use std::os::unix::net::UnixListener;
|
||||
use anyhow::Result;
|
||||
use super::conn::Connection;
|
||||
|
||||
/// Server-side handle for the `--daemon` process: listens on a Unix
|
||||
/// socket and hands out `Connection`s to accepted clients.
|
||||
pub struct IpcServer {
|
||||
listener: UnixListener,
|
||||
}
|
||||
|
||||
impl IpcServer {
|
||||
/// Bind a new Unix-socket listener at `path`.
|
||||
///
|
||||
/// Why: removes any stale socket file at `path` first, since a prior
|
||||
/// crashed daemon can leave one behind and `UnixListener::bind` fails
|
||||
/// on an existing path.
|
||||
pub fn bind_unix(path: &str) -> Result<Self> {
|
||||
let _ = std::fs::remove_file(path);
|
||||
let listener = UnixListener::bind(path)?;
|
||||
Ok(IpcServer { listener })
|
||||
}
|
||||
|
||||
/// Block until a client connects, then wrap it as a `Connection`.
|
||||
pub fn accept(&self) -> Result<Connection> {
|
||||
let (stream, _addr) = self.listener.accept()?;
|
||||
Connection::from_stream(stream)
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
//! Point-in-time state snapshots for external inspection/persistence of
|
||||
//! a running session (distinct from the incremental `StateDiff` in
|
||||
//! `diff.rs`).
|
||||
//!
|
||||
//! Flow: `StateSnapshot::new` builds an empty, `dirty`-marked snapshot →
|
||||
//! callers populate/replace its fields as state changes →
|
||||
//! `serialize_snapshot`/`deserialize_snapshot` move it to/from JSON bytes
|
||||
//! for storage or IPC transport.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Point-in-time summary of app state (mode, session, counts, arbitrary
|
||||
/// `payload`) used for external inspection/persistence of a running session.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StateSnapshot {
|
||||
pub timestamp: i64,
|
||||
@@ -15,6 +26,7 @@ pub struct StateSnapshot {
|
||||
}
|
||||
|
||||
impl StateSnapshot {
|
||||
/// Build a fresh, empty snapshot marked `dirty` for the given session.
|
||||
pub fn new(session_id: String, mode: String, model: String) -> Self {
|
||||
StateSnapshot {
|
||||
timestamp: chrono::Utc::now().timestamp_millis(),
|
||||
@@ -30,11 +42,13 @@ impl StateSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize a `StateSnapshot` to JSON bytes.
|
||||
pub fn serialize_snapshot(snapshot: &StateSnapshot) -> anyhow::Result<Vec<u8>> {
|
||||
let data = serde_json::to_vec(snapshot)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Deserialize JSON bytes back into a `StateSnapshot`.
|
||||
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