fix: preserve drafts, canvas edits, and remote Continue tools

Keep paragraph formatting and checklist todos when removing canvas pages,
and preserve accepted undo/redo history after oversized input is rejected.
Merge restored queued text into the latest conversation draft and retain
image delivery when OCR returns image references and generated warnings only.

Deliver session MCP servers to bridged remote Continue Execute while keeping
Ask read-only and preserving local model-profile isolation. Add regression
coverage and update feature documentation. Correct stale conversation-limit
and forced-color assertions and the measured Electron layout test deadline.

Validation: 4,826 tests passed, 67 skipped; full typecheck and lint passed.
Current-source Linux x64 Execute/Ask/Execute MCP delivery passed, using 10
real text-provider requests and no image-provider requests. Image generation
itself was not exercised. Staged whitespace checks passed before commit.

Release note: 修复恢复队列时覆盖新输入、画布删页丢失清单格式及超限输入清空撤销历史的问题;图片 OCR 无正文时保留原图发送方式,远程 Continue Execute 正确保留会话 MCP 工具。
This commit is contained in:
mesalogo
2026-09-19 23:09:48 +08:00
parent 375b2159c8
commit a3b7a27045
29 changed files with 664 additions and 60 deletions
+29
View File
@@ -7,6 +7,8 @@ import { join, resolve } from 'node:path'
import { createServer } from 'vite'
import react from '@vitejs/plugin-react'
import { expect, it } from 'vitest'
import { AssistantDatabase } from '../src/main/assistant/assistant-database'
import type { MagicNoteCanvasContent } from '../src/shared/magic-notes-contracts'
it('preserves annotations when flow removes the active page and renders scoped thumbnails', async () => {
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-canvas-core-'))
@@ -54,6 +56,33 @@ it('preserves annotations when flow removes the active page and renders scoped t
expect(code, output).toBe(0)
} finally { clearTimeout(timeout) }
const result = JSON.parse(await readFile(join(directory, 'result.json'), 'utf8'))
expect(result.checklistAfter).toEqual(result.checklistBefore)
expect(result.overflowErrors).toHaveLength(2)
expect(result.selectionAfterRejection).toEqual({ index: 20000, length: 0 })
expect(result.selectionWithRedo).toEqual({ index: 3, length: 4 })
expect(result.acceptedLength).toBe(20000)
expect(result.undoneLength).toBe(19995)
expect(result.retainedRedo).toBe(true)
expect(result.redoneLength).toBe(20000)
const database = new AssistantDatabase(join(directory, 'assistant.sqlite'))
try {
database.initialize(directory)
const content: MagicNoteCanvasContent = {
version: 2, kind: 'paged-canvas', assets: [], flow: result.checklistBefore,
pages: [{ id: 'page-1', width: 794, height: 1123, objects: [],
background: { type: 'template', template: 'blank' } }]
}
const entry = database.createMagicNote({ title: 'Checklist preservation', content }).entries[0]!
const todos = database.listMagicTodos()
expect(todos).toHaveLength(1)
database.updateMagicNoteEntry({ entryId: entry.id, expectedRevision: entry.revision,
content: { ...content, flow: result.checklistAfter }, plainText: '' })
expect(database.listMagicTodos()).toEqual(todos)
database.close()
database.initialize(directory)
expect(database.listMagicTodos()).toEqual(todos)
expect(database.getMagicNoteEntry(entry.id).content).toEqual({ ...content, flow: result.checklistAfter })
} finally { database.close() }
console.info('Canvas Electron regression:', JSON.stringify(result))
expect(result.errors).toEqual([])
expect(result.initialPages).toBe(2)
+4 -3
View File
@@ -130,7 +130,8 @@ it('resizes record columns with native Electron input and preserves desktop widt
let output = ''
child.stdout.on('data', data => { output += data })
child.stderr.on('data', data => { output += data })
const timeout = setTimeout(() => child.kill(), 60000)
// Hidden Electron windows can run rAF at 1 Hz; the measured scenario takes 63s.
const timeout = setTimeout(() => child.kill(), 90000)
try {
const code = await new Promise((resolve, reject) => { child.once('exit', resolve); child.once('error', reject) })
expect(code, output).toBe(0)
@@ -140,6 +141,6 @@ it('resizes record columns with native Electron input and preserves desktop widt
expect(result.persisted).toBe(true)
} finally {
await server.close()
await rm(directory, { recursive: true, force: true })
await rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
}
}, 90000)
}, 120000)
+39 -1
View File
@@ -1,4 +1,5 @@
import Quill from 'quill'
import { mountFlowText } from '../../src/renderer/src/magic-canvas/canvas-flow.mjs'
import { createRoot } from 'react-dom/client'
import { mountCanvasNote } from '../../src/renderer/src/magic-canvas/canvas-note.mjs'
import { MagicCanvasThumbnail } from '../../src/renderer/src/MagicCanvasThumbnail'
@@ -88,10 +89,47 @@ async function canvasRegression() {
await editor.destroy()
}
host.remove()
const layer = document.createElement('div')
const toolbar = document.createElement('div')
document.body.append(layer, toolbar)
let flow = mountFlowText(layer, toolbar, { version: 1, ops: [
{ insert: 'Task' }, { insert: '\n', attributes: { list: 'unchecked' } }, { insert: '\n' }
] })
let flowQuill = Quill.find(layer.firstElementChild!) as Quill
const checklistBefore = flow.content()
flowQuill.setSelection(5, 0, 'api')
flow.insertPageBreak('page-2')
flow.removePageBreak('page-2')
const checklistAfter = await flow.flush()
flow.destroy()
const overflowErrors: string[] = []
flow = mountFlowText(layer, toolbar, { version: 1, ops: [{ insert: 'x'.repeat(19995) + '\n' }] },
{ onError: (error: string) => overflowErrors.push(error) })
flowQuill = Quill.find(layer.firstElementChild!) as Quill
flowQuill.setSelection(19995, 0, 'api')
flowQuill.insertText(19995, 'HELLO', 'user')
flowQuill.setSelection(20000, 0, 'api')
flowQuill.history.cutoff()
flowQuill.insertText(20000, '!', 'user')
const selectionAfterRejection = flowQuill.getSelection()
const acceptedLength = flow.text().length
flow.undo()
const undoneLength = flow.text().length
flowQuill.setSelection(3, 4, 'api')
flowQuill.insertText(3, 'REJECTED', 'user')
const selectionWithRedo = flowQuill.getSelection()
const retainedRedo = flow.canRedo()
flow.redo()
const redoneLength = flow.text().length
flow.destroy()
layer.remove()
toolbar.remove()
return { errors, initialPages: initial.pages.length, initialObjects: initial.pages[0].objects,
beforeShrink, afterShrink, savedPages: saved.pages.length, savedObjects: saved.pages[0].objects,
reopenedObjects: reopened.pages[0].objects, visibleInkBefore, visibleInkAfter, reopenedInk, flowTransform,
editorStyles, thumbnailStyles, thumbnailMatchesEditor, imageImports }
editorStyles, thumbnailStyles, thumbnailMatchesEditor, imageImports,
checklistBefore, checklistAfter, overflowErrors, selectionAfterRejection, selectionWithRedo,
acceptedLength, undoneLength, retainedRedo, redoneLength }
}
Object.assign(window, { canvasRegression })