Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a271c5d
wip
brendan-kellam Mar 1, 2026
337816a
wip
brendan-kellam Mar 1, 2026
0469fe0
wip
brendan-kellam Mar 3, 2026
c5adc10
wip - improve readFile
brendan-kellam Mar 3, 2026
a93b0af
Merge branch 'main' into bkellam/agent-improvements
brendan-kellam Mar 6, 2026
1b12d03
Merge branch 'main' into bkellam/agent-improvements
brendan-kellam Mar 16, 2026
5e7ab53
Merge branch 'main' into bkellam/agent-improvements
brendan-kellam Mar 17, 2026
7927a84
migrate readFile
brendan-kellam Mar 17, 2026
e9c4b3d
migrate listRepos & listCommits
brendan-kellam Mar 17, 2026
92fd313
migrate the rest
brendan-kellam Mar 18, 2026
6ab9bc7
wip
brendan-kellam Mar 18, 2026
bbbb982
wip
brendan-kellam Mar 18, 2026
7f38753
readonly hint
brendan-kellam Mar 18, 2026
f4ef924
add isIdempotent
brendan-kellam Mar 18, 2026
8106033
Merge branch 'main' into bkellam/agent-improvements
brendan-kellam Mar 18, 2026
5839591
feedback
brendan-kellam Mar 18, 2026
1204343
improve search tool by changing it's interface to look like grep
brendan-kellam Mar 18, 2026
7255033
fix SOU-569
brendan-kellam Mar 18, 2026
945f93b
wip
brendan-kellam Mar 18, 2026
dbd69a1
small improvement
brendan-kellam Mar 18, 2026
2e67a0d
improve listTree output
brendan-kellam Mar 19, 2026
290d32b
remove everything before and including answer tag
brendan-kellam Mar 19, 2026
f46b563
fix answer part detection
brendan-kellam Mar 19, 2026
768864b
plumb repo selection to grep tool
brendan-kellam Mar 19, 2026
addb243
grep prompt improvement
brendan-kellam Mar 20, 2026
b3f768d
further wip
brendan-kellam Mar 21, 2026
b4500ea
nit
brendan-kellam Mar 21, 2026
ea801f1
add path to /api/commits api
brendan-kellam Mar 21, 2026
7044321
list commits updated ui
brendan-kellam Mar 21, 2026
9a3ea1c
list repos tool
brendan-kellam Mar 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,5 @@ SOURCEBOT_TELEMETRY_DISABLED=true # Disables telemetry collection
# CONFIG_MAX_REPOS_NO_TOKEN=
NODE_ENV=development
# SOURCEBOT_TENANCY_MODE=single

DEBUG_WRITE_CHAT_MESSAGES_TO_FILE=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 | 🟡 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.

12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Added `find_symbol_definitions`, and `find_symbol_references` tools to the MCP server. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014)
- Added `list_tree` tool to the ask agent. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014)
- Added input & output token breakdown in ask details card. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014)
- Added `path` parameter to the `/api/commits` api to allow filtering commits by paths. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014)

### Fixed
- Fixed issue where ask responses would sometimes appear in the details panel while generating. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014)

### Changed
- Changed the `webUrl` property of the `/api/repos` api to return a URL rather than just a path. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014)

## [4.15.9] - 2026-03-17

### Added
Expand Down
19 changes: 19 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ Exceptions:
- Special files like `README.md`, `CHANGELOG.md`, `LICENSE`
- Next.js conventions: `page.tsx`, `layout.tsx`, `loading.tsx`, etc.

## Code Style

Always use curly braces for `if` statements, with the body on a new line — even for single-line bodies:

```ts
// Correct
if (!value) {
return;
}
if (condition) {
doSomething();
}

// Incorrect
if (!value) return;
if (!value) { return; }
if (condition) doSomething();
```

## Tailwind CSS

Use Tailwind color classes directly instead of CSS variable syntax:
Expand Down
27 changes: 13 additions & 14 deletions docs/docs/features/mcp-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -304,22 +304,19 @@ Pass the key as an `Authorization: Bearer <key>` header when connecting to the M

