Skip to content

refactor tool module - #233

Merged
F16shen merged 5 commits into
AI-Shell-Team:mainfrom
F16shen:main
Jun 5, 2026
Merged

refactor tool module#233
F16shen merged 5 commits into
AI-Shell-Team:mainfrom
F16shen:main

Conversation

@F16shen

@F16shen F16shen commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Problem:
  • Changes:
  • Related Issue: #

Change Type

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Other

Scope

  • Core shell / PTY
  • AI agent / LLM
  • Skills / Tools
  • Security
  • Configuration
  • CLI / Interface
  • Packaging / Installation
  • CI/CD
  • Documentation

User-visible Changes

Compatibility

  • Backward compatible? (Yes/No)
  • Config changes? (Yes/No - if yes, describe migration)

Testing

Checklist

  • Code follows project style
  • Tests added if needed
  • Documentation updated if needed

Summary by CodeRabbit

  • New Features

    • WebFetch tool for fetching and summarizing web content
    • New file tools: read, write, edit
    • New plan-mode tools (enter/exit/list) and added tools: grep, glob, memory, python, final-answer, host-note, skill, system-diagnose
  • Improvements

    • System prompts now include per-tool instruction sections; tools can be registered/updated at runtime
    • TUI panel rendering and Unicode/ANSI-aware wrapping improved
    • i18n updates: localized messages refined and WebFetch strings added
  • Documentation

    • Architecture docs updated to list WebFetch and reflect i18n changes

@github-actions github-actions Bot added the docs Documentation-related issue label Jun 5, 2026
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Thanks for the pull request. A maintainer will review it when available.

Please keep the PR focused, explain the why in the description, and make sure local checks pass before requesting review.

Contribution guide: https://github.com/AI-Shell-Team/aish/blob/main/CONTRIBUTING.md

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Centralizes tool prompt metadata and adds Tool::prompt and session prompt injection; modularizes many tools and file/plan tooling; implements WebFetch (validation, fetch, sanitize, cache, secondary LLM); updates shell wiring, locales, Cargo deps, and README docs.

Changes

Tooling Refactor and WebFetch

