init claude-code

This commit is contained in:
2026-04-01 17:32:37 +02:00
commit 73b208c009
1902 changed files with 513237 additions and 0 deletions
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
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
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
+49
View 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==
File diff suppressed because one or more lines are too long
+209
View 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])
}
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,
}
}
+25
View 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,
},
})
}