feat(repo-map): extract clean repo map branch
This commit is contained in:
176
src/tools/RepoMapTool/RepoMapTool.ts
Normal file
176
src/tools/RepoMapTool/RepoMapTool.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { z } from 'zod/v4'
|
||||
import { buildTool, type ToolDef } from '../../Tool.js'
|
||||
import { getCwd } from '../../utils/cwd.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { checkReadPermissionForTool } from '../../utils/permissions/filesystem.js'
|
||||
import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js'
|
||||
import { buildRepoMap } from '../../context/repoMap/index.js'
|
||||
import { REPO_MAP_TOOL_NAME, getDescription } from './prompt.js'
|
||||
import {
|
||||
getToolUseSummary,
|
||||
renderToolResultMessage,
|
||||
renderToolUseErrorMessage,
|
||||
renderToolUseMessage,
|
||||
} from './UI.js'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
max_tokens: z
|
||||
.number()
|
||||
.int()
|
||||
.min(256)
|
||||
.max(16384)
|
||||
.optional()
|
||||
.describe(
|
||||
'Maximum token budget for the rendered map. Higher values include more files. Default: 1024.',
|
||||
),
|
||||
focus_files: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
'Relative file or directory paths to boost in the ranking (e.g. ["src/tools/", "src/context.ts"]).',
|
||||
),
|
||||
focus_symbols: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
'Symbol names to boost — files defining these symbols rank higher (e.g. ["buildTool", "ToolUseContext"]).',
|
||||
),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
|
||||
const outputSchema = lazySchema(() =>
|
||||
z.object({
|
||||
rendered: z.string(),
|
||||
token_count: z.number(),
|
||||
file_count: z.number(),
|
||||
total_file_count: z.number(),
|
||||
cache_hit: z.boolean(),
|
||||
build_time_ms: z.number(),
|
||||
}),
|
||||
)
|
||||
type OutputSchema = ReturnType<typeof outputSchema>
|
||||
|
||||
type Output = z.infer<OutputSchema>
|
||||
|
||||
export const RepoMapTool = buildTool({
|
||||
name: REPO_MAP_TOOL_NAME,
|
||||
searchHint: 'structural map of repository files and symbols',
|
||||
maxResultSizeChars: 50_000,
|
||||
async description() {
|
||||
return getDescription()
|
||||
},
|
||||
userFacingName() {
|
||||
return 'Repository map'
|
||||
},
|
||||
getToolUseSummary,
|
||||
getActivityDescription(input) {
|
||||
if (input?.focus_files?.length) {
|
||||
return `Building repository map (focus: ${input.focus_files.join(', ')})`
|
||||
}
|
||||
return 'Building repository map'
|
||||
},
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
get outputSchema(): OutputSchema {
|
||||
return outputSchema()
|
||||
},
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
isSearchOrReadCommand() {
|
||||
return { isSearch: false, isRead: true }
|
||||
},
|
||||
toAutoClassifierInput(input) {
|
||||
const parts: string[] = ['repomap']
|
||||
if (input.focus_files?.length) parts.push(`focus: ${input.focus_files.join(',')}`)
|
||||
return parts.join(' ')
|
||||
},
|
||||
async checkPermissions(input, context): Promise<PermissionDecision> {
|
||||
const appState = context.getAppState()
|
||||
return checkReadPermissionForTool(
|
||||
RepoMapTool,
|
||||
input,
|
||||
appState.toolPermissionContext,
|
||||
)
|
||||
},
|
||||
async prompt() {
|
||||
return getDescription()
|
||||
},
|
||||
renderToolUseMessage,
|
||||
renderToolUseErrorMessage,
|
||||
renderToolResultMessage,
|
||||
extractSearchText({ rendered }) {
|
||||
return rendered
|
||||
},
|
||||
mapToolResultToToolResultBlockParam(output, toolUseID) {
|
||||
const summary = [
|
||||
`Repository map: ${output.file_count} files ranked (${output.total_file_count} total), ${output.token_count} tokens`,
|
||||
output.cache_hit ? '(cached)' : `(built in ${output.build_time_ms}ms)`,
|
||||
].join(' ')
|
||||
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: `${summary}\n\n${output.rendered}`,
|
||||
}
|
||||
},
|
||||
async call(
|
||||
{ max_tokens = 1024, focus_files, focus_symbols },
|
||||
{ abortController },
|
||||
) {
|
||||
const root = getCwd()
|
||||
|
||||
// Resolve focus_symbols to file paths by searching the tag cache
|
||||
let resolvedFocusFiles = focus_files ?? []
|
||||
if (focus_symbols?.length) {
|
||||
// Import the symbol lookup dynamically to avoid circular deps at module load
|
||||
const { getRepoFiles } = await import('../../context/repoMap/gitFiles.js')
|
||||
const { extractTags } = await import('../../context/repoMap/symbolExtractor.js')
|
||||
const { initParser } = await import('../../context/repoMap/parser.js')
|
||||
|
||||
await initParser()
|
||||
const files = await getRepoFiles(root)
|
||||
const symbolFiles: string[] = []
|
||||
const symbolSet = new Set(focus_symbols)
|
||||
|
||||
// Scan files for matching symbol definitions
|
||||
for (const file of files) {
|
||||
if (abortController.signal.aborted) break
|
||||
const tags = await extractTags(file, root)
|
||||
if (tags) {
|
||||
const hasMatch = tags.tags.some(
|
||||
t => t.kind === 'def' && symbolSet.has(t.name),
|
||||
)
|
||||
if (hasMatch) {
|
||||
symbolFiles.push(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolvedFocusFiles = [...resolvedFocusFiles, ...symbolFiles]
|
||||
}
|
||||
|
||||
const result = await buildRepoMap({
|
||||
root,
|
||||
maxTokens: max_tokens,
|
||||
focusFiles: resolvedFocusFiles.length > 0 ? resolvedFocusFiles : undefined,
|
||||
})
|
||||
|
||||
const output: Output = {
|
||||
rendered: result.map,
|
||||
token_count: result.tokenCount,
|
||||
file_count: result.fileCount,
|
||||
total_file_count: result.totalFileCount,
|
||||
cache_hit: result.cacheHit,
|
||||
build_time_ms: result.buildTimeMs,
|
||||
}
|
||||
|
||||
return { data: output }
|
||||
},
|
||||
} satisfies ToolDef<InputSchema, Output>)
|
||||
Reference in New Issue
Block a user