asdf
Squash the current repository state back into one baseline commit while preserving the README reframing and repository contents. Constraint: User explicitly requested a single squashed commit with subject "asdf" Confidence: high Scope-risk: broad Reversibility: clean Directive: This commit intentionally rewrites published history; coordinate before future force-pushes Tested: git status clean; local history rewritten to one commit; force-pushed main to origin and instructkr Not-tested: Fresh clone verification after push
This commit is contained in:
commit
d2542c9a62
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
333
src/components/permissions/FallbackPermissionRequest.tsx
Normal file
333
src/components/permissions/FallbackPermissionRequest.tsx
Normal file
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
460
src/components/permissions/PermissionDecisionDebugInfo.tsx
Normal file
460
src/components/permissions/PermissionDecisionDebugInfo.tsx
Normal file
File diff suppressed because one or more lines are too long
72
src/components/permissions/PermissionDialog.tsx
Normal file
72
src/components/permissions/PermissionDialog.tsx
Normal file
File diff suppressed because one or more lines are too long
272
src/components/permissions/PermissionExplanation.tsx
Normal file
272
src/components/permissions/PermissionExplanation.tsx
Normal file
File diff suppressed because one or more lines are too long
336
src/components/permissions/PermissionPrompt.tsx
Normal file
336
src/components/permissions/PermissionPrompt.tsx
Normal file
File diff suppressed because one or more lines are too long
217
src/components/permissions/PermissionRequest.tsx
Normal file
217
src/components/permissions/PermissionRequest.tsx
Normal file
File diff suppressed because one or more lines are too long
66
src/components/permissions/PermissionRequestTitle.tsx
Normal file
66
src/components/permissions/PermissionRequestTitle.tsx
Normal file
@@ -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=
|
||||
121
src/components/permissions/PermissionRuleExplanation.tsx
Normal file
121
src/components/permissions/PermissionRuleExplanation.tsx
Normal file
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
163
src/components/permissions/SandboxPermissionRequest.tsx
Normal file
163
src/components/permissions/SandboxPermissionRequest.tsx
Normal file
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
49
src/components/permissions/WorkerBadge.tsx
Normal file
49
src/components/permissions/WorkerBadge.tsx
Normal file
@@ -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==
|
||||
105
src/components/permissions/WorkerPendingPermission.tsx
Normal file
105
src/components/permissions/WorkerPendingPermission.tsx
Normal file
File diff suppressed because one or more lines are too long
209
src/components/permissions/hooks.ts
Normal file
209
src/components/permissions/hooks.ts
Normal file
@@ -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])
|
||||
}
|
||||
180
src/components/permissions/rules/AddPermissionRules.tsx
Normal file
180
src/components/permissions/rules/AddPermissionRules.tsx
Normal file
File diff suppressed because one or more lines are too long
340
src/components/permissions/rules/AddWorkspaceDirectory.tsx
Normal file
340
src/components/permissions/rules/AddWorkspaceDirectory.tsx
Normal file
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+XG4gICAgICAgICAgICBBbnkgdXNlIG9mIHRoZSA8VGV4dCBib2xkPntydWxlVmFsdWUudG9vbE5hbWV9PC9UZXh0PiB0b29sXG4gICAgICAgICAgPC9UZXh0PlxuICAgICAgICApXG4gICAgICB9IGVsc2Uge1xuICAgICAgICByZXR1cm4gbnVsbFxuICAgICAgfVxuICAgIH1cbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBTyxLQUFLQSxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxJQUFJLFFBQVEsaUJBQWlCO0FBQ3RDLFNBQVNDLFFBQVEsUUFBUSxxQ0FBcUM7QUFDOUQsY0FBY0MsbUJBQW1CLFFBQVEsOENBQThDO0FBRXZGLEtBQUtDLGlCQUFpQixHQUFHO0VBQ3ZCQyxTQUFTLEVBQUVGLG1CQUFtQjtBQUNoQyxDQUFDO0FBRUQsT0FBTyxTQUFBRywwQkFBQUMsRUFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUFtQztJQUFBSjtFQUFBLElBQUFFLEVBRXRCO0VBQ2xCLFFBQVFGLFNBQVMsQ0FBQUssUUFBUztJQUFBLEtBQ25CUixRQUFRLENBQUFTLElBQUs7TUFBQTtRQUNoQixJQUFJTixTQUFTLENBQUFPLFdBQVk7VUFDdkIsSUFBSVAsU0FBUyxDQUFBTyxXQUFZLENBQUFDLFFBQVMsQ0FBQyxJQUFJLENBQUM7WUFBQSxJQUFBQyxFQUFBO1lBQUEsSUFBQU4sQ0FBQSxRQUFBSCxTQUFBLENBQUFPLFdBQUE7Y0FJdEJFLEVBQUEsR0FBQVQsU0FBUyxDQUFBTyxXQUFZLENBQUFHLEtBQU0sQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDO2NBQUFQLENBQUEsTUFBQUgsU0FBQSxDQUFBTyxXQUFBO2NBQUFKLENBQUEsTUFBQU0sRUFBQTtZQUFBO2NBQUFBLEVBQUEsR0FBQU4sQ0FBQTtZQUFBO1lBQUEsSUFBQVEsRUFBQTtZQUFBLElBQUFSLENBQUEsUUFBQU0sRUFBQTtjQUZoREUsRUFBQSxJQUFDLElBQUksQ0FBQyxRQUFRLENBQVIsS0FBTyxDQUFDLENBQUMsOEJBQ2tCLElBQUUsQ0FDakMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFKLEtBQUcsQ0FBQyxDQUFFLENBQUFGLEVBQWlDLENBQUUsRUFBOUMsSUFBSSxDQUNQLEVBSEMsSUFBSSxDQUdFO2NBQUFOLENBQUEsTUFBQU0sRUFBQTtjQUFBTixDQUFBLE1BQUFRLEVBQUE7WUFBQTtjQUFBQSxFQUFBLEdBQUFSLENBQUE7WUFBQTtZQUFBLE9BSFBRLEVBR087VUFBQTtZQUFBLElBQUFGLEVBQUE7WUFBQSxJQUFBTixDQUFBLFFBQUFILFNBQUEsQ0FBQU8sV0FBQTtjQUlQRSxFQUFBLElBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBUixLQUFPLENBQUMsQ0FBQyxpQkFDSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUosS0FBRyxDQUFDLENBQUUsQ0FBQVQsU0FBUyxDQUFBTyxXQUFXLENBQUUsRUFBakMsSUFBSSxDQUN4QixFQUZDLElBQUksQ0FFRTtjQUFBSixDQUFBLE1BQUFILFNBQUEsQ0FBQU8sV0FBQTtjQUFBSixDQUFBLE1BQUFNLEVBQUE7WUFBQTtjQUFBQSxFQUFBLEdBQUFOLENBQUE7WUFBQTtZQUFBLE9BRlBNLEVBRU87VUFBQTtRQUVWO1VBQUEsSUFBQUEsRUFBQTtVQUFBLElBQUFOLENBQUEsUUFBQVMsTUFBQSxDQUFBQyxHQUFBO1lBRU1KLEVBQUEsSUFBQyxJQUFJLENBQUMsUUFBUSxDQUFSLEtBQU8sQ0FBQyxDQUFDLGdCQUFnQixFQUE5QixJQUFJLENBQWlDO1lBQUFOLENBQUEsTUFBQU0sRUFBQTtVQUFBO1lBQUFBLEVBQUEsR0FBQU4sQ0FBQTtVQUFBO1VBQUEsT0FBdENNLEVBQXNDO1FBQUE7TUFDOUM7SUFBQTtNQUFBO1FBR0QsSUFBSSxDQUFDVCxTQUFTLENBQUFPLFdBQVk7VUFBQSxJQUFBRSxFQUFBO1VBQUEsSUFBQU4sQ0FBQSxRQUFBSCxTQUFBLENBQUFLLFFBQUE7WUFFdEJJLEVBQUEsSUFBQyxJQUFJLENBQUMsUUFBUSxDQUFSLEtBQU8sQ0FBQyxDQUFDLGVBQ0UsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFKLEtBQUcsQ0FBQyxDQUFFLENBQUFULFNBQVMsQ0FBQUssUUFBUSxDQUFFLEVBQTlCLElBQUksQ0FBaUMsS0FDdkQsRUFGQyxJQUFJLENBRUU7WUFBQUYsQ0FBQSxNQUFBSCxTQUFBLENBQUFLLFFBQUE7WUFBQUYsQ0FBQSxNQUFBTSxFQUFBO1VBQUE7WUFBQUEsRUFBQSxHQUFBTixDQUFBO1VBQUE7VUFBQSxPQUZQTSxFQUVPO1FBQUE7VUFBQSxPQUdGLElBQUk7UUFBQTtNQUNaO0VBRUw7QUFBQyIsImlnbm9yZUxpc3QiOltdfQ==
|
||||
138
src/components/permissions/rules/PermissionRuleInput.tsx
Normal file
138
src/components/permissions/rules/PermissionRuleInput.tsx
Normal file
File diff suppressed because one or more lines are too long
1179
src/components/permissions/rules/PermissionRuleList.tsx
Normal file
1179
src/components/permissions/rules/PermissionRuleList.tsx
Normal file
File diff suppressed because one or more lines are too long
207
src/components/permissions/rules/RecentDenialsTab.tsx
Normal file
207
src/components/permissions/rules/RecentDenialsTab.tsx
Normal file
File diff suppressed because one or more lines are too long
110
src/components/permissions/rules/RemoveWorkspaceDirectory.tsx
Normal file
110
src/components/permissions/rules/RemoveWorkspaceDirectory.tsx
Normal file
File diff suppressed because one or more lines are too long
150
src/components/permissions/rules/WorkspaceTab.tsx
Normal file
150
src/components/permissions/rules/WorkspaceTab.tsx
Normal file
File diff suppressed because one or more lines are too long
164
src/components/permissions/shellPermissionHelpers.tsx
Normal file
164
src/components/permissions/shellPermissionHelpers.tsx
Normal file
File diff suppressed because one or more lines are too long
148
src/components/permissions/useShellPermissionFeedback.ts
Normal file
148
src/components/permissions/useShellPermissionFeedback.ts
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
25
src/components/permissions/utils.ts
Normal file
25
src/components/permissions/utils.ts
Normal file
@@ -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