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:
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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, " ");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
.split(",")
|
||||
.map(s => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
const names = deps.normalizeToolNames(
|
||||
value
|
||||
.split(",")
|
||||
.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.
|
||||
|
||||
@@ -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>
|
||||
>,
|
||||
|
||||
@@ -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
-1
@@ -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.
|
||||
+3
-3
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
+32
-32
@@ -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,
|
||||
@@ -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");
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+4
-4
@@ -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");
|
||||
+6
-6
@@ -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,
|
||||
+27
-27
@@ -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",
|
||||
+2
-2
@@ -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 {
|
||||
+50
-50
@@ -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",
|
||||
+13
-21
@@ -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,
|
||||
{
|
||||
pattern: "needle",
|
||||
},
|
||||
);
|
||||
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"),
|
||||
@@ -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");
|
||||
|
||||
+10
-10
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -138,4 +142,4 @@
|
||||
|
||||
### Security
|
||||
|
||||
- Hardened transcript Markdown rendering by escaping embedded HTML and allowing only safe link schemes
|
||||
- Hardened transcript Markdown rendering by escaping embedded HTML and allowing only safe link schemes
|
||||
@@ -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,
|
||||
|
||||
+2
-2
@@ -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 };
|
||||
+4
-4
@@ -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 };
|
||||
@@ -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). */
|
||||
|
||||
Reference in New Issue
Block a user