Files
claude-code/src/components/memory/MemoryFileSelector.tsx
claude-code-best 2fb1c9dcd8 feat: 工具层及 mcp 大重构 (#252)
* 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>
2026-04-13 09:52:05 +08:00

324 lines
11 KiB
TypeScript

import { feature } from 'bun:bundle'
import chalk from 'chalk'
import { mkdir } from 'fs/promises'
import { join } from 'path'
import * as React from 'react'
import { use, useEffect, useState } from 'react'
import { getOriginalCwd } from '../../bootstrap/state.js'
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
import { Box, Text, ListItem } from '@anthropic/ink'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import { getAutoMemPath, isAutoMemoryEnabled } from '../../memdir/paths.js'
import { logEvent } from '../../services/analytics/index.js'
import { isAutoDreamEnabled } from '../../services/autoDream/config.js'
import { readLastConsolidatedAt } from '../../services/autoDream/consolidationLock.js'
import { useAppState } from '../../state/AppState.js'
import { getAgentMemoryDir } from '@claude-code-best/builtin-tools/tools/AgentTool/agentMemory.js'
import { openPath } from '../../utils/browser.js'
import { getMemoryFiles, type MemoryFileInfo } from '../../utils/claudemd.js'
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
import { getDisplayPath } from '../../utils/file.js'
import { formatRelativeTimeAgo } from '../../utils/format.js'
import { projectIsInGitRepo } from '../../utils/memory/versions.js'
import { updateSettingsForSource } from '../../utils/settings/settings.js'
import { Select } from '../CustomSelect/index.js'
/* eslint-disable @typescript-eslint/no-require-imports */
const teamMemPaths = feature('TEAMMEM')
? (require('../../memdir/teamMemPaths.js') as typeof import('../../memdir/teamMemPaths.js'))
: null
/* eslint-enable @typescript-eslint/no-require-imports */
interface ExtendedMemoryFileInfo extends MemoryFileInfo {
isNested?: boolean
exists: boolean
}
// Remember last selected path
let lastSelectedPath: string | undefined
const OPEN_FOLDER_PREFIX = '__open_folder__'
type Props = {
onSelect: (path: string) => void
onCancel: () => void
}
export function MemoryFileSelector({
onSelect,
onCancel,
}: Props): React.ReactNode {
const existingMemoryFiles = use(getMemoryFiles()) as MemoryFileInfo[]
// Create entries for User and Project CLAUDE.md even if they don't exist
const userMemoryPath = join(getClaudeConfigHomeDir(), 'CLAUDE.md')
const projectMemoryPath = join(getOriginalCwd(), 'CLAUDE.md')
// Check if these are already in the existing files
const hasUserMemory = existingMemoryFiles.some(f => f.path === userMemoryPath)
const hasProjectMemory = existingMemoryFiles.some(
f => f.path === projectMemoryPath,
)
// Filter out AutoMem/TeamMem entrypoints: these are MEMORY.md files, and
// /memory already surfaces "Open auto-memory folder" / "Open team memory
// folder" options below. Listing the entrypoint file separately is redundant.
const allMemoryFiles: ExtendedMemoryFileInfo[] = [
...existingMemoryFiles
.filter(f => f.type !== 'AutoMem' && f.type !== 'TeamMem')
.map(f => ({ ...f, exists: true })),
// Add User memory if it doesn't exist
...(hasUserMemory
? []
: [
{
path: userMemoryPath,
type: 'User' as const,
content: '',
exists: false,
},
]),
// Add Project memory if it doesn't exist
...(hasProjectMemory
? []
: [
{
path: projectMemoryPath,
type: 'Project' as const,
content: '',
exists: false,
},
]),
]
const depths = new Map<string, number>()
// Create options for the select component
const memoryOptions = allMemoryFiles.map(file => {
const displayPath = getDisplayPath(file.path)
const existsLabel = file.exists ? '' : ' (new)'
// Calculate depth based on parent
const depth = file.parent ? (depths.get(file.parent) ?? 0) + 1 : 0
depths.set(file.path, depth)
const indent = depth > 0 ? ' '.repeat(depth - 1) : ''
// Format label based on type
let label: string
if (
file.type === 'User' &&
!file.isNested &&
file.path === userMemoryPath
) {
label = `User memory`
} else if (
file.type === 'Project' &&
!file.isNested &&
file.path === projectMemoryPath
) {
label = `Project memory`
} else if (depth > 0) {
// For child nodes (imported files), show indented with L
label = `${indent}L ${displayPath}${existsLabel}`
} else {
// For other memory files, just show the path
label = `${displayPath}`
}
// Create description based on type - keep the original descriptions for built-in types
let description: string
const isGit = projectIsInGitRepo(getOriginalCwd())
if (file.type === 'User' && !file.isNested) {
description = 'Saved in ~/.claude/CLAUDE.md'
} else if (
file.type === 'Project' &&
!file.isNested &&
file.path === projectMemoryPath
) {
description = `${isGit ? 'Checked in at' : 'Saved in'} ./CLAUDE.md`
} else if (file.parent) {
// For imported files (with @-import)
description = '@-imported'
} else if (file.isNested) {
// For nested files (dynamically loaded)
description = 'dynamically loaded'
} else {
description = ''
}
return {
label,
value: file.path,
description,
}
})
// Add "Open folder" options for auto-memory and agent memory directories
const folderOptions: Array<{
label: string
value: string
description: string
}> = []
const agentDefinitions = useAppState(s => s.agentDefinitions)
if (isAutoMemoryEnabled()) {
// Always show auto-memory folder option
folderOptions.push({
label: 'Open auto-memory folder',
value: `${OPEN_FOLDER_PREFIX}${getAutoMemPath()}`,
description: '',
})
// Team memory directly below auto-memory (team dir is a subdir of auto dir)
if (feature('TEAMMEM') && teamMemPaths!.isTeamMemoryEnabled()) {
folderOptions.push({
label: 'Open team memory folder',
value: `${OPEN_FOLDER_PREFIX}${teamMemPaths!.getTeamMemPath()}`,
description: '',
})
}
// Add agent memory folders for agents that have memory configured
for (const agent of agentDefinitions.activeAgents) {
if (agent.memory) {
const agentDir = getAgentMemoryDir(agent.agentType, agent.memory)
folderOptions.push({
label: `Open ${chalk.bold(agent.agentType)} agent memory`,
value: `${OPEN_FOLDER_PREFIX}${agentDir}`,
description: `${agent.memory} scope`,
})
}
}
}
memoryOptions.push(...folderOptions)
// Initialize with last selected path if it's still in the options, otherwise use first option
const initialPath =
lastSelectedPath &&
memoryOptions.some(opt => opt.value === lastSelectedPath)
? lastSelectedPath
: memoryOptions[0]?.value || ''
// Toggle state (local copy of settings so the UI updates immediately)
const [autoMemoryOn, setAutoMemoryOn] = useState(isAutoMemoryEnabled)
const [autoDreamOn, setAutoDreamOn] = useState(isAutoDreamEnabled)
// Dream row is only meaningful when auto-memory is on (dream consolidates
// that dir). Snapshot at mount so the row doesn't vanish mid-navigation
// if the user toggles auto-memory off.
const [showDreamRow] = useState(isAutoMemoryEnabled)
// Dream status: prefer live task state (this session fired it), fall back
// to the cross-process lock mtime.
const isDreamRunning = useAppState(s =>
Object.values(s.tasks).some(
t => t.type === 'dream' && t.status === 'running',
),
)
const [lastDreamAt, setLastDreamAt] = useState<number | null>(null)
useEffect(() => {
if (!showDreamRow) return
void readLastConsolidatedAt().then(setLastDreamAt)
}, [showDreamRow, isDreamRunning])
const dreamStatus = isDreamRunning
? 'running'
: lastDreamAt === null
? '' // stat in flight
: lastDreamAt === 0
? 'never'
: `last ran ${formatRelativeTimeAgo(new Date(lastDreamAt))}`
// null = Select has focus, 0 = auto-memory, 1 = auto-dream (if showDreamRow)
const [focusedToggle, setFocusedToggle] = useState<number | null>(null)
const toggleFocused = focusedToggle !== null
const lastToggleIndex = showDreamRow ? 1 : 0
function handleToggleAutoMemory(): void {
const newValue = !autoMemoryOn
updateSettingsForSource('userSettings', { autoMemoryEnabled: newValue })
setAutoMemoryOn(newValue)
logEvent('tengu_auto_memory_toggled', { enabled: newValue })
}
function handleToggleAutoDream(): void {
const newValue = !autoDreamOn
updateSettingsForSource('userSettings', { autoDreamEnabled: newValue })
setAutoDreamOn(newValue)
logEvent('tengu_auto_dream_toggled', { enabled: newValue })
}
useExitOnCtrlCDWithKeybindings()
useKeybinding('confirm:no', onCancel, { context: 'Confirmation' })
useKeybinding(
'confirm:yes',
() => {
if (focusedToggle === 0) handleToggleAutoMemory()
else if (focusedToggle === 1) handleToggleAutoDream()
},
{ context: 'Confirmation', isActive: toggleFocused },
)
useKeybinding(
'select:next',
() => {
setFocusedToggle(prev =>
prev !== null && prev < lastToggleIndex ? prev + 1 : null,
)
},
{ context: 'Select', isActive: toggleFocused },
)
useKeybinding(
'select:previous',
() => {
setFocusedToggle(prev => (prev !== null && prev > 0 ? prev - 1 : prev))
},
{ context: 'Select', isActive: toggleFocused },
)
return (
<Box flexDirection="column" width="100%">
<Box flexDirection="column" marginBottom={1}>
<ListItem isFocused={focusedToggle === 0}>
<Text>Auto-memory: {autoMemoryOn ? 'on' : 'off'}</Text>
</ListItem>
{showDreamRow && (
<ListItem isFocused={focusedToggle === 1} styled={false}>
<Text color={focusedToggle === 1 ? 'suggestion' : undefined}>
Auto-dream: {autoDreamOn ? 'on' : 'off'}
{dreamStatus && <Text dimColor> · {dreamStatus}</Text>}
{!isDreamRunning && autoDreamOn && (
<Text dimColor> · /dream to run</Text>
)}
</Text>
</ListItem>
)}
</Box>
<Select
defaultFocusValue={initialPath}
options={memoryOptions}
isDisabled={toggleFocused}
onChange={value => {
if (value.startsWith(OPEN_FOLDER_PREFIX)) {
const folderPath = value.slice(OPEN_FOLDER_PREFIX.length)
// Ensure folder exists before opening (idempotent; swallow
// permission errors to match previous behavior)
void mkdir(folderPath, { recursive: true })
.catch(() => {})
.then(() => openPath(folderPath))
return
}
lastSelectedPath = value // Remember the selection
onSelect(value)
}}
onCancel={onCancel}
onUpFromFirstItem={() => setFocusedToggle(lastToggleIndex)}
/>
</Box>
)
}