mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix(desktop): make memory files preview-first (#533)
Tested: cd desktop && bun run test src/__tests__/memorySettings.test.tsx Tested: bun run check:desktop Scope-risk: narrow Confidence: high
This commit is contained in:
parent
0a8f247781
commit
d11748d61a
@ -124,7 +124,7 @@ describe('MemorySettings', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('loads project-scoped markdown memory and saves manual edits', async () => {
|
||||
it('opens memory files in preview mode, edits on demand, and returns to preview after save', async () => {
|
||||
render(<MemorySettings />)
|
||||
|
||||
expect(await screen.findByText('Project Memory')).toBeInTheDocument()
|
||||
@ -136,6 +136,11 @@ describe('MemorySettings', () => {
|
||||
expect(screen.queryByPlaceholderText('MEMORY.md or notes/project.md')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /create memory file/i })).not.toBeInTheDocument()
|
||||
|
||||
expect(await screen.findByTestId('markdown-preview')).toHaveTextContent('Project Memory')
|
||||
expect(screen.queryByLabelText('Editor')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
|
||||
const editor = await screen.findByLabelText('Editor')
|
||||
expect(editor).toHaveValue('# Project Memory\n')
|
||||
|
||||
@ -143,7 +148,7 @@ describe('MemorySettings', () => {
|
||||
target: { value: '# Project Memory\n\n- Prefer small diffs.\n' },
|
||||
})
|
||||
expect(screen.getByText('Unsaved')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('markdown-preview')).toHaveTextContent('Prefer small diffs')
|
||||
expect(screen.queryByTestId('markdown-preview')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /save/i }))
|
||||
|
||||
@ -154,6 +159,101 @@ describe('MemorySettings', () => {
|
||||
content: '# Project Memory\n\n- Prefer small diffs.\n',
|
||||
})
|
||||
})
|
||||
expect(screen.queryByLabelText('Editor')).not.toBeInTheDocument()
|
||||
expect(await screen.findByTestId('markdown-preview')).toHaveTextContent('Prefer small diffs')
|
||||
})
|
||||
|
||||
it('cancels edit mode by discarding the unsaved draft', async () => {
|
||||
render(<MemorySettings />)
|
||||
|
||||
expect(await screen.findByTestId('markdown-preview')).toHaveTextContent('Project Memory')
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
|
||||
const editor = await screen.findByLabelText('Editor')
|
||||
fireEvent.change(editor, {
|
||||
target: { value: '# Changed Memory\n' },
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
|
||||
|
||||
expect(memoryApiMock.saveFile).not.toHaveBeenCalled()
|
||||
expect(screen.queryByLabelText('Editor')).not.toBeInTheDocument()
|
||||
expect(await screen.findByTestId('markdown-preview')).toHaveTextContent('Project Memory')
|
||||
expect(screen.queryByText('Changed Memory')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps unsaved edits when file switching is not confirmed', async () => {
|
||||
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false)
|
||||
memoryApiMock.listFiles.mockResolvedValue({
|
||||
files: [
|
||||
{
|
||||
path: 'MEMORY.md',
|
||||
name: 'MEMORY.md',
|
||||
title: 'MEMORY.md',
|
||||
bytes: 18,
|
||||
updatedAt: '2026-05-01T00:00:00.000Z',
|
||||
type: 'project',
|
||||
description: 'Project conventions.',
|
||||
isIndex: true,
|
||||
},
|
||||
{
|
||||
path: 'notes/manual.md',
|
||||
name: 'manual.md',
|
||||
title: 'Manual',
|
||||
bytes: 42,
|
||||
updatedAt: '2026-05-01T00:02:00.000Z',
|
||||
type: 'guidance',
|
||||
description: 'Operator workflow.',
|
||||
isIndex: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
memoryApiMock.readFile.mockImplementation((_projectId: string, path: string) => Promise.resolve({
|
||||
file: {
|
||||
path,
|
||||
content: path === 'notes/manual.md' ? '# Manual\n' : '# Project Memory\n',
|
||||
updatedAt: '2026-05-01T00:00:00.000Z',
|
||||
bytes: 18,
|
||||
},
|
||||
}))
|
||||
|
||||
render(<MemorySettings />)
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Edit' }))
|
||||
const editor = await screen.findByLabelText('Editor')
|
||||
fireEvent.change(editor, {
|
||||
target: { value: '# Unsaved Memory\n' },
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('Manual'))
|
||||
|
||||
expect(confirmSpy).toHaveBeenCalled()
|
||||
expect(memoryApiMock.readFile).not.toHaveBeenCalledWith('-workspace-demo', 'notes/manual.md')
|
||||
expect(screen.getByLabelText('Editor')).toHaveValue('# Unsaved Memory\n')
|
||||
|
||||
confirmSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('saves with the platform shortcut while editing and returns to preview mode', async () => {
|
||||
render(<MemorySettings />)
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Edit' }))
|
||||
const editor = await screen.findByLabelText('Editor')
|
||||
fireEvent.change(editor, {
|
||||
target: { value: '# Project Memory\n\n- Shortcut save.\n' },
|
||||
})
|
||||
|
||||
fireEvent.keyDown(document, { key: 's', metaKey: true })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(memoryApiMock.saveFile).toHaveBeenCalledWith({
|
||||
projectId: '-workspace-demo',
|
||||
path: 'MEMORY.md',
|
||||
content: '# Project Memory\n\n- Shortcut save.\n',
|
||||
})
|
||||
})
|
||||
expect(screen.queryByLabelText('Editor')).not.toBeInTheDocument()
|
||||
expect(await screen.findByTestId('markdown-preview')).toHaveTextContent('Shortcut save')
|
||||
})
|
||||
|
||||
it('filters the unified resource tree by project path', async () => {
|
||||
@ -257,7 +357,8 @@ describe('MemorySettings', () => {
|
||||
await waitFor(() => {
|
||||
expect(memoryApiMock.readFile).toHaveBeenCalledWith('-workspace-demo', 'notes/manual.md')
|
||||
})
|
||||
expect(await screen.findByLabelText('Editor')).toHaveValue('# Manual\n')
|
||||
expect(await screen.findByTestId('markdown-preview')).toHaveTextContent('Manual')
|
||||
expect(screen.queryByLabelText('Editor')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens linked memory markdown files from the rendered preview', async () => {
|
||||
@ -302,7 +403,8 @@ describe('MemorySettings', () => {
|
||||
await waitFor(() => {
|
||||
expect(memoryApiMock.readFile).toHaveBeenCalledWith('-workspace-demo', 'notes/manual.md')
|
||||
})
|
||||
expect(await screen.findByLabelText('Editor')).toHaveValue('# Manual\n')
|
||||
expect(await screen.findByTestId('markdown-preview')).toHaveTextContent('Manual')
|
||||
expect(screen.queryByLabelText('Editor')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps frontmatter editable but removes it from the rendered preview', async () => {
|
||||
@ -317,10 +419,13 @@ describe('MemorySettings', () => {
|
||||
|
||||
render(<MemorySettings />)
|
||||
|
||||
expect(await screen.findByTestId('markdown-preview')).toHaveTextContent('Project Memory')
|
||||
expect(screen.getByTestId('markdown-preview')).not.toHaveTextContent('type: project')
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Edit' }))
|
||||
|
||||
const editor = await screen.findByLabelText('Editor')
|
||||
expect(editor).toHaveValue('---\ntype: project\n---\n\n# Project Memory\n')
|
||||
expect(screen.getByTestId('markdown-preview')).toHaveTextContent('Project Memory')
|
||||
expect(screen.getByTestId('markdown-preview')).not.toHaveTextContent('type: project')
|
||||
})
|
||||
|
||||
it('opens the exact memory file requested from chat', async () => {
|
||||
@ -373,8 +478,8 @@ describe('MemorySettings', () => {
|
||||
|
||||
render(<MemorySettings />)
|
||||
|
||||
const editor = await screen.findByLabelText('Editor')
|
||||
expect(editor).toHaveValue('# Preferences\n')
|
||||
expect(await screen.findByTestId('markdown-preview')).toHaveTextContent('Preferences')
|
||||
expect(screen.queryByLabelText('Editor')).not.toBeInTheDocument()
|
||||
expect(memoryApiMock.readFile).toHaveBeenCalledWith('-workspace-other', 'preferences.md')
|
||||
expect(useMemoryStore.getState().selectedProjectId).toBe('-workspace-other')
|
||||
expect(useUIStore.getState().pendingMemoryPath).toBeNull()
|
||||
|
||||
@ -704,6 +704,7 @@ export const en = {
|
||||
'settings.memory.files': 'Memory files',
|
||||
'settings.memory.resourceManager': 'Resource manager',
|
||||
'settings.memory.editor': 'Editor',
|
||||
'settings.memory.edit': 'Edit',
|
||||
'settings.memory.preview': 'Preview',
|
||||
'settings.memory.rendered': 'Rendered',
|
||||
'settings.memory.emptyProjects': 'No memory projects found.',
|
||||
@ -718,6 +719,7 @@ export const en = {
|
||||
'settings.memory.unsaved': 'Unsaved',
|
||||
'settings.memory.saved': 'Saved',
|
||||
'settings.memory.revert': 'Revert',
|
||||
'settings.memory.discardUnsavedConfirm': 'Discard unsaved memory changes?',
|
||||
'settings.memory.projectSearchPlaceholder': 'Search projects by path...',
|
||||
'settings.memory.fileSearchPlaceholder': 'Search memory files...',
|
||||
'settings.memory.resourceSearchPlaceholder': 'Search projects or memory files...',
|
||||
|
||||
@ -706,6 +706,7 @@ export const jp: Record<TranslationKey, string> = {
|
||||
'settings.memory.files': 'メモリファイル',
|
||||
'settings.memory.resourceManager': 'リソースマネージャー',
|
||||
'settings.memory.editor': 'エディター',
|
||||
'settings.memory.edit': '編集',
|
||||
'settings.memory.preview': 'プレビュー',
|
||||
'settings.memory.rendered': 'レンダリング後',
|
||||
'settings.memory.emptyProjects': 'メモリのあるプロジェクトが見つかりません。',
|
||||
@ -720,6 +721,7 @@ export const jp: Record<TranslationKey, string> = {
|
||||
'settings.memory.unsaved': '未保存',
|
||||
'settings.memory.saved': '保存済み',
|
||||
'settings.memory.revert': '元に戻す',
|
||||
'settings.memory.discardUnsavedConfirm': '未保存のメモリ変更を破棄しますか?',
|
||||
'settings.memory.projectSearchPlaceholder': 'パスでプロジェクトを検索...',
|
||||
'settings.memory.fileSearchPlaceholder': 'メモリファイルを検索...',
|
||||
'settings.memory.resourceSearchPlaceholder': 'プロジェクトまたはメモリファイルを検索...',
|
||||
|
||||
@ -706,6 +706,7 @@ export const kr: Record<TranslationKey, string> = {
|
||||
'settings.memory.files': '메모리 파일',
|
||||
'settings.memory.resourceManager': '리소스 관리자',
|
||||
'settings.memory.editor': '편집기',
|
||||
'settings.memory.edit': '편집',
|
||||
'settings.memory.preview': '미리 보기',
|
||||
'settings.memory.rendered': '렌더링됨',
|
||||
'settings.memory.emptyProjects': '메모리가 있는 프로젝트를 찾을 수 없습니다.',
|
||||
@ -720,6 +721,7 @@ export const kr: Record<TranslationKey, string> = {
|
||||
'settings.memory.unsaved': '저장 안 됨',
|
||||
'settings.memory.saved': '저장됨',
|
||||
'settings.memory.revert': '되돌리기',
|
||||
'settings.memory.discardUnsavedConfirm': '저장하지 않은 메모리 변경을 버릴까요?',
|
||||
'settings.memory.projectSearchPlaceholder': '경로로 프로젝트 검색...',
|
||||
'settings.memory.fileSearchPlaceholder': '메모리 파일 검색...',
|
||||
'settings.memory.resourceSearchPlaceholder': '프로젝트 또는 메모리 파일 검색...',
|
||||
|
||||
@ -706,6 +706,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.memory.files': '記憶檔案',
|
||||
'settings.memory.resourceManager': '資源管理器',
|
||||
'settings.memory.editor': '編輯',
|
||||
'settings.memory.edit': '編輯',
|
||||
'settings.memory.preview': '預覽',
|
||||
'settings.memory.rendered': '已渲染',
|
||||
'settings.memory.emptyProjects': '還沒有找到記憶專案。',
|
||||
@ -720,6 +721,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.memory.unsaved': '未儲存',
|
||||
'settings.memory.saved': '已儲存',
|
||||
'settings.memory.revert': '還原',
|
||||
'settings.memory.discardUnsavedConfirm': '要放棄未儲存的記憶修改嗎?',
|
||||
'settings.memory.projectSearchPlaceholder': '按路徑搜尋專案...',
|
||||
'settings.memory.fileSearchPlaceholder': '搜尋記憶檔案...',
|
||||
'settings.memory.resourceSearchPlaceholder': '搜尋專案或記憶檔案...',
|
||||
|
||||
@ -706,6 +706,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.memory.files': '记忆文件',
|
||||
'settings.memory.resourceManager': '资源管理器',
|
||||
'settings.memory.editor': '编辑',
|
||||
'settings.memory.edit': '编辑',
|
||||
'settings.memory.preview': '预览',
|
||||
'settings.memory.rendered': '已渲染',
|
||||
'settings.memory.emptyProjects': '还没有找到记忆项目。',
|
||||
@ -720,6 +721,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.memory.unsaved': '未保存',
|
||||
'settings.memory.saved': '已保存',
|
||||
'settings.memory.revert': '还原',
|
||||
'settings.memory.discardUnsavedConfirm': '要放弃未保存的记忆修改吗?',
|
||||
'settings.memory.projectSearchPlaceholder': '按路径搜索项目...',
|
||||
'settings.memory.fileSearchPlaceholder': '搜索记忆文件...',
|
||||
'settings.memory.resourceSearchPlaceholder': '搜索项目或记忆文件...',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { BookOpenText, ChevronDown, ChevronRight, Database, FileText, Folder, FolderGit2, RefreshCw, RotateCcw, Save, Search, X } from 'lucide-react'
|
||||
import { BookOpenText, ChevronDown, ChevronRight, Database, FileText, Folder, FolderGit2, PencilLine, RefreshCw, RotateCcw, Save, Search, X } from 'lucide-react'
|
||||
import { Button } from '../components/shared/Button'
|
||||
import { MarkdownRenderer } from '../components/markdown/MarkdownRenderer'
|
||||
import { useTranslation } from '../i18n'
|
||||
@ -40,6 +40,7 @@ export function MemorySettings() {
|
||||
const [resourceQuery, setResourceQuery] = useState('')
|
||||
const [expandedProjectId, setExpandedProjectId] = useState<string | null>(null)
|
||||
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(new Set())
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
|
||||
const activeSession = useMemo(
|
||||
() => sessions.find((session) => session.id === activeSessionId),
|
||||
@ -61,6 +62,7 @@ export function MemorySettings() {
|
||||
[filteredFiles],
|
||||
)
|
||||
const previewContent = stripMarkdownFrontmatter(draftContent)
|
||||
const selectedFilePath = selectedFile?.path ?? null
|
||||
|
||||
useEffect(() => {
|
||||
void fetchProjects(activeCwd)
|
||||
@ -76,6 +78,10 @@ export function MemorySettings() {
|
||||
setExpandedProjectId(selectedProjectId)
|
||||
}, [selectedProjectId])
|
||||
|
||||
useEffect(() => {
|
||||
setIsEditing(false)
|
||||
}, [selectedFilePath])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedProjectId || selectedFile || isLoadingFiles || isLoadingFile) return
|
||||
if (pendingMemoryPath) return
|
||||
@ -115,7 +121,13 @@ export function MemorySettings() {
|
||||
setPendingMemoryPath,
|
||||
])
|
||||
|
||||
const canLeaveDirtyEdit = () => {
|
||||
if (!isEditing || !isDirty) return true
|
||||
return window.confirm(t('settings.memory.discardUnsavedConfirm'))
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (!canLeaveDirtyEdit()) return
|
||||
void fetchProjects(activeCwd)
|
||||
if (selectedProjectId) {
|
||||
void fetchFiles(selectedProjectId)
|
||||
@ -127,6 +139,7 @@ export function MemorySettings() {
|
||||
setExpandedProjectId(null)
|
||||
return
|
||||
}
|
||||
if (projectId !== selectedProjectId && !canLeaveDirtyEdit()) return
|
||||
setExpandedProjectId(projectId)
|
||||
if (projectId !== selectedProjectId) {
|
||||
selectProject(projectId)
|
||||
@ -135,9 +148,33 @@ export function MemorySettings() {
|
||||
|
||||
const handleFileOpen = (file: MemoryFile) => {
|
||||
if (!selectedProjectId || file.path === selectedFile?.path) return
|
||||
if (!canLeaveDirtyEdit()) return
|
||||
void openFile(selectedProjectId, file.path)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!selectedFile) return
|
||||
if (!isDirty) {
|
||||
setIsEditing(false)
|
||||
return
|
||||
}
|
||||
const saved = await saveFile()
|
||||
if (saved) {
|
||||
setIsEditing(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditing || !selectedFile) return
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 's') return
|
||||
event.preventDefault()
|
||||
void handleSave()
|
||||
}
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
const handlePreviewLinkClick = (href: string): boolean => {
|
||||
if (!selectedProjectId || !selectedFile) return false
|
||||
const targetPath = resolveMarkdownMemoryLink(
|
||||
@ -147,6 +184,7 @@ export function MemorySettings() {
|
||||
files,
|
||||
)
|
||||
if (!targetPath || targetPath === selectedFile.path) return false
|
||||
if (!canLeaveDirtyEdit()) return true
|
||||
void openFile(selectedProjectId, targetPath)
|
||||
return true
|
||||
}
|
||||
@ -165,6 +203,13 @@ export function MemorySettings() {
|
||||
|
||||
const forceExpandFiles = Boolean(resourceQuery.trim())
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
if (selectedFile) {
|
||||
updateDraft(selectedFile.content)
|
||||
}
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-[640px] flex-col overflow-hidden rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)]">
|
||||
<header className="grid min-h-[58px] border-b border-[var(--color-border)] bg-[var(--color-surface-container-low)] lg:grid-cols-[280px_minmax(0,1fr)]">
|
||||
@ -199,26 +244,6 @@ export function MemorySettings() {
|
||||
>
|
||||
{t('settings.memory.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={!selectedFile || !isDirty || isSaving}
|
||||
onClick={() => selectedFile && updateDraft(selectedFile.content)}
|
||||
icon={<RotateCcw size={14} aria-hidden="true" />}
|
||||
>
|
||||
{t('settings.memory.revert')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={!selectedFile || !isDirty}
|
||||
loading={isSaving}
|
||||
onClick={() => void saveFile()}
|
||||
icon={<Save size={14} aria-hidden="true" />}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -306,11 +331,44 @@ export function MemorySettings() {
|
||||
</div>
|
||||
|
||||
{selectedFile ? (
|
||||
<div className="grid min-h-[560px] grid-rows-[minmax(300px,1fr)_minmax(260px,0.95fr)] 2xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] 2xl:grid-rows-1">
|
||||
<div className="min-h-0 border-b border-[var(--color-border)] 2xl:border-b-0 2xl:border-r">
|
||||
isEditing ? (
|
||||
<div className="min-h-[560px]">
|
||||
<div className="flex h-10 items-center justify-between border-b border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-3 text-xs font-medium uppercase tracking-normal text-[var(--color-text-tertiary)]">
|
||||
<span>{t('settings.memory.editor')}</span>
|
||||
<span>MARKDOWN</span>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span>{t('settings.memory.editor')}</span>
|
||||
<span>MARKDOWN</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2 normal-case">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={isSaving}
|
||||
onClick={handleCancelEdit}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={!isDirty || isSaving}
|
||||
onClick={() => selectedFile && updateDraft(selectedFile.content)}
|
||||
icon={<RotateCcw size={14} aria-hidden="true" />}
|
||||
>
|
||||
{t('settings.memory.revert')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={isSaving}
|
||||
loading={isSaving}
|
||||
onClick={() => void handleSave()}
|
||||
icon={<Save size={14} aria-hidden="true" />}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
aria-label={t('settings.memory.editor')}
|
||||
@ -320,10 +378,22 @@ export function MemorySettings() {
|
||||
className="h-[calc(100%-40px)] w-full resize-none overflow-auto bg-transparent p-5 font-mono text-[13px] leading-6 text-[var(--color-text-primary)] outline-none placeholder:text-[var(--color-text-tertiary)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-h-0 overflow-y-auto">
|
||||
) : (
|
||||
<div className="min-h-[560px] overflow-y-auto">
|
||||
<div className="flex h-10 items-center justify-between border-b border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-3 text-xs font-medium uppercase tracking-normal text-[var(--color-text-tertiary)]">
|
||||
<span>{t('settings.memory.preview')}</span>
|
||||
<span>{t('settings.memory.rendered')}</span>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span>{t('settings.memory.preview')}</span>
|
||||
<span>{t('settings.memory.rendered')}</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label={t('settings.memory.edit')}
|
||||
title={t('settings.memory.edit')}
|
||||
onClick={() => setIsEditing(true)}
|
||||
icon={<PencilLine size={14} aria-hidden="true" />}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<MarkdownRenderer
|
||||
@ -333,7 +403,7 @@ export function MemorySettings() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex min-h-[520px] items-center justify-center p-8">
|
||||
<EmptyState icon={<FileText size={20} />} text={isLoadingFile ? t('common.loading') : t('settings.memory.selectFile')} />
|
||||
|
||||
@ -20,7 +20,7 @@ type MemoryStore = {
|
||||
fetchFiles: (projectId: string) => Promise<void>
|
||||
openFile: (projectId: string, path: string) => Promise<void>
|
||||
updateDraft: (content: string) => void
|
||||
saveFile: () => Promise<void>
|
||||
saveFile: () => Promise<boolean>
|
||||
createFile: (projectId: string, path: string, content: string) => Promise<void>
|
||||
}
|
||||
|
||||
@ -100,7 +100,7 @@ export const useMemoryStore = create<MemoryStore>((set, get) => ({
|
||||
|
||||
saveFile: async () => {
|
||||
const { selectedProjectId, selectedFile, draftContent } = get()
|
||||
if (!selectedProjectId || !selectedFile) return
|
||||
if (!selectedProjectId || !selectedFile) return false
|
||||
set({ isSaving: true, error: null })
|
||||
try {
|
||||
const { file } = await memoryApi.saveFile({
|
||||
@ -119,8 +119,10 @@ export const useMemoryStore = create<MemoryStore>((set, get) => ({
|
||||
lastSavedAt: file.updatedAt,
|
||||
})
|
||||
await get().fetchFiles(selectedProjectId)
|
||||
return true
|
||||
} catch (err) {
|
||||
set({ error: (err as Error).message, isSaving: false })
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user