From 33ede2982ca16790f4b0228120f2ed2b288ea4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Wed, 13 May 2026 18:26:02 +0800 Subject: [PATCH] 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. --- desktop/src/__tests__/memorySettings.test.tsx | 42 +------ desktop/src/i18n/locales/en.ts | 9 +- desktop/src/i18n/locales/zh.ts | 9 +- desktop/src/pages/MemorySettings.tsx | 112 +----------------- 4 files changed, 11 insertions(+), 161 deletions(-) diff --git a/desktop/src/__tests__/memorySettings.test.tsx b/desktop/src/__tests__/memorySettings.test.tsx index de7619c8..6166ede1 100644 --- a/desktop/src/__tests__/memorySettings.test.tsx +++ b/desktop/src/__tests__/memorySettings.test.tsx @@ -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() - - 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: [ diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index 705d3c3f..bcac534c 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -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 diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index 43764257..4108c6dd 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -551,11 +551,9 @@ export const zh: Record = { '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 = { '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 diff --git a/desktop/src/pages/MemorySettings.tsx b/desktop/src/pages/MemorySettings.tsx index 2060cf68..9a5fa951 100644 --- a/desktop/src/pages/MemorySettings.tsx +++ b/desktop/src/pages/MemorySettings.tsx @@ -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(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 (
@@ -214,23 +195,8 @@ export function MemorySettings() { title={t('settings.memory.files')} meta={isLoadingFiles ? t('common.loading') : `${files.length}`} /> -
- { - setNewPath(value) - setNewPathError(null) - }} - onCreate={handleCreate} - disabled={!selectedProjectId} - loading={isSaving && !selectedFile} - placeholder={t('settings.memory.newPathPlaceholder')} - label={t('settings.memory.createMemoryFile')} - /> - {newPathError && ( -

{newPathError}

- )} - {files.length > 3 ? ( + {files.length > 3 ? ( +
- ) : null} -
+
+ ) : null}
{files.length === 0 && !isLoadingFiles ? ( @@ -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 ( -
- 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)]" - /> - -
- ) -} - function PanelHeader({ title, meta }: { title: string; meta: string }) { return (
@@ -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 ''