## Available Tools

### `search_code`
### `grep`

Searches for code that matches the provided search query as a substring by default, or as a regular expression if `useRegex` is true.
Searches for code matching a regular expression pattern across repositories, similar to `grep`/`ripgrep`. Always case-sensitive. Results are grouped by file and include line numbers.

Parameters:
| Name | Required | Description |
|:----------------------|:---------|:---------------------------------------------------------------------------------------------------------------------|
| `query` | yes | The search pattern to match against code contents. Do not escape quotes in your query. |
| `useRegex` | no | Whether to use regular expression matching. When false, substring matching is used (default: false). |
| `filterByRepos` | no | Scope the search to specific repositories. |
| `filterByLanguages` | no | Scope the search to specific languages. |
| `filterByFilepaths` | no | Scope the search to specific filepaths. |
| `caseSensitive` | no | Whether the search should be case sensitive (default: false). |
| `includeCodeSnippets` | no | Whether to include code snippets in the response (default: false). |
|:----------|:---------|:--------------------------------------------------------------------------------------------------------------|
| `pattern` | yes | The regex pattern to search for in file contents. |
| `path` | no | Directory path to scope the search to. Defaults to the repository root. |
| `include` | no | File glob pattern to include in the search (e.g. `*.ts`, `*.{ts,tsx}`). |
| `repo` | no | Repository name to search in. If not provided, searches all repositories. Use the full name including host (e.g. `github.com/org/repo`). |
| `ref` | no | Commit SHA, branch or tag name to search on. If not provided, defaults to the default branch. |
| `maxTokens` | no | The maximum number of tokens to return (default: 10000). |
| `limit` | no | Maximum number of matching files to return (default: 100). |

### `list_repos`

Expand All @@ -336,18 +333,20 @@ Parameters:

### `read_file`

Reads the source code for a given file.
Reads the source code for a given file, with optional line range control for large files.

Parameters:
| Name | Required | Description |
|:-------|:---------|:-------------------------------------------------------------------------------------------------------|
|:---------|:---------|:-------------------------------------------------------------------------------------------------------|
| `repo` | yes | The repository name. |
| `path` | yes | The path to the file. |
| `ref` | no | Commit SHA, branch or tag name to fetch the source code for. If not provided, uses the default branch. |
| `offset` | no | Line number to start reading from (1-indexed). Omit to start from the beginning. |
| `limit` | no | Maximum number of lines to read (max: 500). Omit to read up to 500 lines. |

### `list_tree`

Lists files and directories from a repository path. Can be used as a directory listing tool (`depth: 1`) or a repo-tree tool (`depth > 1`).
Lists files and directories from a repository path. Directories are shown before files at each level.

Parameters:
| Name | Required | Description |
Expand Down
9 changes: 4 additions & 5 deletions packages/shared/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,11 @@ const datadogFormat = format((info) => {
return info;
});

