source dump of claude code
at main 235 lines 7.3 kB view raw
1import { BASH_TOOL_NAME } from '../tools/BashTool/toolName.js' 2import { FILE_READ_TOOL_NAME } from '../tools/FileReadTool/prompt.js' 3import { GREP_TOOL_NAME } from '../tools/GrepTool/prompt.js' 4import { WEB_FETCH_TOOL_NAME } from '../tools/WebFetchTool/prompt.js' 5import type { ContextData } from './analyzeContext.js' 6import { getDisplayPath } from './file.js' 7import { formatTokens } from './format.js' 8 9// -- 10 11export type SuggestionSeverity = 'info' | 'warning' 12 13export type ContextSuggestion = { 14 severity: SuggestionSeverity 15 title: string 16 detail: string 17 /** Estimated tokens that could be saved */ 18 savingsTokens?: number 19} 20 21// Thresholds for triggering suggestions 22const LARGE_TOOL_RESULT_PERCENT = 15 // tool results > 15% of context 23const LARGE_TOOL_RESULT_TOKENS = 10_000 24const READ_BLOAT_PERCENT = 5 // Read results > 5% of context 25const NEAR_CAPACITY_PERCENT = 80 26const MEMORY_HIGH_PERCENT = 5 27const MEMORY_HIGH_TOKENS = 5_000 28 29// -- 30 31export function generateContextSuggestions( 32 data: ContextData, 33): ContextSuggestion[] { 34 const suggestions: ContextSuggestion[] = [] 35 36 checkNearCapacity(data, suggestions) 37 checkLargeToolResults(data, suggestions) 38 checkReadResultBloat(data, suggestions) 39 checkMemoryBloat(data, suggestions) 40 checkAutoCompactDisabled(data, suggestions) 41 42 // Sort: warnings first, then by savings descending 43 suggestions.sort((a, b) => { 44 if (a.severity !== b.severity) { 45 return a.severity === 'warning' ? -1 : 1 46 } 47 return (b.savingsTokens ?? 0) - (a.savingsTokens ?? 0) 48 }) 49 50 return suggestions 51} 52 53// -- 54 55function checkNearCapacity( 56 data: ContextData, 57 suggestions: ContextSuggestion[], 58): void { 59 if (data.percentage >= NEAR_CAPACITY_PERCENT) { 60 suggestions.push({ 61 severity: 'warning', 62 title: `Context is ${data.percentage}% full`, 63 detail: data.isAutoCompactEnabled 64 ? 'Autocompact will trigger soon, which discards older messages. Use /compact now to control what gets kept.' 65 : 'Autocompact is disabled. Use /compact to free space, or enable autocompact in /config.', 66 }) 67 } 68} 69 70function checkLargeToolResults( 71 data: ContextData, 72 suggestions: ContextSuggestion[], 73): void { 74 if (!data.messageBreakdown) return 75 76 for (const tool of data.messageBreakdown.toolCallsByType) { 77 const totalToolTokens = tool.callTokens + tool.resultTokens 78 const percent = (totalToolTokens / data.rawMaxTokens) * 100 79 80 if ( 81 percent < LARGE_TOOL_RESULT_PERCENT || 82 totalToolTokens < LARGE_TOOL_RESULT_TOKENS 83 ) { 84 continue 85 } 86 87 const suggestion = getLargeToolSuggestion( 88 tool.name, 89 totalToolTokens, 90 percent, 91 ) 92 if (suggestion) { 93 suggestions.push(suggestion) 94 } 95 } 96} 97 98function getLargeToolSuggestion( 99 toolName: string, 100 tokens: number, 101 percent: number, 102): ContextSuggestion | null { 103 const tokenStr = formatTokens(tokens) 104 105 switch (toolName) { 106 case BASH_TOOL_NAME: 107 return { 108 severity: 'warning', 109 title: `Bash results using ${tokenStr} tokens (${percent.toFixed(0)}%)`, 110 detail: 111 'Pipe output through head, tail, or grep to reduce result size. Avoid cat on large files \u2014 use Read with offset/limit instead.', 112 savingsTokens: Math.floor(tokens * 0.5), 113 } 114 case FILE_READ_TOOL_NAME: 115 return { 116 severity: 'info', 117 title: `Read results using ${tokenStr} tokens (${percent.toFixed(0)}%)`, 118 detail: 119 'Use offset and limit parameters to read only the sections you need. Avoid re-reading entire files when you only need a few lines.', 120 savingsTokens: Math.floor(tokens * 0.3), 121 } 122 case GREP_TOOL_NAME: 123 return { 124 severity: 'info', 125 title: `Grep results using ${tokenStr} tokens (${percent.toFixed(0)}%)`, 126 detail: 127 'Add more specific patterns or use the glob or type parameter to narrow file types. Consider Glob for file discovery instead of Grep.', 128 savingsTokens: Math.floor(tokens * 0.3), 129 } 130 case WEB_FETCH_TOOL_NAME: 131 return { 132 severity: 'info', 133 title: `WebFetch results using ${tokenStr} tokens (${percent.toFixed(0)}%)`, 134 detail: 135 'Web page content can be very large. Consider extracting only the specific information needed.', 136 savingsTokens: Math.floor(tokens * 0.4), 137 } 138 default: 139 if (percent >= 20) { 140 return { 141 severity: 'info', 142 title: `${toolName} using ${tokenStr} tokens (${percent.toFixed(0)}%)`, 143 detail: `This tool is consuming a significant portion of context.`, 144 savingsTokens: Math.floor(tokens * 0.2), 145 } 146 } 147 return null 148 } 149} 150 151function checkReadResultBloat( 152 data: ContextData, 153 suggestions: ContextSuggestion[], 154): void { 155 if (!data.messageBreakdown) return 156 157 const callsByType = data.messageBreakdown.toolCallsByType 158 const readTool = callsByType.find(t => t.name === FILE_READ_TOOL_NAME) 159 if (!readTool) return 160 161 const totalReadTokens = readTool.callTokens + readTool.resultTokens 162 const totalReadPercent = (totalReadTokens / data.rawMaxTokens) * 100 163 const readPercent = (readTool.resultTokens / data.rawMaxTokens) * 100 164 165 // Skip if already covered by checkLargeToolResults (>= 15% band) 166 if ( 167 totalReadPercent >= LARGE_TOOL_RESULT_PERCENT && 168 totalReadTokens >= LARGE_TOOL_RESULT_TOKENS 169 ) { 170 return 171 } 172 173 if ( 174 readPercent >= READ_BLOAT_PERCENT && 175 readTool.resultTokens >= LARGE_TOOL_RESULT_TOKENS 176 ) { 177 suggestions.push({ 178 severity: 'info', 179 title: `File reads using ${formatTokens(readTool.resultTokens)} tokens (${readPercent.toFixed(0)}%)`, 180 detail: 181 'If you are re-reading files, consider referencing earlier reads. Use offset/limit for large files.', 182 savingsTokens: Math.floor(readTool.resultTokens * 0.3), 183 }) 184 } 185} 186 187function checkMemoryBloat( 188 data: ContextData, 189 suggestions: ContextSuggestion[], 190): void { 191 const totalMemoryTokens = data.memoryFiles.reduce( 192 (sum, f) => sum + f.tokens, 193 0, 194 ) 195 const memoryPercent = (totalMemoryTokens / data.rawMaxTokens) * 100 196 197 if ( 198 memoryPercent >= MEMORY_HIGH_PERCENT && 199 totalMemoryTokens >= MEMORY_HIGH_TOKENS 200 ) { 201 const largestFiles = [...data.memoryFiles] 202 .sort((a, b) => b.tokens - a.tokens) 203 .slice(0, 3) 204 .map(f => { 205 const name = getDisplayPath(f.path) 206 return `${name} (${formatTokens(f.tokens)})` 207 }) 208 .join(', ') 209 210 suggestions.push({ 211 severity: 'info', 212 title: `Memory files using ${formatTokens(totalMemoryTokens)} tokens (${memoryPercent.toFixed(0)}%)`, 213 detail: `Largest: ${largestFiles}. Use /memory to review and prune stale entries.`, 214 savingsTokens: Math.floor(totalMemoryTokens * 0.3), 215 }) 216 } 217} 218 219function checkAutoCompactDisabled( 220 data: ContextData, 221 suggestions: ContextSuggestion[], 222): void { 223 if ( 224 !data.isAutoCompactEnabled && 225 data.percentage >= 50 && 226 data.percentage < NEAR_CAPACITY_PERCENT 227 ) { 228 suggestions.push({ 229 severity: 'info', 230 title: 'Autocompact is disabled', 231 detail: 232 'Without autocompact, you will hit context limits and lose the conversation. Enable it in /config or use /compact manually.', 233 }) 234 } 235}