Skip to content

refactor(web): unify tool definitions between ask agent and MCP server#1014

Open
brendan-kellam wants to merge 16 commits intomainfrom
bkellam/agent-improvements
Open

refactor(web): unify tool definitions between ask agent and MCP server#1014
brendan-kellam wants to merge 16 commits intomainfrom
bkellam/agent-improvements

Conversation

@brendan-kellam
Copy link
Contributor

@brendan-kellam brendan-kellam commented Mar 18, 2026

Summary

  • Introduces a shared ToolDefinition<TName, TShape, TMetadata> abstraction in features/tools/ with a unified execute(input, context) signature and ToolResult<TMetadata> return shape
  • All tools (read_file, list_commits, list_repos, search_code, find_symbol_references, find_symbol_definitions, list_tree) are now defined once and registered with both the Vercel AI agent and the MCP server via toVercelAITool and registerMcpTool adapters
  • Adapters inject call-site context (source: 'agent' or source: 'mcp') into every tool execution
  • isReadOnly and isIdempotent hints are declared on each tool definition and forwarded to MCP annotations via registerMcpTool
  • Tool names normalized to snake_case throughout; toolNames constant eliminated in favour of xDefinition.name
  • Added list_tree tool to the Vercel AI agent with a UI component in the details card
  • Tool metadata fields normalized (repositoryrepo); webUrl added to search_code output
  • Shared logger.ts for tools; debug logging added to all tools using consistent snake_case names
  • Tool descriptions updated with usage guidance (retry on repo-not-found errors, list_repos discovery, webUrl linking)
  • Copy-to-clipboard button moved into ToolHeader so all tool components get it for free

TODO:

  • Fix search_code tool

Test plan

  • Verify ask agent tools work end-to-end (search, read file, list repos, list commits, list tree, find symbol references/definitions)
  • Verify MCP tools work end-to-end via an MCP client
  • Confirm isReadOnly/isIdempotent annotations appear in MCP tool listings (e.g. Cursor Ask mode)
  • Confirm copy button appears on hover in all tool components in the details card

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added many developer-facing tools to chat: code search, file reader, repo listing, commit listing, directory tree, symbol references and definitions.
    • Added ability to import plain-text tool docs for tooling UI.
  • Improvements

    • Centralized tool registry and server wiring for more consistent tool behavior.
    • Tool headers now include a copy action to quickly copy tool outputs.
    • Added configurable debug flag to write chat messages to disk.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 18, 2026

Walkthrough

Consolidates scattered tool definitions into a centralized tools surface, adds adapters for Vercel AI and MCP integration, introduces multiple read-only tools (search_code, read_file, list_tree, list_repos, list_commits, find_symbol_*), updates chat UI to consume metadata-shaped outputs, and wires MCP server registrations declaratively.

Changes

