feat: show incomplete Magic Notes todo counts
Magic Notes can now show a default-on incomplete todo badge in the primary navigation. The badge hides at zero, caps its visual label at 99+ while retaining the exact accessible count, and refreshes after user or Agent/MCP note changes. Platform Features > Magic Notes includes a persistent switch that hides only the count without changing notes or the Magic Notes entry. Existing settings migrate with the count enabled. Release note: 左侧“魔法笔记”入口现在可显示未完成待办数量;可在“平台功能 → 魔法笔记”中关闭,超过 99 项显示“99+”。
This commit is contained in:
@@ -58,13 +58,15 @@ describe('ApplicationSettingsStore', () => {
|
||||
await expect(
|
||||
store.update({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: false
|
||||
})
|
||||
).resolves.toEqual({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: false,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
@@ -73,15 +75,17 @@ describe('ApplicationSettingsStore', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: false,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
|
||||
version: 7,
|
||||
version: 8,
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: false,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined',
|
||||
lastSeenReleaseNotesVersion: null
|
||||
@@ -107,6 +111,7 @@ describe('ApplicationSettingsStore', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
@@ -130,6 +135,7 @@ describe('ApplicationSettingsStore', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
@@ -153,6 +159,7 @@ describe('ApplicationSettingsStore', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
@@ -176,6 +183,7 @@ describe('ApplicationSettingsStore', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'after-save-manual',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
@@ -201,11 +209,12 @@ describe('ApplicationSettingsStore', () => {
|
||||
new ApplicationSettingsStore(filePath).getLastSeenReleaseNotesVersion()
|
||||
).resolves.toBe('0.8.18')
|
||||
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
|
||||
version: 7,
|
||||
version: 8,
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'after-save-manual',
|
||||
magicNoteCommentFormat: 'narrative',
|
||||
lastSeenReleaseNotesVersion: '0.8.18'
|
||||
@@ -232,6 +241,7 @@ describe('ApplicationSettingsStore', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'after-save-auto',
|
||||
magicNoteCommentFormat: 'structured'
|
||||
})
|
||||
@@ -258,6 +268,7 @@ describe('ApplicationSettingsStore', () => {
|
||||
updateSource: 'mirror',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'after-save-auto',
|
||||
magicNoteCommentFormat: 'structured'
|
||||
})
|
||||
@@ -268,11 +279,40 @@ describe('ApplicationSettingsStore', () => {
|
||||
await store.update({ modelDownloadSource: 'hugging-face' })
|
||||
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
|
||||
...versionSix,
|
||||
version: 7,
|
||||
modelDownloadSource: 'hugging-face'
|
||||
version: 8,
|
||||
modelDownloadSource: 'hugging-face',
|
||||
magicNotesShowIncompleteTodoCount: true
|
||||
})
|
||||
})
|
||||
|
||||
it('migrates version 7 to show incomplete todo counts', async () => {
|
||||
const { filePath, store } = await createStore()
|
||||
const versionSeven = {
|
||||
version: 7,
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined',
|
||||
lastSeenReleaseNotesVersion: null
|
||||
}
|
||||
await writeFile(filePath, JSON.stringify(versionSeven), 'utf8')
|
||||
|
||||
await expect(store.get()).resolves.toEqual({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual(
|
||||
versionSeven
|
||||
)
|
||||
})
|
||||
|
||||
it('strictly rejects incomplete full settings', () => {
|
||||
for (const input of [
|
||||
{},
|
||||
@@ -319,6 +359,7 @@ describe('ApplicationSettingsStore', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
@@ -433,15 +474,17 @@ describe('ApplicationSettingsStore', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
|
||||
version: 7,
|
||||
version: 8,
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined',
|
||||
lastSeenReleaseNotesVersion: null
|
||||
@@ -467,6 +510,7 @@ describe('ApplicationSettingsStore', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ export {
|
||||
} from '../shared/application-settings-contracts'
|
||||
export type { ApplicationSettings } from '../shared/application-settings-contracts'
|
||||
|
||||
const CURRENT_SETTINGS_VERSION = 7
|
||||
const CURRENT_SETTINGS_VERSION = 8
|
||||
|
||||
const legacyStoredApplicationSettingsSchema = z
|
||||
.object({
|
||||
@@ -47,14 +47,22 @@ const versionThreeStoredApplicationSettingsSchema = z
|
||||
.strict()
|
||||
|
||||
const versionFourStoredApplicationSettingsSchema = applicationSettingsSchema
|
||||
.omit({ updateSource: true, modelDownloadSource: true })
|
||||
.omit({
|
||||
updateSource: true,
|
||||
modelDownloadSource: true,
|
||||
magicNotesShowIncompleteTodoCount: true
|
||||
})
|
||||
.extend({
|
||||
version: z.literal(4)
|
||||
})
|
||||
.strict()
|
||||
|
||||
const versionFiveStoredApplicationSettingsSchema = applicationSettingsSchema
|
||||
.omit({ updateSource: true, modelDownloadSource: true })
|
||||
.omit({
|
||||
updateSource: true,
|
||||
modelDownloadSource: true,
|
||||
magicNotesShowIncompleteTodoCount: true
|
||||
})
|
||||
.extend({
|
||||
version: z.literal(5),
|
||||
lastSeenReleaseNotesVersion: releaseVersionSchema.nullable()
|
||||
@@ -62,13 +70,25 @@ const versionFiveStoredApplicationSettingsSchema = applicationSettingsSchema
|
||||
.strict()
|
||||
|
||||
const versionSixStoredApplicationSettingsSchema = applicationSettingsSchema
|
||||
.omit({ modelDownloadSource: true })
|
||||
.omit({
|
||||
modelDownloadSource: true,
|
||||
magicNotesShowIncompleteTodoCount: true
|
||||
})
|
||||
.extend({
|
||||
version: z.literal(6),
|
||||
lastSeenReleaseNotesVersion: releaseVersionSchema.nullable()
|
||||
})
|
||||
.strict()
|
||||
|
||||
const versionSevenStoredApplicationSettingsSchema =
|
||||
applicationSettingsSchema
|
||||
.omit({ magicNotesShowIncompleteTodoCount: true })
|
||||
.extend({
|
||||
version: z.literal(7),
|
||||
lastSeenReleaseNotesVersion: releaseVersionSchema.nullable()
|
||||
})
|
||||
.strict()
|
||||
|
||||
const storedApplicationSettingsSchema = applicationSettingsSchema
|
||||
.extend({
|
||||
version: z.literal(CURRENT_SETTINGS_VERSION),
|
||||
@@ -85,6 +105,7 @@ export const defaultApplicationSettings: ApplicationSettings = {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
}
|
||||
@@ -140,13 +161,24 @@ export class ApplicationSettingsStore {
|
||||
)
|
||||
const result = storedApplicationSettingsSchema.safeParse(parsed)
|
||||
if (!result.success) {
|
||||
const versionSevenResult =
|
||||
versionSevenStoredApplicationSettingsSchema.safeParse(parsed)
|
||||
if (versionSevenResult.success) {
|
||||
this.settings = {
|
||||
...versionSevenResult.data,
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
magicNotesShowIncompleteTodoCount: true
|
||||
}
|
||||
return this.settings
|
||||
}
|
||||
const versionSixResult =
|
||||
versionSixStoredApplicationSettingsSchema.safeParse(parsed)
|
||||
if (versionSixResult.success) {
|
||||
this.settings = {
|
||||
...versionSixResult.data,
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
modelDownloadSource: 'modelscope'
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesShowIncompleteTodoCount: true
|
||||
}
|
||||
return this.settings
|
||||
}
|
||||
@@ -157,7 +189,8 @@ export class ApplicationSettingsStore {
|
||||
...versionFiveResult.data,
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope'
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesShowIncompleteTodoCount: true
|
||||
}
|
||||
return this.settings
|
||||
}
|
||||
@@ -169,6 +202,7 @@ export class ApplicationSettingsStore {
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
lastSeenReleaseNotesVersion: null
|
||||
}
|
||||
return this.settings
|
||||
@@ -181,6 +215,7 @@ export class ApplicationSettingsStore {
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentFormat: 'combined',
|
||||
lastSeenReleaseNotesVersion: null
|
||||
}
|
||||
@@ -194,6 +229,7 @@ export class ApplicationSettingsStore {
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined',
|
||||
lastSeenReleaseNotesVersion: null
|
||||
@@ -210,6 +246,7 @@ export class ApplicationSettingsStore {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined',
|
||||
lastSeenReleaseNotesVersion: null
|
||||
@@ -251,6 +288,8 @@ export class ApplicationSettingsStore {
|
||||
updateSource: stored.updateSource,
|
||||
modelDownloadSource: stored.modelDownloadSource,
|
||||
magicNotesEnabled: stored.magicNotesEnabled,
|
||||
magicNotesShowIncompleteTodoCount:
|
||||
stored.magicNotesShowIncompleteTodoCount,
|
||||
magicNoteCommentMode: stored.magicNoteCommentMode,
|
||||
magicNoteCommentFormat: stored.magicNoteCommentFormat,
|
||||
...(this.warnings.length > 0
|
||||
@@ -284,6 +323,8 @@ export class ApplicationSettingsStore {
|
||||
updateSource: next.updateSource,
|
||||
modelDownloadSource: next.modelDownloadSource,
|
||||
magicNotesEnabled: next.magicNotesEnabled,
|
||||
magicNotesShowIncompleteTodoCount:
|
||||
next.magicNotesShowIncompleteTodoCount,
|
||||
magicNoteCommentMode: next.magicNoteCommentMode,
|
||||
magicNoteCommentFormat: next.magicNoteCommentFormat
|
||||
}
|
||||
|
||||
@@ -24,10 +24,17 @@ afterEach(async () => {
|
||||
)
|
||||
})
|
||||
|
||||
async function createDatabase(): Promise<AssistantDatabase> {
|
||||
async function createDatabase(
|
||||
options: {
|
||||
onMagicTodosChanged?: () => void
|
||||
} = {}
|
||||
): Promise<AssistantDatabase> {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-assistant-'))
|
||||
temporaryDirectories.push(directory)
|
||||
const database = new AssistantDatabase(join(directory, 'assistant.sqlite'))
|
||||
const database = new AssistantDatabase(
|
||||
join(directory, 'assistant.sqlite'),
|
||||
options
|
||||
)
|
||||
database.initialize('C:\\Workspace')
|
||||
return database
|
||||
}
|
||||
@@ -4064,6 +4071,60 @@ describe('AssistantDatabase', () => {
|
||||
database.close()
|
||||
})
|
||||
|
||||
it('counts incomplete todos and publishes todo-changing writes', async () => {
|
||||
const onMagicTodosChanged = vi.fn()
|
||||
const database = await createDatabase({ onMagicTodosChanged })
|
||||
const note = database.createMagicNote({ title: '发布笔记' })
|
||||
|
||||
expect(database.getMagicTodoStatus()).toEqual({
|
||||
incompleteCount: 0
|
||||
})
|
||||
expect(onMagicTodosChanged).not.toHaveBeenCalled()
|
||||
|
||||
database.createMagicNoteEntry({
|
||||
noteId: note.id,
|
||||
content: {
|
||||
version: 1,
|
||||
ops: [
|
||||
{ insert: '核对发布材料' },
|
||||
{ insert: '\n', attributes: { list: 'unchecked' } },
|
||||
{ insert: '上传构建产物' },
|
||||
{ insert: '\n', attributes: { list: 'unchecked' } },
|
||||
{ insert: '通知负责人' },
|
||||
{ insert: '\n', attributes: { list: 'checked' } }
|
||||
]
|
||||
},
|
||||
plainText: '核对发布材料\n上传构建产物\n通知负责人'
|
||||
})
|
||||
|
||||
expect(database.getMagicTodoStatus()).toEqual({
|
||||
incompleteCount: 2
|
||||
})
|
||||
expect(onMagicTodosChanged).toHaveBeenCalledTimes(1)
|
||||
|
||||
const todo = database
|
||||
.listMagicTodos()
|
||||
.find((candidate) => !candidate.completed)!
|
||||
database.updateMagicTodo({
|
||||
todoId: todo.id,
|
||||
completed: true,
|
||||
expectedRevision: todo.revision
|
||||
})
|
||||
|
||||
expect(database.getMagicTodoStatus()).toEqual({
|
||||
incompleteCount: 1
|
||||
})
|
||||
expect(onMagicTodosChanged).toHaveBeenCalledTimes(2)
|
||||
|
||||
database.deleteMagicNote(note.id)
|
||||
expect(database.getMagicTodoStatus()).toEqual({
|
||||
incompleteCount: 0
|
||||
})
|
||||
expect(onMagicTodosChanged).toHaveBeenCalledTimes(3)
|
||||
|
||||
database.close()
|
||||
})
|
||||
|
||||
it('protects magic note records from stale revisions', async () => {
|
||||
const database = await createDatabase()
|
||||
const note = database.createMagicNote({ title: '并发笔记' })
|
||||
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
type MagicNoteSearchResult,
|
||||
type MagicNoteSummary,
|
||||
type MagicTodoItem,
|
||||
type MagicTodoStatus,
|
||||
type MagicTodoUpdateInput
|
||||
} from '../../shared/magic-notes-contracts'
|
||||
import type { ComputerControlAuditEvent } from '../computer-control/audit'
|
||||
@@ -967,7 +968,16 @@ export class AssistantDatabase {
|
||||
private channelEventWrites = 0
|
||||
private channelOutboxWrites = 0
|
||||
|
||||
constructor(private readonly databasePath: string) {}
|
||||
constructor(
|
||||
private readonly databasePath: string,
|
||||
private readonly options: {
|
||||
onMagicTodosChanged?: () => void
|
||||
} = {}
|
||||
) {}
|
||||
|
||||
private notifyMagicTodosChanged(): void {
|
||||
this.options.onMagicTodosChanged?.()
|
||||
}
|
||||
|
||||
initialize(defaultRootPath: string): void {
|
||||
if (this.database) {
|
||||
@@ -2605,7 +2615,11 @@ export class AssistantDatabase {
|
||||
database.exec('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
return this.getMagicNote(id)
|
||||
const detail = this.getMagicNote(id)
|
||||
if (input.content) {
|
||||
this.notifyMagicTodosChanged()
|
||||
}
|
||||
return detail
|
||||
}
|
||||
|
||||
updateMagicNote(input: {
|
||||
@@ -2644,6 +2658,7 @@ export class AssistantDatabase {
|
||||
if (result.changes !== 1) {
|
||||
throw new Error('笔记不存在')
|
||||
}
|
||||
this.notifyMagicTodosChanged()
|
||||
}
|
||||
|
||||
createMagicNoteEntry(input: {
|
||||
@@ -2696,7 +2711,9 @@ export class AssistantDatabase {
|
||||
database.exec('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
return this.getMagicNote(input.noteId)
|
||||
const detail = this.getMagicNote(input.noteId)
|
||||
this.notifyMagicTodosChanged()
|
||||
return detail
|
||||
}
|
||||
|
||||
updateMagicNoteEntry(input: {
|
||||
@@ -2758,7 +2775,9 @@ export class AssistantDatabase {
|
||||
database.exec('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
return this.getMagicNote(existing.note_id)
|
||||
const detail = this.getMagicNote(existing.note_id)
|
||||
this.notifyMagicTodosChanged()
|
||||
return detail
|
||||
}
|
||||
|
||||
deleteMagicNoteEntry(entryId: string): MagicNoteDetail {
|
||||
@@ -2787,7 +2806,9 @@ export class AssistantDatabase {
|
||||
database.exec('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
return this.getMagicNote(existing.note_id)
|
||||
const detail = this.getMagicNote(existing.note_id)
|
||||
this.notifyMagicTodosChanged()
|
||||
return detail
|
||||
}
|
||||
|
||||
getMagicNoteEntry(entryId: string): MagicNoteEntry {
|
||||
@@ -2861,6 +2882,17 @@ export class AssistantDatabase {
|
||||
).map(toMagicTodo)
|
||||
}
|
||||
|
||||
getMagicTodoStatus(): MagicTodoStatus {
|
||||
const row = this.requireDatabase()
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS incomplete_count
|
||||
FROM magic_todos
|
||||
WHERE source = 'note' AND completed = 0`
|
||||
)
|
||||
.get() as { incomplete_count: number }
|
||||
return { incompleteCount: row.incomplete_count }
|
||||
}
|
||||
|
||||
searchMagicNotes(query: string, limit: number): MagicNoteSearchResult[] {
|
||||
const pattern = `%${query.replace(/[\\%_]/gu, '\\$&')}%`
|
||||
return (
|
||||
@@ -2985,7 +3017,9 @@ export class AssistantDatabase {
|
||||
database.exec('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
return this.getMagicTodo(input.todoId)
|
||||
const todo = this.getMagicTodo(input.todoId)
|
||||
this.notifyMagicTodosChanged()
|
||||
return todo
|
||||
}
|
||||
|
||||
saveMagicTodoAnalysis(input: {
|
||||
|
||||
+16
-1
@@ -511,7 +511,22 @@ if (hasSingleInstanceLock) {
|
||||
| ReturnType<typeof createRerankProvider>
|
||||
| undefined
|
||||
const startupAssistantDatabase = new AssistantDatabase(
|
||||
join(app.getPath('userData'), 'assistant.sqlite')
|
||||
join(app.getPath('userData'), 'assistant.sqlite'),
|
||||
{
|
||||
onMagicTodosChanged: () => {
|
||||
queueMicrotask(() => {
|
||||
if (
|
||||
mainWindow &&
|
||||
!mainWindow.isDestroyed() &&
|
||||
!mainWindow.webContents.isDestroyed()
|
||||
) {
|
||||
mainWindow.webContents.send(
|
||||
ipcChannels.magicTodosStatusChanged
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
assistantDatabase = startupAssistantDatabase
|
||||
const goodbuddyConfigService = new GoodBuddyConfigService(
|
||||
|
||||
@@ -5682,6 +5682,14 @@ export function registerIpcHandlers(
|
||||
}
|
||||
)
|
||||
|
||||
registerHandler(
|
||||
ipcChannels.magicTodosStatus,
|
||||
(event) => {
|
||||
assertTrustedSender(event, window)
|
||||
return assistantDatabase.getMagicTodoStatus()
|
||||
}
|
||||
)
|
||||
|
||||
registerHandler(
|
||||
ipcChannels.magicTodosUpdate,
|
||||
(event, input: unknown) => {
|
||||
|
||||
@@ -107,6 +107,7 @@ import type {
|
||||
MagicNoteDetail,
|
||||
MagicNotesSnapshot,
|
||||
MagicTodoItem,
|
||||
MagicTodoStatus,
|
||||
MagicTodoUpdateResult,
|
||||
MagicTodosSnapshot
|
||||
} from '../shared/magic-notes-contracts'
|
||||
@@ -1110,6 +1111,10 @@ const desktopApi: DesktopApi = {
|
||||
ipcRenderer.invoke(
|
||||
ipcChannels.magicTodosList
|
||||
) as Promise<MagicTodosSnapshot>,
|
||||
getTodoStatus: () =>
|
||||
ipcRenderer.invoke(
|
||||
ipcChannels.magicTodosStatus
|
||||
) as Promise<MagicTodoStatus>,
|
||||
updateTodo: (input) =>
|
||||
ipcRenderer.invoke(
|
||||
ipcChannels.magicTodosUpdate,
|
||||
@@ -1131,6 +1136,15 @@ const desktopApi: DesktopApi = {
|
||||
ipcChannels.magicNotesAnalysisEvent,
|
||||
handler
|
||||
)
|
||||
},
|
||||
onTodoStatusChanged: (listener) => {
|
||||
const handler = (): void => listener()
|
||||
ipcRenderer.on(ipcChannels.magicTodosStatusChanged, handler)
|
||||
return () =>
|
||||
ipcRenderer.removeListener(
|
||||
ipcChannels.magicTodosStatusChanged,
|
||||
handler
|
||||
)
|
||||
}
|
||||
},
|
||||
knowledge: {
|
||||
|
||||
@@ -107,6 +107,7 @@ let fileSelectionProgressListener:
|
||||
>[0]
|
||||
| undefined
|
||||
let newConversationListener: (() => void) | undefined
|
||||
let magicTodoStatusChangedListener: (() => void) | undefined
|
||||
let maximizedChangedListener: ((maximized: boolean) => void) | undefined
|
||||
let beforeQuitListener: (() => Promise<void>) | undefined
|
||||
let conversationQueueDispatchListener:
|
||||
@@ -682,13 +683,20 @@ const api: DesktopApi = {
|
||||
throw new Error('not used')
|
||||
}),
|
||||
listTodos: vi.fn(async () => ({ todos: [] })),
|
||||
getTodoStatus: vi.fn(async () => ({ incompleteCount: 0 })),
|
||||
updateTodo: vi.fn(async () => {
|
||||
throw new Error('not used')
|
||||
}),
|
||||
analyzeTodo: vi.fn(async () => {
|
||||
throw new Error('not used')
|
||||
}),
|
||||
onAnalysisEvent: vi.fn(() => vi.fn())
|
||||
onAnalysisEvent: vi.fn(() => vi.fn()),
|
||||
onTodoStatusChanged: vi.fn((listener) => {
|
||||
magicTodoStatusChangedListener = listener
|
||||
return () => {
|
||||
magicTodoStatusChangedListener = undefined
|
||||
}
|
||||
})
|
||||
},
|
||||
knowledge: {
|
||||
getSnapshot: vi.fn(async () => ({
|
||||
@@ -870,6 +878,18 @@ describe('App', () => {
|
||||
delete document.documentElement.dataset.theme
|
||||
document.documentElement.style.colorScheme = ''
|
||||
vi.clearAllMocks()
|
||||
magicTodoStatusChangedListener = undefined
|
||||
vi.mocked(api.magicNotes.getTodoStatus)
|
||||
.mockReset()
|
||||
.mockResolvedValue({ incompleteCount: 0 })
|
||||
vi.mocked(api.magicNotes.onTodoStatusChanged)
|
||||
.mockReset()
|
||||
.mockImplementation((listener) => {
|
||||
magicTodoStatusChangedListener = listener
|
||||
return () => {
|
||||
magicTodoStatusChangedListener = undefined
|
||||
}
|
||||
})
|
||||
vi.mocked(api.conversations.list).mockReset().mockResolvedValue([])
|
||||
vi.mocked(api.conversations.replace)
|
||||
.mockReset()
|
||||
@@ -1849,6 +1869,7 @@ describe('App', () => {
|
||||
updateSource: 'github' as const,
|
||||
modelDownloadSource: 'modelscope' as const,
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
@@ -1942,6 +1963,7 @@ describe('App', () => {
|
||||
updateSource: 'github' as const,
|
||||
modelDownloadSource: 'modelscope' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
@@ -1950,6 +1972,7 @@ describe('App', () => {
|
||||
updateSource: 'github' as const,
|
||||
modelDownloadSource: 'modelscope' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
@@ -2044,6 +2067,7 @@ describe('App', () => {
|
||||
updateSource: 'github' as const,
|
||||
modelDownloadSource: 'modelscope' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
@@ -2052,6 +2076,7 @@ describe('App', () => {
|
||||
updateSource: 'github' as const,
|
||||
modelDownloadSource: 'modelscope' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
@@ -3887,6 +3912,7 @@ describe('App', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
}
|
||||
@@ -8617,6 +8643,7 @@ describe('App', () => {
|
||||
updateSource: 'github' as const,
|
||||
modelDownloadSource: 'modelscope' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
@@ -8625,6 +8652,7 @@ describe('App', () => {
|
||||
updateSource: 'github' as const,
|
||||
modelDownloadSource: 'modelscope' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
@@ -8659,6 +8687,91 @@ describe('App', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('shows and refreshes the incomplete Magic Notes todo count', async () => {
|
||||
vi.mocked(api.magicNotes.getTodoStatus).mockResolvedValueOnce({
|
||||
incompleteCount: 4
|
||||
})
|
||||
api.updates = {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github' as const,
|
||||
modelDownloadSource: 'modelscope' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
updateSettings: vi.fn(),
|
||||
check: vi.fn(),
|
||||
openReleasePage: vi.fn(async () => {}),
|
||||
onResult: vi.fn(() => () => {})
|
||||
}
|
||||
try {
|
||||
render(<App />)
|
||||
|
||||
expect(
|
||||
await screen.findByLabelText('4 个未完成待办')
|
||||
).toHaveTextContent('4')
|
||||
|
||||
vi.mocked(api.magicNotes.getTodoStatus).mockResolvedValueOnce({
|
||||
incompleteCount: 0
|
||||
})
|
||||
await act(async () => {
|
||||
magicTodoStatusChangedListener?.()
|
||||
})
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByLabelText('4 个未完成待办')
|
||||
).not.toBeInTheDocument()
|
||||
)
|
||||
|
||||
vi.mocked(api.magicNotes.getTodoStatus).mockResolvedValueOnce({
|
||||
incompleteCount: 125
|
||||
})
|
||||
await act(async () => {
|
||||
magicTodoStatusChangedListener?.()
|
||||
})
|
||||
expect(
|
||||
await screen.findByLabelText('125 个未完成待办')
|
||||
).toHaveTextContent('99+')
|
||||
} finally {
|
||||
delete api.updates
|
||||
}
|
||||
})
|
||||
|
||||
it('hides the todo count when its Magic Notes setting is off', async () => {
|
||||
vi.mocked(api.magicNotes.getTodoStatus).mockResolvedValue({
|
||||
incompleteCount: 4
|
||||
})
|
||||
api.updates = {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github' as const,
|
||||
modelDownloadSource: 'modelscope' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: false,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
updateSettings: vi.fn(),
|
||||
check: vi.fn(),
|
||||
openReleasePage: vi.fn(async () => {}),
|
||||
onResult: vi.fn(() => () => {})
|
||||
}
|
||||
try {
|
||||
render(<App />)
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: '魔法笔记' })
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByLabelText('4 个未完成待办')
|
||||
).not.toBeInTheDocument()
|
||||
expect(api.magicNotes.getTodoStatus).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
delete api.updates
|
||||
}
|
||||
})
|
||||
it('hides Magic Notes when the platform feature is disabled', async () => {
|
||||
api.updates = {
|
||||
getSettings: vi.fn(async () => ({
|
||||
@@ -8666,6 +8779,7 @@ describe('App', () => {
|
||||
updateSource: 'github' as const,
|
||||
modelDownloadSource: 'modelscope' as const,
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
@@ -8674,6 +8788,7 @@ describe('App', () => {
|
||||
updateSource: 'github' as const,
|
||||
modelDownloadSource: 'modelscope' as const,
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
@@ -8707,6 +8822,7 @@ describe('App', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
}
|
||||
|
||||
@@ -2313,6 +2313,12 @@ function App(): React.JSX.Element {
|
||||
const [settingsInitialChannel, setSettingsInitialChannel] =
|
||||
useState<ProjectChannel>()
|
||||
const [magicNotesEnabled, setMagicNotesEnabled] = useState(false)
|
||||
const [
|
||||
magicNotesShowIncompleteTodoCount,
|
||||
setMagicNotesShowIncompleteTodoCount
|
||||
] = useState(true)
|
||||
const [incompleteMagicTodoCount, setIncompleteMagicTodoCount] =
|
||||
useState(0)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const deferredSearchQuery = useDeferredValue(searchQuery)
|
||||
const [searchConversationSnapshot, setSearchConversationSnapshot] =
|
||||
@@ -2713,6 +2719,9 @@ function App(): React.JSX.Element {
|
||||
.getSettings()
|
||||
.then(async (settings) => {
|
||||
setMagicNotesEnabled(settings.magicNotesEnabled)
|
||||
setMagicNotesShowIncompleteTodoCount(
|
||||
settings.magicNotesShowIncompleteTodoCount
|
||||
)
|
||||
if (!settings.magicNotesEnabled) {
|
||||
setView((current) =>
|
||||
current === 'magic-notes' ? 'chat' : current
|
||||
@@ -2736,6 +2745,42 @@ function App(): React.JSX.Element {
|
||||
.catch(() => undefined)
|
||||
}, [i18n, setView])
|
||||
|
||||
useEffect(() => {
|
||||
const magicNotes = window.goodbuddy.magicNotes
|
||||
if (
|
||||
!magicNotesEnabled ||
|
||||
!magicNotesShowIncompleteTodoCount
|
||||
) {
|
||||
return
|
||||
}
|
||||
let active = true
|
||||
let requestId = 0
|
||||
const refresh = async (): Promise<void> => {
|
||||
const currentRequestId = ++requestId
|
||||
try {
|
||||
const status = await magicNotes.getTodoStatus()
|
||||
if (active && requestId === currentRequestId) {
|
||||
setIncompleteMagicTodoCount(status.incompleteCount)
|
||||
}
|
||||
} catch {
|
||||
if (active && requestId === currentRequestId) {
|
||||
setIncompleteMagicTodoCount(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
const removeListener = magicNotes.onTodoStatusChanged(() => {
|
||||
void refresh()
|
||||
})
|
||||
void refresh()
|
||||
return () => {
|
||||
active = false
|
||||
removeListener()
|
||||
}
|
||||
}, [
|
||||
magicNotesEnabled,
|
||||
magicNotesShowIncompleteTodoCount
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const releaseNotesApi = window.goodbuddy.releaseNotes
|
||||
if (!releaseNotesApi || startupReleaseNotesStartedRef.current) {
|
||||
@@ -7489,6 +7534,19 @@ function App(): React.JSX.Element {
|
||||
>
|
||||
<Sparkles aria-hidden="true" size={17} />
|
||||
<span>{t('navigation.magicNotes')}</span>
|
||||
{magicNotesShowIncompleteTodoCount &&
|
||||
incompleteMagicTodoCount > 0 && (
|
||||
<span
|
||||
aria-label={t('navigation.incompleteTodos', {
|
||||
count: incompleteMagicTodoCount
|
||||
})}
|
||||
className="nav-item__badge"
|
||||
>
|
||||
{incompleteMagicTodoCount > 99
|
||||
? '99+'
|
||||
: incompleteMagicTodoCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -9619,7 +9677,18 @@ function App(): React.JSX.Element {
|
||||
}}
|
||||
onMagicNotesEnabledChange={(enabled) => {
|
||||
setMagicNotesEnabled(enabled)
|
||||
if (!enabled) {
|
||||
setIncompleteMagicTodoCount(0)
|
||||
}
|
||||
}}
|
||||
onMagicNotesShowIncompleteTodoCountChange={
|
||||
(enabled) => {
|
||||
setMagicNotesShowIncompleteTodoCount(enabled)
|
||||
if (!enabled) {
|
||||
setIncompleteMagicTodoCount(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
onNotify={notify}
|
||||
onLeaveRequestReady={registerSettingsLeaveRequester}
|
||||
onSaved={(settings) => {
|
||||
|
||||
@@ -190,6 +190,7 @@ const getApplicationSettings = vi.fn<() => Promise<ApplicationSettings>>(async (
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
}))
|
||||
@@ -202,6 +203,7 @@ beforeEach(() => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
@@ -1065,6 +1067,7 @@ describe('MagicNotesWorkspace', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'after-save-manual',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
@@ -1097,6 +1100,7 @@ describe('MagicNotesWorkspace', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'after-save-manual',
|
||||
magicNoteCommentFormat: 'narrative'
|
||||
})
|
||||
@@ -1212,6 +1216,7 @@ describe('MagicNotesWorkspace', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'after-save-auto',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
|
||||
@@ -25,6 +25,9 @@ import {
|
||||
|
||||
type PlatformFeaturesSettingsSectionProps = {
|
||||
onMagicNotesEnabledChange: (enabled: boolean) => void
|
||||
onMagicNotesShowIncompleteTodoCountChange: (
|
||||
enabled: boolean
|
||||
) => void
|
||||
onNotify?: (notification: AppNotificationInput) => void
|
||||
onDirtyChange?: (dirty: boolean) => void
|
||||
onShortcutSettingsChanged?: (
|
||||
@@ -48,6 +51,7 @@ const shortcutErrorTranslationKeys: Record<
|
||||
|
||||
export function PlatformFeaturesSettingsSection({
|
||||
onMagicNotesEnabledChange,
|
||||
onMagicNotesShowIncompleteTodoCountChange,
|
||||
onNotify,
|
||||
onDirtyChange,
|
||||
onShortcutSettingsChanged
|
||||
@@ -312,6 +316,32 @@ export function PlatformFeaturesSettingsSection({
|
||||
}
|
||||
}
|
||||
|
||||
const changeIncompleteTodoCount = async (
|
||||
magicNotesShowIncompleteTodoCount: boolean
|
||||
): Promise<void> => {
|
||||
const updates = window.goodbuddy.updates
|
||||
if (!updates || !settings) {
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
setError(undefined)
|
||||
try {
|
||||
const nextSettings = await updates.updateSettings({
|
||||
magicNotesShowIncompleteTodoCount
|
||||
})
|
||||
setSettings(nextSettings)
|
||||
onMagicNotesShowIncompleteTodoCountChange(
|
||||
nextSettings.magicNotesShowIncompleteTodoCount
|
||||
)
|
||||
} catch {
|
||||
setError(
|
||||
t('platformFeatures.errors.saveIncompleteTodoCountFailed')
|
||||
)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const changeCommentFormat = async (
|
||||
magicNoteCommentFormat: MagicNoteCommentFormat
|
||||
): Promise<void> => {
|
||||
@@ -580,6 +610,31 @@ export function PlatformFeaturesSettingsSection({
|
||||
/>
|
||||
<span>{t('platformFeatures.magicNotes.showEntry')}</span>
|
||||
</label>
|
||||
<label className="toggle-row">
|
||||
<input
|
||||
checked={
|
||||
settings.magicNotesShowIncompleteTodoCount
|
||||
}
|
||||
disabled={saving}
|
||||
onChange={(event) =>
|
||||
void changeIncompleteTodoCount(
|
||||
event.target.checked
|
||||
)
|
||||
}
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
{t(
|
||||
'platformFeatures.magicNotes.showIncompleteTodoCount'
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
<p className="settings-notice">
|
||||
{t(
|
||||
'platformFeatures.magicNotes.showIncompleteTodoCountHelp'
|
||||
)}
|
||||
</p>
|
||||
<div className="platform-feature-option">
|
||||
<span>
|
||||
{t('platformFeatures.magicNotes.commentMode')}
|
||||
|
||||
@@ -404,6 +404,7 @@ let applicationSettings: ApplicationSettings = {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
}
|
||||
@@ -625,6 +626,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: false,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
}
|
||||
@@ -1116,12 +1118,16 @@ describe('SettingsPanel runtime files', () => {
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('toggles the Magic Notes platform entry setting', async () => {
|
||||
it('toggles Magic Notes navigation settings', async () => {
|
||||
const onMagicNotesEnabledChange = vi.fn()
|
||||
const onMagicNotesShowIncompleteTodoCountChange = vi.fn()
|
||||
render(
|
||||
<SettingsPanel
|
||||
{...heartbeatSettingsProps}
|
||||
onMagicNotesEnabledChange={onMagicNotesEnabledChange}
|
||||
onMagicNotesShowIncompleteTodoCountChange={
|
||||
onMagicNotesShowIncompleteTodoCountChange
|
||||
}
|
||||
open
|
||||
onClearLocalData={vi.fn(async () => {})}
|
||||
onClose={vi.fn()}
|
||||
@@ -1146,6 +1152,19 @@ describe('SettingsPanel runtime files', () => {
|
||||
})
|
||||
)
|
||||
expect(onMagicNotesEnabledChange).toHaveBeenCalledWith(true)
|
||||
const countToggle = screen.getByRole('switch', {
|
||||
name: '显示未完成待办数量'
|
||||
})
|
||||
expect(countToggle).toBeChecked()
|
||||
fireEvent.click(countToggle)
|
||||
await waitFor(() =>
|
||||
expect(updateApplicationSettings).toHaveBeenCalledWith({
|
||||
magicNotesShowIncompleteTodoCount: false
|
||||
})
|
||||
)
|
||||
expect(
|
||||
onMagicNotesShowIncompleteTodoCountChange
|
||||
).toHaveBeenCalledWith(false)
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '保存后自动' })
|
||||
)
|
||||
|
||||
@@ -185,6 +185,9 @@ type SettingsPanelProps = {
|
||||
onAppearanceThemeChange?: (theme: AppearanceTheme) => void
|
||||
magicNotesEnabled?: boolean
|
||||
onMagicNotesEnabledChange?: (enabled: boolean) => void
|
||||
onMagicNotesShowIncompleteTodoCountChange?: (
|
||||
enabled: boolean
|
||||
) => void
|
||||
onShortcutSettingsChanged?: (
|
||||
snapshot: GlobalShortcutSettingsSnapshot
|
||||
) => void
|
||||
@@ -542,6 +545,7 @@ export function SettingsPanel({
|
||||
onAppearanceThemeChange = () => {},
|
||||
magicNotesEnabled = false,
|
||||
onMagicNotesEnabledChange = () => {},
|
||||
onMagicNotesShowIncompleteTodoCountChange = () => {},
|
||||
onShortcutSettingsChanged = () => {},
|
||||
onLeaveRequestReady = () => {}
|
||||
}: SettingsPanelProps): React.JSX.Element | null {
|
||||
@@ -1857,6 +1861,9 @@ export function SettingsPanel({
|
||||
<PlatformFeaturesSettingsSection
|
||||
onDirtyChange={setPlatformFeaturesDirty}
|
||||
onMagicNotesEnabledChange={onMagicNotesEnabledChange}
|
||||
onMagicNotesShowIncompleteTodoCountChange={
|
||||
onMagicNotesShowIncompleteTodoCountChange
|
||||
}
|
||||
onNotify={onNotify}
|
||||
onShortcutSettingsChanged={onShortcutSettingsChanged}
|
||||
/>
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('UpdateSettingsSection', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export const app = {
|
||||
knowledge: 'Knowledge',
|
||||
heartbeat: 'Smart Heartbeat',
|
||||
activity: 'Run history',
|
||||
incompleteTodos: '{{count}} incomplete to-dos',
|
||||
pendingSuggestions: '{{count}} pending suggestions'
|
||||
},
|
||||
route: {
|
||||
|
||||
@@ -226,6 +226,8 @@ export const settingsSections = {
|
||||
'Application settings are not available in this version',
|
||||
readFailed: 'Could not load platform feature settings',
|
||||
saveMagicNotesFailed: 'Could not save Magic Notes settings. Try again.',
|
||||
saveIncompleteTodoCountFailed:
|
||||
'Could not save the incomplete to-do count setting. Try again.',
|
||||
saveCommentModeFailed:
|
||||
'Could not save the AI comment mode. Try again.',
|
||||
saveCommentFormatFailed:
|
||||
@@ -297,6 +299,9 @@ export const settingsSections = {
|
||||
description:
|
||||
'Off by default. Enable it to capture notes and to-dos and analyze content with AI.',
|
||||
showEntry: 'Show Magic Notes',
|
||||
showIncompleteTodoCount: 'Show incomplete to-do count',
|
||||
showIncompleteTodoCountHelp:
|
||||
'Shows the number of incomplete to-dos beside Magic Notes in the primary navigation. Counts above 99 appear as 99+.',
|
||||
commentMode: 'AI comment mode',
|
||||
commentModeAria: 'Magic Notes AI comment mode',
|
||||
modes: {
|
||||
|
||||
@@ -45,6 +45,7 @@ export const app = {
|
||||
knowledge: '知识库',
|
||||
heartbeat: '智能心跳',
|
||||
activity: '运行记录',
|
||||
incompleteTodos: '{{count}} 个未完成待办',
|
||||
pendingSuggestions: '{{count}} 条待处理建议'
|
||||
},
|
||||
route: {
|
||||
|
||||
@@ -211,6 +211,8 @@ export const settingsSections = {
|
||||
serviceUnavailable: '当前版本未提供应用设置服务',
|
||||
readFailed: '读取平台功能设置失败',
|
||||
saveMagicNotesFailed: '保存魔法笔记设置失败,请重试',
|
||||
saveIncompleteTodoCountFailed:
|
||||
'保存未完成待办数量设置失败,请重试',
|
||||
saveCommentModeFailed: '保存 AI 评论方式失败,请重试',
|
||||
saveCommentFormatFailed: '保存 AI 评论形式失败,请重试',
|
||||
saveModelDownloadSourceFailed: '保存模型下载源失败,请重试'
|
||||
@@ -267,6 +269,9 @@ export const settingsSections = {
|
||||
title: '魔法笔记',
|
||||
description: '默认关闭;开启后可记录笔记与待办,并使用 AI 分析内容',
|
||||
showEntry: '显示魔法笔记入口',
|
||||
showIncompleteTodoCount: '显示未完成待办数量',
|
||||
showIncompleteTodoCountHelp:
|
||||
'开启后,左侧魔法笔记入口显示未完成待办数量;超过 99 项显示 99+。',
|
||||
commentMode: 'AI 评论方式',
|
||||
commentModeAria: '魔法笔记 AI 评论方式',
|
||||
modes: {
|
||||
|
||||
@@ -30,6 +30,7 @@ const applicationPreferencesSchema = z
|
||||
updateSource: updateSourceSchema,
|
||||
modelDownloadSource: modelDownloadSourceSchema,
|
||||
magicNotesEnabled: z.boolean(),
|
||||
magicNotesShowIncompleteTodoCount: z.boolean(),
|
||||
magicNoteCommentMode: magicNoteCommentModeSchema,
|
||||
magicNoteCommentFormat: magicNoteCommentFormatSchema
|
||||
})
|
||||
|
||||
@@ -63,6 +63,7 @@ import type {
|
||||
MagicNotesSnapshot,
|
||||
MagicNoteUpdateInput,
|
||||
MagicTodoItem,
|
||||
MagicTodoStatus,
|
||||
MagicTodoUpdateInput,
|
||||
MagicTodoUpdateResult,
|
||||
MagicTodosSnapshot
|
||||
@@ -1703,6 +1704,7 @@ export type DesktopApi = {
|
||||
options: MagicNoteAnalysisOptions
|
||||
) => Promise<MagicNoteDraftAnalysis>
|
||||
listTodos: () => Promise<MagicTodosSnapshot>
|
||||
getTodoStatus: () => Promise<MagicTodoStatus>
|
||||
updateTodo: (
|
||||
input: MagicTodoUpdateInput
|
||||
) => Promise<MagicTodoUpdateResult>
|
||||
@@ -1713,6 +1715,7 @@ export type DesktopApi = {
|
||||
onAnalysisEvent: (
|
||||
listener: (event: MagicNoteAnalysisStreamEvent) => void
|
||||
) => () => void
|
||||
onTodoStatusChanged: (listener: () => void) => () => void
|
||||
}
|
||||
knowledge: {
|
||||
getSnapshot: (libraryId?: string) => Promise<KnowledgeSnapshot>
|
||||
|
||||
@@ -136,6 +136,7 @@ describe('GoodBuddy configuration contracts', () => {
|
||||
updateSource: 'github',
|
||||
modelDownloadSource: 'modelscope',
|
||||
magicNotesEnabled: true,
|
||||
magicNotesShowIncompleteTodoCount: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
},
|
||||
|
||||
@@ -178,6 +178,8 @@ export const ipcChannels = {
|
||||
magicNotesAnalyzeDraft: 'magic-notes:analyze-draft',
|
||||
magicNotesAnalysisEvent: 'magic-notes:analysis-event',
|
||||
magicTodosList: 'magic-todos:list',
|
||||
magicTodosStatus: 'magic-todos:status',
|
||||
magicTodosStatusChanged: 'magic-todos:status-changed',
|
||||
magicTodosUpdate: 'magic-todos:update',
|
||||
magicTodosAnalyze: 'magic-todos:analyze',
|
||||
knowledgeSnapshot: 'knowledge:snapshot',
|
||||
|
||||
@@ -438,6 +438,10 @@ export type MagicTodoUpdateResult = {
|
||||
note: MagicNoteDetail
|
||||
}
|
||||
|
||||
export type MagicTodoStatus = {
|
||||
incompleteCount: number
|
||||
}
|
||||
|
||||
export type MagicTodosSnapshot = {
|
||||
todos: MagicTodoItem[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user