source dump of claude code
at main 133 lines 3.8 kB view raw
1import { useMemo, useRef } from 'react' 2import { BASH_TOOL_NAME } from '../tools/BashTool/toolName.js' 3import type { Message } from '../types/message.js' 4import { getUserMessageText } from '../utils/messages.js' 5 6const EXTERNAL_COMMAND_PATTERNS = [ 7 /\bcurl\b/, 8 /\bwget\b/, 9 /\bssh\b/, 10 /\bkubectl\b/, 11 /\bsrun\b/, 12 /\bdocker\b/, 13 /\bbq\b/, 14 /\bgsutil\b/, 15 /\bgcloud\b/, 16 /\baws\b/, 17 /\bgit\s+push\b/, 18 /\bgit\s+pull\b/, 19 /\bgit\s+fetch\b/, 20 /\bgh\s+(pr|issue)\b/, 21 /\bnc\b/, 22 /\bncat\b/, 23 /\btelnet\b/, 24 /\bftp\b/, 25] 26 27const FRICTION_PATTERNS = [ 28 // "No," or "No!" at start — comma/exclamation implies correction tone 29 // (avoids "No problem", "No thanks", "No I think we should...") 30 /^no[,!]\s/i, 31 // Direct corrections about Claude's output 32 /\bthat'?s (wrong|incorrect|not (what|right|correct))\b/i, 33 /\bnot what I (asked|wanted|meant|said)\b/i, 34 // Referencing prior instructions Claude missed 35 /\bI (said|asked|wanted|told you|already said)\b/i, 36 // Questioning Claude's actions 37 /\bwhy did you\b/i, 38 /\byou should(n'?t| not)? have\b/i, 39 /\byou were supposed to\b/i, 40 // Explicit retry/revert of Claude's work 41 /\btry again\b/i, 42 /\b(undo|revert) (that|this|it|what you)\b/i, 43] 44 45export function isSessionContainerCompatible(messages: Message[]): boolean { 46 for (const msg of messages) { 47 if (msg.type !== 'assistant') { 48 continue 49 } 50 const content = msg.message.content 51 if (!Array.isArray(content)) { 52 continue 53 } 54 for (const block of content) { 55 if (block.type !== 'tool_use' || !('name' in block)) { 56 continue 57 } 58 const toolName = block.name as string 59 if (toolName.startsWith('mcp__')) { 60 return false 61 } 62 if (toolName === BASH_TOOL_NAME) { 63 const input = (block as { input?: Record<string, unknown> }).input 64 const command = (input?.command as string) || '' 65 if (EXTERNAL_COMMAND_PATTERNS.some(p => p.test(command))) { 66 return false 67 } 68 } 69 } 70 } 71 return true 72} 73 74export function hasFrictionSignal(messages: Message[]): boolean { 75 for (let i = messages.length - 1; i >= 0; i--) { 76 const msg = messages[i]! 77 if (msg.type !== 'user') { 78 continue 79 } 80 const text = getUserMessageText(msg) 81 if (!text) { 82 continue 83 } 84 return FRICTION_PATTERNS.some(p => p.test(text)) 85 } 86 return false 87} 88 89const MIN_SUBMIT_COUNT = 3 90const COOLDOWN_MS = 30 * 60 * 1000 91 92export function useIssueFlagBanner( 93 messages: Message[], 94 submitCount: number, 95): boolean { 96 if (process.env.USER_TYPE !== 'ant') { 97 return false 98 } 99 100 // biome-ignore lint/correctness/useHookAtTopLevel: process.env.USER_TYPE is a compile-time constant 101 const lastTriggeredAtRef = useRef(0) 102 // biome-ignore lint/correctness/useHookAtTopLevel: process.env.USER_TYPE is a compile-time constant 103 const activeForSubmitRef = useRef(-1) 104 105 // Memoize the O(messages) scans. This hook runs on every REPL render 106 // (including every keystroke), but messages is stable during typing. 107 // isSessionContainerCompatible walks all messages + regex-tests each 108 // bash command — by far the heaviest work here. 109 // biome-ignore lint/correctness/useHookAtTopLevel: process.env.USER_TYPE is a compile-time constant 110 const shouldTrigger = useMemo( 111 () => isSessionContainerCompatible(messages) && hasFrictionSignal(messages), 112 [messages], 113 ) 114 115 // Keep showing the banner until the user submits another message 116 if (activeForSubmitRef.current === submitCount) { 117 return true 118 } 119 120 if (Date.now() - lastTriggeredAtRef.current < COOLDOWN_MS) { 121 return false 122 } 123 if (submitCount < MIN_SUBMIT_COUNT) { 124 return false 125 } 126 if (!shouldTrigger) { 127 return false 128 } 129 130 lastTriggeredAtRef.current = Date.now() 131 activeForSubmitRef.current = submitCount 132 return true 133}