const humanReadableFormat = printf(({ level, message, timestamp, stack, label: _label }) => {
const humanReadableFormat = printf(({ level, message, timestamp, stack, label: _label, ...rest }) => {
const label = `[${_label}] `;
if (stack) {
return `${timestamp} ${level}: ${label}${message}\n${stack}`;
}
return `${timestamp} ${level}: ${label}${message}`;
const extras = Object.keys(rest).length > 0 ? ` ${JSON.stringify(rest)}` : '';
const base = `${timestamp} ${level}: ${label}${message}${extras}`;
return stack ? `${base}\n${stack}` : base;
});

const createLogger = (label: string) => {
Expand Down
9 changes: 8 additions & 1 deletion packages/web/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ const nextConfig = {
]
},

turbopack: {},
turbopack: {
rules: {
'*.txt': {
loaders: ['raw-loader'],
as: '*.js',
},
},
},
Comment on lines +66 to +73
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.


// @see: https://github.com/vercel/next.js/issues/58019#issuecomment-1910531929
...(process.env.NODE_ENV === 'development' ? {
Expand Down
4 changes: 4 additions & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
"escape-string-regexp": "^5.0.0",
"fast-deep-equal": "^3.1.3",
"fuse.js": "^7.0.0",
"glob-to-regexp": "^0.4.1",
"google-auth-library": "^10.1.0",
"graphql": "^16.9.0",
"http-status-codes": "^2.3.0",
Expand Down Expand Up @@ -189,6 +190,7 @@
"stripe": "^17.6.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"use-stick-to-bottom": "^1.1.3",
"usehooks-ts": "^3.1.0",
"vscode-icons-js": "^11.6.1",
"zod": "^3.25.74",
Expand All @@ -202,6 +204,7 @@
"@tanstack/eslint-plugin-query": "^5.74.7",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.0",
"@types/glob-to-regexp": "^0.4.4",
"@types/micromatch": "^4.0.9",
"@types/node": "^20",
"@types/nodemailer": "^6.4.17",
Expand All @@ -218,6 +221,7 @@
"jsdom": "^25.0.1",
"npm-run-all": "^4.1.5",
"postcss": "^8",
"raw-loader": "^4.0.2",
"react-email": "^5.1.0",
"react-grab": "^0.1.23",
"tailwindcss": "^3.4.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/app/api/(server)/commits/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const listCommitsQueryParamsSchema = z.object({
until: z.string().optional(),
author: z.string().optional(),
ref: z.string().optional(),
path: z.string().optional(),
page: z.coerce.number().int().positive().default(1),
perPage: z.coerce.number().int().positive().max(100).default(50),
});
Expand Down Expand Up @@ -57,6 +58,7 @@ export const GET = apiHandler(async (request: NextRequest): Promise<Response> =>
...(searchParams.until ? { until: searchParams.until } : {}),
...(searchParams.author ? { author: searchParams.author } : {}),
...(searchParams.ref ? { ref: searchParams.ref } : {}),
...(searchParams.path ? { path: searchParams.path } : {}),
},
});
if (linkHeader) headers.set('Link', linkHeader);
Expand Down
65 changes: 37 additions & 28 deletions packages/web/src/features/chat/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ import {
} from "ai";
import { randomUUID } from "crypto";
import _dedent from "dedent";
import { ANSWER_TAG, FILE_REFERENCE_PREFIX, toolNames } from "./constants";
import { createCodeSearchTool, findSymbolDefinitionsTool, findSymbolReferencesTool, listCommitsTool, listReposTool, readFilesTool } from "./tools";
import { ANSWER_TAG, FILE_REFERENCE_PREFIX } from "./constants";
import { findSymbolReferencesDefinition } from "@/features/tools/findSymbolReferences";
import { findSymbolDefinitionsDefinition } from "@/features/tools/findSymbolDefinitions";
import { readFileDefinition } from "@/features/tools/readFile";
import { grepDefinition } from "@/features/tools/grep";
import { Source } from "./types";
import { addLineNumbers, fileReferenceToString } from "./utils";
import { createTools } from "./tools";
import { listTreeDefinition } from "../tools";

const dedent = _dedent.withOptions({ alignValues: true });

