init claude-code
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,42 @@
|
||||
import type { ToolInput } from './useFilePermissionDialog.js'
|
||||
|
||||
export interface FileEdit {
|
||||
old_string: string
|
||||
new_string: string
|
||||
replace_all?: boolean
|
||||
}
|
||||
|
||||
export interface IDEDiffConfig {
|
||||
filePath: string
|
||||
edits?: FileEdit[]
|
||||
editMode?: 'single' | 'multiple'
|
||||
}
|
||||
|
||||
export interface IDEDiffChangeInput {
|
||||
file_path: string
|
||||
edits: FileEdit[]
|
||||
}
|
||||
|
||||
export interface IDEDiffSupport<TInput extends ToolInput> {
|
||||
getConfig(input: TInput): IDEDiffConfig
|
||||
applyChanges(input: TInput, modifiedEdits: FileEdit[]): TInput
|
||||
}
|
||||
|
||||
export function createSingleEditDiffConfig(
|
||||
filePath: string,
|
||||
oldString: string,
|
||||
newString: string,
|
||||
replaceAll?: boolean,
|
||||
): IDEDiffConfig {
|
||||
return {
|
||||
filePath,
|
||||
edits: [
|
||||
{
|
||||
old_string: oldString,
|
||||
new_string: newString,
|
||||
replace_all: replaceAll,
|
||||
},
|
||||
],
|
||||
editMode: 'single',
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,212 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useAppState } from 'src/state/AppState.js'
|
||||
import { useKeybindings } from '../../../keybindings/useKeybinding.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../../services/analytics/index.js'
|
||||
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'
|
||||
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'
|
||||
import type { CompletionType } from '../../../utils/unaryLogging.js'
|
||||
import type { ToolUseConfirm } from '../PermissionRequest.js'
|
||||
import {
|
||||
type FileOperationType,
|
||||
getFilePermissionOptions,
|
||||
type PermissionOption,
|
||||
type PermissionOptionWithLabel,
|
||||
} from './permissionOptions.js'
|
||||
import {
|
||||
PERMISSION_HANDLERS,
|
||||
type PermissionHandlerParams,
|
||||
} from './usePermissionHandler.js'
|
||||
|
||||
export interface ToolInput {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type UseFilePermissionDialogProps<T extends ToolInput> = {
|
||||
filePath: string
|
||||
completionType: CompletionType
|
||||
languageName: string | Promise<string>
|
||||
toolUseConfirm: ToolUseConfirm
|
||||
onDone: () => void
|
||||
onReject: () => void
|
||||
parseInput: (input: unknown) => T
|
||||
operationType?: FileOperationType
|
||||
}
|
||||
|
||||
export type UseFilePermissionDialogResult<T> = {
|
||||
options: PermissionOptionWithLabel[]
|
||||
onChange: (option: PermissionOption, input: T, feedback?: string) => void
|
||||
acceptFeedback: string
|
||||
rejectFeedback: string
|
||||
focusedOption: string
|
||||
setFocusedOption: (option: string) => void
|
||||
handleInputModeToggle: (value: string) => void
|
||||
yesInputMode: boolean
|
||||
noInputMode: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for handling file permission dialogs with common logic
|
||||
*/
|
||||
export function useFilePermissionDialog<T extends ToolInput>({
|
||||
filePath,
|
||||
completionType,
|
||||
languageName,
|
||||
toolUseConfirm,
|
||||
onDone,
|
||||
onReject,
|
||||
parseInput,
|
||||
operationType = 'write',
|
||||
}: UseFilePermissionDialogProps<T>): UseFilePermissionDialogResult<T> {
|
||||
const toolPermissionContext = useAppState(s => s.toolPermissionContext)
|
||||
const [acceptFeedback, setAcceptFeedback] = useState('')
|
||||
const [rejectFeedback, setRejectFeedback] = useState('')
|
||||
const [focusedOption, setFocusedOption] = useState('yes')
|
||||
const [yesInputMode, setYesInputMode] = useState(false)
|
||||
const [noInputMode, setNoInputMode] = useState(false)
|
||||
// Track whether user ever entered feedback mode (persists after collapse)
|
||||
const [yesFeedbackModeEntered, setYesFeedbackModeEntered] = useState(false)
|
||||
const [noFeedbackModeEntered, setNoFeedbackModeEntered] = useState(false)
|
||||
|
||||
// Generate options based on context
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getFilePermissionOptions({
|
||||
filePath,
|
||||
toolPermissionContext,
|
||||
operationType,
|
||||
onRejectFeedbackChange: setRejectFeedback,
|
||||
onAcceptFeedbackChange: setAcceptFeedback,
|
||||
yesInputMode,
|
||||
noInputMode,
|
||||
}),
|
||||
[filePath, toolPermissionContext, operationType, yesInputMode, noInputMode],
|
||||
)
|
||||
|
||||
// Handle option selection using shared handlers
|
||||
const onChange = useCallback(
|
||||
(option: PermissionOption, input: T, feedback?: string) => {
|
||||
const params: PermissionHandlerParams = {
|
||||
messageId: toolUseConfirm.assistantMessage.message.id,
|
||||
path: filePath,
|
||||
toolUseConfirm,
|
||||
toolPermissionContext,
|
||||
onDone,
|
||||
onReject,
|
||||
completionType,
|
||||
languageName,
|
||||
operationType,
|
||||
}
|
||||
|
||||
// Override the input in toolUseConfirm to pass the parsed input
|
||||
const originalOnAllow = toolUseConfirm.onAllow
|
||||
toolUseConfirm.onAllow = (
|
||||
_input: unknown,
|
||||
permissionUpdates: PermissionUpdate[],
|
||||
feedback?: string,
|
||||
) => {
|
||||
originalOnAllow(input, permissionUpdates, feedback)
|
||||
}
|
||||
|
||||
const handler = PERMISSION_HANDLERS[option.type]
|
||||
handler(params, {
|
||||
feedback,
|
||||
hasFeedback: !!feedback,
|
||||
enteredFeedbackMode:
|
||||
option.type === 'accept-once'
|
||||
? yesFeedbackModeEntered
|
||||
: noFeedbackModeEntered,
|
||||
scope: option.type === 'accept-session' ? option.scope : undefined,
|
||||
})
|
||||
},
|
||||
[
|
||||
filePath,
|
||||
completionType,
|
||||
languageName,
|
||||
toolUseConfirm,
|
||||
toolPermissionContext,
|
||||
onDone,
|
||||
onReject,
|
||||
operationType,
|
||||
yesFeedbackModeEntered,
|
||||
noFeedbackModeEntered,
|
||||
],
|
||||
)
|
||||
|
||||
// Handler for confirm:cycleMode - select accept-session option
|
||||
const handleCycleMode = useCallback(() => {
|
||||
const sessionOption = options.find(o => o.option.type === 'accept-session')
|
||||
if (sessionOption) {
|
||||
const parsedInput = parseInput(toolUseConfirm.input)
|
||||
onChange(sessionOption.option, parsedInput)
|
||||
}
|
||||
}, [options, parseInput, toolUseConfirm.input, onChange])
|
||||
|
||||
// Register keyboard shortcut handler via keybindings system
|
||||
useKeybindings(
|
||||
{ 'confirm:cycleMode': handleCycleMode },
|
||||
{ context: 'Confirmation' },
|
||||
)
|
||||
|
||||
// Wrap setFocusedOption and reset input mode when navigating away
|
||||
const handleFocusedOptionChange = useCallback(
|
||||
(value: string) => {
|
||||
// Reset input mode when navigating away, but only if no text typed
|
||||
if (value !== 'yes' && yesInputMode && !acceptFeedback.trim()) {
|
||||
setYesInputMode(false)
|
||||
}
|
||||
if (value !== 'no' && noInputMode && !rejectFeedback.trim()) {
|
||||
setNoInputMode(false)
|
||||
}
|
||||
setFocusedOption(value)
|
||||
},
|
||||
[yesInputMode, noInputMode, acceptFeedback, rejectFeedback],
|
||||
)
|
||||
|
||||
// Handle Tab key toggling input mode for Yes/No options
|
||||
const handleInputModeToggle = useCallback(
|
||||
(value: string) => {
|
||||
const analyticsProps = {
|
||||
toolName: sanitizeToolNameForAnalytics(
|
||||
toolUseConfirm.tool.name,
|
||||
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
isMcp: toolUseConfirm.tool.isMcp ?? false,
|
||||
}
|
||||
|
||||
if (value === 'yes') {
|
||||
if (yesInputMode) {
|
||||
setYesInputMode(false)
|
||||
logEvent('tengu_accept_feedback_mode_collapsed', analyticsProps)
|
||||
} else {
|
||||
setYesInputMode(true)
|
||||
setYesFeedbackModeEntered(true)
|
||||
logEvent('tengu_accept_feedback_mode_entered', analyticsProps)
|
||||
}
|
||||
} else if (value === 'no') {
|
||||
if (noInputMode) {
|
||||
setNoInputMode(false)
|
||||
logEvent('tengu_reject_feedback_mode_collapsed', analyticsProps)
|
||||
} else {
|
||||
setNoInputMode(true)
|
||||
setNoFeedbackModeEntered(true)
|
||||
logEvent('tengu_reject_feedback_mode_entered', analyticsProps)
|
||||
}
|
||||
}
|
||||
},
|
||||
[yesInputMode, noInputMode, toolUseConfirm],
|
||||
)
|
||||
|
||||
return {
|
||||
options,
|
||||
onChange,
|
||||
acceptFeedback,
|
||||
rejectFeedback,
|
||||
focusedOption,
|
||||
setFocusedOption: handleFocusedOptionChange,
|
||||
handleInputModeToggle,
|
||||
yesInputMode,
|
||||
noInputMode,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../../services/analytics/index.js'
|
||||
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'
|
||||
import type { ToolPermissionContext } from '../../../Tool.js'
|
||||
import {
|
||||
CLAUDE_FOLDER_PERMISSION_PATTERN,
|
||||
FILE_EDIT_TOOL_NAME,
|
||||
GLOBAL_CLAUDE_FOLDER_PERMISSION_PATTERN,
|
||||
} from '../../../tools/FileEditTool/constants.js'
|
||||
import { env } from '../../../utils/env.js'
|
||||
import { generateSuggestions } from '../../../utils/permissions/filesystem.js'
|
||||
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'
|
||||
import {
|
||||
type CompletionType,
|
||||
logUnaryEvent,
|
||||
} from '../../../utils/unaryLogging.js'
|
||||
import type { ToolUseConfirm } from '../PermissionRequest.js'
|
||||
import type {
|
||||
FileOperationType,
|
||||
PermissionOption,
|
||||
} from './permissionOptions.js'
|
||||
|
||||
function logPermissionEvent(
|
||||
event: 'accept' | 'reject',
|
||||
completionType: CompletionType,
|
||||
languageName: string | Promise<string>,
|
||||
messageId: string,
|
||||
hasFeedback?: boolean,
|
||||
): void {
|
||||
void logUnaryEvent({
|
||||
completion_type: completionType,
|
||||
event,
|
||||
metadata: {
|
||||
language_name: languageName,
|
||||
message_id: messageId,
|
||||
platform: env.platform,
|
||||
hasFeedback: hasFeedback ?? false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export type PermissionHandlerParams = {
|
||||
messageId: string
|
||||
path: string | null
|
||||
toolUseConfirm: ToolUseConfirm
|
||||
toolPermissionContext: ToolPermissionContext
|
||||
onDone: () => void
|
||||
onReject: () => void
|
||||
completionType: CompletionType
|
||||
languageName: string | Promise<string>
|
||||
operationType: FileOperationType
|
||||
}
|
||||
|
||||
export type PermissionHandlerOptions = {
|
||||
hasFeedback?: boolean
|
||||
feedback?: string
|
||||
enteredFeedbackMode?: boolean
|
||||
scope?: 'claude-folder' | 'global-claude-folder'
|
||||
}
|
||||
|
||||
function handleAcceptOnce(
|
||||
params: PermissionHandlerParams,
|
||||
options?: PermissionHandlerOptions,
|
||||
): void {
|
||||
const { messageId, toolUseConfirm, onDone, completionType, languageName } =
|
||||
params
|
||||
|
||||
logPermissionEvent('accept', completionType, languageName, messageId)
|
||||
|
||||
// Log accept submission with feedback context
|
||||
logEvent('tengu_accept_submitted', {
|
||||
toolName: sanitizeToolNameForAnalytics(
|
||||
toolUseConfirm.tool.name,
|
||||
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
isMcp: toolUseConfirm.tool.isMcp ?? false,
|
||||
has_instructions: !!options?.feedback,
|
||||
instructions_length: options?.feedback?.length ?? 0,
|
||||
entered_feedback_mode: options?.enteredFeedbackMode ?? false,
|
||||
})
|
||||
|
||||
onDone()
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [], options?.feedback)
|
||||
}
|
||||
|
||||
function handleAcceptSession(
|
||||
params: PermissionHandlerParams,
|
||||
options?: PermissionHandlerOptions,
|
||||
): void {
|
||||
const {
|
||||
messageId,
|
||||
path,
|
||||
toolUseConfirm,
|
||||
toolPermissionContext,
|
||||
onDone,
|
||||
completionType,
|
||||
languageName,
|
||||
operationType,
|
||||
} = params
|
||||
|
||||
logPermissionEvent('accept', completionType, languageName, messageId)
|
||||
|
||||
// For claude-folder scope, grant session-level access to all .claude/ files
|
||||
if (
|
||||
options?.scope === 'claude-folder' ||
|
||||
options?.scope === 'global-claude-folder'
|
||||
) {
|
||||
const pattern =
|
||||
options.scope === 'global-claude-folder'
|
||||
? GLOBAL_CLAUDE_FOLDER_PERMISSION_PATTERN
|
||||
: CLAUDE_FOLDER_PERMISSION_PATTERN
|
||||
const suggestions: PermissionUpdate[] = [
|
||||
{
|
||||
type: 'addRules',
|
||||
rules: [
|
||||
{
|
||||
toolName: FILE_EDIT_TOOL_NAME,
|
||||
ruleContent: pattern,
|
||||
},
|
||||
],
|
||||
behavior: 'allow',
|
||||
destination: 'session',
|
||||
},
|
||||
]
|
||||
onDone()
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, suggestions)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate permission updates if path is provided
|
||||
const suggestions = path
|
||||
? generateSuggestions(path, operationType, toolPermissionContext)
|
||||
: []
|
||||
|
||||
onDone()
|
||||
// Pass permission updates directly to onAllow
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, suggestions)
|
||||
}
|
||||
|
||||
function handleReject(
|
||||
params: PermissionHandlerParams,
|
||||
options?: PermissionHandlerOptions,
|
||||
): void {
|
||||
const {
|
||||
messageId,
|
||||
toolUseConfirm,
|
||||
onDone,
|
||||
onReject,
|
||||
completionType,
|
||||
languageName,
|
||||
} = params
|
||||
|
||||
logPermissionEvent(
|
||||
'reject',
|
||||
completionType,
|
||||
languageName,
|
||||
messageId,
|
||||
options?.hasFeedback,
|
||||
)
|
||||
|
||||
// Log reject submission with feedback context
|
||||
logEvent('tengu_reject_submitted', {
|
||||
toolName: sanitizeToolNameForAnalytics(
|
||||
toolUseConfirm.tool.name,
|
||||
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
isMcp: toolUseConfirm.tool.isMcp ?? false,
|
||||
has_instructions: !!options?.feedback,
|
||||
instructions_length: options?.feedback?.length ?? 0,
|
||||
entered_feedback_mode: options?.enteredFeedbackMode ?? false,
|
||||
})
|
||||
|
||||
onDone()
|
||||
onReject()
|
||||
toolUseConfirm.onReject(options?.feedback)
|
||||
}
|
||||
|
||||
export const PERMISSION_HANDLERS: Record<
|
||||
PermissionOption['type'],
|
||||
(params: PermissionHandlerParams, options?: PermissionHandlerOptions) => void
|
||||
> = {
|
||||
'accept-once': handleAcceptOnce,
|
||||
'accept-session': handleAcceptSession,
|
||||
reject: handleReject,
|
||||
}
|
||||
Reference in New Issue
Block a user