mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
* feat: 第一版大重构 * fix: 修复类型问题 * chore: 更新版本到 1.3.2 * Add brave as alternative WebSearchTool * fix: 修正顺序 * fix: 修复对穷鬼模式的 auto dream 和 session memory 越过 * feat: 穷鬼模式去除 session-summary * feat: 创建 builtin-tools 包,搬运所有工具实现 将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/, 内部导入路径已更新为 src/ alias 模式。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/ - src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/ - 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock - tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射 - 新增 packages/builtin-tools/src 至 include Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀 所有包名及 import 路径统一添加 @claude-code-best/ 前缀: - builtin-tools → @claude-code-best/builtin-tools - mcp-client → @claude-code-best/mcp-client - agent-tools → @claude-code-best/agent-tools Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复 node 环境没有 bun 的问题 --------- Co-authored-by: Eric-Guo <eric.guocz@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
478 lines
15 KiB
TypeScript
478 lines
15 KiB
TypeScript
import figures from 'figures'
|
|
import React, { useCallback, useMemo, useState } from 'react'
|
|
import { mcpInfoFromString } from 'src/services/mcp/mcpStringUtils.js'
|
|
import { isMcpTool } from 'src/services/mcp/utils.js'
|
|
import type { Tool, Tools } from 'src/Tool.js'
|
|
import { filterToolsForAgent } from '@claude-code-best/builtin-tools/tools/AgentTool/agentToolUtils.js'
|
|
import { AGENT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/AgentTool/constants.js'
|
|
import { BashTool } from '@claude-code-best/builtin-tools/tools/BashTool/BashTool.js'
|
|
import { ExitPlanModeV2Tool } from '@claude-code-best/builtin-tools/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
|
|
import { FileEditTool } from '@claude-code-best/builtin-tools/tools/FileEditTool/FileEditTool.js'
|
|
import { FileReadTool } from '@claude-code-best/builtin-tools/tools/FileReadTool/FileReadTool.js'
|
|
import { FileWriteTool } from '@claude-code-best/builtin-tools/tools/FileWriteTool/FileWriteTool.js'
|
|
import { GlobTool } from '@claude-code-best/builtin-tools/tools/GlobTool/GlobTool.js'
|
|
import { GrepTool } from '@claude-code-best/builtin-tools/tools/GrepTool/GrepTool.js'
|
|
import { ListMcpResourcesTool } from '@claude-code-best/builtin-tools/tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
|
|
import { NotebookEditTool } from '@claude-code-best/builtin-tools/tools/NotebookEditTool/NotebookEditTool.js'
|
|
import { ReadMcpResourceTool } from '@claude-code-best/builtin-tools/tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
|
|
import { TaskOutputTool } from '@claude-code-best/builtin-tools/tools/TaskOutputTool/TaskOutputTool.js'
|
|
import { TaskStopTool } from '@claude-code-best/builtin-tools/tools/TaskStopTool/TaskStopTool.js'
|
|
import { TodoWriteTool } from '@claude-code-best/builtin-tools/tools/TodoWriteTool/TodoWriteTool.js'
|
|
import { TungstenTool } from '@claude-code-best/builtin-tools/tools/TungstenTool/TungstenTool.js'
|
|
import { WebFetchTool } from '@claude-code-best/builtin-tools/tools/WebFetchTool/WebFetchTool.js'
|
|
import { WebSearchTool } from '@claude-code-best/builtin-tools/tools/WebSearchTool/WebSearchTool.js'
|
|
import { type KeyboardEvent, Box, Text } from '@anthropic/ink'
|
|
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
|
import { count } from '../../utils/array.js'
|
|
import { plural } from '../../utils/stringUtils.js'
|
|
import { Divider } from '@anthropic/ink'
|
|
|
|
type Props = {
|
|
tools: Tools
|
|
initialTools: string[] | undefined
|
|
onComplete: (selectedTools: string[] | undefined) => void
|
|
onCancel?: () => void
|
|
}
|
|
|
|
type ToolBucket = {
|
|
name: string
|
|
toolNames: Set<string>
|
|
isMcp?: boolean
|
|
}
|
|
|
|
type ToolBuckets = {
|
|
READ_ONLY: ToolBucket
|
|
EDIT: ToolBucket
|
|
EXECUTION: ToolBucket
|
|
MCP: ToolBucket
|
|
OTHER: ToolBucket
|
|
}
|
|
|
|
function getToolBuckets(): ToolBuckets {
|
|
return {
|
|
READ_ONLY: {
|
|
name: 'Read-only tools',
|
|
toolNames: new Set([
|
|
GlobTool.name,
|
|
GrepTool.name,
|
|
ExitPlanModeV2Tool.name,
|
|
FileReadTool.name,
|
|
WebFetchTool.name,
|
|
TodoWriteTool.name,
|
|
WebSearchTool.name,
|
|
TaskStopTool.name,
|
|
TaskOutputTool.name,
|
|
ListMcpResourcesTool.name,
|
|
ReadMcpResourceTool.name,
|
|
]),
|
|
},
|
|
EDIT: {
|
|
name: 'Edit tools',
|
|
toolNames: new Set([
|
|
FileEditTool.name,
|
|
FileWriteTool.name,
|
|
NotebookEditTool.name,
|
|
]),
|
|
},
|
|
EXECUTION: {
|
|
name: 'Execution tools',
|
|
toolNames: new Set(
|
|
[
|
|
BashTool.name,
|
|
process.env.USER_TYPE === 'ant' ? TungstenTool.name : undefined,
|
|
].filter(n => n !== undefined),
|
|
),
|
|
},
|
|
MCP: {
|
|
name: 'MCP tools',
|
|
toolNames: new Set(), // Dynamic - no static list
|
|
isMcp: true,
|
|
},
|
|
OTHER: {
|
|
name: 'Other tools',
|
|
toolNames: new Set(), // Dynamic - catch-all for uncategorized tools
|
|
},
|
|
}
|
|
}
|
|
|
|
// Helper to get MCP server buckets dynamically
|
|
function getMcpServerBuckets(tools: Tools): Array<{
|
|
serverName: string
|
|
tools: Tools
|
|
}> {
|
|
const serverMap = new Map<string, Tool[]>()
|
|
|
|
tools.forEach(tool => {
|
|
if (isMcpTool(tool)) {
|
|
const mcpInfo = mcpInfoFromString(tool.name)
|
|
if (mcpInfo?.serverName) {
|
|
const existing = serverMap.get(mcpInfo.serverName) || []
|
|
existing.push(tool)
|
|
serverMap.set(mcpInfo.serverName, existing)
|
|
}
|
|
}
|
|
})
|
|
|
|
return Array.from(serverMap.entries())
|
|
.map(([serverName, tools]) => ({ serverName, tools }))
|
|
.sort((a, b) => a.serverName.localeCompare(b.serverName))
|
|
}
|
|
|
|
export function ToolSelector({
|
|
tools,
|
|
initialTools,
|
|
onComplete,
|
|
onCancel,
|
|
}: Props): React.ReactNode {
|
|
// Filter tools for custom agents
|
|
const customAgentTools = useMemo(
|
|
() => filterToolsForAgent({ tools, isBuiltIn: false, isAsync: false }),
|
|
[tools],
|
|
)
|
|
|
|
// Expand wildcard or undefined to explicit tool list for internal state
|
|
const expandedInitialTools =
|
|
!initialTools || initialTools.includes('*')
|
|
? customAgentTools.map(t => t.name)
|
|
: initialTools
|
|
|
|
const [selectedTools, setSelectedTools] =
|
|
useState<string[]>(expandedInitialTools)
|
|
const [focusIndex, setFocusIndex] = useState(0)
|
|
const [showIndividualTools, setShowIndividualTools] = useState(false)
|
|
|
|
// Filter selectedTools to only include tools that currently exist
|
|
// This handles MCP tools that disconnect while selected
|
|
const validSelectedTools = useMemo(() => {
|
|
const toolNames = new Set(customAgentTools.map(t => t.name))
|
|
return selectedTools.filter(name => toolNames.has(name))
|
|
}, [selectedTools, customAgentTools])
|
|
|
|
const selectedSet = new Set(validSelectedTools)
|
|
const isAllSelected =
|
|
validSelectedTools.length === customAgentTools.length &&
|
|
customAgentTools.length > 0
|
|
|
|
const handleToggleTool = (toolName: string) => {
|
|
if (!toolName) return
|
|
|
|
setSelectedTools(current =>
|
|
current.includes(toolName)
|
|
? current.filter(t => t !== toolName)
|
|
: [...current, toolName],
|
|
)
|
|
}
|
|
|
|
const handleToggleTools = (toolNames: string[], select: boolean) => {
|
|
setSelectedTools(current => {
|
|
if (select) {
|
|
const toolsToAdd = toolNames.filter(t => !current.includes(t))
|
|
return [...current, ...toolsToAdd]
|
|
} else {
|
|
return current.filter(t => !toolNames.includes(t))
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleConfirm = () => {
|
|
// Convert to undefined if all tools are selected (for cleaner file format)
|
|
const allToolNames = customAgentTools.map(t => t.name)
|
|
const areAllToolsSelected =
|
|
validSelectedTools.length === allToolNames.length &&
|
|
allToolNames.every(name => validSelectedTools.includes(name))
|
|
const finalTools = areAllToolsSelected ? undefined : validSelectedTools
|
|
|
|
onComplete(finalTools)
|
|
}
|
|
|
|
// Group tools by bucket
|
|
const toolsByBucket = useMemo(() => {
|
|
const toolBuckets = getToolBuckets()
|
|
const buckets = {
|
|
readOnly: [] as Tool[],
|
|
edit: [] as Tool[],
|
|
execution: [] as Tool[],
|
|
mcp: [] as Tool[],
|
|
other: [] as Tool[],
|
|
}
|
|
|
|
customAgentTools.forEach(tool => {
|
|
// Check if it's an MCP tool first
|
|
if (isMcpTool(tool)) {
|
|
buckets.mcp.push(tool)
|
|
} else if (toolBuckets.READ_ONLY.toolNames.has(tool.name)) {
|
|
buckets.readOnly.push(tool)
|
|
} else if (toolBuckets.EDIT.toolNames.has(tool.name)) {
|
|
buckets.edit.push(tool)
|
|
} else if (toolBuckets.EXECUTION.toolNames.has(tool.name)) {
|
|
buckets.execution.push(tool)
|
|
} else if (tool.name !== AGENT_TOOL_NAME) {
|
|
// Catch-all for uncategorized tools (except Task)
|
|
buckets.other.push(tool)
|
|
}
|
|
})
|
|
|
|
return buckets
|
|
}, [customAgentTools])
|
|
|
|
const createBucketToggleAction = (bucketTools: Tool[]) => {
|
|
const selected = count(bucketTools, t => selectedSet.has(t.name))
|
|
const needsSelection = selected < bucketTools.length
|
|
|
|
return () => {
|
|
const toolNames = bucketTools.map(t => t.name)
|
|
handleToggleTools(toolNames, needsSelection)
|
|
}
|
|
}
|
|
|
|
// Build navigable items (no separators)
|
|
const navigableItems: Array<{
|
|
id: string
|
|
label: string
|
|
action: () => void
|
|
isContinue?: boolean
|
|
isToggle?: boolean
|
|
isHeader?: boolean
|
|
}> = []
|
|
|
|
// Continue button
|
|
navigableItems.push({
|
|
id: 'continue',
|
|
label: 'Continue',
|
|
action: handleConfirm,
|
|
isContinue: true,
|
|
})
|
|
|
|
// All tools
|
|
navigableItems.push({
|
|
id: 'bucket-all',
|
|
label: `${isAllSelected ? figures.checkboxOn : figures.checkboxOff} All tools`,
|
|
action: () => {
|
|
const allToolNames = customAgentTools.map(t => t.name)
|
|
handleToggleTools(allToolNames, !isAllSelected)
|
|
},
|
|
})
|
|
|
|
// Create bucket menu items
|
|
const toolBuckets = getToolBuckets()
|
|
const bucketConfigs = [
|
|
{
|
|
id: 'bucket-readonly',
|
|
name: toolBuckets.READ_ONLY.name,
|
|
tools: toolsByBucket.readOnly,
|
|
},
|
|
{
|
|
id: 'bucket-edit',
|
|
name: toolBuckets.EDIT.name,
|
|
tools: toolsByBucket.edit,
|
|
},
|
|
{
|
|
id: 'bucket-execution',
|
|
name: toolBuckets.EXECUTION.name,
|
|
tools: toolsByBucket.execution,
|
|
},
|
|
{
|
|
id: 'bucket-mcp',
|
|
name: toolBuckets.MCP.name,
|
|
tools: toolsByBucket.mcp,
|
|
},
|
|
{
|
|
id: 'bucket-other',
|
|
name: toolBuckets.OTHER.name,
|
|
tools: toolsByBucket.other,
|
|
},
|
|
]
|
|
|
|
bucketConfigs.forEach(({ id, name, tools: bucketTools }) => {
|
|
if (bucketTools.length === 0) return
|
|
|
|
const selected = count(bucketTools, t => selectedSet.has(t.name))
|
|
const isFullySelected = selected === bucketTools.length
|
|
|
|
navigableItems.push({
|
|
id,
|
|
label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${name}`,
|
|
action: createBucketToggleAction(bucketTools),
|
|
})
|
|
})
|
|
|
|
// Toggle button for individual tools
|
|
const toggleButtonIndex = navigableItems.length
|
|
navigableItems.push({
|
|
id: 'toggle-individual',
|
|
label: showIndividualTools
|
|
? 'Hide advanced options'
|
|
: 'Show advanced options',
|
|
action: () => {
|
|
setShowIndividualTools(!showIndividualTools)
|
|
// If hiding tools and focus is on an individual tool, move focus to toggle button
|
|
if (showIndividualTools && focusIndex > toggleButtonIndex) {
|
|
setFocusIndex(toggleButtonIndex)
|
|
}
|
|
},
|
|
isToggle: true,
|
|
})
|
|
|
|
// Memoize MCP server buckets (must be outside conditional for hooks rules)
|
|
const mcpServerBuckets = useMemo(
|
|
() => getMcpServerBuckets(customAgentTools),
|
|
[customAgentTools],
|
|
)
|
|
|
|
// Individual tools (only if expanded)
|
|
if (showIndividualTools) {
|
|
// Add MCP server buckets if any exist
|
|
if (mcpServerBuckets.length > 0) {
|
|
navigableItems.push({
|
|
id: 'mcp-servers-header',
|
|
label: 'MCP Servers:',
|
|
action: () => {}, // No action - just a header
|
|
isHeader: true,
|
|
})
|
|
|
|
mcpServerBuckets.forEach(({ serverName, tools: serverTools }) => {
|
|
const selected = count(serverTools, t => selectedSet.has(t.name))
|
|
const isFullySelected = selected === serverTools.length
|
|
|
|
navigableItems.push({
|
|
id: `mcp-server-${serverName}`,
|
|
label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${serverName} (${serverTools.length} ${plural(serverTools.length, 'tool')})`,
|
|
action: () => {
|
|
const toolNames = serverTools.map(t => t.name)
|
|
handleToggleTools(toolNames, !isFullySelected)
|
|
},
|
|
})
|
|
})
|
|
|
|
// Add separator header before individual tools
|
|
navigableItems.push({
|
|
id: 'tools-header',
|
|
label: 'Individual Tools:',
|
|
action: () => {},
|
|
isHeader: true,
|
|
})
|
|
}
|
|
|
|
// Add individual tools
|
|
customAgentTools.forEach(tool => {
|
|
let displayName = tool.name
|
|
if (tool.name.startsWith('mcp__')) {
|
|
const mcpInfo = mcpInfoFromString(tool.name)
|
|
displayName = mcpInfo
|
|
? `${mcpInfo.toolName} (${mcpInfo.serverName})`
|
|
: tool.name
|
|
}
|
|
|
|
navigableItems.push({
|
|
id: `tool-${tool.name}`,
|
|
label: `${selectedSet.has(tool.name) ? figures.checkboxOn : figures.checkboxOff} ${displayName}`,
|
|
action: () => handleToggleTool(tool.name),
|
|
})
|
|
})
|
|
}
|
|
|
|
const handleCancel = useCallback(() => {
|
|
if (onCancel) {
|
|
onCancel()
|
|
} else {
|
|
onComplete(initialTools)
|
|
}
|
|
}, [onCancel, onComplete, initialTools])
|
|
|
|
useKeybinding('confirm:no', handleCancel, { context: 'Confirmation' })
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'return') {
|
|
e.preventDefault()
|
|
const item = navigableItems[focusIndex]
|
|
if (item && !item.isHeader) {
|
|
item.action()
|
|
}
|
|
} else if (e.key === 'up') {
|
|
e.preventDefault()
|
|
let newIndex = focusIndex - 1
|
|
// Skip headers when navigating up
|
|
while (newIndex > 0 && navigableItems[newIndex]?.isHeader) {
|
|
newIndex--
|
|
}
|
|
setFocusIndex(Math.max(0, newIndex))
|
|
} else if (e.key === 'down') {
|
|
e.preventDefault()
|
|
let newIndex = focusIndex + 1
|
|
// Skip headers when navigating down
|
|
while (
|
|
newIndex < navigableItems.length - 1 &&
|
|
navigableItems[newIndex]?.isHeader
|
|
) {
|
|
newIndex++
|
|
}
|
|
setFocusIndex(Math.min(navigableItems.length - 1, newIndex))
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
flexDirection="column"
|
|
marginTop={1}
|
|
tabIndex={0}
|
|
autoFocus
|
|
onKeyDown={handleKeyDown}
|
|
>
|
|
{/* Render Continue button */}
|
|
<Text
|
|
color={focusIndex === 0 ? 'suggestion' : undefined}
|
|
bold={focusIndex === 0}
|
|
>
|
|
{focusIndex === 0 ? `${figures.pointer} ` : ' '}[ Continue ]
|
|
</Text>
|
|
|
|
{/* Separator */}
|
|
<Divider width={40} />
|
|
|
|
{/* Render all navigable items except Continue (which is at index 0) */}
|
|
{navigableItems.slice(1).map((item, index) => {
|
|
const isCurrentlyFocused = index + 1 === focusIndex
|
|
const isToggleButton = item.isToggle
|
|
const isHeader = item.isHeader
|
|
|
|
return (
|
|
<React.Fragment key={item.id}>
|
|
{/* Add separator before toggle button */}
|
|
{isToggleButton && <Divider width={40} />}
|
|
|
|
{/* Add margin before headers */}
|
|
{isHeader && index > 0 && <Box marginTop={1} />}
|
|
|
|
<Text
|
|
color={
|
|
isHeader
|
|
? undefined
|
|
: isCurrentlyFocused
|
|
? 'suggestion'
|
|
: undefined
|
|
}
|
|
dimColor={isHeader}
|
|
bold={isToggleButton && isCurrentlyFocused}
|
|
>
|
|
{isHeader
|
|
? ''
|
|
: isCurrentlyFocused
|
|
? `${figures.pointer} `
|
|
: ' '}
|
|
{isToggleButton ? `[ ${item.label} ]` : item.label}
|
|
</Text>
|
|
</React.Fragment>
|
|
)
|
|
})}
|
|
|
|
<Box marginTop={1} flexDirection="column">
|
|
<Text dimColor>
|
|
{isAllSelected
|
|
? 'All tools selected'
|
|
: `${selectedSet.size} of ${customAgentTools.length} tools selected`}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|