Expand Down Expand Up @@ -198,14 +203,7 @@ const createAgentStream = async ({
providerOptions,
messages: inputMessages,
system: systemPrompt,
tools: {
[toolNames.searchCode]: createCodeSearchTool(selectedRepos),
[toolNames.readFiles]: readFilesTool,
[toolNames.findSymbolReferences]: findSymbolReferencesTool,
[toolNames.findSymbolDefinitions]: findSymbolDefinitionsTool,
[toolNames.listRepos]: listReposTool,
[toolNames.listCommits]: listCommitsTool,
},
tools: createTools({ source: 'sourcebot-ask-agent', selectedRepos }),
temperature: env.SOURCEBOT_CHAT_MODEL_TEMPERATURE,
stopWhen: [
stepCountIsGTE(env.SOURCEBOT_CHAT_MAX_STEP_COUNT),
Expand All @@ -223,39 +221,46 @@ const createAgentStream = async ({
return;
}

if (toolName === toolNames.readFiles) {
output.forEach((file) => {
if (toolName === readFileDefinition.name) {
onWriteSource({
type: 'file',
repo: output.metadata.repo,
path: output.metadata.path,
revision: output.metadata.revision,
name: output.metadata.path.split('/').pop() ?? output.metadata.path,
});
} else if (toolName === grepDefinition.name) {
output.metadata.files.forEach((file) => {
onWriteSource({
type: 'file',
language: file.language,
repo: file.repository,
repo: file.repo,
path: file.path,
revision: file.revision,
name: file.path.split('/').pop() ?? file.path,
});
});
} else if (toolName === toolNames.searchCode) {
output.files.forEach((file) => {
} else if (toolName === findSymbolDefinitionsDefinition.name || toolName === findSymbolReferencesDefinition.name) {
output.metadata.files.forEach((file) => {
onWriteSource({
type: 'file',
language: file.language,
repo: file.repository,
repo: file.repo,
path: file.fileName,
revision: file.revision,
name: file.fileName.split('/').pop() ?? file.fileName,
});
});
} else if (toolName === toolNames.findSymbolDefinitions || toolName === toolNames.findSymbolReferences) {
output.forEach((file) => {
onWriteSource({
type: 'file',
language: file.language,
repo: file.repository,
path: file.fileName,
revision: file.revision,
name: file.fileName.split('/').pop() ?? file.fileName,
} else if (toolName === listTreeDefinition.name) {
output.metadata.entries
.filter((entry) => entry.type === 'blob')
.forEach((entry) => {
onWriteSource({
type: 'file',
repo: output.metadata.repo,
path: entry.path,
revision: output.metadata.ref,
name: entry.name,
});
});
});
}
});
},
Expand Down Expand Up @@ -312,6 +317,10 @@ const createPrompt = ({
<selected_repositories>
The user has explicitly selected the following repositories for analysis:
${repos.map(repo => `- ${repo}`).join('\n')}

When calling tools that accept a \`repo\` parameter (e.g. \`read_file\`, \`list_commits\`, \`list_tree\`, \`grep\`), use these repository names exactly as listed above, including the full host prefix (e.g. \`github.com/org/repo\`).

When using \`grep\` to search across ALL selected repositories (e.g. "which repos have X?"), omit the \`repo\` parameter entirely — the tool will automatically search across all selected repositories in a single call. Do NOT call \`grep\` once per repository when a single broad search would suffice.
</selected_repositories>
` : ''}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { AnswerCard } from './answerCard';
import { DetailsCard } from './detailsCard';
import { MarkdownRenderer, REFERENCE_PAYLOAD_ATTRIBUTE } from './markdownRenderer';
import { ReferencedSourcesListView } from './referencedSourcesListView';
import { uiVisiblePartTypes } from '../../constants';
import isEqual from "fast-deep-equal/react";
import { ANSWER_TAG } from '../../constants';

interface ChatThreadListItemProps {
userMessage: SBChatMessage;
Expand Down Expand Up @@ -95,19 +95,24 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
(step) => step
// First, filter out any parts that are not text
.filter((part) => {
if (part.type !== 'text') {
return true;
if (part.type === 'text') {
return !part.text.includes(ANSWER_TAG);
}

return part.text !== answerPart?.text;
return true;
})
.filter((part) => {
return uiVisiblePartTypes.includes(part.type);
// Only include text, reasoning, and tool parts
return (
part.type === 'text' ||
part.type === 'reasoning' ||
part.type.startsWith('tool-')
)
})
)
// Then, filter out any steps that are empty
.filter(step => step.length > 0);
}, [answerPart, assistantMessage?.parts]);
}, [assistantMessage?.parts]);

// "thinking" is when the agent is generating output that is not the answer.
const isThinking = useMemo(() => {
Expand Down
Loading
Loading