Layer / File(s) Summary
Tool prompt contract & session injection
crates/aish-llm/src/types.rs, crates/aish-llm/src/session.rs, crates/aish-llm/src/agent.rs
Adds Tool::prompt(), builds per-tool ## Tool Instructions sections filtered by plan phase, and injects composed system prompt into LlmSession/agent flow; tests updated.
Shared prompt modules & migration
crates/aish-tools/src/*/prompt.rs, many tool impls
Moves per-tool description, parameters(), and prompt() into new prompt modules and rewires existing tool implementations to delegate to them (bash, grep, glob, python, memory, host_note, skill, final_answer, channel tools, secure_bash, system_diagnose, etc.).
File tools: read / write / edit
crates/aish-tools/src/read_file/*, crates/aish-tools/src/write_file/*, crates/aish-tools/src/edit_file/*
Adds dedicated read_file, write_file, and edit_file modules with prompt metadata, implementations (size limits, SSH offload restriction for read), tests, and removes previous monolithic fs.rs.
Plan tools: enter/exit/list
crates/aish-tools/src/plan_tool/*
Splits plan tooling into enter_plan_mode, exit_plan_mode, and list_plan_templates modules with shared prompt/schema builders and tests.
WebFetch: tool, utils, preapproved hosts
crates/aish-tools/src/web_fetch/{preapproved,prompt,utils,web_fetch}.rs
New WebFetchTool with preflight/async execute, URL validation, redirect/security rules, HTML-to-text sanitization, truncated/model prep, caching, secondary-model prompt + LLM call, plus preapproved-host allowlist and unit tests.
Crate wiring & deps
crates/aish-tools/src/lib.rs, crates/aish-tools/Cargo.toml
Refactors module declarations to inline submodules and re-export items; adds web_fetch export and workspace deps (reqwest, tokio, futures).
Shell registration & panel UI
crates/aish-shell/src/app.rs, crates/aish-shell/src/ai_handler.rs
Registers WebFetchTool at shell startup and on config/model changes; adds AiHandler.register_tool; refactors confirmation/iteration UI panels with ANSI-aware width/truncation and Unicode-aware wrapping.
i18n locale updates
crates/aish-i18n/locales/*
Removes per-tool description/param metadata across locales and adds tools.web_fetch message templates; retains runtime/error strings.
Docs & misc
README.md, README_CN.md
Updates architecture tables to list WebFetch in aish-tools built-in tools list.

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~60 minutes

"I'm a rabbit with a dev-time hop,
Prompts tucked in, caches set to stop,
WebFetch scampers, trims private lanes,
Tools now whisper extra instructions in chains.
Huzzah — tiny paws, big refactor pop!" 🐇✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

This pull request description looks incomplete. Please update the missing sections below before review.

Missing items:

  • User-visible Changes
  • Testing

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (11)
crates/aish-tools/src/channel_ask_user/prompt.rs (1)

3-8: ⚡ Quick win

PROMPT lacks guidance on the kind parameter.

The PROMPT instructions don't explain when to use "text_input" vs "choice_or_text", even though kind appears in the schema. Consider adding usage guidance similar to how ask_user/prompt.rs explains allow_freeform_input.

📝 Suggested addition
 pub(crate) const PROMPT: &str = r#"Use this tool only when a small amount of user input is needed to continue.
 
 Usage:
 - Ask one focused question at a time.
 - Prefer options when the likely answers are known.
+- Use kind=choice_or_text when providing predefined options.
 - Do not ask for secrets such as passwords, API keys, or tokens."#;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/channel_ask_user/prompt.rs` around lines 3 - 8, The
PROMPT constant (pub(crate) const PROMPT) is missing guidance for the schema's
kind parameter; update the PROMPT text to explain when to use kind="text_input"
(use for freeform user responses) versus kind="choice_or_text" (present explicit
options but allow a freeform override), mirroring the style of
ask_user/prompt.rs's explanation of allow_freeform_input; mention examples and a
short rule-of-thumb (choose text_input for open-ended questions, choice_or_text
when likely answers are known but a custom response should be allowed) so
callers of the channel_ask_user schema understand which kind to pick.
crates/aish-tools/src/host_note/prompt.rs (1)

10-30: 💤 Low value

Consider adding conditional parameter requirements to the schema.

The current schema marks only action as required, but content is mandatory when action="store" and keyword is mandatory when action="forget". While runtime validation in execute() likely catches this, the schema would be more accurate with JSON Schema if/then conditionals or oneOf patterns.

If schema-level enforcement is impractical, consider adding a note in the parameter descriptions clarifying the action-dependent requirements.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/host_note/prompt.rs` around lines 10 - 30, Update the
parameters() JSON Schema so action-dependent fields are enforced: add JSON
Schema conditionals (if/then) such that if "action" == "store" then "required":
["action","content"], and if "action" == "forget" then "required":
["action","keyword"]; place these conditionals inside the top-level schema
returned by the parameters() function (or, if adding conditionals is
impractical, update the "content" and "keyword" property descriptions to
explicitly state they are required for action="store" and action="forget"
respectively so callers see the dependency — ensure this aligns with the runtime
checks in execute()).
crates/aish-tools/src/web_fetch/web_fetch.rs (1)

65-65: 💤 Low value

Consider removing the empty system message.

Line 65 creates a system message with an empty string. If no system-level instructions are needed for the secondary model, consider removing this message entirely and using only the user message.

♻️ Proposed simplification
-        let messages = vec![ChatMessage::system(""), ChatMessage::user(model_prompt)];
+        let messages = vec![ChatMessage::user(model_prompt)];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/web_fetch/web_fetch.rs` at line 65, The messages vector
currently includes an empty system instruction (constructed via
ChatMessage::system("")) which is unnecessary; update the construction of
messages in web_fetch.rs to omit the empty system message and use only
ChatMessage::user(model_prompt), i.e., remove the ChatMessage::system("") entry
(or replace it with a meaningful system prompt if intended) so the secondary
model receives just the intended user prompt.
crates/aish-tools/src/web_fetch/preapproved.rs (1)

105-105: 💤 Low value

Remove unnecessary continue statement.

The continue statement at line 105 is unnecessary because it's at the end of the loop iteration. The loop will continue to the next iteration anyway.

♻️ Proposed cleanup
                    return true;
                }
            }
-            continue;
        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/web_fetch/preapproved.rs` at line 105, Remove the
redundant `continue;` at the end of the loop in the preapproved handling block
in crates/aish-tools/src/web_fetch/preapproved.rs (the loop that processes each
preapproved entry); simply delete the `continue;` statement so the loop
naturally proceeds to the next iteration without the no-op explicit continue.
crates/aish-tools/src/read_file/prompt.rs (1)

18-25: ⚡ Quick win

Consider adding validation constraints to offset and limit parameters.

The offset and limit parameters currently lack minimum value constraints in the schema. Adding "minimum": 0 for offset and "minimum": 1 for limit would prevent invalid negative values and improve input validation.

🛡️ Proposed validation constraints
             "offset": {
                 "type": "integer",
                 "description": "Line offset to start reading from, 0-based.",
+                "minimum": 0
             },
             "limit": {
                 "type": "integer",
                 "description": "Maximum number of lines to read.",
+                "minimum": 1
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/read_file/prompt.rs` around lines 18 - 25, The JSON
schema for the read-file prompt currently allows negative or zero values; update
the schema entries for the "offset" and "limit" fields in
crates/aish-tools/src/read_file/prompt.rs so that "offset" includes "minimum": 0
and "limit" includes "minimum": 1 to enforce non-negative starting offsets and a
positive read limit; locate the schema block where "offset" and "limit" are
defined and add those minimum constraints to their definitions.
crates/aish-tools/src/plan_tool/list_plan_templates.rs (1)

72-78: 💤 Low value

Consider making template assertions more flexible.

The test hardcodes expectations for specific template names ("default", "bugfix", "feature") and an exact count of 3. This couples the test to the implementation of aish_core::plan::get_available_templates() and will break if templates are added, removed, or renamed. Consider asserting general properties instead (e.g., at least one template exists, each has required fields) or using a configuration-driven approach.

♻️ Proposed more flexible assertions
         let result = tool.execute(serde_json::json!({}));
         assert!(result.ok);
         assert!(result.output.contains("Available plan templates"));
-        assert!(result.output.contains("default"));
-        assert!(result.output.contains("bugfix"));
-        assert!(result.output.contains("feature"));
 
         let meta = result.meta.unwrap();
         let templates = meta["templates"].as_array().unwrap();
-        assert_eq!(templates.len(), 3);
+        assert!(!templates.is_empty(), "Should have at least one template");
 
         for t in templates {
             assert!(t["name"].is_string());
             assert!(t["description"].is_string());
             assert!(t["content"].is_string());
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/plan_tool/list_plan_templates.rs` around lines 72 - 78,
The test currently rigidly asserts that result.output contains "default",
"bugfix", "feature" and that templates.len() == 3 which couples it to
aish_core::plan::get_available_templates(); change the assertions to be more
flexible by verifying that result.meta is present, that meta["templates"] parses
to an array (templates) with templates.len() >= 1, and that each element in
templates contains required fields (e.g., name and description or whatever keys
your Template struct exposes) rather than exact names or exact count; update
references to result, meta, and templates in the test to reflect these general
checks so the test passes when templates are added/removed/renamed.
crates/aish-tools/src/plan_tool/enter_plan_mode.rs (2)

107-109: 💤 Low value

Consider removing the hardcoded plan_id length assertion.

The test asserts that plan_id length is exactly 12 characters. This couples the test to the internal implementation of generate_plan_id() and will break if the ID format changes. Consider removing this assertion or replacing it with a weaker check (e.g., non-empty, matches expected format pattern).

♻️ Proposed alternative assertion
         assert!(meta["plan_id"].is_string());
-        assert_eq!(meta["plan_id"].as_str().unwrap().len(), 12);
+        assert!(!meta["plan_id"].as_str().unwrap().is_empty());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/plan_tool/enter_plan_mode.rs` around lines 107 - 109,
The test currently ties to the internal ID format by asserting meta["plan_id"]
length == 12; update the assertion to avoid fragility: remove the
assert_eq!(meta["plan_id"].as_str().unwrap().len(), 12) and instead assert that
meta["plan_id"] exists and is non-empty (or matches a stable pattern if you
expect a specific format). Locate the assertions around meta["phase"] and
meta["plan_id"] in enter_plan_mode.rs (the test that validates
generate_plan_id()), replace the hard length check with a non-empty check or a
regex-based pattern match so the test no longer depends on the exact length.

6-16: ⚡ Quick win

Update synchronization: VISIBLE_TOOLS_DURING_PLANNING matches aish_core::plan::PLANNING_VISIBLE_TOOLS (same 8 tool names today); consider importing the core constant to prevent future drift.

  • aish_core::plan::PLANNING_VISIBLE_TOOLS is public and currently contains exactly: read_file, glob, grep, ask_user, memory, write_file, edit_file, exit_plan_mode.
  • aish-core already has tests asserting PLANNING_VISIBLE_TOOLS contents.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/plan_tool/enter_plan_mode.rs` around lines 6 - 16,
Replace the hard-coded VISIBLE_TOOLS_DURING_PLANNING slice with the canonical
constant from core to avoid drift: remove the local const and re-export or alias
aish_core::plan::PLANNING_VISIBLE_TOOLS (e.g. add a module-level `use
aish_core::plan::PLANNING_VISIBLE_TOOLS as VISIBLE_TOOLS_DURING_PLANNING;` or
`pub(crate) use ... as ...`), so the code refers to the single source of truth
(`aish_core::plan::PLANNING_VISIBLE_TOOLS`) instead of duplicating the eight
tool names.
crates/aish-tools/src/write_file/write_file.rs (1)

62-81: ⚡ Quick win

Consider adding a content size limit.

ReadFileTool enforces a 32 KiB size limit (see Line 57 in read_file.rs), but WriteFileTool has no size restriction on the content parameter. An LLM could generate and write arbitrarily large files, potentially exhausting disk space.

Consider adding a content length check before writing, especially if this tool is exposed in untrusted contexts.

🛡️ Proposed size limit
         };
+
+        const SIZE_LIMIT: usize = 32 * 1024; // Match read_file limit
+        if content.len() > SIZE_LIMIT {
+            let mut args_map = std::collections::HashMap::new();
+            args_map.insert("size".to_string(), content.len().to_string());
+            args_map.insert("limit".to_string(), SIZE_LIMIT.to_string());
+            return ToolResult::error(aish_i18n::t_with_args(
+                "tools.fs.write_file.content_too_large",
+                &args_map,
+            ));
+        }

         if let Some(parent) = Path::new(path).parent() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/write_file/write_file.rs` around lines 62 - 81, Add a
content-size guard in write_file.rs before performing std::fs::write: define a
MAX_WRITE_SIZE constant (e.g. 32 * 1024) to match ReadFileTool, check
content.len() against it in the WriteFileTool execution path, and if it exceeds
the limit return ToolResult::error (use a similar i18n key as other errors)
including the attempted byte length and path in the args_map; only call
std::fs::write when the size check passes.
crates/aish-tools/src/edit_file/edit_file.rs (1)

73-85: ⚡ Quick win

Optimize: Combine string presence and count checks.

Lines 73-80 check content.contains(old), then Line 85 counts content.matches(old).count(). This scans the string twice. Combine into a single matches().count() call, then check if count is zero.

⚡ Proposed optimization
-        if !content.contains(old) {
+        let count = content.matches(old).count();
+        if count == 0 {
             let mut args_map = std::collections::HashMap::new();
             args_map.insert("path".to_string(), path.to_string());
             return ToolResult::error(aish_i18n::t_with_args(
                 "tools.fs.edit_file.old_string_not_found",
                 &args_map,
             ));
         }

         let new_content = if replace_all {
             content.replace(old, new)
         } else {
-            let count = content.matches(old).count();
             if count > 1 {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/edit_file/edit_file.rs` around lines 73 - 85, Compute
matches count once and use it for both presence check and replacement: call
content.matches(old).count() into a variable (e.g., count), if count == 0 return
the same ToolResult::error via aish_i18n::t_with_args, and then produce
new_content by using content.replace(old, new) when replace_all is true,
otherwise use content.replacen(old, new, 1) to replace only the first
occurrence; update references to content.contains(old) and the separate count()
call in edit_file.rs accordingly.
crates/aish-tools/src/read_file/read_file.rs (1)

103-118: ⚡ Quick win

Reduce duplication in line-formatting logic.

The if-branch and else-branch differ only in the .take(limit) call. Extract the iteration logic to reduce duplication.

♻️ Proposed refactor
-    let selected: Vec<String> = if let Some(limit) = limit {
-        lines
-            .iter()
-            .skip(offset)
-            .take(limit)
-            .enumerate()
-            .map(|(i, line)| format!("{:>6}\t{}", offset + i + 1, line))
-            .collect()
-    } else {
-        lines
-            .iter()
-            .skip(offset)
-            .enumerate()
-            .map(|(i, line)| format!("{:>6}\t{}", offset + i + 1, line))
-            .collect()
-    };
+    let mut iter = lines.iter().skip(offset);
+    if let Some(limit) = limit {
+        iter = iter.take(limit);
+    }
+    let selected: Vec<String> = iter
+        .enumerate()
+        .map(|(i, line)| format!("{:>6}\t{}", offset + i + 1, line))
+        .collect();

Note: This requires storing the iterator in a variable, which may need a different approach due to type inference. Alternatively, use a helper function.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/read_file/read_file.rs` around lines 103 - 118, The
formatting logic for building selected lines is duplicated; refactor the block
that constructs selected (in read_file.rs) by creating a single iterator over
lines.iter().skip(offset) and conditionally applying .take(limit) when
limit.is_some(), then map/enumerate/format in one place (or move the map/format
into a helper like format_line) so you only have one .map(...).collect() call;
refer to the selected variable construction and the
.skip(offset)/.take(limit)/.map(|(i, line)| format!("{:>6}\t{}", offset + i + 1,
line)) sequence when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aish-llm/src/session.rs`:
- Around line 331-333: Remove the unnecessary borrow when calling
ChatMessage::system: replace the argument
&self.system_prompt_with_tool_prompts(sys) with
self.system_prompt_with_tool_prompts(sys) because
system_prompt_with_tool_prompts returns a String and ChatMessage::system accepts
impl Into<String> (remove the leading & in the messages.push call).

In `@crates/aish-shell/src/app.rs`:
- Around line 1102-1130: Long content passed into print_panel_line (used around
the confirmation dialog for ctx.tool_name, ctx.message and
t("shell.confirm_dialog_question")) can exceed the panel inner_width because
print_panel_line pads but does not truncate; fix by ensuring every string passed
to print_panel_line is wrapped/truncated to fit inner_width (accounting for any
left padding/ANSI escape lengths) before printing—for example, reuse wrap_text
or a new truncate_to_width helper to split or truncate lines from format!()
(including the "Reason:" first line and subsequent reason_lines) so you only
call print_panel_line with lines that are guaranteed to be <= inner_width.
- Around line 1115-1125: The reason text wrapping is using byte/char lengths via
wrap_text and so miscalculates width for wide (CJK/emoji) characters; update the
wrapping logic used to produce reason_lines (the wrap_text implementation) to
measure terminal display width using unicode-width (UnicodeWidthStr::width) or a
display-width-aware wrapper (or swap to textwrap configured for Unicode widths),
ensure spaces and padding account for display widths, then regenerate
reason_lines so print_panel_line and the indented continuation lines (the loop
using reason_lines.lines().skip(1)) never exceed inner_width when wide
characters are present.
- Around line 448-454: The WebFetchTool instance registered in AishShell::new
(via tool_registry.register(Box::new(aish_tools::WebFetchTool::new(...)))) does
not get updated when the runtime model/api/key change (ai_handler.update_model),
causing config drift; modify the same update paths used by handle_model_command
and run_setup_wizard to either recreate and re-register a new WebFetchTool with
the current config or call an exposed update method on the existing WebFetchTool
so its api_base, api_key, model, temperature, and max_tokens mirror the active
session settings whenever update_model is invoked.

In `@crates/aish-tools/src/channel_ask_user/prompt.rs`:
- Line 55: The schema currently lists the property "kind" inside the "required"
array but the runtime (which defaults missing kind to "text_input") contradicts
that; update the JSON schema by removing "kind" from the "required" array and
add a "default": "text_input" to the "kind" property definition so the schema
matches the implementation (search for the "required" array and the "kind"
property in the prompt schema and align them with the defaulting behavior in
channel_ask_user.rs).

In `@crates/aish-tools/src/edit_file/edit_file.rs`:
- Around line 38-117: The edit_file::execute implementation allows unrestricted
file paths (variables path in execute) which can let an LLM edit arbitrary
files; add a restricted SSH wrapper similar to SshReadFileTool/WriteFileTool to
validate paths before calling EditFileTool. Implement a new SshEditFileTool type
that accepts an allowed-paths predicate (or a PathValidator) and performs the
same SSH/context checks and path normalization used by SshReadFileTool (reuse
its validation logic), then call EditFileTool::execute only after validating the
provided path; ensure the wrapper exposes the same tool interface and returns
the same ToolResult error messages when path is disallowed.
- Around line 60-71: The EditFileTool currently calls
std::fs::read_to_string(path) without checking file size; before that call (in
the block that builds `content`), call std::fs::metadata(path) and verify
metadata.len() <= 32 * 1024 (32 KiB), returning a ToolResult::error via
aish_i18n::t_with_args (similar to the existing error path) if the file is too
large; keep the existing error map keys ("path" and "error" or use a new "size"
key) and ensure the new check runs before attempting read_to_string so you avoid
OOM on very large files.

In `@crates/aish-tools/src/read_file/prompt.rs`:
- Around line 7-8: Update the PROMPT and the `offset` parameter description to
explicitly state that `offset` is 0-based (used for indexing into the file)
while the line numbers returned in results are 1-based for human readability;
reference the PROMPT constant and the `offset` parameter in the read-file prompt
code and add a short note explaining that to map an index to a displayed line
number you add 1 (and to convert a displayed line number back to a 0-based index
you subtract 1), so the LLM and users clearly understand the conversion.

In `@crates/aish-tools/src/read_file/read_file.rs`:
- Line 176: Replace the hardcoded English string passed to ToolResult::error
with an i18n call (use aish_i18n::t or aish_i18n::t_with_args) so the
access-denied message is localized; locate the return in read_file.rs where
ToolResult::error("Access denied: path is not inside offload directory") is used
and change it to call aish_i18n::t("...") or t_with_args(...) with an
appropriate message key (and any args if needed) so the user-facing error
follows the existing i18n pattern.
- Around line 131-138: The new() constructor currently uses
std::fs::canonicalize(&offload_root).unwrap_or(offload_root) which silently
keeps a non-canonical offload_root and enables path-traversal escapes when later
comparing input paths (see the prefix check that uses offload_root). Fix by
ensuring the offload directory exists and then canonicalizing — i.e., create the
directory if missing (std::fs::create_dir_all) before calling canonicalize and
propagate or panic on canonicalize failures so offload_root is always canonical;
update the new() implementation that constructs offload_root and retain
ReadFileTool::new() unchanged.
- Around line 44-67: The code reads the entire file into raw_bytes via
std::fs::read before enforcing SIZE_LIMIT, causing potential OOM; change the
logic in read_file.rs to call std::fs::metadata(path) first, check
metadata.len() against SIZE_LIMIT (const SIZE_LIMIT) and return the same
ToolResult::error using aish_i18n::t_with_args if the file is too large, and
only then call std::fs::read(path), also handling and mapping metadata errors
similarly to how read errors are handled (include path and error in args_map).

In `@crates/aish-tools/src/web_fetch/web_fetch.rs`:
- Around line 51-78: The temperature and max_tokens fields stored on the struct
are never used because chat_completion_raw is called with hardcoded Some(0.1)
and Some(2048) inside apply_prompt_to_content; either (A) make
chat_completion_raw use the struct values by passing self.temperature and
self.max_tokens (or their .or(Some(...)) fallbacks) instead of the hardcoded
literals, ensuring LlmSession::new remains unchanged, or (B) remove the
temperature and max_tokens fields and constructor parameters entirely (and
simplify LlmSession::new/chat call to the fixed values) so there is no dead
field; update the apply_prompt_to_content call site and the struct/constructor
accordingly (refer to apply_prompt_to_content, LlmSession::new, and
chat_completion_raw for locations to change).

In `@crates/aish-tools/src/write_file/write_file.rs`:
- Around line 40-83: The WriteFileTool::execute currently writes content without
size limits so large payloads can exhaust disk; add a max-size guard (e.g.,
const MAX_WRITE_BYTES = 32 * 1024 or reuse ReadFileTool::SIZE_LIMIT) and check
content.len() before calling std::fs::write; if content.len() > MAX_WRITE_BYTES
return ToolResult::error with an i18n key like "tools.fs.write_file.too_large"
(include path/bytes in args_map similar to existing errors) and do not attempt
directory creation or file write when over the limit.

---

Nitpick comments:
In `@crates/aish-tools/src/channel_ask_user/prompt.rs`:
- Around line 3-8: The PROMPT constant (pub(crate) const PROMPT) is missing
guidance for the schema's kind parameter; update the PROMPT text to explain when
to use kind="text_input" (use for freeform user responses) versus
kind="choice_or_text" (present explicit options but allow a freeform override),
mirroring the style of ask_user/prompt.rs's explanation of allow_freeform_input;
mention examples and a short rule-of-thumb (choose text_input for open-ended
questions, choice_or_text when likely answers are known but a custom response
should be allowed) so callers of the channel_ask_user schema understand which
kind to pick.

In `@crates/aish-tools/src/edit_file/edit_file.rs`:
- Around line 73-85: Compute matches count once and use it for both presence
check and replacement: call content.matches(old).count() into a variable (e.g.,
count), if count == 0 return the same ToolResult::error via
aish_i18n::t_with_args, and then produce new_content by using
content.replace(old, new) when replace_all is true, otherwise use
content.replacen(old, new, 1) to replace only the first occurrence; update
references to content.contains(old) and the separate count() call in
edit_file.rs accordingly.

In `@crates/aish-tools/src/host_note/prompt.rs`:
- Around line 10-30: Update the parameters() JSON Schema so action-dependent
fields are enforced: add JSON Schema conditionals (if/then) such that if
"action" == "store" then "required": ["action","content"], and if "action" ==
"forget" then "required": ["action","keyword"]; place these conditionals inside
the top-level schema returned by the parameters() function (or, if adding
conditionals is impractical, update the "content" and "keyword" property
descriptions to explicitly state they are required for action="store" and
action="forget" respectively so callers see the dependency — ensure this aligns
with the runtime checks in execute()).

In `@crates/aish-tools/src/plan_tool/enter_plan_mode.rs`:
- Around line 107-109: The test currently ties to the internal ID format by
asserting meta["plan_id"] length == 12; update the assertion to avoid fragility:
remove the assert_eq!(meta["plan_id"].as_str().unwrap().len(), 12) and instead
assert that meta["plan_id"] exists and is non-empty (or matches a stable pattern
if you expect a specific format). Locate the assertions around meta["phase"] and
meta["plan_id"] in enter_plan_mode.rs (the test that validates
generate_plan_id()), replace the hard length check with a non-empty check or a
regex-based pattern match so the test no longer depends on the exact length.
- Around line 6-16: Replace the hard-coded VISIBLE_TOOLS_DURING_PLANNING slice
with the canonical constant from core to avoid drift: remove the local const and
re-export or alias aish_core::plan::PLANNING_VISIBLE_TOOLS (e.g. add a
module-level `use aish_core::plan::PLANNING_VISIBLE_TOOLS as
VISIBLE_TOOLS_DURING_PLANNING;` or `pub(crate) use ... as ...`), so the code
refers to the single source of truth (`aish_core::plan::PLANNING_VISIBLE_TOOLS`)
instead of duplicating the eight tool names.

In `@crates/aish-tools/src/plan_tool/list_plan_templates.rs`:
- Around line 72-78: The test currently rigidly asserts that result.output
contains "default", "bugfix", "feature" and that templates.len() == 3 which
couples it to aish_core::plan::get_available_templates(); change the assertions
to be more flexible by verifying that result.meta is present, that
meta["templates"] parses to an array (templates) with templates.len() >= 1, and
that each element in templates contains required fields (e.g., name and
description or whatever keys your Template struct exposes) rather than exact
names or exact count; update references to result, meta, and templates in the
test to reflect these general checks so the test passes when templates are
added/removed/renamed.

In `@crates/aish-tools/src/read_file/prompt.rs`:
- Around line 18-25: The JSON schema for the read-file prompt currently allows
negative or zero values; update the schema entries for the "offset" and "limit"
fields in crates/aish-tools/src/read_file/prompt.rs so that "offset" includes
"minimum": 0 and "limit" includes "minimum": 1 to enforce non-negative starting
offsets and a positive read limit; locate the schema block where "offset" and
"limit" are defined and add those minimum constraints to their definitions.

In `@crates/aish-tools/src/read_file/read_file.rs`:
- Around line 103-118: The formatting logic for building selected lines is
duplicated; refactor the block that constructs selected (in read_file.rs) by
creating a single iterator over lines.iter().skip(offset) and conditionally
applying .take(limit) when limit.is_some(), then map/enumerate/format in one
place (or move the map/format into a helper like format_line) so you only have
one .map(...).collect() call; refer to the selected variable construction and
the .skip(offset)/.take(limit)/.map(|(i, line)| format!("{:>6}\t{}", offset + i
+ 1, line)) sequence when making the change.

In `@crates/aish-tools/src/web_fetch/preapproved.rs`:
- Line 105: Remove the redundant `continue;` at the end of the loop in the
preapproved handling block in crates/aish-tools/src/web_fetch/preapproved.rs
(the loop that processes each preapproved entry); simply delete the `continue;`
statement so the loop naturally proceeds to the next iteration without the no-op
explicit continue.

In `@crates/aish-tools/src/web_fetch/web_fetch.rs`:
- Line 65: The messages vector currently includes an empty system instruction
(constructed via ChatMessage::system("")) which is unnecessary; update the
construction of messages in web_fetch.rs to omit the empty system message and
use only ChatMessage::user(model_prompt), i.e., remove the
ChatMessage::system("") entry (or replace it with a meaningful system prompt if
intended) so the secondary model receives just the intended user prompt.

In `@crates/aish-tools/src/write_file/write_file.rs`:
- Around line 62-81: Add a content-size guard in write_file.rs before performing
std::fs::write: define a MAX_WRITE_SIZE constant (e.g. 32 * 1024) to match
ReadFileTool, check content.len() against it in the WriteFileTool execution
path, and if it exceeds the limit return ToolResult::error (use a similar i18n
key as other errors) including the attempted byte length and path in the
args_map; only call std::fs::write when the size check passes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ca8c8048-cbf4-48cf-8b82-c4775af6f44c

📥 Commits

Reviewing files that changed from the base of the PR and between b4ae841 and f441070.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (55)
  • README.md
  • README_CN.md
  • crates/aish-i18n/locales/de-DE.yaml
  • crates/aish-i18n/locales/en-US.yaml
  • crates/aish-i18n/locales/es-ES.yaml
  • crates/aish-i18n/locales/fr-FR.yaml
  • crates/aish-i18n/locales/ja-JP.yaml
  • crates/aish-i18n/locales/zh-CN.yaml
  • crates/aish-llm/src/agent.rs
  • crates/aish-llm/src/session.rs
  • crates/aish-llm/src/types.rs
  • crates/aish-shell/src/app.rs
  • crates/aish-tools/Cargo.toml
  • crates/aish-tools/src/ask_user/ask_user.rs
  • crates/aish-tools/src/ask_user/prompt.rs
  • crates/aish-tools/src/bash/bash.rs
  • crates/aish-tools/src/bash/prompt.rs
  • crates/aish-tools/src/channel_ask_user/channel_ask_user.rs
  • crates/aish-tools/src/channel_ask_user/prompt.rs
  • crates/aish-tools/src/channel_bash/channel_bash.rs
  • crates/aish-tools/src/channel_bash/prompt.rs
  • crates/aish-tools/src/edit_file/edit_file.rs
  • crates/aish-tools/src/edit_file/prompt.rs
  • crates/aish-tools/src/final_answer/final_answer.rs
  • crates/aish-tools/src/final_answer/prompt.rs
  • crates/aish-tools/src/fs.rs
  • crates/aish-tools/src/glob_tool/glob_tool.rs
  • crates/aish-tools/src/glob_tool/prompt.rs
  • crates/aish-tools/src/grep_tool/grep_tool.rs
  • crates/aish-tools/src/grep_tool/prompt.rs
  • crates/aish-tools/src/host_note/host_note.rs
  • crates/aish-tools/src/host_note/prompt.rs
  • crates/aish-tools/src/lib.rs
  • crates/aish-tools/src/memory_tool/memory_tool.rs
  • crates/aish-tools/src/memory_tool/prompt.rs
  • crates/aish-tools/src/plan_tool.rs
  • crates/aish-tools/src/plan_tool/enter_plan_mode.rs
  • crates/aish-tools/src/plan_tool/exit_plan_mode.rs
  • crates/aish-tools/src/plan_tool/list_plan_templates.rs
  • crates/aish-tools/src/plan_tool/prompt.rs
  • crates/aish-tools/src/python/prompt.rs
  • crates/aish-tools/src/python/python.rs
  • crates/aish-tools/src/read_file/prompt.rs
  • crates/aish-tools/src/read_file/read_file.rs
  • crates/aish-tools/src/secure_bash/secure_bash.rs
  • crates/aish-tools/src/skill_tool/prompt.rs
  • crates/aish-tools/src/skill_tool/skill_tool.rs
  • crates/aish-tools/src/system_diagnose/prompt.rs
  • crates/aish-tools/src/system_diagnose/system_diagnose.rs
  • crates/aish-tools/src/web_fetch/preapproved.rs
  • crates/aish-tools/src/web_fetch/prompt.rs
  • crates/aish-tools/src/web_fetch/utils.rs
  • crates/aish-tools/src/web_fetch/web_fetch.rs
  • crates/aish-tools/src/write_file/prompt.rs
  • crates/aish-tools/src/write_file/write_file.rs
💤 Files with no reviewable changes (6)
  • crates/aish-tools/src/fs.rs
  • crates/aish-tools/src/plan_tool.rs
  • crates/aish-i18n/locales/ja-JP.yaml
  • crates/aish-i18n/locales/de-DE.yaml
  • crates/aish-i18n/locales/fr-FR.yaml
  • crates/aish-i18n/locales/es-ES.yaml

Comment thread crates/aish-llm/src/session.rs
Comment thread crates/aish-shell/src/app.rs
Comment thread crates/aish-shell/src/app.rs
Comment thread crates/aish-shell/src/app.rs
Comment thread crates/aish-tools/src/channel_ask_user/prompt.rs Outdated
Comment thread crates/aish-tools/src/read_file/read_file.rs Outdated
Comment thread crates/aish-tools/src/read_file/read_file.rs
Comment thread crates/aish-tools/src/read_file/read_file.rs Outdated
Comment thread crates/aish-tools/src/web_fetch/web_fetch.rs
Comment thread crates/aish-tools/src/write_file/write_file.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/aish-tools/src/edit_file/edit_file.rs (1)

98-110: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject empty old_string before doing match/replace.

At Line 108, replace_all with old_string == "" can expand at every string boundary and trigger very large allocations/writes (OOM/disk pressure). Validate non-empty old_string up front.

🔧 Suggested fix
         let old = match args.get("old_string").and_then(|o| o.as_str()) {
             Some(o) => o,
             None => {
                 return ToolResult::error(aish_i18n::t("tools.fs.edit_file.missing_old_string"))
             }
         };
+        if old.is_empty() {
+            return ToolResult::error(aish_i18n::t("tools.fs.edit_file.missing_old_string"));
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/edit_file/edit_file.rs` around lines 98 - 110, The code
counts and replaces using content.matches(old) and content.replace(old, new)
which will catastrophically expand if old is an empty string; add an early check
for old.is_empty() in the edit logic (before calling content.matches or
performing replace_all) and return a ToolResult::error (using the same pattern
as the existing error flow that builds args_map and calls
aish_i18n::t_with_args) to reject empty `old` strings; update the branch that
currently computes `count` and the `replace_all` path to assume non-empty `old`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/aish-tools/src/edit_file/edit_file.rs`:
- Around line 98-110: The code counts and replaces using content.matches(old)
and content.replace(old, new) which will catastrophically expand if old is an
empty string; add an early check for old.is_empty() in the edit logic (before
calling content.matches or performing replace_all) and return a
ToolResult::error (using the same pattern as the existing error flow that builds
args_map and calls aish_i18n::t_with_args) to reject empty `old` strings; update
the branch that currently computes `count` and the `replace_all` path to assume
non-empty `old`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 39681690-5ffe-4173-9907-647011cae0bf

📥 Commits

Reviewing files that changed from the base of the PR and between 111f659 and 94a0302.

📒 Files selected for processing (12)
  • crates/aish-i18n/locales/en-US.yaml
  • crates/aish-i18n/locales/zh-CN.yaml
  • crates/aish-shell/src/ai_handler.rs
  • crates/aish-shell/src/app.rs
  • crates/aish-tools/src/channel_ask_user/prompt.rs
  • crates/aish-tools/src/edit_file/edit_file.rs
  • crates/aish-tools/src/host_note/prompt.rs
  • crates/aish-tools/src/read_file/prompt.rs
  • crates/aish-tools/src/read_file/read_file.rs
  • crates/aish-tools/src/web_fetch/preapproved.rs
  • crates/aish-tools/src/web_fetch/web_fetch.rs
  • crates/aish-tools/src/write_file/write_file.rs
💤 Files with no reviewable changes (1)
  • crates/aish-tools/src/web_fetch/preapproved.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/aish-tools/src/read_file/prompt.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/aish-tools/src/host_note/prompt.rs
  • crates/aish-i18n/locales/zh-CN.yaml
  • crates/aish-tools/src/read_file/read_file.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant