mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Remove manual memory creation from settings
Memory files are produced by the agent during conversation, so the desktop settings surface should focus on browsing, search, and editing existing Markdown memory rather than offering a manual file factory. Constraint: Project memory creation belongs to the conversation/runtime flow, not Settings. Rejected: Keep the add box as a fallback | it implies users should manually seed runtime-owned memory and creates low-value UI clutter. Confidence: high Scope-risk: narrow Directive: Do not reintroduce manual memory-file creation in Settings unless the runtime product flow explicitly changes. Tested: cd desktop && bunx vitest run src/__tests__/memorySettings.test.tsx Tested: cd desktop && bun run lint Tested: bun run check:desktop Tested: Browser smoke on local Vite/server verified the create placeholder, create action, and duplicate-path error text are absent while file search and four memory files render.
This commit is contained in:
parent
c125963346
commit
33ede2982c
@ -112,6 +112,8 @@ describe('MemorySettings', () => {
|
||||
expect(memoryApiMock.listProjects).toHaveBeenCalledWith('/workspace/demo')
|
||||
expect(await screen.findByText('/workspace/demo')).toBeInTheDocument()
|
||||
expect(await screen.findByText('Project conventions.')).toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('MEMORY.md or notes/project.md')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /create memory file/i })).not.toBeInTheDocument()
|
||||
|
||||
const editor = await screen.findByLabelText('Editor')
|
||||
expect(editor).toHaveValue('# Project Memory\n')
|
||||
@ -133,46 +135,6 @@ describe('MemorySettings', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('creates a new markdown memory file with a project frontmatter template', async () => {
|
||||
memoryApiMock.listFiles
|
||||
.mockResolvedValueOnce({ files: [] })
|
||||
.mockResolvedValueOnce({
|
||||
files: [
|
||||
{
|
||||
path: 'notes/team.md',
|
||||
name: 'team.md',
|
||||
title: 'team',
|
||||
bytes: 75,
|
||||
updatedAt: '2026-05-01T00:02:00.000Z',
|
||||
type: 'project',
|
||||
isIndex: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
memoryApiMock.readFile.mockResolvedValueOnce({
|
||||
file: {
|
||||
path: 'notes/team.md',
|
||||
content: '# team\n',
|
||||
updatedAt: '2026-05-01T00:02:00.000Z',
|
||||
bytes: 75,
|
||||
},
|
||||
})
|
||||
|
||||
render(<MemorySettings />)
|
||||
|
||||
const pathInput = await screen.findByPlaceholderText('MEMORY.md or notes/project.md')
|
||||
fireEvent.change(pathInput, { target: { value: 'notes/team.md' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: /create memory file/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(memoryApiMock.saveFile).toHaveBeenCalledWith({
|
||||
projectId: '-workspace-demo',
|
||||
path: 'notes/team.md',
|
||||
content: expect.stringContaining('type: project'),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('filters projects by path so large memory lists are navigable', async () => {
|
||||
memoryApiMock.listProjects.mockResolvedValue({
|
||||
projects: [
|
||||
|
||||
@ -549,11 +549,9 @@ export const en = {
|
||||
'settings.memory.files': 'Memory files',
|
||||
'settings.memory.editor': 'Editor',
|
||||
'settings.memory.preview': 'Preview',
|
||||
'settings.memory.newPathPlaceholder': 'MEMORY.md or notes/project.md',
|
||||
'settings.memory.newFile': 'New',
|
||||
'settings.memory.emptyProjects': 'No memory projects found.',
|
||||
'settings.memory.emptyFiles': 'No memory files yet. Create MEMORY.md to start.',
|
||||
'settings.memory.selectFile': 'Select or create a memory file.',
|
||||
'settings.memory.emptyFiles': 'No memory files found yet.',
|
||||
'settings.memory.selectFile': 'Select a memory file.',
|
||||
'settings.memory.selectProject': 'Select a project.',
|
||||
'settings.memory.noFileSelected': 'No file selected',
|
||||
'settings.memory.current': 'Current',
|
||||
@ -561,13 +559,10 @@ export const en = {
|
||||
'settings.memory.unsaved': 'Unsaved',
|
||||
'settings.memory.saved': 'Saved',
|
||||
'settings.memory.revert': 'Revert',
|
||||
'settings.memory.invalidPath': 'Use a relative .md path without .. segments.',
|
||||
'settings.memory.fileExists': 'A memory file already exists at that path.',
|
||||
'settings.memory.projectSearchPlaceholder': 'Search projects by path...',
|
||||
'settings.memory.fileSearchPlaceholder': 'Search memory files...',
|
||||
'settings.memory.noProjectMatches': 'No projects match this search.',
|
||||
'settings.memory.noFileMatches': 'No memory files match this search.',
|
||||
'settings.memory.createMemoryFile': 'Create memory file',
|
||||
'settings.memory.clearSearch': 'Clear search',
|
||||
|
||||
// Settings > Plugins
|
||||
|
||||
@ -551,11 +551,9 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.memory.files': '记忆文件',
|
||||
'settings.memory.editor': '编辑',
|
||||
'settings.memory.preview': '预览',
|
||||
'settings.memory.newPathPlaceholder': 'MEMORY.md 或 notes/project.md',
|
||||
'settings.memory.newFile': '新建',
|
||||
'settings.memory.emptyProjects': '还没有找到记忆项目。',
|
||||
'settings.memory.emptyFiles': '还没有记忆文件。可以先创建 MEMORY.md。',
|
||||
'settings.memory.selectFile': '选择或创建一个记忆文件。',
|
||||
'settings.memory.emptyFiles': '还没有找到记忆文件。',
|
||||
'settings.memory.selectFile': '选择一个记忆文件。',
|
||||
'settings.memory.selectProject': '选择一个项目。',
|
||||
'settings.memory.noFileSelected': '未选择文件',
|
||||
'settings.memory.current': '当前',
|
||||
@ -563,13 +561,10 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.memory.unsaved': '未保存',
|
||||
'settings.memory.saved': '已保存',
|
||||
'settings.memory.revert': '还原',
|
||||
'settings.memory.invalidPath': '请使用相对 .md 路径,且不能包含 .. 片段。',
|
||||
'settings.memory.fileExists': '该路径下已经有记忆文件。',
|
||||
'settings.memory.projectSearchPlaceholder': '按路径搜索项目...',
|
||||
'settings.memory.fileSearchPlaceholder': '搜索记忆文件...',
|
||||
'settings.memory.noProjectMatches': '没有匹配的项目。',
|
||||
'settings.memory.noFileMatches': '没有匹配的记忆文件。',
|
||||
'settings.memory.createMemoryFile': '创建记忆文件',
|
||||
'settings.memory.clearSearch': '清空搜索',
|
||||
|
||||
// Settings > Plugins
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { FileText, Plus, Search, X } from 'lucide-react'
|
||||
import { FileText, Search, X } from 'lucide-react'
|
||||
import { Button } from '../components/shared/Button'
|
||||
import { MarkdownRenderer } from '../components/markdown/MarkdownRenderer'
|
||||
import { useTranslation } from '../i18n'
|
||||
@ -31,14 +31,11 @@ export function MemorySettings() {
|
||||
openFile,
|
||||
updateDraft,
|
||||
saveFile,
|
||||
createFile,
|
||||
} = useMemoryStore()
|
||||
const sessions = useSessionStore((s) => s.sessions)
|
||||
const activeSessionId = useSessionStore((s) => s.activeSessionId)
|
||||
const pendingMemoryPath = useUIStore((s) => s.pendingMemoryPath)
|
||||
const setPendingMemoryPath = useUIStore((s) => s.setPendingMemoryPath)
|
||||
const [newPath, setNewPath] = useState('')
|
||||
const [newPathError, setNewPathError] = useState<string | null>(null)
|
||||
const [projectQuery, setProjectQuery] = useState('')
|
||||
const [fileQuery, setFileQuery] = useState('')
|
||||
|
||||
@ -130,22 +127,6 @@ export function MemorySettings() {
|
||||
void openFile(selectedProjectId, file.path)
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!selectedProjectId) return
|
||||
const path = normalizeMemoryPath(newPath || DEFAULT_MEMORY_PATH)
|
||||
if (!isValidMemoryPath(path)) {
|
||||
setNewPathError(t('settings.memory.invalidPath'))
|
||||
return
|
||||
}
|
||||
if (files.some((file) => file.path === path)) {
|
||||
setNewPathError(t('settings.memory.fileExists'))
|
||||
return
|
||||
}
|
||||
setNewPath('')
|
||||
setNewPathError(null)
|
||||
void createFile(selectedProjectId, path, buildMemoryTemplate(path))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-[640px] flex-col gap-5">
|
||||
<header className="flex flex-col gap-4 border-b border-[var(--color-border)] pb-5 sm:flex-row sm:items-end sm:justify-between">
|
||||
@ -214,23 +195,8 @@ export function MemorySettings() {
|
||||
title={t('settings.memory.files')}
|
||||
meta={isLoadingFiles ? t('common.loading') : `${files.length}`}
|
||||
/>
|
||||
<div className="grid gap-3 border-b border-[var(--color-border)] p-3">
|
||||
<CreateFileControl
|
||||
value={newPath}
|
||||
onChange={(value) => {
|
||||
setNewPath(value)
|
||||
setNewPathError(null)
|
||||
}}
|
||||
onCreate={handleCreate}
|
||||
disabled={!selectedProjectId}
|
||||
loading={isSaving && !selectedFile}
|
||||
placeholder={t('settings.memory.newPathPlaceholder')}
|
||||
label={t('settings.memory.createMemoryFile')}
|
||||
/>
|
||||
{newPathError && (
|
||||
<p className="mt-2 text-xs text-[var(--color-error)]">{newPathError}</p>
|
||||
)}
|
||||
{files.length > 3 ? (
|
||||
{files.length > 3 ? (
|
||||
<div className="border-b border-[var(--color-border)] p-3">
|
||||
<SearchField
|
||||
value={fileQuery}
|
||||
onChange={setFileQuery}
|
||||
@ -238,8 +204,8 @@ export function MemorySettings() {
|
||||
ariaLabel={t('settings.memory.fileSearchPlaceholder')}
|
||||
clearLabel={t('settings.memory.clearSearch')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="max-h-[420px] overflow-y-auto p-2">
|
||||
{files.length === 0 && !isLoadingFiles ? (
|
||||
<EmptyState text={t('settings.memory.emptyFiles')} />
|
||||
@ -373,48 +339,6 @@ function SearchField({
|
||||
)
|
||||
}
|
||||
|
||||
function CreateFileControl({
|
||||
value,
|
||||
onChange,
|
||||
onCreate,
|
||||
disabled,
|
||||
loading,
|
||||
placeholder,
|
||||
label,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
onCreate: () => void
|
||||
disabled: boolean
|
||||
loading: boolean
|
||||
placeholder: string
|
||||
label: string
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_44px] gap-2">
|
||||
<input
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') onCreate()
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
aria-label={label}
|
||||
className="h-11 min-w-0 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 text-sm text-[var(--color-text-primary)] outline-none transition-colors duration-150 placeholder:text-[var(--color-text-tertiary)] focus:border-[var(--color-border-focus)] focus:shadow-[var(--shadow-focus-ring)]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={label}
|
||||
disabled={disabled || loading}
|
||||
onClick={onCreate}
|
||||
className="flex h-11 w-11 items-center justify-center rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-primary)] transition-colors duration-150 hover:bg-[var(--color-surface-hover)] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<Plus size={18} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PanelHeader({ title, meta }: { title: string; meta: string }) {
|
||||
return (
|
||||
<div className="flex h-12 items-center justify-between border-b border-[var(--color-border)] px-3">
|
||||
@ -510,10 +434,6 @@ function EmptyState({ text }: { text: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeMemoryPath(value: string): string {
|
||||
return value.trim().replace(/\\/g, '/').replace(/^\/+/, '')
|
||||
}
|
||||
|
||||
function normalizeSearch(value: string): string {
|
||||
return value.toLowerCase().replace(/\\/g, '/').replace(/\/+/g, '/').trim()
|
||||
}
|
||||
@ -571,28 +491,6 @@ function resolveMemoryFileTarget(projects: MemoryProject[], absolutePath: string
|
||||
return null
|
||||
}
|
||||
|
||||
function isValidMemoryPath(path: string): boolean {
|
||||
return (
|
||||
path.length > 0 &&
|
||||
path.endsWith('.md') &&
|
||||
!path.includes('\0') &&
|
||||
!path.split('/').some((part) => part === '' || part === '.' || part === '..')
|
||||
)
|
||||
}
|
||||
|
||||
function buildMemoryTemplate(path: string): string {
|
||||
const parts = path.split('/')
|
||||
const title = parts[parts.length - 1]?.replace(/\.md$/, '') || 'Memory'
|
||||
return `---
|
||||
type: project
|
||||
description: Manually curated project memory.
|
||||
---
|
||||
|
||||
# ${title}
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
function formatDate(value: string): string {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user