Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ LLM chat interfaces. Paste LLM-generated tool calls (in XML format), review and
audit dangerous operations, and manage sessions with full history tracking --
all running locally on your machine.

> **Version**: 0.4.0 | **Python**: >=3.14
> **Version**: 0.4.1 | **Python**: >=3.14

---

Expand Down
2 changes: 1 addition & 1 deletion README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

ManualAid 提供了一个基于 Textual 的 TUI 控制台,在剪贴板和 LLM 聊天界面之间架起桥梁. 粘贴 LLM 生成的工具调用(XML 格式),审查和审计危险操作,并通过完整的历史追踪管理会话 -- 一切都在本地运行.

> **版本**: 0.4.0 | **Python**: >=3.14
> **版本**: 0.4.1 | **Python**: >=3.14

---

Expand Down
25 changes: 25 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.1] - 2026-05-04

### Added

- **Single File Search Support**: Extended search tools (`exact_search`,
`regex_search`) to handle single file paths directly. The `files_to_search`
initialization now includes `is_file()` branching with adjusted
`relative_path` calculation for non-directory scenarios
([#110](https://github.com/SunYanbox/ManualAid/issues/110)).

### Fixed

- **Audit Tab MarkupError Crash**: Fixed Rich Markup parsing errors in the audit
tab caused by unescaped text. Set `markup=False` on `Static` widgets and
applied `rich.markup.escape()` to dynamically generated log results,
preventing crashes when rendering approve/reject statuses containing special
characters ([#124](https://github.com/SunYanbox/ManualAid/issues/124)).
- **Single File Search Failure**: Fixed a bug where `exact_search` and
`regex_search` tools failed to read content when the search path pointed to a
single file instead of a directory, due to the recursive glob (`rglob`) not
handling file paths
([#110](https://github.com/SunYanbox/ManualAid/issues/110)).

## [0.4.0] - 2026-05-03

### Added
Expand Down Expand Up @@ -153,5 +176,7 @@ and this project adheres to

_Initial release features and history._

[0.4.1]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.4.1
[0.4.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.4.0
[0.3.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.3.0
[0.2.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.2.0
21 changes: 21 additions & 0 deletions docs/CHANGELOG_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,25 @@
[Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/). 并采用
[语义化版本](https://semver.org/lang/Chinese/).

## [0.4.1] - 2026-05-04

### 新增

- **单文件路径搜索支持**: 扩展 `exact_search`、`regex_search`
工具以直接处理单文件路径. `files_to_search` 初始化阶段增加了 `is_file()`
分支判断,并调整了 `relative_path`
计算逻辑以适配非目录场景 ([#110](https://github.com/SunYanbox/ManualAid/issues/110)).

### 修复

- **审核标签页 MarkupError 崩溃**: 修复了审核标签页中因未转义文本导致的 Rich
Markup 解析错误. 在 `Static` 组件上设置 `markup=False`
并对动态生成的日志结果调用
`rich.markup.escape()`,防止渲染包含特殊字符的批准/拒绝状态时崩溃 ([#124](https://github.com/SunYanbox/ManualAid/issues/124)).
- **单文件搜索失效**: 修复了当搜索路径指向单个文件时,`exact_search` 和
`regex_search` 工具因递归查找 (`rglob`) 无法处理文件路径而无法读取内容的 Bug
([#110](https://github.com/SunYanbox/ManualAid/issues/110)).

## [0.4.0] - 2026-05-03

### 新增
Expand Down Expand Up @@ -125,5 +144,7 @@

_初始发布的功能和历史记录._

[0.4.1]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.4.1
[0.4.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.4.0
[0.3.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.3.0
[0.2.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.2.0
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "ManualAid"
version = "0.4.0"
version = "0.4.1"
description = ""
requires-python = ">=3.14"
dependencies = [
Expand Down
6 changes: 4 additions & 2 deletions src/console/ui/widgets/audit_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from collections import defaultdict
from typing import ClassVar

from rich.markup import escape
from textual.containers import Horizontal, Vertical
from textual.widgets import Button, Collapsible, Label, Static

Expand Down Expand Up @@ -136,7 +137,7 @@ async def _refresh(self) -> None:
snap_id = snap[0]
diff_content = snap[4] or "(空 diff)"

diff_container = Vertical(Static(diff_content), classes="audit-diff")
diff_container = Vertical(Static(diff_content, markup=False), classes="audit-diff")
btn_row = Horizontal(
Button("批准", variant="primary", id=f"approve-{snap_id}", classes="audit-approve"),
Button("拒绝", variant="error", id=f"reject-{snap_id}", classes="audit-reject"),
Expand Down Expand Up @@ -186,7 +187,8 @@ async def on_button_pressed(self, event: Button.Pressed) -> None:
log = self.query_one("#audit-result-log", Vertical)

color = "green" if "已批准" in result or "已拒绝" in result else "red"
await log.mount(Static(f"[{color}]{result}[/{color}]"))
escaped = escape(result)
await log.mount(Static(f"[{color}]{escaped}[/{color}]"))
except Exception:
pass

Expand Down
2 changes: 1 addition & 1 deletion src/constants/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.4.0"
__version__ = "0.4.1"
56 changes: 30 additions & 26 deletions src/workspace/tools/exact_search_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,39 +118,43 @@ def exact_search(
results = []
file_count = 0

# 遍历所有文件(递归)
for file_path in search_path.rglob("*"):
if file_path.is_file():
# 检查是否达到限制
if len(results) >= limit:
break
# 确定要搜索的文件列表(支持单文件或目录)
files_to_search = [search_path] if search_path.is_file() else list(search_path.rglob("*"))

# 遍历所有文件
for file_path in files_to_search:
if not file_path.is_file():
continue
# 检查是否达到限制
if len(results) >= limit:
break

# 检查是否应该忽略
should_ignore = False
relative_path = file_path.relative_to(search_path)
# 检查是否应该忽略
should_ignore = False
relative_path = file_path.relative_to(search_path) if search_path.is_dir() else file_path

for ignore_pattern in ignore_patterns:
if ignore_pattern.search(str(relative_path)):
should_ignore = True
break
for ignore_pattern in ignore_patterns:
if ignore_pattern.search(str(relative_path)):
should_ignore = True
break

if should_ignore:
continue
if should_ignore:
continue

try:
# 读取文件内容
with open(file_path, encoding="utf-8") as f:
lines = f.readlines()
try:
# 读取文件内容
with open(file_path, encoding="utf-8") as f:
lines = f.readlines()

# 搜索匹配行
file_matches = _search_exact_in_file(lines, search_string, case_sensitive, whole_word)
# 搜索匹配行
file_matches = _search_exact_in_file(lines, search_string, case_sensitive, whole_word)

if file_matches:
results.append({"file": str(file_path), "matches": file_matches})
file_count += 1
if file_matches:
results.append({"file": str(file_path), "matches": file_matches})
file_count += 1

except OSError, UnicodeDecodeError, PermissionError:
continue # 跳过无法读取的文件
except OSError, UnicodeDecodeError, PermissionError:
continue # 跳过无法读取的文件

# 格式化输出
return _format_exact_results(results, pattern, limit, file_count, case_sensitive, whole_word)
9 changes: 7 additions & 2 deletions src/workspace/tools/regex_search_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,20 @@ def regex_search(
results = []
file_count = 0

# 确定要搜索的文件列表(支持单文件或目录)
files_to_search = [search_path] if search_path.is_file() else list(search_path.rglob(file_pattern))

# 遍历文件
for file_path in search_path.rglob(file_pattern):
for file_path in files_to_search:
if not file_path.is_file():
continue
# 检查是否达到限制
if len(results) >= limit:
break

# 检查是否应该忽略该文件或文件夹
should_ignore = False
relative_path = file_path.relative_to(search_path)
relative_path = file_path.relative_to(search_path) if search_path.is_dir() else file_path

for ignore_pattern in ignore_patterns:
# 检查是否匹配忽略模式
Expand Down
30 changes: 14 additions & 16 deletions src/workspace/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,6 @@ def search_content(
try:
path = self.path_validator.validate(folder_path)

if not path.is_dir():
return ToolErrorResponse(
self.search_content.__name__, ValueError(f"{path} is not a directory")
).to_str()

# 初始化排除目录集合
exclude_set = set(exclude_dirs or [".git", "__pycache__", "node_modules", ".venv", "venv", "dist", "build"])

Expand All @@ -87,18 +82,21 @@ def search_content(
except re.error as e:
return f"错误:正则表达式无效 - {e}"

# 收集所有要搜索的文件
# 收集所有要搜索的文件(支持单文件或目录)
files_to_search = []
for file_path in path.rglob(file_pattern):
if file_path.is_file():
# 检查是否在排除目录中
should_exclude = False
for parent in file_path.parents:
if parent.name in exclude_set:
should_exclude = True
break
if not should_exclude:
files_to_search.append(file_path)
if path.is_file():
files_to_search = [path]
else:
for file_path in path.rglob(file_pattern):
if file_path.is_file():
# 检查是否在排除目录中
should_exclude = False
for parent in file_path.parents:
if parent.name in exclude_set:
should_exclude = True
break
if not should_exclude:
files_to_search.append(file_path)

if not files_to_search:
return f"在 {folder_path} 中没有找到匹配 {file_pattern} 的文件"
Expand Down