refactor: renamed search and find tools to grep and glob

- Renamed the `find` and `search` tools to `glob` and `grep` respectively across the codebase to improve command clarity.
- Implemented full-stack support for the renamed tools, including CLI arguments, system prompts, SDK exports, and tool registration.
- Added automated migration logic in `settings` to transform legacy `find` and `search` configuration keys to their new equivalents.
- Updated the `collab-web` renderer registry to ensure backwards compatibility with legacy tool outputs.
This commit is contained in:
can1357
2026-06-27 00:44:28 +02:00
parent 53af82d539
commit ae1650d689
93 changed files with 887 additions and 551 deletions
+1 -1
View File
@@ -116,7 +116,7 @@ Current callers:
- `@`-mention fuzzy file autocomplete enables cache (`fuzzyFind` with `cache: true`):
- `packages/tui/src/autocomplete.ts`
- Mutation flows invalidate through `packages/coding-agent/src/tools/fs-cache-invalidation.ts`.
- Tool-level search integration (`packages/coding-agent/src/tools/search.ts`) currently calls native `grep` with `cache: false`.
- Tool-level grep integration (`packages/coding-agent/src/tools/grep.ts`) currently calls native `grep` with `cache: false`.
## Invalidation contract
+1 -1
View File
@@ -418,7 +418,7 @@ tools:
| `tools.artifactTailBytes` | number | `20` | KB of tail kept inline on spill. |
| `tools.artifactTailLines` | number | `500` | Max tail lines kept inline on spill. |
Individual built-in tools are toggled by their own keys, e.g. `bash.enabled`, `eval.py`, `eval.js`, `find.enabled`, `search.enabled`, `fetch.enabled`, `browser.enabled`, `astEdit.enabled`, `astGrep.enabled`, `web_search.enabled`, `inspect_image.enabled`.
Individual built-in tools are toggled by their own keys, e.g. `bash.enabled`, `eval.py`, `eval.js`, `glob.enabled`, `grep.enabled`, `fetch.enabled`, `browser.enabled`, `astEdit.enabled`, `astGrep.enabled`, `web_search.enabled`, `inspect_image.enabled`.
### Shell, eval, and LSP
+3 -3
View File
@@ -96,7 +96,7 @@ Stdout and stderr are merged before the model sees them. Definite non-zero exit
- Starts like a foreground managed job, then backgrounds it when it outlives the wait window.
6. Intercepted command
- No subprocess created.
- Returns a `ToolError` pointing the model at `read`, `search`, `find`, `edit`, or `write`.
- Returns a `ToolError` pointing the model at `read`, `grep`, `glob`, `edit`, or `write`.
## Side Effects
- Filesystem
@@ -153,8 +153,8 @@ Stdout and stderr are merged before the model sees them. Definite non-zero exit
- `checkBashInterception()` blocks only when the matching rule's `tool` name is present in `ctx.toolNames`; missing tools disable their corresponding rule.
- Default interceptor rules come from `DEFAULT_BASH_INTERCEPTOR_RULES` in `packages/coding-agent/src/config/settings-schema.ts`:
- `cat|head|tail|less|more` -> `read`
- `grep|rg|ripgrep|ag|ack` -> `search`
- `find|fd|locate` with name/type/glob flags -> `find`
- `grep|rg|ripgrep|ag|ack` -> `grep`
- `find|fd|locate` with name/type/glob flags -> `glob`
- `sed -i`, `perl -i`, `awk -i inplace` -> `edit`
- `echo|printf|cat <<` with redirection -> `write`
- PTY mode is ignored in non-UI contexts and when `PI_NO_PTY=1` (gated by `canUseInteractiveBashPty()`); the tool falls back to non-PTY execution and appends a `pty requested but unavailable in this environment; ran without a terminal` notice.
+2 -2
View File
@@ -22,7 +22,7 @@
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `input` | `string` | Yes | One or more file sections. Anchored sections must start with `[PATH#TAG]`; `TAG` is the four-hex snapshot tag emitted by the latest `read`/`search`/`write`/successful `edit`. Optional `*** Begin Patch` / `*** End Patch` envelope is ignored if present. |
| `input` | `string` | Yes | One or more file sections. Anchored sections must start with `[PATH#TAG]`; `TAG` is the four-hex snapshot tag emitted by the latest `read`/`grep`/`write`/successful `edit`. Optional `*** Begin Patch` / `*** End Patch` envelope is ignored if present. |
Patch language inside `input`:
@@ -45,7 +45,7 @@ Patch language inside `input`:
- There is no repeat row kind. To keep a line, leave it out of every range; split edits into multiple hunks when needed.
- `-` rows are invalid. Literal text beginning with `-` or `+` must be written as `+-text` / `++text`.
Anchors come from `read`/`search` output. `read` emits a `[PATH#TAG]` header from the session snapshot store and lines as `LINE:TEXT`; copy the header into the edit section and copy only the line number into hunk headers.
Anchors come from `read`/`grep` output. `read` emits a `[PATH#TAG]` header from the session snapshot store and lines as `LINE:TEXT`; copy the header into the edit section and copy only the line number into hunk headers.
### Tolerated input shapes (lenient parsing)
+14 -14
View File
@@ -1,10 +1,10 @@
# find
# glob
> Find filesystem paths by glob; use `search` when you need content matches instead of path matches.
> Find filesystem paths by glob; use `grep` when you need content matches instead of path matches.
## Source
- Entry: `packages/coding-agent/src/tools/find.ts`
- Model-facing prompt: `packages/coding-agent/src/prompts/tools/find.md`
- Entry: `packages/coding-agent/src/tools/glob.ts`
- Model-facing prompt: `packages/coding-agent/src/prompts/tools/glob.md`
- Key collaborators:
- `packages/coding-agent/src/tools/path-utils.ts` — normalize inputs; split base path vs glob.
- `packages/coding-agent/src/tools/list-limit.ts` — apply result-count caps.
@@ -41,8 +41,8 @@ The tool returns a single text block plus structured `details`.
## Flow
1. `FindTool.execute()` expands delimiter-flattened local `paths` entries with `expandDelimitedPathEntries(..., parseFindPattern)` unless custom operations are injected. The splitter validates candidate parts by statting their parsed base paths, keeps existing delimiter-containing paths intact, accepts comma/semicolon splits when at least one part resolves, and accepts whitespace splits only when every part resolves.
2. The tool normalizes each resulting entry with `normalizePathLikeInput()` and `/\\/g -> "/"` (`packages/coding-agent/src/tools/find.ts`). Empty normalized entries fail with `` `paths` must contain non-empty globs or paths ``.
1. `GlobTool.execute()` expands delimiter-flattened local `paths` entries with `expandDelimitedPathEntries(..., parseFindPattern)` unless custom operations are injected. The splitter validates candidate parts by statting their parsed base paths, keeps existing delimiter-containing paths intact, accepts comma/semicolon splits when at least one part resolves, and accepts whitespace splits only when every part resolves.
2. The tool normalizes each resulting entry with `normalizePathLikeInput()` and `/\\/g -> "/"` (`packages/coding-agent/src/tools/glob.ts`). Empty normalized entries fail with `` `paths` must contain non-empty globs or paths ``.
3. For multi-path local calls, `partitionExistingPaths(..., parseFindPattern)` (`packages/coding-agent/src/tools/path-utils.ts`) stats each base path. Missing entries are skipped; if all are missing, the tool throws `Path not found: ...`. Single missing paths still hard-fail.
4. The tool calls `resolveExplicitFindPatterns()` for multi-entry calls; it parses each entry into its own `(basePath, globPattern, hasGlob)` target so every path is walked as its own root (collapsing to a shared ancestor would scan unrelated siblings). Single-entry calls parse with `parseFindPattern()` directly.
5. `parseFindPattern()` determines `(basePath, globPattern, hasGlob)`:
@@ -52,7 +52,7 @@ The tool returns a single text block plus structured `details`.
6. `resolveToCwd()` converts the base path to an absolute path under the session cwd. A resolved `/` is rejected with `Searching from root directory '/' is not allowed`.
7. `limit` defaults to `DEFAULT_LIMIT` (`200`), must be positive and finite, is floored, then clamped to `MAX_LIMIT` (`200`). `hidden` and `gitignore` both default to `true`. An internal timeout of `5` seconds (`5000` ms) is built via `AbortSignal.timeout(...)`.
8. Execution then branches:
- **Custom operations branch**: if `FindToolOptions.operations.glob` exists, the tool checks existence with `operations.exists()`, short-circuits exact-file inputs via `operations.stat()` when available, then calls `operations.glob(globPattern, searchPath, { ignore: ["**/node_modules/**", "**/.git/**"], limit })`.
- **Custom operations branch**: if `GlobToolOptions.operations.glob` exists, the tool checks existence with `operations.exists()`, short-circuits exact-file inputs via `operations.stat()` when available, then calls `operations.glob(globPattern, searchPath, { ignore: ["**/node_modules/**", "**/.git/**"], limit })`.
- **Built-in local branch**: the tool stats each target's `searchPath`. Exact-file inputs return immediately. Directory inputs call `natives.glob()` with `hidden`, `maxResults: effectiveLimit`, `sortByMtime: true`, `gitignore: useGitignore`, `recursive: false` (recursion comes from the `**/` prefix `parseFindPattern()` adds), and the combined abort signal; multi-target calls run their globs concurrently.
9. In the local branch, optional `onMatch` callbacks convert each match to a cwd-relative display path and emit throttled progress updates.
10. After native glob returns, JS merges per-target results, deduplicates repeated display paths, and sorts the merged list by `mtime` descending before formatting paths.
@@ -66,7 +66,7 @@ The tool returns a single text block plus structured `details`.
- **Multi-path search**: multiple inputs resolved by `resolveExplicitFindPatterns()` into per-entry targets, each walked as its own root concurrently and merged afterwards.
- **Partial multi-path search with missing inputs**: local multi-path calls skip missing base paths and surface them as `missingPaths` / `Skipped missing paths: ...`.
- **Internal URL input**: supported when the internal router resolves the URL to a backing file. Internal URL globs are rejected.
- **Custom delegated search**: uses injected `FindOperations` instead of local fs + native glob.
- **Custom delegated search**: uses injected `GlobOperations` instead of local fs + native glob.
## Side Effects
- Filesystem
@@ -81,31 +81,31 @@ The tool returns a single text block plus structured `details`.
- Local globbing is cancellable through the caller abort signal plus the internal timeout.
## Limits & Caps
- Default result limit: `200` (`DEFAULT_LIMIT` in `packages/coding-agent/src/tools/find.ts`).
- Default result limit: `200` (`DEFAULT_LIMIT` in `packages/coding-agent/src/tools/glob.ts`).
- Maximum result limit: `200` (`MAX_LIMIT`); larger inputs are clamped.
- Local glob timeout: fixed at `5000` ms.
- Output byte cap: `50 * 1024` bytes (`DEFAULT_MAX_BYTES` in `packages/coding-agent/src/session/streaming-output.ts`).
- Default generic line cap in `truncateHead()` is `3000`, but `find` overrides `maxLines` to `Number.MAX_SAFE_INTEGER`, so byte size — not line count — is the practical output truncation cap.
- Default generic line cap in `truncateHead()` is `3000`, but `glob` overrides `maxLines` to `Number.MAX_SAFE_INTEGER`, so byte size — not line count — is the practical output truncation cap.
- Streaming update throttle: `200` ms between `onUpdate` emissions.
- Sort order: most recent `mtime` first in the built-in local branch and promised in the prompt. The tool re-sorts in JS even though native glob receives `sortByMtime: true` so native code can still stop early at `maxResults`.
## Errors
- User-facing `ToolError`s from `FindTool.execute()` include:
- User-facing `ToolError`s from `GlobTool.execute()` include:
- `` `paths` must contain non-empty globs or paths ``
- `Path not found: ...`
- `Searching from root directory '/' is not allowed`
- `Limit must be a positive number`
- `Path is not a directory: ...`
- timeout result text is `find timed out after <seconds>s; returning <N> partial matches — narrow the pattern instead of retrying blindly` and is returned as a successful, truncated partial result rather than an error.
- timeout result text is `glob timed out after <seconds>s; returning <N> partial matches — narrow the pattern instead of retrying blindly` and is returned as a successful, truncated partial result rather than an error.
- If the caller aborts, the local branch converts `AbortError` into `ToolAbortError`.
- Non-`ENOENT` stat failures and other unexpected errors are rethrown.
- Empty matches are not errors; they return the no-files text result.
## Notes
- Reach for `find` for filename / path discovery. Reach for `search` when the selection criterion is file contents or regex matches; `search` takes a `pattern` and returns anchored content matches, while `find` only returns matching paths (`packages/coding-agent/src/prompts/tools/find.md`, `packages/coding-agent/src/prompts/tools/search.md`).
- Reach for `glob` for filename / path discovery. Reach for `grep` when the selection criterion is file contents or regex matches; `grep` takes a `pattern` and returns anchored content matches, while `glob` only returns matching paths (`packages/coding-agent/src/prompts/tools/glob.md`, `packages/coding-agent/src/prompts/tools/grep.md`).
- Bare top-level globs are made recursive. `*.ts` is parsed as base `.` plus glob `**/*.ts`; `src/*.ts` stays rooted at `src` with a non-recursive `*.ts` segment; `src/**/*.ts` preserves explicit recursion.
- `.gitignore` defaults to enabled in the built-in local branch. Use `gitignore: false` to disable it for native traversal.
- `hidden` defaults to `true`; hidden-file exclusion is opt-out, not opt-in.
- Multi-path missing-input tolerance applies in both branches, but only the built-in local branch surfaces `missingPaths` / `Skipped missing paths: ...`. The custom-operations branch hard-fails a missing `searchPath` only for single-input calls; in multi-input calls a missing target silently contributes no results.
- The custom `FindOperations.glob()` hook receives `ignore` and `limit`, but not the `hidden` flag or an explicit `.gitignore` toggle. A remote delegate must account for that itself if it wants parity with the local branch.
- The custom `GlobOperations.glob()` hook receives `ignore` and `limit`, but not the `hidden` flag or an explicit `.gitignore` toggle. A remote delegate must account for that itself if it wants parity with the local branch.
- Built-in local globbing does not force `fileType: File`; it can return files and directories from native glob. Directory outputs also occur through exact-path passthrough or custom delegates that return them.
+19 -19
View File
@@ -1,10 +1,10 @@
# search
# grep
> Search file contents with a regex across files, directories, globs, and internal URLs.
> Grep file contents with a regex across files, directories, globs, and internal URLs.
## Source
- Entry: `packages/coding-agent/src/tools/search.ts`
- Model-facing prompt: `packages/coding-agent/src/prompts/tools/search.md`
- Entry: `packages/coding-agent/src/tools/grep.ts`
- Model-facing prompt: `packages/coding-agent/src/prompts/tools/grep.md`
- Key collaborators:
- `packages/coding-agent/src/tools/match-line-format.ts` — model-facing anchor formatting.
- `packages/coding-agent/src/tools/path-utils.ts` — path normalization, glob splitting, internal URL resolution.
@@ -20,11 +20,11 @@
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `pattern` | `string` | Yes | Regex pattern. `search.ts` rejects whitespace-only input but otherwise preserves the pattern verbatim (leading/trailing whitespace is meaningful in regexes). The native matcher enables multiline only when the pattern text contains a literal newline or the two-character sequence `\\n`. The native layer auto-escapes braces that cannot be valid repetition quantifiers, so patterns like `${platform}` stay searchable (see Notes). |
| `pattern` | `string` | Yes | Regex pattern. `grep.ts` rejects whitespace-only input but otherwise preserves the pattern verbatim (leading/trailing whitespace is meaningful in regexes). The native matcher enables multiline only when the pattern text contains a literal newline or the two-character sequence `\\n`. The native layer auto-escapes braces that cannot be valid repetition quantifiers, so patterns like `${platform}` stay searchable (see Notes). |
| `paths` | `string \| string[]` | No | One file path, directory path, glob-like path, archive member, internal URL, or an array of those. Omitted or empty defaults to `.` (the workspace root). Append a line-range selector such as `:50-100` or `:5-16,960-973` to a single file/archive/internal-resource input to constrain matches. Empty strings are rejected after trimming/quote stripping. Single entries accidentally joined with comma, semicolon, or whitespace are expanded only after existence validation; existing paths containing delimiters stay intact. Filesystem-backed internal URLs search their backing file; virtual internal resources search resolved text in memory. Internal URLs cannot contain glob characters. |
| `case` | `boolean` | No | Case-sensitive search. Defaults to `true`. Passed to native `ignoreCase` or JS `RegExp` flags for virtual resources. |
| `gitignore` | `boolean` | No | Respect `.gitignore` during directory scans. Defaults to `true`. Passed to native `gitignore`. |
| `skip` | `number` | No | File-page offset for multi-file results. Defaults to `0`; `search.ts` floors finite numbers and rejects negative or non-finite values. Single-file searches ignore it because they do not paginate by file. |
| `skip` | `number` | No | File-page offset for multi-file results. Defaults to `0`; `grep.ts` floors finite numbers and rejects negative or non-finite values. Single-file searches ignore it because they do not paginate by file. |
## Outputs
The tool returns a single text block in `content[0].text` plus structured `details`.
@@ -45,13 +45,13 @@ The tool returns a single text block in `content[0].text` plus structured `detai
- No-match result text is `No matches found` (or `No more results (...)` when `skip` points past the last file page), optionally followed by skipped missing-path, unreadable-archive, or oversized-file notes.
## Flow
1. `SearchTool.execute()` validates and normalizes input in `packages/coding-agent/src/tools/search.ts`:
1. `GrepTool.execute()` validates and normalizes input in `packages/coding-agent/src/tools/grep.ts`:
- rejects whitespace-only patterns while preserving the pattern verbatim;
- defaults omitted or empty `paths` to `["."]` (the workspace root);
- normalizes `skip` to a non-negative integer;
- expands delimiter-flattened `paths` entries with `expandDelimitedPathEntries()`, keeping existing delimiter-containing paths intact, accepting comma/semicolon splits when at least one part resolves, and accepting whitespace splits only when every part resolves;
- peels any line-range selector from each resulting entry;
- reads `search.contextBefore` and `search.contextAfter` from session settings (`1` and `3` by default);
- reads `grep.contextBefore` and `grep.contextAfter` from session settings (`1` and `3` by default);
- enables multiline only when `pattern` contains `\n` or an actual newline.
2. Each `paths` entry is normalized with `normalizePathLikeInput()` again during shared scope resolution; this is a no-op for entries already normalized by delimiter expansion.
3. Archive member paths such as `bundle.zip:src/foo.ts` are materialized to temporary UTF-8 scratch files before native grep. Binary or non-UTF-8 archive members are reported as skipped/unreadable.
@@ -66,7 +66,7 @@ The tool returns a single text block in `content[0].text` plus structured `detai
- one entry: `parseSearchPath()` splits `basePath` and optional glob;
- multiple entries: `resolveExplicitSearchPaths()` (via `resolveToolSearchScope()`) computes a common base directory, brace-union glob, exact-file list, or per-entry target list. Targets fan out when the common ancestor is not itself a requested scope, or when a plain-file entry would otherwise be demoted into a directory walk's glob union (`fanOutFileTargets`).
7. Line-range selectors are validated after path/archive/internal resolution. They are allowed only for single files, archive members, or virtual resources; glob/directory line-range selectors error.
8. `search.ts` stats the resolved base path to decide file vs directory behavior.
8. `grep.ts` stats the resolved base path to decide file vs directory behavior.
9. It calls native `grep()` from `@oh-my-pi/pi-natives` with:
- `pattern`, `ignoreCase`, `multiline`, `gitignore`;
- `hidden: true`;
@@ -81,7 +81,7 @@ The tool returns a single text block in `content[0].text` plus structured `detai
- `build_matcher()` sanitizes non-quantifier braces before regex compile;
- if compile fails with unopened/unclosed-group errors, it retries after escaping previously unescaped parentheses;
- directory scans use the grep pipeline described in `docs/natives-text-search-pipeline.md`.
11. Search dispatch differs by resolved path set:
11. Grep dispatch differs by resolved path set:
- exact explicit files or fanned-out multi-targets: JS loops over targets, merges `grep()` results itself, and deduplicates overlapping targets by absolute path + line number;
- single file/directory base: one `grep()` call handles native scanning.
12. Virtual internal resources are searched in JS with `RegExp`; archive scratch paths and virtual paths are remapped back to user-facing selectors before rendering.
@@ -127,19 +127,19 @@ The tool returns a single text block in `content[0].text` plus structured `detai
- Populates tool `details.meta` with truncation/limit metadata.
- Background work / cancellation
- Wrapped in `untilAborted(signal, ...)` at the JS level.
- `search.ts` passes the abort `signal` and `timeoutMs: SEARCH_GREP_TIMEOUT_MS` (`30_000`) into native `grep()`, so native scans are cancellable and time-bounded.
- `grep.ts` passes the abort `signal` and `timeoutMs: SEARCH_GREP_TIMEOUT_MS` (`30_000`) into native `grep()`, so native scans are cancellable and time-bounded.
## Limits & Caps
- File page limit: `20` files (`DEFAULT_FILE_LIMIT` in `packages/coding-agent/src/tools/search.ts`).
- File page limit: `20` files (`DEFAULT_FILE_LIMIT` in `packages/coding-agent/src/tools/grep.ts`).
- Per-file match caps: `20` for multi-file scopes (`MULTI_FILE_PER_FILE_MATCHES`), `200` for single-file scopes (`SINGLE_FILE_MATCHES`).
- Native/JS preselection cap: `2000` matches (`INTERNAL_TOTAL_CAP`).
- Line truncation: `512` characters per emitted line (`DEFAULT_MAX_COLUMN` in `packages/coding-agent/src/session/streaming-output.ts`). Native grep marks truncated lines; JS reports `linesTruncated`.
- Final text truncation: `truncateHead()` default byte cap `50 * 1024` bytes (`DEFAULT_MAX_BYTES` in `packages/coding-agent/src/session/streaming-output.ts`). `search.ts` overrides `maxLines` to `Number.MAX_SAFE_INTEGER`, so normal search output is byte-capped, not line-capped.
- Context defaults: `search.contextBefore = 1`, `search.contextAfter = 3` in `packages/coding-agent/src/config/settings-schema.ts`.
- Final text truncation: `truncateHead()` default byte cap `50 * 1024` bytes (`DEFAULT_MAX_BYTES` in `packages/coding-agent/src/session/streaming-output.ts`). `grep.ts` overrides `maxLines` to `Number.MAX_SAFE_INTEGER`, so normal grep output is byte-capped, not line-capped.
- Context defaults: `grep.contextBefore = 1`, `grep.contextAfter = 3` in `packages/coding-agent/src/config/settings-schema.ts`.
- Pagination: `skip` is a file-page offset for multi-file scopes. The result text says `Use skip=<N> for the next page` when more files remain.
- Native directory-scan cache: available in `grep.rs`, but this tool always sets `cache: false`.
- Native grep wall-clock budget: `30_000ms` per invocation (`SEARCH_GREP_TIMEOUT_MS` in `packages/coding-agent/src/tools/search.ts`); hitting it raises `Search timed out after 30s; ...`.
- Native per-file size cap: `4 * 1024 * 1024` bytes (`MAX_FILE_BYTES` in `crates/pi-natives/src/grep.rs`, mirrored as `NATIVE_GREP_MAX_FILE_BYTES` in `search.ts`). Oversized files are silently skipped by native grep; `search.ts` surfaces a `Skipped oversized file(s)` note (with names for explicit file targets, a count for directory scans).
- Native grep wall-clock budget: `30_000ms` per invocation (`SEARCH_GREP_TIMEOUT_MS` in `packages/coding-agent/src/tools/grep.ts`); hitting it raises `Grep timed out after 30s; ...`.
- Native per-file size cap: `4 * 1024 * 1024` bytes (`MAX_FILE_BYTES` in `crates/pi-natives/src/grep.rs`, mirrored as `NATIVE_GREP_MAX_FILE_BYTES` in `grep.ts`). Oversized files are silently skipped by native grep; `grep.ts` surfaces a `Skipped oversized file(s)` note (with names for explicit file targets, a count for directory scans).
## Errors
- `Pattern must not be empty` when trimmed `pattern` is empty.
@@ -151,14 +151,14 @@ The tool returns a single text block in `content[0].text` plus structured `detai
- `Path not found: ...; pass each path as its own array element` when a filesystem-backed resolved base path is missing, or when every multi-path filesystem entry is missing (with an archive hint when unreadable archive members contributed).
- Virtual internal URL regex compile failures are reported as `Invalid regex: ...` from JavaScript `RegExp`; filesystem-backed regex failures beginning with `regex` or `regex parse error` are normalized to `Invalid regex: ...`.
- Multi-file native scans skip per-file open/search failures inside `grep.rs`; the scan continues with surviving files.
- ``Search timed out after 30s; narrow paths or pattern, or scope with `find` first`` when native grep hits `SEARCH_GREP_TIMEOUT_MS`.
- ``Grep timed out after 30s; narrow paths or pattern, or scope with `glob` first`` when native grep hits `SEARCH_GREP_TIMEOUT_MS`.
## Notes
- The model-facing prompt documents Rust regex syntax (RE2-style; no lookaround or backreferences). Filesystem-backed searches use that native engine; virtual internal URL content is searched with JavaScript `RegExp`.
- Native `build_matcher()` already auto-escapes braces that cannot be valid quantifiers, so patterns like `${platform}` become searchable instead of failing. Valid quantifiers like `a{2,4}` remain unchanged.
- Native compile retry also escapes unescaped literal parentheses only after an unopened/unclosed-group parse error. It is a fallback, not a general parser mode.
- Internal URLs are resolved before path existence checks. Backed resources become ordinary filesystem paths; virtual resources stay in memory and do not mint editable hashline anchors.
- `hidden:true` is hard-coded in `search.ts`; there is no model-facing flag to exclude dotfiles.
- `hidden:true` is hard-coded in `grep.ts`; there is no model-facing flag to exclude dotfiles.
- `gitignore:false` only affects native directory traversal. It does not disable the tool's own path normalization or explicit-file handling.
- When `paths` resolves to multiple exact files, each target uses the `2000` internal cap before JS grouping.
- The section tag in hashline mode is a four-hex opaque snapshot tag from the session snapshot store; `search` records whole-file snapshots when possible and prints bare line numbers beneath the header.
- The section tag in hashline mode is a four-hex opaque snapshot tag from the session snapshot store; `grep` records whole-file snapshots when possible and prints bare line numbers beneath the header.
+1 -1
View File
@@ -113,6 +113,6 @@
- Built-in entries appear only in `"all"` mode and only for registry tools whose `loadMode === "discoverable"` and are not currently active.
- Hidden/internal built-ins are intentionally excluded from the built-in corpus: `resolve`, `yield`, `report_finding`, `report_tool_issue` are called out in the `#collectDiscoverableBuiltinTools()` comment.
- `DiscoverableToolSource` includes `"extension"` and `"custom"`, but `AgentSession.getDiscoverableTools()` currently assembles only built-in and MCP sources.
- On startup, `packages/coding-agent/src/sdk.ts` resolves `"auto"` after the full registry exists and injects `search_tool_bm25` when the count exceeds 40. It hides non-essential discoverable built-ins only in `tools.discoveryMode = "all"`. Tools whose class is marked as `loadMode === "essential"` (defaults are `read`, `bash`, `edit`, `write`, and `find`) are always active; they survive hiding regardless of configuration. `tools.essentialOverride` can be used to treat additional discoverable tools as essential (active on startup) or to explicitly specify the active essential list.
- On startup, `packages/coding-agent/src/sdk.ts` resolves `"auto"` after the full registry exists and injects `search_tool_bm25` when the count exceeds 40. It hides non-essential discoverable built-ins only in `tools.discoveryMode = "all"`. Tools whose class is marked as `loadMode === "essential"` (defaults are `read`, `bash`, `edit`, `write`, and `glob`) are always active; they survive hiding regardless of configuration. `tools.essentialOverride` can be used to treat additional discoverable tools as essential (active on startup) or to explicitly specify the active essential list.
- Query tokenization is simple and deterministic: Unicode is NFKD-normalized, combining marks are dropped, acronym/camelCase and digit-to-capital boundaries are split, non-letter/non-number characters become spaces, tokens are lowercased, and only non-empty tokens survive.
- Scores are rounded differently by surface: `details.tools[].score` keeps 6 decimals; the TUI line renders 3.
+5
View File
@@ -1,12 +1,17 @@
# Changelog
## [Unreleased]
### Added
- Added support for file deletion and moving within file editing operations
### Changed
- Renamed `search` tool to `grep`
- Renamed `find` tool to `glob`
- Updated configuration keys and UI labels to reflect `grep` and `glob` tool naming
- Automatically migrated existing user settings for `search` and `find` to `grep` and `glob` configs
- Changed the `inlineToolDescriptors` setting ("Inline Tool Descriptors") from a boolean to a three-way enum (`auto` | `on` | `off`), defaulting to `auto`. `auto` inlines tool descriptors into the system prompt (and strips them from provider tool schemas) only for Gemini models, leaving them in the schemas otherwise; `on`/`off` force the behavior regardless of model. Existing `true`/`false` configs migrate to `on`/`off`.
- Replaced `as string | undefined` inline casts with `typeof` guards in the TUI usage renderer's account identity resolution (`formatAccountLabel`, `formatUnlimitedReportLabel`, reset-credits label, and unlimited-plan tier), so empty-string metadata values fall through to the next fallback instead of being displayed
@@ -31,7 +31,7 @@ describe("advisor", () => {
{
type: "toolCall",
id: "search-timeout",
name: "search",
name: "grep",
arguments: { pattern: "needle", paths: ["packages/coding-agent/src"] },
},
],
@@ -40,7 +40,7 @@ describe("advisor", () => {
{
role: "toolResult",
toolCallId: "search-timeout",
toolName: "search",
toolName: "grep",
content: [{ type: "text", text: "timed out after 30s" }],
isError: true,
timestamp: 2,
@@ -711,13 +711,13 @@ describe("advisor", () => {
} as AgentMessage,
{
role: "assistant",
content: [{ type: "toolCall", id: "b", name: "search", arguments: { pattern: "y" } }],
content: [{ type: "toolCall", id: "b", name: "grep", arguments: { pattern: "y" } }],
timestamp: 4,
} as unknown as AgentMessage,
{
role: "toolResult",
toolCallId: "b",
toolName: "search",
toolName: "grep",
content: [{ type: "text", text: "ok" }],
isError: false,
timestamp: 5,
@@ -1044,9 +1044,9 @@ describe("advisor", () => {
describe("read-only tool allowlist", () => {
it("selects only the investigation tools from a mixed toolset", () => {
const toolset = ["read", "edit", "search", "bash", "find", "write", "advise"];
const toolset = ["read", "edit", "grep", "bash", "glob", "write", "advise"];
const selected = toolset.filter(name => ADVISOR_READONLY_TOOL_NAMES.has(name));
expect(selected).toEqual(["read", "search", "find"]);
expect(selected).toEqual(["read", "grep", "glob"]);
expect(ADVISOR_READONLY_TOOL_NAMES.has("edit")).toBe(false);
expect(ADVISOR_READONLY_TOOL_NAMES.has("bash")).toBe(false);
expect(ADVISOR_READONLY_TOOL_NAMES.has("write")).toBe(false);
@@ -137,7 +137,7 @@ export function deriveAdvisorTelemetry(
* inspect the workspace before weighing in. Names match the primary session's
* tool instances, which the advisor reuses.
*/
export const ADVISOR_READONLY_TOOL_NAMES: ReadonlySet<string> = new Set(["read", "search", "find"]);
export const ADVISOR_READONLY_TOOL_NAMES: ReadonlySet<string> = new Set(["read", "grep", "glob"]);
function advisorNoteDedupeKey(note: string): string {
return note.trim().replace(/\s+/g, " ");
+3 -2
View File
@@ -4,7 +4,7 @@
import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import { CLI_THINKING_LEVELS, type ConfiguredThinkingLevel, parseCliThinkingLevel } from "../thinking";
import { BUILTIN_TOOL_NAMES } from "../tools/builtin-names";
import { BUILTIN_TOOL_NAMES, normalizeToolNames } from "../tools/builtin-names";
import {
OPTIONAL_FLAGS,
OPTIONAL_VALUE_FLAGS,
@@ -90,6 +90,7 @@ const PARSE_DEPS: ParseDeps = {
logger,
parseThinking: parseCliThinkingLevel,
builtinToolNames: BUILTIN_TOOL_NAMES,
normalizeToolNames,
thinkingEfforts: CLI_THINKING_LEVELS,
};
@@ -330,7 +331,7 @@ ${chalk.bold("Available Tools (default-enabled unless noted):")}
edit - Edit files with find/replace
write - Write files (creates/overwrites)
grep - Search file contents
find - Find files by glob pattern
glob - Find files by glob pattern
lsp - Language server protocol (code intelligence)
python - Execute Python code (requires: ${APP_NAME} setup python)
notebook - Edit Jupyter notebooks
+6 -3
View File
@@ -46,6 +46,7 @@ export interface ParseDeps {
logger: { warn: (message: string, meta?: Record<string, unknown>) => void };
parseThinking: (value: string | null | undefined) => ConfiguredThinkingLevel | undefined;
builtinToolNames: readonly string[];
normalizeToolNames: (values: Iterable<string>) => string[];
thinkingEfforts: readonly string[];
}
@@ -147,10 +148,12 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
result.models = value.split(",").map(s => s.trim());
},
"--tools": (result, value, deps) => {
const names = value
const names = deps.normalizeToolNames(
value
.split(",")
.map(s => s.trim().toLowerCase())
.filter(Boolean);
.map(s => s.trim())
.filter(Boolean),
);
const valid: string[] = [];
for (const name of names) {
if (deps.builtinToolNames.includes(name)) {
@@ -1,17 +1,17 @@
// biome-ignore-all lint/suspicious/noTemplateCurlyInString: sample source-code strings (read fixtures) intentionally contain literal ${...}.
// Gallery fixtures for the filesystem tools (read, write, find).
// Gallery fixtures for the filesystem tools (read, write, glob).
import { ReadToolGroupComponent } from "../../modes/components/read-tool-group";
import type { GalleryFixture, GalleryFixtureState, GalleryResult } from "./types";
const readSnippet = [
"export const findToolRenderer = {",
"export const globToolRenderer = {",
"\tinline: true,",
"\trenderCall(args: FindRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {",
"\trenderCall(args: GlobRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {",
"\t\tconst meta: string[] = [];",
"\t\tif (args.limit !== undefined) meta.push(`limit:${args.limit}`);",
"",
"\t\tconst text = renderStatusLine(",
'\t\t\t{ icon: "pending", title: "Find", description: formatFindRenderPaths(args.paths) || "*", meta },',
'\t\t\t{ icon: "pending", title: "Glob", description: formatGlobRenderPaths(args.paths) || "*", meta },',
"\t\t\tuiTheme,",
"\t\t);",
"\t\treturn new Text(text, 0, 0);",
@@ -99,22 +99,22 @@ export const fsFixtures: Record<string, GalleryFixture> = {
read: {
label: "Read",
// Streaming: path still being typed, selector not yet appended.
streamingArgs: { path: "packages/coding-agent/src/tools/find" },
args: { path: "packages/coding-agent/src/tools/find.ts:437-448" },
streamingArgs: { path: "packages/coding-agent/src/tools/glob" },
args: { path: "packages/coding-agent/src/tools/glob.ts:437-448" },
result: {
content: [
{
type: "text",
text: [
"[packages/coding-agent/src/tools/find.ts#E48E]",
"437:export const findToolRenderer = {",
"[packages/coding-agent/src/tools/glob.ts#E48E]",
"437:export const globToolRenderer = {",
"438:\tinline: true,",
"439:\trenderCall(args: FindRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {",
"439:\trenderCall(args: GlobRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {",
"440:\t\tconst meta: string[] = [];",
"441:\t\tif (args.limit !== undefined) meta.push(`limit:${args.limit}`);",
"442:",
"443:\t\tconst text = renderStatusLine(",
'444:\t\t\t{ icon: "pending", title: "Find", description: formatFindRenderPaths(args.paths) || "*", meta },',
'444:\t\t\t{ icon: "pending", title: "Glob", description: formatGlobRenderPaths(args.paths) || "*", meta },',
"445:\t\t\tuiTheme,",
"446:\t\t);",
"447:\t\treturn new Text(text, 0, 0);",
@@ -124,7 +124,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
],
details: {
kind: "file",
resolvedPath: "/Users/dev/Projects/pi/packages/coding-agent/src/tools/find.ts",
resolvedPath: "/Users/dev/Projects/pi/packages/coding-agent/src/tools/glob.ts",
contentType: "text/typescript",
displayContent: { text: readSnippet, startLine: 437 },
},
@@ -134,7 +134,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
content: [
{
type: "text",
text: "Error: ENOENT: no such file or directory, open 'packages/coding-agent/src/tools/find.ts'",
text: "Error: ENOENT: no such file or directory, open 'packages/coding-agent/src/tools/glob.ts'",
},
],
},
@@ -179,8 +179,8 @@ export const fsFixtures: Record<string, GalleryFixture> = {
},
},
find: {
label: "Find",
glob: {
label: "Glob",
// Streaming: glob half-typed, no limit yet.
streamingArgs: { paths: ["packages/coding-agent/src/tools/*-render"] },
args: { paths: ["packages/coding-agent/src/**/*.test.ts"], limit: 50 },
@@ -191,7 +191,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
text: [
"packages/coding-agent/src/tools/read.test.ts",
"packages/coding-agent/src/tools/write.test.ts",
"packages/coding-agent/src/tools/find.test.ts",
"packages/coding-agent/src/tools/glob.test.ts",
"packages/coding-agent/src/cli/gallery-cli.test.ts",
"packages/coding-agent/src/edit/edit.test.ts",
].join("\n"),
@@ -205,7 +205,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
files: [
"packages/coding-agent/src/cli/gallery-cli.test.ts",
"packages/coding-agent/src/edit/edit.test.ts",
"packages/coding-agent/src/tools/find.test.ts",
"packages/coding-agent/src/tools/glob.test.ts",
"packages/coding-agent/src/tools/read.test.ts",
"packages/coding-agent/src/tools/write.test.ts",
],
@@ -213,7 +213,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
},
errorResult: {
isError: true,
content: [{ type: "text", text: "Find failed: invalid glob pattern '[unclosed'." }],
content: [{ type: "text", text: "Glob failed: invalid glob pattern '[unclosed'." }],
details: { error: "invalid glob pattern '[unclosed'" },
},
},
@@ -1,9 +1,9 @@
/** Gallery fixtures for the search tools (search, search_tool_bm25, ast_grep). */
/** Gallery fixtures for the search tools (grep, search_tool_bm25, ast_grep). */
import type { GalleryFixture } from "./types";
export const searchFixtures: Record<string, GalleryFixture> = {
search: {
label: "Search",
grep: {
label: "Grep",
streamingArgs: {
pattern: "useState",
},
@@ -101,7 +101,7 @@ export const searchFixtures: Record<string, GalleryFixture> = {
limit: 5,
total_tools: 142,
activated_tools: ["docling_extract_tables", "docling_convert", "pdf_read_text"],
active_selected_tools: ["read", "search", "edit", "bash"],
active_selected_tools: ["read", "grep", "edit", "bash"],
tools: [
{
name: "docling_extract_tables",
@@ -289,13 +289,13 @@ export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
},
{
pattern: "^\\s*(grep|rg|ripgrep|ag|ack)\\s+",
tool: "search",
message: "Use the `search` tool instead of grep/rg. It respects .gitignore and provides structured output.",
tool: "grep",
message: "Use the `grep` tool instead of grep/rg. It respects .gitignore and provides structured output.",
},
{
pattern: "^\\s*(find|fd|locate)\\s+.*(-name|-iname|-type|--type|-glob)",
tool: "find",
message: "Use the `find` tool instead of find/fd. It respects .gitignore and is faster for glob patterns.",
tool: "glob",
message: "Use the `glob` tool instead of find/fd. It respects .gitignore and is faster for glob patterns.",
},
{
pattern: "^\\s*sed\\s+(-i|--in-place)",
@@ -3238,37 +3238,37 @@ export const SETTINGS_SCHEMA = {
},
},
// Search and AST tools
"find.enabled": {
// Grep, glob, and AST tools
"glob.enabled": {
type: "boolean",
default: true,
ui: {
tab: "tools",
group: "Available Tools",
label: "Find",
description: "Enable the find tool for glob-based file lookup",
label: "Glob",
description: "Enable the glob tool for glob-based file lookup",
},
},
"search.enabled": {
"grep.enabled": {
type: "boolean",
default: true,
ui: {
tab: "tools",
group: "Available Tools",
label: "Search",
description: "Enable the search tool for regex content search",
label: "Grep",
description: "Enable the grep tool for regex content search",
},
},
"search.contextBefore": {
"grep.contextBefore": {
type: "number",
default: 1,
ui: {
tab: "tools",
group: "Search & Browser",
label: "Search Context Before",
description: "Lines of context before each search match",
group: "Grep & Browser",
label: "Grep Context Before",
description: "Lines of context before each grep match",
options: [
{ value: "0", label: "0 lines" },
{ value: "1", label: "1 line" },
@@ -3279,14 +3279,14 @@ export const SETTINGS_SCHEMA = {
},
},
"search.contextAfter": {
"grep.contextAfter": {
type: "number",
default: 3,
ui: {
tab: "tools",
group: "Search & Browser",
label: "Search Context After",
description: "Lines of context after each search match",
group: "Grep & Browser",
label: "Grep Context After",
description: "Lines of context after each grep match",
options: [
{ value: "0", label: "0 lines" },
{ value: "1", label: "1 line" },
@@ -3618,7 +3618,7 @@ export const SETTINGS_SCHEMA = {
group: "Discovery & MCP",
label: "Essential Tools Override",
description:
"Override the always-loaded built-in tools (default: read, bash, edit, write, find, eval). Leave empty to use defaults.",
"Override the always-loaded built-in tools (default: read, bash, edit, write, glob, eval). Leave empty to use defaults.",
},
},
@@ -30,6 +30,7 @@ import type { ModelRole } from "../config/model-roles";
import { loadCapability } from "../discovery";
import { isLightTheme, setAutoThemeMapping, setColorBlindMode, setSymbolPreset } from "../modes/theme/theme";
import { AgentStorage } from "../session/agent-storage";
import { normalizeToolName } from "../tools/builtin-names";
import { type EditMode, normalizeEditMode } from "../utils/edit-mode";
import { withFileLock } from "./file-lock";
import {
@@ -996,6 +997,118 @@ export class Settings {
delete raw["power.preventDisplaySleep"];
}
// Migration for renamed settings grep.* and glob.* from search.* and find.*:
// 1. Nested settings: find -> glob, search -> grep (per-property merge to avoid clobbering)
const ensureRawObject = (key: "glob" | "grep"): Record<string, unknown> => {
const current = raw[key];
if (current && typeof current === "object" && !Array.isArray(current)) {
return current as Record<string, unknown>;
}
const created: Record<string, unknown> = {};
raw[key] = created;
return created;
};
if ("find" in raw) {
const findObj = raw.find;
if (findObj && typeof findObj === "object" && !Array.isArray(findObj)) {
const globObj = ensureRawObject("glob");
for (const key of ["enabled"]) {
if (key in findObj && !(key in globObj)) {
globObj[key] = findObj[key];
}
}
}
delete raw.find;
}
if ("search" in raw) {
const searchObj = raw.search;
if (searchObj && typeof searchObj === "object" && !Array.isArray(searchObj)) {
const grepObj = ensureRawObject("grep");
for (const key of ["enabled", "contextBefore", "contextAfter"]) {
if (key in searchObj && !(key in grepObj)) {
grepObj[key] = searchObj[key];
}
}
}
delete raw.search;
}
// 2. Flat settings keys: map them to the proper nested target so get/set resolves them correctly
if ("find.enabled" in raw) {
const globObj = ensureRawObject("glob");
if (!("enabled" in globObj)) {
globObj.enabled = raw["find.enabled"];
}
delete raw["find.enabled"];
}
if ("search.enabled" in raw) {
const grepObj = ensureRawObject("grep");
if (!("enabled" in grepObj)) {
grepObj.enabled = raw["search.enabled"];
}
delete raw["search.enabled"];
}
if ("search.contextBefore" in raw) {
const grepObj = ensureRawObject("grep");
if (!("contextBefore" in grepObj)) {
grepObj.contextBefore = raw["search.contextBefore"];
}
delete raw["search.contextBefore"];
}
if ("search.contextAfter" in raw) {
const grepObj = ensureRawObject("grep");
if (!("contextAfter" in grepObj)) {
grepObj.contextAfter = raw["search.contextAfter"];
}
delete raw["search.contextAfter"];
}
// 3. Tool-name arrays use wire IDs too. Preserve user overrides across
// the rename without duplicating entries if they already added grep/glob.
const migrateToolNameList = (names: unknown): unknown => {
if (!Array.isArray(names)) return names;
const out: unknown[] = [];
const seen = new Set<string>();
for (const name of names) {
const migrated = typeof name === "string" ? normalizeToolName(name) : name;
if (typeof migrated === "string") {
if (seen.has(migrated)) continue;
seen.add(migrated);
}
out.push(migrated);
}
return out;
};
const ensureToolsObject = (): Record<string, unknown> => {
const current = raw.tools;
if (current && typeof current === "object" && !Array.isArray(current)) {
return current as Record<string, unknown>;
}
const created: Record<string, unknown> = {};
raw.tools = created;
return created;
};
const toolsObj = raw.tools as Record<string, unknown> | undefined;
if (toolsObj && "essentialOverride" in toolsObj) {
toolsObj.essentialOverride = migrateToolNameList(toolsObj.essentialOverride);
}
if ("tools.essentialOverride" in raw) {
const nestedToolsObj = ensureToolsObject();
if (!("essentialOverride" in nestedToolsObj)) {
nestedToolsObj.essentialOverride = migrateToolNameList(raw["tools.essentialOverride"]);
}
delete raw["tools.essentialOverride"];
}
// Also clean up any empty nested objects we might have created or left behind
if (raw.glob && typeof raw.glob === "object" && Object.keys(raw.glob).length === 0) {
delete raw.glob;
}
if (raw.grep && typeof raw.grep === "object" && Object.keys(raw.grep).length === 0) {
delete raw.grep;
}
// readHashLines: removed. Hashline anchors are now driven solely by
// edit.mode === "hashline"; the separate read toggle only ever produced
// the incoherent "hashline edits without addressable anchors" state.
+1 -1
View File
@@ -178,7 +178,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
async grep(args: Parameters<NonNullable<ICursorExecHandlers["grep"]>>[0]) {
const toolCallId = decodeToolCallId(args.toolCallId);
const searchPath = args.glob ? `${args.path || "."}/${args.glob}` : args.path || ".";
const toolResultMessage = await executeTool(this.options, "search", toolCallId, {
const toolResultMessage = await executeTool(this.options, "grep", toolCallId, {
pattern: args.pattern,
paths: [searchPath],
case: args.caseInsensitive === true ? false : undefined,
@@ -18,6 +18,7 @@ import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../
import type { Skill, SkillFrontmatter } from "../capability/skill";
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
import { parseThinkingLevel } from "../thinking";
import { normalizeToolNames } from "../tools/builtin-names";
import { buildPluginDirRoot } from "./plugin-dir-roots";
@@ -246,7 +247,8 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
return null;
}
let tools = parseArrayOrCSV(frontmatter.tools)?.map(tool => tool.toLowerCase());
let tools = parseArrayOrCSV(frontmatter.tools);
if (tools) tools = normalizeToolNames(tools);
// Subagents with explicit tool lists always need yield
if (tools && !tools.includes("yield")) {
@@ -88,9 +88,9 @@ function summarizeToolResult(
path: record.path,
count: details.matchCount ?? undefined,
});
case "find":
case "glob":
return withError({
op: "find",
op: "glob",
pattern: record.pattern,
count: details.fileCount ?? undefined,
matches: Array.isArray(details.files) ? details.files.slice(0, 20) : undefined,
@@ -433,10 +433,12 @@
const cmd = rawCmd.replace(/[\n\t]/g, ' ').trim().slice(0, 50);
return `[bash: ${cmd}${rawCmd.length > 50 ? '...' : ''}]`;
}
case 'search':
case 'grep':
return `[grep: /${args.pattern || ''}/ in ${shortenPath(String((args.paths || [args.path || '.']).join(', ')))}]`;
case 'find':
return `[find: ${shortenPath(String((args.paths || [args.pattern || '.']).join(', ')))}]`;
case 'glob':
return `[glob: ${shortenPath(String((args.paths || [args.pattern || '.']).join(', ')))}]`;
case 'ls':
return `[ls: ${shortenPath(String(args.path || '.'))}]`;
default: {
@@ -50,12 +50,12 @@ import type { ReadonlySessionManager, SessionManager } from "../../session/sessi
import type {
BashToolDetails,
BashToolInput,
FindToolDetails,
FindToolInput,
GlobToolDetails,
GlobToolInput,
GrepToolDetails,
GrepToolInput,
ReadToolDetails,
ReadToolInput,
SearchToolDetails,
SearchToolInput,
WriteToolInput,
} from "../../tools";
import type { ApprovalMode } from "../../tools/approval";
@@ -697,14 +697,14 @@ export interface WriteToolCallEvent extends ToolCallEventBase {
input: WriteToolInput;
}
export interface SearchToolCallEvent extends ToolCallEventBase {
toolName: "search";
input: SearchToolInput;
export interface GrepToolCallEvent extends ToolCallEventBase {
toolName: "grep";
input: GrepToolInput;
}
export interface FindToolCallEvent extends ToolCallEventBase {
toolName: "find";
input: FindToolInput;
export interface GlobToolCallEvent extends ToolCallEventBase {
toolName: "glob";
input: GlobToolInput;
}
export interface CustomToolCallEvent extends ToolCallEventBase {
@@ -718,8 +718,8 @@ export type ToolCallEvent =
| ReadToolCallEvent
| EditToolCallEvent
| WriteToolCallEvent
| SearchToolCallEvent
| FindToolCallEvent
| GrepToolCallEvent
| GlobToolCallEvent
| CustomToolCallEvent;
interface ToolResultEventBase {
@@ -750,14 +750,14 @@ export interface WriteToolResultEvent extends ToolResultEventBase {
details: undefined;
}
export interface SearchToolResultEvent extends ToolResultEventBase {
toolName: "search";
details: SearchToolDetails | undefined;
export interface GrepToolResultEvent extends ToolResultEventBase {
toolName: "grep";
details: GrepToolDetails | undefined;
}
export interface FindToolResultEvent extends ToolResultEventBase {
toolName: "find";
details: FindToolDetails | undefined;
export interface GlobToolResultEvent extends ToolResultEventBase {
toolName: "glob";
details: GlobToolDetails | undefined;
}
export interface CustomToolResultEvent extends ToolResultEventBase {
@@ -771,8 +771,8 @@ export type ToolResultEvent =
| ReadToolResultEvent
| EditToolResultEvent
| WriteToolResultEvent
| SearchToolResultEvent
| FindToolResultEvent
| GrepToolResultEvent
| GlobToolResultEvent
| CustomToolResultEvent;
/**
@@ -799,8 +799,8 @@ export function isToolCallEventType(toolName: "bash", event: ToolCallEvent): eve
export function isToolCallEventType(toolName: "read", event: ToolCallEvent): event is ReadToolCallEvent;
export function isToolCallEventType(toolName: "edit", event: ToolCallEvent): event is EditToolCallEvent;
export function isToolCallEventType(toolName: "write", event: ToolCallEvent): event is WriteToolCallEvent;
export function isToolCallEventType(toolName: "search", event: ToolCallEvent): event is SearchToolCallEvent;
export function isToolCallEventType(toolName: "find", event: ToolCallEvent): event is FindToolCallEvent;
export function isToolCallEventType(toolName: "grep", event: ToolCallEvent): event is GrepToolCallEvent;
export function isToolCallEventType(toolName: "glob", event: ToolCallEvent): event is GlobToolCallEvent;
export function isToolCallEventType<TName extends string, TInput extends Record<string, unknown>>(
toolName: TName,
event: ToolCallEvent,
@@ -10,7 +10,7 @@ import type * as PiCodingAgent from "../../index";
import type { Theme } from "../../modes/theme/theme";
import type { HookMessage } from "../../session/messages";
import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
import type { BashToolDetails, FindToolDetails, ReadToolDetails, SearchToolDetails } from "../../tools";
import type { BashToolDetails, GlobToolDetails, GrepToolDetails, ReadToolDetails } from "../../tools";
import type {
AgentEndEvent,
AgentStartEvent,
@@ -352,16 +352,16 @@ export interface WriteToolResultEvent extends ToolResultEventBase {
details: undefined;
}
/** Tool result event for search tool */
export interface SearchToolResultEvent extends ToolResultEventBase {
toolName: "search";
details: SearchToolDetails | undefined;
/** Tool result event for grep tool */
export interface GrepToolResultEvent extends ToolResultEventBase {
toolName: "grep";
details: GrepToolDetails | undefined;
}
/** Tool result event for find tool */
export interface FindToolResultEvent extends ToolResultEventBase {
toolName: "find";
details: FindToolDetails | undefined;
/** Tool result event for glob tool */
export interface GlobToolResultEvent extends ToolResultEventBase {
toolName: "glob";
details: GlobToolDetails | undefined;
}
/** Tool result event for custom/unknown tools */
@@ -380,8 +380,8 @@ export type ToolResultEvent =
| ReadToolResultEvent
| EditToolResultEvent
| WriteToolResultEvent
| SearchToolResultEvent
| FindToolResultEvent
| GrepToolResultEvent
| GlobToolResultEvent
| CustomToolResultEvent;
/**
@@ -93,6 +93,7 @@ export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([
"@oh-my-pi/pi-ai/usage/google-antigravity",
"@oh-my-pi/pi-ai/usage/kimi",
"@oh-my-pi/pi-ai/usage/minimax-code",
"@oh-my-pi/pi-ai/usage/ollama",
"@oh-my-pi/pi-ai/usage/openai-codex-reset",
"@oh-my-pi/pi-ai/usage/openai-codex",
"@oh-my-pi/pi-ai/usage/opencode-go",
@@ -352,6 +353,7 @@ export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([
"@oh-my-pi/pi-coding-agent/config/append-only-context-mode",
"@oh-my-pi/pi-coding-agent/config/config-file",
"@oh-my-pi/pi-coding-agent/config/file-lock",
"@oh-my-pi/pi-coding-agent/config/inline-tool-descriptors-mode",
"@oh-my-pi/pi-coding-agent/config/keybindings",
"@oh-my-pi/pi-coding-agent/config/model-discovery",
"@oh-my-pi/pi-coding-agent/config/model-registry",
@@ -780,13 +782,14 @@ export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([
"@oh-my-pi/pi-coding-agent/tools/eval",
"@oh-my-pi/pi-coding-agent/tools/fetch",
"@oh-my-pi/pi-coding-agent/tools/file-recorder",
"@oh-my-pi/pi-coding-agent/tools/find",
"@oh-my-pi/pi-coding-agent/tools/fs-cache-invalidation",
"@oh-my-pi/pi-coding-agent/tools/gh-cache-invalidation",
"@oh-my-pi/pi-coding-agent/tools/gh-format",
"@oh-my-pi/pi-coding-agent/tools/gh-renderer",
"@oh-my-pi/pi-coding-agent/tools/gh",
"@oh-my-pi/pi-coding-agent/tools/github-cache",
"@oh-my-pi/pi-coding-agent/tools/glob",
"@oh-my-pi/pi-coding-agent/tools/grep",
"@oh-my-pi/pi-coding-agent/tools/grouped-file-output",
"@oh-my-pi/pi-coding-agent/tools/image-gen",
"@oh-my-pi/pi-coding-agent/tools/inspect-image-renderer",
@@ -817,7 +820,6 @@ export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([
"@oh-my-pi/pi-coding-agent/tools/resolve",
"@oh-my-pi/pi-coding-agent/tools/review",
"@oh-my-pi/pi-coding-agent/tools/search-tool-bm25",
"@oh-my-pi/pi-coding-agent/tools/search",
"@oh-my-pi/pi-coding-agent/tools/sqlite-reader",
"@oh-my-pi/pi-coding-agent/tools/ssh",
"@oh-my-pi/pi-coding-agent/tools/todo",
@@ -116,6 +116,7 @@ import * as bundledPiAiUsageGithubCopilot from "@oh-my-pi/pi-ai/usage/github-cop
import * as bundledPiAiUsageGoogleAntigravity from "@oh-my-pi/pi-ai/usage/google-antigravity";
import * as bundledPiAiUsageKimi from "@oh-my-pi/pi-ai/usage/kimi";
import * as bundledPiAiUsageMinimaxCode from "@oh-my-pi/pi-ai/usage/minimax-code";
import * as bundledPiAiUsageOllama from "@oh-my-pi/pi-ai/usage/ollama";
import * as bundledPiAiUsageOpenaiCodex from "@oh-my-pi/pi-ai/usage/openai-codex";
import * as bundledPiAiUsageOpenaiCodexReset from "@oh-my-pi/pi-ai/usage/openai-codex-reset";
import * as bundledPiAiUsageOpencodeGo from "@oh-my-pi/pi-ai/usage/opencode-go";
@@ -320,6 +321,7 @@ import * as bundledPiCodingAgentConfigApiKeyResolver from "@oh-my-pi/pi-coding-a
import * as bundledPiCodingAgentConfigAppendOnlyContextMode from "@oh-my-pi/pi-coding-agent/config/append-only-context-mode";
import * as bundledPiCodingAgentConfigConfigFile from "@oh-my-pi/pi-coding-agent/config/config-file";
import * as bundledPiCodingAgentConfigFileLock from "@oh-my-pi/pi-coding-agent/config/file-lock";
import * as bundledPiCodingAgentConfigInlineToolDescriptorsMode from "@oh-my-pi/pi-coding-agent/config/inline-tool-descriptors-mode";
import * as bundledPiCodingAgentConfigKeybindings from "@oh-my-pi/pi-coding-agent/config/keybindings";
import * as bundledPiCodingAgentConfigModelDiscovery from "@oh-my-pi/pi-coding-agent/config/model-discovery";
import * as bundledPiCodingAgentConfigModelRegistry from "@oh-my-pi/pi-coding-agent/config/model-registry";
@@ -782,13 +784,14 @@ import * as bundledPiCodingAgentToolsEvalBackends from "@oh-my-pi/pi-coding-agen
import * as bundledPiCodingAgentToolsEvalRender from "@oh-my-pi/pi-coding-agent/tools/eval-render";
import * as bundledPiCodingAgentToolsFetch from "@oh-my-pi/pi-coding-agent/tools/fetch";
import * as bundledPiCodingAgentToolsFileRecorder from "@oh-my-pi/pi-coding-agent/tools/file-recorder";
import * as bundledPiCodingAgentToolsFind from "@oh-my-pi/pi-coding-agent/tools/find";
import * as bundledPiCodingAgentToolsFsCacheInvalidation from "@oh-my-pi/pi-coding-agent/tools/fs-cache-invalidation";
import * as bundledPiCodingAgentToolsGh from "@oh-my-pi/pi-coding-agent/tools/gh";
import * as bundledPiCodingAgentToolsGhCacheInvalidation from "@oh-my-pi/pi-coding-agent/tools/gh-cache-invalidation";
import * as bundledPiCodingAgentToolsGhFormat from "@oh-my-pi/pi-coding-agent/tools/gh-format";
import * as bundledPiCodingAgentToolsGhRenderer from "@oh-my-pi/pi-coding-agent/tools/gh-renderer";
import * as bundledPiCodingAgentToolsGithubCache from "@oh-my-pi/pi-coding-agent/tools/github-cache";
import * as bundledPiCodingAgentToolsGlob from "@oh-my-pi/pi-coding-agent/tools/glob";
import * as bundledPiCodingAgentToolsGrep from "@oh-my-pi/pi-coding-agent/tools/grep";
import * as bundledPiCodingAgentToolsGroupedFileOutput from "@oh-my-pi/pi-coding-agent/tools/grouped-file-output";
import * as bundledPiCodingAgentToolsImageGen from "@oh-my-pi/pi-coding-agent/tools/image-gen";
import * as bundledPiCodingAgentToolsInspectImage from "@oh-my-pi/pi-coding-agent/tools/inspect-image";
@@ -818,7 +821,6 @@ import * as bundledPiCodingAgentToolsRenderers from "@oh-my-pi/pi-coding-agent/t
import * as bundledPiCodingAgentToolsReportToolIssue from "@oh-my-pi/pi-coding-agent/tools/report-tool-issue";
import * as bundledPiCodingAgentToolsResolve from "@oh-my-pi/pi-coding-agent/tools/resolve";
import * as bundledPiCodingAgentToolsReview from "@oh-my-pi/pi-coding-agent/tools/review";
import * as bundledPiCodingAgentToolsSearch from "@oh-my-pi/pi-coding-agent/tools/search";
import * as bundledPiCodingAgentToolsSearchToolBm25 from "@oh-my-pi/pi-coding-agent/tools/search-tool-bm25";
import * as bundledPiCodingAgentToolsSqliteReader from "@oh-my-pi/pi-coding-agent/tools/sqlite-reader";
import * as bundledPiCodingAgentToolsSsh from "@oh-my-pi/pi-coding-agent/tools/ssh";
@@ -1180,6 +1182,7 @@ export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string
>,
"@oh-my-pi/pi-ai/usage/kimi": bundledPiAiUsageKimi as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-ai/usage/minimax-code": bundledPiAiUsageMinimaxCode as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-ai/usage/ollama": bundledPiAiUsageOllama as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-ai/usage/openai-codex-reset": bundledPiAiUsageOpenaiCodexReset as unknown as Readonly<
Record<string, unknown>
>,
@@ -1782,6 +1785,8 @@ export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string
"@oh-my-pi/pi-coding-agent/config/file-lock": bundledPiCodingAgentConfigFileLock as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/config/inline-tool-descriptors-mode":
bundledPiCodingAgentConfigInlineToolDescriptorsMode as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/config/keybindings": bundledPiCodingAgentConfigKeybindings as unknown as Readonly<
Record<string, unknown>
>,
@@ -2828,9 +2833,6 @@ export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string
"@oh-my-pi/pi-coding-agent/tools/file-recorder": bundledPiCodingAgentToolsFileRecorder as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/tools/find": bundledPiCodingAgentToolsFind as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/tools/fs-cache-invalidation":
bundledPiCodingAgentToolsFsCacheInvalidation as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/tools/gh-cache-invalidation":
@@ -2845,6 +2847,12 @@ export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string
"@oh-my-pi/pi-coding-agent/tools/github-cache": bundledPiCodingAgentToolsGithubCache as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/tools/glob": bundledPiCodingAgentToolsGlob as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/tools/grep": bundledPiCodingAgentToolsGrep as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/tools/grouped-file-output":
bundledPiCodingAgentToolsGroupedFileOutput as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/tools/image-gen": bundledPiCodingAgentToolsImageGen as unknown as Readonly<
@@ -2927,9 +2935,6 @@ export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string
"@oh-my-pi/pi-coding-agent/tools/search-tool-bm25": bundledPiCodingAgentToolsSearchToolBm25 as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/tools/search": bundledPiCodingAgentToolsSearch as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/tools/sqlite-reader": bundledPiCodingAgentToolsSqliteReader as unknown as Readonly<
Record<string, unknown>
>,
+1 -1
View File
@@ -559,7 +559,7 @@ function shouldPersistResponseItemForMemories(message: AgentMessage): boolean {
}
if (role !== "toolResult") return false;
const toolName = (message as { toolName?: string }).toolName;
if (toolName === "bash" || toolName === "eval" || toolName === "read" || toolName === "search") {
if (toolName === "bash" || toolName === "eval" || toolName === "read" || toolName === "grep") {
const text = extractMessageText(message);
return text.length > 0 && text.length <= 32_000;
}
@@ -143,8 +143,8 @@ export function mapToolKind(toolName: string): ToolKind {
case "exec":
case "eval":
return "execute";
case "search":
case "find":
case "grep":
case "glob":
case "ast_grep":
return "search";
case "web_search":
@@ -15,8 +15,8 @@ import type { TreeFilterMode } from "../../config/settings-schema";
import { theme } from "../../modes/theme/theme";
import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
import type { SessionTreeNode } from "../../session/session-entries";
import { toPathList } from "../../tools/grep";
import { shortenPath } from "../../tools/render-utils";
import { toPathList } from "../../tools/search";
import { canonicalizeMessage } from "../../utils/thinking-display";
import { DynamicBorder } from "./dynamic-border";
import { centeredWindow, contentRowWidth, renderScrollableList } from "./selector-helpers";
@@ -740,7 +740,7 @@ class TreeList implements Component {
.slice(0, 50);
return `[bash: ${cmd}${rawCmd.length > 50 ? "..." : ""}]`;
}
case "search": {
case "grep": {
const pattern = String(args.pattern || "");
const searchPathsInput =
typeof args.paths === "string" || Array.isArray(args.paths)
@@ -750,11 +750,11 @@ class TreeList implements Component {
: undefined;
const paths = toPathList(searchPathsInput);
const scope = paths.length > 0 ? paths.join(", ") : ".";
return `[search: /${pattern}/ in ${shortenPath(scope)}]`;
return `[grep: /${pattern}/ in ${shortenPath(scope)}]`;
}
case "find": {
case "glob": {
const paths = Array.isArray(args.paths) ? args.paths.join(", ") : String(args.pattern || ".");
return `[find: ${shortenPath(paths)}]`;
return `[glob: ${shortenPath(paths)}]`;
}
case "ls": {
const path = shortenPath(String(args.path || "."));
@@ -11,7 +11,7 @@ Your job is to offer that view before they sink work into the wrong direction.
<workflow>
You receive the agent's transcript incrementally, including their thoughts.
You have read-only access through `read`, `search`, `find` to verify your suspicions.
You have read-only access through `read`, `grep`, `glob` to verify your suspicions.
Keep exploration lean:
- 2–3 tool calls per advise.
- Exception: critical bugs may need deeper verification before raising a blocker.
@@ -44,7 +44,7 @@ Cite only transcript evidence or tool output you personally inspected.
Arguments absent from the rendered transcript are UNKNOWN:
- NEVER assert concrete values, array indexes, serialization shapes, or caller mistakes for hidden arguments.
- Hidden/omitted arguments + failure? Say what is observable; suggest inspecting the missing field.
- Example: if `search` times out and transcript only shows `pattern`, NEVER claim `paths[0]`, array flattening, or malformed `paths`.
- Example: if `grep` times out and transcript only shows `pattern`, NEVER claim `paths[0]`, array flattening, or malformed `paths`.
Cite the exact instruction or risk.
</critical>
@@ -16,7 +16,7 @@ Implement and review UI designs. Edit files, create components, run commands whe
<design-system>
Treat the design system as the foundation — UI built without one collapses into inconsistency. Work four phases in order:
1. **Token-first analysis (before any CSS/JSX/Svelte).** `search`/`read` for the design tokens (colors, spacing, typography, shadows, radii), theme files (CSS variables, Tailwind config, `theme.ts`), and shared primitives (Button, Card, Input, Layout). Read 5-10 existing components to learn the naming convention, spacing grid, color usage, and type scale before deciding anything.
1. **Token-first analysis (before any CSS/JSX/Svelte).** `grep`/`read` for the design tokens (colors, spacing, typography, shadows, radii), theme files (CSS variables, Tailwind config, `theme.ts`), and shared primitives (Button, Card, Input, Layout). Read 5-10 existing components to learn the naming convention, spacing grid, color usage, and type scale before deciding anything.
2. **No coherent system? Build the minimal one first.** Extract what exists, then define a palette, type scale, spacing scale (4px/8px base), radii/shadows/transitions, and primitive components — THEN implement the request against it.
3. **Compose with the system, never around it.** Colors → tokens/CSS variables, never hardcoded hex; spacing → scale values, never arbitrary px; type → scale steps; components → extend/compose existing primitives, not one-off div soup. Need something outside the system? Add the new token to the system first, then use it — never a one-off override.
4. **Verify before done.** Every color a token, every spacing on the scale, every component on the existing composition pattern, zero magic numbers — a designer would see consistency across old and new. Any "no" → not done.
@@ -1,7 +1,7 @@
---
name: explore
description: Fast read-only codebase scout returning compressed context for handoff
tools: read, search, find, web_search
tools: read, grep, glob, web_search
model: pi/smol
thinking-level: medium
read-summarize: false
@@ -1,7 +1,7 @@
---
name: librarian
description: Researches external libraries and APIs by reading source code. Returns definitive, source-verified answers.
tools: read, search, find, bash, lsp, web_search, ast_grep
tools: read, grep, glob, bash, lsp, web_search, ast_grep
model: pi/smol
thinking-level: minimal
read-summarize: false
@@ -86,7 +86,7 @@ You MUST operate as read-only on the user's project. You NEVER modify any projec
## 3. Investigate
- Read `package.json`, `Cargo.toml`, or equivalent for version info and entry points.
- Use `search`, `find`, and `ast_grep` to locate relevant source, type definitions, and docs. Parallelize searches.
- Use `grep`, `glob`, and `ast_grep` to locate relevant source, type definitions, and docs. Parallelize searches.
- Read the actual implementation — not just README examples. READMEs are aspirational; source code is truth.
- For behavior questions: trace through the implementation. Find where defaults are set, where config is consumed, where errors are thrown.
- Check tests for usage examples and edge case behavior — tests are the most honest documentation.
@@ -1,7 +1,7 @@
---
name: plan
description: Software architect for complex multi-file architectural decisions. NOT for simple tasks, single-file changes, or tasks completable in <5 tool calls.
tools: read, search, find, bash, lsp, web_search, ast_grep
tools: read, grep, glob, bash, lsp, web_search, ast_grep
spawns: explore
model: pi/plan, pi/slow
thinking-level: high
@@ -14,7 +14,7 @@ Analyze the codebase and the user's request. Produce a detailed implementation p
2. Identify ambiguities; list assumptions
## Phase 2: Explore
1. Find existing patterns via `search`/`find`
1. Find existing patterns via `grep`/`glob`
2. Read key files; understand architecture
3. Trace data flow through relevant paths
4. Identify types, interfaces, contracts
@@ -1,7 +1,7 @@
---
name: reviewer
description: "Code review specialist for quality/security analysis"
tools: read, search, find, bash, lsp, web_search, ast_grep, report_finding
tools: read, grep, glob, bash, lsp, web_search, ast_grep, report_finding
spawns: explore
model: pi/slow
thinking-level: high
@@ -1,6 +1,6 @@
You are a worker agent for delegated tasks.
You have FULL access to all tools (edit, write, bash, search, read, etc.) and you MUST use them as needed to complete your task.
You have FULL access to all tools (edit, write, bash, grep, read, etc.) and you MUST use them as needed to complete your task.
You MUST maintain hyperfocus on the assigned task. NEVER deviate from it.
@@ -8,7 +8,7 @@ You MUST maintain hyperfocus on the assigned task. NEVER deviate from it.
- You MUST finish only the assigned work and return the minimum useful result. Do not repeat what you have written to the filesystem.
- You SHOULD make file edits, run commands, and create files when your task requires it.
- You MUST be concise. You NEVER include filler, repetition, or tool transcripts. The user cannot see you. Your result is just the notes you are leaving for yourself.
- You SHOULD prefer narrow lookups (`search`/`find`), then read only the needed ranges. Ignore anything beyond your current scope.
- You SHOULD prefer narrow lookups (`grep`/`glob`), then read only the needed ranges. Ignore anything beyond your current scope.
- AVOID full-file reads unless necessary.
- You SHOULD prefer edits to existing files over creating new ones.
- You NEVER create documentation files (*.md) unless explicitly requested.
@@ -37,7 +37,7 @@ Write each section together with its body — block ops need a multi-line sectio
You eliminate unknowns by discovering facts, not by asking.
- **Discoverable facts** (file locations, current behavior, signatures, configs): you MUST find them yourself with `find`, `search`, `read`, or parallel `explore` subagents. Every path, symbol, signature, and behavior the plan states as fact MUST come from something you actually read this session. Anything you could not confirm you mark inline (`unverified — confirm first`); you NEVER present a guess as settled. Ask only when several real candidates survive exploration — then present them with a recommendation.
- **Discoverable facts** (file locations, current behavior, signatures, configs): you MUST find them yourself with `glob`, `grep`, `read`, or parallel `explore` subagents. Every path, symbol, signature, and behavior the plan states as fact MUST come from something you actually read this session. Anything you could not confirm you mark inline (`unverified — confirm first`); you NEVER present a guess as settled. Ask only when several real candidates survive exploration — then present them with a recommendation.
- **Preferences and tradeoffs** (intent, UX, scope edges, performance-vs-simplicity): not derivable from code. Surface these early via `{{askToolName}}` with 2–4 mutually exclusive options and a recommended default. Left unanswered → proceed with the default and record it under Assumptions.
Every question MUST change the plan or settle a load-bearing choice. Batch them. You NEVER ask what exploration answers, and you NEVER ask filler.
@@ -57,7 +57,7 @@ Every question MUST change the plan or settle a load-bearing choice. Batch them.
## Workflow — iterative
<procedure>
1. **Explore** — use `find`/`search`/`read` to ground in the real code; hunt for existing functions, utilities, and conventions to reuse before proposing anything new.
1. **Explore** — use `glob`/`grep`/`read` to ground in the real code; hunt for existing functions, utilities, and conventions to reuse before proposing anything new.
2. **Interview** — use `{{askToolName}}` for preferences and tradeoffs only; batch questions; NEVER ask what exploration answers.
3. **Update** — revise the plan with `{{editToolName}}` as you learn.
4. **Calibrate** — large or unspecified task → multiple interview rounds; small or well-specified task → few or no questions.
@@ -82,7 +82,7 @@ Write scannable markdown using these sections. Let depth track the change, not a
- State the concrete edit — verb + exact target + the new behavior — NEVER just an area to "update" or "handle".
- Name existing functions/utilities to reuse, with paths; introduce new code only with a one-line note that no existing equivalent was found.
- For a new or changed symbol whose callers must fit it, or whose value is load-bearing (enum member, error/log string, config key, wire/JSON field), give the exact signature or literal.
- For a rename, signature change, or removal, list every callsite to update (or the exact `search` that returns exactly them) and what to delete — default to a clean cutover with no dead code or compatibility aliases.
- For a rename, signature change, or removal, list every callsite to update (or the exact `grep` that returns exactly them) and what to delete — default to a clean cutover with no dead code or compatibility aliases.
- When rival patterns exist, name the one to copy and the one to avoid.
- Specify the edge and failure handling for each new path (empty, missing, conflict, error), or state that none is needed and why.
- **Critical files & anchors** — the ≤5 files that disambiguate non-obvious work, each as path + the symbol or region + a one-line reason. Line numbers are hints; the implementer re-reads before editing. Skip files already obvious from the Approach.
@@ -26,7 +26,7 @@ Before making changes within these directories, you MUST read:
{{/if}}
{{#ifAny contextFiles.length agentsMdSearch.files.length}}
The context files above are loaded automatically. You NEVER `search`/`find` for `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, or similar agent/context files — the relevant ones are already in your context; any others are noise.
The context files above are loaded automatically. You NEVER `grep`/`glob` for `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, or similar agent/context files — the relevant ones are already in your context; any others are noise.
{{/ifAny}}
{{#if includeWorkspaceTree}}
@@ -35,7 +35,7 @@ The context files above are loaded automatically. You NEVER `search`/`find` for
Working directory layout (sorted by mtime, recent first; depth ≤ 3):
{{workspaceTree.rendered}}
{{#if workspaceTree.truncated}}
(some entries elided to keep the tree short — use `find`/`read` to drill in)
(some entries elided to keep the tree short — use `glob`/`read` to drill in)
{{/if}}
</workspace-tree>
{{/if}}
@@ -109,8 +109,8 @@ You MUST use the specialized tool over its shell equivalent:
{{#has tools "edit"}}- Surgical edits → `{{toolRefs.edit}}`.{{/has}}
{{#has tools "write"}}- Create or overwrite → `{{toolRefs.write}}`.{{/has}}
{{#has tools "lsp"}}- Code intelligence → `{{toolRefs.lsp}}`.{{/has}}
{{#has tools "search"}}- Regex search → `{{toolRefs.search}}`, not `grep`, `rg`, or `awk`.{{/has}}
{{#has tools "find"}}- Globbing → `{{toolRefs.find}}`, not `ls **/*.ext` or `fd`.{{/has}}
{{#has tools "grep"}}- Regex search → `{{toolRefs.grep}}`, not `grep`, `rg`, or `awk`.{{/has}}
{{#has tools "glob"}}- Globbing → `{{toolRefs.glob}}`, not `ls **/*.ext` or `fd`.{{/has}}
{{#has tools "eval"}}- Default for any compute: `{{toolRefs.eval}}` cells. Bash is the EXCEPTION — only single binary calls or short fact-computing pipelines (`wc -l`, `sort | uniq -c`, `diff`, checksums). The moment a command grows a loop, conditional, heredoc, `-e`/`-c` script, `$(…)` nesting, or >2 pipe stages, it's a program → `{{toolRefs.eval}}`. NEVER write multiline or inline-script bash.{{/has}}
{{#has tools "bash"}}- `{{toolRefs.bash}}`: real binaries and short fact pipelines only. Commands shadowing the specialized tools above are blocked.{{/has}}
{{#has tools "bash"}}- Litmus: one external-CLI call or short pipeline returning a count, frequency, set difference, or checksum → bash.{{#has tools "eval"}} Needs control flow, state, or fights shell quoting → `{{toolRefs.eval}}`.{{/has}} Merely moves, pages, or trims bytes a tool can fetch → use the tool.{{/has}}
@@ -124,8 +124,8 @@ You MUST use the specialized tool over its shell equivalent:
# Exploration
You NEVER open a file hoping. Hope is not a strategy.
- You MUST load only what's necessary; AVOID reading files or sections you don't need.
{{#has tools "search"}}- Use `{{toolRefs.search}}` to locate targets.{{/has}}
{{#has tools "find"}}- Use `{{toolRefs.find}}` to map structure.{{/has}}
{{#has tools "grep"}}- Use `{{toolRefs.grep}}` to locate targets.{{/has}}
{{#has tools "glob"}}- Use `{{toolRefs.glob}}` to map structure.{{/has}}
{{#has tools "read"}}- Use `{{toolRefs.read}}` with offset/limit instead of whole-file reads.{{/has}}
{{#has tools "task"}}- Use `{{toolRefs.task}}` to map unknown code instead of reading file after file yourself.{{/has}}
@@ -141,7 +141,7 @@ You NEVER use search or manual edits for code intelligence when a language serve
You SHOULD use syntax-aware tools before text hacks:
{{#has tools "ast_grep"}}- `{{toolRefs.ast_grep}}` for structural discovery.{{/has}}
{{#has tools "ast_edit"}}- `{{toolRefs.ast_edit}}` for codemods.{{/has}}
- Use `search` only for plain-text lookup when structure is irrelevant.
- Use `grep` only for plain-text lookup when structure is irrelevant.
{{/ifAny}}
# Delegation
@@ -180,7 +180,7 @@ EXECUTION WORKFLOW
- Fix problems at the source. Remove obsolete code—no leftover comments, aliases, or re-exports.
- Prefer updating existing files over creating new ones.
- Review changes from the user's perspective.
{{#has tools "search"}}- Search instead of guessing.{{/has}}
{{#has tools "grep"}}- Grep instead of guessing.{{/has}}
{{#has tools "ask"}}- Ask before destructive commands or deleting code you didn't write.{{else}}- Don't run destructive git commands or delete code you didn't write.{{/has}}
# 5. Verify
@@ -29,8 +29,8 @@ Anything below → `eval` cell, not bash:
<critical>
- Bash invokes real binaries with simple args; it is NOT a scripting surface. Loops, conditionals, heredocs, inline interpreter scripts (`-e`/`-c`/`--eval`) when an eval runtime exists, several piped stages, or quote/JSON escaping mean you're writing a program → use `eval` cells: restartable, stateful, and free of shell-quoting traps.
- NEVER shell out to search content or files: `grep/rg` → `search`.
- NEVER use `ls` or `find` to list or locate files — `ls` → `read` (a directory path lists entries), `find` → the `find` tool (globbing). This is non-negotiable, even for a single quick listing.
- NEVER shell out to search content or files: `grep/rg` → `grep`.
- NEVER use `ls` or `find` to list or locate files — `ls` → `read` (a directory path lists entries), `find` → the `glob` tool (globbing). This is non-negotiable, even for a single quick listing.
- Avoid head/tail/redirections: stderr already merged; long output auto-truncated, FULL capture kept at `artifact://<id>`.
</critical>
@@ -1,6 +1,6 @@
Creates a context checkpoint before exploratory work so you can later rewind and keep only a concise report.
Use this when you need to investigate with many intermediate tool calls (read/search/find/lsp/etc.) and want to minimize context cost afterward.
Use this when you need to investigate with many intermediate tool calls (read/grep/glob/lsp/etc.) and want to minimize context cost afterward.
Rules:
- You MUST call `rewind` before yielding after starting a checkpoint.
@@ -1,4 +1,4 @@
Finds files and directories via fast pattern matching, any codebase size.
Globs files and directories via fast pattern matching, any codebase size.
<instruction>
- `paths`: array of globs, files, or directories.
@@ -1,4 +1,4 @@
Searches files using regex.
Greps files using regex.
<instruction>
- Rust regex (RE2-style) — no lookaround/backreferences; use line anchors or post-filters instead of (?!…)/(?<!…).
@@ -17,6 +17,6 @@ Searches files using regex.
</output>
<critical>
- MUST use built-in `search` for any content search. NEVER shell out to `grep`, `rg`, `ripgrep`, `ag`, `ack`, `git grep`, `awk`, `sed`-for-search, or any CLI search via Bash — not even for one match or a quick check.
- Open-ended search needing multiple rounds? MUST use the Task tool with the explore subagent, NOT chained `search` calls.
- MUST use built-in `grep` for any content search. NEVER shell out to `grep`, `rg`, `ripgrep`, `ag`, `ack`, `git grep`, `awk`, `sed`-for-search, or any CLI search via Bash — not even for one match or a quick check.
- Open-ended search needing multiple rounds? MUST use the Task tool with the explore subagent, NOT chained `grep` calls.
</critical>
+14 -16
View File
@@ -160,8 +160,9 @@ import {
discoverStartupLspServers,
EditTool,
EvalTool,
FindTool,
filterInitialToolsForDiscoveryAll,
GlobTool,
GrepTool,
getSearchTools,
HIDDEN_TOOLS,
isImageProviderPreference,
@@ -172,7 +173,6 @@ import {
ReadTool,
ResolveTool,
renderSearchToolBm25Description,
SearchTool,
SearchToolBm25Tool,
setExcludedSearchProviders,
setPreferredImageProvider,
@@ -183,6 +183,7 @@ import {
WriteTool,
warmupLspServers,
} from "./tools";
import { normalizeToolName, normalizeToolNames } from "./tools/builtin-names";
import { ToolContextStore } from "./tools/context";
import { getImageGenTools } from "./tools/image-gen";
import { wrapToolWithMetaNotice } from "./tools/output-meta";
@@ -608,12 +609,12 @@ export {
createTools,
EditTool,
EvalTool,
FindTool,
GlobTool,
GrepTool,
HIDDEN_TOOLS,
loadSshTool,
ReadTool,
ResolveTool,
SearchTool,
type ToolSession,
WebSearchTool,
WriteTool,
@@ -1491,9 +1492,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
hasUI: options.hasUI ?? false,
enableLsp,
get hasEditTool() {
const requestedToolNames = options.toolNames
? [...new Set(options.toolNames.map(name => name.toLowerCase()))]
: undefined;
const requestedToolNames = options.toolNames ? normalizeToolNames(options.toolNames) : undefined;
return !requestedToolNames || requestedToolNames.includes("edit");
},
skipPythonPreflight: options.skipPythonPreflight,
@@ -2284,9 +2283,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
};
const toolNamesFromRegistry = Array.from(toolRegistry.keys());
const explicitlyRequestedToolNames = options.toolNames
? [...new Set(options.toolNames.map(name => name.toLowerCase()))]
: undefined;
const explicitlyRequestedToolNames = options.toolNames ? normalizeToolNames(options.toolNames) : undefined;
// When `requireYieldTool` is set, the subagent's prompts and idle-reminders demand a
// `yield` call to terminate. The tool registry already includes `yield` (see
// `createTools`), but an explicit `toolNames` list would otherwise drop it from the
@@ -2336,13 +2333,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
discoveryDefaultServers,
)
: [];
const normalizeRenamedBuiltinToolName = normalizeToolName;
let initialSelectedMCPToolNames: string[] = [];
let defaultSelectedMCPToolNames: string[] = [];
let initialToolNames = [...initialRequestedActiveToolNames];
if (mcpDiscoveryEnabled) {
const restoredSelectedMCPToolNames = existingSession.selectedMCPToolNames.filter(name =>
toolRegistry.has(name),
);
const restoredSelectedMCPToolNames = existingSession.selectedMCPToolNames
.map(normalizeRenamedBuiltinToolName)
.filter(name => toolRegistry.has(name));
defaultSelectedMCPToolNames = [
...new Set([...discoveryDefaultServerToolNames, ...explicitlyRequestedMCPToolNames]),
];
@@ -2391,10 +2389,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
initialToolNames = filterInitialToolsForDiscoveryAll(initialToolNames, {
loadModeOf: name => toolRegistry.get(name)?.loadMode,
essentialNames: new Set(computeEssentialBuiltinNames(settings)),
explicitlyRequested: new Set(options.toolNames?.map(name => name.toLowerCase()) ?? []),
explicitlyRequested: new Set(options.toolNames ? normalizeToolNames(options.toolNames) : []),
// Back-compat: persisted activations live under selectedMCPToolNames today (built-in
// activation persistence is a follow-up). MCP names won't collide with built-in names.
restored: new Set(existingSession.selectedMCPToolNames),
restored: new Set(existingSession.selectedMCPToolNames.map(normalizeRenamedBuiltinToolName)),
forceActive,
});
}
@@ -2626,7 +2624,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
}
// Hard-isolated read-only toolset for the advisor (built unconditionally so
// it can be toggled at runtime). Fresh ReadTool/SearchTool/FindTool bound to a
// it can be toggled at runtime). Fresh ReadTool/GrepTool/GlobTool bound to a
// DISTINCT ToolSession so the advisor's investigative reads never touch the
// primary's snapshot, seen-lines, conflict, or summary caches (all keyed on
// session identity). `cwd` stays dynamic; edit/yield capabilities are off.
@@ -271,6 +271,7 @@ import {
selectDiscoverableToolNamesByServer,
} from "../tool-discovery/tool-index";
import { assertEditableFile } from "../tools/auto-generated-guard";
import { normalizeToolNames } from "../tools/builtin-names";
import type { CheckpointState } from "../tools/checkpoint";
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
@@ -5163,7 +5164,7 @@ export class AgentSession {
toolNames: string[],
options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
): Promise<void> {
toolNames = [...new Set(toolNames.map(name => name.toLowerCase()))];
toolNames = normalizeToolNames(toolNames);
const previousSelectedMCPToolNames = options?.previousSelectedMCPToolNames ?? this.getSelectedMCPToolNames();
const tools: AgentTool[] = [];
const validToolNames: string[] = [];
@@ -105,14 +105,14 @@ function primaryArg(name: string, args: Record<string, unknown> | undefined): st
if (note) return oneLine(note);
if (severity) return oneLine(severity);
}
if (name === "search") {
if (name === "grep") {
const pattern = primaryArgValue(args.pattern);
const paths = primaryArgValue(args.paths);
if (pattern && paths) return oneLine(`${pattern} @ ${paths}`);
if (pattern) return oneLine(pattern);
if (paths) return oneLine(paths);
}
if (name === "find") {
if (name === "glob") {
const paths = primaryArgValue(args.paths);
if (paths) return oneLine(paths);
}
+3 -3
View File
@@ -136,8 +136,8 @@ export {
// Fail-safe: any unknown tool makes the agent not read-only.
export const READ_ONLY_TOOL_NAMES: ReadonlySet<string> = new Set([
"read",
"search",
"find",
"grep",
"glob",
"web_search",
"ast_grep",
"yield",
@@ -1076,7 +1076,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
}
const planModeState = this.session.getPlanModeState?.();
const planModeBaseTools = ["read", "search", "find", "lsp", "web_search"];
const planModeBaseTools = ["read", "grep", "glob", "lsp", "web_search"];
const planModeTools = [
...planModeBaseTools,
...(agent.tools ?? []).filter(
+2 -2
View File
@@ -399,8 +399,8 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
autoBackgroundThresholdSeconds: Math.max(0, Math.floor(this.#autoBackgroundThresholdMs / 1000)),
hasAstGrep: this.session.settings.get("astGrep.enabled"),
hasAstEdit: this.session.settings.get("astEdit.enabled"),
hasSearch: this.session.settings.get("search.enabled"),
hasFind: this.session.settings.get("find.enabled"),
hasGrep: this.session.settings.get("grep.enabled"),
hasGlob: this.session.settings.get("glob.enabled"),
});
}
@@ -9,8 +9,8 @@ export const BUILTIN_TOOL_NAMES = [
"eval",
"ssh",
"github",
"find",
"search",
"glob",
"grep",
"lsp",
"inspect_image",
"browser",
@@ -32,3 +32,27 @@ export const BUILTIN_TOOL_NAMES = [
] as const;
export type BuiltinToolName = (typeof BUILTIN_TOOL_NAMES)[number];
const LEGACY_BUILTIN_TOOL_NAME_ALIASES: ReadonlyMap<string, BuiltinToolName> = new Map([
["search", "grep"],
["find", "glob"],
]);
/** Return the canonical tool name for current and legacy built-in tool IDs. */
export function normalizeToolName(name: string): string {
const normalized = name.toLowerCase();
return LEGACY_BUILTIN_TOOL_NAME_ALIASES.get(normalized) ?? normalized;
}
/** Normalize and deduplicate tool names while preserving first-seen order. */
export function normalizeToolNames(names: Iterable<string>): string[] {
const out: string[] = [];
const seen = new Set<string>();
for (const name of names) {
const normalized = normalizeToolName(name);
if (seen.has(normalized)) continue;
seen.add(normalized);
out.push(normalized);
}
return out;
}
@@ -10,7 +10,7 @@ import { type } from "arktype";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import { InternalUrlRouter } from "../internal-urls";
import type { Theme } from "../modes/theme/theme";
import findDescription from "../prompts/tools/find.md" with { type: "text" };
import globDescription from "../prompts/tools/glob.md" with { type: "text" };
import { type TruncationResult, truncateHead } from "../session/streaming-output";
import { Ellipsis, fileHyperlink, renderFileList, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
import type { ToolSession } from ".";
@@ -47,13 +47,13 @@ const findSchema = type({
"limit?": type("number").describe("max results"),
});
export type FindToolInput = typeof findSchema.infer;
export type GlobToolInput = typeof findSchema.infer;
const DEFAULT_LIMIT = 200;
const MAX_LIMIT = 200;
const DEFAULT_GLOB_TIMEOUT_MS = 5000;
export interface FindToolDetails {
export interface GlobToolDetails {
truncation?: TruncationResult;
resultLimitReached?: number;
meta?: OutputMeta;
@@ -76,7 +76,7 @@ export interface FindToolDetails {
* Pluggable operations for the find tool.
* Override these to delegate file search to remote systems (e.g., SSH).
*/
export interface FindOperations {
export interface GlobOperations {
/** Check if path exists */
exists: (absolutePath: string) => Promise<boolean> | boolean;
/** Optional stat for distinguishing files vs directories. */
@@ -87,28 +87,28 @@ export interface FindOperations {
glob: (pattern: string, cwd: string, options: { ignore: string[]; limit: number }) => Promise<string[]> | string[];
}
export interface FindToolOptions {
export interface GlobToolOptions {
/** Custom operations for find. Default: local filesystem + rg */
operations?: FindOperations;
operations?: GlobOperations;
}
interface FindTarget {
interface GlobTarget {
searchPath: string;
globPattern: string;
hasGlob: boolean;
}
export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
readonly name = "find";
export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
readonly name = "glob";
readonly approval = "read" as const;
readonly loadMode = "essential";
readonly label = "Find";
readonly label = "Glob";
readonly description: string;
readonly parameters = findSchema;
readonly examples: readonly ToolExample<typeof findSchema.infer>[] = [
{
caption: "Find files",
caption: "Glob files",
call: { paths: ["src/**/*.ts"] },
},
{
@@ -116,33 +116,33 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
call: { paths: ["src/**/*.ts", "test/**/*.ts"] },
},
{
caption: "Find gitignored files like .env",
caption: "Glob gitignored files like .env",
call: { paths: [".env*"], gitignore: false },
},
{
caption: "Find directories matching a name (returns both files and dirs; directories are suffixed with `/`)",
caption: "Glob directories matching a name (returns both files and dirs; directories are suffixed with `/`)",
call: { paths: ["**/tests"] },
},
];
readonly strict = true;
readonly #customOps?: FindOperations;
readonly #customOps?: GlobOperations;
constructor(
private readonly session: ToolSession,
options?: FindToolOptions,
options?: GlobToolOptions,
) {
this.#customOps = options?.operations;
this.description = prompt.render(findDescription);
this.description = prompt.render(globDescription);
}
async execute(
_toolCallId: string,
params: typeof findSchema.infer,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<FindToolDetails>,
onUpdate?: AgentToolUpdateCallback<GlobToolDetails>,
_context?: AgentToolContext,
): Promise<AgentToolResult<FindToolDetails>> {
): Promise<AgentToolResult<GlobToolDetails>> {
const { paths, limit, hidden, gitignore } = params;
return untilAborted(signal, async () => {
@@ -194,7 +194,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
const multiPattern = await resolveExplicitFindPatterns(effectivePatterns, this.session.cwd);
const isSingle = !multiPattern;
const targets: FindTarget[] = multiPattern
const targets: GlobTarget[] = multiPattern
? multiPattern.targets.map(target => ({
searchPath: resolveToCwd(target.basePath, this.session.cwd),
globPattern: target.globPattern,
@@ -242,11 +242,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
const buildResult = (
files: string[],
opts?: { notice?: string; forceTruncated?: boolean },
): AgentToolResult<FindToolDetails> => {
): AgentToolResult<GlobToolDetails> => {
const notice = opts?.notice;
const forceTruncated = opts?.forceTruncated ?? false;
if (files.length === 0) {
const details: FindToolDetails = {
const details: GlobToolDetails = {
scopePath,
fileCount: 0,
files: [],
@@ -272,7 +272,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
const rawOutput = trailingNotes.length > 0 ? `${baseOutput}\n\n${trailingNotes.join("\n")}` : baseOutput;
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
const details: FindToolDetails = {
const details: GlobToolDetails = {
scopePath,
fileCount: limited.length,
files: limited,
@@ -337,7 +337,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
const now = Date.now();
if (now - lastUpdate < updateIntervalMs) return;
lastUpdate = now;
const details: FindToolDetails = {
const details: GlobToolDetails = {
scopePath,
fileCount: onUpdateMatches.length,
files: onUpdateMatches.slice(),
@@ -362,7 +362,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
};
let timedOut = false;
const runTarget = async (target: FindTarget): Promise<Array<{ path: string; mtime: number }>> => {
const runTarget = async (target: GlobTarget): Promise<Array<{ path: string; mtime: number }>> => {
throwIfAborted(signal);
let stat: fs.Stats;
try {
@@ -435,7 +435,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
partial.sort((a, b) => b.m - a.m);
const sortedPaths = partial.map(entry => entry.p);
const seconds = timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}` : (timeoutMs / 1000).toFixed(1);
const notice = `find timed out after ${seconds}s; returning ${sortedPaths.length} partial matches — narrow the pattern instead of retrying blindly`;
const notice = `glob timed out after ${seconds}s; returning ${sortedPaths.length} partial matches — narrow the pattern instead of retrying blindly`;
return buildResult(sortedPaths, { notice, forceTruncated: true });
}
@@ -461,33 +461,33 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
// TUI Renderer
// =============================================================================
interface FindRenderArgs {
interface GlobRenderArgs {
paths?: string | string[];
limit?: number;
}
function formatFindRenderPaths(paths: FindRenderArgs["paths"]): string | undefined {
function formatGlobRenderPaths(paths: GlobRenderArgs["paths"]): string | undefined {
return Array.isArray(paths) ? paths.join(", ") : paths;
}
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
function findStatusIcon(uiTheme: Theme): string {
function globStatusIcon(uiTheme: Theme): string {
return uiTheme.fg("toolTitle", uiTheme.symbol("icon.search"));
}
export const findToolRenderer = {
export const globToolRenderer = {
inline: true,
renderCall(args: FindRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
renderCall(args: GlobRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
const meta: string[] = [];
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
const text = renderStatusLine(
{
icon: "pending",
title: "Find",
title: "Glob",
titleColor: "toolTitle",
description: formatFindRenderPaths(args.paths) || "*",
description: formatGlobRenderPaths(args.paths) || "*",
meta,
},
uiTheme,
@@ -496,10 +496,10 @@ export const findToolRenderer = {
},
renderResult(
result: { content: Array<{ type: string; text?: string }>; details?: FindToolDetails; isError?: boolean },
result: { content: Array<{ type: string; text?: string }>; details?: GlobToolDetails; isError?: boolean },
options: RenderResultOptions,
uiTheme: Theme,
args?: FindRenderArgs,
args?: GlobRenderArgs,
): Component {
const details = result.details;
@@ -524,10 +524,10 @@ export const findToolRenderer = {
const lines = textContent.split("\n").filter(l => l.trim());
const header = renderStatusLine(
{
iconOverride: findStatusIcon(uiTheme),
title: "Find",
iconOverride: globStatusIcon(uiTheme),
title: "Glob",
titleColor: "toolTitle",
description: formatFindRenderPaths(args?.paths),
description: formatGlobRenderPaths(args?.paths),
meta: [formatCount("file", lines.length)],
},
uiTheme,
@@ -565,9 +565,9 @@ export const findToolRenderer = {
const header = renderStatusLine(
{
icon: "warning",
title: "Find",
title: "Glob",
titleColor: "toolTitle",
description: formatFindRenderPaths(args?.paths),
description: formatGlobRenderPaths(args?.paths),
meta: ["0 files"],
},
uiTheme,
@@ -581,10 +581,10 @@ export const findToolRenderer = {
if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
const header = renderStatusLine(
{
...(truncated ? { icon: "warning" as const } : { iconOverride: findStatusIcon(uiTheme) }),
title: "Find",
...(truncated ? { icon: "warning" as const } : { iconOverride: globStatusIcon(uiTheme) }),
title: "Glob",
titleColor: "toolTitle",
description: formatFindRenderPaths(args?.paths),
description: formatGlobRenderPaths(args?.paths),
meta,
},
uiTheme,
@@ -14,7 +14,7 @@ import type { LocalProtocolOptions } from "../internal-urls/local-protocol";
import { InternalUrlRouter } from "../internal-urls/router";
import type { InternalResource, ResolveContext } from "../internal-urls/types";
import type { Theme } from "../modes/theme/theme";
import searchDescription from "../prompts/tools/search.md" with { type: "text" };
import grepDescription from "../prompts/tools/grep.md" with { type: "text" };
import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead, truncateLine } from "../session/streaming-output";
import {
Ellipsis,
@@ -77,7 +77,7 @@ const searchSchema = type({
.describe("files to skip before collecting results — use to paginate when the prior call hit the file limit"),
});
export type SearchToolInput = typeof searchSchema.infer;
export type GrepToolInput = typeof searchSchema.infer;
export function toPathList(input: string | string[] | undefined): string[] {
return typeof input === "string" ? [input] : (input ?? []);
}
@@ -110,14 +110,14 @@ const SEARCH_GREP_TIMEOUT_MS = 30_000;
* line-range selector peeled off the trailing `:N-M` (or `:N+K`, `:N,M`, …)
* chunk via {@link splitPathAndSel}.
*/
interface SearchPathSpec {
interface GrepPathSpec {
original: string;
clean: string;
ranges?: [LineRange, ...LineRange[]];
}
function parsePathSpecs(rawEntries: readonly string[]): SearchPathSpec[] {
const specs: SearchPathSpec[] = [];
function parsePathSpecs(rawEntries: readonly string[]): GrepPathSpec[] {
const specs: GrepPathSpec[] = [];
for (const entry of rawEntries) {
// Internal URLs (`artifact://`, `skill://`, …) use the URL-aware splitter,
// which peels selector-shaped tails only for selector-capable schemes and
@@ -572,7 +572,7 @@ async function expandVirtualInternalResource(
}
async function resolveInternalSearchInputs(opts: {
pathSpecs: readonly SearchPathSpec[];
pathSpecs: readonly GrepPathSpec[];
resolvedPaths: string[];
cwd: string;
settings: unknown;
@@ -634,7 +634,7 @@ async function resolveInternalSearchInputs(opts: {
};
}
export interface SearchToolDetails {
export interface GrepToolDetails {
truncation?: TruncationResult;
fileLimitReached?: number;
perFileLimitReached?: number;
@@ -666,19 +666,19 @@ export interface SearchToolDetails {
type SearchParams = typeof searchSchema.infer;
export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDetails> {
readonly name = "search";
export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails> {
readonly name = "grep";
readonly approval = "read" as const;
readonly label = "Search";
readonly label = "Grep";
readonly loadMode = "discoverable";
readonly summary = "Search file contents using ripgrep (fast text search)";
readonly summary = "Grep file contents using ripgrep (fast regex search)";
readonly description: string;
readonly parameters = searchSchema;
readonly strict = true;
constructor(private readonly session: ToolSession) {
const displayMode = resolveFileDisplayMode(session);
this.description = prompt.render(searchDescription, {
this.description = prompt.render(grepDescription, {
IS_HL_MODE: displayMode.hashLines,
IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
});
@@ -688,9 +688,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
_toolCallId: string,
params: SearchParams,
signal?: AbortSignal,
_onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
_onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
_toolContext?: AgentToolContext,
): Promise<AgentToolResult<SearchToolDetails>> {
): Promise<AgentToolResult<GrepToolDetails>> {
const { pattern, paths: rawPaths, case: caseSensitive, gitignore, skip } = params;
return untilAborted(signal, async () => {
@@ -773,8 +773,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
`or pass a UTF-8 text member.`,
);
}
const normalizedContextBefore = this.session.settings.get("search.contextBefore");
const normalizedContextAfter = this.session.settings.get("search.contextAfter");
const normalizedContextBefore = this.session.settings.get("grep.contextBefore");
const normalizedContextAfter = this.session.settings.get("grep.contextAfter");
const ignoreCase = !(caseSensitive ?? true);
const useGitignore = gitignore ?? true;
const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
@@ -952,7 +952,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
}
if (err instanceof Error && err.message.includes("Aborted: Timeout")) {
throw new ToolError(
`Search timed out after ${SEARCH_GREP_TIMEOUT_MS / 1000}s; narrow paths or pattern, or scope with \`find\` first`,
`Grep timed out after ${SEARCH_GREP_TIMEOUT_MS / 1000}s; narrow paths or pattern, or scope with \`glob\` first`,
);
}
throw err;
@@ -1108,7 +1108,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
.filter((s): s is string => Boolean(s))
.join("\n") || undefined;
if (selectedMatches.length === 0) {
const details: SearchToolDetails = {
const details: GrepToolDetails = {
scopePath,
searchPath,
cwd: this.session.cwd,
@@ -1245,7 +1245,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
const truncated = Boolean(
fileLimitReached || perFileLimitReached || result.limitReached || truncation.truncated || linesTruncated,
);
const details: SearchToolDetails = {
const details: GrepToolDetails = {
scopePath,
searchPath,
cwd: this.session.cwd,
@@ -1282,7 +1282,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
// TUI Renderer
// =============================================================================
interface SearchRenderArgs {
interface GrepRenderArgs {
pattern: string;
paths?: string | string[];
case?: boolean;
@@ -1298,7 +1298,7 @@ const EXPANDED_TEXT_LIMIT = PREVIEW_LIMITS.EXPANDED_LINES * 2;
const SEARCH_CODE_FRAME_LINE_RE = /^\s*\*?(\d+)│/;
function searchScopeMeta(details: SearchToolDetails | undefined): string | undefined {
function searchScopeMeta(details: GrepToolDetails | undefined): string | undefined {
if (!details?.scopePath) return undefined;
const label = details.searchPath ? fileHyperlink(details.searchPath, details.scopePath) : details.scopePath;
return `in ${label}`;
@@ -1446,13 +1446,13 @@ function renderBudgetedSearchGroups(
return lines;
}
function searchStatusIcon(uiTheme: Theme): string {
function grepStatusIcon(uiTheme: Theme): string {
return uiTheme.fg("toolTitle", uiTheme.symbol("icon.search"));
}
export const searchToolRenderer = {
export const grepToolRenderer = {
inline: true,
renderCall(args: SearchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
renderCall(args: GrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
const paths = toPathList(args.paths);
const meta: string[] = [];
if (paths.length) meta.push(`in ${paths.join(", ")}`);
@@ -1461,17 +1461,17 @@ export const searchToolRenderer = {
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
const text = renderStatusLine(
{ icon: "pending", title: "Search", titleColor: "toolTitle", description: args.pattern || "?", meta },
{ icon: "pending", title: "Grep", titleColor: "toolTitle", description: args.pattern || "?", meta },
uiTheme,
);
return new Text(text, 1, 0);
},
renderResult(
result: { content: Array<{ type: string; text?: string }>; details?: SearchToolDetails; isError?: boolean },
result: { content: Array<{ type: string; text?: string }>; details?: GrepToolDetails; isError?: boolean },
options: RenderResultOptions,
uiTheme: Theme,
args?: SearchRenderArgs,
args?: GrepRenderArgs,
): Component {
const details = result.details;
@@ -1491,8 +1491,8 @@ export const searchToolRenderer = {
const description = args?.pattern ?? undefined;
const header = renderStatusLine(
{
iconOverride: searchStatusIcon(uiTheme),
title: "Search",
iconOverride: grepStatusIcon(uiTheme),
title: "Grep",
titleColor: "toolTitle",
description,
meta: [formatCount("item", lines.length)],
@@ -1536,7 +1536,7 @@ export const searchToolRenderer = {
const scopeMeta = searchScopeMeta(details);
if (scopeMeta) meta.push(scopeMeta);
const header = renderStatusLine(
{ icon: "warning", title: "Search", titleColor: "toolTitle", description: args?.pattern, meta },
{ icon: "warning", title: "Grep", titleColor: "toolTitle", description: args?.pattern, meta },
uiTheme,
);
const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
@@ -1552,8 +1552,8 @@ export const searchToolRenderer = {
const description = args?.pattern ?? undefined;
const header = renderStatusLine(
{
...(truncated ? { icon: "warning" as const } : { iconOverride: searchStatusIcon(uiTheme) }),
title: "Search",
...(truncated ? { icon: "warning" as const } : { iconOverride: grepStatusIcon(uiTheme) }),
title: "Grep",
titleColor: "toolTitle",
description,
meta,
+13 -14
View File
@@ -39,13 +39,14 @@ import { AstEditTool } from "./ast-edit";
import { AstGrepTool } from "./ast-grep";
import { BashTool } from "./bash";
import { BrowserTool } from "./browser";
import type { BuiltinToolName } from "./builtin-names";
import { type BuiltinToolName, normalizeToolNames } from "./builtin-names";
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
import { DebugTool } from "./debug";
import { EvalTool } from "./eval";
import { resolveEvalBackends } from "./eval-backends";
import { FindTool } from "./find";
import { GithubTool } from "./gh";
import { GlobTool } from "./glob";
import { GrepTool } from "./grep";
import { InspectImageTool } from "./inspect-image";
import { IrcTool, isIrcEnabled } from "./irc";
import { JobTool } from "./job";
@@ -60,7 +61,6 @@ import { ReadTool } from "./read";
import { createReportToolIssueTool, isAutoQaEnabled } from "./report-tool-issue";
import { ResolveTool } from "./resolve";
import { reportFindingTool } from "./review";
import { SearchTool } from "./search";
import { SearchToolBm25Tool } from "./search-tool-bm25";
import { loadSshTool } from "./ssh";
import { type TodoPhase, TodoTool } from "./todo";
@@ -82,8 +82,9 @@ export * from "./checkpoint";
export * from "./debug";
export * from "./eval";
export * from "./eval-backends";
export * from "./find";
export * from "./gh";
export * from "./glob";
export * from "./grep";
export * from "./image-gen";
export * from "./inspect-image";
export * from "./irc";
@@ -98,7 +99,6 @@ export * from "./read";
export * from "./report-tool-issue";
export * from "./resolve";
export * from "./review";
export * from "./search";
export * from "./search-tool-bm25";
export * from "./ssh";
export * from "./todo";
@@ -384,7 +384,7 @@ export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = [
"bash",
"edit",
"write",
"find",
"glob",
"eval",
] as const;
@@ -395,7 +395,7 @@ export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = [
*/
export function computeEssentialBuiltinNames(settings: Settings): string[] {
const override = settings.get("tools.essentialOverride") ?? [];
const cleaned = override.map(name => name.trim()).filter(Boolean);
const cleaned = normalizeToolNames(override.map(name => name.trim()).filter(Boolean));
if (cleaned.length > 0) {
return cleaned.filter(name => name in BUILTIN_TOOLS);
}
@@ -449,8 +449,8 @@ export const BUILTIN_TOOLS: Record<BuiltinToolName, ToolFactory> = {
eval: s => new EvalTool(s),
ssh: loadSshTool,
github: GithubTool.createIf,
find: s => new FindTool(s),
search: s => new SearchTool(s),
glob: s => new GlobTool(s),
grep: s => new GrepTool(s),
lsp: LspTool.createIf,
inspect_image: s => new InspectImageTool(s),
browser: s => new BrowserTool(s),
@@ -487,8 +487,7 @@ export type ToolName = BuiltinToolName;
export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
const includeYield = session.requireYieldTool === true;
const enableLsp = session.enableLsp ?? true;
let requestedTools =
toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined;
let requestedTools = toolNames && toolNames.length > 0 ? normalizeToolNames(toolNames) : undefined;
const goalEnabled = session.settings.get("goal.enabled");
const goalModeActive = goalEnabled && session.getGoalModeState?.()?.enabled === true;
if (goalModeActive && requestedTools && !requestedTools.includes("goal")) {
@@ -552,7 +551,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
// Auto-include AST counterparts when their text-based sibling is present
if (requestedTools) {
if (
requestedTools.includes("search") &&
requestedTools.includes("grep") &&
!requestedTools.includes("ast_grep") &&
session.settings.get("astGrep.enabled")
) {
@@ -602,8 +601,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
if (name === "eval") return allowEval;
if (name === "debug") return session.settings.get("debug.enabled");
if (name === "todo") return !includeYield && session.settings.get("todo.enabled");
if (name === "find") return session.settings.get("find.enabled");
if (name === "search") return session.settings.get("search.enabled");
if (name === "glob") return session.settings.get("glob.enabled");
if (name === "grep") return session.settings.get("grep.enabled");
if (name === "github") return session.settings.get("github.enabled");
if (name === "ast_grep") return session.settings.get("astGrep.enabled");
if (name === "ast_edit") return session.settings.get("astEdit.enabled");
+4 -4
View File
@@ -18,15 +18,15 @@ import { bashToolRenderer } from "./bash";
import { browserToolRenderer } from "./browser/render";
import { debugToolRenderer } from "./debug";
import { evalToolRenderer } from "./eval-render";
import { findToolRenderer } from "./find";
import { githubToolRenderer } from "./gh-renderer";
import { globToolRenderer } from "./glob";
import { grepToolRenderer } from "./grep";
import { inspectImageToolRenderer } from "./inspect-image-renderer";
import { ircToolRenderer } from "./irc";
import { jobToolRenderer } from "./job";
import { recallToolRenderer, reflectToolRenderer, retainToolRenderer } from "./memory-render";
import { readToolRenderer } from "./read";
import { resolveToolRenderer } from "./resolve";
import { searchToolRenderer } from "./search";
import { searchToolBm25Renderer } from "./search-tool-bm25";
import { sshToolRenderer } from "./ssh";
import { todoToolRenderer } from "./todo";
@@ -75,8 +75,8 @@ export const toolRenderers: Record<string, ToolRenderer> = {
eval: evalToolRenderer as ToolRenderer,
edit: editToolRenderer as ToolRenderer,
apply_patch: editToolRenderer as ToolRenderer,
find: findToolRenderer as ToolRenderer,
search: searchToolRenderer as ToolRenderer,
glob: globToolRenderer as ToolRenderer,
grep: grepToolRenderer as ToolRenderer,
lsp: lspToolRenderer as ToolRenderer,
inspect_image: inspectImageToolRenderer as ToolRenderer,
irc: ircToolRenderer as ToolRenderer,
@@ -785,3 +785,13 @@ it("read tool: requestPermission is never called for non-gated tools", async ()
expect(permissionSpy).toHaveBeenCalledTimes(0);
expect(readTool.executeCalls).toBe(1);
});
it("setActiveToolsByName normalizes legacy tool names", async () => {
const grepTool = makeFakeTool("grep");
const globTool = makeFakeTool("glob");
session = await createSession([grepTool, globTool]);
await session.setActiveToolsByName(["Search", "find", "grep"]);
expect(session.getActiveToolNames()).toEqual(["grep", "glob"]);
});
@@ -448,7 +448,7 @@ describe("AgentSession auto-compaction progress guard", () => {
const bigCallId = "call-big-useless";
sessionManager.appendMessage({
role: "assistant",
content: [{ type: "toolCall", id: bigCallId, name: "search", arguments: { pattern: "TODO" } }],
content: [{ type: "toolCall", id: bigCallId, name: "grep", arguments: { pattern: "TODO" } }],
api: "anthropic-messages",
provider: "anthropic",
model: "claude-sonnet-4-5",
@@ -466,7 +466,7 @@ describe("AgentSession auto-compaction progress guard", () => {
sessionManager.appendMessage({
role: "toolResult",
toolCallId: bigCallId,
toolName: "search",
toolName: "grep",
content: [{ type: "text", text: "match line\n".repeat(20000) }], // ~40k+ tokens
isError: false,
useless: true,
@@ -503,7 +503,7 @@ describe("AgentSession auto-compaction queue resume", () => {
const bigCallId = "call-big-useless";
sessionManager.appendMessage({
role: "assistant",
content: [{ type: "toolCall", id: bigCallId, name: "search", arguments: { pattern: "TODO" } }],
content: [{ type: "toolCall", id: bigCallId, name: "grep", arguments: { pattern: "TODO" } }],
api: "anthropic-messages",
provider: "anthropic",
model: "claude-sonnet-4-5",
@@ -521,7 +521,7 @@ describe("AgentSession auto-compaction queue resume", () => {
sessionManager.appendMessage({
role: "toolResult",
toolCallId: bigCallId,
toolName: "search",
toolName: "grep",
content: [{ type: "text", text: "match line\n".repeat(20000) }], // ~40k+ tokens
isError: false,
useless: true,
@@ -17,8 +17,8 @@ function makeSession(bridge: ClientBridge): ToolSession {
if (key === "bashInterceptor.enabled") return false;
if (key === "astGrep.enabled") return false;
if (key === "astEdit.enabled") return false;
if (key === "search.enabled") return false;
if (key === "find.enabled") return false;
if (key === "grep.enabled") return false;
if (key === "glob.enabled") return false;
return undefined;
},
getBashInterceptorRules() {
@@ -17,8 +17,8 @@ function makeSession(): ToolSession {
if (key === "bash.stripTrailingHeadTail") return false;
if (key === "astGrep.enabled") return false;
if (key === "astEdit.enabled") return false;
if (key === "search.enabled") return false;
if (key === "find.enabled") return false;
if (key === "grep.enabled") return false;
if (key === "glob.enabled") return false;
return undefined;
},
getBashInterceptorRules() {
@@ -120,7 +120,7 @@ async function main() {
const hasCheckpoint = toolSequence.includes("checkpoint");
const hasRewind = toolSequence.includes("rewind");
const hasFind = toolSequence.includes("find");
const hasGlob = toolSequence.includes("glob");
const hasRead = toolSequence.includes("read");
const activeHasRewindReport = messages.some(
@@ -132,7 +132,7 @@ async function main() {
.map(message => message.toolName);
const activeHasRewindResult = activeToolResults.includes("rewind");
const activeHasFindResult = activeToolResults.includes("find");
const activeHasGlobResult = activeToolResults.includes("glob");
const activeHasReadResult = activeToolResults.includes("read");
const rewindReportEntries = customMessages.filter(entry => entry.customType === "rewind-report");
@@ -163,7 +163,7 @@ async function main() {
if (!hasCheckpoint || !hasRewind) {
throw new Error("Agent did not execute both checkpoint and rewind.");
}
if (!hasFind || !hasRead) {
if (!hasGlob || !hasRead) {
throw new Error("Agent did not perform requested exploratory find/read inside checkpoint.");
}
if (!activeHasRewindReport) {
@@ -172,7 +172,7 @@ async function main() {
if (activeHasRewindResult) {
throw new Error("Active context still contains rewind tool result; rewind did not prune it.");
}
if (activeHasFindResult || activeHasReadResult) {
if (activeHasGlobResult || activeHasReadResult) {
throw new Error("Active context still contains exploratory find/read tool results after rewind.");
}
if (rewindReportEntries.length === 0) {
@@ -4,10 +4,10 @@ import { renderKernelDisplay } from "@oh-my-pi/pi-coding-agent/eval/py/display";
describe("renderKernelDisplay (raw bundle shape)", () => {
it("renders status events without text output", async () => {
const { text, outputs } = await renderKernelDisplay({
"application/x-omp-status": { op: "find", count: 12, pattern: "foo" },
"application/x-omp-status": { op: "glob", count: 12, pattern: "foo" },
});
expect(text).toBe("");
expect(outputs).toEqual([{ type: "status", event: { op: "find", count: 12, pattern: "foo" } }]);
expect(outputs).toEqual([{ type: "status", event: { op: "glob", count: 12, pattern: "foo" } }]);
});
it("prefers text/markdown over text/plain", async () => {
@@ -4,7 +4,7 @@ import * as os from "node:os";
import * as path from "node:path";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { CursorExecHandlers } from "@oh-my-pi/pi-coding-agent/cursor";
import { SearchTool, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { GrepTool, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
function createTestSession(cwd: string, overrides: Partial<ToolSession> = {}): ToolSession {
return {
@@ -19,16 +19,16 @@ function createTestSession(cwd: string, overrides: Partial<ToolSession> = {}): T
describe("CursorExecHandlers.grep bridge", () => {
let cwd: string;
let searchTool: SearchTool;
let searchTool: GrepTool;
let handlers: CursorExecHandlers;
beforeEach(async () => {
cwd = await fs.mkdtemp(path.join(os.tmpdir(), "cursor-exec-test-"));
await Bun.write(path.join(cwd, "sample.txt"), "Hello World\nhello world\n");
searchTool = new SearchTool(createTestSession(cwd));
searchTool = new GrepTool(createTestSession(cwd));
handlers = new CursorExecHandlers({
cwd,
tools: new Map([["search", searchTool as any]]),
tools: new Map([["grep", searchTool as any]]),
});
});
@@ -64,7 +64,17 @@ describe("parseAgentFields", () => {
tools: ["Read", "Search"],
});
expect(fields?.tools).toEqual(["read", "search", "yield"]);
expect(fields?.tools).toEqual(["read", "grep", "yield"]);
});
test("maps legacy search and find tool names", () => {
const fields = parseAgentFields({
name: "reviewer",
description: "desc",
tools: ["Find", "Glob", "Search", "Grep"],
});
expect(fields?.tools).toEqual(["glob", "grep", "yield"]);
});
test("parses autoloadSkills from array frontmatter", () => {
@@ -7,7 +7,7 @@ import { type ExecuteHashlineSingleOptions, executeHashlineSingle } from "@oh-my
import { canonicalSnapshotKey, getFileSnapshotStore } from "@oh-my-pi/pi-coding-agent/edit/file-snapshot-store";
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { ReadTool } from "@oh-my-pi/pi-coding-agent/tools/read";
import { SearchTool } from "@oh-my-pi/pi-coding-agent/tools/search";
import { GrepTool } from "../../src/tools/grep";
function createSession(cwd: string): ToolSession {
return {
@@ -193,7 +193,7 @@ describe("search → edit seen-line guard", () => {
getArtifactsDir: () => path.join(cwd, "artifacts"),
allocateOutputArtifact: async () => ({ id: "artifact-1", path: path.join(cwd, "artifact-1.log") }),
// Zero context so the seen set is exactly the matched lines.
settings: Settings.isolated({ "search.contextBefore": 0, "search.contextAfter": 0 }),
settings: Settings.isolated({ "grep.contextBefore": 0, "grep.contextAfter": 0 }),
enableLsp: false,
} as ToolSession;
}
@@ -204,7 +204,7 @@ describe("search → edit seen-line guard", () => {
await Bun.write(file, `${lines.join("\n")}\n`);
const session = searchSession(tmpDir);
const search = await new SearchTool(session).execute("s1", { pattern: "NEEDLE", paths: [file] });
const search = await new GrepTool(session).execute("s1", { pattern: "NEEDLE", paths: [file] });
const tag = tagFromOutput(resultText(search));
const seen = getFileSnapshotStore(session).byHash(canonicalSnapshotKey(file), tag)?.seenLines;
@@ -222,7 +222,7 @@ describe("search → edit seen-line guard", () => {
await Bun.write(file, `${lines.join("\n")}\n`);
const session = searchSession(tmpDir);
const search = await new SearchTool(session).execute("s1", { pattern: "NEEDLE", paths: [file] });
const search = await new GrepTool(session).execute("s1", { pattern: "NEEDLE", paths: [file] });
const tag = tagFromOutput(resultText(search));
await expect(executeHashlineSingle(execOptions(`[code.txt#${tag}]\nSWAP 8.=8:\n+X`, session))).rejects.toThrow(
@@ -48,6 +48,14 @@ describe("OPTIONAL_VALUE_FLAGS table is honored by args.ts parseArgs", () => {
}
});
describe("--tools legacy aliases", () => {
it("maps search and find to grep and glob", () => {
const result = parseArgs(["--tools", "search,find,grep"]);
expect(result.tools).toEqual(["grep", "glob"]);
});
});
describe("OPTIONAL_FLAGS per-flag quirks", () => {
it("treats empty string as bare resume for --resume", () => {
const result = parseArgs(["--resume", ""]);
@@ -38,7 +38,7 @@ function toolStartWithIntent(toolCallId: string, intent: string): AgentSessionEv
return {
type: "tool_execution_start",
toolCallId,
toolName: "search",
toolName: "grep",
args: {},
intent,
} as unknown as AgentSessionEvent;
@@ -140,7 +140,7 @@ describe("createAgentSession defaultInactive tool activation", () => {
it("activates the yield tool when requireYieldTool is set and toolNames is explicit", async () => {
// Regression for #1408: plan-mode subagents pass an explicit `toolNames` list
// (e.g. `["read", "search", "find", "lsp", "web_search"]`). Without this
// (e.g. `["read", "grep", "glob", "lsp", "web_search"]`). Without this
// invariant, `yield` ended up registered but not active, and the model
// could not satisfy the idle-reminder contract that demands a `yield` call.
const tempDir = makeTempDir();
@@ -148,7 +148,7 @@ describe("createAgentSession defaultInactive tool activation", () => {
const { session } = await createAgentSession({
...baseOptions(tempDir),
requireYieldTool: true,
toolNames: ["read", "search", "find", "web_search"],
toolNames: ["read", "grep", "glob", "web_search"],
});
try {
@@ -158,6 +158,27 @@ describe("createAgentSession defaultInactive tool activation", () => {
}
});
it("normalizes legacy builtin toolNames before selecting the active SDK tools", async () => {
const tempDir = makeTempDir();
const { session } = await createAgentSession({
...baseOptions(tempDir),
toolNames: ["read", "search", "find"],
});
try {
const activeToolNames = session.getActiveToolNames();
expect(activeToolNames).toContain("read");
expect(activeToolNames).toContain("grep");
expect(activeToolNames).toContain("glob");
expect(activeToolNames).not.toContain("search");
expect(activeToolNames).not.toContain("find");
} finally {
await session.dispose();
}
});
it("keeps the hidden resolve tool registered for plan mode even when no deferrable tool is requested", async () => {
// Regression for #1428: plan mode submits its finalized plan via
// `resolve { action: "apply" }` dispatched through a standing handler
@@ -170,7 +191,7 @@ describe("createAgentSession defaultInactive tool activation", () => {
const { session } = await createAgentSession({
...baseOptions(tempDir),
toolNames: ["read", "search", "find", "web_search"],
toolNames: ["read", "grep", "glob", "web_search"],
});
try {
@@ -189,7 +210,7 @@ describe("createAgentSession defaultInactive tool activation", () => {
const { session } = await createAgentSession({
...baseOptions(tempDir),
settings,
toolNames: ["read", "search", "find", "web_search"],
toolNames: ["read", "grep", "glob", "web_search"],
});
try {
@@ -82,8 +82,8 @@ describe("formatSessionDumpText tool parameters", () => {
messages: [],
tools: [
{
name: "find",
description: "Finds files.",
name: "glob",
description: "Globs files.",
parameters: findSchema,
examples: [{ call: { paths: ["src/**/*.ts"] } }],
},
@@ -92,7 +92,7 @@ describe("formatSessionDumpText tool parameters", () => {
expect(out).toContain("## Available Tools");
expect(out).toContain("<examples>");
expect(out).toContain('<invoke name="find">');
expect(out).toContain('<invoke name="glob">');
});
it("omits the Available Tools section if inlineToolDescriptors is true", () => {
@@ -106,7 +106,7 @@ describe("formatSessionHistoryMarkdown", () => {
{
role: "toolResult",
toolCallId: "tc-orphan",
toolName: "search",
toolName: "grep",
content: [{ type: "text", text: "one match" }],
isError: false,
timestamp: 1,
@@ -122,8 +122,8 @@ describe("formatSessionHistoryMarkdown", () => {
content: [
{
type: "toolCall",
id: "tc-find",
name: "find",
id: "tc-glob",
name: "glob",
arguments: { paths: ["packages/coding-agent/src/**/*.ts"] },
},
],
@@ -131,8 +131,8 @@ describe("formatSessionHistoryMarkdown", () => {
},
{
role: "toolResult",
toolCallId: "tc-find",
toolName: "find",
toolCallId: "tc-glob",
toolName: "glob",
content: [{ type: "text", text: "session-history-format.ts" }],
isError: false,
timestamp: 2,
@@ -150,8 +150,8 @@ describe("formatSessionHistoryMarkdown", () => {
content: [
{
type: "toolCall",
id: "tc-search",
name: "search",
id: "tc-grep",
name: "grep",
arguments: { pattern: "PRIMARY_ARG_KEYS", paths: ["packages/coding-agent/src/session"] },
},
],
@@ -159,8 +159,8 @@ describe("formatSessionHistoryMarkdown", () => {
},
{
role: "toolResult",
toolCallId: "tc-search",
toolName: "search",
toolCallId: "tc-grep",
toolName: "grep",
content: [{ type: "text", text: "timed out" }],
isError: true,
timestamp: 2,
@@ -511,6 +511,69 @@ describe("Settings", () => {
expect(fs.readFileSync(path.join(agentDir, "last-changelog-version"), "utf8")).toBe("0.41.0");
});
it("migrates legacy find and search settings to glob and grep", async () => {
await writeSettings({
find: { enabled: false },
search: {
enabled: false,
contextBefore: 2,
contextAfter: 5,
},
});
const settings = await Settings.init({ cwd: projectDir, agentDir });
expect(settings.get("glob.enabled")).toBe(false);
expect(settings.get("grep.enabled")).toBe(false);
expect(settings.get("grep.contextBefore")).toBe(2);
expect(settings.get("grep.contextAfter")).toBe(5);
});
it("migrates flat legacy find and search settings keys to nested glob and grep", async () => {
await writeSettings({
"find.enabled": false,
"search.enabled": false,
"search.contextBefore": 2,
"search.contextAfter": 5,
});
const settings = await Settings.init({ cwd: projectDir, agentDir });
expect(settings.get("glob.enabled")).toBe(false);
expect(settings.get("grep.enabled")).toBe(false);
expect(settings.get("grep.contextBefore")).toBe(2);
expect(settings.get("grep.contextAfter")).toBe(5);
});
it("does not clobber existing glob/grep settings when migrating legacy find/search ones", async () => {
await writeSettings({
find: { enabled: false },
glob: { enabled: true },
search: { enabled: false },
grep: { enabled: true },
"find.enabled": false,
"glob.enabled": true,
"search.enabled": false,
"grep.enabled": true,
});
const settings = await Settings.init({ cwd: projectDir, agentDir });
expect(settings.get("glob.enabled")).toBe(true);
expect(settings.get("grep.enabled")).toBe(true);
});
it("migrates legacy tool names in persisted essential overrides", async () => {
await writeSettings({
tools: { essentialOverride: ["read", "find", "search", "grep"] },
"tools.essentialOverride": ["find", "search", "read"],
});
const settings = await Settings.init({ cwd: projectDir, agentDir });
expect(settings.get("tools.essentialOverride")).toEqual(["read", "glob", "grep"]);
});
it("migrates from settings.json containing comments", async () => {
const jsonPath = path.join(agentDir, "settings.json");
await fs.promises.writeFile(
+2 -2
View File
@@ -363,7 +363,7 @@ describe("AgentSession shake", () => {
const bigCallId = "call-big-useless-for-shake";
sessionManager.appendMessage({
role: "assistant",
content: [{ type: "toolCall", id: bigCallId, name: "search", arguments: { pattern: "TODO" } }],
content: [{ type: "toolCall", id: bigCallId, name: "grep", arguments: { pattern: "TODO" } }],
...apiInfo,
stopReason: "toolUse",
usage,
@@ -372,7 +372,7 @@ describe("AgentSession shake", () => {
sessionManager.appendMessage({
role: "toolResult",
toolCallId: bigCallId,
toolName: "search",
toolName: "grep",
content: [{ type: "text", text: "match line\n".repeat(20000) }],
isError: false,
useless: true,
@@ -17,8 +17,8 @@ const allToolsSettings = Settings.isolated({
"astGrep.enabled": true,
"astEdit.enabled": true,
"debug.enabled": true,
"find.enabled": true,
"search.enabled": true,
"glob.enabled": true,
"grep.enabled": true,
"github.enabled": true,
"lsp.enabled": true,
"inspect_image.enabled": true,
@@ -98,8 +98,13 @@ describe("computeEssentialBuiltinNames", () => {
});
it("respects tools.essentialOverride when provided", () => {
const settings = Settings.isolated({ "tools.essentialOverride": ["read", "find"] });
expect(computeEssentialBuiltinNames(settings).sort()).toEqual(["find", "read"]);
const settings = Settings.isolated({ "tools.essentialOverride": ["read", "glob"] });
expect(computeEssentialBuiltinNames(settings).sort()).toEqual(["glob", "read"]);
});
it("maps legacy essential override tool names", () => {
const settings = Settings.isolated({ "tools.essentialOverride": ["read", "find", "search", "glob"] });
expect(computeEssentialBuiltinNames(settings).sort()).toEqual(["glob", "grep", "read"]);
});
it("filters override entries that are not known built-in tools", () => {
@@ -147,18 +152,18 @@ describe("filterInitialToolsForDiscoveryAll", () => {
};
const base = {
loadModeOf: (name: string): BuiltinToolLoadMode | undefined => loadModes[name],
essentialNames: new Set(["read", "bash", "edit", "write", "find"]),
essentialNames: new Set(["read", "bash", "edit", "write", "glob"]),
explicitlyRequested: new Set<string>(),
restored: new Set<string>(),
forceActive: new Set<string>(),
};
it("hides non-essential discoverable built-ins", () => {
expect(filterInitialToolsForDiscoveryAll(["read", "edit", "todo", "search"], base)).toEqual(["read", "edit"]);
expect(filterInitialToolsForDiscoveryAll(["read", "edit", "todo", "grep"], base)).toEqual(["read", "edit"]);
});
it("keeps discoverable tools required by a forced tool_choice (eager todo)", () => {
const result = filterInitialToolsForDiscoveryAll(["read", "todo", "search"], {
const result = filterInitialToolsForDiscoveryAll(["read", "todo", "grep"], {
...base,
forceActive: new Set(["todo"]),
});
@@ -166,15 +171,15 @@ describe("filterInitialToolsForDiscoveryAll", () => {
});
it("keeps explicitly requested and restored discoverable tools", () => {
const result = filterInitialToolsForDiscoveryAll(["todo", "search"], {
const result = filterInitialToolsForDiscoveryAll(["todo", "grep"], {
...base,
explicitlyRequested: new Set(["search"]),
explicitlyRequested: new Set(["grep"]),
restored: new Set(["todo"]),
});
expect([...result].sort()).toEqual(["search", "todo"]);
expect([...result].sort()).toEqual(["grep", "todo"]);
});
it("never hides tools without a built-in loadMode (MCP/custom/extension)", () => {
expect(filterInitialToolsForDiscoveryAll(["mcp__server__tool", "search"], base)).toEqual(["mcp__server__tool"]);
expect(filterInitialToolsForDiscoveryAll(["mcp__server__tool", "grep"], base)).toEqual(["mcp__server__tool"]);
});
});
@@ -8,8 +8,8 @@ import {
describe("generic index: DiscoverableTool round-trip", () => {
const tools: DiscoverableTool[] = [
{
name: "find",
label: "find",
name: "glob",
label: "glob",
summary: "Find files matching a glob pattern",
source: "builtin",
schemaKeys: ["pattern", "path"],
@@ -30,7 +30,7 @@ describe("generic index: DiscoverableTool round-trip", () => {
expect(index.documents).toHaveLength(2);
const findResults = searchDiscoverableTools(index, "find files", 3);
expect(findResults.some((r: any) => r.tool.name === "find")).toBe(true);
expect(findResults.some((r: any) => r.tool.name === "glob")).toBe(true);
const ghResults = searchDiscoverableTools(index, "github search", 3);
expect(ghResults.some((r: any) => r.tool.name === "mcp__gh_search")).toBe(true);
@@ -261,8 +261,8 @@ describe("BM25 search", () => {
schemaKeys: ["channel", "text"],
},
{
name: "find",
label: "find",
name: "glob",
label: "glob",
summary: "Find files and directories matching a glob pattern",
source: "builtin",
schemaKeys: ["pattern", "path"],
@@ -284,7 +284,7 @@ describe("BM25 search", () => {
it("finds built-in tools too", () => {
const results = searchDiscoverableTools(index, "find files", 5);
expect(results.some(r => r.tool.name === "find")).toBe(true);
expect(results.some(r => r.tool.name === "glob")).toBe(true);
});
it("respects the limit", () => {
+10 -10
View File
@@ -11,15 +11,15 @@ import { EditTool } from "@oh-my-pi/pi-coding-agent/edit";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { BashTool } from "@oh-my-pi/pi-coding-agent/tools/bash";
import { FindTool } from "@oh-my-pi/pi-coding-agent/tools/find";
import { JobTool } from "@oh-my-pi/pi-coding-agent/tools/job";
import { wrapToolWithMetaNotice } from "@oh-my-pi/pi-coding-agent/tools/output-meta";
import { ReadTool } from "@oh-my-pi/pi-coding-agent/tools/read";
import { DEFAULT_FILE_LIMIT, MULTI_FILE_PER_FILE_MATCHES, SearchTool } from "@oh-my-pi/pi-coding-agent/tools/search";
import * as toolTimeouts from "@oh-my-pi/pi-coding-agent/tools/tool-timeouts";
import { WriteTool } from "@oh-my-pi/pi-coding-agent/tools/write";
import { unzip } from "@oh-my-pi/pi-coding-agent/utils/zip";
import { $which, removeSyncWithRetries, Snowflake } from "@oh-my-pi/pi-utils";
import { GlobTool } from "../src/tools/glob";
import { DEFAULT_FILE_LIMIT, GrepTool, MULTI_FILE_PER_FILE_MATCHES } from "../src/tools/grep";
// Helper to extract text from content blocks
function getTextOutput(result: any): string {
@@ -268,8 +268,8 @@ describe("Coding Agent Tools", () => {
let writeTool: WriteTool;
let editTool: EditTool;
let bashTool: BashTool;
let searchTool: SearchTool;
let findTool: FindTool;
let searchTool: GrepTool;
let findTool: GlobTool;
let originalEditVariant: string | undefined;
beforeAll(async () => {
@@ -294,8 +294,8 @@ describe("Coding Agent Tools", () => {
writeTool = wrapToolWithMetaNotice(new WriteTool(session));
editTool = wrapToolWithMetaNotice(new EditTool(session));
bashTool = wrapToolWithMetaNotice(new BashTool(session));
searchTool = wrapToolWithMetaNotice(new SearchTool(session));
findTool = wrapToolWithMetaNotice(new FindTool(session));
searchTool = wrapToolWithMetaNotice(new GrepTool(session));
findTool = wrapToolWithMetaNotice(new GlobTool(session));
});
afterEach(() => {
@@ -1691,9 +1691,9 @@ function b() {
const content = ["before", "match one", "after", "middle", "match two", "after two"].join("\n");
fs.writeFileSync(testFile, content);
const contextSettings = Settings.isolated({ "search.contextBefore": 1, "search.contextAfter": 1 });
const contextSettings = Settings.isolated({ "grep.contextBefore": 1, "grep.contextAfter": 1 });
const contextSearchTool = wrapToolWithMetaNotice(
new SearchTool(createTestToolSession(testDir, contextSettings)),
new GrepTool(createTestToolSession(testDir, contextSettings)),
);
const result = await contextSearchTool.execute("test-call-12", {
pattern: "match",
@@ -1713,9 +1713,9 @@ function b() {
const lines = Array.from({ length: 10 }, (_, idx) => (idx === 0 || idx === 5 ? "match" : `filler ${idx}`));
fs.writeFileSync(testFile, lines.join("\n"));
const noContextSettings = Settings.isolated({ "search.contextBefore": 0, "search.contextAfter": 0 });
const noContextSettings = Settings.isolated({ "grep.contextBefore": 0, "grep.contextAfter": 0 });
const noContextSearchTool = wrapToolWithMetaNotice(
new SearchTool(createTestToolSession(testDir, noContextSettings)),
new GrepTool(createTestToolSession(testDir, noContextSettings)),
);
const result = await noContextSearchTool.execute("test-call-12-gap", {
pattern: "match",
@@ -29,8 +29,8 @@ function createBashTool(): BashTool {
case "bash.autoBackground.enabled":
case "astGrep.enabled":
case "astEdit.enabled":
case "search.enabled":
case "find.enabled":
case "grep.enabled":
case "glob.enabled":
return false;
case "bash.autoBackground.thresholdMs":
return 60_000;
@@ -1,10 +1,10 @@
import { describe, expect, it } from "bun:test";
import { getThemeByName } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { findToolRenderer } from "@oh-my-pi/pi-coding-agent/tools/find";
import { sanitizeText } from "@oh-my-pi/pi-utils";
import { globToolRenderer } from "../../src/tools/glob";
describe("findToolRenderer", () => {
it("indents inline find output and avoids accent-colored success headers", async () => {
describe("globToolRenderer", () => {
it("indents inline glob output and avoids accent-colored success headers", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
@@ -16,7 +16,7 @@ describe("findToolRenderer", () => {
},
};
const renderedLines = findToolRenderer
const renderedLines = globToolRenderer
.renderResult(result as never, { expanded: true, isPartial: false }, uiTheme, { paths: "src/**/*.ts" })
.render(240);
const plainLines = sanitizeText(renderedLines.join("\n")).split("\n");
@@ -4,7 +4,6 @@ import * as os from "node:os";
import * as path from "node:path";
import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
import { getThemeByName, initTheme, type Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { findToolRenderer } from "@oh-my-pi/pi-coding-agent/tools/find";
import {
expandDelimitedPathEntries,
parseFindPattern,
@@ -12,6 +11,7 @@ import {
splitDelimitedPathEntry,
} from "@oh-my-pi/pi-coding-agent/tools/path-utils";
import type { Component } from "@oh-my-pi/pi-tui";
import { globToolRenderer } from "../../src/tools/glob";
let uiTheme: Theme;
@@ -133,24 +133,24 @@ describe("delimited path expansion", () => {
});
});
describe("findToolRenderer", () => {
describe("globToolRenderer", () => {
it("accepts a single string paths value before validation", async () => {
const args = { paths: "src/**/*.ts" };
const renderings = [
findToolRenderer.renderCall(args, renderOptions, uiTheme),
findToolRenderer.renderResult(
globToolRenderer.renderCall(args, renderOptions, uiTheme),
globToolRenderer.renderResult(
{ content: [{ type: "text", text: "src/index.ts\n" }] },
renderOptions,
uiTheme,
args,
),
findToolRenderer.renderResult(
globToolRenderer.renderResult(
{ content: [{ type: "text", text: "" }], details: { fileCount: 0, files: [] } },
renderOptions,
uiTheme,
args,
),
findToolRenderer.renderResult(
globToolRenderer.renderResult(
{ content: [{ type: "text", text: "src/index.ts" }], details: { fileCount: 1, files: ["src/index.ts"] } },
renderOptions,
uiTheme,
@@ -13,9 +13,9 @@ import {
} from "@oh-my-pi/pi-coding-agent/internal-urls";
import { AgentRegistry } from "@oh-my-pi/pi-coding-agent/registry/agent-registry";
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { FindTool } from "@oh-my-pi/pi-coding-agent/tools/find";
import { ReadTool } from "@oh-my-pi/pi-coding-agent/tools/read";
import { SearchTool } from "@oh-my-pi/pi-coding-agent/tools/search";
import { GlobTool } from "../../src/tools/glob";
import { GrepTool } from "../../src/tools/grep";
function getResultText(result: { content: Array<{ type: string; text?: string }> }): string {
return result.content
@@ -62,7 +62,7 @@ function registerVirtualDocs(docs: ReadonlyMap<string, string>): void {
InternalUrlRouter.instance().register(handler);
}
describe("SearchTool internal URL resolution", () => {
describe("GrepTool internal URL resolution", () => {
let tmpDir: string;
let artifactsDir: string;
@@ -100,7 +100,7 @@ describe("SearchTool internal URL resolution", () => {
hasUI: false,
getSessionFile: () => null,
getSessionSpawns: () => "*",
settings: Settings.isolated({ "search.contextBefore": 0, "search.contextAfter": 0 }),
settings: Settings.isolated({ "grep.contextBefore": 0, "grep.contextAfter": 0 }),
...overrides,
};
}
@@ -160,8 +160,8 @@ describe("SearchTool internal URL resolution", () => {
it("walks skill:// directory subpaths for search and find", async () => {
await registerSkillDirectory();
const session = createSession({ hasEditTool: true });
const searchTool = new SearchTool(session);
const findTool = new FindTool(session);
const searchTool = new GrepTool(session);
const findTool = new GlobTool(session);
const searchResult = await searchTool.execute("test-search", {
pattern: "deep needle",
@@ -182,7 +182,7 @@ describe("SearchTool internal URL resolution", () => {
await Bun.write(path.join(artifactsDir, "5.bash.log"), content);
const session = createSession();
const tool = new SearchTool(session);
const tool = new GrepTool(session);
const result = await tool.execute("test-call", {
pattern: "needle",
@@ -198,7 +198,7 @@ describe("SearchTool internal URL resolution", () => {
await Bun.write(path.join(artifactsDir, "3.python.log"), content);
const session = createSession();
const tool = new SearchTool(session);
const tool = new GrepTool(session);
const result = await tool.execute("test-call", {
pattern: "ERROR.*",
@@ -216,7 +216,7 @@ describe("SearchTool internal URL resolution", () => {
registerVirtualDocs(new Map([["doc.md", "alpha line\nneedle in virtual content\ngamma line\n"]]));
const session = createSession();
const tool = new SearchTool(session);
const tool = new GrepTool(session);
const result = await tool.execute("test-call", {
pattern: "needle",
@@ -232,7 +232,7 @@ describe("SearchTool internal URL resolution", () => {
registerVirtualDocs(new Map([["doc.md", "needle outside range\nmiddle line\nneedle inside range\n"]]));
const session = createSession();
const tool = new SearchTool(session);
const tool = new GrepTool(session);
const result = await tool.execute("test-call", {
pattern: "needle",
@@ -246,21 +246,21 @@ describe("SearchTool internal URL resolution", () => {
it("expands omp:// root to grep embedded documentation files", async () => {
const session = createSession();
const tool = new SearchTool(session);
const tool = new GrepTool(session);
const result = await tool.execute("test-call", {
pattern: "Search file contents with a regex across files",
pattern: "Greps files using regex.",
paths: ["omp://"],
});
const text = getResultText(result);
expect(text).toContain("# omp://tools/search.md");
expect(text).toContain("Search file contents with a regex across files");
expect(text).toContain("# omp://tools/grep.md");
expect(text).toContain("Greps files using regex.");
});
it("expands omp://docs to grep embedded documentation files", async () => {
const session = createSession();
const tool = new SearchTool(session);
const tool = new GrepTool(session);
const result = await tool.execute("test-call", {
pattern: "Read files, directories, archives",
@@ -274,7 +274,7 @@ describe("SearchTool internal URL resolution", () => {
it("throws when internal URL has no sourcePath", async () => {
const session = createSession();
const tool = new SearchTool(session);
const tool = new GrepTool(session);
expect(tool.execute("test-call", { pattern: "foo", paths: ["artifact://999"] })).rejects.toThrow(
"Artifact 999 not found",
@@ -285,7 +285,7 @@ describe("SearchTool internal URL resolution", () => {
await Bun.write(path.join(tmpDir, "test.txt"), "hello world\n");
const session = createSession();
const tool = new SearchTool(session);
const tool = new GrepTool(session);
const result = await tool.execute("test-call", {
pattern: "hello",
@@ -300,7 +300,7 @@ describe("SearchTool internal URL resolution", () => {
await Bun.write(path.join(tmpDir, "data.log"), "some data here\n");
const session = createSession();
const tool = new SearchTool(session);
const tool = new GrepTool(session);
const result = await tool.execute("test-call", {
pattern: "data",
@@ -316,7 +316,7 @@ describe("SearchTool internal URL resolution", () => {
await Bun.write(path.join(artifactsDir, "9.bash.log"), content);
const session = createSession({ hasEditTool: true });
const tool = new SearchTool(session);
const tool = new GrepTool(session);
const result = await tool.execute("test-call", {
pattern: "needle",
@@ -338,7 +338,7 @@ describe("SearchTool internal URL resolution", () => {
LocalProtocolHandler.setOverride({ getArtifactsDir: () => artifactsDir, getSessionId: () => "session" });
const session = createSession();
const tool = new FindTool(session);
const tool = new GlobTool(session);
const result = await tool.execute("test-call", {
paths: ["local://PLAN.md"],
@@ -357,7 +357,7 @@ describe("SearchTool internal URL resolution", () => {
const session = createSession({ hasEditTool: true });
const readResult = await new ReadTool(session).execute("test-read", { path: "local://notes" });
const findResult = await new FindTool(session).execute("test-find", {
const findResult = await new GlobTool(session).execute("test-find", {
paths: ["local://notes"],
});
const dirResource = await InternalUrlRouter.instance().resolve("local://notes");
@@ -378,7 +378,7 @@ describe("SearchTool internal URL resolution", () => {
LocalProtocolHandler.setOverride({ getArtifactsDir: () => artifactsDir, getSessionId: () => "session" });
const session = createSession({ hasEditTool: true });
const tool = new SearchTool(session);
const tool = new GrepTool(session);
const result = await tool.execute("test-call", {
pattern: "needle",
@@ -398,7 +398,7 @@ describe("SearchTool internal URL resolution", () => {
await Bun.write(path.join(tmpDir, "mixed.txt"), "mixed needle line\n");
const session = createSession({ hasEditTool: true });
const tool = new SearchTool(session);
const tool = new GrepTool(session);
const result = await tool.execute("test-call", {
pattern: "needle",
@@ -414,7 +414,7 @@ describe("SearchTool internal URL resolution", () => {
it("throws on nonexistent artifact ID", async () => {
const session = createSession();
const tool = new SearchTool(session);
const tool = new GrepTool(session);
expect(tool.execute("test-call", { pattern: "foo", paths: ["artifact://999"] })).rejects.toThrow(
"Artifact 999 not found",
@@ -425,9 +425,9 @@ describe("SearchTool internal URL resolution", () => {
registerVirtualDocs(new Map([["doc.md", "l1\nneedle a\nl3\nneedle b\nl5\nl6\nl7\nl8\n"]]));
const session = createSession({
settings: Settings.isolated({ "search.contextBefore": 1, "search.contextAfter": 3 }),
settings: Settings.isolated({ "grep.contextBefore": 1, "grep.contextAfter": 3 }),
});
const tool = new SearchTool(session);
const tool = new GrepTool(session);
const result = await tool.execute("test-call", {
pattern: "needle",
@@ -453,7 +453,7 @@ describe("SearchTool internal URL resolution", () => {
await Bun.write(path.join(tmpDir, "b.txt"), "needle in b\n");
const session = createSession();
const tool = new SearchTool(session);
const tool = new GrepTool(session);
const result = await tool.execute("test-call", {
pattern: "needle",
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { SearchTool, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { GrepTool, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { ToolError } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
function createTestSession(cwd: string, overrides: Partial<ToolSession> = {}): ToolSession {
@@ -30,7 +30,7 @@ describe("search tool invalid regex handling", () => {
});
it("wraps invalid regex pattern errors in a ToolError", async () => {
const tool = new SearchTool(createTestSession(cwd));
const tool = new GrepTool(createTestSession(cwd));
let caught: unknown;
try {
@@ -19,8 +19,8 @@ import { AgentRegistry } from "@oh-my-pi/pi-coding-agent/registry/agent-registry
import type { SessionEntry, SessionTreeNode } from "@oh-my-pi/pi-coding-agent/session/session-entries";
import { ToolChoiceQueue } from "@oh-my-pi/pi-coding-agent/session/tool-choice-queue";
import { createTools, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { searchToolRenderer } from "@oh-my-pi/pi-coding-agent/tools/search";
import { Text } from "@oh-my-pi/pi-tui";
import { grepToolRenderer } from "../../src/tools/grep";
function createTestSession(cwd: string, overrides: Partial<ToolSession> = {}): ToolSession {
return {
@@ -143,9 +143,9 @@ describe("tool path arrays", () => {
it("search accepts explicit path arrays", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "search");
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing search tool");
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("search-path-array", {
pattern: "shared-needle",
@@ -165,9 +165,9 @@ describe("tool path arrays", () => {
it("search expands delimited path entries", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "search");
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing search tool");
if (!tool) throw new Error("Missing grep tool");
for (const [name, entry] of [
["comma", "apps/grep.txt, packages/grep.txt"],
@@ -192,9 +192,9 @@ describe("tool path arrays", () => {
it("search keeps comma-delimited surviving entries when peers are missing", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "search");
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing search tool");
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("search-delimited-missing", {
pattern: "shared-needle",
@@ -213,9 +213,9 @@ describe("tool path arrays", () => {
it("records hashline snapshots for matched files", async () => {
const session = createTestSession(tempDir);
const tools = await createTools(session);
const tool = tools.find(entry => entry.name === "search");
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing search tool");
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("search-records-snapshot", {
pattern: "shared-needle",
@@ -235,9 +235,9 @@ describe("tool path arrays", () => {
it("search accepts a single string path through tool validation", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "search");
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing search tool");
if (!tool) throw new Error("Missing grep tool");
const args = validateToolArguments(tool, {
type: "toolCall",
@@ -265,8 +265,8 @@ describe("tool path arrays", () => {
await Bun.write(path.join(tmp, "apps", "[id]", "page.tsx"), "bracket-needle\n");
const tools = await createTools(createTestSession(tmp));
const tool = tools.find(entry => entry.name === "search");
if (!tool) throw new Error("Missing search tool");
const tool = tools.find(entry => entry.name === "grep");
if (!tool) throw new Error("Missing grep tool");
const single = await tool.execute("search-bracket-literal-single", {
pattern: "bracket-needle",
@@ -282,8 +282,8 @@ describe("tool path arrays", () => {
await fs.rm(tmp, { recursive: true, force: true });
});
it("search pending renderer accepts a single string path", () => {
const component = searchToolRenderer.renderCall(
it("grep pending renderer accepts a single string path", () => {
const component = grepToolRenderer.renderCall(
{ pattern: "space-needle", paths: "folder with spaces/" },
renderOptions,
plainTheme,
@@ -292,7 +292,7 @@ describe("tool path arrays", () => {
expect(component).toBeInstanceOf(Text);
expect((component as Text).getText()).toContain("in folder with spaces/");
});
it("agent hub chat renders a single-string search path summary", async () => {
it("agent hub chat renders a single-string grep path summary", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "search-path-lists-"));
const sessionFile = await makeJsonlSessionFile(tmp, [
{ type: "session", version: 3, id: "search-overlay-session", timestamp: new Date().toISOString() },
@@ -301,7 +301,7 @@ describe("tool path arrays", () => {
id: "msg-user-1",
parentId: null,
timestamp: new Date().toISOString(),
message: { role: "user", content: "search", timestamp: 1 },
message: { role: "user", content: "grep", timestamp: 1 },
},
{
type: "message",
@@ -314,7 +314,7 @@ describe("tool path arrays", () => {
{
type: "toolCall",
id: "search-call-1",
name: "search",
name: "grep",
arguments: { pattern: "space-needle", paths: "folder with spaces/" },
},
],
@@ -369,15 +369,15 @@ describe("tool path arrays", () => {
const rendered = Bun.stripANSI(viewer.render(120).join("\n"));
viewer.dispose();
// The hub chat now renders through searchToolRenderer.renderCall; the
// The hub chat now renders through grepToolRenderer.renderCall; the
// single-string `paths` arg shows up as the "in <paths>" scope meta on the
// pending call line (a completed result merges the call line away).
expect(rendered).toContain("in folder with spaces/");
await fs.rm(tmp, { recursive: true, force: true });
});
it("tree selector renders a single-string search path summary", () => {
const root = makeMessageNode({ role: "user", content: "search", timestamp: 1 });
it("tree selector renders a single-string grep path summary", () => {
const root = makeMessageNode({ role: "user", content: "grep", timestamp: 1 });
const assistant = makeMessageNode(
{
role: "assistant",
@@ -385,7 +385,7 @@ describe("tool path arrays", () => {
{
type: "toolCall",
id: "search-call-1",
name: "search",
name: "grep",
arguments: { pattern: "space-needle", paths: "folder with spaces/" },
},
],
@@ -409,7 +409,7 @@ describe("tool path arrays", () => {
{
role: "toolResult",
toolCallId: "search-call-1",
toolName: "search",
toolName: "grep",
content: [{ type: "text", text: "note.txt" }],
isError: false,
timestamp: 3,
@@ -427,9 +427,9 @@ describe("tool path arrays", () => {
it("search keeps a single path that contains spaces", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "search");
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing search tool");
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("search-space-directory", {
pattern: "space-needle",
@@ -445,9 +445,9 @@ describe("tool path arrays", () => {
it("search accepts quoted directory paths", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "search");
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing search tool");
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("search-quoted-path", {
pattern: "shared-needle",
@@ -464,9 +464,9 @@ describe("tool path arrays", () => {
it("search formats absolute in-cwd paths relative to cwd", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "search");
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing search tool");
if (!tool) throw new Error("Missing grep tool");
const absoluteAppsPath = path.join(tempDir, "apps");
const result = await tool.execute("search-absolute-in-cwd", {
@@ -653,9 +653,9 @@ describe("tool path arrays", () => {
it("find accepts explicit path arrays", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "find");
const tool = tools.find(entry => entry.name === "glob");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing find tool");
if (!tool) throw new Error("Missing glob tool");
const result = await tool.execute("find-path-array", {
paths: ["apps/", "packages/", "phases/"],
@@ -683,9 +683,9 @@ describe("tool path arrays", () => {
it("find expands delimited path entries", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "find");
const tool = tools.find(entry => entry.name === "glob");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing find tool");
if (!tool) throw new Error("Missing glob tool");
for (const [name, entry] of [
["comma", "apps/grep.txt, packages/grep.txt"],
@@ -710,9 +710,9 @@ describe("tool path arrays", () => {
it("find keeps comma-delimited surviving entries when peers are missing", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "find");
const tool = tools.find(entry => entry.name === "glob");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing find tool");
if (!tool) throw new Error("Missing glob tool");
const result = await tool.execute("find-delimited-missing", {
paths: ["missing.txt, packages/grep.txt"],
@@ -730,9 +730,9 @@ describe("tool path arrays", () => {
it("find keeps a single path that contains spaces", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "find");
const tool = tools.find(entry => entry.name === "glob");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing find tool");
if (!tool) throw new Error("Missing glob tool");
const result = await tool.execute("find-space-directory", {
paths: ["folder with spaces/"],
@@ -748,9 +748,9 @@ describe("tool path arrays", () => {
it("find accepts quoted directory patterns", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "find");
const tool = tools.find(entry => entry.name === "glob");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing find tool");
if (!tool) throw new Error("Missing glob tool");
const result = await tool.execute("find-quoted-pattern", {
paths: ['"packages/"'],
@@ -770,9 +770,9 @@ describe("tool path arrays", () => {
try {
await Bun.write(path.join(outsideDir, "outside.txt"), "outside\n");
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "find");
const tool = tools.find(entry => entry.name === "glob");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing find tool");
if (!tool) throw new Error("Missing glob tool");
const result = await tool.execute("find-outside-cwd", {
paths: [outsideDir],
@@ -793,9 +793,9 @@ describe("tool path arrays", () => {
it("grep accepts bare directory name arrays", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "search");
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing search tool");
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("grep-bare-path-array", {
pattern: "shared-needle",
@@ -821,9 +821,9 @@ describe("tool path arrays", () => {
await Bun.write(path.join(tmp, "nested", "beta.txt"), "exact-needle nested beta\n");
const tools = await createTools(createTestSession(tmp));
const tool = tools.find(entry => entry.name === "search");
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing search tool");
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("grep-exact-file-array", {
pattern: "exact-needle",
@@ -844,9 +844,9 @@ describe("tool path arrays", () => {
it("grep renders only file headings that have child lines", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "search");
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing search tool");
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("grep-no-empty-headings", {
pattern: "shared-needle",
@@ -872,12 +872,12 @@ describe("tool path arrays", () => {
const tools = await createTools(
createTestSession(tmp, {
settings: Settings.isolated({ "search.contextBefore": 1, "search.contextAfter": 1 }),
settings: Settings.isolated({ "grep.contextBefore": 1, "grep.contextAfter": 1 }),
}),
);
const tool = tools.find(entry => entry.name === "search");
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing search tool");
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("grep-context-label", {
pattern: "needle",
@@ -3,8 +3,8 @@ import * as path from "node:path";
import * as url from "node:url";
import { resetSettingsForTest, Settings, settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { getThemeByName } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { searchToolRenderer } from "@oh-my-pi/pi-coding-agent/tools/search";
import { sanitizeText } from "@oh-my-pi/pi-utils";
import { grepToolRenderer } from "../../src/tools/grep";
function extractLinkUris(text: string): string[] {
return [...text.matchAll(/\x1b\]8;[^;]*;([^\x1b]+)\x1b\\/g)].map(match => match[1]!);
@@ -23,8 +23,8 @@ afterAll(() => {
resetSettingsForTest();
});
describe("searchToolRenderer", () => {
it("indents inline search output and avoids accent-colored success headers", async () => {
describe("grepToolRenderer", () => {
it("indents inline grep output and avoids accent-colored success headers", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
@@ -37,7 +37,7 @@ describe("searchToolRenderer", () => {
},
};
const renderedLines = searchToolRenderer
const renderedLines = grepToolRenderer
.renderResult(result as never, { expanded: true, isPartial: false }, uiTheme, { pattern: "needle" })
.render(240);
const plainLines = sanitizeText(renderedLines.join("\n")).split("\n");
@@ -68,14 +68,9 @@ describe("searchToolRenderer", () => {
},
};
const collapsed = searchToolRenderer.renderResult(
result as never,
{ expanded: false, isPartial: false },
uiTheme,
{
const collapsed = grepToolRenderer.renderResult(result as never, { expanded: false, isPartial: false }, uiTheme, {
pattern: "needle",
},
);
});
const renderedLines = sanitizeText(collapsed.render(200).join("\n")).split("\n");
const bodyLines = renderedLines.slice(1);
@@ -111,12 +106,9 @@ describe("searchToolRenderer", () => {
},
};
const collapsed = searchToolRenderer.renderResult(
result as never,
{ expanded: false, isPartial: false },
uiTheme,
{ pattern: "Flag" },
);
const collapsed = grepToolRenderer.renderResult(result as never, { expanded: false, isPartial: false }, uiTheme, {
pattern: "Flag",
});
const renderedLines = sanitizeText(collapsed.render(240).join("\n")).split("\n");
const bodyLines = renderedLines.slice(1);
@@ -147,7 +139,7 @@ describe("searchToolRenderer", () => {
},
};
const rendered = searchToolRenderer
const rendered = grepToolRenderer
.renderResult(result as never, { expanded: true, isPartial: false }, uiTheme, { pattern: "needle" })
.render(240)
.join("\n");
@@ -178,7 +170,7 @@ describe("searchToolRenderer", () => {
},
};
const rendered = searchToolRenderer
const rendered = grepToolRenderer
.renderResult(result as never, { expanded: true, isPartial: false }, uiTheme, { pattern: "needle" })
.render(240)
.join("\n");
@@ -218,7 +210,7 @@ describe("searchToolRenderer", () => {
const render = (expanded: boolean) =>
sanitizeText(
searchToolRenderer
grepToolRenderer
.renderResult(result as never, { expanded, isPartial: false }, uiTheme, { pattern: "needle" })
.render(200)
.join("\n"),
+20 -6
View File
@@ -75,8 +75,8 @@ describe("createTools", () => {
expect(names).toContain("read");
expect(names).toContain("edit");
expect(names).toContain("write");
expect(names).toContain("search");
expect(names).toContain("find");
expect(names).toContain("grep");
expect(names).toContain("glob");
expect(names).toContain("lsp");
expect(names).toContain("task");
expect(names).toContain("todo");
@@ -86,6 +86,20 @@ describe("createTools", () => {
expect(names).not.toContain("vim");
});
it("normalizes legacy explicit tool names", async () => {
const session = createTestSession({
settings: createSettingsWithOverrides({ "astGrep.enabled": false }),
});
const tools = await createTools(session, ["search", "find", "grep"]);
const names = tools.map(t => t.name);
expect(names.filter(name => name === "grep")).toHaveLength(1);
expect(names).toContain("glob");
expect(names).toContain("resolve");
expect(names).not.toContain("search");
expect(names).not.toContain("find");
});
it("includes bash and eval when both eval backends are allowed", async () => {
const session = createTestSession({
settings: createSettingsWithOverrides({
@@ -197,8 +211,8 @@ describe("createTools", () => {
it("filters disabled builtin tools by settings", async () => {
const session = createTestSession({
settings: createSettingsWithOverrides({
"find.enabled": false,
"search.enabled": false,
"glob.enabled": false,
"grep.enabled": false,
"astGrep.enabled": false,
"astEdit.enabled": false,
"bash.enabled": false,
@@ -211,8 +225,8 @@ describe("createTools", () => {
const names = tools.map(t => t.name);
expect(names).not.toContain("bash");
expect(names).not.toContain("find");
expect(names).not.toContain("search");
expect(names).not.toContain("glob");
expect(names).not.toContain("grep");
expect(names).not.toContain("ast_grep");
expect(names).not.toContain("ast_edit");
expect(names).not.toContain("web_search");
@@ -58,8 +58,8 @@ describe.skipIf(isWindows)("search with omitted paths", () => {
it("defaults to the workspace root when paths is omitted", async () => {
const tools = await createTools(createTestSession(cwd));
const tool = tools.find(entry => entry.name === "search");
if (!tool) throw new Error("Missing search tool");
const tool = tools.find(entry => entry.name === "grep");
if (!tool) throw new Error("Missing grep tool");
// Callers that omit `paths` would otherwise be rejected at schema
// validation with `paths: Invalid input` and never run. Omission must
@@ -74,8 +74,8 @@ describe.skipIf(isWindows)("search with omitted paths", () => {
it("defaults to the workspace root when paths is an empty array", async () => {
const tools = await createTools(createTestSession(cwd));
const tool = tools.find(entry => entry.name === "search");
if (!tool) throw new Error("Missing search tool");
const tool = tools.find(entry => entry.name === "grep");
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("search-empty-paths", {
pattern: "default-needle",
@@ -112,8 +112,8 @@ describe.skipIf(isWindows)("search across unrelated filesystem trees", () => {
it("returns matches from both trees without rooting the scan at /", async () => {
const tools = await createTools(createTestSession(cwd));
const tool = tools.find(entry => entry.name === "search");
if (!tool) throw new Error("Missing search tool");
const tool = tools.find(entry => entry.name === "grep");
if (!tool) throw new Error("Missing grep tool");
const start = performance.now();
const result = await tool.execute("search-cross-tree", {
@@ -206,8 +206,8 @@ describe.skipIf(isWindows)("search with explicit walker-pruned file targets", ()
// explicit file into the walk's glob union silently returned 0 matches.
// The file must be read directly as its own target.
const tools = await createTools(createTestSession(repo));
const tool = tools.find(entry => entry.name === "search");
if (!tool) throw new Error("Missing search tool");
const tool = tools.find(entry => entry.name === "grep");
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("search-git-config", {
pattern: "followTags",
@@ -223,8 +223,8 @@ describe.skipIf(isWindows)("search with explicit walker-pruned file targets", ()
await fs.mkdir(path.join(repo, "src"), { recursive: true });
await Bun.write(path.join(repo, "src", "a.ts"), "needle-dup\n");
const tools = await createTools(createTestSession(repo));
const tool = tools.find(entry => entry.name === "search");
if (!tool) throw new Error("Missing search tool");
const tool = tools.find(entry => entry.name === "grep");
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("search-overlap", {
pattern: "needle-dup",
@@ -44,8 +44,8 @@ describe("multi-path tools tolerate missing entries", () => {
it("search returns matches from existing paths and reports the missing one", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "search");
if (!tool) throw new Error("Missing search tool");
const tool = tools.find(entry => entry.name === "grep");
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("search-multi-missing", {
pattern: "shared-needle",
@@ -64,8 +64,8 @@ describe("multi-path tools tolerate missing entries", () => {
it("search errors only when every path is missing", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "search");
if (!tool) throw new Error("Missing search tool");
const tool = tools.find(entry => entry.name === "grep");
if (!tool) throw new Error("Missing grep tool");
const promise = tool.execute("search-all-missing", {
pattern: "shared-needle",
@@ -77,8 +77,8 @@ describe("multi-path tools tolerate missing entries", () => {
it("find returns matches from existing globs and reports the missing one", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "find");
if (!tool) throw new Error("Missing find tool");
const tool = tools.find(entry => entry.name === "glob");
if (!tool) throw new Error("Missing glob tool");
const result = await tool.execute("find-multi-missing", {
paths: ["src/**/*.ts", "tests/**/*.ts"],
@@ -98,8 +98,8 @@ describe("multi-path tools tolerate missing entries", () => {
it("find errors only when every glob's base directory is missing", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "find");
if (!tool) throw new Error("Missing find tool");
const tool = tools.find(entry => entry.name === "glob");
if (!tool) throw new Error("Missing glob tool");
const promise = tool.execute("find-all-missing", {
paths: ["nope/**/*.ts", "also-nope/**/*.ts"],
@@ -102,7 +102,7 @@ describe("flushGrievances", () => {
});
it("skips network when consent is missing and leaves rows intact", async () => {
insertGrievance(db, "find", "weird ordering");
insertGrievance(db, "glob", "weird ordering");
const fetchSpy = vi.fn(async () => new Response("unexpected", { status: 200 }));
// `denied` is the user-facing kill switch for push.
@@ -116,7 +116,7 @@ describe("flushGrievances", () => {
});
it("skips network when endpoint is missing", async () => {
insertGrievance(db, "find", "weird ordering");
insertGrievance(db, "glob", "weird ordering");
const fetchSpy = vi.fn(async () => new Response("unexpected", { status: 200 }));
const result = await flushGrievances(db, pushSettings({ "dev.autoqaPush.endpoint": "" }), {
@@ -138,7 +138,7 @@ describe("flushGrievances", () => {
});
it("posts pending rows with bearer header and marks them pushed=1 on 200", async () => {
insertGrievance(db, "find", "weird ordering");
insertGrievance(db, "glob", "weird ordering");
insertGrievance(db, "read", "selector ignored");
let capturedInput: string | URL | Request | undefined;
@@ -170,7 +170,7 @@ describe("flushGrievances", () => {
expect(typeof body.arch).toBe("string");
expect(body.installId).toBe("11111111-2222-3333-4444-555555555555");
expect(body.entries).toEqual([
{ id: 1, model: "test-model", version: "test-version", tool: "find", report: "weird ordering" },
{ id: 1, model: "test-model", version: "test-version", tool: "glob", report: "weird ordering" },
{ id: 2, model: "test-model", version: "test-version", tool: "read", report: "selector ignored" },
]);
@@ -182,7 +182,7 @@ describe("flushGrievances", () => {
});
it("omits the Authorization header when no token is configured", async () => {
insertGrievance(db, "find", "no token here");
insertGrievance(db, "glob", "no token here");
let capturedInit: RequestInit | undefined;
const fetchSpy = vi.fn(async (_input: string | URL | Request, init: RequestInit | undefined) => {
capturedInit = init;
@@ -199,7 +199,7 @@ describe("flushGrievances", () => {
});
it("leaves rows unpushed on 5xx and reports failure", async () => {
insertGrievance(db, "find", "boom");
insertGrievance(db, "glob", "boom");
const fetchSpy = vi.fn(async () => new Response("nope", { status: 500 }));
const result = await flushGrievances(db, pushSettings(), { fetch: mockFetch(fetchSpy) });
@@ -211,7 +211,7 @@ describe("flushGrievances", () => {
});
it("drains mid-flight inserts in a follow-up batch within the same loop", async () => {
insertGrievance(db, "find", "first");
insertGrievance(db, "glob", "first");
const fetchEntered = Promise.withResolvers<void>();
const releaseFirstFetch = Promise.withResolvers<Response>();
@@ -245,7 +245,7 @@ describe("flushGrievances", () => {
});
it("collapses concurrent callers onto a single in-flight push", async () => {
insertGrievance(db, "find", "single-flight");
insertGrievance(db, "glob", "single-flight");
const releaseFetch = Promise.withResolvers<Response>();
const fetchSpy = vi.fn(() => releaseFetch.promise);
@@ -265,7 +265,7 @@ describe("flushGrievances", () => {
});
it("skips the next push within the failure cooldown window", async () => {
insertGrievance(db, "find", "first");
insertGrievance(db, "glob", "first");
const fetchSpy = vi.fn(async () => new Response("nope", { status: 500 }));
const settings = pushSettings();
@@ -284,7 +284,7 @@ describe("flushGrievances", () => {
// partial final one), exercising both the LIMIT semantics and the
// "remainder smaller than batch" tail.
const total = 127;
for (let i = 0; i < total; i++) insertGrievance(db, "find", `report-${i}`);
for (let i = 0; i < total; i++) insertGrievance(db, "glob", `report-${i}`);
const seenBatchSizes: number[] = [];
const fetchSpy = vi.fn(async (_input: string | URL | Request, init: RequestInit | undefined) => {
@@ -309,7 +309,7 @@ describe("flushGrievances", () => {
// rows stay flagged unpushed.
const firstBatch = 50;
const secondBatch = 10;
for (let i = 0; i < firstBatch + secondBatch; i++) insertGrievance(db, "find", `r-${i}`);
for (let i = 0; i < firstBatch + secondBatch; i++) insertGrievance(db, "glob", `r-${i}`);
let call = 0;
const fetchSpy = vi.fn(() => {
@@ -64,7 +64,7 @@ describe("tool path root alias", () => {
it("searches from cwd when path is slash", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "search");
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing search tool");
@@ -94,7 +94,7 @@ describe("tool path root alias", () => {
it("finds from cwd when pattern is slash", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "find");
const tool = tools.find(entry => entry.name === "glob");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing find tool");
@@ -194,8 +194,8 @@ describe("SearchToolBm25Tool", () => {
it("discovers built-in tools when using the new tools.discoveryMode=all setting", async () => {
const builtinTools: DiscoverableTool[] = [
builtinTool("find", "Find files and directories matching a glob pattern"),
builtinTool("search", "Search file contents using ripgrep"),
builtinTool("glob", "Find files and directories matching a glob pattern"),
builtinTool("grep", "Search file contents using ripgrep"),
];
const allTools = [...discoverableTools, ...builtinTools];
const session = createSession(discoverableTools, {
@@ -206,7 +206,7 @@ describe("SearchToolBm25Tool", () => {
const result = await tool.execute("call-builtin", { query: "find files" });
const names = result.details?.tools.map(t => t.name) ?? [];
expect(names).toContain("find");
expect(names).toContain("glob");
});
});
@@ -218,8 +218,8 @@ describe("renderSearchToolBm25Description", () => {
it("lists discoverable built-in tool names alphabetically without leaking them into the MCP server line", () => {
const rendered = renderSearchToolBm25Description([
builtinTool("write", "Create or overwrite a file"),
builtinTool("find", "Find files by name"),
builtinTool("search", "Search file contents"),
builtinTool("glob", "Find files by name"),
builtinTool("grep", "Search file contents"),
mcpTool("mcp__github_create_issue", "github", "create_issue", "Create a GitHub issue", ["owner"]),
mcpTool("mcp__slack_post_message", "slack", "post_message", "Post a message to Slack", ["channel"]),
]);
@@ -233,7 +233,7 @@ describe("renderSearchToolBm25Description", () => {
const mcpLine = lineWith(rendered, "Discoverable MCP servers");
expect(mcpLine).toBe("Discoverable MCP servers in this session: github (1 tool), slack (1 tool).");
expect(mcpLine).not.toContain("write");
expect(mcpLine).not.toContain("find");
expect(mcpLine).not.toContain("glob");
expect(rendered).toContain(
"Discoverable MCP servers in this session: github (1 tool), slack (1 tool).\n" +
"Discoverable built-in tools: find, search, write.\n" +
@@ -255,7 +255,7 @@ describe("renderSearchToolBm25Description", () => {
it("keeps built-ins counted in the total discoverable tools line", () => {
const rendered = renderSearchToolBm25Description([
builtinTool("write", "Create or overwrite a file"),
builtinTool("find", "Find files by name"),
builtinTool("glob", "Find files by name"),
mcpTool("mcp__slack_post_message", "slack", "post_message", "Post a message to Slack", ["channel"]),
]);
@@ -53,7 +53,7 @@ describe("task agent capability descriptions", () => {
name: "read_scout",
description: "Read-only scout",
systemPrompt: "Scout the codebase.",
tools: ["read", "search", "find"],
tools: ["read", "grep", "glob"],
source: "project",
},
{
@@ -9,8 +9,8 @@ import { getThemeByName, initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { astGrepToolRenderer } from "@oh-my-pi/pi-coding-agent/tools/ast-grep";
import { ReadTool, readToolRenderer } from "@oh-my-pi/pi-coding-agent/tools/read";
import { searchToolRenderer } from "@oh-my-pi/pi-coding-agent/tools/search";
import { WriteTool, writeToolRenderer } from "@oh-my-pi/pi-coding-agent/tools/write";
import { grepToolRenderer } from "../../src/tools/grep";
// 1x1 PNG so the read tool takes its image branch.
const TINY_PNG_BASE64 =
@@ -128,7 +128,7 @@ describe("tool output OSC 8 file:// hyperlinks", () => {
displayContent: ["# src/", "## interactive-mode.ts#abcd", "*12│const needle = true;"].join("\n"),
},
};
const rendered = searchToolRenderer
const rendered = grepToolRenderer
.renderResult(result as never, { expanded: true, isPartial: false }, theme, { pattern: "needle" })
.render(240)
.join("\n");
+4
View File
@@ -1,6 +1,10 @@
# Changelog
## [Unreleased]
### Added
- Added dedicated renderers for `glob` and `grep` tools to improve result readability
- Enabled display support for legacy `find` and `search` tool outputs via new renderers
## [16.1.23] - 2026-06-26
+63 -5
View File
@@ -227,6 +227,64 @@ export const fixtureEntries: SessionEntry[] = [
timestamp: NOW - 20 * MIN,
},
},
{
id: "e09-legacy-call",
parentId: "e09",
timestamp: iso(NOW - 19 * MIN),
type: "message",
message: {
role: "assistant",
content: [
{ type: "text", text: "Legacy transcript aliases should still render for old saved sessions." },
{
type: "toolCall",
id: "legacy-search-01",
name: "search",
arguments: { pattern: "relay", paths: ["docs/collab.md"] },
intent: "Legacy search alias sample",
},
{
type: "toolCall",
id: "legacy-find-01",
name: "find",
arguments: { paths: ["docs/**/*.md"] },
intent: "Legacy find alias sample",
},
],
model: fixtureModel.id,
usage: mkUsage(4_020, 54, 23_500, 0.014),
stopReason: "toolUse",
timestamp: NOW - 19 * MIN,
},
},
{
id: "e09-legacy-search-result",
parentId: "e09-legacy-call",
timestamp: iso(NOW - 19 * MIN + 1_000),
type: "message",
message: {
role: "toolResult",
toolCallId: "legacy-search-01",
toolName: "search",
content: [{ type: "text", text: "docs/collab.md:12:relay reconnect notes" }],
isError: false,
timestamp: NOW - 19 * MIN + 1_000,
},
},
{
id: "e09-legacy-find-result",
parentId: "e09-legacy-call",
timestamp: iso(NOW - 19 * MIN + 1_500),
type: "message",
message: {
role: "toolResult",
toolCallId: "legacy-find-01",
toolName: "find",
content: [{ type: "text", text: "docs/collab.md\ndocs/collab-protocol.md" }],
isError: false,
timestamp: NOW - 19 * MIN + 1_500,
},
},
{
id: "e10",
parentId: "e09",
@@ -318,11 +376,11 @@ export const fixtureAgents: AgentSnapshot[] = [
},
];
const PROBE_TOOLS = ["bash", "read", "search", "edit"] as const;
const PROBE_TOOLS = ["bash", "read", "grep", "edit"] as const;
const PROBE_TOOL_ARGS: Record<(typeof PROBE_TOOLS)[number], string> = {
bash: "bun test packages/coding-agent/test/collab --filter reconnect",
read: "packages/coding-agent/src/collab/relay-client.ts:168-197",
search: "scheduleRetry|failFatal",
grep: "scheduleRetry|failFatal",
edit: "packages/coding-agent/test/collab/reconnect.test.ts",
};
@@ -396,8 +454,8 @@ const subagentTranscriptLines: unknown[] = [
{
type: "toolCall",
id: "sub-call-01",
name: "search",
arguments: { pattern: "40\\d\\d", path: "docs/collab.md" },
name: "grep",
arguments: { pattern: "40\\d\\d", paths: ["docs/collab.md"] },
intent: "Finding close codes",
},
],
@@ -415,7 +473,7 @@ const subagentTranscriptLines: unknown[] = [
message: {
role: "toolResult",
toolCallId: "sub-call-01",
toolName: "search",
toolName: "grep",
content: [
{
type: "text",
@@ -1,7 +1,6 @@
/**
* Tool renderer registry. Keys are wire tool names; aliases map legacy or
* sibling tool names onto a shared renderer. Unknown tools fall back to the
* generic JSON renderer.
* Tool renderer registry. Keys are current wire tool names; aliases keep old
* transcript names renderable. Unknown tools fall back to the generic JSON renderer.
*/
import { genericRenderer } from "./generic";
import { askRenderer } from "./tools/ask";
@@ -13,10 +12,11 @@ import { debugRenderer } from "./tools/debug";
import { editRenderer } from "./tools/edit";
import { evalRenderer } from "./tools/eval";
import { fetchRenderer } from "./tools/fetch";
import { findRenderer } from "./tools/find";
import { generateImageRenderer } from "./tools/generate-image";
import { githubRenderer } from "./tools/github";
import { globRenderer } from "./tools/glob";
import { goalRenderer } from "./tools/goal";
import { grepRenderer } from "./tools/grep";
import { inspectImageRenderer } from "./tools/inspect-image";
import { ircRenderer } from "./tools/irc";
import { jobRenderer } from "./tools/job";
@@ -28,7 +28,6 @@ import { readRenderer } from "./tools/read";
import { reportFindingRenderer } from "./tools/report-finding";
import { reportToolIssueRenderer } from "./tools/report-tool-issue";
import { resolveRenderer } from "./tools/resolve";
import { searchRenderer } from "./tools/search";
import { searchBm25Renderer } from "./tools/search-bm25";
import { sshRenderer } from "./tools/ssh";
import { taskRenderer } from "./tools/task";
@@ -53,7 +52,8 @@ const RENDERERS: Record<string, ToolRenderer> = {
python: evalRenderer,
notebook: evalRenderer,
fetch: fetchRenderer,
find: findRenderer,
glob: globRenderer,
find: globRenderer,
generate_image: generateImageRenderer,
github: githubRenderer,
goal: goalRenderer,
@@ -71,8 +71,8 @@ const RENDERERS: Record<string, ToolRenderer> = {
report_finding: reportFindingRenderer,
report_tool_issue: reportToolIssueRenderer,
resolve: resolveRenderer,
search: searchRenderer,
grep: searchRenderer,
grep: grepRenderer,
search: grepRenderer,
search_tool_bm25: searchBm25Renderer,
ssh: sshRenderer,
task: taskRenderer,
@@ -1,4 +1,4 @@
/** `find` — glob-based file finder; results are paths sorted by mtime. */
/** `glob` (legacy `find`) — glob-based file finder; results are paths sorted by mtime. */
import type { ReactNode } from "react";
import { Badge, Badges, InvalidArg, Note, ResultText } from "../parts";
import type { ToolRenderer, ToolRenderProps } from "../types";
@@ -63,4 +63,4 @@ function Body({ args, result }: ToolRenderProps): ReactNode {
);
}
export const findRenderer: ToolRenderer = { Summary, Body };
export const globRenderer: ToolRenderer = { Summary, Body };
@@ -1,10 +1,10 @@
/** `search` (alias `grep`) — ripgrep content search across workspace files. */
/** `grep` (legacy `search`) — ripgrep content search across workspace files. */
import type { ReactNode } from "react";
import { Badge, Badges, InvalidArg, Note, ResultText } from "../parts";
import type { ToolRenderer, ToolRenderProps } from "../types";
import { detailsRecord, num, resultTextOf, shortenPath, str } from "../util";
/** Search targets: modern `paths` (string | string[]) or legacy grep `path`. */
/** Grep targets: current `paths` (string | string[]) or legacy `path`. */
function pathsOf(args: Record<string, unknown>): string[] {
const raw = args.paths ?? args.path;
if (typeof raw === "string") return [shortenPath(raw)];
@@ -18,7 +18,7 @@ function pathsOf(args: Record<string, unknown>): string[] {
return ["."];
}
/** Flag badges covering both arg dialects (search: i/skip/gitignore; grep: glob/type/multiline). */
/** Flag badges covering current and legacy arg dialects. */
function argBadges(args: Record<string, unknown>): ReactNode[] {
const badges: ReactNode[] = [];
const glob = str(args.glob);
@@ -82,4 +82,4 @@ function Body({ args, result }: ToolRenderProps): ReactNode {
);
}
export const searchRenderer: ToolRenderer = { Summary, Body };
export const grepRenderer: ToolRenderer = { Summary, Body };
+1 -5
View File
@@ -584,11 +584,7 @@ export function setWorktreesDir(dir: string | undefined): string | undefined {
* ignored and resolution falls through.
*/
export function getWorktreesDir(): string {
return (
resolveWorktreeBase(process.env.OMP_WORKTREE_DIR) ??
worktreesDirOverride ??
dirs.rootSubdir("wt", "data")
);
return resolveWorktreeBase(process.env.OMP_WORKTREE_DIR) ?? worktreesDirOverride ?? dirs.rootSubdir("wt", "data");
}
/** Get the SSH control socket directory (~/.omp/ssh-control). */