claude-code-best/src/components/permissions/FilePermissionDialog/permissionOptions.tsx
claude-code-best 4d0048a60a fix: 优化权限提示用词和 Help 页面新手引导
- Help General 页添加 3 步 Getting started 引导,替代单段描述
- 权限对话框底部 "Esc to cancel" → "Esc to reject","Tab to amend" → "Tab to add feedback"
- .claude/ 文件夹权限选项标签从 60 字符缩至 49 字符,避免窄终端截断
- 新增 10 个测试覆盖权限提示文案和帮助页引导内容

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 00:09:27 +08:00

183 lines
6.2 KiB
TypeScript

import { homedir } from 'os';
import { basename, join, sep } from 'path';
import React, { type ReactNode } from 'react';
import { getOriginalCwd } from '../../../bootstrap/state.js';
import { Text } from '@anthropic/ink';
import { getShortcutDisplay } from '../../../keybindings/shortcutFormat.js';
import type { ToolPermissionContext } from '../../../Tool.js';
import { expandPath, getDirectoryForPath } from '../../../utils/path.js';
import { normalizeCaseForComparison, pathInAllowedWorkingPath } from '../../../utils/permissions/filesystem.js';
import type { OptionWithDescription } from '../../CustomSelect/select.js';
/**
* Check if a path is within the project's .claude/ folder.
* This is used to determine whether to show the special ".claude folder" permission option.
*/
export function isInClaudeFolder(filePath: string): boolean {
const absolutePath = expandPath(filePath);
const claudeFolderPath = expandPath(`${getOriginalCwd()}/.claude`);
// Check if the path is within the project's .claude folder
const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath);
const normalizedClaudeFolderPath = normalizeCaseForComparison(claudeFolderPath);
// Path must start with the .claude folder path (and be inside it, not just the folder itself)
return (
normalizedAbsolutePath.startsWith(normalizedClaudeFolderPath + sep.toLowerCase()) ||
// Also match case where sep is / on posix systems
normalizedAbsolutePath.startsWith(normalizedClaudeFolderPath + '/')
);
}
/**
* Check if a path is within the global ~/.claude/ folder.
* This is used to determine whether to show the special ".claude folder" permission option
* for files in the user's home directory.
*/
export function isInGlobalClaudeFolder(filePath: string): boolean {
const absolutePath = expandPath(filePath);
const globalClaudeFolderPath = join(homedir(), '.claude');
const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath);
const normalizedGlobalClaudeFolderPath = normalizeCaseForComparison(globalClaudeFolderPath);
return (
normalizedAbsolutePath.startsWith(normalizedGlobalClaudeFolderPath + sep.toLowerCase()) ||
normalizedAbsolutePath.startsWith(normalizedGlobalClaudeFolderPath + '/')
);
}
export type PermissionOption =
| { type: 'accept-once' }
| { type: 'accept-session'; scope?: 'claude-folder' | 'global-claude-folder' }
| { type: 'reject' };
export type PermissionOptionWithLabel = OptionWithDescription<string> & {
option: PermissionOption;
};
export type FileOperationType = 'read' | 'write' | 'create';
export function getFilePermissionOptions({
filePath,
toolPermissionContext,
operationType = 'write',
onRejectFeedbackChange,
onAcceptFeedbackChange,
yesInputMode = false,
noInputMode = false,
}: {
filePath: string;
toolPermissionContext: ToolPermissionContext;
operationType?: FileOperationType;
onRejectFeedbackChange?: (value: string) => void;
onAcceptFeedbackChange?: (value: string) => void;
yesInputMode?: boolean;
noInputMode?: boolean;
}): PermissionOptionWithLabel[] {
const options: PermissionOptionWithLabel[] = [];
const modeCycleShortcut = getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab');
// When in input mode, show input field
if (yesInputMode && onAcceptFeedbackChange) {
options.push({
type: 'input',
label: 'Yes',
value: 'yes',
placeholder: 'and tell Claude what to do next',
onChange: onAcceptFeedbackChange,
allowEmptySubmitToCancel: true,
option: { type: 'accept-once' },
});
} else {
options.push({
label: 'Yes',
value: 'yes',
option: { type: 'accept-once' },
});
}
const inAllowedPath = pathInAllowedWorkingPath(filePath, toolPermissionContext);
// Check if this is a .claude/ folder path (project or global)
const inClaudeFolder = isInClaudeFolder(filePath);
const inGlobalClaudeFolder = isInGlobalClaudeFolder(filePath);
// Option 2: For .claude/ folder, show special option instead of generic session option
// Note: Session-level options are always shown since they only affect in-memory state,
// not persisted settings. The allowManagedPermissionRulesOnly setting only restricts
// persisted permission rules.
if ((inClaudeFolder || inGlobalClaudeFolder) && operationType !== 'read') {
options.push({
label: 'Yes, allow edits to .claude/ config for this session',
value: 'yes-claude-folder',
option: {
type: 'accept-session',
scope: inGlobalClaudeFolder ? 'global-claude-folder' : 'claude-folder',
},
});
} else {
// Option 2: Allow all changes/reads during session
let sessionLabel: ReactNode;
if (inAllowedPath) {
// Inside working directory
if (operationType === 'read') {
sessionLabel = 'Yes, during this session';
} else {
sessionLabel = (
<Text>
Yes, allow all edits during this session <Text bold>({modeCycleShortcut})</Text>
</Text>
);
}
} else {
// Outside working directory - include directory name
const dirPath = getDirectoryForPath(filePath);
const dirName = basename(dirPath) || 'this directory';
if (operationType === 'read') {
sessionLabel = (
<Text>
Yes, allow reading from <Text bold>{dirName}/</Text> during this session
</Text>
);
} else {
sessionLabel = (
<Text>
Yes, allow all edits in <Text bold>{dirName}/</Text> during this session{' '}
<Text bold>({modeCycleShortcut})</Text>
</Text>
);
}
}
options.push({
label: sessionLabel,
value: 'yes-session',
option: { type: 'accept-session' },
});
}
// When in input mode, show input field for reject
if (noInputMode && onRejectFeedbackChange) {
options.push({
type: 'input',
label: 'No',
value: 'no',
placeholder: 'and tell Claude what to do differently',
onChange: onRejectFeedbackChange,
allowEmptySubmitToCancel: true,
option: { type: 'reject' },
});
} else {
// Not in input mode - simple option
options.push({
label: 'No',
value: 'no',
option: { type: 'reject' },
});
}
return options;
}