mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 22:35:51 +00:00
197 lines
5.8 KiB
TypeScript
197 lines
5.8 KiB
TypeScript
import React, { useCallback, useMemo } from 'react'
|
|
import { getOriginalCwd } from '../../bootstrap/state.js'
|
|
import { Box, Text, useTheme } from '../../ink.js'
|
|
import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js'
|
|
import { env } from '../../utils/env.js'
|
|
import { shouldShowAlwaysAllowOptions } from '../../utils/permissions/permissionsLoader.js'
|
|
import { truncateToLines } from '../../utils/stringUtils.js'
|
|
import { logUnaryEvent } from '../../utils/unaryLogging.js'
|
|
import { type UnaryEvent, usePermissionRequestLogging } from './hooks.js'
|
|
import { PermissionDialog } from './PermissionDialog.js'
|
|
import {
|
|
PermissionPrompt,
|
|
type PermissionPromptOption,
|
|
type ToolAnalyticsContext,
|
|
} from './PermissionPrompt.js'
|
|
import type { PermissionRequestProps } from './PermissionRequest.js'
|
|
import { PermissionRuleExplanation } from './PermissionRuleExplanation.js'
|
|
|
|
type FallbackOptionValue = 'yes' | 'yes-dont-ask-again' | 'no'
|
|
|
|
export function FallbackPermissionRequest({
|
|
toolUseConfirm,
|
|
onDone,
|
|
onReject,
|
|
verbose: _verbose,
|
|
workerBadge,
|
|
}: PermissionRequestProps): React.ReactNode {
|
|
const [theme] = useTheme()
|
|
// TODO: Avoid these special cases
|
|
const originalUserFacingName = toolUseConfirm.tool.userFacingName(
|
|
toolUseConfirm.input as never,
|
|
)
|
|
const userFacingName = originalUserFacingName.endsWith(' (MCP)')
|
|
? originalUserFacingName.slice(0, -6)
|
|
: originalUserFacingName
|
|
|
|
const unaryEvent = useMemo<UnaryEvent>(
|
|
() => ({
|
|
completion_type: 'tool_use_single',
|
|
language_name: 'none',
|
|
}),
|
|
[],
|
|
)
|
|
|
|
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
|
|
|
|
const handleSelect = useCallback(
|
|
(value: FallbackOptionValue, feedback?: string) => {
|
|
switch (value) {
|
|
case 'yes':
|
|
void logUnaryEvent({
|
|
completion_type: 'tool_use_single',
|
|
event: 'accept',
|
|
metadata: {
|
|
language_name: 'none',
|
|
message_id: toolUseConfirm.assistantMessage.message.id,
|
|
platform: env.platform,
|
|
},
|
|
})
|
|
toolUseConfirm.onAllow(toolUseConfirm.input, [], feedback)
|
|
onDone()
|
|
break
|
|
case 'yes-dont-ask-again': {
|
|
void logUnaryEvent({
|
|
completion_type: 'tool_use_single',
|
|
event: 'accept',
|
|
metadata: {
|
|
language_name: 'none',
|
|
message_id: toolUseConfirm.assistantMessage.message.id,
|
|
platform: env.platform,
|
|
},
|
|
})
|
|
|
|
toolUseConfirm.onAllow(toolUseConfirm.input, [
|
|
{
|
|
type: 'addRules',
|
|
rules: [
|
|
{
|
|
toolName: toolUseConfirm.tool.name,
|
|
},
|
|
],
|
|
behavior: 'allow',
|
|
destination: 'localSettings',
|
|
},
|
|
])
|
|
onDone()
|
|
break
|
|
}
|
|
case 'no':
|
|
void logUnaryEvent({
|
|
completion_type: 'tool_use_single',
|
|
event: 'reject',
|
|
metadata: {
|
|
language_name: 'none',
|
|
message_id: toolUseConfirm.assistantMessage.message.id,
|
|
platform: env.platform,
|
|
},
|
|
})
|
|
toolUseConfirm.onReject(feedback)
|
|
onReject()
|
|
onDone()
|
|
break
|
|
}
|
|
},
|
|
[toolUseConfirm, onDone, onReject],
|
|
)
|
|
|
|
const handleCancel = useCallback(() => {
|
|
void logUnaryEvent({
|
|
completion_type: 'tool_use_single',
|
|
event: 'reject',
|
|
metadata: {
|
|
language_name: 'none',
|
|
message_id: toolUseConfirm.assistantMessage.message.id,
|
|
platform: env.platform,
|
|
},
|
|
})
|
|
toolUseConfirm.onReject()
|
|
onReject()
|
|
onDone()
|
|
}, [toolUseConfirm, onDone, onReject])
|
|
|
|
const originalCwd = getOriginalCwd()
|
|
const showAlwaysAllowOptions = shouldShowAlwaysAllowOptions()
|
|
const options = useMemo((): PermissionPromptOption<FallbackOptionValue>[] => {
|
|
const result: PermissionPromptOption<FallbackOptionValue>[] = [
|
|
{
|
|
label: 'Yes',
|
|
value: 'yes',
|
|
feedbackConfig: { type: 'accept' },
|
|
},
|
|
]
|
|
|
|
if (showAlwaysAllowOptions) {
|
|
result.push({
|
|
label: (
|
|
<Text>
|
|
Yes, and don't ask again for <Text bold>{userFacingName}</Text>{' '}
|
|
commands in <Text bold>{originalCwd}</Text>
|
|
</Text>
|
|
),
|
|
value: 'yes-dont-ask-again',
|
|
})
|
|
}
|
|
|
|
result.push({
|
|
label: 'No',
|
|
value: 'no',
|
|
feedbackConfig: { type: 'reject' },
|
|
})
|
|
|
|
return result
|
|
}, [userFacingName, originalCwd, showAlwaysAllowOptions])
|
|
|
|
const toolAnalyticsContext = useMemo(
|
|
(): ToolAnalyticsContext => ({
|
|
toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name),
|
|
isMcp: toolUseConfirm.tool.isMcp ?? false,
|
|
}),
|
|
[toolUseConfirm.tool.name, toolUseConfirm.tool.isMcp],
|
|
)
|
|
|
|
return (
|
|
<PermissionDialog title="Tool use" workerBadge={workerBadge}>
|
|
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
|
<Text>
|
|
{userFacingName}(
|
|
{toolUseConfirm.tool.renderToolUseMessage(
|
|
toolUseConfirm.input as never,
|
|
{ theme, verbose: true },
|
|
)}
|
|
)
|
|
{originalUserFacingName.endsWith(' (MCP)') ? (
|
|
<Text dimColor> (MCP)</Text>
|
|
) : (
|
|
''
|
|
)}
|
|
</Text>
|
|
<Text dimColor>{truncateToLines(toolUseConfirm.description, 3)}</Text>
|
|
</Box>
|
|
|
|
<Box flexDirection="column">
|
|
<PermissionRuleExplanation
|
|
permissionResult={toolUseConfirm.permissionResult}
|
|
toolType="tool"
|
|
/>
|
|
<PermissionPrompt
|
|
options={options}
|
|
onSelect={handleSelect}
|
|
onCancel={handleCancel}
|
|
toolAnalyticsContext={toolAnalyticsContext}
|
|
/>
|
|
</Box>
|
|
</PermissionDialog>
|
|
)
|
|
}
|