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:
@@ -829,6 +829,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
|||||||
- “平台功能 / 通用设置”同时提供默认开启的“在会话中渲染 HTML”持久 Switch。说明必须
|
- “平台功能 / 通用设置”同时提供默认开启的“在会话中渲染 HTML”持久 Switch。说明必须
|
||||||
明确该能力只生成禁用脚本、联网、表单提交和窗口操作的静态预览;保存成功后当前会话立即
|
明确该能力只生成禁用脚本、联网、表单提交和窗口操作的静态预览;保存成功后当前会话立即
|
||||||
更新,保存失败时保留最后确认状态。
|
更新,保存失败时保留最后确认状态。
|
||||||
|
- 通用设置按快捷唤起、会话与通知、工作目录与下载分组,复用现有卡片。“桌面通知”为默认开启的持久 Switch,保存后立即控制窗口未聚焦时的任务完成、失败和取消系统通知,关闭不影响应用内通知。旧配置缺少该字段时默认开启,明确关闭后重启仍保持关闭;保存失败保留最后确认状态并显示应用内错误通知。
|
||||||
- “平台功能”使用共享 `PageTabs` 区分“通用设置”和“远程项目”,默认进入通用设置。“远程项目”页将持久 Switch 与用户数据目录中的 Linux x64、Linux arm64、macOS arm64 Agent 包清单分成独立卡片;每张卡片与下载进度按平台和架构区分,不能把两个 arm64 目标合并。清单延迟到首次打开该页签时由 Main 完整校验,并读取所选更新源的小型签名目录,显示本地版本、在线最新兼容版本以及“有更新”“已是最新”“未下载”“校验失败”。打开页签或刷新不得自动下载 `.gbagent`;只有用户点击带目标版本的下载/更新按钮后才传输包。签名目录不可用时继续显示并允许导出已验证本地包,提供就地重试错误;清单不混入 Host 已安装状态、缓存路径或敏感签名细节。下载和导入期间保留上下文进度,成功进入全局通知视口。全局模型下载源使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片;选中状态同时依靠 Radio、边框和背景表达,读取失败时不得用默认值伪装为已保存选择。
|
- “平台功能”使用共享 `PageTabs` 区分“通用设置”和“远程项目”,默认进入通用设置。“远程项目”页将持久 Switch 与用户数据目录中的 Linux x64、Linux arm64、macOS arm64 Agent 包清单分成独立卡片;每张卡片与下载进度按平台和架构区分,不能把两个 arm64 目标合并。清单延迟到首次打开该页签时由 Main 完整校验,并读取所选更新源的小型签名目录,显示本地版本、在线最新兼容版本以及“有更新”“已是最新”“未下载”“校验失败”。打开页签或刷新不得自动下载 `.gbagent`;只有用户点击带目标版本的下载/更新按钮后才传输包。签名目录不可用时继续显示并允许导出已验证本地包,提供就地重试错误;清单不混入 Host 已安装状态、缓存路径或敏感签名细节。下载和导入期间保留上下文进度,成功进入全局通知视口。全局模型下载源使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片;选中状态同时依靠 Radio、边框和背景表达,读取失败时不得用默认值伪装为已保存选择。
|
||||||
- “关于与更新”的更新源位于“启动时检查新版本”开关下方,常规宽度下将标签、同行帮助和原生单选下拉框放在同一行,用途说明收进 `InlineHelp`,并复用设置表单的统一控件样式;关闭启动检查后仍可选择更新源,手动检查继续使用当前选择。选项显示“GitHub(默认)”和中性的“镜像节点”。该选择同时控制手动检查、启动时检查、下载页和 Agent 包在线下载,不显示底层服务商名称。
|
- “关于与更新”的更新源位于“启动时检查新版本”开关下方,常规宽度下将标签、同行帮助和原生单选下拉框放在同一行,用途说明收进 `InlineHelp`,并复用设置表单的统一控件样式;关闭启动检查后仍可选择更新源,手动检查继续使用当前选择。选项显示“GitHub(默认)”和中性的“镜像节点”。该选择同时控制手动检查、启动时检查、下载页和 Agent 包在线下载,不显示底层服务商名称。
|
||||||
- “关于与更新”在版本信息之后提供应用内反馈卡片。反馈对话框使用原生字段、明确隐私说明和一张可选截图;另提供默认关闭的原生 Checkbox,让用户主动附加最近桌面诊断摘要。Checkbox 附近必须持续说明摘要只含时间、组件、阶段、稳定错误码、错误类型和固定短消息,不含对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。勾选时为诊断摘要预留描述字符预算,超长草稿必须保留并显示字段错误,不得静默截断。失败时保留草稿、勾选状态和请求编号并提供就地重试,成功时在对话框内保留可复制编号而不重复发送全局通知;对话框遵循统一 Modal 的背景隔离、初始焦点、Tab 循环、Escape、焦点恢复和窄窗口布局。
|
- “关于与更新”在版本信息之后提供应用内反馈卡片。反馈对话框使用原生字段、明确隐私说明和一张可选截图;另提供默认关闭的原生 Checkbox,让用户主动附加最近桌面诊断摘要。Checkbox 附近必须持续说明摘要只含时间、组件、阶段、稳定错误码、错误类型和固定短消息,不含对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。勾选时为诊断摘要预留描述字符预算,超长草稿必须保留并显示字段错误,不得静默截断。失败时保留草稿、勾选状态和请求编号并提供就地重试,成功时在对话框内保留可复制编号而不重复发送全局通知;对话框遵循统一 Modal 的背景隔离、初始焦点、Tab 循环、Escape、焦点恢复和窄窗口布局。
|
||||||
|
|||||||
@@ -225,6 +225,10 @@
|
|||||||
|
|
||||||
### 7.1 布局
|
### 7.1 布局
|
||||||
|
|
||||||
|
通用设置按快捷唤起、会话与通知、工作目录与下载分成三张卡片。模型下载源位于
|
||||||
|
“工作目录与下载”卡片内的工作目录输入之后;以下仅展示下载源控件,完整分组及通知
|
||||||
|
开关行为见 [UI 设计规范](../../UI-DESIGN.md#138-设置中心)。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
平台功能
|
平台功能
|
||||||
管理通用平台行为与可选工作区能力
|
管理通用平台行为与可选工作区能力
|
||||||
@@ -244,7 +248,7 @@
|
|||||||
└────────────────────────────────────────────────────────┘
|
└────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
页面只保留一个视觉上最突出的任务。模型下载源保存属于即时设置,不增加与来源选项竞争的
|
模型下载源保存属于即时设置,不增加与来源选项竞争的
|
||||||
大型“保存全部”按钮。
|
大型“保存全部”按钮。
|
||||||
|
|
||||||
### 7.2 控件
|
### 7.2 控件
|
||||||
|
|||||||
@@ -44,6 +44,34 @@ afterEach(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('ApplicationSettingsStore', () => {
|
describe('ApplicationSettingsStore', () => {
|
||||||
|
it.each([11, 12])('defaults missing desktop notifications to on in version %s', async (version) => {
|
||||||
|
const { filePath, store } = await createStore()
|
||||||
|
const legacy: Record<string, unknown> = { ...defaultApplicationSettings, version, lastSeenReleaseNotesVersion: null, checkUpdatesOnStartup: false }
|
||||||
|
delete legacy.desktopNotificationsEnabled
|
||||||
|
await writeFile(filePath, JSON.stringify(legacy))
|
||||||
|
expect(await store.get()).toEqual({ ...defaultApplicationSettings, checkUpdatesOnStartup: false })
|
||||||
|
expect(applicationSettingsSchema.parse({ ...defaultApplicationSettings, desktopNotificationsEnabled: undefined }).desktopNotificationsEnabled).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('persists and publishes desktop notification choices without resetting them on unrelated updates', async () => {
|
||||||
|
const { filePath, store } = await createStore()
|
||||||
|
expect((await store.get()).desktopNotificationsEnabled).toBe(true)
|
||||||
|
const changed = vi.fn()
|
||||||
|
store.onChanged(changed)
|
||||||
|
for (const desktopNotificationsEnabled of [false, true]) {
|
||||||
|
const saved = await store.update({ desktopNotificationsEnabled })
|
||||||
|
expect(saved.desktopNotificationsEnabled).toBe(desktopNotificationsEnabled)
|
||||||
|
expect(changed).toHaveBeenLastCalledWith(saved)
|
||||||
|
await store.update({ checkUpdatesOnStartup: false })
|
||||||
|
expect((await createApplicationSettingsStore(filePath).get()).desktopNotificationsEnabled).toBe(desktopNotificationsEnabled)
|
||||||
|
expect(JSON.parse(await readFile(filePath, 'utf8')).desktopNotificationsEnabled).toBe(desktopNotificationsEnabled)
|
||||||
|
}
|
||||||
|
expect(applicationSettingsUpdateSchema.parse({ checkUpdatesOnStartup: false })).toEqual({ checkUpdatesOnStartup: false })
|
||||||
|
for (const desktopNotificationsEnabled of ['false', 0, null]) {
|
||||||
|
await expect(store.update({ desktopNotificationsEnabled })).rejects.toThrow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('persists review pagination and batch controls without enabling supervision', async () => {
|
it('persists review pagination and batch controls without enabling supervision', async () => {
|
||||||
const { filePath, store } = await createStore()
|
const { filePath, store } = await createStore()
|
||||||
const supervisionReview = { pageSize: 1, batchCharacters: 16000, batchMessages: 50, executionSeconds: 3600 }
|
const supervisionReview = { pageSize: 1, batchCharacters: 16000, batchMessages: 50, executionSeconds: 3600 }
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export const resolveLegacyLocalToolEnvironmentPaths: LegacyLocalToolEnvironmentR
|
|||||||
|
|
||||||
export const defaultApplicationSettings: ApplicationSettings = {
|
export const defaultApplicationSettings: ApplicationSettings = {
|
||||||
checkUpdatesOnStartup: true,
|
checkUpdatesOnStartup: true,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: 'github',
|
updateSource: 'github',
|
||||||
modelDownloadSource: 'modelscope',
|
modelDownloadSource: 'modelscope',
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -289,6 +290,7 @@ export class ApplicationSettingsStore {
|
|||||||
settings: Omit<
|
settings: Omit<
|
||||||
StoredApplicationSettings,
|
StoredApplicationSettings,
|
||||||
| 'conversationHtmlRenderingEnabled'
|
| 'conversationHtmlRenderingEnabled'
|
||||||
|
| 'desktopNotificationsEnabled'
|
||||||
| 'localToolEnvironment'
|
| 'localToolEnvironment'
|
||||||
| 'version'
|
| 'version'
|
||||||
| 'applicationNavigation'
|
| 'applicationNavigation'
|
||||||
@@ -297,6 +299,7 @@ export class ApplicationSettingsStore {
|
|||||||
| 'magicNoteCanvasPageCount'
|
| 'magicNoteCanvasPageCount'
|
||||||
> & {
|
> & {
|
||||||
conversationHtmlRenderingEnabled?: boolean
|
conversationHtmlRenderingEnabled?: boolean
|
||||||
|
desktopNotificationsEnabled?: boolean
|
||||||
magicNoteCanvasPageCount?: number
|
magicNoteCanvasPageCount?: number
|
||||||
version: number
|
version: number
|
||||||
}
|
}
|
||||||
@@ -522,6 +525,7 @@ export class ApplicationSettingsStore {
|
|||||||
const stored = await this.loadStored()
|
const stored = await this.loadStored()
|
||||||
return {
|
return {
|
||||||
checkUpdatesOnStartup: stored.checkUpdatesOnStartup,
|
checkUpdatesOnStartup: stored.checkUpdatesOnStartup,
|
||||||
|
desktopNotificationsEnabled: stored.desktopNotificationsEnabled,
|
||||||
updateSource: stored.updateSource,
|
updateSource: stored.updateSource,
|
||||||
modelDownloadSource: stored.modelDownloadSource,
|
modelDownloadSource: stored.modelDownloadSource,
|
||||||
localToolEnvironment: stored.localToolEnvironment,
|
localToolEnvironment: stored.localToolEnvironment,
|
||||||
@@ -567,6 +571,7 @@ export class ApplicationSettingsStore {
|
|||||||
this.warnings = []
|
this.warnings = []
|
||||||
const settings: ApplicationSettings = {
|
const settings: ApplicationSettings = {
|
||||||
checkUpdatesOnStartup: next.checkUpdatesOnStartup,
|
checkUpdatesOnStartup: next.checkUpdatesOnStartup,
|
||||||
|
desktopNotificationsEnabled: next.desktopNotificationsEnabled,
|
||||||
updateSource: next.updateSource,
|
updateSource: next.updateSource,
|
||||||
modelDownloadSource: next.modelDownloadSource,
|
modelDownloadSource: next.modelDownloadSource,
|
||||||
localToolEnvironment: next.localToolEnvironment,
|
localToolEnvironment: next.localToolEnvironment,
|
||||||
|
|||||||
+59
-1
@@ -34,6 +34,8 @@ import type {
|
|||||||
import { defaultKnowledgeOntologySettings } from '../shared/knowledge-ontology'
|
import { defaultKnowledgeOntologySettings } from '../shared/knowledge-ontology'
|
||||||
import { AssistantDatabase } from './assistant/assistant-database'
|
import { AssistantDatabase } from './assistant/assistant-database'
|
||||||
import { ImageGenerationService } from './agent/image-generation-service'
|
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 type { AgentExecutionRequest } from './agent/runtime'
|
||||||
import { KnowledgeService } from './knowledge/knowledge-service'
|
import { KnowledgeService } from './knowledge/knowledge-service'
|
||||||
import { KnowledgeDatabase } from './knowledge/knowledge-database'
|
import { KnowledgeDatabase } from './knowledge/knowledge-database'
|
||||||
@@ -5201,7 +5203,8 @@ describe('registerIpcHandlers agent terminal state', () => {
|
|||||||
heartbeatEnabled?: boolean,
|
heartbeatEnabled?: boolean,
|
||||||
obsidianService?: ObsidianService,
|
obsidianService?: ObsidianService,
|
||||||
nativeClientCoordinator?: Parameters<typeof registerIpcHandlers>[45],
|
nativeClientCoordinator?: Parameters<typeof registerIpcHandlers>[45],
|
||||||
nativeTerminalManager?: Parameters<typeof registerIpcHandlers>[41]
|
nativeTerminalManager?: Parameters<typeof registerIpcHandlers>[41],
|
||||||
|
applicationSettingsStore?: ApplicationSettingsStore
|
||||||
) {
|
) {
|
||||||
const assistantDatabase = {
|
const assistantDatabase = {
|
||||||
createTask: vi.fn(),
|
createTask: vi.fn(),
|
||||||
@@ -5438,6 +5441,7 @@ describe('registerIpcHandlers agent terminal state', () => {
|
|||||||
args[44] = obsidianService
|
args[44] = obsidianService
|
||||||
args[45] = nativeClientCoordinator
|
args[45] = nativeClientCoordinator
|
||||||
args[41] = nativeTerminalManager
|
args[41] = nativeTerminalManager
|
||||||
|
if (applicationSettingsStore) args[15] = applicationSettingsStore
|
||||||
const dispose = registerIpcHandlers(...args)
|
const dispose = registerIpcHandlers(...args)
|
||||||
return {
|
return {
|
||||||
approvalBroker,
|
approvalBroker,
|
||||||
@@ -5483,6 +5487,60 @@ describe('registerIpcHandlers agent terminal state', () => {
|
|||||||
senderFrame: webContents.mainFrame
|
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(
|
function createManagedSshHarness(
|
||||||
selectedRuntime: Record<string, unknown>
|
selectedRuntime: Record<string, unknown>
|
||||||
) {
|
) {
|
||||||
|
|||||||
+14
-4
@@ -11,6 +11,7 @@ import {
|
|||||||
dialog,
|
dialog,
|
||||||
ipcMain,
|
ipcMain,
|
||||||
shell,
|
shell,
|
||||||
|
type NotificationConstructorOptions,
|
||||||
type IpcMainInvokeEvent
|
type IpcMainInvokeEvent
|
||||||
} from 'electron'
|
} from 'electron'
|
||||||
import {
|
import {
|
||||||
@@ -1043,6 +1044,15 @@ export function registerIpcHandlers(
|
|||||||
obsidianService?: ObsidianService,
|
obsidianService?: ObsidianService,
|
||||||
nativeClientCoordinator?: Pick<NativeClientCoordinator, 'open' | 'get' | 'stop' | 'closeOwner'>
|
nativeClientCoordinator?: Pick<NativeClientCoordinator, 'open' | 'get' | 'stop' | 'closeOwner'>
|
||||||
): () => Promise<void> {
|
): () => Promise<void> {
|
||||||
|
const notifyDesktop = async (options: NotificationConstructorOptions): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if ((await applicationSettingsStore?.get())?.desktopNotificationsEnabled === false) return
|
||||||
|
} catch {
|
||||||
|
// A notification preference read must not change the task's terminal result.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showDesktopNotificationWhenUnfocused(window, options)
|
||||||
|
}
|
||||||
type ActiveRequestLease = {
|
type ActiveRequestLease = {
|
||||||
controller: AbortController
|
controller: AbortController
|
||||||
isReply: boolean
|
isReply: boolean
|
||||||
@@ -2601,7 +2611,7 @@ export function registerIpcHandlers(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
assistantDatabase.updateTaskStatus(taskId, 'completed')
|
assistantDatabase.updateTaskStatus(taskId, 'completed')
|
||||||
showDesktopNotificationWhenUnfocused(window, {
|
await notifyDesktop({
|
||||||
title:
|
title:
|
||||||
origin === 'channel'
|
origin === 'channel'
|
||||||
? `${remoteContext?.channelLabel ?? '远程通道'}请求已完成`
|
? `${remoteContext?.channelLabel ?? '远程通道'}请求已完成`
|
||||||
@@ -2631,7 +2641,7 @@ export function registerIpcHandlers(
|
|||||||
cancelled ? 'cancelled' : 'failed',
|
cancelled ? 'cancelled' : 'failed',
|
||||||
message
|
message
|
||||||
)
|
)
|
||||||
showDesktopNotificationWhenUnfocused(window, {
|
await notifyDesktop({
|
||||||
title:
|
title:
|
||||||
origin === 'channel'
|
origin === 'channel'
|
||||||
? `${remoteContext?.channelLabel ?? '远程通道'}请求失败`
|
? `${remoteContext?.channelLabel ?? '远程通道'}请求失败`
|
||||||
@@ -4496,7 +4506,7 @@ export function registerIpcHandlers(
|
|||||||
'completed'
|
'completed'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
showDesktopNotificationWhenUnfocused(window, {
|
await notifyDesktop({
|
||||||
title: 'GoodBuddy 任务已完成',
|
title: 'GoodBuddy 任务已完成',
|
||||||
body: '回复已生成,可返回会话查看。'
|
body: '回复已生成,可返回会话查看。'
|
||||||
})
|
})
|
||||||
@@ -4573,7 +4583,7 @@ export function registerIpcHandlers(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
showDesktopNotificationWhenUnfocused(window, {
|
await notifyDesktop({
|
||||||
title: cancelled
|
title: cancelled
|
||||||
? 'GoodBuddy 任务已取消'
|
? 'GoodBuddy 任务已取消'
|
||||||
: 'GoodBuddy 任务失败',
|
: 'GoodBuddy 任务失败',
|
||||||
|
|||||||
@@ -1130,6 +1130,7 @@ function installRemoteProjectsSetting(enabled: boolean): {
|
|||||||
} {
|
} {
|
||||||
let settings: ApplicationSettings = {
|
let settings: ApplicationSettings = {
|
||||||
checkUpdatesOnStartup: false,
|
checkUpdatesOnStartup: false,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github",
|
updateSource: "github",
|
||||||
modelDownloadSource: "modelscope",
|
modelDownloadSource: "modelscope",
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -3144,6 +3145,7 @@ describe("App", () => {
|
|||||||
api.updates = {
|
api.updates = {
|
||||||
getSettings: vi.fn(async () => ({
|
getSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: false,
|
checkUpdatesOnStartup: false,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github" as const,
|
updateSource: "github" as const,
|
||||||
modelDownloadSource: "modelscope" as const,
|
modelDownloadSource: "modelscope" as const,
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -3247,6 +3249,7 @@ describe("App", () => {
|
|||||||
api.updates = {
|
api.updates = {
|
||||||
getSettings: vi.fn(async () => ({
|
getSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: true,
|
checkUpdatesOnStartup: true,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github" as const,
|
updateSource: "github" as const,
|
||||||
modelDownloadSource: "modelscope" as const,
|
modelDownloadSource: "modelscope" as const,
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -3262,6 +3265,7 @@ describe("App", () => {
|
|||||||
})),
|
})),
|
||||||
updateSettings: vi.fn(async () => ({
|
updateSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: true,
|
checkUpdatesOnStartup: true,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github" as const,
|
updateSource: "github" as const,
|
||||||
modelDownloadSource: "modelscope" as const,
|
modelDownloadSource: "modelscope" as const,
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -3363,6 +3367,7 @@ describe("App", () => {
|
|||||||
api.updates = {
|
api.updates = {
|
||||||
getSettings: vi.fn(async () => ({
|
getSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: true,
|
checkUpdatesOnStartup: true,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github" as const,
|
updateSource: "github" as const,
|
||||||
modelDownloadSource: "modelscope" as const,
|
modelDownloadSource: "modelscope" as const,
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -3378,6 +3383,7 @@ describe("App", () => {
|
|||||||
})),
|
})),
|
||||||
updateSettings: vi.fn(async () => ({
|
updateSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: true,
|
checkUpdatesOnStartup: true,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github" as const,
|
updateSource: "github" as const,
|
||||||
modelDownloadSource: "modelscope" as const,
|
modelDownloadSource: "modelscope" as const,
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -5653,6 +5659,7 @@ describe("App", () => {
|
|||||||
it("updates and removes the composer shortcut hint immediately after saving Settings", async () => {
|
it("updates and removes the composer shortcut hint immediately after saving Settings", async () => {
|
||||||
let applicationSettings: ApplicationSettings = {
|
let applicationSettings: ApplicationSettings = {
|
||||||
checkUpdatesOnStartup: false,
|
checkUpdatesOnStartup: false,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github",
|
updateSource: "github",
|
||||||
modelDownloadSource: "modelscope",
|
modelDownloadSource: "modelscope",
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -13541,6 +13548,7 @@ describe("App", () => {
|
|||||||
api.updates = {
|
api.updates = {
|
||||||
getSettings: vi.fn(async () => ({
|
getSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: false,
|
checkUpdatesOnStartup: false,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github" as const,
|
updateSource: "github" as const,
|
||||||
modelDownloadSource: "modelscope" as const,
|
modelDownloadSource: "modelscope" as const,
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -13556,6 +13564,7 @@ describe("App", () => {
|
|||||||
})),
|
})),
|
||||||
updateSettings: vi.fn(async () => ({
|
updateSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: false,
|
checkUpdatesOnStartup: false,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github" as const,
|
updateSource: "github" as const,
|
||||||
modelDownloadSource: "modelscope" as const,
|
modelDownloadSource: "modelscope" as const,
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -13608,6 +13617,7 @@ describe("App", () => {
|
|||||||
api.updates = {
|
api.updates = {
|
||||||
getSettings: vi.fn(async () => ({
|
getSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: false,
|
checkUpdatesOnStartup: false,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github" as const,
|
updateSource: "github" as const,
|
||||||
modelDownloadSource: "modelscope" as const,
|
modelDownloadSource: "modelscope" as const,
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -13667,6 +13677,7 @@ describe("App", () => {
|
|||||||
api.updates = {
|
api.updates = {
|
||||||
getSettings: vi.fn(async () => ({
|
getSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: false,
|
checkUpdatesOnStartup: false,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github" as const,
|
updateSource: "github" as const,
|
||||||
modelDownloadSource: "modelscope" as const,
|
modelDownloadSource: "modelscope" as const,
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -13703,6 +13714,7 @@ describe("App", () => {
|
|||||||
api.updates = {
|
api.updates = {
|
||||||
getSettings: vi.fn(async () => ({
|
getSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: false,
|
checkUpdatesOnStartup: false,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github" as const,
|
updateSource: "github" as const,
|
||||||
modelDownloadSource: "modelscope" as const,
|
modelDownloadSource: "modelscope" as const,
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -13718,6 +13730,7 @@ describe("App", () => {
|
|||||||
})),
|
})),
|
||||||
updateSettings: vi.fn(async () => ({
|
updateSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: false,
|
checkUpdatesOnStartup: false,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github" as const,
|
updateSource: "github" as const,
|
||||||
modelDownloadSource: "modelscope" as const,
|
modelDownloadSource: "modelscope" as const,
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -13779,6 +13792,7 @@ describe("App", () => {
|
|||||||
]);
|
]);
|
||||||
let applicationSettings: ApplicationSettings = {
|
let applicationSettings: ApplicationSettings = {
|
||||||
checkUpdatesOnStartup: false,
|
checkUpdatesOnStartup: false,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github",
|
updateSource: "github",
|
||||||
modelDownloadSource: "modelscope",
|
modelDownloadSource: "modelscope",
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -13837,6 +13851,7 @@ describe("App", () => {
|
|||||||
it("omits global notes settings while retaining application center settings without navigating the workspace", async () => {
|
it("omits global notes settings while retaining application center settings without navigating the workspace", async () => {
|
||||||
let applicationSettings: ApplicationSettings = {
|
let applicationSettings: ApplicationSettings = {
|
||||||
checkUpdatesOnStartup: false,
|
checkUpdatesOnStartup: false,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: "github",
|
updateSource: "github",
|
||||||
modelDownloadSource: "modelscope",
|
modelDownloadSource: "modelscope",
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ const todo: MagicTodoItem = {
|
|||||||
}
|
}
|
||||||
const otherTodo: MagicTodoItem = { ...todo, id: '00000000-0000-4000-8000-000000000604', noteId: secondNoteId, noteTitle: second.title, title: '准备演示' }
|
const otherTodo: MagicTodoItem = { ...todo, id: '00000000-0000-4000-8000-000000000604', noteId: secondNoteId, noteTitle: second.title, title: '准备演示' }
|
||||||
const settings: ApplicationSettings = {
|
const settings: ApplicationSettings = {
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
checkUpdatesOnStartup: false, updateSource: 'github', modelDownloadSource: 'modelscope',
|
checkUpdatesOnStartup: false, updateSource: 'github', modelDownloadSource: 'modelscope',
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings, applicationNavigation: defaultApplicationNavigation,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings, applicationNavigation: defaultApplicationNavigation,
|
||||||
localInferenceEnabled: true, conversationHtmlRenderingEnabled: true, remoteProjectsEnabled: false,
|
localInferenceEnabled: true, conversationHtmlRenderingEnabled: true, remoteProjectsEnabled: false,
|
||||||
|
|||||||
@@ -493,6 +493,29 @@ export function PlatformFeaturesSettingsSection({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changeDesktopNotifications = async (
|
||||||
|
enabled: boolean
|
||||||
|
): Promise<void> => {
|
||||||
|
const updates = window.goodbuddy.updates
|
||||||
|
if (!updates || !settings || saving) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
setSettings(await updates.updateSettings({
|
||||||
|
desktopNotificationsEnabled: enabled
|
||||||
|
}))
|
||||||
|
} catch {
|
||||||
|
onNotify?.({
|
||||||
|
tone: 'error',
|
||||||
|
message: t('platformFeatures.errors.saveDesktopNotificationsFailed'),
|
||||||
|
dedupeKey: 'desktop-notifications-save'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const changeRemoteProjects = async (
|
const changeRemoteProjects = async (
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
@@ -556,53 +579,6 @@ export function PlatformFeaturesSettingsSection({
|
|||||||
id="platform-features-panel-general"
|
id="platform-features-panel-general"
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
>
|
>
|
||||||
<article className="capability-card">
|
|
||||||
<div className="capability-card__header">
|
|
||||||
<span className="inline-help-label">
|
|
||||||
<FolderOpen aria-hidden="true" size={16} />
|
|
||||||
<strong>{settingsT('runtime.workspace.title')}</strong>
|
|
||||||
<InlineHelp label={settingsT('runtime.workspace.title')}>
|
|
||||||
{settingsT('runtime.workspace.description')}
|
|
||||||
</InlineHelp>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<label className="field">
|
|
||||||
<span>{settingsT('runtime.workspace.directoryLabel')}</span>
|
|
||||||
<div className="workspace-picker">
|
|
||||||
<input
|
|
||||||
aria-label={settingsT('runtime.workspace.directoryLabel')}
|
|
||||||
onChange={(event) =>
|
|
||||||
onWorkspacePathChange(event.target.value)
|
|
||||||
}
|
|
||||||
value={workspacePath}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="secondary-button"
|
|
||||||
onClick={() => {
|
|
||||||
void window.goodbuddy.settings
|
|
||||||
.selectWorkspace()
|
|
||||||
.then((selected) => {
|
|
||||||
if (selected) {
|
|
||||||
onWorkspacePathChange(selected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((reason: unknown) => {
|
|
||||||
setError(
|
|
||||||
displayErrorMessage(
|
|
||||||
reason,
|
|
||||||
settingsT('errors.selectWorkspace')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{settingsT('actions.select')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article className="capability-card">
|
<article className="capability-card">
|
||||||
<div className="capability-card__header">
|
<div className="capability-card__header">
|
||||||
<span className="inline-help-label">
|
<span className="inline-help-label">
|
||||||
@@ -710,17 +686,30 @@ export function PlatformFeaturesSettingsSection({
|
|||||||
)}
|
)}
|
||||||
</article>
|
</article>
|
||||||
{settings && (
|
{settings && (
|
||||||
<article className="capability-card">
|
<article
|
||||||
|
aria-label={t('platformFeatures.groups.conversation')}
|
||||||
|
className="capability-card"
|
||||||
|
>
|
||||||
<div className="capability-card__header">
|
<div className="capability-card__header">
|
||||||
<span className="inline-help-label">
|
<strong>{t('platformFeatures.groups.conversation')}</strong>
|
||||||
<strong>
|
|
||||||
{t('platformFeatures.conversationHtml.title')}
|
|
||||||
</strong>
|
|
||||||
<InlineHelp label={t('platformFeatures.conversationHtml.title')}>
|
|
||||||
{t('platformFeatures.conversationHtml.description')}
|
|
||||||
</InlineHelp>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<label className="toggle-row">
|
||||||
|
<input
|
||||||
|
aria-describedby="desktop-notifications-description"
|
||||||
|
checked={settings.desktopNotificationsEnabled}
|
||||||
|
disabled={saving}
|
||||||
|
onChange={(event) =>
|
||||||
|
void changeDesktopNotifications(event.target.checked)
|
||||||
|
}
|
||||||
|
role="switch"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<span>{t('platformFeatures.desktopNotifications.enabled')}</span>
|
||||||
|
</label>
|
||||||
|
<p id="desktop-notifications-description">
|
||||||
|
{t('platformFeatures.desktopNotifications.description')}
|
||||||
|
</p>
|
||||||
|
<div className="inline-help-label">
|
||||||
<label className="toggle-row">
|
<label className="toggle-row">
|
||||||
<input
|
<input
|
||||||
checked={settings.conversationHtmlRenderingEnabled}
|
checked={settings.conversationHtmlRenderingEnabled}
|
||||||
@@ -737,10 +726,62 @@ export function PlatformFeaturesSettingsSection({
|
|||||||
{t('platformFeatures.conversationHtml.enabled')}
|
{t('platformFeatures.conversationHtml.enabled')}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<InlineHelp label={t('platformFeatures.conversationHtml.title')}>
|
||||||
|
{t('platformFeatures.conversationHtml.description')}
|
||||||
|
</InlineHelp>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
)}
|
)}
|
||||||
|
<article
|
||||||
|
aria-label={t('platformFeatures.groups.workspace')}
|
||||||
|
className="capability-card"
|
||||||
|
>
|
||||||
|
<div className="capability-card__header">
|
||||||
|
<span className="inline-help-label">
|
||||||
|
<FolderOpen aria-hidden="true" size={16} />
|
||||||
|
<strong>{t('platformFeatures.groups.workspace')}</strong>
|
||||||
|
<InlineHelp label={settingsT('runtime.workspace.title')}>
|
||||||
|
{settingsT('runtime.workspace.description')}
|
||||||
|
</InlineHelp>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<label className="field">
|
||||||
|
<span>{settingsT('runtime.workspace.directoryLabel')}</span>
|
||||||
|
<div className="workspace-picker">
|
||||||
|
<input
|
||||||
|
aria-label={settingsT('runtime.workspace.directoryLabel')}
|
||||||
|
onChange={(event) =>
|
||||||
|
onWorkspacePathChange(event.target.value)
|
||||||
|
}
|
||||||
|
value={workspacePath}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="secondary-button"
|
||||||
|
onClick={() => {
|
||||||
|
void window.goodbuddy.settings
|
||||||
|
.selectWorkspace()
|
||||||
|
.then((selected) => {
|
||||||
|
if (selected) {
|
||||||
|
onWorkspacePathChange(selected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((reason: unknown) => {
|
||||||
|
setError(
|
||||||
|
displayErrorMessage(
|
||||||
|
reason,
|
||||||
|
settingsT('errors.selectWorkspace')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{settingsT('actions.select')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
{settings ? (
|
{settings ? (
|
||||||
<article className="capability-card">
|
<>
|
||||||
<div className="capability-card__header">
|
<div className="capability-card__header">
|
||||||
<span className="inline-help-label">
|
<span className="inline-help-label">
|
||||||
<strong>
|
<strong>
|
||||||
@@ -794,7 +835,7 @@ export function PlatformFeaturesSettingsSection({
|
|||||||
{sourceError}
|
{sourceError}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</article>
|
</>
|
||||||
) : (
|
) : (
|
||||||
!error && (
|
!error && (
|
||||||
<p className="settings-notice" role="status">
|
<p className="settings-notice" role="status">
|
||||||
@@ -802,6 +843,7 @@ export function PlatformFeaturesSettingsSection({
|
|||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
</article>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -546,6 +546,7 @@ const removeEmbeddingModel = vi.fn(
|
|||||||
)
|
)
|
||||||
let applicationSettings: ApplicationSettings = {
|
let applicationSettings: ApplicationSettings = {
|
||||||
checkUpdatesOnStartup: true,
|
checkUpdatesOnStartup: true,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: 'github',
|
updateSource: 'github',
|
||||||
modelDownloadSource: 'modelscope',
|
modelDownloadSource: 'modelscope',
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -839,6 +840,7 @@ describe('SettingsPanel runtime files', () => {
|
|||||||
await changeUiLocale('zh-CN')
|
await changeUiLocale('zh-CN')
|
||||||
applicationSettings = {
|
applicationSettings = {
|
||||||
checkUpdatesOnStartup: true,
|
checkUpdatesOnStartup: true,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: 'github',
|
updateSource: 'github',
|
||||||
modelDownloadSource: 'modelscope',
|
modelDownloadSource: 'modelscope',
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -1426,7 +1428,7 @@ describe('SettingsPanel runtime files', () => {
|
|||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
screen.getByRole('tab', { name: /Platform Features/i })
|
screen.getByRole('tab', { name: /Platform Features/i })
|
||||||
)
|
)
|
||||||
expect(screen.getByText('Default workspace')).toBeInTheDocument()
|
expect(screen.getByText('Workspace & downloads')).toBeInTheDocument()
|
||||||
expect(
|
expect(
|
||||||
screen.getByLabelText('Default workspace folder')
|
screen.getByLabelText('Default workspace folder')
|
||||||
).toBeInTheDocument()
|
).toBeInTheDocument()
|
||||||
@@ -1574,6 +1576,100 @@ describe('SettingsPanel runtime files', () => {
|
|||||||
).toBeInTheDocument()
|
).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['zh-CN', '平台功能', '桌面通知'],
|
||||||
|
['en-US', 'Platform features', 'Desktop notifications']
|
||||||
|
] as const)('saves desktop notifications immediately in %s', async (locale, category, label) => {
|
||||||
|
await changeUiLocale(locale)
|
||||||
|
render(
|
||||||
|
<SettingsPanel
|
||||||
|
{...heartbeatSettingsProps}
|
||||||
|
open
|
||||||
|
onClearLocalData={vi.fn(async () => {})}
|
||||||
|
onClose={vi.fn()}
|
||||||
|
onSaved={vi.fn()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('tab', { name: category }))
|
||||||
|
const toggle = await screen.findByRole('switch', { name: label })
|
||||||
|
expect(toggle).toBeChecked()
|
||||||
|
expect(toggle).toHaveAccessibleDescription()
|
||||||
|
fireEvent.click(toggle)
|
||||||
|
await waitFor(() => expect(toggle).not.toBeChecked())
|
||||||
|
expect(updateApplicationSettings).toHaveBeenLastCalledWith({
|
||||||
|
desktopNotificationsEnabled: false
|
||||||
|
})
|
||||||
|
fireEvent.click(toggle)
|
||||||
|
await waitFor(() => expect(toggle).toBeChecked())
|
||||||
|
expect(updateApplicationSettings).toHaveBeenLastCalledWith({
|
||||||
|
desktopNotificationsEnabled: true
|
||||||
|
})
|
||||||
|
expect(updateApplicationSettings).toHaveBeenCalledTimes(2)
|
||||||
|
expect(updateRuntime).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads disabled desktop notifications and groups related General settings', async () => {
|
||||||
|
applicationSettings.desktopNotificationsEnabled = false
|
||||||
|
render(
|
||||||
|
<SettingsPanel
|
||||||
|
{...heartbeatSettingsProps}
|
||||||
|
open
|
||||||
|
onClearLocalData={vi.fn(async () => {})}
|
||||||
|
onClose={vi.fn()}
|
||||||
|
onSaved={vi.fn()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('tab', { name: '平台功能' }))
|
||||||
|
const conversation = await screen.findByRole('article', { name: '会话与通知' })
|
||||||
|
expect(within(conversation).getByRole('switch', { name: '桌面通知' })).not.toBeChecked()
|
||||||
|
expect(within(conversation).getByRole('switch', { name: '在会话中渲染 HTML' })).toBeChecked()
|
||||||
|
const workspace = screen.getByRole('article', { name: '工作目录与下载' })
|
||||||
|
expect(within(workspace).getByRole('textbox')).toHaveValue(runtimeSettings.workspacePath)
|
||||||
|
expect(within(workspace).getByRole('radio', { name: /ModelScope/u })).toBeChecked()
|
||||||
|
expect(updateApplicationSettings).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps desktop notifications confirmed while saving and reports failure through onNotify', async () => {
|
||||||
|
const onNotify = vi.fn()
|
||||||
|
let rejectSave!: (reason: Error) => void
|
||||||
|
updateApplicationSettings.mockImplementationOnce(() => new Promise((_, reject) => {
|
||||||
|
rejectSave = reject
|
||||||
|
}))
|
||||||
|
render(
|
||||||
|
<SettingsPanel
|
||||||
|
{...heartbeatSettingsProps}
|
||||||
|
onNotify={onNotify}
|
||||||
|
open
|
||||||
|
onClearLocalData={vi.fn(async () => {})}
|
||||||
|
onClose={vi.fn()}
|
||||||
|
onSaved={vi.fn()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('tab', { name: '平台功能' }))
|
||||||
|
const toggle = await screen.findByRole('switch', { name: '桌面通知' })
|
||||||
|
fireEvent.click(toggle)
|
||||||
|
expect(toggle).toBeDisabled()
|
||||||
|
expect(toggle).toBeChecked()
|
||||||
|
fireEvent.click(toggle)
|
||||||
|
expect(updateApplicationSettings).toHaveBeenCalledTimes(1)
|
||||||
|
await act(async () => rejectSave(new Error('save failed')))
|
||||||
|
expect(toggle).toBeEnabled()
|
||||||
|
expect(toggle).toBeChecked()
|
||||||
|
expect(onNotify).toHaveBeenCalledExactlyOnceWith({
|
||||||
|
tone: 'error',
|
||||||
|
message: '保存桌面通知设置失败,请重试',
|
||||||
|
dedupeKey: 'desktop-notifications-save'
|
||||||
|
})
|
||||||
|
expect(screen.queryByText('保存桌面通知设置失败,请重试')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
fireEvent.click(toggle)
|
||||||
|
await waitFor(() => expect(toggle).not.toBeChecked())
|
||||||
|
expect(updateApplicationSettings).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
it('disables conversation HTML rendering from General settings', async () => {
|
it('disables conversation HTML rendering from General settings', async () => {
|
||||||
const onConversationHtmlRenderingEnabledChange = vi.fn()
|
const onConversationHtmlRenderingEnabledChange = vi.fn()
|
||||||
render(
|
render(
|
||||||
@@ -1629,6 +1725,9 @@ describe('SettingsPanel runtime files', () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.queryByRole('radio', { name: /ModelScope/u })
|
screen.queryByRole('radio', { name: /ModelScope/u })
|
||||||
).not.toBeInTheDocument()
|
).not.toBeInTheDocument()
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('switch', { name: '桌面通知' })
|
||||||
|
).not.toBeInTheDocument()
|
||||||
expect(
|
expect(
|
||||||
screen.queryByText('当前选择:ModelScope')
|
screen.queryByText('当前选择:ModelScope')
|
||||||
).not.toBeInTheDocument()
|
).not.toBeInTheDocument()
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ describe('UpdateSettingsSection', () => {
|
|||||||
it('checks the official release manifest and updates the startup preference', async () => {
|
it('checks the official release manifest and updates the startup preference', async () => {
|
||||||
let applicationSettings: ApplicationSettings = {
|
let applicationSettings: ApplicationSettings = {
|
||||||
checkUpdatesOnStartup: true,
|
checkUpdatesOnStartup: true,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: 'github',
|
updateSource: 'github',
|
||||||
modelDownloadSource: 'modelscope',
|
modelDownloadSource: 'modelscope',
|
||||||
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
||||||
@@ -186,6 +187,7 @@ describe('UpdateSettingsSection', () => {
|
|||||||
updates: {
|
updates: {
|
||||||
getSettings: vi.fn(async () => ({
|
getSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: true,
|
checkUpdatesOnStartup: true,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: 'github',
|
updateSource: 'github',
|
||||||
magicNotesEnabled: true,
|
magicNotesEnabled: true,
|
||||||
magicNoteCommentMode: 'immediate',
|
magicNoteCommentMode: 'immediate',
|
||||||
@@ -193,6 +195,7 @@ describe('UpdateSettingsSection', () => {
|
|||||||
})),
|
})),
|
||||||
updateSettings: vi.fn(async () => ({
|
updateSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: true,
|
checkUpdatesOnStartup: true,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: 'github',
|
updateSource: 'github',
|
||||||
magicNotesEnabled: true,
|
magicNotesEnabled: true,
|
||||||
magicNoteCommentMode: 'immediate',
|
magicNoteCommentMode: 'immediate',
|
||||||
@@ -239,6 +242,7 @@ describe('UpdateSettingsSection', () => {
|
|||||||
updates: {
|
updates: {
|
||||||
getSettings: vi.fn(async () => ({
|
getSettings: vi.fn(async () => ({
|
||||||
checkUpdatesOnStartup: false,
|
checkUpdatesOnStartup: false,
|
||||||
|
desktopNotificationsEnabled: true,
|
||||||
updateSource: 'mirror',
|
updateSource: 'mirror',
|
||||||
magicNotesEnabled: true,
|
magicNotesEnabled: true,
|
||||||
magicNoteCommentMode: 'immediate',
|
magicNoteCommentMode: 'immediate',
|
||||||
|
|||||||
@@ -541,6 +541,8 @@ export const settingsSections = {
|
|||||||
serviceUnavailable:
|
serviceUnavailable:
|
||||||
'Application settings are not available in this version',
|
'Application settings are not available in this version',
|
||||||
readFailed: 'Could not load platform feature settings',
|
readFailed: 'Could not load platform feature settings',
|
||||||
|
saveDesktopNotificationsFailed:
|
||||||
|
'Could not save the desktop notification setting. Try again.',
|
||||||
saveConversationHtmlRenderingFailed:
|
saveConversationHtmlRenderingFailed:
|
||||||
'Could not save the conversation HTML rendering setting. Try again.',
|
'Could not save the conversation HTML rendering setting. Try again.',
|
||||||
saveRemoteProjectsFailed:
|
saveRemoteProjectsFailed:
|
||||||
@@ -560,6 +562,15 @@ export const settingsSections = {
|
|||||||
ariaLabel: 'Platform feature settings',
|
ariaLabel: 'Platform feature settings',
|
||||||
general: 'General'
|
general: 'General'
|
||||||
},
|
},
|
||||||
|
groups: {
|
||||||
|
conversation: 'Conversations & notifications',
|
||||||
|
workspace: 'Workspace & downloads'
|
||||||
|
},
|
||||||
|
desktopNotifications: {
|
||||||
|
enabled: 'Desktop notifications',
|
||||||
|
description:
|
||||||
|
'Show system notifications when background work needs your attention. Turning this off does not affect in-app notifications.'
|
||||||
|
},
|
||||||
conversationHtml: {
|
conversationHtml: {
|
||||||
title: 'Conversation HTML rendering',
|
title: 'Conversation HTML rendering',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -516,6 +516,7 @@ export const settingsSections = {
|
|||||||
errors: {
|
errors: {
|
||||||
serviceUnavailable: '当前版本未提供应用设置服务',
|
serviceUnavailable: '当前版本未提供应用设置服务',
|
||||||
readFailed: '读取平台功能设置失败',
|
readFailed: '读取平台功能设置失败',
|
||||||
|
saveDesktopNotificationsFailed: '保存桌面通知设置失败,请重试',
|
||||||
saveConversationHtmlRenderingFailed:
|
saveConversationHtmlRenderingFailed:
|
||||||
'保存会话 HTML 渲染设置失败,请重试',
|
'保存会话 HTML 渲染设置失败,请重试',
|
||||||
saveRemoteProjectsFailed: '保存远程项目设置失败,请重试',
|
saveRemoteProjectsFailed: '保存远程项目设置失败,请重试',
|
||||||
@@ -531,6 +532,15 @@ export const settingsSections = {
|
|||||||
ariaLabel: '平台功能设置',
|
ariaLabel: '平台功能设置',
|
||||||
general: '通用设置'
|
general: '通用设置'
|
||||||
},
|
},
|
||||||
|
groups: {
|
||||||
|
conversation: '会话与通知',
|
||||||
|
workspace: '工作目录与下载'
|
||||||
|
},
|
||||||
|
desktopNotifications: {
|
||||||
|
enabled: '桌面通知',
|
||||||
|
description:
|
||||||
|
'后台工作需要关注时显示系统通知。关闭后不影响应用内通知。'
|
||||||
|
},
|
||||||
conversationHtml: {
|
conversationHtml: {
|
||||||
title: '会话 HTML 渲染',
|
title: '会话 HTML 渲染',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export const applicationNavigationSchema = z.object({
|
|||||||
const applicationPreferencesSchema = z
|
const applicationPreferencesSchema = z
|
||||||
.object({
|
.object({
|
||||||
checkUpdatesOnStartup: z.boolean(),
|
checkUpdatesOnStartup: z.boolean(),
|
||||||
|
desktopNotificationsEnabled: z.boolean().default(true),
|
||||||
updateSource: updateSourceSchema,
|
updateSource: updateSourceSchema,
|
||||||
modelDownloadSource: modelDownloadSourceSchema,
|
modelDownloadSource: modelDownloadSourceSchema,
|
||||||
localToolEnvironment: localToolEnvironmentSettingsSchema,
|
localToolEnvironment: localToolEnvironmentSettingsSchema,
|
||||||
@@ -88,6 +89,7 @@ export const applicationSettingsUpdateSchema = applicationPreferencesSchema
|
|||||||
.partial()
|
.partial()
|
||||||
.extend({
|
.extend({
|
||||||
applicationNavigation: applicationNavigationSchema.optional(),
|
applicationNavigation: applicationNavigationSchema.optional(),
|
||||||
|
desktopNotificationsEnabled: z.boolean().optional(),
|
||||||
localInferenceEnabled: z.boolean().optional(),
|
localInferenceEnabled: z.boolean().optional(),
|
||||||
magicNotesEnabled: z.boolean().optional(),
|
magicNotesEnabled: z.boolean().optional(),
|
||||||
heartbeatEnabled: z.boolean().optional(),
|
heartbeatEnabled: z.boolean().optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user