mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 00:05:51 +00:00
更新大量 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>
This commit is contained in:
@@ -1,50 +1,61 @@
|
||||
import { relative } from 'path';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useDiffInIDE } from '../../../hooks/useDiffInIDE.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import type { ToolUseContext } from '../../../Tool.js';
|
||||
import { getLanguageName } from '../../../utils/cliHighlight.js';
|
||||
import { getCwd } from '../../../utils/cwd.js';
|
||||
import { getFsImplementation, safeResolvePath } from '../../../utils/fsOperations.js';
|
||||
import { expandPath } from '../../../utils/path.js';
|
||||
import type { CompletionType } from '../../../utils/unaryLogging.js';
|
||||
import { Select } from '../../CustomSelect/index.js';
|
||||
import { ShowInIDEPrompt } from '../../ShowInIDEPrompt.js';
|
||||
import { usePermissionRequestLogging } from '../hooks.js';
|
||||
import { PermissionDialog } from '../PermissionDialog.js';
|
||||
import type { ToolUseConfirm } from '../PermissionRequest.js';
|
||||
import type { WorkerBadgeProps } from '../WorkerBadge.js';
|
||||
import type { IDEDiffSupport } from './ideDiffConfig.js';
|
||||
import type { FileOperationType, PermissionOption } from './permissionOptions.js';
|
||||
import { type ToolInput, useFilePermissionDialog } from './useFilePermissionDialog.js';
|
||||
import { relative } from 'path'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useDiffInIDE } from '../../../hooks/useDiffInIDE.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import type { ToolUseContext } from '../../../Tool.js'
|
||||
import { getLanguageName } from '../../../utils/cliHighlight.js'
|
||||
import { getCwd } from '../../../utils/cwd.js'
|
||||
import {
|
||||
getFsImplementation,
|
||||
safeResolvePath,
|
||||
} from '../../../utils/fsOperations.js'
|
||||
import { expandPath } from '../../../utils/path.js'
|
||||
import type { CompletionType } from '../../../utils/unaryLogging.js'
|
||||
import { Select } from '../../CustomSelect/index.js'
|
||||
import { ShowInIDEPrompt } from '../../ShowInIDEPrompt.js'
|
||||
import { usePermissionRequestLogging } from '../hooks.js'
|
||||
import { PermissionDialog } from '../PermissionDialog.js'
|
||||
import type { ToolUseConfirm } from '../PermissionRequest.js'
|
||||
import type { WorkerBadgeProps } from '../WorkerBadge.js'
|
||||
import type { IDEDiffSupport } from './ideDiffConfig.js'
|
||||
import type {
|
||||
FileOperationType,
|
||||
PermissionOption,
|
||||
} from './permissionOptions.js'
|
||||
import {
|
||||
type ToolInput,
|
||||
useFilePermissionDialog,
|
||||
} from './useFilePermissionDialog.js'
|
||||
|
||||
export type FilePermissionDialogProps<T extends ToolInput = ToolInput> = {
|
||||
// Required props from PermissionRequestProps
|
||||
toolUseConfirm: ToolUseConfirm;
|
||||
toolUseContext: ToolUseContext;
|
||||
onDone: () => void;
|
||||
onReject: () => void;
|
||||
toolUseConfirm: ToolUseConfirm
|
||||
toolUseContext: ToolUseContext
|
||||
onDone: () => void
|
||||
onReject: () => void
|
||||
|
||||
// Dialog customization
|
||||
title: string;
|
||||
subtitle?: React.ReactNode;
|
||||
question?: string | React.ReactNode;
|
||||
content?: React.ReactNode; // Can be general content or diff component
|
||||
title: string
|
||||
subtitle?: React.ReactNode
|
||||
question?: string | React.ReactNode
|
||||
content?: React.ReactNode // Can be general content or diff component
|
||||
|
||||
// Logging
|
||||
completionType?: CompletionType;
|
||||
languageName?: string; // override — derived from path when omitted
|
||||
completionType?: CompletionType
|
||||
languageName?: string // override — derived from path when omitted
|
||||
|
||||
// File/directory operations
|
||||
path: string | null;
|
||||
parseInput: (input: unknown) => T;
|
||||
operationType?: FileOperationType;
|
||||
path: string | null
|
||||
parseInput: (input: unknown) => T
|
||||
operationType?: FileOperationType
|
||||
|
||||
// IDE diff support
|
||||
ideDiffSupport?: IDEDiffSupport<T>;
|
||||
ideDiffSupport?: IDEDiffSupport<T>
|
||||
|
||||
// Worker badge for teammate permission requests
|
||||
workerBadge: WorkerBadgeProps | undefined;
|
||||
};
|
||||
workerBadge: WorkerBadgeProps | undefined
|
||||
}
|
||||
|
||||
export function FilePermissionDialog<T extends ToolInput = ToolInput>({
|
||||
toolUseConfirm,
|
||||
toolUseContext,
|
||||
@@ -60,33 +71,38 @@ export function FilePermissionDialog<T extends ToolInput = ToolInput>({
|
||||
operationType = 'write',
|
||||
ideDiffSupport,
|
||||
workerBadge,
|
||||
languageName: languageNameOverride
|
||||
languageName: languageNameOverride,
|
||||
}: FilePermissionDialogProps<T>): React.ReactNode {
|
||||
// Derive from path unless caller provided an explicit override (NotebookEdit
|
||||
// passes 'python'/'markdown' from cell_type). getLanguageName is async;
|
||||
// downstream UnaryEvent.language_name and logPermissionEvent already accept
|
||||
// Promise<string>. useMemo keeps the promise stable across renders.
|
||||
const languageName = useMemo(() => languageNameOverride ?? (path ? getLanguageName(path) : 'none'), [languageNameOverride, path]);
|
||||
const unaryEvent = useMemo(() => ({
|
||||
completion_type: completionType,
|
||||
language_name: languageName
|
||||
}), [completionType, languageName]);
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent);
|
||||
const languageName = useMemo(
|
||||
() => languageNameOverride ?? (path ? getLanguageName(path) : 'none'),
|
||||
[languageNameOverride, path],
|
||||
)
|
||||
const unaryEvent = useMemo(
|
||||
() => ({
|
||||
completion_type: completionType,
|
||||
language_name: languageName,
|
||||
}),
|
||||
[completionType, languageName],
|
||||
)
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
|
||||
|
||||
const symlinkTarget = useMemo(() => {
|
||||
if (!path || operationType === 'read') {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const expandedPath = expandPath(path);
|
||||
const fs = getFsImplementation();
|
||||
const {
|
||||
resolvedPath,
|
||||
isSymlink
|
||||
} = safeResolvePath(fs, expandedPath);
|
||||
const expandedPath = expandPath(path)
|
||||
const fs = getFsImplementation()
|
||||
const { resolvedPath, isSymlink } = safeResolvePath(fs, expandedPath)
|
||||
if (isSymlink) {
|
||||
return resolvedPath;
|
||||
return resolvedPath
|
||||
}
|
||||
return null;
|
||||
}, [path, operationType]);
|
||||
return null
|
||||
}, [path, operationType])
|
||||
|
||||
const fileDialogResult = useFilePermissionDialog({
|
||||
filePath: path || '',
|
||||
completionType,
|
||||
@@ -95,8 +111,8 @@ export function FilePermissionDialog<T extends ToolInput = ToolInput>({
|
||||
onDone,
|
||||
onReject,
|
||||
parseInput,
|
||||
operationType
|
||||
});
|
||||
operationType,
|
||||
})
|
||||
|
||||
// Use file dialog results for options
|
||||
const {
|
||||
@@ -107,97 +123,150 @@ export function FilePermissionDialog<T extends ToolInput = ToolInput>({
|
||||
handleInputModeToggle,
|
||||
focusedOption,
|
||||
yesInputMode,
|
||||
noInputMode
|
||||
} = fileDialogResult;
|
||||
noInputMode,
|
||||
} = fileDialogResult
|
||||
|
||||
// Parse input using the provided parser
|
||||
const parsedInput = parseInput(toolUseConfirm.input);
|
||||
const parsedInput = parseInput(toolUseConfirm.input)
|
||||
|
||||
// Set up IDE diff support if enabled. Memoized: getConfig may do disk I/O
|
||||
// (FileWrite's getConfig calls readFileSync for the old-content diff).
|
||||
// Keyed on the raw input — parseInput is a pure Zod parse whose result
|
||||
// depends only on toolUseConfirm.input.
|
||||
const ideDiffConfig = useMemo(() => ideDiffSupport ? ideDiffSupport.getConfig(parseInput(toolUseConfirm.input)) : null, [ideDiffSupport, toolUseConfirm.input]);
|
||||
const ideDiffConfig = useMemo(
|
||||
() =>
|
||||
ideDiffSupport
|
||||
? ideDiffSupport.getConfig(parseInput(toolUseConfirm.input))
|
||||
: null,
|
||||
[ideDiffSupport, toolUseConfirm.input],
|
||||
)
|
||||
|
||||
// Create diff params based on whether IDE diff is available
|
||||
const diffParams = ideDiffConfig ? {
|
||||
onChange: (option: PermissionOption, input: {
|
||||
file_path: string;
|
||||
edits: Array<{
|
||||
old_string: string;
|
||||
new_string: string;
|
||||
replace_all?: boolean;
|
||||
}>;
|
||||
}) => {
|
||||
const transformedInput = ideDiffSupport!.applyChanges(parsedInput, input.edits);
|
||||
fileDialogResult.onChange(option, transformedInput);
|
||||
},
|
||||
toolUseContext,
|
||||
filePath: ideDiffConfig.filePath,
|
||||
edits: (ideDiffConfig.edits || []).map(e => ({
|
||||
old_string: e.old_string,
|
||||
new_string: e.new_string,
|
||||
replace_all: e.replace_all || false
|
||||
})),
|
||||
editMode: ideDiffConfig.editMode || 'single'
|
||||
} : {
|
||||
onChange: () => {},
|
||||
toolUseContext,
|
||||
filePath: '',
|
||||
edits: [],
|
||||
editMode: 'single' as const
|
||||
};
|
||||
const {
|
||||
closeTabInIDE,
|
||||
showingDiffInIDE,
|
||||
ideName
|
||||
} = useDiffInIDE(diffParams);
|
||||
const onChange = (option_0: PermissionOption, feedback?: string) => {
|
||||
closeTabInIDE?.();
|
||||
fileDialogResult.onChange(option_0, parsedInput, feedback?.trim());
|
||||
};
|
||||
if (showingDiffInIDE && ideDiffConfig && path) {
|
||||
return <ShowInIDEPrompt onChange={(option_1: PermissionOption, _input, feedback_0?: string) => onChange(option_1, feedback_0)} options={options} filePath={path} input={parsedInput} ideName={ideName} symlinkTarget={symlinkTarget} rejectFeedback={rejectFeedback} acceptFeedback={acceptFeedback} setFocusedOption={setFocusedOption} onInputModeToggle={handleInputModeToggle} focusedOption={focusedOption} yesInputMode={yesInputMode} noInputMode={noInputMode} />;
|
||||
const diffParams = ideDiffConfig
|
||||
? {
|
||||
onChange: (
|
||||
option: PermissionOption,
|
||||
input: {
|
||||
file_path: string
|
||||
edits: Array<{
|
||||
old_string: string
|
||||
new_string: string
|
||||
replace_all?: boolean
|
||||
}>
|
||||
},
|
||||
) => {
|
||||
const transformedInput = ideDiffSupport!.applyChanges(
|
||||
parsedInput,
|
||||
input.edits,
|
||||
)
|
||||
fileDialogResult.onChange(option, transformedInput)
|
||||
},
|
||||
toolUseContext,
|
||||
filePath: ideDiffConfig.filePath,
|
||||
edits: (ideDiffConfig.edits || []).map(e => ({
|
||||
old_string: e.old_string,
|
||||
new_string: e.new_string,
|
||||
replace_all: e.replace_all || false,
|
||||
})),
|
||||
editMode: ideDiffConfig.editMode || 'single',
|
||||
}
|
||||
: {
|
||||
onChange: () => {},
|
||||
toolUseContext,
|
||||
filePath: '',
|
||||
edits: [],
|
||||
editMode: 'single' as const,
|
||||
}
|
||||
|
||||
const { closeTabInIDE, showingDiffInIDE, ideName } = useDiffInIDE(diffParams)
|
||||
|
||||
const onChange = (option: PermissionOption, feedback?: string) => {
|
||||
closeTabInIDE?.()
|
||||
fileDialogResult.onChange(option, parsedInput, feedback?.trim())
|
||||
}
|
||||
const isSymlinkOutsideCwd = symlinkTarget != null && relative(getCwd(), symlinkTarget).startsWith('..');
|
||||
const symlinkWarning = symlinkTarget ? <Box paddingX={1} marginBottom={1}>
|
||||
|
||||
if (showingDiffInIDE && ideDiffConfig && path) {
|
||||
return (
|
||||
<ShowInIDEPrompt
|
||||
onChange={(option: PermissionOption, _input, feedback?: string) =>
|
||||
onChange(option, feedback)
|
||||
}
|
||||
options={options}
|
||||
filePath={path}
|
||||
input={parsedInput}
|
||||
ideName={ideName}
|
||||
symlinkTarget={symlinkTarget}
|
||||
rejectFeedback={rejectFeedback}
|
||||
acceptFeedback={acceptFeedback}
|
||||
setFocusedOption={setFocusedOption}
|
||||
onInputModeToggle={handleInputModeToggle}
|
||||
focusedOption={focusedOption}
|
||||
yesInputMode={yesInputMode}
|
||||
noInputMode={noInputMode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const isSymlinkOutsideCwd =
|
||||
symlinkTarget != null && relative(getCwd(), symlinkTarget).startsWith('..')
|
||||
|
||||
const symlinkWarning = symlinkTarget ? (
|
||||
<Box paddingX={1} marginBottom={1}>
|
||||
<Text color="warning">
|
||||
{isSymlinkOutsideCwd ? `This will modify ${symlinkTarget} (outside working directory) via a symlink` : `Symlink target: ${symlinkTarget}`}
|
||||
{isSymlinkOutsideCwd
|
||||
? `This will modify ${symlinkTarget} (outside working directory) via a symlink`
|
||||
: `Symlink target: ${symlinkTarget}`}
|
||||
</Text>
|
||||
</Box> : null;
|
||||
return <>
|
||||
<PermissionDialog title={title} subtitle={subtitle} innerPaddingX={0} workerBadge={workerBadge}>
|
||||
</Box>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<>
|
||||
<PermissionDialog
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
innerPaddingX={0}
|
||||
workerBadge={workerBadge}
|
||||
>
|
||||
{symlinkWarning}
|
||||
{content}
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
{typeof question === 'string' ? <Text>{question}</Text> : question}
|
||||
<Select options={options} inlineDescriptions onChange={value => {
|
||||
const selected = options.find(opt => opt.value === value);
|
||||
if (selected) {
|
||||
// For reject option
|
||||
if (selected.option.type === 'reject') {
|
||||
const trimmedFeedback = rejectFeedback.trim();
|
||||
onChange(selected.option, trimmedFeedback || undefined);
|
||||
return;
|
||||
}
|
||||
// For accept-once option, pass accept feedback if present
|
||||
if (selected.option.type === 'accept-once') {
|
||||
const trimmedFeedback_0 = acceptFeedback.trim();
|
||||
onChange(selected.option, trimmedFeedback_0 || undefined);
|
||||
return;
|
||||
}
|
||||
onChange(selected.option);
|
||||
}
|
||||
}} onCancel={() => onChange({
|
||||
type: 'reject'
|
||||
})} onFocus={value_0 => setFocusedOption(value_0)} onInputModeToggle={handleInputModeToggle} />
|
||||
<Select
|
||||
options={options}
|
||||
inlineDescriptions
|
||||
onChange={value => {
|
||||
const selected = options.find(opt => opt.value === value)
|
||||
if (selected) {
|
||||
// For reject option
|
||||
if (selected.option.type === 'reject') {
|
||||
const trimmedFeedback = rejectFeedback.trim()
|
||||
onChange(selected.option, trimmedFeedback || undefined)
|
||||
return
|
||||
}
|
||||
// For accept-once option, pass accept feedback if present
|
||||
if (selected.option.type === 'accept-once') {
|
||||
const trimmedFeedback = acceptFeedback.trim()
|
||||
onChange(selected.option, trimmedFeedback || undefined)
|
||||
return
|
||||
}
|
||||
onChange(selected.option)
|
||||
}
|
||||
}}
|
||||
onCancel={() => onChange({ type: 'reject' })}
|
||||
onFocus={value => setFocusedOption(value)}
|
||||
onInputModeToggle={handleInputModeToggle}
|
||||
/>
|
||||
</Box>
|
||||
</PermissionDialog>
|
||||
<Box paddingX={1} marginTop={1}>
|
||||
<Text dimColor>
|
||||
Esc to cancel
|
||||
{(focusedOption === 'yes' && !yesInputMode || focusedOption === 'no' && !noInputMode) && ' · Tab to amend'}
|
||||
{((focusedOption === 'yes' && !yesInputMode) ||
|
||||
(focusedOption === 'no' && !noInputMode)) &&
|
||||
' · Tab to amend'}
|
||||
</Text>
|
||||
</Box>
|
||||
</>;
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
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 '../../../ink.js';
|
||||
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';
|
||||
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 '../../../ink.js'
|
||||
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`);
|
||||
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);
|
||||
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 + '/');
|
||||
return (
|
||||
normalizedAbsolutePath.startsWith(
|
||||
normalizedClaudeFolderPath + sep.toLowerCase(),
|
||||
) ||
|
||||
// Also match case where sep is / on posix systems
|
||||
normalizedAbsolutePath.startsWith(normalizedClaudeFolderPath + '/')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,24 +40,33 @@ export function isInClaudeFolder(filePath: string): boolean {
|
||||
* 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 + '/');
|
||||
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 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';
|
||||
option: PermissionOption
|
||||
}
|
||||
|
||||
export type FileOperationType = 'read' | 'write' | 'create'
|
||||
|
||||
export function getFilePermissionOptions({
|
||||
filePath,
|
||||
toolPermissionContext,
|
||||
@@ -57,18 +74,22 @@ export function getFilePermissionOptions({
|
||||
onRejectFeedbackChange,
|
||||
onAcceptFeedbackChange,
|
||||
yesInputMode = false,
|
||||
noInputMode = false
|
||||
noInputMode = false,
|
||||
}: {
|
||||
filePath: string;
|
||||
toolPermissionContext: ToolPermissionContext;
|
||||
operationType?: FileOperationType;
|
||||
onRejectFeedbackChange?: (value: string) => void;
|
||||
onAcceptFeedbackChange?: (value: string) => void;
|
||||
yesInputMode?: boolean;
|
||||
noInputMode?: boolean;
|
||||
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');
|
||||
const options: PermissionOptionWithLabel[] = []
|
||||
const modeCycleShortcut = getShortcutDisplay(
|
||||
'chat:cycleMode',
|
||||
'Chat',
|
||||
'shift+tab',
|
||||
)
|
||||
|
||||
// When in input mode, show input field
|
||||
if (yesInputMode && onAcceptFeedbackChange) {
|
||||
@@ -79,24 +100,24 @@ export function getFilePermissionOptions({
|
||||
placeholder: 'and tell Claude what to do next',
|
||||
onChange: onAcceptFeedbackChange,
|
||||
allowEmptySubmitToCancel: true,
|
||||
option: {
|
||||
type: 'accept-once'
|
||||
}
|
||||
});
|
||||
option: { type: 'accept-once' },
|
||||
})
|
||||
} else {
|
||||
options.push({
|
||||
label: 'Yes',
|
||||
value: 'yes',
|
||||
option: {
|
||||
type: 'accept-once'
|
||||
}
|
||||
});
|
||||
option: { type: 'accept-once' },
|
||||
})
|
||||
}
|
||||
const inAllowedPath = pathInAllowedWorkingPath(filePath, toolPermissionContext);
|
||||
|
||||
const inAllowedPath = pathInAllowedWorkingPath(
|
||||
filePath,
|
||||
toolPermissionContext,
|
||||
)
|
||||
|
||||
// Check if this is a .claude/ folder path (project or global)
|
||||
const inClaudeFolder = isInClaudeFolder(filePath);
|
||||
const inGlobalClaudeFolder = isInGlobalClaudeFolder(filePath);
|
||||
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,
|
||||
@@ -108,45 +129,52 @@ export function getFilePermissionOptions({
|
||||
value: 'yes-claude-folder',
|
||||
option: {
|
||||
type: 'accept-session',
|
||||
scope: inGlobalClaudeFolder ? 'global-claude-folder' : 'claude-folder'
|
||||
}
|
||||
});
|
||||
scope: inGlobalClaudeFolder ? 'global-claude-folder' : 'claude-folder',
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Option 2: Allow all changes/reads during session
|
||||
let sessionLabel: ReactNode;
|
||||
let sessionLabel: ReactNode
|
||||
|
||||
if (inAllowedPath) {
|
||||
// Inside working directory
|
||||
if (operationType === 'read') {
|
||||
sessionLabel = 'Yes, during this session';
|
||||
sessionLabel = 'Yes, during this session'
|
||||
} else {
|
||||
sessionLabel = <Text>
|
||||
sessionLabel = (
|
||||
<Text>
|
||||
Yes, allow all edits during this session{' '}
|
||||
<Text bold>({modeCycleShortcut})</Text>
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Outside working directory - include directory name
|
||||
const dirPath = getDirectoryForPath(filePath);
|
||||
const dirName = basename(dirPath) || 'this directory';
|
||||
const dirPath = getDirectoryForPath(filePath)
|
||||
const dirName = basename(dirPath) || 'this directory'
|
||||
|
||||
if (operationType === 'read') {
|
||||
sessionLabel = <Text>
|
||||
sessionLabel = (
|
||||
<Text>
|
||||
Yes, allow reading from <Text bold>{dirName}/</Text> during this
|
||||
session
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
} else {
|
||||
sessionLabel = <Text>
|
||||
sessionLabel = (
|
||||
<Text>
|
||||
Yes, allow all edits in <Text bold>{dirName}/</Text> during this
|
||||
session <Text bold>({modeCycleShortcut})</Text>
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
options.push({
|
||||
label: sessionLabel,
|
||||
value: 'yes-session',
|
||||
option: {
|
||||
type: 'accept-session'
|
||||
}
|
||||
});
|
||||
option: { type: 'accept-session' },
|
||||
})
|
||||
}
|
||||
|
||||
// When in input mode, show input field for reject
|
||||
@@ -158,19 +186,16 @@ export function getFilePermissionOptions({
|
||||
placeholder: 'and tell Claude what to do differently',
|
||||
onChange: onRejectFeedbackChange,
|
||||
allowEmptySubmitToCancel: true,
|
||||
option: {
|
||||
type: 'reject'
|
||||
}
|
||||
});
|
||||
option: { type: 'reject' },
|
||||
})
|
||||
} else {
|
||||
// Not in input mode - simple option
|
||||
options.push({
|
||||
label: 'No',
|
||||
value: 'no',
|
||||
option: {
|
||||
type: 'reject'
|
||||
}
|
||||
});
|
||||
option: { type: 'reject' },
|
||||
})
|
||||
}
|
||||
return options;
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user