更新大量 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:
claude-code-best
2026-04-04 23:24:27 +08:00
committed by GitHub
parent 02694918b5
commit 5b1a52b8e0
559 changed files with 103807 additions and 101817 deletions

View File

@@ -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>
</>;
</>
)
}

View File

@@ -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
}