( toolUseConfirm: ToolUseConfirm, unaryEvent: UnaryEvent, )
| 99 | * Handles both the analytics event and the unary event logging. |
| 100 | */ |
| 101 | export function usePermissionRequestLogging( |
| 102 | toolUseConfirm: ToolUseConfirm, |
| 103 | unaryEvent: UnaryEvent, |
| 104 | ): void { |
| 105 | const setAppState = useSetAppState() |
| 106 | // Guard against effect re-firing if toolUseConfirm's object reference |
| 107 | // changes during a single dialog's lifetime (e.g., parent re-renders with a |
| 108 | // fresh object). Without this, the unconditional setAppState below can |
| 109 | // cascade into an infinite microtask loop — each re-fire does another |
| 110 | // setAppState spread + (ant builds) splitCommand → shell-quote regex, |
| 111 | // pegging CPU at 100% and leaking ~500MB/min in JSRopeString/RegExp allocs. |
| 112 | // The component is keyed by toolUseID, so this ref resets on remount — |
| 113 | // we only need to dedupe re-fires WITHIN one dialog instance. |
| 114 | const loggedToolUseID = useRef<string | null>(null) |
| 115 | |
| 116 | useEffect(() => { |
| 117 | if (loggedToolUseID.current === toolUseConfirm.toolUseID) { |
| 118 | return |
| 119 | } |
| 120 | loggedToolUseID.current = toolUseConfirm.toolUseID |
| 121 | |
| 122 | // Increment permission prompt count for attribution tracking |
| 123 | setAppState(prev => ({ |
| 124 | ...prev, |
| 125 | attribution: { |
| 126 | ...prev.attribution, |
| 127 | permissionPromptCount: prev.attribution.permissionPromptCount + 1, |
| 128 | }, |
| 129 | })) |
| 130 | |
| 131 | // Log analytics event |
| 132 | logEvent('tengu_tool_use_show_permission_request', { |
| 133 | messageID: toolUseConfirm.assistantMessage.message |
| 134 | .id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 135 | toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name), |
| 136 | isMcp: toolUseConfirm.tool.isMcp ?? false, |
| 137 | decisionReasonType: toolUseConfirm.permissionResult.decisionReason |
| 138 | ?.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 139 | sandboxEnabled: SandboxManager.isSandboxingEnabled(), |
| 140 | }) |
| 141 | |
| 142 | if (process.env.USER_TYPE === 'ant') { |
| 143 | const permissionResult = toolUseConfirm.permissionResult |
| 144 | if ( |
| 145 | toolUseConfirm.tool.name === BashTool.name && |
| 146 | permissionResult.behavior === 'ask' && |
| 147 | !hasRules(permissionResult.suggestions) |
| 148 | ) { |
| 149 | // Log if no rule suggestions ("always allow") are provided |
| 150 | logEvent('tengu_internal_tool_use_permission_request_no_always_allow', { |
| 151 | messageID: toolUseConfirm.assistantMessage.message |
| 152 | .id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 153 | toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name), |
| 154 | isMcp: toolUseConfirm.tool.isMcp ?? false, |
| 155 | decisionReasonType: (permissionResult.decisionReason?.type ?? |
| 156 | 'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 157 | sandboxEnabled: SandboxManager.isSandboxingEnabled(), |
| 158 |
no test coverage detected