feat: add desktop notification toggle to general settings
Add a default-on persisted desktop notification switch that takes effect immediately for background task results while leaving in-app notifications unchanged. Group General settings into shortcut, conversation and notification, and workspace and download cards. Validation: 756 related tests passed, 1 skipped; typecheck and lint passed. Full suite timed out with three supervision preview failures; native OS notification delivery was not manually verified. Release note: 通用设置新增默认开启的桌面通知开关,可随时关闭系统任务提醒并保留应用内提示;设置项按用途分组,方便查找。
This commit is contained in:
+59
-1
@@ -34,6 +34,8 @@ import type {
|
||||
import { defaultKnowledgeOntologySettings } from '../shared/knowledge-ontology'
|
||||
import { AssistantDatabase } from './assistant/assistant-database'
|
||||
import { ImageGenerationService } from './agent/image-generation-service'
|
||||
import { ApplicationSettingsStore } from './application-settings-store'
|
||||
import * as desktopNotification from './desktop-notification'
|
||||
import type { AgentExecutionRequest } from './agent/runtime'
|
||||
import { KnowledgeService } from './knowledge/knowledge-service'
|
||||
import { KnowledgeDatabase } from './knowledge/knowledge-database'
|
||||
@@ -5201,7 +5203,8 @@ describe('registerIpcHandlers agent terminal state', () => {
|
||||
heartbeatEnabled?: boolean,
|
||||
obsidianService?: ObsidianService,
|
||||
nativeClientCoordinator?: Parameters<typeof registerIpcHandlers>[45],
|
||||
nativeTerminalManager?: Parameters<typeof registerIpcHandlers>[41]
|
||||
nativeTerminalManager?: Parameters<typeof registerIpcHandlers>[41],
|
||||
applicationSettingsStore?: ApplicationSettingsStore
|
||||
) {
|
||||
const assistantDatabase = {
|
||||
createTask: vi.fn(),
|
||||
@@ -5438,6 +5441,7 @@ describe('registerIpcHandlers agent terminal state', () => {
|
||||
args[44] = obsidianService
|
||||
args[45] = nativeClientCoordinator
|
||||
args[41] = nativeTerminalManager
|
||||
if (applicationSettingsStore) args[15] = applicationSettingsStore
|
||||
const dispose = registerIpcHandlers(...args)
|
||||
return {
|
||||
approvalBroker,
|
||||
@@ -5483,6 +5487,60 @@ describe('registerIpcHandlers agent terminal state', () => {
|
||||
senderFrame: webContents.mainFrame
|
||||
})
|
||||
|
||||
it.each([
|
||||
['conversation', 'completed'], ['conversation', 'failed'],
|
||||
['channel', 'completed'], ['channel', 'failed']
|
||||
] as const)('uses the latest saved desktop notification preference for %s %s results', async (origin, status) => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-notification-ipc-'))
|
||||
const store = new ApplicationSettingsStore(join(directory, 'application.json'))
|
||||
await store.update({ magicNotesEnabled: false })
|
||||
const notify = vi.spyOn(desktopNotification, 'showDesktopNotificationWhenUnfocused').mockReturnValue(true)
|
||||
let preference: 'default' | boolean | 'unreadable' = 'default'
|
||||
const get = vi.spyOn(store, 'get')
|
||||
const harness = createHarness({
|
||||
capability: 'chat',
|
||||
async *run(request: { requestId: string }) {
|
||||
// Change the persisted choice after admission to detect stale startup/task snapshots.
|
||||
if (typeof preference === 'boolean') await store.update({ desktopNotificationsEnabled: preference })
|
||||
if (preference === 'unreadable') get.mockRejectedValueOnce(new Error('Settings unavailable'))
|
||||
if (status === 'failed') throw new Error('Runtime failed')
|
||||
yield { requestId: request.requestId, type: 'done' }
|
||||
}
|
||||
}, undefined, 'always', undefined, false, undefined, undefined, undefined, false,
|
||||
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, store)
|
||||
try {
|
||||
for (preference of ['default', false, true, 'unreadable'] as const) {
|
||||
notify.mockClear()
|
||||
harness.webContents.send.mockClear()
|
||||
harness.assistantDatabase.updateTaskStatus.mockClear()
|
||||
if (origin === 'channel') {
|
||||
await expect(channelMocks.executor!({
|
||||
channel: 'wecom', eventId: crypto.randomUUID(), senderId: 'user-1',
|
||||
conversationId: 'conversation-1', conversationType: 'direct',
|
||||
text: 'Read only', mentioned: false, workMode: 'ask'
|
||||
}, new AbortController().signal)).resolves.toMatchObject({ status })
|
||||
} else {
|
||||
const requestId = crypto.randomUUID()
|
||||
await harness.handler!(trustedEvent(harness.webContents), {
|
||||
requestId, conversationId: crypto.randomUUID(), prompt: 'Read only', workMode: 'ask'
|
||||
})
|
||||
await vi.waitFor(() => expect(harness.webContents.send).toHaveBeenCalledWith(
|
||||
ipcChannels.agentEvent, expect.objectContaining({ requestId, type: status === 'completed' ? 'done' : 'error' })
|
||||
))
|
||||
}
|
||||
expect(harness.assistantDatabase.updateTaskStatus).toHaveBeenCalledWith(
|
||||
expect.any(String), status, ...(status === 'failed' ? ['Runtime failed'] : [])
|
||||
)
|
||||
expect(notify).toHaveBeenCalledTimes(preference === 'default' || preference === true ? 1 : 0)
|
||||
}
|
||||
} finally {
|
||||
await harness.dispose()
|
||||
notify.mockRestore()
|
||||
get.mockRestore()
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function createManagedSshHarness(
|
||||
selectedRuntime: Record<string, unknown>
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user