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
+136 -5
View File
@@ -1,3 +1,21 @@
//! The `Action` enum and its single dispatcher, `apply_action` — the
//! chokepoint through which every key input, streaming event, and async
//! background-thread result mutates `AppStateRest`.
//!
//! Flow: controllers/subagent threads construct `Action` values → the event
//! loop calls `apply_action(&mut state, action)` → for turn-producing
//! actions (`SubmitInput`), `spawn_turn` is kicked off on a background OS
//! thread which drives `run_agent_turn` (stream to the LLM, gate and
//! execute tool calls via `Harness`, archive messages to SQLite, log edits)
//! and pushes `TurnEvent`s onto a shared queue → on the next `Tick`, queued
//! `TurnEvent`s are drained back into `AppStateRest` (transcript, toasts,
//! usage counters).
//!
//! Why: keeping all state mutation behind one function means callers only
//! need to know how to *produce* actions, not how to update state safely;
//! running turns on plain OS threads (rather than blocking the main loop)
//! keeps the TUI responsive while the LLM streams.
use std::collections::VecDeque;
use crate::app::harness::Verdict;
@@ -9,11 +27,14 @@ use crate::app::state::runtime::TurnEvent;
use crate::app::state::types::{Origin, Overlay, Toast, ToastKind};
use crate::dto::chat::message::{ChatMessage, Role};
// Step bounds intentionally left unbounded (usize::MAX) so the agent can
// continue across as many turns as needed. Each iteration still honours
// `tc.abort_flag` and the per-call LLM timeout, so a runaway loop is
// observable and cancellable from the UI.
/// A single, well-typed event in the app — produced by key input, the
/// streaming pipeline, or subagent threads — that mutates `AppStateRest`
/// when applied via `apply_action`.
///
/// Step bounds intentionally left unbounded (usize::MAX) so the agent can
/// continue across as many turns as needed. Each iteration still honours
/// `tc.abort_flag` and the per-call LLM timeout, so a runaway loop is
/// observable and cancellable from the UI.
#[derive(Debug, Clone)]
pub enum Action {
ForceQuit,
@@ -63,6 +84,18 @@ pub enum Action {
AbortTurn,
}
/// Apply an `Action` to the application state.
///
/// Flow: pattern-match the variant → mutate `state` (input buffer, scroll
/// position, overlay, transcript, runtime, toasts, dirty flag, etc.) →
/// for `Tick`, also drain queued `TurnEvent`s and run periodic side jobs
/// (staleness sweep, pending-lesson commit).
///
/// Why: the single chokepoint that turns every typed key and async event
/// into a state change, so callers (controllers, subagent threads) only
/// need to know how to *produce* actions.
///
/// Return: nothing; `state` is mutated in place.
pub fn apply_action(state: &mut AppStateRest, action: Action) {
match action {
Action::ForceQuit => {
@@ -449,6 +482,19 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
}
}
/// Spawn a background thread that runs one full LLM turn.
///
/// Flow: check that no turn is currently in-flight → bail if so →
/// collect messages and config from state → determine API key (from
/// settings, env var, or default) → resolve generation params from
/// the current effort level → collect all tools (built-in + MCP) →
/// build `TurnCtx` → spawn a thread running `run_agent_turn` →
/// on any error, push a `TurnEvent::Error` → clear the in-flight flag
/// when the thread exits.
///
/// Why: runs on a plain OS thread so the async event loop stays responsive.
///
/// Return: nothing; results flow through `state.turn_events`.
fn spawn_turn(state: &AppStateRest) {
let in_flight = if let Ok(guard) = state.turn_in_flight.lock() {
*guard
@@ -532,6 +578,7 @@ fn spawn_turn(state: &AppStateRest) {
});
}
/// Context bundle passed to `run_agent_turn` on its background thread.
struct TurnCtx {
client: crate::service::provider::LlmClient,
tdefs: Vec<crate::dto::provider::request::ToolDef>,
@@ -547,6 +594,14 @@ struct TurnCtx {
abort_flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
/// Build an ASCII tree of the workspace directory structure for the
/// system prompt, so the LLM can see the file layout.
///
/// Flow: for each root, walk using `ignore::WalkBuilder` (respecting
/// `.gitignore` and hidden files) → prefix `[DIR]` for directories →
/// truncate after 1000 entries.
///
/// Return: a formatted string with one entry per line.
fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String {
let mut out = String::new();
out.push_str("Current Workspace Directory Structure:\n");
@@ -575,6 +630,11 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String {
out
}
/// Persist a `ChatMessage` to the SQLite message log, if a database
/// connection is available.
///
/// Flow: if `db` is `Some`, lock the mutex and call `insert_message`.
/// Errors are silently ignored.
fn archive_message(db: &Option<std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>>, session_id: &str, msg: &ChatMessage) {
if let Some(ref arc) = db {
if let Ok(conn) = arc.lock() {
@@ -583,6 +643,28 @@ fn archive_message(db: &Option<std::sync::Arc<std::sync::Mutex<rusqlite::Connect
}
}
/// Execute one full agent turn: stream the conversation to the LLM,
/// handle tool calls, and loop until the LLM produces a non-tool response
/// or runs out of unfinished todo items.
///
/// Flow: build system prompt with workspace tree → optionally shape
/// (compact) messages via `shortsend` → call `chat_with_tools_streaming`
/// with a callback that pushes `StreamStart`, `StreamToken`, `Reasoning`,
/// and `Usage` events → on streaming success, handle tool calls (gated
/// through `Harness::gate_tool_call`) or unwrap the final assistant
/// message → check for unfinished todo.md tasks (auto-retry with a
/// system message if any remain) → finalise with `Done` and an `edits`
/// SystemNote.
///
/// On streaming failure: retry once with a non-streaming call → if that
/// also fails and there are unfinished tasks, sleep 5s and loop back;
/// otherwise return the error.
///
/// Why: non-streaming fallback handles flaky connections without aborting
/// the turn; todo.md polling lets the agent self-direct toward completeness.
///
/// Return: `Ok(())` on successful completion, or an error from the LLM
/// API after retries are exhausted.
fn run_agent_turn(
tc: TurnCtx,
messages: &[ChatMessage],
@@ -836,6 +918,20 @@ fn run_agent_turn(
Ok(())
}
/// Execute a single tool call: find the tool by name, snapshot the file
/// (if write/edit) for rewind, run the tool, log an `EditLogEntry` for
/// write/edit, and return the output.
///
/// Flow: iterate tools → match by name → for write/edit, snapshot the
/// pre-existing file content into the blob store → call `tool.run()` →
/// for write/edit, compute SHA-256 of the new content and append an
/// `EditLogEntry` → return the tool output string.
///
/// Why: snapshots enable the rewind feature to restore previous content
/// after a write/edit.
///
/// Return: the tool's stdout string, or an error if no matching tool was
/// found or the tool run itself failed.
#[allow(clippy::too_many_arguments)]
fn execute_one_tool(
tools: &[Box<dyn crate::tool::Tool>],
@@ -910,6 +1006,15 @@ fn execute_one_tool(
anyhow::bail!("tool not found: {}", name)
}
/// Optionally push a review-available toast at the end of a turn that
/// performed edits.
///
/// Flow: skip if review is disabled → skip if `edit_count` is zero →
/// push an info toast listing the number of modified files.
///
/// Why: does not launch the review itself (that happens inside
/// `should_trigger_review` on `Tick`), only informs the user that
/// a review has material to examine.
fn maybe_trigger_review(state: &mut AppStateRest) {
if !state.settings.review_enabled {
return;
@@ -928,6 +1033,13 @@ fn maybe_trigger_review(state: &mut AppStateRest) {
));
}
/// Persist the current session metadata and conversation to disk.
///
/// Flow: build a `Session` object → save its metadata → write
/// `rt.messages` as JSON to the conversation file → errors are silently
/// ignored.
///
/// Why: called on `ForceQuit` so the session can be resumed later.
fn save_current_session(state: &AppStateRest) {
let base = state.store_base_dir();
let session = crate::model::session::Session::new(
@@ -943,6 +1055,20 @@ fn save_current_session(state: &AppStateRest) {
}
}
/// Run a browser-based OAuth PKCE flow for the given provider.
///
/// Flow: look up config by provider name ("zen"/"opencode", "openai",
/// or a custom provider via env vars) → bind a loopback server → generate
/// a PKCE code verifier and challenge → build the authorisation URL →
/// wait for the redirect code on the loopback server (with a 120s timeout)
/// → exchange the code for a token → save the token to
/// `~/.config/zesdex/oauth_{provider}.json`.
///
/// Why: the `webbrowser::open` call is currently commented out; the user
/// must open the auth URL manually until that line is reinstated.
///
/// Return: a success message on completion, or an error if the flow fails
/// at any step.
fn run_oauth_flow(provider: &str) -> anyhow::Result<String> {
use crate::service::oauth::manager::{OAuthConfig, OAuthManager};
use crate::service::oauth::loopback::LoopbackServer;
@@ -1013,6 +1139,11 @@ fn run_oauth_flow(provider: &str) -> anyhow::Result<String> {
Ok(format!("Successfully authenticated with {}.", provider))
}
/// Generate `n` pseudo-random bytes from the current sub-second timestamp.
///
/// Why: avoids pulling in a full RNG crate for the OAuth state token;
/// sufficient for a nonce that only needs to be unpredictable over the
/// lifetime of a single OAuth flow.
fn rand_bytes(n: usize) -> Vec<u8> {
use std::time::{SystemTime, UNIX_EPOCH};
let seed = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().subsec_nanos();