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:
+1
-1
@@ -51,7 +51,7 @@
|
||||
- [x] **分块、维护与评估**:支持固定、结构化和父子分块,分块维护、可取消重建及双语检索评估。
|
||||
- [x] **受控知识本体**:每个知识库可定义实体、关系、别名和端点约束,保留证据偏移、置信度和抽取来源,并显式提示图谱重建。
|
||||
- [x] **强制检索与引用上下文**:对话可按需或每次先检索,显示零结果、降级、失败与取消状态,并可查看引用上下文或安全打开来源。
|
||||
- [x] **魔法笔记 / Magic Notes**:提供本地优先的笔记与待办工作台、范围管理、编辑、筛选和受控 AI 评论;创建、保存和评论结果使用统一应用通知。
|
||||
- [x] **魔法笔记 / Magic Notes**:提供本地优先的笔记与待办工作台、范围管理、编辑、筛选和受控 AI 评论;左侧入口可按设置显示未完成待办数量,创建、保存和评论结果使用统一应用通知。
|
||||
- [ ] **MCP Server Control Plane**(规划中):统一 MCP 生命周期、健康检查、重连、Schema 缓存、隔离、审批和审计。
|
||||
- [ ] **可追溯笔记摘录与 AI 编辑**(规划中):从对话、知识和网页收集带来源的摘录,并提供需确认的总结、改写和整理操作。
|
||||
|
||||
|
||||
+2
-1
@@ -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 可用性;清单状态必须区分完整、部分、不可用、仅连接和不支持,不能用进程连通性冒充清单可读。
|
||||
|
||||
@@ -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