diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3d83f2afe..dceca5439 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,15 +1,21 @@ # Changelog ## [Unreleased] - ### Added +- Added `resolveToolAlias` export so tool names in CLI and session setup are normalized to canonical names, including mapping legacy `read` references to `open` +- Added new `open` and `open-chunk` tool prompt documentation pages to describe canonical `open` usage for local files/directories, chunk reads, and URLs - Added full-output retrieval metadata to minimized shell command output by appending an `artifact://` footer with byte counts, allowing users to open the original unminimized command output - Added streaming preview API exports from the package (`resolveEditMode`, `EDIT_MODE_STRATEGIES`, and chunk preview helpers) so editors can reuse mode-aware edit preview logic programmatically - Added `shellMinimizer` configuration options (`enabled`, `settingsPath`, `only`, `except`, and `maxCaptureBytes`) so users can control shell output minimization behavior ### Changed +- Changed the canonical file/URL reader tool from `read` to `open` across default tool lists and routing, including system prompts, plan mode, cursor handlers, and runtime tool registration +- Changed runtime and UI handling to render and track `open` tool calls as first-class (with `read` accepted as legacy alias), including ACP mapping, session observers, and streaming message groups +- Changed fetch output logging so URL-fetch artifacts now use `.open.log` naming instead of `.read.log` +- Changed Bash interception guidance and errors to recommend `open` in place of `read` for cat/head/tail-style commands +- Changed exported SDK tool surface to expose `OpenTool` as canonical and keep `ReadTool` as a compatibility alias - Changed session list loading to use parallel workers and fixed-size prefix reads per session file, reducing latency when loading many or large sessions - Changed edit call rendering to use mode-aware streaming diff previews, including multi-file chunk edit previews grouped by file path while arguments are still streaming - Changed shell execution in both interactive and non-interactive modes to route command output through the configured shell output minimizer @@ -7152,4 +7158,4 @@ Initial public release. - Git branch display in footer - Message queueing during streaming responses - OAuth integration for Gmail and Google Calendar access -- HTML export with syntax highlighting and collapsible sections +- HTML export with syntax highlighting and collapsible sections \ No newline at end of file diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index c8f5de886..65416ad9f 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -5,7 +5,7 @@ import { type Effort, THINKING_EFFORTS } from "@oh-my-pi/pi-ai"; import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { parseEffort } from "../thinking"; -import { BUILTIN_TOOLS } from "../tools"; +import { BUILTIN_TOOLS, resolveToolAlias } from "../tools"; export type Mode = "text" | "json" | "rpc" | "acp"; @@ -129,7 +129,8 @@ export function parseArgs(args: string[], extensionFlags?: Map s.trim().toLowerCase()) .filter(Boolean); const validTools: string[] = []; - for (const name of toolNames) { + for (const rawName of toolNames) { + const name = resolveToolAlias(rawName); if (name in BUILTIN_TOOLS) { validTools.push(name); } else { diff --git a/packages/coding-agent/src/cli/read-cli.ts b/packages/coding-agent/src/cli/read-cli.ts index 12e8aa9b2..bf97749d5 100644 --- a/packages/coding-agent/src/cli/read-cli.ts +++ b/packages/coding-agent/src/cli/read-cli.ts @@ -11,7 +11,7 @@ import { formatChunkedRead, resolveAnchorStyle } from "../edit/modes/chunk"; import { getLanguageFromPath } from "../modes/theme/theme"; import type { ToolSession } from "../tools"; import { parseReadUrlTarget } from "../tools/fetch"; -import { ReadTool } from "../tools/read"; +import { OpenTool } from "../tools/open"; export interface ReadCommandArgs { path: string; @@ -34,7 +34,7 @@ export async function runReadCommand(cmd: ReadCommandArgs): Promise { const parsedUrlTarget = parseReadUrlTarget(cmd.path, cmd.sel); if (parsedUrlTarget) { const settings = await Settings.init({ cwd }); - const tool = new ReadTool(createCliReadSession(cwd, settings)); + const tool = new OpenTool(createCliReadSession(cwd, settings)); const result = await tool.execute("cli-read", { path: cmd.path, sel: cmd.sel }); const text = result.content.find((content): content is { type: "text"; text: string } => content.type === "text"); console.log(text?.text ?? ""); diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 7ddf86a9e..67e9e78f1 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -155,8 +155,8 @@ const EMPTY_MODEL_TAGS_RECORD: ModelTagsSettings = {}; export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [ { pattern: "^\\s*(cat|head|tail|less|more)\\s+", - tool: "read", - message: "Use the `read` tool instead of cat/head/tail. It provides better context and handles binary files.", + tool: "open", + message: "Use the `open` tool instead of cat/head/tail. It provides better context and handles binary files.", }, { pattern: "^\\s*(grep|rg|ripgrep|ag|ack)\\s+", diff --git a/packages/coding-agent/src/cursor.ts b/packages/coding-agent/src/cursor.ts index 6e4fe597f..044bbbfb3 100644 --- a/packages/coding-agent/src/cursor.ts +++ b/packages/coding-agent/src/cursor.ts @@ -164,14 +164,14 @@ export class CursorExecHandlers implements ICursorExecHandlers { async read(args: Parameters>[0]) { const toolCallId = decodeToolCallId(args.toolCallId); - const toolResultMessage = await executeTool(this.options, "read", toolCallId, { path: args.path }); + const toolResultMessage = await executeTool(this.options, "open", toolCallId, { path: args.path }); return toolResultMessage; } async ls(args: Parameters>[0]) { const toolCallId = decodeToolCallId(args.toolCallId); // Redirect ls to read tool, which handles directories - const toolResultMessage = await executeTool(this.options, "read", toolCallId, { path: args.path }); + const toolResultMessage = await executeTool(this.options, "open", toolCallId, { path: args.path }); return toolResultMessage; } diff --git a/packages/coding-agent/src/edit/renderer.ts b/packages/coding-agent/src/edit/renderer.ts index e9c211f18..8013f211c 100644 --- a/packages/coding-agent/src/edit/renderer.ts +++ b/packages/coding-agent/src/edit/renderer.ts @@ -31,10 +31,6 @@ import { expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./m import type { Operation, PatchEditEntry } from "./modes/patch"; import type { PerFileDiffPreview } from "./streaming"; -// ═══════════════════════════════════════════════════════════════════════════ -// LSP Batching -// ═══════════════════════════════════════════════════════════════════════════ - export { getLspBatchRequest, type LspBatchRequest }; // ═══════════════════════════════════════════════════════════════════════════ diff --git a/packages/coding-agent/src/export/html/template.js b/packages/coding-agent/src/export/html/template.js index f2f9c88d9..7d56c3dde 100644 --- a/packages/coding-agent/src/export/html/template.js +++ b/packages/coding-agent/src/export/html/template.js @@ -394,7 +394,8 @@ function formatToolCall(name, args) { switch (name) { - case 'read': { + case 'read': // legacy alias + case 'open': { const path = shortenPath(String(args.path || args.file_path || '')); const offset = args.offset; const limit = args.limit; @@ -809,7 +810,7 @@ const filePath = str(args.file_path == null ? args.path : args.file_path); let pathHtml = pathDisplay(filePath, args.offset, args.limit); if (args.sel) pathHtml += ':' + escapeHtml(String(args.sel)) + ''; - let html = toolHead('read', pathHtml); + let html = toolHead('open', pathHtml); if (result) { html += ctx.renderResultImages(); const output = ctx.getResultText(); diff --git a/packages/coding-agent/src/extensibility/extensions/types.ts b/packages/coding-agent/src/extensibility/extensions/types.ts index 0bc78c5a0..a12ce4e7b 100644 --- a/packages/coding-agent/src/extensibility/extensions/types.ts +++ b/packages/coding-agent/src/extensibility/extensions/types.ts @@ -48,8 +48,8 @@ import type { FindToolInput, GrepToolDetails, GrepToolInput, - ReadToolDetails, - ReadToolInput, + OpenToolDetails, + OpenToolInput, WriteToolInput, } from "../../tools"; import type { TodoItem } from "../../tools/todo-write"; @@ -668,9 +668,9 @@ export interface BashToolCallEvent extends ToolCallEventBase { input: BashToolInput; } -export interface ReadToolCallEvent extends ToolCallEventBase { - toolName: "read"; - input: ReadToolInput; +export interface OpenToolCallEvent extends ToolCallEventBase { + toolName: "open"; + input: OpenToolInput; } export interface EditToolCallEvent extends ToolCallEventBase { @@ -701,7 +701,7 @@ export interface CustomToolCallEvent extends ToolCallEventBase { /** Fired before a tool executes. Can block. */ export type ToolCallEvent = | BashToolCallEvent - | ReadToolCallEvent + | OpenToolCallEvent | EditToolCallEvent | WriteToolCallEvent | GrepToolCallEvent @@ -721,9 +721,9 @@ export interface BashToolResultEvent extends ToolResultEventBase { details: BashToolDetails | undefined; } -export interface ReadToolResultEvent extends ToolResultEventBase { - toolName: "read"; - details: ReadToolDetails | undefined; +export interface OpenToolResultEvent extends ToolResultEventBase { + toolName: "open"; + details: OpenToolDetails | undefined; } export interface EditToolResultEvent extends ToolResultEventBase { @@ -754,7 +754,7 @@ export interface CustomToolResultEvent extends ToolResultEventBase { /** Fired after a tool executes. Can modify result. */ export type ToolResultEvent = | BashToolResultEvent - | ReadToolResultEvent + | OpenToolResultEvent | EditToolResultEvent | WriteToolResultEvent | GrepToolResultEvent @@ -782,7 +782,7 @@ export type ToolResultEvent = * CustomToolCallEvent.toolName is `string` which overlaps with all literals. */ export function isToolCallEventType(toolName: "bash", event: ToolCallEvent): event is BashToolCallEvent; -export function isToolCallEventType(toolName: "read", event: ToolCallEvent): event is ReadToolCallEvent; +export function isToolCallEventType(toolName: "open", event: ToolCallEvent): event is OpenToolCallEvent; export function isToolCallEventType(toolName: "edit", event: ToolCallEvent): event is EditToolCallEvent; export function isToolCallEventType(toolName: "write", event: ToolCallEvent): event is WriteToolCallEvent; export function isToolCallEventType(toolName: "grep", event: ToolCallEvent): event is GrepToolCallEvent; diff --git a/packages/coding-agent/src/extensibility/hooks/types.ts b/packages/coding-agent/src/extensibility/hooks/types.ts index acc629ace..eb1eac35c 100644 --- a/packages/coding-agent/src/extensibility/hooks/types.ts +++ b/packages/coding-agent/src/extensibility/hooks/types.ts @@ -21,7 +21,7 @@ import type { SessionEntry, SessionManager, } from "../../session/session-manager"; -import type { BashToolDetails, FindToolDetails, GrepToolDetails, ReadToolDetails } from "../../tools"; +import type { BashToolDetails, FindToolDetails, GrepToolDetails, OpenToolDetails } from "../../tools"; import type { TodoItem } from "../../tools/todo-write"; // Re-export for backward compatibility @@ -477,9 +477,9 @@ export interface BashToolResultEvent extends ToolResultEventBase { } /** Tool result event for read tool */ -export interface ReadToolResultEvent extends ToolResultEventBase { - toolName: "read"; - details: ReadToolDetails | undefined; +export interface OpenToolResultEvent extends ToolResultEventBase { + toolName: "open"; + details: OpenToolDetails | undefined; } /** Tool result event for edit tool */ @@ -519,7 +519,7 @@ export interface CustomToolResultEvent extends ToolResultEventBase { */ export type ToolResultEvent = | BashToolResultEvent - | ReadToolResultEvent + | OpenToolResultEvent | EditToolResultEvent | WriteToolResultEvent | GrepToolResultEvent diff --git a/packages/coding-agent/src/memories/index.ts b/packages/coding-agent/src/memories/index.ts index 23f2ff914..096d7dcf5 100644 --- a/packages/coding-agent/src/memories/index.ts +++ b/packages/coding-agent/src/memories/index.ts @@ -539,7 +539,7 @@ function shouldPersistResponseItemForMemories(message: AgentMessage): boolean { } if (role !== "toolResult") return false; const toolName = (message as { toolName?: string }).toolName; - if (toolName === "bash" || toolName === "python" || toolName === "read" || toolName === "grep") { + if (toolName === "bash" || toolName === "python" || toolName === "open" || toolName === "grep") { const text = extractMessageText(message); return text.length > 0 && text.length <= 32_000; } diff --git a/packages/coding-agent/src/modes/acp/acp-event-mapper.ts b/packages/coding-agent/src/modes/acp/acp-event-mapper.ts index 43a784acf..1a68ca2e8 100644 --- a/packages/coding-agent/src/modes/acp/acp-event-mapper.ts +++ b/packages/coding-agent/src/modes/acp/acp-event-mapper.ts @@ -94,7 +94,8 @@ const ACP_TEXT_LIMIT = 4_000; export function mapToolKind(toolName: string): ToolKind { switch (toolName) { - case "read": + case "read": // legacy alias + case "open": return "read"; case "write": case "edit": diff --git a/packages/coding-agent/src/modes/components/session-observer-overlay.ts b/packages/coding-agent/src/modes/components/session-observer-overlay.ts index 5959b3c09..b9519a23b 100644 --- a/packages/coding-agent/src/modes/components/session-observer-overlay.ts +++ b/packages/coding-agent/src/modes/components/session-observer-overlay.ts @@ -342,7 +342,8 @@ export class SessionObserverOverlayComponent extends Container { #formatToolArgs(toolName: string, args: Record): string { // Show the most relevant arg for common tools switch (toolName) { - case "read": + case "read": // legacy alias + case "open": return args.path ? `path: ${args.path}` : ""; case "write": return args.path ? `path: ${args.path}` : ""; diff --git a/packages/coding-agent/src/modes/components/tree-selector.ts b/packages/coding-agent/src/modes/components/tree-selector.ts index 19649ecb4..280e042e4 100644 --- a/packages/coding-agent/src/modes/components/tree-selector.ts +++ b/packages/coding-agent/src/modes/components/tree-selector.ts @@ -637,7 +637,8 @@ class TreeList implements Component { #formatToolCall(name: string, args: Record): string { switch (name) { - case "read": { + case "read": // legacy alias + case "open": { const path = shortenPath(String(args.path || args.file_path || "")); const offset = args.offset as number | undefined; const limit = args.limit as number | undefined; diff --git a/packages/coding-agent/src/modes/controllers/event-controller.ts b/packages/coding-agent/src/modes/controllers/event-controller.ts index 311f146e4..2ca4ef956 100644 --- a/packages/coding-agent/src/modes/controllers/event-controller.ts +++ b/packages/coding-agent/src/modes/controllers/event-controller.ts @@ -212,7 +212,7 @@ export class EventController { for (const content of this.ctx.streamingMessage.content) { if (content.type !== "toolCall") continue; - if (content.name === "read") { + if (content.name === "open" || content.name === "read") { this.#trackReadToolCall(content.id, content.arguments); const component = this.ctx.pendingTools.get(content.id); if (component) { @@ -309,7 +309,7 @@ export class EventController { async #handleToolExecutionStart(event: Extract): Promise { this.#updateWorkingMessageFromIntent(event.intent); if (!this.ctx.pendingTools.has(event.toolCallId)) { - if (event.toolName === "read") { + if (event.toolName === "open" || event.toolName === "read") { this.#trackReadToolCall(event.toolCallId, event.args); const component = this.ctx.pendingTools.get(event.toolCallId); if (component) { @@ -366,7 +366,7 @@ export class EventController { } async #handleToolExecutionEnd(event: Extract): Promise { - if (event.toolName === "read") { + if (event.toolName === "open" || event.toolName === "read") { if (this.#inlineReadToolImages(event.toolCallId, event.result)) { const component = this.ctx.pendingTools.get(event.toolCallId); if (component) { diff --git a/packages/coding-agent/src/modes/utils/ui-helpers.ts b/packages/coding-agent/src/modes/utils/ui-helpers.ts index 8502baf5f..8c68f58fb 100644 --- a/packages/coding-agent/src/modes/utils/ui-helpers.ts +++ b/packages/coding-agent/src/modes/utils/ui-helpers.ts @@ -254,7 +254,7 @@ export class UiHelpers { continue; } - if (content.name === "read") { + if (content.name === "open" || content.name === "read") { if (hasErrorStop && errorMessage) { if (!readGroup) { readGroup = new ReadToolGroupComponent({ @@ -315,7 +315,7 @@ export class UiHelpers { } } } else if (message.role === "toolResult") { - if (message.toolName === "read") { + if (message.toolName === "open" || message.toolName === "read") { const assistantComponent = readToolCallAssistantComponents.get(message.toolCallId); const images: ImageContent[] = message.content.filter( (content): content is ImageContent => content.type === "image", diff --git a/packages/coding-agent/src/prompts/system/system-prompt.md b/packages/coding-agent/src/prompts/system/system-prompt.md index 9dd651d58..42d1603c1 100644 --- a/packages/coding-agent/src/prompts/system/system-prompt.md +++ b/packages/coding-agent/src/prompts/system/system-prompt.md @@ -180,14 +180,14 @@ If the task may involve external systems, SaaS APIs, chat, tickets, databases, d {{#ifAny (includes tools "python") (includes tools "bash")}} ### Tool priority -1. Use specialized tools first{{#ifAny (includes tools "read") (includes tools "grep") (includes tools "find") (includes tools "edit") (includes tools "lsp")}}: {{#has tools "read"}}`read`, {{/has}}{{#has tools "grep"}}`grep`, {{/has}}{{#has tools "find"}}`find`, {{/has}}{{#has tools "edit"}}`edit`, {{/has}}{{#has tools "lsp"}}`lsp`{{/has}}{{/ifAny}} +1. Use specialized tools first{{#ifAny (includes tools "open") (includes tools "grep") (includes tools "find") (includes tools "edit") (includes tools "lsp")}}: {{#has tools "open"}}`open`, {{/has}}{{#has tools "grep"}}`grep`, {{/has}}{{#has tools "find"}}`find`, {{/has}}{{#has tools "edit"}}`edit`, {{/has}}{{#has tools "lsp"}}`lsp`{{/has}}{{/ifAny}} 2. Python: logic, loops, processing, display 3. Bash: simple one-liners only You **MUST NOT** use Python or Bash when a specialized tool exists. {{/ifAny}} -{{#ifAny (includes tools "read") (includes tools "write") (includes tools "grep") (includes tools "find") (includes tools "edit")}} -{{#has tools "read"}}- Use `read`, not `cat` or `open`.{{/has}} +{{#ifAny (includes tools "open") (includes tools "write") (includes tools "grep") (includes tools "find") (includes tools "edit")}} +{{#has tools "open"}}- Use `open`, not `cat`/`head`/`tail`/`curl`/`wget`.{{/has}} {{#has tools "write"}}- Use `write`, not shell redirection.{{/has}} {{#has tools "grep"}}- Use `grep`, not shell regex search.{{/has}} {{#has tools "find"}}- Use `find`, not shell file globbing.{{/has}} @@ -232,7 +232,7 @@ Match commands to the host shell: linux/bash and macos/zsh use Unix commands; wi ### Search before you read {{#has tools "grep"}}- Use `grep` to locate targets.{{/has}} {{#has tools "find"}}- Use `find` to map structure.{{/has}} -{{#has tools "read"}}- Use `read` with offset or limit rather than whole-file reads when practical.{{/has}} +{{#has tools "open"}}- Use `open` with offset or limit rather than whole-file reads when practical.{{/has}} {{#has tools "task"}}- Use `task` for investigate+edit when available.{{/has}} - Do not read a file hoping to find the right thing. diff --git a/packages/coding-agent/src/prompts/tools/bash.md b/packages/coding-agent/src/prompts/tools/bash.md index 6d7e501fa..d53d0f94c 100644 --- a/packages/coding-agent/src/prompts/tools/bash.md +++ b/packages/coding-agent/src/prompts/tools/bash.md @@ -33,13 +33,13 @@ You **MUST NOT** use bash for file operations where specialized tools exist: |Instead of (WRONG)|Use (CORRECT)| |---|---| -|`cat file`, `head -n N file`|`read(path="file", limit=N)`| -|`cat -n file \|sed -n '50,150p'`|`read(path="file", offset=50, limit=100)`| +|`cat file`, `head -n N file`|`open(path="file", limit=N)`| +|`cat -n file \|sed -n '50,150p'`|`open(path="file", offset=50, limit=100)`| {{#if hasGrep}}|`grep -A 20 'pat' file`|`grep(pattern="pat", path="file", post=20)`| |`grep -rn 'pat' dir/`|`grep(pattern="pat", path="dir/")`| |`rg 'pattern' dir/`|`grep(pattern="pattern", path="dir/")`|{{/if}} {{#if hasFind}}|`find dir -name '*.ts'`|`find(pattern="dir/**/*.ts")`|{{/if}} -|`ls dir/`|`read(path="dir/")`| +|`ls dir/`|`open(path="dir/")`| |`cat <<'EOF' > file`|`write(path="file", content="…")`| |`sed -i 's/old/new/' file`|`edit(path="file", edits=[…])`| {{#if hasAstEdit}}|`sed -i 's/oldFn(/newFn(/' src/*.ts`|`ast_edit({ops:[{pat:"oldFn($$$A)", out:"newFn($$$A)"}], path:"src/"})`|{{/if}} diff --git a/packages/coding-agent/src/prompts/tools/browser.md b/packages/coding-agent/src/prompts/tools/browser.md index 0039d7b49..180abd6eb 100644 --- a/packages/coding-agent/src/prompts/tools/browser.md +++ b/packages/coding-agent/src/prompts/tools/browser.md @@ -1,7 +1,7 @@ Navigates, clicks, types, scrolls, drags, queries DOM content, and captures screenshots. -- For fetching static web content (articles, docs, issues/PRs, JSON, PDFs, feeds), prefer the `read` tool with a URL — it returns clean reader-mode text without spinning up a browser. Use this tool only when you need JS execution, authentication, or interactive actions. +- For fetching static web content (articles, docs, issues/PRs, JSON, PDFs, feeds), prefer the `open` tool with a URL — it returns clean reader-mode text without spinning up a browser. Use this tool only when you need JS execution, authentication, or interactive actions. - `"open"` starts a headless session (or implicitly on first action); `"goto"` navigates to `url`; `"close"` releases the browser - `"observe"` captures a numbered accessibility snapshot — prefer `click_id`/`type_id`/`fill_id` using returned `element_id` values; flags: `include_all`, `viewport_only` - `"click"`, `"type"`, `"fill"`, `"press"`, `"scroll"`, `"drag"` for selector-based interactions — prefer ARIA/text selectors (`p-aria/[name="Sign in"]`, `p-text/Continue`) over brittle CSS diff --git a/packages/coding-agent/src/prompts/tools/chunk-edit.md b/packages/coding-agent/src/prompts/tools/chunk-edit.md index 3d7c9231a..2d7c90e41 100644 --- a/packages/coding-agent/src/prompts/tools/chunk-edit.md +++ b/packages/coding-agent/src/prompts/tools/chunk-edit.md @@ -1,4 +1,4 @@ -Edits files via syntax-aware chunks. Run `read(path="file.ts")` first. +Edits files via syntax-aware chunks. Run `open(path="file.ts")` first. - `write` rewrites the entire targeted region — best for most edits. - `replace` does surgical find-and-replace within a chunk — use when making small changes to a large chunk, or batching multiple substitutions. - `insert` adds content before/after a chunk. @@ -8,7 +8,7 @@ Call format: `{"edits": [{"path": "file:chunk#ID~", "write": "new body"}, …]}` - **MUST** `read` first. Never invent chunk paths or IDs. Copy them from the latest `read` output or edit response. - `path` format: `file:selector` — e.g. `src/app.ts:fn_foo#ABCD~`. Append `~` for body, `^` for head, or nothing for the whole chunk. Include `#ID` for `put`/`find`+`replace`/`delete`. -- If the exact chunk path is unclear, run `read(path="file", sel="?")` and copy a selector from that listing. +- If the exact chunk path is unclear, run `open(path="file", sel="?")` and copy a selector from that listing. {{#if chunkAutoIndent}} - Use `\t` for indentation in `content`. Write content at indent-level 0 — the tool re-indents it to match the chunk's position in the file. For example, to replace `~` of a method, write the body starting at column 0: ``` @@ -29,7 +29,7 @@ Call format: `{"edits": [{"path": "file:chunk#ID~", "write": "new body"}, …]}` {{/if}} - Region suffixes only apply to container chunks (classes, functions, impl blocks, sections). On leaf chunks (enum variants, fields, single statements, and compound statements like `if`/`for`/`while`/`match`/`try`), `~` and `^` silently fall back to whole-chunk replacement — prefer the unsuffixed form and always supply the complete replacement (condition + body, not just the body) to avoid dropping structural parts. - `put`, `find`+`replace`, and `delete` require the current ID. `prepend`/`append` do not. -- **IDs change after every edit.** The edit response always carries the new IDs — use those for the next call or run `read(path="file", sel="?")` to refresh. Never reuse an ID from before the latest edit. +- **IDs change after every edit.** The edit response always carries the new IDs — use those for the next call or run `open(path="file", sel="?")` to refresh. Never reuse an ID from before the latest edit. diff --git a/packages/coding-agent/src/prompts/tools/lsp.md b/packages/coding-agent/src/prompts/tools/lsp.md index 8b78deff2..2826de8b5 100644 --- a/packages/coding-agent/src/prompts/tools/lsp.md +++ b/packages/coding-agent/src/prompts/tools/lsp.md @@ -36,4 +36,4 @@ Interacts with Language Server Protocol servers for code intelligence. - You **MUST** use `lsp` for symbol-aware operations (rename, find references, go to definition/implementation, code actions) whenever a language server is available — it is safer and more accurate than text-based alternatives. - You **MUST NOT** perform cross-file renames with `ast_edit`, `sed`, `rsed`, or manual edits when `lsp` `rename` can do it. Text-based renames miss shadowing, re-exports, and usages in other files. - Prefer `lsp` `code_actions` for imports, quick-fixes, and refactors the language server already knows how to apply. - \ No newline at end of file + diff --git a/packages/coding-agent/src/prompts/tools/read-chunk.md b/packages/coding-agent/src/prompts/tools/open-chunk.md similarity index 89% rename from packages/coding-agent/src/prompts/tools/read-chunk.md rename to packages/coding-agent/src/prompts/tools/open-chunk.md index 6e98c294c..2848c4a60 100644 --- a/packages/coding-agent/src/prompts/tools/read-chunk.md +++ b/packages/coding-agent/src/prompts/tools/open-chunk.md @@ -6,7 +6,7 @@ Reads files using syntax-aware chunks. - `timeout` — seconds, for URLs only Each anchor `@full.chunk.path#CCCC` (with `-` prefixes for nesting depth) in the output identifies a chunk. Use `full.chunk.path#CCCC` as-is to read truncated chunks. -If you need a canonical target list, run `read(path="file", sel="?")`. That listing shows chunk paths with IDs. +If you need a canonical target list, run `open(path="file", sel="?")`. That listing shows chunk paths with IDs. Line numbers in the gutter are absolute file line numbers. `L20` (single line, no explicit end) is shorthand for `L20` to end-of-file. Use `L20-L20` for a one-line window. @@ -30,6 +30,6 @@ When used against a SQLite database (`.sqlite`, `.sqlite3`, `.db`, `.db3`), retu -- **MUST** `read` before editing — never invent chunk names or IDs. - - Chunk names are truncated (e.g., `handleRequest` becomes `fn_handleRequ`). Always copy chunk paths from `read` or `?` output — never construct them from source identifiers. +- **MUST** `open` before editing — never invent chunk names or IDs. + - Chunk names are truncated (e.g., `handleRequest` becomes `fn_handleRequ`). Always copy chunk paths from `open` or `?` output — never construct them from source identifiers. diff --git a/packages/coding-agent/src/prompts/tools/read.md b/packages/coding-agent/src/prompts/tools/open.md similarity index 74% rename from packages/coding-agent/src/prompts/tools/read.md rename to packages/coding-agent/src/prompts/tools/open.md index 9de9c2355..527f5f463 100644 --- a/packages/coding-agent/src/prompts/tools/read.md +++ b/packages/coding-agent/src/prompts/tools/open.md @@ -1,9 +1,11 @@ -Reads the content at the specified path or URL. +Opens and reads the content at the specified path or URL. -The `read` tool is multi-purpose and more capable than it looks — inspects files, directories, archives, SQLite databases, images, documents (PDF/DOCX/PPTX/XLSX/RTF/EPUB/ipynb), **and URLs**. -- You **MUST** parallelize reads when exploring related files -- For URLs, `read` fetches the page and returns clean extracted text/markdown by default (reader-mode). It handles HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs, etc. You **SHOULD** reach for `read` — not a browser/puppeteer tool — for fetching and inspecting web content. +The `open` tool is multi-purpose and far more capable than its name might suggest — it inspects files, directories, archives, SQLite databases, images, documents (PDF/DOCX/PPTX/XLSX/RTF/EPUB/ipynb), **and URLs**. + +(Previously named `read`. The old name is still accepted as an alias in agent configs and tool lists, but the canonical name is `open`.) +- You **MUST** parallelize calls when exploring related files +- For URLs, `open` fetches the page and returns clean extracted text/markdown by default (reader-mode). It handles HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs, etc. You **SHOULD** reach for `open` — not a browser/puppeteer tool — for fetching and inspecting web content. ## Parameters - `path` — file path or URL (required) @@ -46,13 +48,13 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`: - `file.db?q=SELECT …` — read-only SELECT query # URLs -Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout. You **SHOULD** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser when the page requires JS execution, authentication, or interactive actions (clicks, forms, scrolling). +Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout. You **SHOULD** prefer `open` over a browser/puppeteer tool for fetching URL content; only use a browser when the page requires JS execution, authentication, or interactive actions (clicks, forms, scrolling). -- You **MUST** use `read` (never bash `cat`/`head`/`tail`/`less`/`more`/`ls`/`tar`/`unzip`/`curl`/`wget`) for all file, directory, archive, and URL reads. -- You **MUST NOT** reach for a browser/puppeteer tool to fetch static web content — `read` handles HTML, PDFs, JSON, feeds, and docs directly. Reserve browser tools for JS-heavy pages or interactive flows. +- You **MUST** use `open` (never bash `cat`/`head`/`tail`/`less`/`more`/`ls`/`tar`/`unzip`/`curl`/`wget`) for all file, directory, archive, and URL reads. +- You **MUST NOT** reach for a browser/puppeteer tool to fetch static web content — `open` handles HTML, PDFs, JSON, feeds, and docs directly. Reserve browser tools for JS-heavy pages or interactive flows. - You **MUST** always include the `path` parameter; never call with `{}`. -- For specific line ranges, use `sel`: `read(path="file", sel="L50-L150")` — not `cat -n file | sed`. +- For specific line ranges, use `sel`: `open(path="file", sel="L50-L150")` — not `cat -n file | sed`. - You **MAY** use `sel` with URL reads; the tool paginates cached fetched output. diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 5a1e5b6f3..e8dfc5142 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -116,10 +116,11 @@ import { isSearchProviderPreference, type LspStartupServerInfo, loadSshTool, + OpenTool, PythonTool, - ReadTool, ResolveTool, renderSearchToolBm25Description, + resolveToolAlias, setPreferredImageProvider, setPreferredSearchProvider, type Tool, @@ -267,8 +268,8 @@ export { GrepTool, HIDDEN_TOOLS, loadSshTool, + OpenTool as ReadTool, PythonTool, - ReadTool, ResolveTool, type ToolSession, WriteTool, @@ -1353,8 +1354,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} const toolNamesFromRegistry = Array.from(toolRegistry.keys()); const requestedToolNames = - (options.toolNames ? [...new Set(options.toolNames.map(name => name.toLowerCase()))] : undefined) ?? - toolNamesFromRegistry; + (options.toolNames + ? [...new Set(options.toolNames.map(name => resolveToolAlias(name.toLowerCase())))] + : undefined) ?? toolNamesFromRegistry; const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name)); const includeExitPlanMode = requestedToolNames.includes("exit_plan_mode"); const mcpDiscoveryEnabled = settings.get("mcp.discoveryMode") ?? false; diff --git a/packages/coding-agent/src/session/compaction/pruning.ts b/packages/coding-agent/src/session/compaction/pruning.ts index 76ea24437..2700d1c58 100644 --- a/packages/coding-agent/src/session/compaction/pruning.ts +++ b/packages/coding-agent/src/session/compaction/pruning.ts @@ -18,7 +18,7 @@ export interface PruneConfig { export const DEFAULT_PRUNE_CONFIG: PruneConfig = { protectTokens: 40_000, minimumSavings: 20_000, - protectedTools: ["skill", "read"], + protectedTools: ["skill", "open", "read"], }; export interface PruneResult { diff --git a/packages/coding-agent/src/session/compaction/utils.ts b/packages/coding-agent/src/session/compaction/utils.ts index 7da8de00b..48b861b3f 100644 --- a/packages/coding-agent/src/session/compaction/utils.ts +++ b/packages/coding-agent/src/session/compaction/utils.ts @@ -44,7 +44,8 @@ export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOp if (!path) continue; switch (block.name) { - case "read": + case "read": // legacy alias + case "open": fileOps.read.add(path); break; case "write": diff --git a/packages/coding-agent/src/system-prompt.ts b/packages/coding-agent/src/system-prompt.ts index bda4dfebf..2a2b34f58 100644 --- a/packages/coding-agent/src/system-prompt.ts +++ b/packages/coding-agent/src/system-prompt.ts @@ -561,7 +561,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): toolNames = Array.from(tools.keys()); } else { // Use defaults - toolNames = ["read", "bash", "python", "edit", "write"]; // TODO: Why? + toolNames = ["open", "bash", "python", "edit", "write"]; // TODO: Why? } } @@ -573,7 +573,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): })); // Filter skills to only include those with read tool - const hasRead = tools?.has("read"); + const hasRead = tools?.has("open"); const filteredSkills = hasRead ? skills : []; const effectiveSystemPromptCustomization = dedupePromptSource(systemPromptCustomization, [ diff --git a/packages/coding-agent/src/task/index.ts b/packages/coding-agent/src/task/index.ts index ea31be1eb..53c449139 100644 --- a/packages/coding-agent/src/task/index.ts +++ b/packages/coding-agent/src/task/index.ts @@ -567,7 +567,7 @@ export class TaskTool implements AgentTool { } const planModeState = this.session.getPlanModeState?.(); - const planModeTools = ["read", "grep", "find", "ls", "lsp", "web_search"]; + const planModeTools = ["open", "grep", "find", "ls", "lsp", "web_search"]; const effectiveAgent: typeof agent = planModeState?.enabled ? { ...agent, diff --git a/packages/coding-agent/src/tools/bash-interceptor.ts b/packages/coding-agent/src/tools/bash-interceptor.ts index 8fca9e981..03c793cce 100644 --- a/packages/coding-agent/src/tools/bash-interceptor.ts +++ b/packages/coding-agent/src/tools/bash-interceptor.ts @@ -6,6 +6,7 @@ * the specialized tools instead. */ import { type BashInterceptorRule, DEFAULT_BASH_INTERCEPTOR_RULES } from "../config/settings-schema"; +import { resolveToolAlias } from "./index"; export interface InterceptionResult { /** If true, the bash command should be blocked */ @@ -50,7 +51,7 @@ export function checkBashInterception( for (const { rule, regex } of compiled) { // Only block if the suggested tool is actually available - if (!availableTools.includes(rule.tool)) { + if (!availableTools.some(name => resolveToolAlias(name) === rule.tool)) { continue; } diff --git a/packages/coding-agent/src/tools/fetch.ts b/packages/coding-agent/src/tools/fetch.ts index 76a95e0f6..e08156492 100644 --- a/packages/coding-agent/src/tools/fetch.ts +++ b/packages/coding-agent/src/tools/fetch.ts @@ -1189,7 +1189,7 @@ async function materializeReadUrlCacheEntry( } async function persistReadUrlArtifact(session: ToolSession, output: string): Promise { - const { path: artifactPath, id } = (await session.allocateOutputArtifact?.("read")) ?? {}; + const { path: artifactPath, id } = (await session.allocateOutputArtifact?.("open")) ?? {}; if (!artifactPath) return undefined; await Bun.write(artifactPath, output); return id; diff --git a/packages/coding-agent/src/tools/index.ts b/packages/coding-agent/src/tools/index.ts index 72a58832d..3b775ae34 100644 --- a/packages/coding-agent/src/tools/index.ts +++ b/packages/coding-agent/src/tools/index.ts @@ -43,10 +43,10 @@ import { import { GrepTool } from "./grep"; import { InspectImageTool } from "./inspect-image"; import { NotebookTool } from "./notebook"; +import { OpenTool } from "./open"; import { wrapToolWithMetaNotice } from "./output-meta"; import { PollTool } from "./poll-tool"; import { PythonTool } from "./python"; -import { ReadTool } from "./read"; import { RenderMermaidTool } from "./render-mermaid"; import { createReportToolIssueTool, isAutoQaEnabled } from "./report-tool-issue"; import { ResolveTool } from "./resolve"; @@ -82,9 +82,9 @@ export * from "./gh"; export * from "./grep"; export * from "./inspect-image"; export * from "./notebook"; +export * from "./open"; export * from "./poll-tool"; export * from "./python"; -export * from "./read"; export * from "./render-mermaid"; export * from "./report-tool-issue"; export * from "./resolve"; @@ -230,7 +230,7 @@ export const BUILTIN_TOOLS: Record = { grep: s => new GrepTool(s), lsp: LspTool.createIf, notebook: s => new NotebookTool(s), - read: s => new ReadTool(s), + open: s => new OpenTool(s), inspect_image: s => new InspectImageTool(s), browser: s => new BrowserTool(s), checkpoint: CheckpointTool.createIf, @@ -244,6 +244,20 @@ export const BUILTIN_TOOLS: Record = { write: s => new WriteTool(s), }; +/** + * Legacy tool-name aliases. Accepted wherever users supply tool names + * (agent `tools:` fields, `--tools` flags, settings, etc.) and normalized + * to the canonical name before lookup. + */ +export const TOOL_ALIASES: Record = { + read: "open", +}; + +/** Resolve a possibly-aliased tool name to its canonical registry key. */ +export function resolveToolAlias(name: string): string { + return TOOL_ALIASES[name] ?? name; +} + export const HIDDEN_TOOLS: Record = { submit_result: s => new SubmitResultTool(s), report_finding: () => reportFindingTool, @@ -291,7 +305,9 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P const includeSubmitResult = session.requireSubmitResultTool === true; const enableLsp = session.enableLsp ?? true; const requestedTools = - toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined; + toolNames && toolNames.length > 0 + ? [...new Set(toolNames.map(name => resolveToolAlias(name.toLowerCase())))] + : undefined; if (requestedTools && !requestedTools.includes("exit_plan_mode")) { requestedTools.push("exit_plan_mode"); } diff --git a/packages/coding-agent/src/tools/read.ts b/packages/coding-agent/src/tools/open.ts similarity index 96% rename from packages/coding-agent/src/tools/read.ts rename to packages/coding-agent/src/tools/open.ts index dd21214fb..c49087bf7 100644 --- a/packages/coding-agent/src/tools/read.ts +++ b/packages/coding-agent/src/tools/open.ts @@ -21,8 +21,8 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types"; import { parseInternalUrl } from "../internal-urls/parse"; import type { InternalUrl } from "../internal-urls/types"; import { getLanguageFromPath, type Theme } from "../modes/theme/theme"; -import readDescription from "../prompts/tools/read.md" with { type: "text" }; -import readChunkDescription from "../prompts/tools/read-chunk.md" with { type: "text" }; +import openDescription from "../prompts/tools/open.md" with { type: "text" }; +import openChunkDescription from "../prompts/tools/open-chunk.md" with { type: "text" }; import type { ToolSession } from "../sdk"; import { DEFAULT_MAX_BYTES, @@ -71,7 +71,7 @@ import { import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors"; import { toolResult } from "./tool-result"; -const PROSE_LANGUAGES = new Set(["markdown", "text", "log", "asciidoc", "restructuredtext"]); +const PROSE_LANGUAGES = new Set(["text", "log", "restructuredtext"]); function isProseLanguage(language: string | undefined): boolean { return language !== undefined && PROSE_LANGUAGES.has(language); @@ -368,15 +368,15 @@ function prependSuffixResolutionNotice(text: string, suffixResolution?: { from: return text ? `${notice}\n${text}` : notice; } -const readSchema = Type.Object({ +const openSchema = Type.Object({ path: Type.String({ description: "Path or URL to read" }), sel: Type.Optional(Type.String({ description: "Selector: chunk path, L10-L50, or raw" })), timeout: Type.Optional(Type.Number({ description: "Timeout in seconds", default: 20 })), }); -export type ReadToolInput = Static; +export type OpenToolInput = Static; -export interface ReadToolDetails { +export interface OpenToolDetails { kind?: "file" | "url"; truncation?: TruncationResult; isDirectory?: boolean; @@ -391,7 +391,7 @@ export interface ReadToolDetails { meta?: OutputMeta; } -type ReadParams = ReadToolInput; +type OpenParams = OpenToolInput; /** Parsed representation of the `sel` parameter. */ type ParsedSelector = @@ -465,11 +465,11 @@ function parseSqliteSelectorInput(selector: string | undefined): { subPath: stri * Reads files with support for images, converted documents (via markit), and text. * Directories return a formatted listing with modification times. */ -export class ReadTool implements AgentTool { - readonly name = "read"; +export class OpenTool implements AgentTool { + readonly name = "open"; readonly label = "Read"; readonly description: string; - readonly parameters = readSchema; + readonly parameters = openSchema; readonly nonAbortable = true; readonly strict = true; @@ -487,11 +487,11 @@ export class ReadTool implements AgentTool { this.#inspectImageEnabled = session.settings.get("inspect_image.enabled"); this.description = resolveEditMode(session) === "chunk" - ? prompt.render(readChunkDescription, { + ? prompt.render(openChunkDescription, { anchorStyle: resolveAnchorStyle(session.settings), chunkAutoIndent: resolveChunkAutoIndent(), }) - : prompt.render(readDescription, { + : prompt.render(openDescription, { DEFAULT_LIMIT: String(this.#defaultLimit), DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES), IS_HASHLINE_MODE: displayMode.hashLines, @@ -593,14 +593,14 @@ export class ReadTool implements AgentTool { offset: number | undefined, limit: number | undefined, options: { - details?: ReadToolDetails; + details?: OpenToolDetails; sourcePath?: string; sourceUrl?: string; sourceInternal?: string; entityLabel: string; ignoreResultLimits?: boolean; }, - ): AgentToolResult { + ): AgentToolResult { const displayMode = resolveFileDisplayMode(this.session); const details = options.details ?? {}; const allLines = text.split("\n"); @@ -701,9 +701,9 @@ export class ReadTool implements AgentTool { archivePath: string, subPath: string, limit: number | undefined, - details: ReadToolDetails, + details: OpenToolDetails, signal?: AbortSignal, - ): Promise> { + ): Promise> { const DEFAULT_LIMIT = 500; const effectiveLimit = limit ?? DEFAULT_LIMIT; const entries = archive.listDirectory(subPath); @@ -727,8 +727,8 @@ export class ReadTool implements AgentTool { const output = results.length > 0 ? results.join("\n") : "(empty archive directory)"; const text = prependSuffixResolutionNotice(output, details.suffixResolution); const truncation = truncateHead(text, { maxLines: Number.MAX_SAFE_INTEGER }); - const directoryDetails: ReadToolDetails = { ...details, isDirectory: true }; - const resultBuilder = toolResult(directoryDetails).text(truncation.content); + const directoryDetails: OpenToolDetails = { ...details, isDirectory: true }; + const resultBuilder = toolResult(directoryDetails).text(truncation.content); resultBuilder.sourcePath(archivePath).limits({ resultLimit: limitMeta.resultLimit?.reached }); if (truncation.truncated) { directoryDetails.truncation = truncation; @@ -743,12 +743,12 @@ export class ReadTool implements AgentTool { limit: number | undefined, resolvedArchivePath: ResolvedArchiveReadPath, signal?: AbortSignal, - ): Promise> { + ): Promise> { throwIfAborted(signal); const archive = await openArchive(resolvedArchivePath.absolutePath); throwIfAborted(signal); - const details: ReadToolDetails = { + const details: OpenToolDetails = { resolvedPath: resolvedArchivePath.absolutePath, suffixResolution: resolvedArchivePath.suffixResolution, }; @@ -772,7 +772,7 @@ export class ReadTool implements AgentTool { const entry = await archive.readFile(resolvedArchivePath.archiveSubPath); const text = decodeUtf8Text(entry.bytes); if (text === null) { - return toolResult(details) + return toolResult(details) .text( prependSuffixResolutionNotice( `[Cannot read binary archive entry '${entry.path}' (${formatBytes(entry.size)})]`, @@ -799,14 +799,14 @@ export class ReadTool implements AgentTool { sel: string | undefined, resolvedSqlitePath: ResolvedSqliteReadPath, signal?: AbortSignal, - ): Promise> { + ): Promise> { throwIfAborted(signal); const selectorInput = sel ? parseSqliteSelectorInput(sel) : { subPath: resolvedSqlitePath.sqliteSubPath, queryString: resolvedSqlitePath.queryString }; const selector = parseSqliteSelector(selectorInput.subPath, selectorInput.queryString); - const details: ReadToolDetails = { + const details: OpenToolDetails = { resolvedPath: resolvedSqlitePath.absolutePath, suffixResolution: resolvedSqlitePath.suffixResolution, }; @@ -826,7 +826,7 @@ export class ReadTool implements AgentTool { ); const truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER }); details.truncation = truncation.truncated ? truncation : undefined; - const resultBuilder = toolResult(details) + const resultBuilder = toolResult(details) .text(truncation.content) .sourcePath(resolvedSqlitePath.absolutePath) .limits({ resultLimit: listLimit.meta.resultLimit?.reached }); @@ -845,7 +845,7 @@ export class ReadTool implements AgentTool { const remaining = sampleRows.totalCount - sampleRows.rows.length; output += `\n[${remaining} more rows; use sel="${selector.table}?limit=20&offset=${sampleRows.rows.length}" to continue]`; } - return toolResult(details) + return toolResult(details) .text(prependSuffixResolutionNotice(output, resolvedSqlitePath.suffixResolution)) .sourcePath(resolvedSqlitePath.absolutePath) .done(); @@ -857,7 +857,7 @@ export class ReadTool implements AgentTool { ? getRowByKey(db, selector.table, lookup, selector.key) : getRowByRowId(db, selector.table, selector.key); if (!row) { - return toolResult(details) + return toolResult(details) .text( prependSuffixResolutionNotice( `No row found in table '${selector.table}' for key '${selector.key}'.`, @@ -867,14 +867,14 @@ export class ReadTool implements AgentTool { .sourcePath(resolvedSqlitePath.absolutePath) .done(); } - return toolResult(details) + return toolResult(details) .text(prependSuffixResolutionNotice(renderRow(row), resolvedSqlitePath.suffixResolution)) .sourcePath(resolvedSqlitePath.absolutePath) .done(); } case "query": { const page = queryRows(db, selector.table, selector); - return toolResult(details) + return toolResult(details) .text( prependSuffixResolutionNotice( renderTable(page.columns, page.rows, { @@ -892,7 +892,7 @@ export class ReadTool implements AgentTool { } case "raw": { const result = executeReadQuery(db, selector.sql); - return toolResult(details) + return toolResult(details) .text( prependSuffixResolutionNotice( renderTable(result.columns, result.rows, { @@ -923,11 +923,11 @@ export class ReadTool implements AgentTool { async execute( _toolCallId: string, - params: ReadParams, + params: OpenParams, signal?: AbortSignal, - _onUpdate?: AgentToolUpdateCallback, + _onUpdate?: AgentToolUpdateCallback, _toolContext?: AgentToolContext, - ): Promise> { + ): Promise> { let { path: readPath, sel, timeout } = params; if (readPath.startsWith("file://")) { readPath = expandPath(readPath); @@ -1071,7 +1071,7 @@ export class ReadTool implements AgentTool { if (suffixResolution) { text = prependSuffixResolutionNotice(text, suffixResolution); } - return toolResult({ + return toolResult({ resolvedPath: absolutePath, suffixResolution, chunk: chunkResult.chunk, @@ -1083,7 +1083,7 @@ export class ReadTool implements AgentTool { // Read the file based on type let content: Array; - let details: ReadToolDetails = {}; + let details: OpenToolDetails = {}; let sourcePath: string | undefined; let truncationInfo: | { result: TruncationResult; options: { direction: "head"; startLine?: number; totalFileLines?: number } } @@ -1178,7 +1178,7 @@ export class ReadTool implements AgentTool { if (suffixResolution) { text = prependSuffixResolutionNotice(text, suffixResolution); } - return toolResult({ + return toolResult({ resolvedPath: absolutePath, suffixResolution, chunk: chunkResult.chunk, @@ -1222,7 +1222,7 @@ export class ReadTool implements AgentTool { totalFileLines === 0 ? "The file is empty." : `Use sel=L1 to read from the start, or sel=L${totalFileLines} to read the last line.`; - return toolResult({ resolvedPath: absolutePath, suffixResolution }) + return toolResult({ resolvedPath: absolutePath, suffixResolution }) .text(`Line ${startLineDisplay} is beyond end of file (${totalFileLines} lines total). ${suggestion}`) .done(); } @@ -1328,7 +1328,7 @@ export class ReadTool implements AgentTool { * Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://). * Supports pagination via offset/limit but rejects them when query extraction is used. */ - async #handleInternalUrl(url: string, offset?: number, limit?: number): Promise> { + async #handleInternalUrl(url: string, offset?: number, limit?: number): Promise> { const internalRouter = this.session.internalRouter!; // Check if URL has query extraction (agent:// only). @@ -1355,7 +1355,7 @@ export class ReadTool implements AgentTool { // Resolve the internal URL const resource = await internalRouter.resolve(url); - const details: ReadToolDetails = { resolvedPath: resource.sourcePath }; + const details: OpenToolDetails = { resolvedPath: resource.sourcePath }; // If extraction was used, return directly (no pagination) if (hasExtraction) { @@ -1376,7 +1376,7 @@ export class ReadTool implements AgentTool { absolutePath: string, limit: number | undefined, signal?: AbortSignal, - ): Promise> { + ): Promise> { const DEFAULT_LIMIT = 500; const effectiveLimit = limit ?? DEFAULT_LIMIT; @@ -1425,7 +1425,7 @@ export class ReadTool implements AgentTool { const output = results.join("\n"); const truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER }); - const details: ReadToolDetails = { + const details: OpenToolDetails = { isDirectory: true, }; @@ -1445,7 +1445,7 @@ export class ReadTool implements AgentTool { // TUI Renderer // ============================================================================= -interface ReadRenderArgs { +interface OpenRenderArgs { path?: string; file_path?: string; sel?: string; @@ -1456,8 +1456,8 @@ interface ReadRenderArgs { raw?: boolean; } -export const readToolRenderer = { - renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component { +export const openToolRenderer = { + renderCall(args: OpenRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component { if (isReadableUrlPath(args.file_path || args.path || "")) { return renderReadUrlCall(args, _options, uiTheme); } @@ -1479,10 +1479,10 @@ export const readToolRenderer = { }, renderResult( - result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails }, + result: { content: Array<{ type: string; text?: string }>; details?: OpenToolDetails }, _options: RenderResultOptions, uiTheme: Theme, - args?: ReadRenderArgs, + args?: OpenRenderArgs, ): Component { const urlDetails = result.details as ReadUrlToolDetails | undefined; if (urlDetails?.kind === "url" || isReadableUrlPath(args?.file_path || args?.path || "")) { diff --git a/packages/coding-agent/src/tools/python.ts b/packages/coding-agent/src/tools/python.ts index 719ae9623..ea33121b3 100644 --- a/packages/coding-agent/src/tools/python.ts +++ b/packages/coding-agent/src/tools/python.ts @@ -590,7 +590,8 @@ function formatStatusEvent(event: PythonStatusEvent, theme: Theme): string { // Build description based on common fields switch (op) { - case "read": + case "read": // legacy alias + case "open": parts.push(`${data.chars} chars`); if (data.path) parts.push(`from ${shortenPath(String(data.path))}`); break; @@ -794,7 +795,8 @@ function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): stri case "git_branch": if (data.branches) addItems(data.branches as unknown[], b => String(b)); break; - case "read": + case "read": // legacy alias + case "open": case "cat": case "head": case "tail": diff --git a/packages/coding-agent/src/tools/renderers.ts b/packages/coding-agent/src/tools/renderers.ts index cda88dc08..eca427679 100644 --- a/packages/coding-agent/src/tools/renderers.ts +++ b/packages/coding-agent/src/tools/renderers.ts @@ -21,8 +21,8 @@ import { ghRunWatchToolRenderer } from "./gh-renderer"; import { grepToolRenderer } from "./grep"; import { inspectImageToolRenderer } from "./inspect-image-renderer"; import { notebookToolRenderer } from "./notebook"; +import { openToolRenderer } from "./open"; import { pythonToolRenderer } from "./python"; -import { readToolRenderer } from "./read"; import { resolveToolRenderer } from "./resolve"; import { searchToolBm25Renderer } from "./search-tool-bm25"; import { sshToolRenderer } from "./ssh"; @@ -57,7 +57,7 @@ export const toolRenderers: Record = { lsp: lspToolRenderer as ToolRenderer, notebook: notebookToolRenderer as ToolRenderer, inspect_image: inspectImageToolRenderer as ToolRenderer, - read: readToolRenderer as ToolRenderer, + read: openToolRenderer as ToolRenderer, resolve: resolveToolRenderer as ToolRenderer, search_tool_bm25: searchToolBm25Renderer as ToolRenderer, ssh: sshToolRenderer as ToolRenderer, diff --git a/packages/coding-agent/test/args.test.ts b/packages/coding-agent/test/args.test.ts index c17d90dbc..fdb2bdfec 100644 --- a/packages/coding-agent/test/args.test.ts +++ b/packages/coding-agent/test/args.test.ts @@ -215,17 +215,17 @@ describe("parseArgs", () => { test("parses --no-tools with explicit --tools flags", () => { const result = parseArgs(["--no-tools", "--tools", "read,bash"]); expect(result.noTools).toBe(true); - expect(result.tools).toEqual(["read", "bash"]); + expect(result.tools).toEqual(["open", "bash"]); }); test("lowercases tool names passed to --tools", () => { const result = parseArgs(["--tools", "Read,Grep"]); - expect(result.tools).toEqual(["read", "grep"]); + expect(result.tools).toEqual(["open", "grep"]); }); test("parses --tools=value with equals syntax", () => { const result = parseArgs(["--tools=read,bash"]); - expect(result.tools).toEqual(["read", "bash"]); + expect(result.tools).toEqual(["open", "bash"]); }); test("parses --tools=value with single tool", () => { diff --git a/packages/coding-agent/test/block-images.test.ts b/packages/coding-agent/test/block-images.test.ts index cec63ea1d..e2dcb664c 100644 --- a/packages/coding-agent/test/block-images.test.ts +++ b/packages/coding-agent/test/block-images.test.ts @@ -5,7 +5,7 @@ import * as path from "node:path"; import { processFileArguments } from "@oh-my-pi/pi-coding-agent/cli/file-processor"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; -import { ReadTool } from "@oh-my-pi/pi-coding-agent/tools/read"; +import { OpenTool } from "@oh-my-pi/pi-coding-agent/tools/open"; // 1x1 red PNG image as base64 (smallest valid PNG) const TINY_PNG_BASE64 = @@ -71,7 +71,7 @@ describe("blockImages setting", () => { const imagePath = path.join(testDir, "test.png"); fs.writeFileSync(imagePath, Buffer.from(TINY_PNG_BASE64, "base64")); - const tool = new ReadTool( + const tool = new OpenTool( createTestToolSession(testDir, Settings.isolated({ "inspect_image.enabled": false })), ); const result = await tool.execute("test-1", { path: imagePath }); @@ -87,7 +87,7 @@ describe("blockImages setting", () => { const textPath = path.join(testDir, "test.txt"); fs.writeFileSync(textPath, "Hello, world!"); - const tool = new ReadTool(createTestToolSession(testDir)); + const tool = new OpenTool(createTestToolSession(testDir)); const result = await tool.execute("test-2", { path: textPath }); expect(result.content).toHaveLength(1); diff --git a/packages/coding-agent/test/sdk-mcp-discovery.test.ts b/packages/coding-agent/test/sdk-mcp-discovery.test.ts index 71b957579..dd4fdf796 100644 --- a/packages/coding-agent/test/sdk-mcp-discovery.test.ts +++ b/packages/coding-agent/test/sdk-mcp-discovery.test.ts @@ -105,7 +105,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { await session.activateDiscoveredMCPTools(["mcp_slack_post_message"]); expect(session.getActiveToolNames()).toEqual( - expect.arrayContaining(["read", "search_tool_bm25", "mcp_github_create_issue", "mcp_slack_post_message"]), + expect.arrayContaining(["open", "search_tool_bm25", "mcp_github_create_issue", "mcp_slack_post_message"]), ); expect(session.getSelectedMCPToolNames()).toEqual(["mcp_github_create_issue", "mcp_slack_post_message"]); }); @@ -127,7 +127,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["read", "search_tool_bm25"], + toolNames: ["open", "search_tool_bm25"], customTools: [ createMcpCustomTool("mcp_github_create_issue", "github", "create_issue"), createMcpCustomTool("mcp_slack_post_message", "slack", "post_message"), @@ -136,7 +136,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { try { expect(session.getSelectedMCPToolNames()).toEqual(["mcp_github_create_issue"]); expect(session.getActiveToolNames()).toEqual( - expect.arrayContaining(["read", "search_tool_bm25", "mcp_github_create_issue"]), + expect.arrayContaining(["open", "search_tool_bm25", "mcp_github_create_issue"]), ); expect(session.getActiveToolNames()).not.toContain("mcp_slack_post_message"); } finally { @@ -158,7 +158,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["read", "search_tool_bm25"], + toolNames: ["open", "search_tool_bm25"], customTools: [createMcpCustomTool("mcp_github_create_issue", "github", "create_issue")], }); @@ -185,7 +185,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["read", "search_tool_bm25"], + toolNames: ["open", "search_tool_bm25"], customTools: [ createMcpCustomTool("mcp_github_create_issue", "github", "create_issue"), createMcpCustomTool("mcp_slack_post_message", "slack", "post_message"), @@ -221,7 +221,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["read", "search_tool_bm25"], + toolNames: ["open", "search_tool_bm25"], customTools: [ createMcpCustomTool("mcp_github_create_issue", "github", "create_issue"), createMcpCustomTool("mcp_slack_post_message", "slack", "post_message"), @@ -232,7 +232,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { expect(resumedSession.serviceTier).toBe("priority"); expect(resumedSession.getSelectedMCPToolNames()).toEqual(["mcp_slack_post_message"]); expect(resumedSession.getActiveToolNames()).toEqual( - expect.arrayContaining(["read", "search_tool_bm25", "mcp_slack_post_message"]), + expect.arrayContaining(["open", "search_tool_bm25", "mcp_slack_post_message"]), ); expect(resumedSession.systemPrompt).toContain("mcp_slack_post_message"); expect(fs.readFileSync(sessionFile!, "utf8")).toBe(persistedBeforeResume); @@ -274,7 +274,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["read", "search_tool_bm25"], + toolNames: ["open", "search_tool_bm25"], customTools: [ createMcpCustomTool("mcp_github_create_issue", "github", "create_issue"), createMcpCustomTool("mcp_slack_post_message", "slack", "post_message"), @@ -285,7 +285,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { expect(session.serviceTier).toBe("priority"); expect(session.getSelectedMCPToolNames()).toEqual(["mcp_github_create_issue"]); expect(session.getActiveToolNames()).toEqual( - expect.arrayContaining(["read", "search_tool_bm25", "mcp_github_create_issue"]), + expect.arrayContaining(["open", "search_tool_bm25", "mcp_github_create_issue"]), ); expect(session.sessionManager.buildSessionContext().hasPersistedMCPToolSelection).toBe(false); expect(fs.readFileSync(sessionFile!, "utf8")).toBe(persistedBeforeResume); @@ -310,13 +310,13 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["read", "search_tool_bm25", "mcp_github_create_issue"], + toolNames: ["open", "search_tool_bm25", "mcp_github_create_issue"], customTools: [ createMcpCustomTool("mcp_github_create_issue", "github", "create_issue"), createMcpCustomTool("mcp_slack_post_message", "slack", "post_message"), ], }); - await firstSession.setActiveToolsByName(["read", "search_tool_bm25"]); + await firstSession.setActiveToolsByName(["open", "search_tool_bm25"]); expect(firstSession.getSelectedMCPToolNames()).toEqual([]); const sessionFile = firstSession.sessionFile; expect(sessionFile).toBeDefined(); @@ -337,7 +337,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { slashCommands: [], enableMCP: false, enableLsp: false, - toolNames: ["read", "search_tool_bm25", "mcp_github_create_issue"], + toolNames: ["open", "search_tool_bm25", "mcp_github_create_issue"], customTools: [ createMcpCustomTool("mcp_github_create_issue", "github", "create_issue"), createMcpCustomTool("mcp_slack_post_message", "slack", "post_message"), @@ -345,7 +345,7 @@ describe("createAgentSession MCP discovery prompt gating", () => { }); try { expect(resumedSession.getSelectedMCPToolNames()).toEqual([]); - expect(resumedSession.getActiveToolNames()).toEqual(expect.arrayContaining(["read", "search_tool_bm25"])); + expect(resumedSession.getActiveToolNames()).toEqual(expect.arrayContaining(["open", "search_tool_bm25"])); expect(resumedSession.getActiveToolNames()).not.toContain("mcp_github_create_issue"); } finally { await resumedSession.dispose(); diff --git a/packages/coding-agent/test/sdk-tool-activation.test.ts b/packages/coding-agent/test/sdk-tool-activation.test.ts index f1b67e302..8286f5613 100644 --- a/packages/coding-agent/test/sdk-tool-activation.test.ts +++ b/packages/coding-agent/test/sdk-tool-activation.test.ts @@ -104,7 +104,7 @@ describe("createAgentSession defaultInactive tool activation", () => { try { expect(session.getActiveToolNames()).toEqual( - expect.arrayContaining(["read", "default_active_tool", "default_inactive_tool"]), + expect.arrayContaining(["open", "default_active_tool", "default_inactive_tool"]), ); expect(session.systemPrompt).toContain("default_inactive_tool"); } finally { diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index 7fb9e4f00..475ab7b92 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -14,9 +14,9 @@ import { BashTool } from "@oh-my-pi/pi-coding-agent/tools/bash"; import { CancelJobTool } from "@oh-my-pi/pi-coding-agent/tools/cancel-job"; import { FindTool } from "@oh-my-pi/pi-coding-agent/tools/find"; import { GrepTool } from "@oh-my-pi/pi-coding-agent/tools/grep"; +import { OpenTool } from "@oh-my-pi/pi-coding-agent/tools/open"; import { wrapToolWithMetaNotice } from "@oh-my-pi/pi-coding-agent/tools/output-meta"; import { PollTool } from "@oh-my-pi/pi-coding-agent/tools/poll-tool"; -import { ReadTool } from "@oh-my-pi/pi-coding-agent/tools/read"; import { WriteTool } from "@oh-my-pi/pi-coding-agent/tools/write"; import * as markitUtils from "@oh-my-pi/pi-coding-agent/utils/markit"; import { $which, Snowflake } from "@oh-my-pi/pi-utils"; @@ -216,7 +216,7 @@ function createTestToolContext(toolNames: string[]): AgentToolContext { describe("Coding Agent Tools", () => { let testDir: string; let session: ToolSession; - let readTool: ReadTool; + let readTool: OpenTool; let writeTool: WriteTool; let editTool: EditTool; let bashTool: BashTool; @@ -235,7 +235,7 @@ describe("Coding Agent Tools", () => { // Create tools for this test directory session = createTestToolSession(testDir); - readTool = wrapToolWithMetaNotice(new ReadTool(session)); + readTool = wrapToolWithMetaNotice(new OpenTool(session)); writeTool = wrapToolWithMetaNotice(new WriteTool(session)); editTool = wrapToolWithMetaNotice(new EditTool(session)); bashTool = wrapToolWithMetaNotice(new BashTool(session)); @@ -519,7 +519,7 @@ describe("Coding Agent Tools", () => { fs.writeFileSync(testFile, pngBuffer); const legacyReadTool = wrapToolWithMetaNotice( - new ReadTool(createTestToolSession(testDir, Settings.isolated({ "inspect_image.enabled": false }))), + new OpenTool(createTestToolSession(testDir, Settings.isolated({ "inspect_image.enabled": false }))), ); const result = await legacyReadTool.execute("test-call-img-1", { path: testFile }); @@ -543,7 +543,7 @@ describe("Coding Agent Tools", () => { fs.writeFileSync(testFile, pngBuffer); const inspectModeReadTool = wrapToolWithMetaNotice( - new ReadTool(createTestToolSession(testDir, Settings.isolated({ "inspect_image.enabled": true }))), + new OpenTool(createTestToolSession(testDir, Settings.isolated({ "inspect_image.enabled": true }))), ); const result = await inspectModeReadTool.execute("test-call-img-guidance", { path: testFile }); const output = getTextOutput(result); @@ -820,7 +820,7 @@ function b() { undefined, createTestToolContext(["read"]), ), - ).rejects.toThrow(/Use the `read` tool instead of cat\/head\/tail/); + ).rejects.toThrow(/Use the `open` tool instead of cat\/head\/tail/); }); it("should allow an explicit empty interceptor pattern list", async () => { diff --git a/packages/coding-agent/test/tools/fetch-kagi-toggle.test.ts b/packages/coding-agent/test/tools/fetch-kagi-toggle.test.ts index 31d5a618e..1a447841b 100644 --- a/packages/coding-agent/test/tools/fetch-kagi-toggle.test.ts +++ b/packages/coding-agent/test/tools/fetch-kagi-toggle.test.ts @@ -4,7 +4,7 @@ import * as os from "node:os"; import * as path from "node:path"; import { type SettingPath, Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; -import { ReadTool } from "@oh-my-pi/pi-coding-agent/tools/read"; +import { OpenTool } from "@oh-my-pi/pi-coding-agent/tools/open"; import * as imageResize from "@oh-my-pi/pi-coding-agent/utils/image-resize"; import * as toolsManager from "@oh-my-pi/pi-coding-agent/utils/tools-manager"; import * as scrapers from "@oh-my-pi/pi-coding-agent/web/scrapers/types"; @@ -59,7 +59,7 @@ describe("read tool URL selector shorthands", () => { it("supports embedded raw selectors in URL paths", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); const pageUrl = "https://example.com/embedded-raw"; const loadPageSpy = vi.spyOn(scrapers, "loadPage").mockImplementation(async requestedUrl => { if (requestedUrl !== pageUrl) { @@ -85,7 +85,7 @@ describe("read tool URL selector shorthands", () => { it("supports embedded line selectors in URL paths", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); const pageUrl = "https://example.com/embedded-lines"; const loadPageSpy = vi.spyOn(scrapers, "loadPage").mockImplementation(async requestedUrl => { if (requestedUrl !== pageUrl) { @@ -152,7 +152,7 @@ describe("read tool URL handling", () => { it("returns an image content block when fetching image URLs", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); const imageBytes = new Uint8Array([137, 80, 78, 71]); vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, @@ -196,7 +196,7 @@ describe("read tool URL handling", () => { it("resizes fetched images before emitting image content blocks", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); const resizeSpy = vi.spyOn(imageResize, "resizeImage").mockResolvedValue({ buffer: new Uint8Array([1, 2, 3]), mimeType: "image/jpeg", @@ -242,7 +242,7 @@ describe("read tool URL handling", () => { it("keeps markit extracted text for image responses", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); const extractedText = "Converted image text content that is definitely longer than fifty characters."; vi.spyOn(imageResize, "resizeImage").mockResolvedValue({ buffer: new Uint8Array([1, 2, 3]), @@ -286,7 +286,7 @@ describe("read tool URL handling", () => { }); it("falls back to text-only output for unsupported image MIME types", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); const fetchBinarySpy = vi.spyOn(scraperUtils, "fetchBinary"); vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, @@ -309,7 +309,7 @@ describe("read tool URL handling", () => { it("uses binary conversion fallback for unsupported image MIME when extension is convertible", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); const convertedText = "Converted image text from markit fallback with sufficient length to pass threshold."; const fetchBinarySpy = vi.spyOn(scraperUtils, "fetchBinary").mockResolvedValue({ ok: true, @@ -342,7 +342,7 @@ describe("read tool URL handling", () => { it("does not treat text/html at .png paths as inline images", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); vi.spyOn(scraperUtils, "fetchBinary").mockResolvedValue({ ok: false, error: "not an image" }); vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, @@ -364,7 +364,7 @@ describe("read tool URL handling", () => { it("falls back to textual output when inline image refetch fails", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); const convertSpy = vi.spyOn(scraperUtils, "convertWithMarkit"); vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, @@ -390,7 +390,7 @@ describe("read tool URL handling", () => { }); it("falls back to text-only output when image payload bytes are invalid", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, status: 200, @@ -431,7 +431,7 @@ describe("read tool URL handling", () => { }); it("prefers rendered page content over site-wide llms.txt for deep pages", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); const pageUrl = "https://bun.com/reference/bun/UnixSocketOptions"; const pageHtml = "

