source dump of claude code
at main 127 lines 4.7 kB view raw
1type TriggerPosition = { word: string; start: number; end: number } 2 3const OPEN_TO_CLOSE: Record<string, string> = { 4 '`': '`', 5 '"': '"', 6 '<': '>', 7 '{': '}', 8 '[': ']', 9 '(': ')', 10 "'": "'", 11} 12 13/** 14 * Find keyword positions, skipping occurrences that are clearly not a 15 * launch directive: 16 * 17 * - Inside paired delimiters: backticks, double quotes, angle brackets 18 * (tag-like only, so `n < 5 ultraplan n > 10` is not a phantom range), 19 * curly braces, square brackets (innermost — preExpansionInput has 20 * `[Pasted text #N]` placeholders), parentheses. Single quotes are 21 * delimiters only when not an apostrophe — the opening quote must be 22 * preceded by a non-word char (or start) and the closing quote must be 23 * followed by a non-word char (or end), so "let's ultraplan it's" 24 * still triggers. 25 * 26 * - Path/identifier-like context: immediately preceded or followed by 27 * `/`, `\`, or `-`, or followed by `.` + word char (file extension). 28 * `\b` sees a boundary at `-`, so `ultraplan-s` would otherwise 29 * match. This keeps `src/ultraplan/foo.ts`, `ultraplan.tsx`, and 30 * `--ultraplan-mode` from triggering while `ultraplan.` at a sentence 31 * end still does. 32 * 33 * - Followed by `?`: a question about the feature shouldn't invoke it. 34 * Other sentence punctuation (`.`, `,`, `!`) still triggers. 35 * 36 * - Slash command input: text starting with `/` is a slash command 37 * invocation (processUserInput.ts routes it to processSlashCommand, 38 * not keyword detection), so `/rename ultraplan foo` never triggers. 39 * Without this, PromptInput would rainbow-highlight the word and show 40 * the "will launch ultraplan" notification even though submitting the 41 * input runs /rename, not /ultraplan. 42 * 43 * Shape matches findThinkingTriggerPositions (thinking.ts) so 44 * PromptInput treats both trigger types uniformly. 45 */ 46function findKeywordTriggerPositions( 47 text: string, 48 keyword: string, 49): TriggerPosition[] { 50 const re = new RegExp(keyword, 'i') 51 if (!re.test(text)) return [] 52 if (text.startsWith('/')) return [] 53 const quotedRanges: Array<{ start: number; end: number }> = [] 54 let openQuote: string | null = null 55 let openAt = 0 56 const isWord = (ch: string | undefined) => !!ch && /[\p{L}\p{N}_]/u.test(ch) 57 for (let i = 0; i < text.length; i++) { 58 const ch = text[i]! 59 if (openQuote) { 60 if (openQuote === '[' && ch === '[') { 61 openAt = i 62 continue 63 } 64 if (ch !== OPEN_TO_CLOSE[openQuote]) continue 65 if (openQuote === "'" && isWord(text[i + 1])) continue 66 quotedRanges.push({ start: openAt, end: i + 1 }) 67 openQuote = null 68 } else if ( 69 (ch === '<' && i + 1 < text.length && /[a-zA-Z/]/.test(text[i + 1]!)) || 70 (ch === "'" && !isWord(text[i - 1])) || 71 (ch !== '<' && ch !== "'" && ch in OPEN_TO_CLOSE) 72 ) { 73 openQuote = ch 74 openAt = i 75 } 76 } 77 78 const positions: TriggerPosition[] = [] 79 const wordRe = new RegExp(`\\b${keyword}\\b`, 'gi') 80 const matches = text.matchAll(wordRe) 81 for (const match of matches) { 82 if (match.index === undefined) continue 83 const start = match.index 84 const end = start + match[0].length 85 if (quotedRanges.some(r => start >= r.start && start < r.end)) continue 86 const before = text[start - 1] 87 const after = text[end] 88 if (before === '/' || before === '\\' || before === '-') continue 89 if (after === '/' || after === '\\' || after === '-' || after === '?') 90 continue 91 if (after === '.' && isWord(text[end + 1])) continue 92 positions.push({ word: match[0], start, end }) 93 } 94 return positions 95} 96 97export function findUltraplanTriggerPositions(text: string): TriggerPosition[] { 98 return findKeywordTriggerPositions(text, 'ultraplan') 99} 100 101export function findUltrareviewTriggerPositions( 102 text: string, 103): TriggerPosition[] { 104 return findKeywordTriggerPositions(text, 'ultrareview') 105} 106 107export function hasUltraplanKeyword(text: string): boolean { 108 return findUltraplanTriggerPositions(text).length > 0 109} 110 111export function hasUltrareviewKeyword(text: string): boolean { 112 return findUltrareviewTriggerPositions(text).length > 0 113} 114 115/** 116 * Replace the first triggerable "ultraplan" with "plan" so the forwarded 117 * prompt stays grammatical ("please ultraplan this" → "please plan this"). 118 * Preserves the user's casing of the "plan" suffix. 119 */ 120export function replaceUltraplanKeyword(text: string): string { 121 const [trigger] = findUltraplanTriggerPositions(text) 122 if (!trigger) return text 123 const before = text.slice(0, trigger.start) 124 const after = text.slice(trigger.end) 125 if (!(before + after).trim()) return '' 126 return before + trigger.word.slice('ultra'.length) + after 127}