init claude-code
This commit is contained in:
+645
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,179 @@
|
||||
import { useCallback, useReducer } from 'react'
|
||||
|
||||
export type AnswerValue = string
|
||||
|
||||
export type QuestionState = {
|
||||
selectedValue?: string | string[]
|
||||
textInputValue: string
|
||||
}
|
||||
|
||||
type State = {
|
||||
currentQuestionIndex: number
|
||||
answers: Record<string, AnswerValue>
|
||||
questionStates: Record<string, QuestionState>
|
||||
isInTextInput: boolean
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: 'next-question' }
|
||||
| { type: 'prev-question' }
|
||||
| {
|
||||
type: 'update-question-state'
|
||||
questionText: string
|
||||
updates: Partial<QuestionState>
|
||||
isMultiSelect: boolean
|
||||
}
|
||||
| {
|
||||
type: 'set-answer'
|
||||
questionText: string
|
||||
answer: string
|
||||
shouldAdvance: boolean
|
||||
}
|
||||
| { type: 'set-text-input-mode'; isInInput: boolean }
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'next-question':
|
||||
return {
|
||||
...state,
|
||||
currentQuestionIndex: state.currentQuestionIndex + 1,
|
||||
isInTextInput: false,
|
||||
}
|
||||
|
||||
case 'prev-question':
|
||||
return {
|
||||
...state,
|
||||
currentQuestionIndex: Math.max(0, state.currentQuestionIndex - 1),
|
||||
isInTextInput: false,
|
||||
}
|
||||
|
||||
case 'update-question-state': {
|
||||
const existing = state.questionStates[action.questionText]
|
||||
const newState: QuestionState = {
|
||||
selectedValue:
|
||||
action.updates.selectedValue ??
|
||||
existing?.selectedValue ??
|
||||
(action.isMultiSelect ? [] : undefined),
|
||||
textInputValue:
|
||||
action.updates.textInputValue ?? existing?.textInputValue ?? '',
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
questionStates: {
|
||||
...state.questionStates,
|
||||
[action.questionText]: newState,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
case 'set-answer': {
|
||||
const newState = {
|
||||
...state,
|
||||
answers: {
|
||||
...state.answers,
|
||||
[action.questionText]: action.answer,
|
||||
},
|
||||
}
|
||||
|
||||
if (action.shouldAdvance) {
|
||||
return {
|
||||
...newState,
|
||||
currentQuestionIndex: newState.currentQuestionIndex + 1,
|
||||
isInTextInput: false,
|
||||
}
|
||||
}
|
||||
|
||||
return newState
|
||||
}
|
||||
|
||||
case 'set-text-input-mode':
|
||||
return {
|
||||
...state,
|
||||
isInTextInput: action.isInInput,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
currentQuestionIndex: 0,
|
||||
answers: {},
|
||||
questionStates: {},
|
||||
isInTextInput: false,
|
||||
}
|
||||
|
||||
export type MultipleChoiceState = {
|
||||
currentQuestionIndex: number
|
||||
answers: Record<string, AnswerValue>
|
||||
questionStates: Record<string, QuestionState>
|
||||
isInTextInput: boolean
|
||||
nextQuestion: () => void
|
||||
prevQuestion: () => void
|
||||
updateQuestionState: (
|
||||
questionText: string,
|
||||
updates: Partial<QuestionState>,
|
||||
isMultiSelect: boolean,
|
||||
) => void
|
||||
setAnswer: (
|
||||
questionText: string,
|
||||
answer: string,
|
||||
shouldAdvance?: boolean,
|
||||
) => void
|
||||
setTextInputMode: (isInInput: boolean) => void
|
||||
}
|
||||
|
||||
export function useMultipleChoiceState(): MultipleChoiceState {
|
||||
const [state, dispatch] = useReducer(reducer, INITIAL_STATE)
|
||||
|
||||
const nextQuestion = useCallback(() => {
|
||||
dispatch({ type: 'next-question' })
|
||||
}, [])
|
||||
|
||||
const prevQuestion = useCallback(() => {
|
||||
dispatch({ type: 'prev-question' })
|
||||
}, [])
|
||||
|
||||
const updateQuestionState = useCallback(
|
||||
(
|
||||
questionText: string,
|
||||
updates: Partial<QuestionState>,
|
||||
isMultiSelect: boolean,
|
||||
) => {
|
||||
dispatch({
|
||||
type: 'update-question-state',
|
||||
questionText,
|
||||
updates,
|
||||
isMultiSelect,
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const setAnswer = useCallback(
|
||||
(questionText: string, answer: string, shouldAdvance: boolean = true) => {
|
||||
dispatch({
|
||||
type: 'set-answer',
|
||||
questionText,
|
||||
answer,
|
||||
shouldAdvance,
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const setTextInputMode = useCallback((isInInput: boolean) => {
|
||||
dispatch({ type: 'set-text-input-mode', isInInput })
|
||||
}, [])
|
||||
|
||||
return {
|
||||
currentQuestionIndex: state.currentQuestionIndex,
|
||||
answers: state.answers,
|
||||
questionStates: state.questionStates,
|
||||
isInTextInput: state.isInTextInput,
|
||||
nextQuestion,
|
||||
prevQuestion,
|
||||
updateQuestionState,
|
||||
setAnswer,
|
||||
setTextInputMode,
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+122
File diff suppressed because one or more lines are too long
+768
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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,
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+166
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,66 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import type { WorkerBadgeProps } from './WorkerBadge.js';
|
||||
type Props = {
|
||||
title: string;
|
||||
subtitle?: React.ReactNode;
|
||||
color?: keyof Theme;
|
||||
workerBadge?: WorkerBadgeProps;
|
||||
};
|
||||
export function PermissionRequestTitle(t0) {
|
||||
const $ = _c(13);
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
color: t1,
|
||||
workerBadge
|
||||
} = t0;
|
||||
const color = t1 === undefined ? "permission" : t1;
|
||||
let t2;
|
||||
if ($[0] !== color || $[1] !== title) {
|
||||
t2 = <Text bold={true} color={color}>{title}</Text>;
|
||||
$[0] = color;
|
||||
$[1] = title;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== workerBadge) {
|
||||
t3 = workerBadge && <Text dimColor={true}>{"\xB7 "}@{workerBadge.name}</Text>;
|
||||
$[3] = workerBadge;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== t2 || $[6] !== t3) {
|
||||
t4 = <Box flexDirection="row" gap={1}>{t2}{t3}</Box>;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== subtitle) {
|
||||
t5 = subtitle != null && (typeof subtitle === "string" ? <Text dimColor={true} wrap="truncate-start">{subtitle}</Text> : subtitle);
|
||||
$[8] = subtitle;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
let t6;
|
||||
if ($[10] !== t4 || $[11] !== t5) {
|
||||
t6 = <Box flexDirection="column">{t4}{t5}</Box>;
|
||||
$[10] = t4;
|
||||
$[11] = t5;
|
||||
$[12] = t6;
|
||||
} else {
|
||||
t6 = $[12];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkJveCIsIlRleHQiLCJUaGVtZSIsIldvcmtlckJhZGdlUHJvcHMiLCJQcm9wcyIsInRpdGxlIiwic3VidGl0bGUiLCJSZWFjdE5vZGUiLCJjb2xvciIsIndvcmtlckJhZGdlIiwiUGVybWlzc2lvblJlcXVlc3RUaXRsZSIsInQwIiwiJCIsIl9jIiwidDEiLCJ1bmRlZmluZWQiLCJ0MiIsInQzIiwibmFtZSIsInQ0IiwidDUiLCJ0NiJdLCJzb3VyY2VzIjpbIlBlcm1pc3Npb25SZXF1ZXN0VGl0bGUudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgQm94LCBUZXh0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHR5cGUgeyBUaGVtZSB9IGZyb20gJy4uLy4uL3V0aWxzL3RoZW1lLmpzJ1xuaW1wb3J0IHR5cGUgeyBXb3JrZXJCYWRnZVByb3BzIH0gZnJvbSAnLi9Xb3JrZXJCYWRnZS5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgdGl0bGU6IHN0cmluZ1xuICBzdWJ0aXRsZT86IFJlYWN0LlJlYWN0Tm9kZVxuICBjb2xvcj86IGtleW9mIFRoZW1lXG4gIHdvcmtlckJhZGdlPzogV29ya2VyQmFkZ2VQcm9wc1xufVxuXG5leHBvcnQgZnVuY3Rpb24gUGVybWlzc2lvblJlcXVlc3RUaXRsZSh7XG4gIHRpdGxlLFxuICBzdWJ0aXRsZSxcbiAgY29sb3IgPSAncGVybWlzc2lvbicsXG4gIHdvcmtlckJhZGdlLFxufTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICByZXR1cm4gKFxuICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCIgZ2FwPXsxfT5cbiAgICAgICAgPFRleHQgYm9sZCBjb2xvcj17Y29sb3J9PlxuICAgICAgICAgIHt0aXRsZX1cbiAgICAgICAgPC9UZXh0PlxuICAgICAgICB7d29ya2VyQmFkZ2UgJiYgKFxuICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPlxuICAgICAgICAgICAgeyfCtyAnfUB7d29ya2VyQmFkZ2UubmFtZX1cbiAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICl9XG4gICAgICA8L0JveD5cbiAgICAgIHtzdWJ0aXRsZSAhPSBudWxsICYmXG4gICAgICAgICh0eXBlb2Ygc3VidGl0bGUgPT09ICdzdHJpbmcnID8gKFxuICAgICAgICAgIDxUZXh0IGRpbUNvbG9yIHdyYXA9XCJ0cnVuY2F0ZS1zdGFydFwiPlxuICAgICAgICAgICAge3N1YnRpdGxlfVxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgKSA6IChcbiAgICAgICAgICBzdWJ0aXRsZVxuICAgICAgICApKX1cbiAgICA8L0JveD5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBTyxLQUFLQSxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxHQUFHLEVBQUVDLElBQUksUUFBUSxjQUFjO0FBQ3hDLGNBQWNDLEtBQUssUUFBUSxzQkFBc0I7QUFDakQsY0FBY0MsZ0JBQWdCLFFBQVEsa0JBQWtCO0FBRXhELEtBQUtDLEtBQUssR0FBRztFQUNYQyxLQUFLLEVBQUUsTUFBTTtFQUNiQyxRQUFRLENBQUMsRUFBRVAsS0FBSyxDQUFDUSxTQUFTO0VBQzFCQyxLQUFLLENBQUMsRUFBRSxNQUFNTixLQUFLO0VBQ25CTyxXQUFXLENBQUMsRUFBRU4sZ0JBQWdCO0FBQ2hDLENBQUM7QUFFRCxPQUFPLFNBQUFPLHVCQUFBQyxFQUFBO0VBQUEsTUFBQUMsQ0FBQSxHQUFBQyxFQUFBO0VBQWdDO0lBQUFSLEtBQUE7SUFBQUMsUUFBQTtJQUFBRSxLQUFBLEVBQUFNLEVBQUE7SUFBQUw7RUFBQSxJQUFBRSxFQUsvQjtFQUZOLE1BQUFILEtBQUEsR0FBQU0sRUFBb0IsS0FBcEJDLFNBQW9CLEdBQXBCLFlBQW9CLEdBQXBCRCxFQUFvQjtFQUFBLElBQUFFLEVBQUE7RUFBQSxJQUFBSixDQUFBLFFBQUFKLEtBQUEsSUFBQUksQ0FBQSxRQUFBUCxLQUFBO0lBTWRXLEVBQUEsSUFBQyxJQUFJLENBQUMsSUFBSSxDQUFKLEtBQUcsQ0FBQyxDQUFRUixLQUFLLENBQUxBLE1BQUksQ0FBQyxDQUNwQkgsTUFBSSxDQUNQLEVBRkMsSUFBSSxDQUVFO0lBQUFPLENBQUEsTUFBQUosS0FBQTtJQUFBSSxDQUFBLE1BQUFQLEtBQUE7SUFBQU8sQ0FBQSxNQUFBSSxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBSixDQUFBO0VBQUE7RUFBQSxJQUFBSyxFQUFBO0VBQUEsSUFBQUwsQ0FBQSxRQUFBSCxXQUFBO0lBQ05RLEVBQUEsR0FBQVIsV0FJQSxJQUhDLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBUixLQUFPLENBQUMsQ0FDWCxRQUFHLENBQUUsQ0FBRSxDQUFBQSxXQUFXLENBQUFTLElBQUksQ0FDekIsRUFGQyxJQUFJLENBR047SUFBQU4sQ0FBQSxNQUFBSCxXQUFBO0lBQUFHLENBQUEsTUFBQUssRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUwsQ0FBQTtFQUFBO0VBQUEsSUFBQU8sRUFBQTtFQUFBLElBQUFQLENBQUEsUUFBQUksRUFBQSxJQUFBSixDQUFBLFFBQUFLLEVBQUE7SUFSSEUsRUFBQSxJQUFDLEdBQUcsQ0FBZSxhQUFLLENBQUwsS0FBSyxDQUFNLEdBQUMsQ0FBRCxHQUFDLENBQzdCLENBQUFILEVBRU0sQ0FDTCxDQUFBQyxFQUlELENBQ0YsRUFUQyxHQUFHLENBU0U7SUFBQUwsQ0FBQSxNQUFBSSxFQUFBO0lBQUFKLENBQUEsTUFBQUssRUFBQTtJQUFBTCxDQUFBLE1BQUFPLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFQLENBQUE7RUFBQTtFQUFBLElBQUFRLEVBQUE7RUFBQSxJQUFBUixDQUFBLFFBQUFOLFFBQUE7SUFDTGMsRUFBQSxHQUFBZCxRQUFRLElBQUksSUFPVCxLQU5ELE9BQU9BLFFBQVEsS0FBSyxRQU1wQixHQUxDLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBUixLQUFPLENBQUMsQ0FBTSxJQUFnQixDQUFoQixnQkFBZ0IsQ0FDakNBLFNBQU8sQ0FDVixFQUZDLElBQUksQ0FLTixHQU5BQSxRQU1DO0lBQUFNLENBQUEsTUFBQU4sUUFBQTtJQUFBTSxDQUFBLE1BQUFRLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFSLENBQUE7RUFBQTtFQUFBLElBQUFTLEVBQUE7RUFBQSxJQUFBVCxDQUFBLFNBQUFPLEVBQUEsSUFBQVAsQ0FBQSxTQUFBUSxFQUFBO0lBbEJOQyxFQUFBLElBQUMsR0FBRyxDQUFlLGFBQVEsQ0FBUixRQUFRLENBQ3pCLENBQUFGLEVBU0ssQ0FDSixDQUFBQyxFQU9FLENBQ0wsRUFuQkMsR0FBRyxDQW1CRTtJQUFBUixDQUFBLE9BQUFPLEVBQUE7SUFBQVAsQ0FBQSxPQUFBUSxFQUFBO0lBQUFSLENBQUEsT0FBQVMsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQVQsQ0FBQTtFQUFBO0VBQUEsT0FuQk5TLEVBbUJNO0FBQUEiLCJpZ25vcmVMaXN0IjpbXX0=
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,49 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { toInkColor } from '../../utils/ink.js';
|
||||
export type WorkerBadgeProps = {
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a colored badge showing the worker's name for permission prompts.
|
||||
* Used to indicate which swarm worker is requesting the permission.
|
||||
*/
|
||||
export function WorkerBadge(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
name,
|
||||
color
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== color) {
|
||||
t1 = toInkColor(color);
|
||||
$[0] = color;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const inkColor = t1;
|
||||
let t2;
|
||||
if ($[2] !== name) {
|
||||
t2 = <Text bold={true}>@{name}</Text>;
|
||||
$[2] = name;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== inkColor || $[5] !== t2) {
|
||||
t3 = <Box flexDirection="row" gap={1}><Text color={inkColor}>{BLACK_CIRCLE} {t2}</Text></Box>;
|
||||
$[4] = inkColor;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkJMQUNLX0NJUkNMRSIsIkJveCIsIlRleHQiLCJ0b0lua0NvbG9yIiwiV29ya2VyQmFkZ2VQcm9wcyIsIm5hbWUiLCJjb2xvciIsIldvcmtlckJhZGdlIiwidDAiLCIkIiwiX2MiLCJ0MSIsImlua0NvbG9yIiwidDIiLCJ0MyJdLCJzb3VyY2VzIjpbIldvcmtlckJhZGdlLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEJMQUNLX0NJUkNMRSB9IGZyb20gJy4uLy4uL2NvbnN0YW50cy9maWd1cmVzLmpzJ1xuaW1wb3J0IHsgQm94LCBUZXh0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHsgdG9JbmtDb2xvciB9IGZyb20gJy4uLy4uL3V0aWxzL2luay5qcydcblxuZXhwb3J0IHR5cGUgV29ya2VyQmFkZ2VQcm9wcyA9IHtcbiAgbmFtZTogc3RyaW5nXG4gIGNvbG9yOiBzdHJpbmdcbn1cblxuLyoqXG4gKiBSZW5kZXJzIGEgY29sb3JlZCBiYWRnZSBzaG93aW5nIHRoZSB3b3JrZXIncyBuYW1lIGZvciBwZXJtaXNzaW9uIHByb21wdHMuXG4gKiBVc2VkIHRvIGluZGljYXRlIHdoaWNoIHN3YXJtIHdvcmtlciBpcyByZXF1ZXN0aW5nIHRoZSBwZXJtaXNzaW9uLlxuICovXG5leHBvcnQgZnVuY3Rpb24gV29ya2VyQmFkZ2Uoe1xuICBuYW1lLFxuICBjb2xvcixcbn06IFdvcmtlckJhZGdlUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBpbmtDb2xvciA9IHRvSW5rQ29sb3IoY29sb3IpXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCIgZ2FwPXsxfT5cbiAgICAgIDxUZXh0IGNvbG9yPXtpbmtDb2xvcn0+XG4gICAgICAgIHtCTEFDS19DSVJDTEV9IDxUZXh0IGJvbGQ+QHtuYW1lfTwvVGV4dD5cbiAgICAgIDwvVGV4dD5cbiAgICA8L0JveD5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBTyxLQUFLQSxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxZQUFZLFFBQVEsNEJBQTRCO0FBQ3pELFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxRQUFRLGNBQWM7QUFDeEMsU0FBU0MsVUFBVSxRQUFRLG9CQUFvQjtBQUUvQyxPQUFPLEtBQUtDLGdCQUFnQixHQUFHO0VBQzdCQyxJQUFJLEVBQUUsTUFBTTtFQUNaQyxLQUFLLEVBQUUsTUFBTTtBQUNmLENBQUM7O0FBRUQ7QUFDQTtBQUNBO0FBQ0E7QUFDQSxPQUFPLFNBQUFDLFlBQUFDLEVBQUE7RUFBQSxNQUFBQyxDQUFBLEdBQUFDLEVBQUE7RUFBcUI7SUFBQUwsSUFBQTtJQUFBQztFQUFBLElBQUFFLEVBR1Q7RUFBQSxJQUFBRyxFQUFBO0VBQUEsSUFBQUYsQ0FBQSxRQUFBSCxLQUFBO0lBQ0FLLEVBQUEsR0FBQVIsVUFBVSxDQUFDRyxLQUFLLENBQUM7SUFBQUcsQ0FBQSxNQUFBSCxLQUFBO0lBQUFHLENBQUEsTUFBQUUsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUYsQ0FBQTtFQUFBO0VBQWxDLE1BQUFHLFFBQUEsR0FBaUJELEVBQWlCO0VBQUEsSUFBQUUsRUFBQTtFQUFBLElBQUFKLENBQUEsUUFBQUosSUFBQTtJQUliUSxFQUFBLElBQUMsSUFBSSxDQUFDLElBQUksQ0FBSixLQUFHLENBQUMsQ0FBQyxDQUFFUixLQUFHLENBQUUsRUFBakIsSUFBSSxDQUFvQjtJQUFBSSxDQUFBLE1BQUFKLElBQUE7SUFBQUksQ0FBQSxNQUFBSSxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBSixDQUFBO0VBQUE7RUFBQSxJQUFBSyxFQUFBO0VBQUEsSUFBQUwsQ0FBQSxRQUFBRyxRQUFBLElBQUFILENBQUEsUUFBQUksRUFBQTtJQUY1Q0MsRUFBQSxJQUFDLEdBQUcsQ0FBZSxhQUFLLENBQUwsS0FBSyxDQUFNLEdBQUMsQ0FBRCxHQUFDLENBQzdCLENBQUMsSUFBSSxDQUFRRixLQUFRLENBQVJBLFNBQU8sQ0FBQyxDQUNsQlosYUFBVyxDQUFFLENBQUMsQ0FBQWEsRUFBd0IsQ0FDekMsRUFGQyxJQUFJLENBR1AsRUFKQyxHQUFHLENBSUU7SUFBQUosQ0FBQSxNQUFBRyxRQUFBO0lBQUFILENBQUEsTUFBQUksRUFBQTtJQUFBSixDQUFBLE1BQUFLLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFMLENBQUE7RUFBQTtFQUFBLE9BSk5LLEVBSU07QUFBQSIsImlnbm9yZUxpc3QiOltdfQ==
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,209 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js'
|
||||
import { BashTool } from 'src/tools/BashTool/BashTool.js'
|
||||
import { splitCommand_DEPRECATED } from 'src/utils/bash/commands.js'
|
||||
import type {
|
||||
PermissionDecisionReason,
|
||||
PermissionResult,
|
||||
} from 'src/utils/permissions/PermissionResult.js'
|
||||
import {
|
||||
extractRules,
|
||||
hasRules,
|
||||
} from 'src/utils/permissions/PermissionUpdate.js'
|
||||
import { permissionRuleValueToString } from 'src/utils/permissions/permissionRuleParser.js'
|
||||
import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js'
|
||||
import type { ToolUseConfirm } from '../../components/permissions/PermissionRequest.js'
|
||||
import { useSetAppState } from '../../state/AppState.js'
|
||||
import { env } from '../../utils/env.js'
|
||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
||||
import { type CompletionType, logUnaryEvent } from '../../utils/unaryLogging.js'
|
||||
|
||||
export type UnaryEvent = {
|
||||
completion_type: CompletionType
|
||||
language_name: string | Promise<string>
|
||||
}
|
||||
|
||||
function permissionResultToLog(permissionResult: PermissionResult): string {
|
||||
switch (permissionResult.behavior) {
|
||||
case 'allow':
|
||||
return 'allow'
|
||||
case 'ask': {
|
||||
const rules = extractRules(permissionResult.suggestions)
|
||||
const suggestions =
|
||||
rules.length > 0
|
||||
? rules.map(r => permissionRuleValueToString(r)).join(', ')
|
||||
: 'none'
|
||||
return `ask: ${permissionResult.message},
|
||||
suggestions: ${suggestions}
|
||||
reason: ${decisionReasonToString(permissionResult.decisionReason)}`
|
||||
}
|
||||
case 'deny':
|
||||
return `deny: ${permissionResult.message},
|
||||
reason: ${decisionReasonToString(permissionResult.decisionReason)}`
|
||||
case 'passthrough': {
|
||||
const rules = extractRules(permissionResult.suggestions)
|
||||
const suggestions =
|
||||
rules.length > 0
|
||||
? rules.map(r => permissionRuleValueToString(r)).join(', ')
|
||||
: 'none'
|
||||
return `passthrough: ${permissionResult.message},
|
||||
suggestions: ${suggestions}
|
||||
reason: ${decisionReasonToString(permissionResult.decisionReason)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decisionReasonToString(
|
||||
decisionReason: PermissionDecisionReason | undefined,
|
||||
): string {
|
||||
if (!decisionReason) {
|
||||
return 'No decision reason'
|
||||
}
|
||||
if (
|
||||
(feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) &&
|
||||
decisionReason.type === 'classifier'
|
||||
) {
|
||||
return `Classifier: ${decisionReason.classifier}, Reason: ${decisionReason.reason}`
|
||||
}
|
||||
switch (decisionReason.type) {
|
||||
case 'rule':
|
||||
return `Rule: ${permissionRuleValueToString(decisionReason.rule.ruleValue)}`
|
||||
case 'mode':
|
||||
return `Mode: ${decisionReason.mode}`
|
||||
case 'subcommandResults':
|
||||
return `Subcommand Results: ${Array.from(decisionReason.reasons.entries())
|
||||
.map(([key, value]) => `${key}: ${permissionResultToLog(value)}`)
|
||||
.join(', \n')}`
|
||||
case 'permissionPromptTool':
|
||||
return `Permission Tool: ${decisionReason.permissionPromptToolName}, Result: ${jsonStringify(decisionReason.toolResult)}`
|
||||
case 'hook':
|
||||
return `Hook: ${decisionReason.hookName}${decisionReason.reason ? `, Reason: ${decisionReason.reason}` : ''}`
|
||||
case 'workingDir':
|
||||
return `Working Directory: ${decisionReason.reason}`
|
||||
case 'safetyCheck':
|
||||
return `Safety check: ${decisionReason.reason}`
|
||||
case 'other':
|
||||
return `Other: ${decisionReason.reason}`
|
||||
default:
|
||||
return jsonStringify(decisionReason, null, 2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs permission request events using analytics and unary logging.
|
||||
* Handles both the analytics event and the unary event logging.
|
||||
*/
|
||||
export function usePermissionRequestLogging(
|
||||
toolUseConfirm: ToolUseConfirm,
|
||||
unaryEvent: UnaryEvent,
|
||||
): void {
|
||||
const setAppState = useSetAppState()
|
||||
// Guard against effect re-firing if toolUseConfirm's object reference
|
||||
// changes during a single dialog's lifetime (e.g., parent re-renders with a
|
||||
// fresh object). Without this, the unconditional setAppState below can
|
||||
// cascade into an infinite microtask loop — each re-fire does another
|
||||
// setAppState spread + (ant builds) splitCommand → shell-quote regex,
|
||||
// pegging CPU at 100% and leaking ~500MB/min in JSRopeString/RegExp allocs.
|
||||
// The component is keyed by toolUseID, so this ref resets on remount —
|
||||
// we only need to dedupe re-fires WITHIN one dialog instance.
|
||||
const loggedToolUseID = useRef<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (loggedToolUseID.current === toolUseConfirm.toolUseID) {
|
||||
return
|
||||
}
|
||||
loggedToolUseID.current = toolUseConfirm.toolUseID
|
||||
|
||||
// Increment permission prompt count for attribution tracking
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
attribution: {
|
||||
...prev.attribution,
|
||||
permissionPromptCount: prev.attribution.permissionPromptCount + 1,
|
||||
},
|
||||
}))
|
||||
|
||||
// Log analytics event
|
||||
logEvent('tengu_tool_use_show_permission_request', {
|
||||
messageID: toolUseConfirm.assistantMessage.message
|
||||
.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name),
|
||||
isMcp: toolUseConfirm.tool.isMcp ?? false,
|
||||
decisionReasonType: toolUseConfirm.permissionResult.decisionReason
|
||||
?.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
sandboxEnabled: SandboxManager.isSandboxingEnabled(),
|
||||
})
|
||||
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
const permissionResult = toolUseConfirm.permissionResult
|
||||
if (
|
||||
toolUseConfirm.tool.name === BashTool.name &&
|
||||
permissionResult.behavior === 'ask' &&
|
||||
!hasRules(permissionResult.suggestions)
|
||||
) {
|
||||
// Log if no rule suggestions ("always allow") are provided
|
||||
logEvent('tengu_internal_tool_use_permission_request_no_always_allow', {
|
||||
messageID: toolUseConfirm.assistantMessage.message
|
||||
.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name),
|
||||
isMcp: toolUseConfirm.tool.isMcp ?? false,
|
||||
decisionReasonType: (permissionResult.decisionReason?.type ??
|
||||
'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
sandboxEnabled: SandboxManager.isSandboxingEnabled(),
|
||||
|
||||
// This DOES contain code/filepaths and should not be logged in the public build!
|
||||
decisionReasonDetails: decisionReasonToString(
|
||||
permissionResult.decisionReason,
|
||||
) as never,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// [ANT-ONLY] Log bash tool calls, so we can categorize
|
||||
// & burn down calls that should have been allowed
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
const parsedInput = BashTool.inputSchema.safeParse(toolUseConfirm.input)
|
||||
if (
|
||||
toolUseConfirm.tool.name === BashTool.name &&
|
||||
toolUseConfirm.permissionResult.behavior === 'ask' &&
|
||||
parsedInput.success
|
||||
) {
|
||||
// Note: All metadata fields in this event contain code/filepaths
|
||||
let split = [parsedInput.data.command]
|
||||
try {
|
||||
split = splitCommand_DEPRECATED(parsedInput.data.command)
|
||||
} catch {
|
||||
// Ignore parse errors here - just log the full command
|
||||
}
|
||||
logEvent('tengu_internal_bash_tool_use_permission_request', {
|
||||
parts: jsonStringify(
|
||||
split,
|
||||
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
input: jsonStringify(
|
||||
toolUseConfirm.input,
|
||||
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
decisionReasonType: toolUseConfirm.permissionResult.decisionReason
|
||||
?.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
decisionReason: decisionReasonToString(
|
||||
toolUseConfirm.permissionResult.decisionReason,
|
||||
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
void logUnaryEvent({
|
||||
completion_type: unaryEvent.completion_type,
|
||||
event: 'response',
|
||||
metadata: {
|
||||
language_name: unaryEvent.language_name,
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
}, [toolUseConfirm, unaryEvent, setAppState])
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,76 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Text } from '../../../ink.js';
|
||||
import { BashTool } from '../../../tools/BashTool/BashTool.js';
|
||||
import type { PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js';
|
||||
type RuleSubtitleProps = {
|
||||
ruleValue: PermissionRuleValue;
|
||||
};
|
||||
export function PermissionRuleDescription(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
ruleValue
|
||||
} = t0;
|
||||
switch (ruleValue.toolName) {
|
||||
case BashTool.name:
|
||||
{
|
||||
if (ruleValue.ruleContent) {
|
||||
if (ruleValue.ruleContent.endsWith(":*")) {
|
||||
let t1;
|
||||
if ($[0] !== ruleValue.ruleContent) {
|
||||
t1 = ruleValue.ruleContent.slice(0, -2);
|
||||
$[0] = ruleValue.ruleContent;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== t1) {
|
||||
t2 = <Text dimColor={true}>Any Bash command starting with{" "}<Text bold={true}>{t1}</Text></Text>;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
} else {
|
||||
let t1;
|
||||
if ($[4] !== ruleValue.ruleContent) {
|
||||
t1 = <Text dimColor={true}>The Bash command <Text bold={true}>{ruleValue.ruleContent}</Text></Text>;
|
||||
$[4] = ruleValue.ruleContent;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
} else {
|
||||
let t1;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text dimColor={true}>Any Bash command</Text>;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
}
|
||||
default:
|
||||
{
|
||||
if (!ruleValue.ruleContent) {
|
||||
let t1;
|
||||
if ($[7] !== ruleValue.toolName) {
|
||||
t1 = <Text dimColor={true}>Any use of the <Text bold={true}>{ruleValue.toolName}</Text> tool</Text>;
|
||||
$[7] = ruleValue.toolName;
|
||||
$[8] = t1;
|
||||
} else {
|
||||
t1 = $[8];
|
||||
}
|
||||
return t1;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlRleHQiLCJCYXNoVG9vbCIsIlBlcm1pc3Npb25SdWxlVmFsdWUiLCJSdWxlU3VidGl0bGVQcm9wcyIsInJ1bGVWYWx1ZSIsIlBlcm1pc3Npb25SdWxlRGVzY3JpcHRpb24iLCJ0MCIsIiQiLCJfYyIsInRvb2xOYW1lIiwibmFtZSIsInJ1bGVDb250ZW50IiwiZW5kc1dpdGgiLCJ0MSIsInNsaWNlIiwidDIiLCJTeW1ib2wiLCJmb3IiXSwic291cmNlcyI6WyJQZXJtaXNzaW9uUnVsZURlc2NyaXB0aW9uLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IFRleHQgfSBmcm9tICcuLi8uLi8uLi9pbmsuanMnXG5pbXBvcnQgeyBCYXNoVG9vbCB9IGZyb20gJy4uLy4uLy4uL3Rvb2xzL0Jhc2hUb29sL0Jhc2hUb29sLmpzJ1xuaW1wb3J0IHR5cGUgeyBQZXJtaXNzaW9uUnVsZVZhbHVlIH0gZnJvbSAnLi4vLi4vLi4vdXRpbHMvcGVybWlzc2lvbnMvUGVybWlzc2lvblJ1bGUuanMnXG5cbnR5cGUgUnVsZVN1YnRpdGxlUHJvcHMgPSB7XG4gIHJ1bGVWYWx1ZTogUGVybWlzc2lvblJ1bGVWYWx1ZVxufVxuXG5leHBvcnQgZnVuY3Rpb24gUGVybWlzc2lvblJ1bGVEZXNjcmlwdGlvbih7XG4gIHJ1bGVWYWx1ZSxcbn06IFJ1bGVTdWJ0aXRsZVByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgc3dpdGNoIChydWxlVmFsdWUudG9vbE5hbWUpIHtcbiAgICBjYXNlIEJhc2hUb29sLm5hbWU6IHtcbiAgICAgIGlmIChydWxlVmFsdWUucnVsZUNvbnRlbnQpIHtcbiAgICAgICAgaWYgKHJ1bGVWYWx1ZS5ydWxlQ29udGVudC5lbmRzV2l0aCgnOionKSkge1xuICAgICAgICAgIHJldHVybiAoXG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgICAgQW55IEJhc2ggY29tbWFuZCBzdGFydGluZyB3aXRoeycgJ31cbiAgICAgICAgICAgICAgPFRleHQgYm9sZD57cnVsZVZhbHVlLnJ1bGVDb250ZW50LnNsaWNlKDAsIC0yKX08L1RleHQ+XG4gICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgKVxuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIHJldHVybiAoXG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgICAgVGhlIEJhc2ggY29tbWFuZCA8VGV4dCBib2xkPntydWxlVmFsdWUucnVsZUNvbnRlbnR9PC9UZXh0PlxuICAgICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgIClcbiAgICAgICAgfVxuICAgICAgfSBlbHNlIHtcbiAgICAgICAgcmV0dXJuIDxUZXh0IGRpbUNvbG9yPkFueSBCYXNoIGNvbW1hbmQ8L1RleHQ+XG4gICAgICB9XG4gICAgfVxuICAgIGRlZmF1bHQ6IHtcbiAgICAgIGlmICghcnVsZVZhbHVlLnJ1bGVDb250ZW50KSB7XG4gICAgICAgIHJldHVybiAoXG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+XG4gICAgICAgICAgICBBbnkgdXNlIG9mIHRoZSA8VGV4dCBib2xkPntydWxlVmFsdWUudG9vbE5hbWV9PC9UZXh0PiB0b29sXG4gICAgICAgICAgPC9UZXh0PlxuICAgICAgICApXG4gICAgICB9IGVsc2Uge1xuICAgICAgICByZXR1cm4gbnVsbFxuICAgICAgfVxuICAgIH1cbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBTyxLQUFLQSxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxJQUFJLFFBQVEsaUJBQWlCO0FBQ3RDLFNBQVNDLFFBQVEsUUFBUSxxQ0FBcUM7QUFDOUQsY0FBY0MsbUJBQW1CLFFBQVEsOENBQThDO0FBRXZGLEtBQUtDLGlCQUFpQixHQUFHO0VBQ3ZCQyxTQUFTLEVBQUVGLG1CQUFtQjtBQUNoQyxDQUFDO0FBRUQsT0FBTyxTQUFBRywwQkFBQUMsRUFBQTtFQUFBLE1BQUFD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
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,148 @@
|
||||
import { useState } from 'react'
|
||||
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 { useSetAppState } from '../../state/AppState.js'
|
||||
import type { ToolUseConfirm } from './PermissionRequest.js'
|
||||
import { logUnaryPermissionEvent } from './utils.js'
|
||||
|
||||
/**
|
||||
* Shared feedback-mode state + handlers for shell permission dialogs (Bash,
|
||||
* PowerShell). Encapsulates the yes/no input-mode toggle, feedback text state,
|
||||
* focus tracking, and reject handling.
|
||||
*/
|
||||
export function useShellPermissionFeedback({
|
||||
toolUseConfirm,
|
||||
onDone,
|
||||
onReject,
|
||||
explainerVisible,
|
||||
}: {
|
||||
toolUseConfirm: ToolUseConfirm
|
||||
onDone: () => void
|
||||
onReject: () => void
|
||||
explainerVisible: boolean
|
||||
}): {
|
||||
yesInputMode: boolean
|
||||
noInputMode: boolean
|
||||
yesFeedbackModeEntered: boolean
|
||||
noFeedbackModeEntered: boolean
|
||||
acceptFeedback: string
|
||||
rejectFeedback: string
|
||||
setAcceptFeedback: (v: string) => void
|
||||
setRejectFeedback: (v: string) => void
|
||||
focusedOption: string
|
||||
handleInputModeToggle: (option: string) => void
|
||||
handleReject: (feedback?: string) => void
|
||||
handleFocus: (value: string) => void
|
||||
} {
|
||||
const setAppState = useSetAppState()
|
||||
const [rejectFeedback, setRejectFeedback] = useState('')
|
||||
const [acceptFeedback, setAcceptFeedback] = useState('')
|
||||
const [yesInputMode, setYesInputMode] = useState(false)
|
||||
const [noInputMode, setNoInputMode] = useState(false)
|
||||
const [focusedOption, setFocusedOption] = useState('yes')
|
||||
// Track whether user ever entered feedback mode (persists after collapse)
|
||||
const [yesFeedbackModeEntered, setYesFeedbackModeEntered] = useState(false)
|
||||
const [noFeedbackModeEntered, setNoFeedbackModeEntered] = useState(false)
|
||||
|
||||
// Handle Tab key toggling input mode for Yes/No options
|
||||
function handleInputModeToggle(option: string) {
|
||||
// Notify that user is interacting with the dialog
|
||||
toolUseConfirm.onUserInteraction()
|
||||
const analyticsProps = {
|
||||
toolName: sanitizeToolNameForAnalytics(
|
||||
toolUseConfirm.tool.name,
|
||||
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
isMcp: toolUseConfirm.tool.isMcp ?? false,
|
||||
}
|
||||
|
||||
if (option === '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 (option === 'no') {
|
||||
if (noInputMode) {
|
||||
setNoInputMode(false)
|
||||
logEvent('tengu_reject_feedback_mode_collapsed', analyticsProps)
|
||||
} else {
|
||||
setNoInputMode(true)
|
||||
setNoFeedbackModeEntered(true)
|
||||
logEvent('tengu_reject_feedback_mode_entered', analyticsProps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleReject(feedback?: string) {
|
||||
const trimmedFeedback = feedback?.trim()
|
||||
const hasFeedback = !!trimmedFeedback
|
||||
|
||||
// Log escape if no feedback was provided (user pressed ESC)
|
||||
if (!hasFeedback) {
|
||||
logEvent('tengu_permission_request_escape', {
|
||||
explainer_visible: explainerVisible,
|
||||
})
|
||||
// Increment escape count for attribution tracking
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
attribution: {
|
||||
...prev.attribution,
|
||||
escapeCount: prev.attribution.escapeCount + 1,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
logUnaryPermissionEvent(
|
||||
'tool_use_single',
|
||||
toolUseConfirm,
|
||||
'reject',
|
||||
hasFeedback,
|
||||
)
|
||||
|
||||
if (trimmedFeedback) {
|
||||
toolUseConfirm.onReject(trimmedFeedback)
|
||||
} else {
|
||||
toolUseConfirm.onReject()
|
||||
}
|
||||
|
||||
onReject()
|
||||
onDone()
|
||||
}
|
||||
|
||||
function handleFocus(value: string) {
|
||||
// Notify that user is interacting with the dialog (only if focus changed)
|
||||
// This prevents triggering on the initial mount/render
|
||||
if (value !== focusedOption) {
|
||||
toolUseConfirm.onUserInteraction()
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
return {
|
||||
yesInputMode,
|
||||
noInputMode,
|
||||
yesFeedbackModeEntered,
|
||||
noFeedbackModeEntered,
|
||||
acceptFeedback,
|
||||
rejectFeedback,
|
||||
setAcceptFeedback,
|
||||
setRejectFeedback,
|
||||
focusedOption,
|
||||
handleInputModeToggle,
|
||||
handleReject,
|
||||
handleFocus,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { getHostPlatformForAnalytics } from '../../utils/env.js'
|
||||
import { type CompletionType, logUnaryEvent } from '../../utils/unaryLogging.js'
|
||||
import type { ToolUseConfirm } from './PermissionRequest.js'
|
||||
|
||||
export function logUnaryPermissionEvent(
|
||||
completion_type: CompletionType,
|
||||
{
|
||||
assistantMessage: {
|
||||
message: { id: message_id },
|
||||
},
|
||||
}: ToolUseConfirm,
|
||||
event: 'accept' | 'reject',
|
||||
hasFeedback?: boolean,
|
||||
): void {
|
||||
void logUnaryEvent({
|
||||
completion_type,
|
||||
event,
|
||||
metadata: {
|
||||
language_name: 'none',
|
||||
message_id,
|
||||
platform: getHostPlatformForAnalytics(),
|
||||
hasFeedback: hasFeedback ?? false,
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user