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:
asepharyana
2026-07-12 11:28:39 +07:00
parent 7158d362fd
commit 2efd40ca88
124 changed files with 2379 additions and 19 deletions
+17
View File
@@ -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()
}
+17
View File
@@ -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 {
+26
View File
@@ -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;
+30
View File
@@ -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)?)
}
+4
View File
@@ -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;
+21
View File
@@ -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>),
+15
View File
@@ -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)
+14
View File
@@ -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)