Cohort / File(s) Summary
Environment & Build
./.env.development, packages/web/next.config.mjs, packages/web/package.json
Added DEBUG_WRITE_CHAT_MESSAGES_TO_FILE env var; turbopack rule for *.txt using raw-loader; added raw-loader@4.0.2.
Shared Logger
packages/shared/src/logger.ts, packages/web/src/features/chat/logger.ts, packages/web/src/features/tools/logger.ts
Extended humanReadableFormat to append extra fields as JSON; added feature loggers ask-agent and tool.
Tool infra & adapters
packages/web/src/features/tools/types.ts, packages/web/src/features/tools/adapters.ts, packages/web/src/features/tools/index.ts
Introduced ToolDefinition/ToolResult/ToolContext types; added toVercelAITool and registerMcpTool adapters; created tools barrel export.
Tool implementations
packages/web/src/features/tools/*.ts (e.g., readFile.ts, searchCode.ts, listCommits.ts, listRepos.ts, listTree.ts, findSymbolReferences.ts, findSymbolDefinitions.ts)
Added new centralized tool definitions with zod input schemas, execute logic, metadata outputs, and read-only/idempotent annotations.
Tool docs & types
packages/web/src/features/tools/*.txt, packages/web/src/features/tools/types.ts, packages/web/src/features/tools/logger.ts
Added plain-text docs for tools and tool-type definitions; new tool-scoped logger.
Tool registry & types usage
packages/web/src/features/chat/tools.ts, packages/web/src/features/chat/types.ts, packages/web/src/features/chat/constants.ts
Replaced per-tool exports with a centralized tools registry; switched type mapping to infer from tools; removed toolNames and uiVisiblePartTypes.
Agent & MCP wiring
packages/web/src/features/chat/agent.ts, packages/web/src/features/mcp/server.ts
Agent updated to consume tool outputs via output.metadata.*; MCP server now registers tools via registerMcpTool(...) and imports tool definitions from @/features/tools.
Chat UI: shared & components
packages/web/src/features/chat/components/chatThread/tools/shared.tsx, .../readFileToolComponent.tsx, .../searchCodeToolComponent.tsx, .../findSymbol*ToolComponent.tsx, .../listReposToolComponent.tsx, .../listCommitsToolComponent.tsx, .../listTreeToolComponent.tsx, .../detailsCard.tsx, .../chatThreadListItem.tsx
Added onCopy support to ToolHeader and many tool components; renamed/normalized components and discriminants to snake_case; updated UIs to use part.output.metadata.*; added ListTreeToolComponent; exhaustive switch handling and type-safety guards applied.
Chat utils & types
packages/web/src/features/chat/utils.ts, packages/web/types.d.ts
Minor formatting change in addLineNumbers; added .txt module declaration for importing raw text.
Large refactor surface
many files across packages/web/src/features/mcp/*, packages/web/src/features/tools/*, packages/web/src/features/chat/*
Broad refactor centralizing tool definitions, types, adapters, MCP registration, and updating UI/agent to new metadata shapes (heterogeneous, cross-cutting changes).

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client (browser)
  participant Agent as Next Agent
  participant Vercel as Vercel AI Runtime
  participant Tool as ToolDefinition
  participant Service as External Service
  rect rgba(100,149,237,0.5)
    Client->>Agent: User invokes assistant/tool action
    Agent->>Vercel: Stream prompt + tool call (via toVercelAITool)
    Vercel->>Tool: Execute tool (input parsed by adapter)
    Tool->>Service: Call backend API (search/read/list)
    Service-->>Tool: Return data/metadata
    Tool-->>Vercel: Return { output: string, metadata }
    Vercel-->>Agent: Tool output streamed
    Agent-->>Client: Render message using output.metadata.* (UI components)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main objective: unifying tool definitions between the ask agent and MCP server through a refactoring effort.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bkellam/agent-improvements
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 18, 2026

License Audit

Status: FAIL

Metric Count
Total packages 2097
Resolved (non-standard) 16
Unresolved 2
Strong copyleft 0
Weak copyleft 27

Fail Reasons

  • 2 packages have unresolvable licenses: @react-grab/cli, @react-grab/mcp

Unresolved Packages

Package Version License Reason
@react-grab/cli 0.1.23 UNKNOWN No license field in npm registry metadata; no repository or homepage URL available to check further
@react-grab/mcp 0.1.23 UNKNOWN No license field in npm registry metadata; no repository or homepage URL available to check further

Weak Copyleft Packages (informational)

Package Version License
@img/sharp-libvips-darwin-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm 1.0.5 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-ppc64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-riscv64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-s390x 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-s390x 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-wasm32 0.33.5 Apache-2.0 AND LGPL-3.0-or-later AND MIT
@img/sharp-wasm32 0.34.5 Apache-2.0 AND LGPL-3.0-or-later AND MIT
@img/sharp-win32-arm64 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-ia32 0.33.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-ia32 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-x64 0.33.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-x64 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
axe-core 4.10.3 MPL-2.0
dompurify 3.3.1 (MPL-2.0 OR Apache-2.0)
Resolved Packages (16)
Package Version Original Resolved Source
codemirror-lang-elixir 4.0.0 UNKNOWN Apache-2.0 npm page
lezer-elixir 1.1.2 UNKNOWN Apache-2.0 npm page
map-stream 0.1.0 UNKNOWN MIT npm page
memorystream 0.3.1 UNKNOWN MIT npm page
valid-url 1.0.9 UNKNOWN MIT GitHub repo (https://github.com/ogt/valid-url/blob/master/LICENSE)
posthog-js 1.345.5 SEE LICENSE IN LICENSE Apache-2.0 GitHub repo (https://github.com/PostHog/posthog-js/blob/main/LICENSE)
pause-stream 0.0.11 ["MIT", "Apache2"] MIT AND Apache-2.0 extracted from license array (non-standard Apache2 normalized to Apache-2.0)
@sentry/cli 2.58.5 FSL-1.1-MIT FSL-1.1-MIT GitHub repo (https://github.com/getsentry/sentry-cli — LICENSE file confirms Functional Source License 1.1, MIT Future License; not a registered SPDX identifier)
@sentry/cli-darwin 2.58.5 FSL-1.1-MIT FSL-1.1-MIT GitHub repo (same sentry-cli repository, FSL-1.1-MIT confirmed)
@sentry/cli-linux-arm 2.58.5 FSL-1.1-MIT FSL-1.1-MIT GitHub repo (same sentry-cli repository, FSL-1.1-MIT confirmed)
@sentry/cli-linux-arm64 2.58.5 FSL-1.1-MIT FSL-1.1-MIT GitHub repo (same sentry-cli repository, FSL-1.1-MIT confirmed)
@sentry/cli-linux-i686 2.58.5 FSL-1.1-MIT FSL-1.1-MIT GitHub repo (same sentry-cli repository, FSL-1.1-MIT confirmed)
@sentry/cli-linux-x64 2.58.5 FSL-1.1-MIT FSL-1.1-MIT GitHub repo (same sentry-cli repository, FSL-1.1-MIT confirmed)
@sentry/cli-win32-arm64 2.58.5 FSL-1.1-MIT FSL-1.1-MIT GitHub repo (same sentry-cli repository, FSL-1.1-MIT confirmed)
@sentry/cli-win32-i686 2.58.5 FSL-1.1-MIT FSL-1.1-MIT GitHub repo (same sentry-cli repository, FSL-1.1-MIT confirmed)
@sentry/cli-win32-x64 2.58.5 FSL-1.1-MIT FSL-1.1-MIT GitHub repo (same sentry-cli repository, FSL-1.1-MIT confirmed)

@brendan-kellam brendan-kellam marked this pull request as ready for review March 18, 2026 03:54
@github-actions

This comment has been minimized.

@brendan-kellam brendan-kellam changed the title wip: unify tool definitions between ask & mcp refactor(web): unify tool definitions between ask agent and MCP server Mar 18, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🧹 Nitpick comments (5)
.env.development (1)

80-80: Move this debug override to .env.development.local instead of committing it in .env.development.

Line 80 is environment-specific behavior and is better kept in local overrides to avoid changing the shared default dev profile.
Based on learnings: Applies to .env.development.local : Use .env.development.local for environment variable overrides instead of modifying .env.development.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.development at line 80, The DEBUG_WRITE_CHAT_MESSAGES_TO_FILE debug
override should not be committed in the shared .env.development; remove the line
"DEBUG_WRITE_CHAT_MESSAGES_TO_FILE=true" from .env.development and add the same
entry to .env.development.local instead, ensuring .env.development.local is
listed in .gitignore so this environment-specific override remains local; update
any docs or README that mention local env overrides if needed.
packages/web/src/features/chat/components/chatThread/tools/shared.tsx (1)

126-134: Accessibility: copy action is not keyboard-accessible.

The eslint-disable suppresses jsx-a11y/click-events-have-key-events and jsx-a11y/no-static-element-interactions, but this means keyboard users cannot trigger the copy action via the wrapper div. While CopyIconButton itself may be focusable, the stopPropagation wrapper intercepts clicks but not keyboard events.

Consider making the wrapper transparent to keyboard events or ensuring CopyIconButton handles its own event propagation:

♻️ Suggested approach
             {onCopy && (
-                // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
-                <div onClick={(e) => e.stopPropagation()}>
+                <div
+                    onClick={(e) => e.stopPropagation()}
+                    onKeyDown={(e) => e.stopPropagation()}
+                >
                     <CopyIconButton
                         onCopy={onCopy}
                         className="opacity-0 group-hover/header:opacity-100 transition-opacity"
                     />
                 </div>
             )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/chat/components/chatThread/tools/shared.tsx` around
lines 126 - 134, The wrapper div around CopyIconButton prevents keyboard
activation because it intercepts click events with onClick={(e) =>
e.stopPropagation()} and is not keyboard-accessible; remove the non-interactive
wrapper or convert it to a keyboard-focusable element (e.g., a button or
role="button" with onKeyDown handling) and ensure it calls e.stopPropagation()
for both click and keyboard activation, or simply move the stopPropagation logic
into CopyIconButton so the wrapper is not needed; update references around the
onCopy prop and CopyIconButton usage to keep identical visual behavior while
restoring keyboard event handling and removing the eslint-disable comments.
packages/web/src/features/tools/index.ts (1)

1-8: Rename this barrel to a camelCase filename.

If this shared export surface is staying, please avoid introducing a new index.ts exception and give it a descriptive camelCase name instead.

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Files should use camelCase starting with a lowercase letter (e.g., shareChatPopover.tsx, userAvatar.tsx, apiClient.ts).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/tools/index.ts` around lines 1 - 8, The barrel file
named index.ts in the tools feature should be renamed to a camelCase filename
(e.g., toolsExports.ts or toolsBarrel.ts) to follow project naming rules; update
the filename that currently re-exports readFile, listCommits, listRepos,
searchCode, findSymbolReferences, findSymbolDefinitions, listTree, and adapters,
and then update all import sites that currently import from the tools directory
(which rely on the index.ts implicit resolution) to import from the new
camelCase module name (ensure references to exports like readFile, listRepos,
searchCode, etc. remain unchanged).
packages/web/src/features/tools/listTree.ts (1)

3-5: Keep shared tree helpers out of features/mcp.

listTreeDefinition lives in the shared tools layer, but it now depends on @/features/mcp/utils for generic tree/path helpers. That inverts the boundary this PR is trying to clean up and makes the tool registry transitively depend on an integration-specific package. Please move these helpers to a neutral shared module.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/tools/listTree.ts` around lines 3 - 5,
listTreeDefinition imports generic helpers from "@/features/mcp/utils" which
inverts module boundaries; move the helpers buildTreeNodeIndex, joinTreePath,
normalizeTreePath, and sortTreeEntries into a neutral shared module (e.g., a new
shared utils under features/tools or a common utils package) and update
listTreeDefinition to import them from that shared location instead of
"@/features/mcp/utils"; ensure exports and any internal types/signatures remain
unchanged and update other references to these helpers to the new module.
packages/web/src/features/mcp/server.ts (1)

28-41: Consider moving list_language_models to a shared tool definition for consistency.

Everything else is now registered through registerMcpTool(...); keeping this one inline leaves the MCP wiring partially divergent. Moving it into features/tools would keep one registration pattern and simplify reuse/testing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/mcp/server.ts` around lines 28 - 41, Move the
inline "list_language_models" tool registration into the shared tool definitions
so MCP wiring is consistent: create a tool definition in the features/tools
module (similar to other tools registered via registerMcpTool) that exposes the
same behavior (calls getConfiguredLanguageModelsInfo and returns the JSON text
payload), export it, and then replace the inline
server.registerTool("list_language_models", ...) call with a registerMcpTool
import/registration of that shared tool; ensure the exported tool name and
behavior match the original list_language_models implementation and reuse
getConfiguredLanguageModelsInfo for fetching models.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.development:
- Line 80: The file ends without a trailing newline which triggers
dotenv-linter's EndingBlankLine; open the .env.development file and add a final
blank line (newline character) after the last entry
DEBUG_WRITE_CHAT_MESSAGES_TO_FILE=true so the file ends with a newline
character.

In `@packages/web/next.config.mjs`:
- Around line 66-73: The turbopack rule for '*.txt' only covers dev; add an
equivalent webpack rule in the Next.js config's webpack property so production
builds (next build) use raw-loader for .txt imports: update the exported config
to include a webpack function that pushes a module.rules entry matching /\.txt$/
and uses 'raw-loader' (mirroring the turbopack loaders setting), and ensure the
loader is installed; locate the existing turbopack block (the turbopack.rules
'*.txt' entry) and add the corresponding webpack.module.rules rule in the same
next.config.mjs config object.

In
`@packages/web/src/features/chat/components/chatThread/tools/findSymbolDefinitionsToolComponent.tsx`:
- Around line 55-60: The list key for FileListItem is not unique across repos
because it uses file.fileName alone; update the map rendering in the
part.output.metadata.files loop to use a repo-qualified key (e.g., combine
file.repo and file.fileName or use `${file.repo || 'unknown'}:${file.fileName}`)
so keys are unique across repositories; change the key prop on the FileListItem
(and any other place relying on file.fileName as a key) to this combined
identifier while keeping path={file.fileName} and repoName={file.repo} intact.

In
`@packages/web/src/features/chat/components/chatThread/tools/listCommitsToolComponent.tsx`:
- Around line 26-28: The onCopy handler currently calls
navigator.clipboard.writeText(...) without awaiting or catching rejections;
update the onCopy definition in listCommitsToolComponent (the handler passed to
CopyIconButton) to be an async function that awaits
navigator.clipboard.writeText(part.output.output) and catches errors (e.g.,
try/catch) so it returns a boolean indicating success (true on success, false on
failure) and logs or handles the error rather than letting a rejected promise
become unhandled.

In
`@packages/web/src/features/chat/components/chatThread/tools/listReposToolComponent.tsx`:
- Around line 26-28: The current onCopy callback is synchronous (onCopy?: () =>
boolean) but clipboard.writeText is async; fix by updating the
ToolHeader/CopyIconButton contract to accept Promise<boolean> (change onCopy to
() => boolean | Promise<boolean>), update CopyIconButton to await the result,
handle errors and reflect success/failure before showing UI feedback, and then
change callers (e.g., listReposToolComponent, listCommitsToolComponent,
searchCodeToolComponent, readFileToolComponent, listTreeToolComponent,
findSymbolReferencesToolComponent, findSymbolDefinitionsToolComponent) to return
a Promise<boolean> that resolves true on successful
navigator.clipboard.writeText(...) and false on rejection so success is
determined after the async operation completes.

In
`@packages/web/src/features/chat/components/chatThread/tools/searchCodeToolComponent.tsx`:
- Around line 34-36: The onCopy callback currently returns true immediately
while navigator.clipboard.writeText(...) is async; update the handler used in
ToolHeader/CopyIconButton (onCopy) to either accept and return a
Promise<boolean> (change signature to onCopy?: () => Promise<boolean>) or wrap
the clipboard call to await it and return a boolean based on success/failure
(i.e., await navigator.clipboard.writeText(...); return true on success, catch
and return false on failure). Apply this same fix consistently in
searchCodeToolComponent.tsx and the other tool components listed (readFile,
listCommits, listTree, listRepos, findSymbolDefinitions, findSymbolReferences)
so CopyIconButton only shows success after the clipboard operation actually
completes.

In `@packages/web/src/features/mcp/server.ts`:
- Around line 12-26: The MCP server is missing registration for the
findSymbolDefinitionsDefinition tool, causing MCP clients to lack parity with
chat tools; in createMcpServer() import findSymbolDefinitionsDefinition from the
tools module and call registerMcpTool(server, findSymbolDefinitionsDefinition)
alongside the other registerMcpTool(...) calls so the MCP server exposes the
same symbol-finding tool as the chat surface.

In `@packages/web/src/features/mcp/utils.ts`:
- Line 3: Replace the runtime import of ListTreeEntry with a type-only import to
break the circular dependency: change the import that currently reads "import {
ListTreeEntry } from '@/features/tools/listTree'" to a type-only import "import
type { ListTreeEntry } from '@/features/tools/listTree'"; keep all usages of the
ListTreeEntry type unchanged (e.g., in functions or type annotations within
mcp/utils.ts) so only the import form is modified.

In `@packages/web/src/features/tools/findSymbolDefinitions.ts`:
- Around line 34-39: The symbol lookup is treating `symbol` as a regex pattern;
to fix it, escape the identifier before interpolating into the regex. In the
helper in codeNav/api.ts (the function that currently builds
"\\b${symbolName}\\b") wrap `symbolName` with `escapeStringRegexp(symbolName)`
(and import escapeStringRegexp if not present) so symbols like "foo.bar", "C++",
or "operator[]" are matched literally; this mirrors how `repoName` is handled in
the same helper and ensures findSearchBasedSymbolDefinitions /
findSymbolDefinitions use a literal-symbol match.

In `@packages/web/src/features/tools/listTree.ts`:
- Around line 63-65: The code enqueues directories into queue/queuedPaths before
applying includeDirectories, causing very wide levels passed to getTree; modify
listTree traversal (references: queue, queuedPaths, seenEntries, getTree,
includeDirectories) to batch or cap currentLevelPaths before each getTree call
so you never call getTree with an unbounded wide list: when building
currentLevelPaths from queue, partition it into chunks (or impose a max per-call
cap) and call getTree repeatedly per chunk, ensuring queuedPaths/seenEntries are
updated per-chunk and that includeDirectories filtering is applied before
enqueuing children so queued directory growth is limited independently from
entries length.
- Around line 77-81: The code calls getTree and then later treats a missing or
non-tree node as an empty directory; instead, after each getTree call (the one
using currentLevelPaths.filter(Boolean) and the later getTree at lines 89-91),
check the resolved node for the requested path (currentNode) and if the original
path was provided but currentNode is falsy or currentNode.type !== 'tree', throw
or return an explicit error (e.g., throw new Error(`path "${path}" not found or
is not a directory`)) so invalid paths fail fast rather than showing an empty
listing; update both call sites (the getTree invocation and the subsequent
getTree at 89-91) to perform this validation using the existing
treeResult/currentNode variables.

In `@packages/web/src/features/tools/readFile.ts`:
- Around line 57-106: The payload byte-cap logic is wrong: you only measured raw
line bytes (loop over lines -> slicedLines) but you later add line-number
prefixes and footers so the final output can exceed MAX_BYTES, and you don't
handle slicedLines.length === 0 (which makes lastReadLine = startLine - 1 and a
non-advancing offset). Fix readFile.ts by building the formatted line entries
(including the `${startLine + i}: ` prefixes and newline separators) and measure
Buffer.byteLength of the full output pieces (header, joined formatted lines, and
footer) against MAX_BYTES before committing a line to slicedLines; when slicing
would exceed MAX_BYTES set truncatedByBytes and stop. Also special-case
slicedLines.length === 0 to set lastReadLine = startLine - 1 (or better: leave
endLine = startLine - 1) and set nextOffset = startLine (or nextOffset =
startLine if nothing consumed) so the continuation offset advances or is stable,
and ensure metadata.isTruncated is true when the footer indicates truncation.

In `@packages/web/src/features/tools/searchCode.ts`:
- Around line 83-97: The current code appends raw filter strings for repos,
languages, filepaths, and ref into the query (see variables repos, languages,
filepaths, ref and the use of escapeStringRegexp) which allows spaces or
operators to break out of the intended filter; change this to build filters
structurally by creating a filter array (e.g., searchFilters) and pushing
properly escaped filter tokens for each type instead of string-splicing into
query, or at minimum run a filter-specific escaping/quoting function on each
language, filepath and ref value before joining; update the logic that currently
builds query with `repo:...`, `lang:...`, `file:...`, `rev:...` so it constructs
and joins safe filter tokens and then appends them to the main query passed to
search().
- Around line 44-48: The schema for limit currently allows negative and
non-integer values which then flow into options.matches; update the zod schema
and normalization so limit becomes a non-negative integer count before use:
change the z.number() for limit to enforce integers and a minimum (e.g.
.int().min(0)) or coerce/normalize by taking Math.floor and clamping between 0
and DEFAULT_SEARCH_LIMIT, then assign that normalized value to options.matches
(referencing the existing limit symbol and options.matches) so
decimals/negatives cannot reach the search call.

---

Nitpick comments:
In @.env.development:
- Line 80: The DEBUG_WRITE_CHAT_MESSAGES_TO_FILE debug override should not be
committed in the shared .env.development; remove the line
"DEBUG_WRITE_CHAT_MESSAGES_TO_FILE=true" from .env.development and add the same
entry to .env.development.local instead, ensuring .env.development.local is
listed in .gitignore so this environment-specific override remains local; update
any docs or README that mention local env overrides if needed.

In `@packages/web/src/features/chat/components/chatThread/tools/shared.tsx`:
- Around line 126-134: The wrapper div around CopyIconButton prevents keyboard
activation because it intercepts click events with onClick={(e) =>
e.stopPropagation()} and is not keyboard-accessible; remove the non-interactive
wrapper or convert it to a keyboard-focusable element (e.g., a button or
role="button" with onKeyDown handling) and ensure it calls e.stopPropagation()
for both click and keyboard activation, or simply move the stopPropagation logic
into CopyIconButton so the wrapper is not needed; update references around the
onCopy prop and CopyIconButton usage to keep identical visual behavior while
restoring keyboard event handling and removing the eslint-disable comments.

In `@packages/web/src/features/mcp/server.ts`:
- Around line 28-41: Move the inline "list_language_models" tool registration
into the shared tool definitions so MCP wiring is consistent: create a tool
definition in the features/tools module (similar to other tools registered via
registerMcpTool) that exposes the same behavior (calls
getConfiguredLanguageModelsInfo and returns the JSON text payload), export it,
and then replace the inline server.registerTool("list_language_models", ...)
call with a registerMcpTool import/registration of that shared tool; ensure the
exported tool name and behavior match the original list_language_models
implementation and reuse getConfiguredLanguageModelsInfo for fetching models.

In `@packages/web/src/features/tools/index.ts`:
- Around line 1-8: The barrel file named index.ts in the tools feature should be
renamed to a camelCase filename (e.g., toolsExports.ts or toolsBarrel.ts) to
follow project naming rules; update the filename that currently re-exports
readFile, listCommits, listRepos, searchCode, findSymbolReferences,
findSymbolDefinitions, listTree, and adapters, and then update all import sites
that currently import from the tools directory (which rely on the index.ts
implicit resolution) to import from the new camelCase module name (ensure
references to exports like readFile, listRepos, searchCode, etc. remain
unchanged).

In `@packages/web/src/features/tools/listTree.ts`:
- Around line 3-5: listTreeDefinition imports generic helpers from
"@/features/mcp/utils" which inverts module boundaries; move the helpers
buildTreeNodeIndex, joinTreePath, normalizeTreePath, and sortTreeEntries into a
neutral shared module (e.g., a new shared utils under features/tools or a common
utils package) and update listTreeDefinition to import them from that shared
location instead of "@/features/mcp/utils"; ensure exports and any internal
types/signatures remain unchanged and update other references to these helpers
to the new module.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49e1a624-6438-4b46-a456-c43652448afa

📥 Commits

Reviewing files that changed from the base of the PR and between e29056b and 8106033.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (42)
  • .env.development
  • packages/shared/src/logger.ts
  • packages/web/next.config.mjs
  • packages/web/package.json
  • packages/web/src/features/chat/agent.ts
  • packages/web/src/features/chat/components/chatThread/chatThreadListItem.tsx
  • packages/web/src/features/chat/components/chatThread/detailsCard.tsx
  • packages/web/src/features/chat/components/chatThread/tools/findSymbolDefinitionsToolComponent.tsx
  • packages/web/src/features/chat/components/chatThread/tools/findSymbolReferencesToolComponent.tsx
  • packages/web/src/features/chat/components/chatThread/tools/listCommitsToolComponent.tsx
  • packages/web/src/features/chat/components/chatThread/tools/listReposToolComponent.tsx
  • packages/web/src/features/chat/components/chatThread/tools/listTreeToolComponent.tsx
  • packages/web/src/features/chat/components/chatThread/tools/readFileToolComponent.tsx
  • packages/web/src/features/chat/components/chatThread/tools/searchCodeToolComponent.tsx
  • packages/web/src/features/chat/components/chatThread/tools/shared.tsx
  • packages/web/src/features/chat/constants.ts
  • packages/web/src/features/chat/logger.ts
  • packages/web/src/features/chat/tools.ts
  • packages/web/src/features/chat/types.ts
  • packages/web/src/features/chat/utils.ts
  • packages/web/src/features/mcp/server.ts
  • packages/web/src/features/mcp/types.ts
  • packages/web/src/features/mcp/utils.ts
  • packages/web/src/features/tools/adapters.ts
  • packages/web/src/features/tools/findSymbolDefinitions.ts
  • packages/web/src/features/tools/findSymbolDefinitions.txt
  • packages/web/src/features/tools/findSymbolReferences.ts
  • packages/web/src/features/tools/findSymbolReferences.txt
  • packages/web/src/features/tools/index.ts
  • packages/web/src/features/tools/listCommits.ts
  • packages/web/src/features/tools/listCommits.txt
  • packages/web/src/features/tools/listRepos.ts
  • packages/web/src/features/tools/listRepos.txt
  • packages/web/src/features/tools/listTree.ts
  • packages/web/src/features/tools/listTree.txt
  • packages/web/src/features/tools/logger.ts
  • packages/web/src/features/tools/readFile.ts
  • packages/web/src/features/tools/readFile.txt
  • packages/web/src/features/tools/searchCode.ts
  • packages/web/src/features/tools/searchCode.txt
  • packages/web/src/features/tools/types.ts
  • packages/web/types.d.ts
💤 Files with no reviewable changes (1)
  • packages/web/src/features/chat/constants.ts

NODE_ENV=development
# SOURCEBOT_TENANCY_MODE=single

DEBUG_WRITE_CHAT_MESSAGES_TO_FILE=true No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a trailing newline at EOF.

dotenv-linter flagged EndingBlankLine on Line 80; please add a final blank line to satisfy the linter.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 80-80: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.development at line 80, The file ends without a trailing newline which
triggers dotenv-linter's EndingBlankLine; open the .env.development file and add
a final blank line (newline character) after the last entry
DEBUG_WRITE_CHAT_MESSAGES_TO_FILE=true so the file ends with a newline
character.

Comment on lines +66 to +73
turbopack: {
rules: {
'*.txt': {
loaders: ['raw-loader'],
as: '*.js',
},
},
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if webpack config for raw-loader exists elsewhere in the codebase
rg -n "raw-loader" --type=js --type=ts -g '!node_modules'

# Check for any existing webpack config modifications
ast-grep --pattern 'webpack: ($config) => { $$$ }'

Repository: sourcebot-dev/sourcebot

Length of output: 49


🏁 Script executed:

# Examine the next.config.mjs file
cat -n packages/web/next.config.mjs

Repository: sourcebot-dev/sourcebot

Length of output: 4920


🏁 Script executed:

# Search for .txt file imports
rg "import\s+.*\.txt" --type=js --type=ts --type=jsx --type=tsx

# Also check for require statements with .txt
rg "require\(.*\.txt" --type=js --type=ts

# Check if raw-loader is in package.json dependencies
cat packages/web/package.json | grep -A 5 -B 5 "raw-loader"

Repository: sourcebot-dev/sourcebot

Length of output: 427


🏁 Script executed:

# Search for any .txt imports without type restrictions
rg "\.txt['\"]" 

# Also list any .txt files in the repo
fd "\.txt$" packages/web --type f

Repository: sourcebot-dev/sourcebot

Length of output: 2295


🏁 Script executed:

# Check if there's any webpack modification in the entire next.config.mjs
grep -n "webpack" packages/web/next.config.mjs

# Check if there are any other config files that might handle webpack
fd "webpack" packages/web --type f

# Check if there's anything in tsconfig or other config files
cat packages/web/tsconfig.json | head -30

Repository: sourcebot-dev/sourcebot

Length of output: 616


🏁 Script executed:

# Check if there's a build or test script that might have caught this
cat packages/web/package.json | grep -A 10 '"scripts"'

# Also verify if there are any error suppression configs that might hide this
rg "suppress\|ignore.*raw-loader\|webpack.*rule" packages/web

Repository: sourcebot-dev/sourcebot

Length of output: 834


Add webpack configuration for raw-loader to fix production builds.

The turbopack config at lines 66–73 only applies to next dev --turbopack. Production builds use webpack via next build, which has no rule for .txt files and will fail when importing them. This affects 7 files that import .txt files in production code.

🔧 Proposed fix
     turbopack: {
         rules: {
             '*.txt': {
                 loaders: ['raw-loader'],
                 as: '*.js',
             },
         },
     },
+
+    webpack: (config) => {
+        config.module.rules.push({
+            test: /\.txt$/,
+            use: 'raw-loader',
+        });
+        return config;
+    },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
turbopack: {
rules: {
'*.txt': {
loaders: ['raw-loader'],
as: '*.js',
},
},
},
turbopack: {
rules: {
'*.txt': {
loaders: ['raw-loader'],
as: '*.js',
},
},
},
webpack: (config) => {
config.module.rules.push({
test: /\.txt$/,
use: 'raw-loader',
});
return config;
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/next.config.mjs` around lines 66 - 73, The turbopack rule for
'*.txt' only covers dev; add an equivalent webpack rule in the Next.js config's
webpack property so production builds (next build) use raw-loader for .txt
imports: update the exported config to include a webpack function that pushes a
module.rules entry matching /\.txt$/ and uses 'raw-loader' (mirroring the
turbopack loaders setting), and ensure the loader is installed; locate the
existing turbopack block (the turbopack.rules '*.txt' entry) and add the
corresponding webpack.module.rules rule in the same next.config.mjs config
object.

Comment on lines +55 to +60
{part.output.metadata.files.map((file) => {
return (
<FileListItem
key={file.fileName}
path={file.fileName}
repoName={file.repository}
repoName={file.repo}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and read the file to see the actual implementation
find . -type f -name "findSymbolDefinitionsToolComponent.tsx" | head -5

Repository: sourcebot-dev/sourcebot

Length of output: 166


🏁 Script executed:

# Check if the file exists and read it
cat -n packages/web/src/features/chat/components/chatThread/tools/findSymbolDefinitionsToolComponent.tsx | head -100

Repository: sourcebot-dev/sourcebot

Length of output: 3841


🏁 Script executed:

# Also check the structure around lines 55-60 specifically
sed -n '40,70p' packages/web/src/features/chat/components/chatThread/tools/findSymbolDefinitionsToolComponent.tsx

Repository: sourcebot-dev/sourcebot

Length of output: 1583


🏁 Script executed:

# Find the type definition for FindSymbolDefinitionsToolUIPart
rg "FindSymbolDefinitionsToolUIPart" --type ts --type tsx -A 5 | head -50

Repository: sourcebot-dev/sourcebot

Length of output: 94


🏁 Script executed:

# Search for where this tool is called or where the metadata.files structure is defined
rg "metadata.*files" --type ts --type tsx -B 3 -A 3 | head -60

Repository: sourcebot-dev/sourcebot

Length of output: 94


🏁 Script executed:

# Look for the actual tool implementation that generates these results
fd -e ts -e tsx | xargs grep -l "FindSymbolDefinitions" | grep -v node_modules

Repository: sourcebot-dev/sourcebot

Length of output: 330


🏁 Script executed:

# Check the tools.ts file for type definitions
cat packages/web/src/features/chat/tools.ts

Repository: sourcebot-dev/sourcebot

Length of output: 1748


🏁 Script executed:

# Check the findSymbolDefinitions.ts file
cat packages/web/src/features/tools/findSymbolDefinitions.ts

Repository: sourcebot-dev/sourcebot

Length of output: 2167


🏁 Script executed:

# Check the FindSymbolFile type definition
rg "type FindSymbolFile" packages/web/src/features/tools/ -A 10

Repository: sourcebot-dev/sourcebot

Length of output: 901


🏁 Script executed:

# Also check the actual implementation to see if multi-repo results are possible
rg "findSearchBasedSymbolDefinitions" -A 20 packages/web/src/features/ | head -60

Repository: sourcebot-dev/sourcebot

Length of output: 5233


🏁 Script executed:

# Let's verify if the search can actually return multi-repo results
rg "FindRelatedSymbols" packages/web/src/features/codeNav/ -A 5 | head -30

Repository: sourcebot-dev/sourcebot

Length of output: 2272


🏁 Script executed:

# Check if there's any filtering or scope limiting that prevents multi-repo results
rg "repository\|repositories" packages/web/src/features/codeNav/api.ts -B 2 -A 2

Repository: sourcebot-dev/sourcebot

Length of output: 49


🏁 Script executed:

# Let's look at the full API implementation to understand multi-repo behavior
cat packages/web/src/features/codeNav/api.ts | head -100

Repository: sourcebot-dev/sourcebot

Length of output: 3386


🏁 Script executed:

# Check the response schema to see if it includes repository field
cat packages/web/src/features/codeNav/types.ts

Repository: sourcebot-dev/sourcebot

Length of output: 1284


Use a repo-qualified key for definition hits.

file.fileName alone is not unique when results span multiple repositories. Since repoName is optional and the search can return matches across all repositories, files like src/index.ts will collide across different repos. This causes React to reuse the wrong list item DOM nodes. Combine both repo and fileName in the key to ensure uniqueness.

Suggested fix
                                     {part.output.metadata.files.map((file) => {
                                         return (
                                             <FileListItem
-                                                key={file.fileName}
+                                                key={`${file.repo}:${file.fileName}`}
                                                 path={file.fileName}
                                                 repoName={file.repo}
                                             />
                                         )
                                     })}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{part.output.metadata.files.map((file) => {
return (
<FileListItem
key={file.fileName}
path={file.fileName}
repoName={file.repository}
repoName={file.repo}
{part.output.metadata.files.map((file) => {
return (
<FileListItem
key={`${file.repo}:${file.fileName}`}
path={file.fileName}
repoName={file.repo}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/web/src/features/chat/components/chatThread/tools/findSymbolDefinitionsToolComponent.tsx`
around lines 55 - 60, The list key for FileListItem is not unique across repos
because it uses file.fileName alone; update the map rendering in the
part.output.metadata.files loop to use a repo-qualified key (e.g., combine
file.repo and file.fileName or use `${file.repo || 'unknown'}:${file.fileName}`)
so keys are unique across repositories; change the key prop on the FileListItem
(and any other place relying on file.fileName as a key) to this combined
identifier while keeping path={file.fileName} and repoName={file.repo} intact.

Comment on lines +26 to +28
const onCopy = part.state === 'output-available' && !isServiceError(part.output)
? () => { navigator.clipboard.writeText(part.output.output); return true; }
: undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clipboard write lacks error handling.

navigator.clipboard.writeText() returns a Promise that can reject (e.g., if the document isn't focused or permissions are denied). The current code doesn't await or catch errors, which could cause unhandled promise rejections.

🔧 Proposed fix
     const onCopy = part.state === 'output-available' && !isServiceError(part.output)
-        ? () => { navigator.clipboard.writeText(part.output.output); return true; }
+        ? () => {
+            navigator.clipboard.writeText(part.output.output).catch(() => {});
+            return true;
+        }
         : undefined;

Alternatively, if CopyIconButton supports async handlers, consider returning the promise result.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onCopy = part.state === 'output-available' && !isServiceError(part.output)
? () => { navigator.clipboard.writeText(part.output.output); return true; }
: undefined;
const onCopy = part.state === 'output-available' && !isServiceError(part.output)
? () => {
navigator.clipboard.writeText(part.output.output).catch(() => {});
return true;
}
: undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/web/src/features/chat/components/chatThread/tools/listCommitsToolComponent.tsx`
around lines 26 - 28, The onCopy handler currently calls
navigator.clipboard.writeText(...) without awaiting or catching rejections;
update the onCopy definition in listCommitsToolComponent (the handler passed to
CopyIconButton) to be an async function that awaits
navigator.clipboard.writeText(part.output.output) and catches errors (e.g.,
try/catch) so it returns a boolean indicating success (true on success, false on
failure) and logs or handles the error rather than letting a rejected promise
become unhandled.

Comment on lines +26 to +28
const onCopy = part.state === 'output-available' && !isServiceError(part.output)
? () => { navigator.clipboard.writeText(part.output.output); return true; }
: undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the actual file and see the code at lines 26-28
cat -n packages/web/src/features/chat/components/chatThread/tools/listReposToolComponent.tsx | head -70

Repository: sourcebot-dev/sourcebot

Length of output: 3813


🏁 Script executed:

# Also find the ToolHeader component to understand what it expects for the onCopy prop
fd -t f "ToolHeader" packages/web/src --type typescript

Repository: sourcebot-dev/sourcebot

Length of output: 244


🏁 Script executed:

# Search for other onCopy handlers in tool components
rg "onCopy" packages/web/src/features/chat/components/chatThread/tools/ -A 3

Repository: sourcebot-dev/sourcebot

Length of output: 9182


🏁 Script executed:

rg "CopyIconButton" packages/web/src/features/chat/components/chatThread/tools/shared.tsx -A 20

Repository: sourcebot-dev/sourcebot

Length of output: 1486


🏁 Script executed:

# Find the CopyIconButton definition
fd -name "*copy*" -o -name "*button*" packages/web/src | head -20

Repository: sourcebot-dev/sourcebot

Length of output: 297


🏁 Script executed:

rg "export.*CopyIconButton" packages/web/src -A 15

Repository: sourcebot-dev/sourcebot

Length of output: 1515


The suggested fix is incompatible with the current ToolHeader/CopyIconButton architecture.

The underlying concern is valid: navigator.clipboard.writeText() is async and returning true synchronously means success is reported before the operation completes, and failures go unhandled. However, the proposed solution won't work.

The ToolHeader expects onCopy?: () => boolean; and CopyIconButton immediately calls const success = onCopy(); and checks it synchronously. Making the callback async () => Promise<boolean> would break the type contract and cause runtime errors.

The pattern also appears in listCommitsToolComponent, searchCodeToolComponent, readFileToolComponent, listTreeToolComponent, findSymbolReferencesToolComponent, and findSymbolDefinitionsToolComponent.

To fix this properly, either:

  • Refactor ToolHeader/CopyIconButton to accept and handle Promise<boolean> or async callbacks, or
  • Keep callbacks synchronous but don't rely on awaited clipboard completion for success feedback
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/web/src/features/chat/components/chatThread/tools/listReposToolComponent.tsx`
around lines 26 - 28, The current onCopy callback is synchronous (onCopy?: () =>
boolean) but clipboard.writeText is async; fix by updating the
ToolHeader/CopyIconButton contract to accept Promise<boolean> (change onCopy to
() => boolean | Promise<boolean>), update CopyIconButton to await the result,
handle errors and reflect success/failure before showing UI feedback, and then
change callers (e.g., listReposToolComponent, listCommitsToolComponent,
searchCodeToolComponent, readFileToolComponent, listTreeToolComponent,
findSymbolReferencesToolComponent, findSymbolDefinitionsToolComponent) to return
a Promise<boolean> that resolves true on successful
navigator.clipboard.writeText(...) and false on rejection so success is
determined after the async operation completes.

Comment on lines +63 to +65
const queue: Array<{ path: string; depth: number }> = [{ path: normalizedPath, depth: 0 }];
const queuedPaths = new Set<string>([normalizedPath]);
const seenEntries = new Set<string>();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Chunk wide directory levels before calling getTree.

Directories are enqueued before the includeDirectories filter runs, so includeDirectories=false can still accumulate a very large queue without increasing entries.length. The next iteration then sends that whole level to one getTree call, which can exceed argv limits and fail on wide repos. Please batch currentLevelPaths or cap queued directories independently of returned entries.

Also applies to: 69-81, 99-105

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/tools/listTree.ts` around lines 63 - 65, The code
enqueues directories into queue/queuedPaths before applying includeDirectories,
causing very wide levels passed to getTree; modify listTree traversal
(references: queue, queuedPaths, seenEntries, getTree, includeDirectories) to
batch or cap currentLevelPaths before each getTree call so you never call
getTree with an unbounded wide list: when building currentLevelPaths from queue,
partition it into chunks (or impose a max per-call cap) and call getTree
repeatedly per chunk, ensuring queuedPaths/seenEntries are updated per-chunk and
that includeDirectories filtering is applied before enqueuing children so queued
directory growth is limited independently from entries length.

Comment on lines +77 to +81
const treeResult = await getTree({
repoName: repo,
revisionName: ref,
paths: currentLevelPaths.filter(Boolean),
}, { source: context.source });
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail fast when path is missing or points to a file.

A bad path currently falls through !currentNode || currentNode.type !== 'tree' and returns an empty listing, which makes invalid input look like an empty directory instead of an error.

Suggested fix
             for (const currentPath of currentLevelPaths) {
                 const currentNode = currentPath === '' ? treeResult.tree : treeNodeIndex.get(currentPath);
-                if (!currentNode || currentNode.type !== 'tree') continue;
+                if (!currentNode) {
+                    throw new Error(`Path "${currentPath}" not found.`);
+                }
+                if (currentNode.type !== 'tree') {
+                    throw new Error(`Path "${currentPath}" is not a directory.`);
+                }
 
                 for (const child of currentNode.children) {

Also applies to: 89-91

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/tools/listTree.ts` around lines 77 - 81, The code
calls getTree and then later treats a missing or non-tree node as an empty
directory; instead, after each getTree call (the one using
currentLevelPaths.filter(Boolean) and the later getTree at lines 89-91), check
the resolved node for the requested path (currentNode) and if the original path
was provided but currentNode is falsy or currentNode.type !== 'tree', throw or
return an explicit error (e.g., throw new Error(`path "${path}" not found or is
not a directory`)) so invalid paths fail fast rather than showing an empty
listing; update both call sites (the getTree invocation and the subsequent
getTree at 89-91) to perform this validation using the existing
treeResult/currentNode variables.

Comment on lines +57 to +106
const lines = fileSource.source.split('\n');
const start = (offset ?? 1) - 1;
const end = start + Math.min(limit ?? READ_FILES_MAX_LINES, READ_FILES_MAX_LINES);

let bytes = 0;
let truncatedByBytes = false;
const slicedLines: string[] = [];
for (const raw of lines.slice(start, end)) {
const line = raw.length > MAX_LINE_LENGTH ? raw.substring(0, MAX_LINE_LENGTH) + MAX_LINE_SUFFIX : raw;
const size = Buffer.byteLength(line, 'utf-8') + (slicedLines.length > 0 ? 1 : 0);
if (bytes + size > MAX_BYTES) {
truncatedByBytes = true;
break;
}
slicedLines.push(line);
bytes += size;
}

const truncatedByLines = end < lines.length;
const startLine = (offset ?? 1);
const lastReadLine = startLine + slicedLines.length - 1;
const nextOffset = lastReadLine + 1;

let output = [
`<repo>${fileSource.repo}</repo>`,
`<path>${fileSource.path}</path>`,
'<content>\n'
].join('\n');

output += slicedLines.map((line, i) => `${startLine + i}: ${line}`).join('\n');

if (truncatedByBytes) {
output += `\n\n(Output capped at ${MAX_BYTES_LABEL}. Showing lines ${startLine}-${lastReadLine} of ${lines.length}. Use offset=${nextOffset} to continue.)`;
} else if (truncatedByLines) {
output += `\n\n(Showing lines ${startLine}-${lastReadLine} of ${lines.length}. Use offset=${nextOffset} to continue.)`;
} else {
output += `\n\n(End of file - ${lines.length} lines total)`;
}

output += `\n</content>`;

const metadata: ReadFileMetadata = {
path: fileSource.path,
repo: fileSource.repo,
language: fileSource.language,
startLine,
endLine: lastReadLine,
isTruncated: truncatedByBytes || truncatedByLines,
revision,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The truncation math doesn't match the payload you actually emit.

Lines 61-73 only count raw line bytes, but Lines 80-96 also add line-number prefixes and footer text, so the returned payload can still exceed the advertised 5KB cap. Also, any empty-slice path (for example, an offset past EOF or the first line being rejected by the byte cap) makes lastReadLine = startLine - 1, which yields invalid metadata and a non-advancing continuation offset. Please base the cap on the fully formatted payload and special-case slicedLines.length === 0.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/tools/readFile.ts` around lines 57 - 106, The
payload byte-cap logic is wrong: you only measured raw line bytes (loop over
lines -> slicedLines) but you later add line-number prefixes and footers so the
final output can exceed MAX_BYTES, and you don't handle slicedLines.length === 0
(which makes lastReadLine = startLine - 1 and a non-advancing offset). Fix
readFile.ts by building the formatted line entries (including the `${startLine +
i}: ` prefixes and newline separators) and measure Buffer.byteLength of the full
output pieces (header, joined formatted lines, and footer) against MAX_BYTES
before committing a line to slicedLines; when slicing would exceed MAX_BYTES set
truncatedByBytes and stop. Also special-case slicedLines.length === 0 to set
lastReadLine = startLine - 1 (or better: leave endLine = startLine - 1) and set
nextOffset = startLine (or nextOffset = startLine if nothing consumed) so the
continuation offset advances or is stable, and ensure metadata.isTruncated is
true when the footer indicates truncation.

Comment on lines +44 to +48
limit: z
.number()
.default(DEFAULT_SEARCH_LIMIT)
.describe(`Maximum number of matches to return (default: ${DEFAULT_SEARCH_LIMIT})`)
.optional(),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate limit as an actual count.

limit currently accepts negatives and decimals, and those values flow straight into options.matches. Since this is model-facing input, it should be normalized before the search call.

🛠️ Suggested diff
     limit: z
         .number()
+        .int()
+        .positive()
         .default(DEFAULT_SEARCH_LIMIT)
-        .describe(`Maximum number of matches to return (default: ${DEFAULT_SEARCH_LIMIT})`)
-        .optional(),
+        .describe(`Maximum number of matches to return (default: ${DEFAULT_SEARCH_LIMIT})`),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
limit: z
.number()
.default(DEFAULT_SEARCH_LIMIT)
.describe(`Maximum number of matches to return (default: ${DEFAULT_SEARCH_LIMIT})`)
.optional(),
limit: z
.number()
.int()
.positive()
.default(DEFAULT_SEARCH_LIMIT)
.describe(`Maximum number of matches to return (default: ${DEFAULT_SEARCH_LIMIT})`),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/tools/searchCode.ts` around lines 44 - 48, The
schema for limit currently allows negative and non-integer values which then
flow into options.matches; update the zod schema and normalization so limit
becomes a non-negative integer count before use: change the z.number() for limit
to enforce integers and a minimum (e.g. .int().min(0)) or coerce/normalize by
taking Math.floor and clamping between 0 and DEFAULT_SEARCH_LIMIT, then assign
that normalized value to options.matches (referencing the existing limit symbol
and options.matches) so decimals/negatives cannot reach the search call.

Comment on lines +83 to +97
if (repos.length > 0) {
query += ` (repo:${repos.map(id => escapeStringRegexp(id)).join(' or repo:')})`;
}

if (languages.length > 0) {
query += ` (lang:${languages.join(' or lang:')})`;
}

if (filepaths.length > 0) {
query += ` (file:${filepaths.join(' or file:')})`;
}

if (ref) {
query += ` (rev:${ref})`;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Build these filters structurally instead of splicing raw strings into the query syntax.

Lines 83-97 append languages, filepaths, and ref directly into a query that search() reparses. Values with spaces or query operators can change the parsed AST instead of staying inside their filter, which can broaden the search outside the intended repo/path/ref scope. Prefer constructing the filter IR directly, or at minimum use filter-specific escaping before concatenation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/tools/searchCode.ts` around lines 83 - 97, The
current code appends raw filter strings for repos, languages, filepaths, and ref
into the query (see variables repos, languages, filepaths, ref and the use of
escapeStringRegexp) which allows spaces or operators to break out of the
intended filter; change this to build filters structurally by creating a filter
array (e.g., searchFilters) and pushing properly escaped filter tokens for each
type instead of string-splicing into query, or at minimum run a filter-specific
escaping/quoting function on each language, filepath and ref value before
joining; update the logic that currently builds query with `repo:...`,
`lang:...`, `file:...`, `rev:...` so it constructs and joins safe filter tokens
and then appends them to the main query passed to search().

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/src/features/mcp/server.ts`:
- Around line 79-81: The tool definition for ask_codebase is incorrectly marked
as read-only; locate the ask_codebase tool entry (the object with annotations: {
readOnlyHint: true }) and change the annotation to reflect that it has side
effects by removing readOnlyHint or setting it to false so MCP clients won't
treat it as a read-only tool; update the annotations on the ask_codebase
definition in server.ts accordingly and ensure any related comment or metadata
reflects that it creates a persisted chat/research session (chatUrl).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8101998d-123e-4f9c-a09d-8b4f6b8a6da6

📥 Commits

Reviewing files that changed from the base of the PR and between 8106033 and 5839591.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • packages/web/src/features/chat/components/chatThread/tools/listCommitsToolComponent.tsx
  • packages/web/src/features/mcp/server.ts

Comment on lines +79 to +81
annotations: {
readOnlyHint: true,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

ask_codebase is incorrectly marked read-only.

Line 80 sets readOnlyHint: true, but this tool creates a persisted chat/research session (chatUrl), so it has side effects. This can cause incorrect tool gating in MCP clients.

Proposed fix
             annotations: {
-                readOnlyHint: true,
+                readOnlyHint: false,
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
annotations: {
readOnlyHint: true,
}
annotations: {
readOnlyHint: false,
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/mcp/server.ts` around lines 79 - 81, The tool
definition for ask_codebase is incorrectly marked as read-only; locate the
ask_codebase tool entry (the object with annotations: { readOnlyHint: true })
and change the annotation to reflect that it has side effects by removing
readOnlyHint or setting it to false so MCP clients won't treat it as a read-only
tool; update the annotations on the ask_codebase definition in server.ts
accordingly and ensure any related comment or metadata reflects that it creates
a persisted chat/research session (chatUrl).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant