Files
claude-code/src/components/permissions/BashPermissionRequest/bashToolUseOptions.tsx
claude-code-best 5b1a52b8e0 更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)
* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files)

纯格式化:移除分号、React Compiler import、import 多行展开。
修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-2): 格式化 commands (79 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-5): 格式化 components其余 + hooks + tools (232 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md

- README.md: 大幅重写,更详细版本历史和配置示例
- Run.ps1: 新增 Windows 启动脚本
- TODO.md: 新增包完成清单
- V6.md: 删除(架构重构规划已不适用)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复以前的问题

* fix: 修复 login 面板的问题

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 23:24:27 +08:00

188 lines
6.2 KiB
TypeScript

import { BASH_TOOL_NAME } from '../../../tools/BashTool/toolName.js'
import { extractOutputRedirections } from '../../../utils/bash/commands.js'
import { isClassifierPermissionsEnabled } from '../../../utils/permissions/bashClassifier.js'
import type { PermissionDecisionReason } from '../../../utils/permissions/PermissionResult.js'
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js'
import type { OptionWithDescription } from '../../CustomSelect/select.js'
import { generateShellSuggestionsLabel } from '../shellPermissionHelpers.js'
export type BashToolUseOption =
| 'yes'
| 'yes-apply-suggestions'
| 'yes-prefix-edited'
| 'yes-classifier-reviewed'
| 'no'
/**
* Check if a description already exists in the allow list.
* Compares lowercase and trailing-whitespace-trimmed versions.
*/
function descriptionAlreadyExists(
description: string,
existingDescriptions: string[],
): boolean {
const normalized = description.toLowerCase().trimEnd()
return existingDescriptions.some(
existing => existing.toLowerCase().trimEnd() === normalized,
)
}
/**
* Strip output redirections so filenames don't show as commands in the label.
*/
function stripBashRedirections(command: string): string {
const { commandWithoutRedirections, redirections } =
extractOutputRedirections(command)
// Only use stripped version if there were actual redirections
return redirections.length > 0 ? commandWithoutRedirections : command
}
export function bashToolUseOptions({
suggestions = [],
decisionReason,
onRejectFeedbackChange,
onAcceptFeedbackChange,
onClassifierDescriptionChange,
classifierDescription,
initialClassifierDescriptionEmpty = false,
existingAllowDescriptions = [],
yesInputMode = false,
noInputMode = false,
editablePrefix,
onEditablePrefixChange,
}: {
suggestions?: PermissionUpdate[]
decisionReason?: PermissionDecisionReason
onRejectFeedbackChange: (value: string) => void
onAcceptFeedbackChange: (value: string) => void
onClassifierDescriptionChange?: (value: string) => void
classifierDescription?: string
/** Whether the initial classifier description was empty. When true, hides the option. */
initialClassifierDescriptionEmpty?: boolean
existingAllowDescriptions?: string[]
yesInputMode?: boolean
noInputMode?: boolean
/** Editable prefix rule content (e.g., "npm run:*"). When set, replaces Haiku-based suggestions. */
editablePrefix?: string
/** Callback when the user edits the prefix value. */
onEditablePrefixChange?: (value: string) => void
}): OptionWithDescription<BashToolUseOption>[] {
const options: OptionWithDescription<BashToolUseOption>[] = []
if (yesInputMode) {
options.push({
type: 'input',
label: 'Yes',
value: 'yes',
placeholder: 'and tell Claude what to do next',
onChange: onAcceptFeedbackChange,
allowEmptySubmitToCancel: true,
})
} else {
options.push({
label: 'Yes',
value: 'yes',
})
}
// Only show "always allow" options when not restricted by allowManagedPermissionRulesOnly
if (shouldShowAlwaysAllowOptions()) {
// Show an editable input for the prefix rule instead of the
// Haiku-generated suggestion label — but only when the suggestions
// don't contain non-Bash items (addDirectories, Read rules) that
// the editable prefix can't represent.
const hasNonBashSuggestions = suggestions.some(
s =>
s.type === 'addDirectories' ||
(s.type === 'addRules' &&
s.rules?.some(r => r.toolName !== BASH_TOOL_NAME)),
)
if (
editablePrefix !== undefined &&
onEditablePrefixChange &&
!hasNonBashSuggestions &&
suggestions.length > 0
) {
options.push({
type: 'input',
label: 'Yes, and don\u2019t ask again for',
value: 'yes-prefix-edited',
placeholder: 'command prefix (e.g., npm run:*)',
initialValue: editablePrefix,
onChange: onEditablePrefixChange,
allowEmptySubmitToCancel: true,
showLabelWithValue: true,
labelValueSeparator: ': ',
resetCursorOnUpdate: true,
})
} else if (suggestions.length > 0) {
const label = generateShellSuggestionsLabel(
suggestions,
BASH_TOOL_NAME,
stripBashRedirections,
)
if (label) {
options.push({
label,
value: 'yes-apply-suggestions',
})
}
}
// Add classifier-reviewed option if enabled, the initial description was
// non-empty, the description doesn't already exist in the allow list,
// and the decision reason is NOT a server-side classifier block
// (prompt-based rules don't help when the server-side classifier triggers first).
// Skip when the editable prefix option is already shown — they serve the
// same role and having two identical-looking "don't ask again" inputs is confusing.
const editablePrefixShown = options.some(
o => o.value === 'yes-prefix-edited',
)
if (
process.env.USER_TYPE === 'ant' &&
!editablePrefixShown &&
isClassifierPermissionsEnabled() &&
onClassifierDescriptionChange &&
!initialClassifierDescriptionEmpty &&
!descriptionAlreadyExists(
classifierDescription ?? '',
existingAllowDescriptions,
) &&
decisionReason?.type !== 'classifier'
) {
options.push({
type: 'input',
label: 'Yes, and don\u2019t ask again for',
value: 'yes-classifier-reviewed',
placeholder: 'describe what to allow...',
initialValue: classifierDescription ?? '',
onChange: onClassifierDescriptionChange,
allowEmptySubmitToCancel: true,
showLabelWithValue: true,
labelValueSeparator: ': ',
resetCursorOnUpdate: true,
})
}
}
if (noInputMode) {
options.push({
type: 'input',
label: 'No',
value: 'no',
placeholder: 'and tell Claude what to do differently',
onChange: onRejectFeedbackChange,
allowEmptySubmitToCancel: true,
})
} else {
options.push({
label: 'No',
value: 'no',
})
}
return options
}