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() // 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(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(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 ( Auto-memory: {autoMemoryOn ? 'on' : 'off'} {showDreamRow && ( Auto-dream: {autoDreamOn ? 'on' : 'off'} {dreamStatus && · {dreamStatus}} {!isDreamRunning && autoDreamOn && ( · /dream to run )} )}