feat: add configurable Magic Notes and polish UI
Cross-platform packages / Validate source (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / Publish GitHub Release (push) Has been cancelled
Cross-platform packages / Validate source (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / Publish GitHub Release (push) Has been cancelled
This commit is contained in:
@@ -11,6 +11,7 @@ import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
ApplicationSettingsStore,
|
||||
applicationSettingsSchema,
|
||||
applicationSettingsUpdateSchema,
|
||||
defaultApplicationSettings
|
||||
} from './application-settings-store'
|
||||
|
||||
@@ -51,18 +52,26 @@ describe('ApplicationSettingsStore', () => {
|
||||
await expect(readdir(directory)).resolves.toEqual([])
|
||||
})
|
||||
|
||||
it('persists only the versioned startup update preference', async () => {
|
||||
it('persists versioned application preferences', async () => {
|
||||
const { directory, filePath, store } = await createStore()
|
||||
|
||||
await expect(
|
||||
store.update({ checkUpdatesOnStartup: false })
|
||||
).resolves.toEqual({ checkUpdatesOnStartup: false })
|
||||
store.update({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false
|
||||
})
|
||||
).resolves.toEqual({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false
|
||||
})
|
||||
await expect(store.get()).resolves.toEqual({
|
||||
checkUpdatesOnStartup: false
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false
|
||||
})
|
||||
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
|
||||
version: 1,
|
||||
checkUpdatesOnStartup: false
|
||||
version: 2,
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false
|
||||
})
|
||||
expect(
|
||||
(await readdir(directory)).filter((name) => name.endsWith('.tmp'))
|
||||
@@ -73,38 +82,103 @@ describe('ApplicationSettingsStore', () => {
|
||||
const { directory } = await createStore()
|
||||
const filePath = join(directory, 'nested', 'application-settings.json')
|
||||
const store = new ApplicationSettingsStore(filePath)
|
||||
await store.update({ checkUpdatesOnStartup: false })
|
||||
await store.update({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false
|
||||
})
|
||||
|
||||
await expect(
|
||||
new ApplicationSettingsStore(filePath).get()
|
||||
).resolves.toEqual({ checkUpdatesOnStartup: false })
|
||||
).resolves.toEqual({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false
|
||||
})
|
||||
})
|
||||
|
||||
it('strictly rejects unknown, missing, and mistyped input', async () => {
|
||||
const { directory, store } = await createStore()
|
||||
it.each([1, 2])(
|
||||
'loads version %s settings missing the field with Magic Notes disabled',
|
||||
async (version) => {
|
||||
const { filePath, store } = await createStore()
|
||||
await writeFile(
|
||||
filePath,
|
||||
JSON.stringify({
|
||||
version,
|
||||
checkUpdatesOnStartup: false
|
||||
}),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(store.get()).resolves.toEqual({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('strictly rejects incomplete full settings', () => {
|
||||
for (const input of [
|
||||
{},
|
||||
{ checkUpdatesOnStartup: 'true' },
|
||||
{ checkUpdatesOnStartup: true, anotherSetting: true },
|
||||
{
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: true,
|
||||
anotherSetting: true
|
||||
},
|
||||
{ checkUpdatesOnStartup: true },
|
||||
null
|
||||
]) {
|
||||
expect(applicationSettingsSchema.safeParse(input).success).toBe(
|
||||
false
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('strictly rejects empty, unknown, and mistyped updates', async () => {
|
||||
const { directory, store } = await createStore()
|
||||
for (const input of [
|
||||
{},
|
||||
{ checkUpdatesOnStartup: 'true' },
|
||||
{ anotherSetting: true },
|
||||
null
|
||||
]) {
|
||||
expect(
|
||||
applicationSettingsUpdateSchema.safeParse(input).success
|
||||
).toBe(false)
|
||||
await expect(store.update(input)).rejects.toThrow()
|
||||
}
|
||||
await expect(readdir(directory)).resolves.toEqual([])
|
||||
})
|
||||
|
||||
it('merges partial updates without overwriting other settings', async () => {
|
||||
const { store } = await createStore()
|
||||
|
||||
await store.update({ magicNotesEnabled: true })
|
||||
await expect(
|
||||
store.update({ checkUpdatesOnStartup: false })
|
||||
).resolves.toEqual({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
'{not-json',
|
||||
JSON.stringify({ version: 2, checkUpdatesOnStartup: false }),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
version: 3,
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false
|
||||
}),
|
||||
JSON.stringify({
|
||||
version: 2,
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true,
|
||||
injected: true
|
||||
}),
|
||||
JSON.stringify({ version: 1, checkUpdatesOnStartup: 'false' })
|
||||
JSON.stringify({
|
||||
version: 2,
|
||||
checkUpdatesOnStartup: 'false',
|
||||
magicNotesEnabled: true
|
||||
})
|
||||
])('isolates corrupt persisted data and restores defaults', async (data) => {
|
||||
const { directory, filePath, store } = await createStore()
|
||||
await writeFile(filePath, data, 'utf8')
|
||||
@@ -142,28 +216,48 @@ describe('ApplicationSettingsStore', () => {
|
||||
const { filePath, store } = await createStore()
|
||||
|
||||
await Promise.all([
|
||||
store.update({ checkUpdatesOnStartup: false }),
|
||||
store.update({ checkUpdatesOnStartup: true }),
|
||||
store.update({ checkUpdatesOnStartup: false })
|
||||
store.update({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true
|
||||
}),
|
||||
store.update({
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: false
|
||||
}),
|
||||
store.update({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false
|
||||
})
|
||||
])
|
||||
|
||||
await expect(store.get()).resolves.toEqual({
|
||||
checkUpdatesOnStartup: false
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false
|
||||
})
|
||||
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
|
||||
version: 1,
|
||||
checkUpdatesOnStartup: false
|
||||
version: 2,
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false
|
||||
})
|
||||
})
|
||||
|
||||
it('continues accepting updates after a validation failure', async () => {
|
||||
const { store } = await createStore()
|
||||
await expect(
|
||||
store.update({ checkUpdatesOnStartup: 'invalid' })
|
||||
store.update({
|
||||
checkUpdatesOnStartup: 'invalid',
|
||||
magicNotesEnabled: true
|
||||
})
|
||||
).rejects.toThrow()
|
||||
|
||||
await expect(
|
||||
store.update({ checkUpdatesOnStartup: false })
|
||||
).resolves.toEqual({ checkUpdatesOnStartup: false })
|
||||
store.update({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true
|
||||
})
|
||||
).resolves.toEqual({
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,12 +10,23 @@ import { dirname } from 'node:path'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
applicationSettingsSchema,
|
||||
applicationSettingsUpdateSchema,
|
||||
type ApplicationSettings
|
||||
} from '../shared/application-settings-contracts'
|
||||
export { applicationSettingsSchema } from '../shared/application-settings-contracts'
|
||||
export {
|
||||
applicationSettingsSchema,
|
||||
applicationSettingsUpdateSchema
|
||||
} from '../shared/application-settings-contracts'
|
||||
export type { ApplicationSettings } from '../shared/application-settings-contracts'
|
||||
|
||||
const CURRENT_SETTINGS_VERSION = 1
|
||||
const CURRENT_SETTINGS_VERSION = 2
|
||||
|
||||
const legacyStoredApplicationSettingsSchema = z
|
||||
.object({
|
||||
version: z.union([z.literal(1), z.literal(2)]),
|
||||
checkUpdatesOnStartup: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
|
||||
const storedApplicationSettingsSchema = applicationSettingsSchema
|
||||
.extend({
|
||||
@@ -28,7 +39,8 @@ type StoredApplicationSettings = z.infer<
|
||||
>
|
||||
|
||||
export const defaultApplicationSettings: ApplicationSettings = {
|
||||
checkUpdatesOnStartup: true
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: false
|
||||
}
|
||||
|
||||
function isMissingFile(error: unknown): boolean {
|
||||
@@ -81,6 +93,17 @@ export class ApplicationSettingsStore {
|
||||
}
|
||||
const result = storedApplicationSettingsSchema.safeParse(parsed)
|
||||
if (!result.success) {
|
||||
const legacyResult =
|
||||
legacyStoredApplicationSettingsSchema.safeParse(parsed)
|
||||
if (legacyResult.success) {
|
||||
this.settings = {
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
checkUpdatesOnStartup:
|
||||
legacyResult.data.checkUpdatesOnStartup,
|
||||
magicNotesEnabled: false
|
||||
}
|
||||
return this.settings
|
||||
}
|
||||
await this.isolateCorruptFile()
|
||||
this.settings = {
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
@@ -106,16 +129,19 @@ export class ApplicationSettingsStore {
|
||||
async get(): Promise<ApplicationSettings> {
|
||||
const stored = await this.loadStored()
|
||||
return {
|
||||
checkUpdatesOnStartup: stored.checkUpdatesOnStartup
|
||||
checkUpdatesOnStartup: stored.checkUpdatesOnStartup,
|
||||
magicNotesEnabled: stored.magicNotesEnabled
|
||||
}
|
||||
}
|
||||
|
||||
update(input: unknown): Promise<ApplicationSettings> {
|
||||
const operation = this.updateQueue.then(async () => {
|
||||
const settings = applicationSettingsSchema.parse(input)
|
||||
const updates = applicationSettingsUpdateSchema.parse(input)
|
||||
const current = await this.loadStored()
|
||||
const next: StoredApplicationSettings = {
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
...settings
|
||||
...current,
|
||||
...updates,
|
||||
version: CURRENT_SETTINGS_VERSION
|
||||
}
|
||||
await mkdir(dirname(this.filePath), { recursive: true })
|
||||
const temporaryPath =
|
||||
@@ -137,7 +163,8 @@ export class ApplicationSettingsStore {
|
||||
}
|
||||
this.settings = next
|
||||
return {
|
||||
checkUpdatesOnStartup: next.checkUpdatesOnStartup
|
||||
checkUpdatesOnStartup: next.checkUpdatesOnStartup,
|
||||
magicNotesEnabled: next.magicNotesEnabled
|
||||
}
|
||||
})
|
||||
this.updateQueue = operation.then(
|
||||
|
||||
+14
-2
@@ -63,7 +63,7 @@ import {
|
||||
dingTalkChannelSettingsInputSchema,
|
||||
weComChannelSettingsInputSchema
|
||||
} from '../shared/channel-settings-contracts'
|
||||
import { applicationSettingsSchema } from '../shared/application-settings-contracts'
|
||||
import { applicationSettingsUpdateSchema } from '../shared/application-settings-contracts'
|
||||
import {
|
||||
speechModelActionInputSchema,
|
||||
speechModelSelectionInputSchema
|
||||
@@ -834,6 +834,7 @@ export function registerIpcHandlers(
|
||||
channel: keyof typeof projectChannelLabels
|
||||
channelLabel: string
|
||||
senderDisplay: string
|
||||
projectId: string
|
||||
projectName: string
|
||||
rootPath: string
|
||||
conversationId: string
|
||||
@@ -1027,6 +1028,8 @@ export function registerIpcHandlers(
|
||||
publishRemoteActivity({
|
||||
requestId,
|
||||
conversationId: remoteContext.conversationId,
|
||||
projectId: remoteContext.projectId,
|
||||
projectName: remoteContext.projectName,
|
||||
channel: remoteContext.channel,
|
||||
kind: 'tool',
|
||||
callId: taskEvent.callId,
|
||||
@@ -1450,6 +1453,8 @@ export function registerIpcHandlers(
|
||||
publishRemoteActivity({
|
||||
requestId: remoteTaskId,
|
||||
conversationId: remoteConversation.id,
|
||||
projectId: project.id,
|
||||
projectName: project.name,
|
||||
channel,
|
||||
kind: 'request',
|
||||
title: `${channelLabel} · ${senderDisplay}`,
|
||||
@@ -1490,6 +1495,8 @@ export function registerIpcHandlers(
|
||||
publishRemoteActivity({
|
||||
requestId: remoteTaskId,
|
||||
conversationId: remoteConversation.id,
|
||||
projectId: project.id,
|
||||
projectName: project.name,
|
||||
channel,
|
||||
kind: 'result',
|
||||
title: `${channelLabel}远程执行不可用`,
|
||||
@@ -1522,6 +1529,8 @@ export function registerIpcHandlers(
|
||||
publishRemoteActivity({
|
||||
requestId: remoteTaskId,
|
||||
conversationId: remoteConversation.id,
|
||||
projectId: project.id,
|
||||
projectName: project.name,
|
||||
channel,
|
||||
kind: 'result',
|
||||
title: `${channelLabel}远程执行不可用`,
|
||||
@@ -1554,6 +1563,7 @@ export function registerIpcHandlers(
|
||||
channel,
|
||||
channelLabel,
|
||||
senderDisplay,
|
||||
projectId: project.id,
|
||||
projectName: project.name,
|
||||
rootPath: project.rootPath,
|
||||
conversationId: remoteConversation.id,
|
||||
@@ -1604,6 +1614,8 @@ export function registerIpcHandlers(
|
||||
publishRemoteActivity({
|
||||
requestId: remoteTaskId,
|
||||
conversationId: remoteConversation.id,
|
||||
projectId: project.id,
|
||||
projectName: project.name,
|
||||
channel,
|
||||
kind: 'result',
|
||||
title:
|
||||
@@ -2409,7 +2421,7 @@ export function registerIpcHandlers(
|
||||
throw new Error('应用设置服务不可用')
|
||||
}
|
||||
return applicationSettingsStore.update(
|
||||
applicationSettingsSchema.parse(input)
|
||||
applicationSettingsUpdateSchema.parse(input)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
+2
-2
@@ -60,8 +60,8 @@ export function createMainWindow(shouldQuit: () => boolean): BrowserWindow {
|
||||
const window = new BrowserWindow({
|
||||
width: 1180,
|
||||
height: 760,
|
||||
minWidth: 920,
|
||||
minHeight: 620,
|
||||
minWidth: 680,
|
||||
minHeight: 560,
|
||||
show: false,
|
||||
frame: false,
|
||||
...(usableIcon ? { icon: usableIcon } : {}),
|
||||
|
||||
Reference in New Issue
Block a user