From fee46867505de30bff11f24c9fa98a98b8db1b39 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Fri, 21 Aug 2026 23:27:41 +0800 Subject: [PATCH] fix: sync todo completion with source notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completing a Magic Notes todo updated the stored checklist but left the cached source note stale until the workspace was remounted. Todo updates now return the refreshed source note and update the active note cache immediately for both completion and reopening. Release note: 修复在待办视图勾选或取消待办后,源笔记必须切换页面才显示最新状态的问题;现在会即时同步。 --- UI-DESIGN.md | 1 + src/main/ipc.ts | 6 +- src/preload/index.ts | 3 +- src/renderer/src/MagicNotesWorkspace.test.tsx | 85 +++++++++++++++++-- src/renderer/src/MagicNotesWorkspace.tsx | 31 ++++--- src/shared/contracts.ts | 3 +- src/shared/magic-notes-contracts.ts | 5 ++ 7 files changed, 114 insertions(+), 20 deletions(-) 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[] }