chore(api): Changed the search api to return raw source (instead of base64 encoding) (#356)

This PR alters the behaviour of the search api (and all apis that depend on it) to return raw source code instead of a base64 encoding. Reasoning: we are decoding it on the client in multiple different places, so it would be beneficial to decode it in a single spot.

**Note**: This is a **breaking change** to the API surface. However, since the API surface is still unofficial/unsupported, I will roll this as a patch version change. See #101
This commit is contained in:
Brendan Kellam 2025-06-17 15:58:04 -07:00 committed by GitHub
parent 22d548e171
commit 1d95e82b95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 25 additions and 47 deletions

View file

@ -7,10 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
<!-- @NOTE: this release includes a API change that affects the MCP package (@sourcebot/mcp). On release, bump the MCP package's version and delete this message. -->
### Fixed ### Fixed
- Delete account join request when redeeming an invite. [#352](https://github.com/sourcebot-dev/sourcebot/pull/352) - Delete account join request when redeeming an invite. [#352](https://github.com/sourcebot-dev/sourcebot/pull/352)
- Fix issue where a repository would not be included in a search context if the context was created before the repository. [#354](https://github.com/sourcebot-dev/sourcebot/pull/354) - Fix issue where a repository would not be included in a search context if the context was created before the repository. [#354](https://github.com/sourcebot-dev/sourcebot/pull/354)
### Changed
- Changed search api (and all apis that depend on it) to return raw source code instead of base64 encoded string. ([356](https://github.com/sourcebot-dev/sourcebot/pull/356)).
## [4.3.0] - 2025-06-11 ## [4.3.0] - 2025-06-11
### Added ### Added

View file

@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Changed
- Updated API client to match the latest Sourcebot release. [#356](https://github.com/sourcebot-dev/sourcebot/pull/356)
## [1.0.2] - 2025-05-28 ## [1.0.2] - 2025-05-28
### Changed ### Changed

View file

@ -8,7 +8,7 @@ import { z } from 'zod';
import { listRepos, search, getFileSource } from './client.js'; import { listRepos, search, getFileSource } from './client.js';
import { env, numberSchema } from './env.js'; import { env, numberSchema } from './env.js';
import { TextContent } from './types.js'; import { TextContent } from './types.js';
import { base64Decode, isServiceError } from './utils.js'; import { isServiceError } from './utils.js';
// Create MCP server // Create MCP server
const server = new McpServer({ const server = new McpServer({
@ -114,8 +114,7 @@ server.tool(
if (includeCodeSnippets) { if (includeCodeSnippets) {
const snippets = file.chunks.map(chunk => { const snippets = file.chunks.map(chunk => {
const content = base64Decode(chunk.content); return `\`\`\`\n${chunk.content}\n\`\`\``
return `\`\`\`\n${content}\n\`\`\``
}).join('\n'); }).join('\n');
text += `\n\n${snippets}`; text += `\n\n${snippets}`;
} }
@ -201,7 +200,7 @@ server.tool(
const content: TextContent[] = [{ const content: TextContent[] = [{
type: "text", type: "text",
text: `file: ${fileName}\nrepository: ${repoId}\nlanguage: ${response.language}\nsource:\n${base64Decode(response.source)}`, text: `file: ${fileName}\nrepository: ${repoId}\nlanguage: ${response.language}\nsource:\n${response.source}`,
}] }]
return { return {

View file

@ -1,10 +1,5 @@
import { ServiceError } from "./types.js"; import { ServiceError } from "./types.js";
// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
export const base64Decode = (base64: string): string => {
const binString = atob(base64);
return Buffer.from(Uint8Array.from(binString, (m) => m.codePointAt(0)!).buffer).toString();
}
export const isServiceError = (data: unknown): data is ServiceError => { export const isServiceError = (data: unknown): data is ServiceError => {
return typeof data === 'object' && return typeof data === 'object' &&

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { base64Decode, getCodeHostInfoForRepo, unwrapServiceError } from "@/lib/utils"; import { getCodeHostInfoForRepo, unwrapServiceError } from "@/lib/utils";
import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams"; import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getFileSource } from "@/features/search/fileSourceApi"; import { getFileSource } from "@/features/search/fileSourceApi";
@ -88,7 +88,7 @@ export const CodePreviewPanel = () => {
</div> </div>
<Separator /> <Separator />
<PureCodePreviewPanel <PureCodePreviewPanel
source={base64Decode(fileSourceResponse.source)} source={fileSourceResponse.source}
language={fileSourceResponse.language} language={fileSourceResponse.language}
repoName={repoName} repoName={repoName}
path={path} path={path}

View file

@ -7,7 +7,6 @@ import { useDomain } from "@/hooks/useDomain";
import { SymbolIcon } from "@radix-ui/react-icons"; import { SymbolIcon } from "@radix-ui/react-icons";
import { SetStateAction, Dispatch, useMemo } from "react"; import { SetStateAction, Dispatch, useMemo } from "react";
import { getFileSource } from "@/features/search/fileSourceApi"; import { getFileSource } from "@/features/search/fileSourceApi";
import { base64Decode } from "@/lib/utils";
import { unwrapServiceError } from "@/lib/utils"; import { unwrapServiceError } from "@/lib/utils";
interface CodePreviewPanelProps { interface CodePreviewPanelProps {
@ -41,10 +40,8 @@ export const CodePreviewPanel = ({
}, domain) }, domain)
), ),
select: (data) => { select: (data) => {
const decodedSource = base64Decode(data.source);
return { return {
content: decodedSource, content: data.source,
filepath: previewedFile.fileName.text, filepath: previewedFile.fileName.text,
matches: previewedFile.chunks, matches: previewedFile.chunks,
link: previewedFile.webUrl, link: previewedFile.webUrl,

View file

@ -1,8 +1,7 @@
'use client'; 'use client';
import { useCallback, useMemo } from "react"; import { useCallback } from "react";
import { SearchResultFile, SearchResultChunk } from "@/features/search/types"; import { SearchResultFile, SearchResultChunk } from "@/features/search/types";
import { base64Decode } from "@/lib/utils";
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter"; import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
@ -17,17 +16,12 @@ export const FileMatch = ({
file, file,
onOpen: _onOpen, onOpen: _onOpen,
}: FileMatchProps) => { }: FileMatchProps) => {
const content = useMemo(() => {
return base64Decode(match.content);
}, [match.content]);
const onOpen = useCallback((isCtrlKeyPressed: boolean) => { const onOpen = useCallback((isCtrlKeyPressed: boolean) => {
const startLineNumber = match.contentStart.lineNumber; const startLineNumber = match.contentStart.lineNumber;
const endLineNumber = content.trimEnd().split('\n').length + startLineNumber - 1; const endLineNumber = match.content.trimEnd().split('\n').length + startLineNumber - 1;
_onOpen(startLineNumber, endLineNumber, isCtrlKeyPressed); _onOpen(startLineNumber, endLineNumber, isCtrlKeyPressed);
}, [content, match.contentStart.lineNumber, _onOpen]); }, [match.content, match.contentStart.lineNumber, _onOpen]);
// If it's just the title, don't show a code preview // If it's just the title, don't show a code preview
if (match.matchRanges.length === 0) { if (match.matchRanges.length === 0) {
@ -57,7 +51,7 @@ export const FileMatch = ({
lineNumbersOffset={match.contentStart.lineNumber} lineNumbersOffset={match.contentStart.lineNumber}
renderWhitespace={true} renderWhitespace={true}
> >
{content} {match.content}
</LightweightCodeHighlighter> </LightweightCodeHighlighter>
</div> </div>
); );

View file

@ -5,7 +5,6 @@ import { PathHeader } from "@/app/[domain]/components/pathHeader";
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter"; import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
import { FindRelatedSymbolsResponse } from "@/features/codeNav/types"; import { FindRelatedSymbolsResponse } from "@/features/codeNav/types";
import { RepositoryInfo, SourceRange } from "@/features/search/types"; import { RepositoryInfo, SourceRange } from "@/features/search/types";
import { base64Decode } from "@/lib/utils";
import { useMemo, useRef } from "react"; import { useMemo, useRef } from "react";
import useCaptureEvent from "@/hooks/useCaptureEvent"; import useCaptureEvent from "@/hooks/useCaptureEvent";
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
@ -155,10 +154,6 @@ const ReferenceListItem = ({
onClick, onClick,
onMouseEnter, onMouseEnter,
}: ReferenceListItemProps) => { }: ReferenceListItemProps) => {
const decodedLineContent = useMemo(() => {
return base64Decode(lineContent);
}, [lineContent]);
const highlightRanges = useMemo(() => [range], [range]); const highlightRanges = useMemo(() => [range], [range]);
return ( return (
@ -174,7 +169,7 @@ const ReferenceListItem = ({
lineNumbersOffset={range.start.lineNumber} lineNumbersOffset={range.start.lineNumber}
renderWhitespace={false} renderWhitespace={false}
> >
{decodedLineContent} {lineContent}
</LightweightCodeHighlighter> </LightweightCodeHighlighter>
</div> </div>
) )

View file

@ -3,7 +3,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter"; import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
import { useMemo } from "react"; import { useMemo } from "react";
import { SourceRange } from "@/features/search/types"; import { SourceRange } from "@/features/search/types";
import { base64Decode } from "@/lib/utils";
interface SymbolDefinitionPreviewProps { interface SymbolDefinitionPreviewProps {
symbolDefinition: { symbolDefinition: {
@ -21,10 +20,6 @@ export const SymbolDefinitionPreview = ({
const { lineContent, language, range } = symbolDefinition; const { lineContent, language, range } = symbolDefinition;
const highlightRanges = useMemo(() => [range], [range]); const highlightRanges = useMemo(() => [range], [range]);
const decodedLineContent = useMemo(() => {
return base64Decode(lineContent);
}, [lineContent]);
return ( return (
<div className="flex flex-col gap-2 mb-2"> <div className="flex flex-col gap-2 mb-2">
<Tooltip <Tooltip
@ -55,7 +50,7 @@ export const SymbolDefinitionPreview = ({
lineNumbersOffset={range.start.lineNumber} lineNumbersOffset={range.start.lineNumber}
renderWhitespace={false} renderWhitespace={false}
> >
{decodedLineContent} {lineContent}
</LightweightCodeHighlighter> </LightweightCodeHighlighter>
</div> </div>
) )

View file

@ -1,7 +1,6 @@
import { sourcebot_context, sourcebot_pr_payload } from "@/features/agents/review-agent/types"; import { sourcebot_context, sourcebot_pr_payload } from "@/features/agents/review-agent/types";
import { getFileSource } from "@/features/search/fileSourceApi"; import { getFileSource } from "@/features/search/fileSourceApi";
import { fileSourceResponseSchema } from "@/features/search/schemas"; import { fileSourceResponseSchema } from "@/features/search/schemas";
import { base64Decode } from "@/lib/utils";
import { isServiceError } from "@/lib/utils"; import { isServiceError } from "@/lib/utils";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
import { createLogger } from "@sourcebot/logger"; import { createLogger } from "@sourcebot/logger";
@ -24,7 +23,7 @@ export const fetchFileContent = async (pr_payload: sourcebot_pr_payload, filenam
} }
const fileSourceResponse = fileSourceResponseSchema.parse(response); const fileSourceResponse = fileSourceResponseSchema.parse(response);
const fileContent = base64Decode(fileSourceResponse.source); const fileContent = fileSourceResponse.source;
const fileContentContext: sourcebot_context = { const fileContentContext: sourcebot_context = {
type: "file_content", type: "file_content",

View file

@ -10,6 +10,7 @@ import { SearchRequest, SearchResponse, SourceRange } from "./types";
import { OrgRole, Repo } from "@sourcebot/db"; import { OrgRole, Repo } from "@sourcebot/db";
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
import { sew, withAuth, withOrgMembership } from "@/actions"; import { sew, withAuth, withOrgMembership } from "@/actions";
import { base64Decode } from "@sourcebot/shared";
// List of supported query prefixes in zoekt. // List of supported query prefixes in zoekt.
// @see : https://github.com/sourcebot-dev/zoekt/blob/main/query/parse.go#L417 // @see : https://github.com/sourcebot-dev/zoekt/blob/main/query/parse.go#L417
@ -264,7 +265,7 @@ export const search = async ({ query, matches, contextLines, whole }: SearchRequ
.filter((chunk) => !chunk.FileName) // Filter out filename chunks. .filter((chunk) => !chunk.FileName) // Filter out filename chunks.
.map((chunk) => { .map((chunk) => {
return { return {
content: chunk.Content, content: base64Decode(chunk.Content),
matchRanges: chunk.Ranges.map((range) => ({ matchRanges: chunk.Ranges.map((range) => ({
start: { start: {
byteOffset: range.Start.ByteOffset, byteOffset: range.Start.ByteOffset,
@ -295,7 +296,7 @@ export const search = async ({ query, matches, contextLines, whole }: SearchRequ
} }
}), }),
branches: file.Branches, branches: file.Branches,
content: file.Content, content: file.Content ? base64Decode(file.Content) : undefined,
} }
}).filter((file) => file !== undefined) ?? []; }).filter((file) => file !== undefined) ?? [];

View file

@ -301,12 +301,6 @@ export const isServiceError = (data: unknown): data is ServiceError => {
'message' in data; 'message' in data;
} }
// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
export const base64Decode = (base64: string): string => {
const binString = atob(base64);
return Buffer.from(Uint8Array.from(binString, (m) => m.codePointAt(0)!).buffer).toString();
}
// @see: https://stackoverflow.com/a/65959350/23221295 // @see: https://stackoverflow.com/a/65959350/23221295
export const isDefined = <T>(arg: T | null | undefined): arg is T extends null | undefined ? never : T => { export const isDefined = <T>(arg: T | null | undefined): arg is T extends null | undefined ? never : T => {
return arg !== null && arg !== undefined; return arg !== null && arg !== undefined;