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:
mesalogo
2026-08-22 08:32:57 +08:00
parent fee4686750
commit fb51793595
25 changed files with 538 additions and 25 deletions
+1 -1
View File
@@ -51,7 +51,7 @@
- [x] **分块、维护与评估**:支持固定、结构化和父子分块,分块维护、可取消重建及双语检索评估。
- [x] **受控知识本体**:每个知识库可定义实体、关系、别名和端点约束,保留证据偏移、置信度和抽取来源,并显式提示图谱重建。
- [x] **强制检索与引用上下文**:对话可按需或每次先检索,显示零结果、降级、失败与取消状态,并可查看引用上下文或安全打开来源。
- [x] **魔法笔记 / Magic Notes**:提供本地优先的笔记与待办工作台、范围管理、编辑、筛选和受控 AI 评论;创建、保存和评论结果使用统一应用通知。
- [x] **魔法笔记 / Magic Notes**:提供本地优先的笔记与待办工作台、范围管理、编辑、筛选和受控 AI 评论;左侧入口可按设置显示未完成待办数量,创建、保存和评论结果使用统一应用通知。
- [ ] **MCP Server Control Plane**(规划中):统一 MCP 生命周期、健康检查、重连、Schema 缓存、隔离、审批和审计。
- [ ] **可追溯笔记摘录与 AI 编辑**(规划中):从对话、知识和网页收集带来源的摘录,并提供需确认的总结、改写和整理操作。
+2 -1
View File
@@ -613,6 +613,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- “笔记 / 待办”属于同一工作台内的同级内容面板,使用 `PageTabs` 的 `segmented` 视觉变体,与模型设置的分段控件保持同一外观。
- 页签切换保留 `tablist`、`tab` 和 `tabpanel` 语义;待办状态仍使用独立的 `SegmentedControl`,不得与内容页签合并。
- 左侧“魔法笔记”入口可显示未完成待办数量。数量为 0 时隐藏徽标,超过 99 时显示 `99+`,可访问名称仍说明精确数量;勾选、取消勾选以及 Agent / MCP 对笔记清单的写入都必须刷新数量。
- 在待办面板勾选或取消勾选后,当前缓存中的源笔记正文必须立即同步对应清单状态,不得依赖离开工作区后重新加载。
- 当前记录草稿非空时,切换笔记、待办或内容面板必须先在编辑器旁就地确认;继续编辑时保留草稿并恢复编辑焦点,只有明确选择放弃后才切换。
- 创建、保存、更新、删除和 AI 评论完成等短期结果进入应用级通知,不在编辑区或列表上方堆放页内通知。
@@ -633,7 +634,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- “模型连接”的认证字段按“认证方式 → API Key → 凭据状态”排列;只有选择 API Key 时显示输入框。已保存的 API Key 属于该连接,修改模型服务地址或临时选择“无需认证”不得自动清除,也不得要求重新输入;仅由明确的“清除凭据”或“删除连接”操作移除。
- “保存并测试模型”必须在保存后发送一次有界的真实模型请求。文本连接校验本次随机测试文本,图像连接校验实际返回的内联图片;仅收到成功 HTTP 状态、模型列表或健康检查响应不算测试成功。界面持续说明该操作可能产生少量服务商用量费用,成功反馈明确写为“真实生成测试通过”。
- 所有显式保存的设置草稿都参与离开保护:关闭、切换分类、主侧栏或工作区导航及托盘导航不得静默丢弃,统一通过设置中心的就地确认提供继续编辑与明确放弃入口,保存失败后保留输入。“平台功能 / 通用设置”承载全局快捷唤起的共享 Switch、可访问 accelerator 录制输入、恢复默认、保存及注册、停用或冲突状态,不新增分类或页签;注册或持久化失败时保留上一组可用快捷键和当前草稿,保存或停用成功后同步更新输入区的快捷键提示。
- “平台功能”使用共享 `PageTabs` 区分“通用设置”和“魔法笔记”,默认进入通用设置。全局模型下载源使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片;选中状态同时依靠 Radio、边框和背景表达,读取失败时不得用默认值伪装为已保存选择。
- “平台功能”使用共享 `PageTabs` 区分“通用设置”和“魔法笔记”,默认进入通用设置。“魔法笔记”页提供默认开启的“显示未完成待办数量”持久 Switch,关闭后只隐藏左侧数量徽标,不改变待办数据或魔法笔记入口。全局模型下载源使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片;选中状态同时依靠 Radio、边框和背景表达,读取失败时不得用默认值伪装为已保存选择。
- “关于与更新”的更新源位于“启动时检查新版本”开关下方,常规宽度下将标签、原生单选下拉框和用途说明放在同一行,并复用设置表单的统一控件样式;关闭启动检查后,下拉框置灰且不可操作。选项显示“GitHub(默认)”和中性的“镜像节点”。该选择同时控制手动检查、启动时检查和下载页,不显示底层服务商名称。
- Agent Runtime 分类页头的“保存设置”同时保存 Runtime 基础配置与 Runtime 原生定制,不在原生定制卡片内提供第二个保存入口。原生定制存在未保存更改时持续显示状态和撤销入口;切换设置分类或 Runtime 不丢弃草稿,关闭设置中心前必须先保存或撤销。
- Agent Runtime 页面在低层程序与配置覆盖之外提供“能力与默认配置”区域。能力清单使用共享 `PageTabs`,按 Agents、Tools、Commands、Skills、MCP、Rules、Prompts、Resources、LSP、Formatters 和上下文 11 类单行滚动展示,一次只呈现当前分类的 `tabpanel`;清单只显示 Runtime 自有能力,不混入 GoodBuddy 分配的 Skills、临时 MCP 或 Continue 预设。Tools 必须独立于 Commands、LSP 和 Formatters,显示工具类型、来源及 Ask/Execute 可用性;清单状态必须区分完整、部分、不可用、仅连接和不支持,不能用进程连通性冒充清单可读。
+50 -6
View File
@@ -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'
})
+47 -6
View File
@@ -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
}
+63 -2
View File
@@ -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: '并发笔记' })
+40 -6
View File
@@ -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
View File
@@ -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(
+8
View File
@@ -5682,6 +5682,14 @@ export function registerIpcHandlers(
}
)
registerHandler(
ipcChannels.magicTodosStatus,
(event) => {
assertTrustedSender(event, window)
return assistantDatabase.getMagicTodoStatus()
}
)
registerHandler(
ipcChannels.magicTodosUpdate,
(event, input: unknown) => {
+14
View File
@@ -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: {
+117 -1
View File
@@ -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'
}
+69
View File
@@ -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')}
+20 -1
View File
@@ -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: '保存后自动' })
)
+7
View File
@@ -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
})
+3
View File
@@ -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'
},
+2
View File
@@ -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',
+4
View File
@@ -438,6 +438,10 @@ export type MagicTodoUpdateResult = {
note: MagicNoteDetail
}
export type MagicTodoStatus = {
incompleteCount: number
}
export type MagicTodosSnapshot = {
todos: MagicTodoItem[]
}