@@ -29,18 +29,42 @@ function estimateTokens(text: string): number {
2929 return Math . ceil ( text . length / CHARS_PER_TOKEN ) ;
3030}
3131
32- function normalizePath ( p : string ) : string {
33- return p . replace ( / \\ / g, '/' ) ;
32+ // ── Formatting ───────────────────────────────────────────
33+
34+ function countSymbolsDeep ( symbols : CodebaseSymbolNode [ ] ) : number {
35+ let count = symbols . length ;
36+ for ( const s of symbols ) {
37+ if ( s . children ) count += countSymbolsDeep ( s . children ) ;
38+ }
39+ return count ;
3440}
3541
36- // ── Formatting ───────────────────────────────────────────
42+ function countImmediateFiles ( nodes : CodebaseTreeNode [ ] ) : number {
43+ let count = 0 ;
44+ for ( const n of nodes ) {
45+ if ( n . type === 'file' ) count ++ ;
46+ }
47+ return count ;
48+ }
3749
38- function formatSymbol ( symbol : CodebaseSymbolNode , depth : number ) : string {
50+ function countImmediateSubfolders ( nodes : CodebaseTreeNode [ ] ) : number {
51+ let count = 0 ;
52+ for ( const n of nodes ) {
53+ if ( n . type === 'directory' ) count ++ ;
54+ }
55+ return count ;
56+ }
57+
58+ function plural ( n : number , singular : string , pluralForm : string ) : string {
59+ return n === 1 ? `${ n } ${ singular } ` : `${ n } ${ pluralForm } ` ;
60+ }
61+
62+ function formatSymbol ( symbol : CodebaseSymbolNode , depth : number , maxSymbolDepth ?: number , currentSymbolDepth = 0 ) : string {
3963 const indent = INDENT . repeat ( depth ) ;
4064 let output = `${ indent } ${ symbol . kind } ${ symbol . name } \n` ;
41- if ( symbol . children ) {
65+ if ( symbol . children && ( maxSymbolDepth === undefined || currentSymbolDepth < maxSymbolDepth ) ) {
4266 for ( const child of symbol . children ) {
43- output += formatSymbol ( child , depth + 1 ) ;
67+ output += formatSymbol ( child , depth + 1 , maxSymbolDepth , currentSymbolDepth + 1 ) ;
4468 }
4569 }
4670 return output ;
@@ -50,37 +74,57 @@ function formatTree(
5074 nodes : CodebaseTreeNode [ ] ,
5175 showSymbols : boolean ,
5276 showFiles : boolean ,
77+ showMetadata : boolean ,
5378 depth : number = 0 ,
79+ maxSymbolDepth ?: number ,
5480) : string {
5581 let output = '' ;
5682 const indent = INDENT . repeat ( depth ) ;
5783
5884 for ( const node of nodes ) {
5985 if ( node . type === 'directory' ) {
60- output += `${ indent } ${ node . name } /\n` ;
86+ if ( showMetadata && node . children ) {
87+ const files = countImmediateFiles ( node . children ) ;
88+ const subs = countImmediateSubfolders ( node . children ) ;
89+ output += `${ indent } [${ plural ( files , 'file' , 'files' ) } , ${ plural ( subs , 'subfolder' , 'subfolders' ) } ] ${ node . name } /\n` ;
90+ } else {
91+ output += `${ indent } ${ node . name } /\n` ;
92+ }
6193 if ( node . children ) {
62- output += formatTree ( node . children , showSymbols , showFiles , depth + 1 ) ;
94+ output += formatTree ( node . children , showSymbols , showFiles , showMetadata , depth + 1 , maxSymbolDepth ) ;
6395 }
6496 } else if ( node . type === 'file' && showFiles ) {
65- output += `${ indent } ${ node . name } \n` ;
97+ if ( showMetadata ) {
98+ const linePart = node . lineCount != null ? `[${ plural ( node . lineCount , 'line' , 'lines' ) } ] ` : '' ;
99+ const symPart = node . symbols ? `[${ plural ( countSymbolsDeep ( node . symbols ) , 'symbol' , 'symbols' ) } ] ` : '' ;
100+ output += `${ indent } ${ linePart } ${ symPart } ${ node . name } \n` ;
101+ } else {
102+ output += `${ indent } ${ node . name } \n` ;
103+ }
66104 if ( showSymbols && node . symbols ) {
67105 for ( const sym of node . symbols ) {
68- output += formatSymbol ( sym , depth + 1 ) ;
106+ output += formatSymbol ( sym , depth + 1 , maxSymbolDepth ) ;
69107 }
70108 }
71109 }
72110 }
73111 return output ;
74112}
75113
76- function formatFlatPaths ( nodes : CodebaseTreeNode [ ] , prefix = '' ) : string {
114+ function formatFlatPaths ( nodes : CodebaseTreeNode [ ] , showMetadata : boolean , prefix = '' ) : string {
77115 let output = '' ;
78116 for ( const node of nodes ) {
79117 const p = prefix ? `${ prefix } /${ node . name } ` : node . name ;
80118 if ( node . type === 'file' ) {
81- output += p + '\n' ;
119+ if ( showMetadata ) {
120+ const linePart = node . lineCount != null ? `[${ plural ( node . lineCount , 'line' , 'lines' ) } ] ` : '' ;
121+ const symPart = node . symbols ? `[${ plural ( countSymbolsDeep ( node . symbols ) , 'symbol' , 'symbols' ) } ] ` : '' ;
122+ output += `${ linePart } ${ symPart } ${ p } \n` ;
123+ } else {
124+ output += p + '\n' ;
125+ }
82126 } else if ( node . children ) {
83- output += formatFlatPaths ( node . children , p ) ;
127+ output += formatFlatPaths ( node . children , showMetadata , p ) ;
84128 }
85129 }
86130 return output ;
@@ -92,8 +136,9 @@ function formatFolderSummary(nodes: CodebaseTreeNode[], depth = 0): string {
92136
93137 for ( const node of nodes ) {
94138 if ( node . type === 'directory' ) {
95- const fileCount = countFiles ( node ) ;
96- output += `${ indent } ${ node . name } / (${ fileCount } files)\n` ;
139+ const files = node . children ? countImmediateFiles ( node . children ) : 0 ;
140+ const subs = node . children ? countImmediateSubfolders ( node . children ) : 0 ;
141+ output += `${ indent } [${ plural ( files , 'file' , 'files' ) } , ${ plural ( subs , 'subfolder' , 'subfolders' ) } ] ${ node . name } /\n` ;
97142 if ( node . children ) {
98143 output += formatFolderSummary ( node . children , depth + 1 ) ;
99144 }
@@ -113,6 +158,28 @@ function countFiles(node: CodebaseTreeNode): number {
113158 return count ;
114159}
115160
161+ function maxSymbolTreeDepth ( symbols : CodebaseSymbolNode [ ] , current = 0 ) : number {
162+ let max = current ;
163+ for ( const s of symbols ) {
164+ if ( s . children && s . children . length > 0 ) {
165+ max = Math . max ( max , maxSymbolTreeDepth ( s . children , current + 1 ) ) ;
166+ }
167+ }
168+ return max ;
169+ }
170+
171+ function maxTreeSymbolDepth ( nodes : CodebaseTreeNode [ ] ) : number {
172+ let max = 0 ;
173+ for ( const node of nodes ) {
174+ if ( node . type === 'file' && node . symbols ) {
175+ max = Math . max ( max , maxSymbolTreeDepth ( node . symbols ) ) ;
176+ } else if ( node . type === 'directory' && node . children ) {
177+ max = Math . max ( max , maxTreeSymbolDepth ( node . children ) ) ;
178+ }
179+ }
180+ return max ;
181+ }
182+
116183// ── Tool Definition ──────────────────────────────────────
117184
118185export const map = defineTool ( {
@@ -123,13 +190,15 @@ export const map = defineTool({
123190 '- `folderPath` — Folder to map (relative or absolute). Defaults to workspace root.\n' +
124191 '- `recursive` — Include subdirectories recursively. Default: false (immediate children only).\n' +
125192 '- `fileTypes` — Which files to include: `"*"` (all), `"none"` (folders only), or array of extensions.\n' +
126- '- `symbols` — Include symbol skeleton (name + kind, hierarchically nested). Default: false.\n\n' +
193+ '- `symbols` — Include symbol skeleton (name + kind, hierarchically nested). Default: false.\n' +
194+ '- `metadata` — Show counts: `[N lines] [M symbols]` per file, `[N files, M subfolders]` per folder. Default: false.\n\n' +
127195 '**EXAMPLES:**\n' +
128196 '- Shallow view of root: `{}`\n' +
129197 '- Full project tree: `{ recursive: true }`\n' +
130198 '- Only TypeScript files: `{ fileTypes: [".ts"], recursive: true }`\n' +
131199 '- Folder structure only: `{ fileTypes: "none", recursive: true }`\n' +
132200 '- Specific folder with symbols: `{ folderPath: "src", recursive: true, symbols: true }`\n' +
201+ '- Tree with metadata: `{ recursive: true, metadata: true }`\n' +
133202 '- Only CSS files in a subfolder: `{ folderPath: "src/styles", fileTypes: [".css", ".scss"] }`' ,
134203 annotations : {
135204 title : 'Codebase Map' ,
@@ -156,6 +225,9 @@ export const map = defineTool({
156225
157226 symbols : zod . boolean ( ) . optional ( )
158227 . describe ( 'Include symbol skeleton (name + kind, hierarchically nested). Default: false.' ) ,
228+
229+ metadata : zod . boolean ( ) . optional ( )
230+ . describe ( 'Show counts per file ([N lines] [M symbols]) and per folder ([N files, M subfolders]). Default: false.' ) ,
159231 } ,
160232 handler : async ( request , response ) => {
161233 const { params} = request ;
@@ -166,6 +238,7 @@ export const map = defineTool({
166238 const recursive = params . recursive ?? false ;
167239 const fileTypes = params . fileTypes ?? '*' ;
168240 const symbols = params . symbols ?? false ;
241+ const metadata = params . metadata ?? false ;
169242 const isNone = fileTypes === 'none' ;
170243
171244 // Dynamic timeout based on request scope
@@ -181,11 +254,11 @@ export const map = defineTool({
181254 fileTypes ,
182255 symbols ,
183256 dynamicTimeout ,
257+ metadata ,
184258 ) ;
185259
186260 if ( overviewResult . summary . totalFiles === 0 && ! isNone ) {
187261 const ignoreContext = readIgnoreContext ( overviewResult . projectRoot ) ;
188- response . appendResponseLine ( `Root: ${ normalizePath ( overviewResult . projectRoot ) } \n` ) ;
189262 response . appendResponseLine ( 'No files found. Check scope patterns or .devtoolsignore.\n' ) ;
190263 if ( ignoreContext . activePatterns . length > 0 ) {
191264 response . appendResponseLine ( 'Current .devtoolsignore patterns:\n' ) ;
@@ -199,36 +272,54 @@ export const map = defineTool({
199272 // Build output
200273 let showFiles = ! isNone ;
201274 let showSymbols = symbols ;
202- let output = formatTree ( overviewResult . tree , showSymbols , showFiles ) ;
275+ let showMetadata = metadata ;
276+ let output = formatTree ( overviewResult . tree , showSymbols , showFiles , showMetadata ) ;
203277 const reductionsApplied : string [ ] = [ ] ;
204278
205- // Adaptive compression — progressively reduce detail if output exceeds token limit
279+ // Adaptive compression — progressively reduce symbol depth before removing symbols entirely
206280 if ( estimateTokens ( output ) > OUTPUT_TOKEN_LIMIT && showSymbols ) {
207- showSymbols = false ;
208- reductionsApplied . push ( 'remove-symbols' ) ;
209- output = formatTree ( overviewResult . tree , showSymbols , showFiles ) ;
281+ showMetadata = true ;
282+ const deepest = maxTreeSymbolDepth ( overviewResult . tree ) ;
283+
284+ // Try each depth level from deepest-1 down to 0 (top-level symbols only)
285+ for ( let d = deepest - 1 ; d >= 0 ; d -- ) {
286+ output = formatTree ( overviewResult . tree , showSymbols , showFiles , showMetadata , 0 , d ) ;
287+ if ( estimateTokens ( output ) <= OUTPUT_TOKEN_LIMIT ) {
288+ reductionsApplied . push ( `symbol-depth-${ d } ` ) ;
289+ break ;
290+ }
291+ }
292+
293+ // If even top-level symbols (depth 0) is too large, remove symbols entirely
294+ if ( estimateTokens ( output ) > OUTPUT_TOKEN_LIMIT ) {
295+ showSymbols = false ;
296+ reductionsApplied . push ( 'remove-symbols' ) ;
297+ output = formatTree ( overviewResult . tree , showSymbols , showFiles , showMetadata ) ;
298+ }
210299 }
211300
212301 if ( estimateTokens ( output ) > OUTPUT_TOKEN_LIMIT && showFiles ) {
213302 showFiles = false ;
214303 reductionsApplied . push ( 'folders-only' ) ;
215- output = formatTree ( overviewResult . tree , showSymbols , showFiles ) ;
304+ output = formatTree ( overviewResult . tree , showSymbols , showFiles , showMetadata ) ;
216305 }
217306
218307 if ( estimateTokens ( output ) > OUTPUT_TOKEN_LIMIT ) {
219308 reductionsApplied . push ( 'flat-paths' ) ;
220- output = formatFlatPaths ( overviewResult . tree ) ;
309+ output = formatFlatPaths ( overviewResult . tree , showMetadata ) ;
221310 }
222311
223312 if ( estimateTokens ( output ) > OUTPUT_TOKEN_LIMIT ) {
224313 reductionsApplied . push ( 'folder-summary' ) ;
225314 output = formatFolderSummary ( overviewResult . tree ) ;
226315 }
227316
228- response . appendResponseLine ( `Root: ${ normalizePath ( overviewResult . projectRoot ) } \n` ) ;
229-
230317 if ( reductionsApplied . length > 0 ) {
231- response . appendResponseLine ( `Compression: ${ reductionsApplied . join ( ' → ' ) } \n` ) ;
318+ const steps = reductionsApplied . join ( ' → ' ) ;
319+ response . appendResponseLine (
320+ `Output exceeded token limit. Compression applied: ${ steps } . ` +
321+ 'Use the returned map to navigate from here, or use file_read with a specific folder/file for full detail.\n' ,
322+ ) ;
232323 }
233324
234325 response . appendResponseLine ( output . trimEnd ( ) ) ;
0 commit comments