Skip to content

Commit 4bab982

Browse files
glob tool
1 parent e4f256c commit 4bab982

File tree

12 files changed

+439
-120
lines changed

12 files changed

+439
-120
lines changed

packages/web/src/features/chat/agent.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { grepDefinition } from "@/features/tools/grep";
2222
import { Source } from "./types";
2323
import { addLineNumbers, fileReferenceToString } from "./utils";
2424
import { createTools } from "./tools";
25-
import { listTreeDefinition } from "../tools";
25+
import { globDefinition, listTreeDefinition } from "../tools";
2626

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

@@ -261,6 +261,16 @@ const createAgentStream = async ({
261261
name: entry.name,
262262
});
263263
});
264+
} else if (toolName === globDefinition.name) {
265+
output.metadata.files.forEach((file) => {
266+
onWriteSource({
267+
type: 'file',
268+
repo: file.repo,
269+
path: file.path,
270+
revision: file.revision,
271+
name: file.path.split('/').pop() ?? file.path,
272+
});
273+
});
264274
}
265275
});
266276
},

packages/web/src/features/chat/components/chatThread/detailsCard.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { SearchScopeIcon } from '../searchScopeIcon';
1717
import { MarkdownRenderer } from './markdownRenderer';
1818
import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinitionsToolComponent';
1919
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
20+
import { GlobToolComponent } from './tools/globToolComponent';
2021
import { GrepToolComponent } from './tools/grepToolComponent';
2122
import { ListCommitsToolComponent } from './tools/listCommitsToolComponent';
2223
import { ListReposToolComponent } from './tools/listReposToolComponent';
@@ -243,6 +244,15 @@ export const StepPartRenderer = ({ part }: { part: SBChatMessagePart }) => {
243244
{(output) => <GrepToolComponent {...output} />}
244245
</ToolOutputGuard>
245246
)
247+
case 'tool-glob':
248+
return (
249+
<ToolOutputGuard
250+
part={part}
251+
loadingText="Searching files..."
252+
>
253+
{(output) => <GlobToolComponent {...output} />}
254+
</ToolOutputGuard>
255+
)
246256
case 'tool-find_symbol_definitions':
247257
return (
248258
<ToolOutputGuard
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client';
2+
3+
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
4+
import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils";
5+
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
6+
import Link from "next/link";
7+
8+
type FileInfo = {
9+
path: string;
10+
name: string;
11+
repo: string;
12+
revision: string;
13+
};
14+
15+
export const FileRow = ({ file }: { file: FileInfo }) => {
16+
const dir = file.path.includes('/')
17+
? file.path.split('/').slice(0, -1).join('/')
18+
: '';
19+
20+
const href = getBrowsePath({
21+
repoName: file.repo,
22+
revisionName: file.revision,
23+
path: file.path,
24+
pathType: 'blob',
25+
domain: SINGLE_TENANT_ORG_DOMAIN,
26+
});
27+
28+
return (
29+
<Link
30+
href={href}
31+
className="flex items-center gap-2 px-3 py-1 hover:bg-accent transition-colors"
32+
>
33+
<VscodeFileIcon fileName={file.name} className="flex-shrink-0" />
34+
<span className="text-sm font-medium flex-shrink-0">{file.name}</span>
35+
{dir && (
36+
<>
37+
<span className="text-xs text-muted-foreground flex-shrink-0">·</span>
38+
<span className="block text-xs text-muted-foreground truncate-start flex-1"><span>{dir}</span></span>
39+
</>
40+
)}
41+
</Link>
42+
);
43+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use client';
2+
3+
import { GlobFile, GlobMetadata, ToolResult } from "@/features/tools";
4+
import { useMemo } from "react";
5+
import { RepoBadge } from "./repoBadge";
6+
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
7+
import { Separator } from "@/components/ui/separator";
8+
import { RepoHeader } from "./repoHeader";
9+
import { FileRow } from "./fileRow";
10+
11+
export const GlobToolComponent = (output: ToolResult<GlobMetadata>) => {
12+
const stats = useMemo(() => {
13+
const { fileCount, repoCount } = output.metadata;
14+
const fileLabel = `${fileCount} ${fileCount === 1 ? 'file' : 'files'}`;
15+
if (fileCount === 0 || repoCount <= 1) {
16+
return fileLabel;
17+
}
18+
const repoLabel = `${repoCount} ${repoCount === 1 ? 'repo' : 'repos'}`;
19+
return `${fileLabel} · ${repoLabel}`;
20+
}, [output]);
21+
22+
const filesByRepo = useMemo(() => {
23+
const groups = new Map<string, GlobFile[]>();
24+
for (const file of output.metadata.files) {
25+
if (!groups.has(file.repo)) {
26+
groups.set(file.repo, []);
27+
}
28+
groups.get(file.repo)!.push(file);
29+
}
30+
return groups;
31+
}, [output.metadata.files]);
32+
33+
return (
34+
<HoverCard openDelay={200}>
35+
<div className="flex items-center gap-2 select-none cursor-default">
36+
<div className="flex-1 min-w-0">
37+
<HoverCardTrigger asChild>
38+
<span className="flex items-center gap-1.5 text-sm text-muted-foreground min-w-0 overflow-hidden">
39+
<span className="flex-shrink-0">Searched files</span>
40+
<code className="text-xs bg-muted px-1 py-0.5 rounded truncate text-foreground max-w-[300px]">{output.metadata.pattern}</code>
41+
{output.metadata.inputRepo && <><span className="flex-shrink-0">in</span><RepoBadge repo={output.metadata.inputRepo} /></>}
42+
</span>
43+
</HoverCardTrigger>
44+
</div>
45+
<span className="text-xs text-muted-foreground flex-shrink-0">{stats}</span>
46+
<Separator orientation="vertical" className="h-3 flex-shrink-0" />
47+
</div>
48+
{output.metadata.files.length > 0 && (
49+
<HoverCardContent align="start" className="w-96 p-0">
50+
<div className="overflow-y-auto max-h-72">
51+
{Array.from(filesByRepo.entries()).map(([repo, files]) => (
52+
<div key={repo}>
53+
<RepoHeader
54+
repo={output.metadata.repoInfoMap[repo]}
55+
repoName={repo}
56+
isPrimary={false}
57+
/>
58+
{files.map((file) => (
59+
<FileRow key={`${file.repo}:${file.path}`} file={file} />
60+
))}
61+
</div>
62+
))}
63+
</div>
64+
</HoverCardContent>
65+
)}
66+
</HoverCard>
67+
);
68+
}
Lines changed: 4 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
'use client';
22

3-
import { GrepFile, GrepMetadata, GrepRepoInfo, ToolResult } from "@/features/tools";
3+
import { GrepFile, GrepMetadata, ToolResult } from "@/features/tools";
44
import { useMemo } from "react";
55
import { RepoBadge } from "./repoBadge";
66
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
7-
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
8-
import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils";
9-
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
10-
import { cn, getCodeHostIcon } from "@/lib/utils";
11-
import Image from "next/image";
127
import { Separator } from "@/components/ui/separator";
13-
import Link from "next/link";
8+
import { RepoHeader } from "./repoHeader";
9+
import { FileRow } from "./fileRow";
1410

1511
export const GrepToolComponent = (output: ToolResult<GrepMetadata>) => {
1612
const stats = useMemo(() => {
@@ -34,10 +30,6 @@ export const GrepToolComponent = (output: ToolResult<GrepMetadata>) => {
3430
return groups;
3531
}, [output.metadata.files]);
3632

37-
const singleRepo = output.metadata.repoCount === 1
38-
? output.metadata.repoInfoMap[output.metadata.files[0]?.repo]
39-
: undefined;
40-
4133
return (
4234
<HoverCard openDelay={200}>
4335
<div className="flex items-center gap-2 select-none cursor-default">
@@ -46,7 +38,7 @@ export const GrepToolComponent = (output: ToolResult<GrepMetadata>) => {
4638
<span className="flex items-center gap-1.5 text-sm text-muted-foreground min-w-0 overflow-hidden">
4739
<span className="flex-shrink-0">Searched</span>
4840
<code className="text-xs bg-muted px-1 py-0.5 rounded truncate text-foreground max-w-[300px]">{output.metadata.pattern}</code>
49-
{singleRepo && <><span className="flex-shrink-0">in</span><RepoBadge repo={singleRepo} /></>}
41+
{output.metadata.inputRepo && <><span className="flex-shrink-0">in</span><RepoBadge repo={output.metadata.inputRepo} /></>}
5042
</span>
5143
</HoverCardTrigger>
5244
</div>
@@ -85,78 +77,3 @@ export const GrepToolComponent = (output: ToolResult<GrepMetadata>) => {
8577
</HoverCard>
8678
);
8779
}
88-
89-
const RepoHeader = ({ repo, repoName, isPrimary }: { repo: GrepRepoInfo | undefined; repoName: string; isPrimary: boolean }) => {
90-
const displayName = repo?.displayName ?? repoName.split('/').slice(1).join('/');
91-
const icon = repo ? getCodeHostIcon(repo.codeHostType) : null;
92-
93-
const href = getBrowsePath({
94-
repoName: repoName,
95-
path: '',
96-
pathType: 'tree',
97-
domain: SINGLE_TENANT_ORG_DOMAIN,
98-
});
99-
100-
const className = cn("top-0 flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-popover border-b border-border",
101-
{
102-
'sticky text-muted-foreground': !isPrimary,
103-
'text-foreground cursor-pointer hover:bg-accent transition-colors': isPrimary,
104-
}
105-
)
106-
107-
const Content = (
108-
<>
109-
{icon && (
110-
<Image src={icon.src} alt={repo!.codeHostType} width={12} height={12} className={icon.className} />
111-
)}
112-
<span>{displayName}</span>
113-
</>
114-
)
115-
116-
if (isPrimary) {
117-
return (
118-
<Link
119-
className={className}
120-
href={href}
121-
>
122-
{Content}
123-
</Link>
124-
)
125-
} else {
126-
return (
127-
<div className={className}>
128-
{Content}
129-
</div>
130-
)
131-
}
132-
}
133-
134-
const FileRow = ({ file }: { file: GrepFile }) => {
135-
const dir = file.path.includes('/')
136-
? file.path.split('/').slice(0, -1).join('/')
137-
: '';
138-
139-
const href = getBrowsePath({
140-
repoName: file.repo,
141-
revisionName: file.revision,
142-
path: file.path,
143-
pathType: 'blob',
144-
domain: SINGLE_TENANT_ORG_DOMAIN,
145-
});
146-
147-
return (
148-
<Link
149-
href={href}
150-
className="flex items-center gap-2 px-3 py-1 hover:bg-accent transition-colors"
151-
>
152-
<VscodeFileIcon fileName={file.name} className="flex-shrink-0" />
153-
<span className="text-sm font-medium flex-shrink-0">{file.name}</span>
154-
{dir && (
155-
<>
156-
<span className="text-xs text-muted-foreground flex-shrink-0">·</span>
157-
<span className="block text-xs text-muted-foreground truncate-start flex-1"><span>{dir}</span></span>
158-
</>
159-
)}
160-
</Link>
161-
);
162-
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use client';
2+
3+
import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils";
4+
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
5+
import { cn, getCodeHostIcon } from "@/lib/utils";
6+
import { CodeHostType } from "@sourcebot/db";
7+
import Image from "next/image";
8+
import Link from "next/link";
9+
10+
type RepoInfo = {
11+
displayName: string;
12+
codeHostType: CodeHostType;
13+
};
14+
15+
export const RepoHeader = ({ repo, repoName, isPrimary }: { repo: RepoInfo | undefined; repoName: string; isPrimary: boolean }) => {
16+
const displayName = repo?.displayName ?? repoName.split('/').slice(1).join('/');
17+
const icon = repo ? getCodeHostIcon(repo.codeHostType) : null;
18+
19+
const href = getBrowsePath({
20+
repoName: repoName,
21+
path: '',
22+
pathType: 'tree',
23+
domain: SINGLE_TENANT_ORG_DOMAIN,
24+
});
25+
26+
const className = cn("top-0 flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-popover border-b border-border",
27+
{
28+
'sticky text-muted-foreground': !isPrimary,
29+
'text-foreground cursor-pointer hover:bg-accent transition-colors': isPrimary,
30+
}
31+
);
32+
33+
const Content = (
34+
<>
35+
{icon && (
36+
<Image src={icon.src} alt={repo!.codeHostType} width={12} height={12} className={icon.className} />
37+
)}
38+
<span>{displayName}</span>
39+
</>
40+
);
41+
42+
if (isPrimary) {
43+
return (
44+
<Link className={className} href={href}>
45+
{Content}
46+
</Link>
47+
);
48+
} else {
49+
return (
50+
<div className={className}>
51+
{Content}
52+
</div>
53+
);
54+
}
55+
}

packages/web/src/features/chat/components/chatThread/tools/toolOutputGuard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export const ToolOutputGuard = <T extends ToolUIPart<{ [K in keyof SBChatMessage
6969
</div>
7070
{hasInput && isExpanded && (
7171
<div className="rounded-lg border border-border text-xs overflow-y-auto max-h-72">
72-
<ResultSection label="Request" onCopy={onCopyRequest}>
72+
<ResultSection label={`Request (${part.type})`} onCopy={onCopyRequest}>
7373
<pre className="whitespace-pre-wrap break-all font-mono">
7474
{requestText}
7575
</pre>

packages/web/src/features/chat/tools.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
listCommitsDefinition,
55
listReposDefinition,
66
grepDefinition,
7+
globDefinition,
78
findSymbolReferencesDefinition,
89
findSymbolDefinitionsDefinition,
910
listTreeDefinition,
@@ -17,6 +18,7 @@ export const createTools = (context: ToolContext) => ({
1718
[listCommitsDefinition.name]: toVercelAITool(listCommitsDefinition, context),
1819
[listReposDefinition.name]: toVercelAITool(listReposDefinition, context),
1920
[grepDefinition.name]: toVercelAITool(grepDefinition, context),
21+
[globDefinition.name]: toVercelAITool(globDefinition, context),
2022
[findSymbolReferencesDefinition.name]: toVercelAITool(findSymbolReferencesDefinition, context),
2123
[findSymbolDefinitionsDefinition.name]: toVercelAITool(findSymbolDefinitionsDefinition, context),
2224
[listTreeDefinition.name]: toVercelAITool(listTreeDefinition, context),
@@ -26,6 +28,7 @@ export type ReadFileToolUIPart = ToolUIPart<{ read_file: SBChatMessageToolTypes[
2628
export type ListCommitsToolUIPart = ToolUIPart<{ list_commits: SBChatMessageToolTypes['list_commits'] }>;
2729
export type ListReposToolUIPart = ToolUIPart<{ list_repos: SBChatMessageToolTypes['list_repos'] }>;
2830
export type GrepToolUIPart = ToolUIPart<{ grep: SBChatMessageToolTypes['grep'] }>;
31+
export type GlobToolUIPart = ToolUIPart<{ glob: SBChatMessageToolTypes['glob'] }>;
2932
export type FindSymbolReferencesToolUIPart = ToolUIPart<{ find_symbol_references: SBChatMessageToolTypes['find_symbol_references'] }>;
3033
export type FindSymbolDefinitionsToolUIPart = ToolUIPart<{ find_symbol_definitions: SBChatMessageToolTypes['find_symbol_definitions'] }>;
3134
export type ListTreeToolUIPart = ToolUIPart<{ list_tree: SBChatMessageToolTypes['list_tree'] }>;

0 commit comments

Comments
 (0)