/ components / permissions / FilePermissionDialog / usePermissionHandler.ts
usePermissionHandler.ts
  1  import {
  2    type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
  3    logEvent,
  4  } from '../../../services/analytics/index.js'
  5  import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'
  6  import type { ToolPermissionContext } from '../../../Tool.js'
  7  import {
  8    CLAUDE_FOLDER_PERMISSION_PATTERN,
  9    FILE_EDIT_TOOL_NAME,
 10    GLOBAL_CLAUDE_FOLDER_PERMISSION_PATTERN,
 11  } from '../../../tools/FileEditTool/constants.js'
 12  import { env } from '../../../utils/env.js'
 13  import { generateSuggestions } from '../../../utils/permissions/filesystem.js'
 14  import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'
 15  import {
 16    type CompletionType,
 17    logUnaryEvent,
 18  } from '../../../utils/unaryLogging.js'
 19  import type { ToolUseConfirm } from '../PermissionRequest.js'
 20  import type {
 21    FileOperationType,
 22    PermissionOption,
 23  } from './permissionOptions.js'
 24  
 25  function logPermissionEvent(
 26    event: 'accept' | 'reject',
 27    completionType: CompletionType,
 28    languageName: string | Promise<string>,
 29    messageId: string,
 30    hasFeedback?: boolean,
 31  ): void {
 32    void logUnaryEvent({
 33      completion_type: completionType,
 34      event,
 35      metadata: {
 36        language_name: languageName,
 37        message_id: messageId,
 38        platform: env.platform,
 39        hasFeedback: hasFeedback ?? false,
 40      },
 41    })
 42  }
 43  
 44  export type PermissionHandlerParams = {
 45    messageId: string
 46    path: string | null
 47    toolUseConfirm: ToolUseConfirm
 48    toolPermissionContext: ToolPermissionContext
 49    onDone: () => void
 50    onReject: () => void
 51    completionType: CompletionType
 52    languageName: string | Promise<string>
 53    operationType: FileOperationType
 54  }
 55  
 56  export type PermissionHandlerOptions = {
 57    hasFeedback?: boolean
 58    feedback?: string
 59    enteredFeedbackMode?: boolean
 60    scope?: 'claude-folder' | 'global-claude-folder'
 61  }
 62  
 63  function handleAcceptOnce(
 64    params: PermissionHandlerParams,
 65    options?: PermissionHandlerOptions,
 66  ): void {
 67    const { messageId, toolUseConfirm, onDone, completionType, languageName } =
 68      params
 69  
 70    logPermissionEvent('accept', completionType, languageName, messageId)
 71  
 72    // Log accept submission with feedback context
 73    logEvent('tengu_accept_submitted', {
 74      toolName: sanitizeToolNameForAnalytics(
 75        toolUseConfirm.tool.name,
 76      ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 77      isMcp: toolUseConfirm.tool.isMcp ?? false,
 78      has_instructions: !!options?.feedback,
 79      instructions_length: options?.feedback?.length ?? 0,
 80      entered_feedback_mode: options?.enteredFeedbackMode ?? false,
 81    })
 82  
 83    onDone()
 84    toolUseConfirm.onAllow(toolUseConfirm.input, [], options?.feedback)
 85  }
 86  
 87  function handleAcceptSession(
 88    params: PermissionHandlerParams,
 89    options?: PermissionHandlerOptions,
 90  ): void {
 91    const {
 92      messageId,
 93      path,
 94      toolUseConfirm,
 95      toolPermissionContext,
 96      onDone,
 97      completionType,
 98      languageName,
 99      operationType,
100    } = params
101  
102    logPermissionEvent('accept', completionType, languageName, messageId)
103  
104    // For claude-folder scope, grant session-level access to all .claude/ files
105    if (
106      options?.scope === 'claude-folder' ||
107      options?.scope === 'global-claude-folder'
108    ) {
109      const pattern =
110        options.scope === 'global-claude-folder'
111          ? GLOBAL_CLAUDE_FOLDER_PERMISSION_PATTERN
112          : CLAUDE_FOLDER_PERMISSION_PATTERN
113      const suggestions: PermissionUpdate[] = [
114        {
115          type: 'addRules',
116          rules: [
117            {
118              toolName: FILE_EDIT_TOOL_NAME,
119              ruleContent: pattern,
120            },
121          ],
122          behavior: 'allow',
123          destination: 'session',
124        },
125      ]
126      onDone()
127      toolUseConfirm.onAllow(toolUseConfirm.input, suggestions)
128      return
129    }
130  
131    // Generate permission updates if path is provided
132    const suggestions = path
133      ? generateSuggestions(path, operationType, toolPermissionContext)
134      : []
135  
136    onDone()
137    // Pass permission updates directly to onAllow
138    toolUseConfirm.onAllow(toolUseConfirm.input, suggestions)
139  }
140  
141  function handleReject(
142    params: PermissionHandlerParams,
143    options?: PermissionHandlerOptions,
144  ): void {
145    const {
146      messageId,
147      toolUseConfirm,
148      onDone,
149      onReject,
150      completionType,
151      languageName,
152    } = params
153  
154    logPermissionEvent(
155      'reject',
156      completionType,
157      languageName,
158      messageId,
159      options?.hasFeedback,
160    )
161  
162    // Log reject submission with feedback context
163    logEvent('tengu_reject_submitted', {
164      toolName: sanitizeToolNameForAnalytics(
165        toolUseConfirm.tool.name,
166      ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
167      isMcp: toolUseConfirm.tool.isMcp ?? false,
168      has_instructions: !!options?.feedback,
169      instructions_length: options?.feedback?.length ?? 0,
170      entered_feedback_mode: options?.enteredFeedbackMode ?? false,
171    })
172  
173    onDone()
174    onReject()
175    toolUseConfirm.onReject(options?.feedback)
176  }
177  
178  export const PERMISSION_HANDLERS: Record<
179    PermissionOption['type'],
180    (params: PermissionHandlerParams, options?: PermissionHandlerOptions) => void
181  > = {
182    'accept-once': handleAcceptOnce,
183    'accept-session': handleAcceptSession,
184    reject: handleReject,
185  }