diff --git a/UI-DESIGN.md b/UI-DESIGN.md index 05a6fef8..3ba08d9e 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -613,6 +613,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证 - “笔记 / 待办”属于同一工作台内的同级内容面板,使用 `PageTabs` 的 `segmented` 视觉变体,与模型设置的分段控件保持同一外观。 - 页签切换保留 `tablist`、`tab` 和 `tabpanel` 语义;待办状态仍使用独立的 `SegmentedControl`,不得与内容页签合并。 +- 在待办面板勾选或取消勾选后,当前缓存中的源笔记正文必须立即同步对应清单状态,不得依赖离开工作区后重新加载。 - 当前记录草稿非空时,切换笔记、待办或内容面板必须先在编辑器旁就地确认;继续编辑时保留草稿并恢复编辑焦点,只有明确选择放弃后才切换。 - 创建、保存、更新、删除和 AI 评论完成等短期结果进入应用级通知,不在编辑区或列表上方堆放页内通知。 - 标题或正文校验、删除确认、同步进度和可就地恢复的错误仍靠近对应编辑器或操作呈现。 diff --git a/src/main/ipc.ts b/src/main/ipc.ts index b69b4270..58728341 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -5686,9 +5686,13 @@ export function registerIpcHandlers( ipcChannels.magicTodosUpdate, (event, input: unknown) => { assertTrustedSender(event, window) - return assistantDatabase.updateMagicTodo( + const todo = assistantDatabase.updateMagicTodo( magicTodoUpdateSchema.parse(input) ) + return { + todo, + note: assistantDatabase.getMagicNote(todo.noteId) + } } ) diff --git a/src/preload/index.ts b/src/preload/index.ts index 08e21ed0..6284b655 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -107,6 +107,7 @@ import type { MagicNoteDetail, MagicNotesSnapshot, MagicTodoItem, + MagicTodoUpdateResult, MagicTodosSnapshot } from '../shared/magic-notes-contracts' import type { @@ -1113,7 +1114,7 @@ const desktopApi: DesktopApi = { ipcRenderer.invoke( ipcChannels.magicTodosUpdate, input - ) as Promise, + ) as Promise, analyzeTodo: (todoId, options) => ipcRenderer.invoke(ipcChannels.magicTodosAnalyze, { todoId, diff --git a/src/renderer/src/MagicNotesWorkspace.test.tsx b/src/renderer/src/MagicNotesWorkspace.test.tsx index 1c4fc312..dedca8e8 100644 --- a/src/renderer/src/MagicNotesWorkspace.test.tsx +++ b/src/renderer/src/MagicNotesWorkspace.test.tsx @@ -14,7 +14,9 @@ import type { ApplicationSettings } from '../../shared/application-settings-cont import type { MagicNoteDetail, MagicNotesSnapshot, + MagicNoteRichContent, MagicTodoItem, + MagicTodoUpdateResult, MagicTodosSnapshot } from '../../shared/magic-notes-contracts' import { MagicNotesWorkspace } from './MagicNotesWorkspace' @@ -47,7 +49,22 @@ vi.mock('./MagicNoteEditor', () => ({ })) vi.mock('./MagicNoteContent', () => ({ - MagicNoteContent: () =>
记录正文
+ MagicNoteContent: ({ content }: { content: MagicNoteRichContent }) => { + const checklistState = + content.ops.find( + (operation) => + operation.attributes?.list === 'checked' || + operation.attributes?.list === 'unchecked' + )?.attributes?.list ?? 'none' + return ( +
+ 记录正文 + + {checklistState} + +
+ ) + } })) const noteId = '00000000-0000-4000-8000-000000000601' @@ -229,11 +246,41 @@ beforeEach(() => { revision: entry.revision + 1 })) }) - updateTodo.mockImplementation(async (input) => ({ - ...noteTodo, - completed: input.completed, - revision: noteTodo.revision + 1 - })) + updateTodo.mockImplementation( + async (input): Promise => ({ + todo: { + ...noteTodo, + completed: input.completed, + revision: input.expectedRevision + 1 + }, + note: { + ...detail, + revision: detail.revision + 1, + entries: detail.entries.map((entry) => + entry.id === noteTodo.entryId + ? { + ...entry, + content: { + version: 1, + ops: [ + { insert: noteTodo.title }, + { + insert: '\n', + attributes: { + list: input.completed ? 'checked' : 'unchecked' + } + } + ] + }, + comments: [], + analyzedAt: undefined, + revision: entry.revision + 1 + } + : entry + ) + } + }) + ) analyze.mockResolvedValue({ ...createdDetail, entries: createdDetail.entries.map((entry) => @@ -957,7 +1004,7 @@ describe('MagicNotesWorkspace', () => { expect(screen.getByText('准备演示')).toBeInTheDocument() }) - it('marks a todo completed from the standalone todo tab', async () => { + it('synchronizes todo completion with its source note immediately', async () => { render() await screen.findByText('记录正文') @@ -986,6 +1033,30 @@ describe('MagicNotesWorkspace', () => { message: '待办已完成' }) ) + + fireEvent.click(screen.getByRole('tab', { name: '笔记' })) + expect( + screen.getByTestId('magic-note-checklist-state') + ).toHaveTextContent('checked') + + fireEvent.click(screen.getByRole('tab', { name: '待办' })) + fireEvent.click( + screen.getByRole('button', { + name: `标记为未完成:${noteTodo.title}` + }) + ) + await waitFor(() => + expect(updateTodo).toHaveBeenNthCalledWith(2, { + todoId: noteTodo.id, + completed: false, + expectedRevision: noteTodo.revision + 1 + }) + ) + + fireEvent.click(screen.getByRole('tab', { name: '笔记' })) + expect( + screen.getByTestId('magic-note-checklist-state') + ).toHaveTextContent('unchecked') }) it('reuses the AI comments pane for selected todos', async () => { diff --git a/src/renderer/src/MagicNotesWorkspace.tsx b/src/renderer/src/MagicNotesWorkspace.tsx index d9130336..b93f5244 100644 --- a/src/renderer/src/MagicNotesWorkspace.tsx +++ b/src/renderer/src/MagicNotesWorkspace.tsx @@ -693,9 +693,7 @@ export function MagicNotesWorkspace({ [] ) - const applyDetail = useCallback((next: MagicNoteDetail) => { - setDetail(next) - setTitleDraft(next.title) + const applyNoteSummary = useCallback((next: MagicNoteDetail) => { setNotes((current) => { const summary = noteSummary(next) const existing = current.some((note) => note.id === next.id) @@ -710,6 +708,15 @@ export function MagicNotesWorkspace({ }) }, []) + const applyDetail = useCallback( + (next: MagicNoteDetail) => { + setDetail(next) + setTitleDraft(next.title) + applyNoteSummary(next) + }, + [applyNoteSummary] + ) + const applyTodo = useCallback((next: MagicTodoItem) => { setTodos((current) => [ @@ -1184,13 +1191,17 @@ export function MagicNotesWorkspace({ } try { const completed = !todo.completed - applyTodo( - await window.goodbuddy.magicNotes.updateTodo({ - todoId: todo.id, - completed, - expectedRevision: todo.revision - }) - ) + const result = await window.goodbuddy.magicNotes.updateTodo({ + todoId: todo.id, + completed, + expectedRevision: todo.revision + }) + applyTodo(result.todo) + if (requestedNoteIdRef.current === result.note.id) { + applyDetail(result.note) + } else { + applyNoteSummary(result.note) + } notifySuccess( t( completed diff --git a/src/shared/contracts.ts b/src/shared/contracts.ts index 085a51bd..fff66f95 100644 --- a/src/shared/contracts.ts +++ b/src/shared/contracts.ts @@ -64,6 +64,7 @@ import type { MagicNoteUpdateInput, MagicTodoItem, MagicTodoUpdateInput, + MagicTodoUpdateResult, MagicTodosSnapshot } from './magic-notes-contracts' import type { @@ -1704,7 +1705,7 @@ export type DesktopApi = { listTodos: () => Promise updateTodo: ( input: MagicTodoUpdateInput - ) => Promise + ) => Promise analyzeTodo: ( todoId: string, options: MagicNoteAnalysisOptions diff --git a/src/shared/magic-notes-contracts.ts b/src/shared/magic-notes-contracts.ts index 6d5d971a..f8186969 100644 --- a/src/shared/magic-notes-contracts.ts +++ b/src/shared/magic-notes-contracts.ts @@ -433,6 +433,11 @@ export type MagicTodoItem = { updatedAt: string } +export type MagicTodoUpdateResult = { + todo: MagicTodoItem + note: MagicNoteDetail +} + export type MagicTodosSnapshot = { todos: MagicTodoItem[] }