|
1 | | -import { z } from "zod"; |
| 1 | +import { getRepoInfoByName } from "@/actions"; |
| 2 | +import { FileTreeNode, getTree } from "@/features/git"; |
2 | 3 | import { isServiceError } from "@/lib/utils"; |
3 | | -import { getTree } from "@/features/git"; |
4 | | -import { buildTreeNodeIndex, joinTreePath, normalizeTreePath, sortTreeEntries } from "@/features/mcp/utils"; |
5 | | -import { ToolDefinition } from "./types"; |
6 | | -import { logger } from "./logger"; |
7 | | -import description from "./listTree.txt"; |
8 | 4 | import { CodeHostType } from "@sourcebot/db"; |
9 | | -import { getRepoInfoByName } from "@/actions"; |
| 5 | +import { z } from "zod"; |
| 6 | +import description from "./listTree.txt"; |
| 7 | +import { logger } from "./logger"; |
| 8 | +import { ToolDefinition } from "./types"; |
10 | 9 |
|
11 | 10 | const DEFAULT_TREE_DEPTH = 1; |
12 | 11 | const MAX_TREE_DEPTH = 10; |
@@ -42,7 +41,6 @@ export type ListTreeMetadata = { |
42 | 41 | repoInfo: ListTreeRepoInfo; |
43 | 42 | ref: string; |
44 | 43 | path: string; |
45 | | - entries: ListTreeEntry[]; |
46 | 44 | totalReturned: number; |
47 | 45 | truncated: boolean; |
48 | 46 | }; |
@@ -152,8 +150,10 @@ export const listTreeDefinition: ToolDefinition<'list_tree', typeof listTreeShap |
152 | 150 |
|
153 | 151 | const sortedEntries = sortTreeEntries(entries); |
154 | 152 | const metadata: ListTreeMetadata = { |
155 | | - repo, repoInfo, ref, path: normalizedPath, |
156 | | - entries: sortedEntries, |
| 153 | + repo, |
| 154 | + repoInfo, |
| 155 | + ref, |
| 156 | + path: normalizedPath, |
157 | 157 | totalReturned: sortedEntries.length, |
158 | 158 | truncated, |
159 | 159 | }; |
@@ -190,6 +190,60 @@ export const listTreeDefinition: ToolDefinition<'list_tree', typeof listTreeShap |
190 | 190 | outputLines.push(`(truncated — showing first ${normalizedMaxEntries} entries)`); |
191 | 191 | } |
192 | 192 |
|
193 | | - return { output: outputLines.join('\n'), metadata }; |
| 193 | + const sources = sortedEntries |
| 194 | + .filter((entry) => entry.type === 'blob') |
| 195 | + .map((entry) => ({ |
| 196 | + type: 'file' as const, |
| 197 | + repo, |
| 198 | + path: entry.path, |
| 199 | + name: entry.name, |
| 200 | + revision: ref, |
| 201 | + })); |
| 202 | + |
| 203 | + return { output: outputLines.join('\n'), metadata, sources }; |
194 | 204 | }, |
195 | 205 | }; |
| 206 | + |
| 207 | +const normalizeTreePath = (path: string): string => { |
| 208 | + const withoutLeading = path.replace(/^\/+/, ''); |
| 209 | + return withoutLeading.replace(/\/+$/, ''); |
| 210 | +} |
| 211 | + |
| 212 | +const joinTreePath = (parentPath: string, name: string): string => { |
| 213 | + if (!parentPath) { |
| 214 | + return name; |
| 215 | + } |
| 216 | + return `${parentPath}/${name}`; |
| 217 | +} |
| 218 | + |
| 219 | +const buildTreeNodeIndex = (root: FileTreeNode): Map<string, FileTreeNode> => { |
| 220 | + const nodeIndex = new Map<string, FileTreeNode>(); |
| 221 | + |
| 222 | + const visit = (node: FileTreeNode, currentPath: string) => { |
| 223 | + nodeIndex.set(currentPath, node); |
| 224 | + for (const child of node.children) { |
| 225 | + visit(child, joinTreePath(currentPath, child.name)); |
| 226 | + } |
| 227 | + }; |
| 228 | + |
| 229 | + visit(root, ''); |
| 230 | + return nodeIndex; |
| 231 | +} |
| 232 | + |
| 233 | +const sortTreeEntries = (entries: ListTreeEntry[]): ListTreeEntry[] => { |
| 234 | + const collator = new Intl.Collator(undefined, { sensitivity: 'base' }); |
| 235 | + |
| 236 | + return [...entries].sort((a, b) => { |
| 237 | + const parentCompare = collator.compare(a.parentPath, b.parentPath); |
| 238 | + if (parentCompare !== 0) return parentCompare; |
| 239 | + |
| 240 | + if (a.type !== b.type) { |
| 241 | + return a.type === 'tree' ? -1 : 1; |
| 242 | + } |
| 243 | + |
| 244 | + const nameCompare = collator.compare(a.name, b.name); |
| 245 | + if (nameCompare !== 0) return nameCompare; |
| 246 | + |
| 247 | + return collator.compare(a.path, b.path); |
| 248 | + }); |
| 249 | +} |
0 commit comments