UnixSocketOptions

Page-specific docs.

"; const renderedMarkdown = `# UnixSocketOptions\n\n${"Page-specific API docs. ".repeat(8)}`; @@ -493,7 +493,7 @@ describe("read tool URL handling", () => { it("uses section-scoped llms.txt fallback without requesting the site-wide file", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); const pageUrl = "https://example.com/docs/reference/widget"; const pageHtml = "

Widget

"; const lowQualityRender = `${"Please enable JavaScript to view this page.\n".repeat(6)}${"navigation\n".repeat(4)}`; @@ -574,7 +574,7 @@ describe("read tool URL handling", () => { it("prefers Parallel extract before other HTML renderers when configured", async () => { process.env.PARALLEL_API_KEY = "test-parallel-key"; const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); const pageUrl = "https://example.com/parallel-page"; const pageHtml = "

Parallel Page

"; const ensureToolSpy = vi.spyOn(toolsManager, "ensureTool"); @@ -649,7 +649,7 @@ describe("read tool URL handling", () => { it("reuses cached output for repeated plain URL reads", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); const pageUrl = "https://example.com/repeated-read-cache"; const loadPageSpy = vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, @@ -673,7 +673,7 @@ describe("read tool URL handling", () => { it("supports offset and limit for URL reads using cached output", async () => { const session = createSession(); - const tool = new ReadTool(session); + const tool = new OpenTool(session); const pageUrl = "https://example.com/offset-test"; const loadPageSpy = vi.spyOn(scrapers, "loadPage").mockResolvedValue({ ok: true, @@ -702,6 +702,6 @@ describe("read tool URL handling", () => { expect(pagedText?.text).toContain("Line 2"); expect(pagedText?.text).not.toContain("Line 3"); expect(loadPageSpy).not.toHaveBeenCalled(); - expect(fs.readdirSync(path.join(testDir, "session")).some(file => file.endsWith(".read.log"))).toBe(true); + expect(fs.readdirSync(path.join(testDir, "session")).some(file => file.endsWith(".open.log"))).toBe(true); }); }); diff --git a/packages/coding-agent/test/tools/index.test.ts b/packages/coding-agent/test/tools/index.test.ts index 34ea76f29..eb84f80d5 100644 --- a/packages/coding-agent/test/tools/index.test.ts +++ b/packages/coding-agent/test/tools/index.test.ts @@ -55,7 +55,7 @@ describe("createTools", () => { // Core tools should always be present expect(names).toContain("python"); expect(names).toContain("bash"); - expect(names).toContain("read"); + expect(names).toContain("open"); expect(names).toContain("edit"); expect(names).toContain("write"); expect(names).toContain("grep"); @@ -130,7 +130,7 @@ describe("createTools", () => { const tools = await createTools(session, ["read", "lsp", "write"]); const names = tools.map(t => t.name); - expect(names).toEqual(["read", "write", "exit_plan_mode"]); + expect(names).toEqual(["open", "write", "exit_plan_mode"]); }); it("excludes lsp tool when disabled", async () => { @@ -146,7 +146,7 @@ describe("createTools", () => { const tools = await createTools(session, ["read", "write"]); const names = tools.map(t => t.name); - expect(names).toEqual(["read", "write", "exit_plan_mode"]); + expect(names).toEqual(["open", "write", "exit_plan_mode"]); }); it("ignores vim as an unknown requested tool even when vim edit mode is active", async () => { @@ -158,7 +158,7 @@ describe("createTools", () => { const tools = await createTools(session, ["read", "vim"]); const names = tools.map(t => t.name); - expect(names).toEqual(["read", "exit_plan_mode"]); + expect(names).toEqual(["open", "exit_plan_mode"]); }); it("lowercases requested tool subset", async () => { @@ -166,7 +166,7 @@ describe("createTools", () => { const tools = await createTools(session, ["Read", "Write"]); const names = tools.map(t => t.name); - expect(names).toEqual(["read", "write", "exit_plan_mode"]); + expect(names).toEqual(["open", "write", "exit_plan_mode"]); }); it("includes hidden tools when explicitly requested", async () => { diff --git a/packages/coding-agent/test/tools/root-path-alias.test.ts b/packages/coding-agent/test/tools/root-path-alias.test.ts index 9b24ae39a..dd87bfd20 100644 --- a/packages/coding-agent/test/tools/root-path-alias.test.ts +++ b/packages/coding-agent/test/tools/root-path-alias.test.ts @@ -80,7 +80,7 @@ describe("tool path root alias", () => { it("reads cwd when path is slash", async () => { const tools = await createTools(createTestSession(tempDir)); - const tool = tools.find(entry => entry.name === "read"); + const tool = tools.find(entry => entry.name === "open"); expect(tool).toBeDefined(); if (!tool) throw new Error("Missing read tool"); diff --git a/packages/coding-agent/test/tools/sqlite.test.ts b/packages/coding-agent/test/tools/sqlite.test.ts index de809d21a..0d6632464 100644 --- a/packages/coding-agent/test/tools/sqlite.test.ts +++ b/packages/coding-agent/test/tools/sqlite.test.ts @@ -5,7 +5,7 @@ import * as os from "node:os"; import * as path from "node:path"; import "../../src/tools/renderers"; import { Settings } from "../../src/config/settings"; -import { ReadTool } from "../../src/tools/read"; +import { OpenTool } from "../../src/tools/open"; import { parseSqlitePathCandidates, parseSqliteSelector, renderTable } from "../../src/tools/sqlite-reader"; import { WriteTool } from "../../src/tools/write"; @@ -13,7 +13,7 @@ type ToolTextResult = { content: Array<{ type: string; text?: string }>; }; -type SessionLike = ConstructorParameters[0]; +type SessionLike = ConstructorParameters[0]; function getText(result: ToolTextResult): string { return result.content @@ -150,7 +150,7 @@ describe("SQLite tool support", () => { let sqlitePath: string; let sqliteDbPath: string; let invalidDbPath: string; - let readTool: ReadTool; + let readTool: OpenTool; let writeTool: WriteTool; let originalEditVariant: string | undefined; @@ -167,7 +167,7 @@ describe("SQLite tool support", () => { await Bun.write(invalidDbPath, "not sqlite\nstill text\n"); const session = createSession(tmpDir); - readTool = new ReadTool(session); + readTool = new OpenTool(session); writeTool = new WriteTool(session); });