Add an opt-in persistent frosted glass setting for the top bar and runtime checklist, with chat scrolling behind the header while preserving scroll anchors and window controls. Retry transient local MCP initialization failures once after cleanup, include redacted connection diagnostics in feedback, and index lifecycle events to reduce execution statistics scans. Release note: Add optional frosted glass appearance with chat visible behind the title bar; improve local tool connection recovery and diagnostic feedback.
629 lines
22 KiB
TypeScript
629 lines
22 KiB
TypeScript
import { readFile, realpath, stat } from 'node:fs/promises'
|
|
import { delimiter, extname, isAbsolute, join } from 'node:path'
|
|
import { z } from 'zod'
|
|
import {
|
|
applicationSettingsSchema,
|
|
applicationSettingsUpdateSchema,
|
|
applicationNavigationSchema,
|
|
builtInApplicationIds,
|
|
defaultApplicationNavigation,
|
|
defaultSupervisionTimeoutSeconds,
|
|
defaultSupervisorModelConcurrency,
|
|
type ApplicationSettings
|
|
} from '../shared/application-settings-contracts'
|
|
import {
|
|
defaultLocalToolEnvironmentSettings,
|
|
localToolExecutablePathSchema,
|
|
type LocalToolEnvironmentSettings
|
|
} from '../shared/local-tool-environment-contracts'
|
|
import { inspectLocalToolExecutable } from './local-tool-environment/local-tool-environment'
|
|
import { releaseVersionSchema } from '../shared/release-notes-contracts'
|
|
import type { SettingsWarning } from '../shared/settings-warning-contracts'
|
|
import {
|
|
assertSupportedSettingsVersion,
|
|
isolateCorruptSettingsFile,
|
|
isMissingFileError,
|
|
UnsupportedSettingsVersionError,
|
|
writeJsonFileAtomically
|
|
} from './settings-file-utils'
|
|
export {
|
|
applicationSettingsSchema,
|
|
applicationSettingsUpdateSchema
|
|
} from '../shared/application-settings-contracts'
|
|
export type { ApplicationSettings } from '../shared/application-settings-contracts'
|
|
|
|
const CURRENT_SETTINGS_VERSION = 12
|
|
|
|
const legacyStoredApplicationSettingsSchema = z
|
|
.object({
|
|
version: z.union([z.literal(1), z.literal(2)]),
|
|
checkUpdatesOnStartup: z.boolean()
|
|
})
|
|
.strict()
|
|
|
|
const versionTwoStoredApplicationSettingsSchema = z
|
|
.object({
|
|
version: z.literal(2),
|
|
checkUpdatesOnStartup: z.boolean(),
|
|
magicNotesEnabled: z.boolean()
|
|
})
|
|
.strict()
|
|
|
|
const versionThreeStoredApplicationSettingsSchema = z
|
|
.object({
|
|
version: z.literal(3),
|
|
checkUpdatesOnStartup: z.boolean(),
|
|
magicNotesEnabled: z.boolean(),
|
|
magicNoteCommentMode: applicationSettingsSchema.shape.magicNoteCommentMode
|
|
})
|
|
.strict()
|
|
|
|
const versionFourStoredApplicationSettingsSchema = applicationSettingsSchema
|
|
.omit({
|
|
updateSource: true,
|
|
modelDownloadSource: true,
|
|
localToolEnvironment: true,
|
|
conversationHtmlRenderingEnabled: true,
|
|
remoteProjectsEnabled: true,
|
|
magicNotesShowIncompleteTodoCount: true
|
|
})
|
|
.extend({
|
|
version: z.literal(4)
|
|
})
|
|
.strict()
|
|
|
|
const versionFiveStoredApplicationSettingsSchema = applicationSettingsSchema
|
|
.omit({
|
|
updateSource: true,
|
|
modelDownloadSource: true,
|
|
localToolEnvironment: true,
|
|
conversationHtmlRenderingEnabled: true,
|
|
remoteProjectsEnabled: true,
|
|
magicNotesShowIncompleteTodoCount: true
|
|
})
|
|
.extend({
|
|
version: z.literal(5),
|
|
lastSeenReleaseNotesVersion: releaseVersionSchema.nullable()
|
|
})
|
|
.strict()
|
|
|
|
const versionSixStoredApplicationSettingsSchema = applicationSettingsSchema
|
|
.omit({
|
|
modelDownloadSource: true,
|
|
localToolEnvironment: true,
|
|
conversationHtmlRenderingEnabled: true,
|
|
remoteProjectsEnabled: true,
|
|
magicNotesShowIncompleteTodoCount: true
|
|
})
|
|
.extend({
|
|
version: z.literal(6),
|
|
lastSeenReleaseNotesVersion: releaseVersionSchema.nullable()
|
|
})
|
|
.strict()
|
|
|
|
const versionSevenStoredApplicationSettingsSchema = applicationSettingsSchema
|
|
.omit({
|
|
localToolEnvironment: true,
|
|
conversationHtmlRenderingEnabled: true,
|
|
remoteProjectsEnabled: true,
|
|
magicNotesShowIncompleteTodoCount: true
|
|
})
|
|
.extend({
|
|
version: z.literal(7),
|
|
lastSeenReleaseNotesVersion: releaseVersionSchema.nullable()
|
|
})
|
|
.strict()
|
|
|
|
const versionEightStoredApplicationSettingsSchema = applicationSettingsSchema
|
|
.omit({
|
|
localToolEnvironment: true,
|
|
conversationHtmlRenderingEnabled: true,
|
|
remoteProjectsEnabled: true
|
|
})
|
|
.extend({
|
|
version: z.literal(8),
|
|
lastSeenReleaseNotesVersion: releaseVersionSchema.nullable()
|
|
})
|
|
.strict()
|
|
|
|
const versionNineStoredApplicationSettingsSchema = applicationSettingsSchema
|
|
.omit({
|
|
localToolEnvironment: true,
|
|
conversationHtmlRenderingEnabled: true
|
|
})
|
|
.extend({
|
|
version: z.literal(9),
|
|
lastSeenReleaseNotesVersion: releaseVersionSchema.nullable()
|
|
})
|
|
.strict()
|
|
|
|
const versionTenStoredApplicationSettingsSchema =
|
|
applicationSettingsSchema
|
|
.omit({ conversationHtmlRenderingEnabled: true })
|
|
.extend({
|
|
version: z.literal(10),
|
|
lastSeenReleaseNotesVersion: releaseVersionSchema.nullable()
|
|
})
|
|
.strict()
|
|
|
|
const storedApplicationSettingsSchema = applicationSettingsSchema
|
|
.extend({
|
|
// Stored partial orders are upgraded here; public settings writes remain strict.
|
|
applicationNavigation: applicationNavigationSchema.extend({
|
|
order: z.array(z.enum(builtInApplicationIds))
|
|
.refine(ids => new Set(ids).size === ids.length, 'Duplicate application')
|
|
.default([])
|
|
.transform(ids => [
|
|
...(['knowledge', 'heartbeat'] as const).filter(id => !ids.includes(id)),
|
|
...ids,
|
|
...defaultApplicationNavigation.order.filter(id => id !== 'knowledge' && id !== 'heartbeat' && !ids.includes(id))
|
|
]),
|
|
pinned: applicationNavigationSchema.shape.pinned.partial()
|
|
.default({})
|
|
.transform(pinned => ({ ...defaultApplicationNavigation.pinned, ...pinned }))
|
|
}).default(defaultApplicationNavigation),
|
|
version: z.literal(CURRENT_SETTINGS_VERSION),
|
|
lastSeenReleaseNotesVersion: releaseVersionSchema.nullable()
|
|
})
|
|
.strict()
|
|
|
|
const versionElevenStoredApplicationSettingsSchema = storedApplicationSettingsSchema.extend({ version: z.literal(11) })
|
|
|
|
type StoredApplicationSettings = z.infer<typeof storedApplicationSettingsSchema>
|
|
|
|
export type LegacyLocalToolEnvironmentPaths = {
|
|
nodeExecutablePath?: string
|
|
pythonExecutablePath?: string
|
|
}
|
|
|
|
export type LegacyLocalToolEnvironmentResolver =
|
|
() => Promise<LegacyLocalToolEnvironmentPaths>
|
|
|
|
async function resolveExecutable(
|
|
kind: 'node' | 'python',
|
|
binaryNames: readonly string[]
|
|
): Promise<string | undefined> {
|
|
const extensions =
|
|
process.platform === 'win32'
|
|
? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD')
|
|
.split(';')
|
|
.filter((extension) => /^\.[A-Za-z0-9]+$/u.test(extension))
|
|
: ['']
|
|
const directories = (
|
|
process.env.PATH ??
|
|
process.env.Path ??
|
|
process.env.path ??
|
|
''
|
|
)
|
|
.split(delimiter)
|
|
.map((directory) => directory.trim())
|
|
.filter(isAbsolute)
|
|
|
|
for (const directory of directories) {
|
|
for (const binaryName of binaryNames) {
|
|
const names =
|
|
process.platform === 'win32' && !extname(binaryName)
|
|
? extensions.map((extension) => `${binaryName}${extension}`)
|
|
: [binaryName]
|
|
for (const name of names) {
|
|
try {
|
|
const executablePath = await realpath(join(directory, name))
|
|
const metadata = await stat(executablePath)
|
|
if (
|
|
metadata.isFile() &&
|
|
localToolExecutablePathSchema.safeParse(executablePath).success
|
|
) {
|
|
const candidate = await inspectLocalToolExecutable(
|
|
kind,
|
|
executablePath,
|
|
{ baseEnvironment: process.env }
|
|
)
|
|
if (candidate) {
|
|
return candidate.executablePath
|
|
}
|
|
}
|
|
} catch {
|
|
// Continue through PATH candidates.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
export const resolveLegacyLocalToolEnvironmentPaths: LegacyLocalToolEnvironmentResolver =
|
|
async () => {
|
|
const [nodeExecutablePath, pythonExecutablePath] = await Promise.all([
|
|
resolveExecutable('node', ['node']),
|
|
resolveExecutable(
|
|
'python',
|
|
process.platform === 'win32'
|
|
? ['python', 'python3']
|
|
: ['python3', 'python']
|
|
)
|
|
])
|
|
return {
|
|
...(nodeExecutablePath ? { nodeExecutablePath } : {}),
|
|
...(pythonExecutablePath ? { pythonExecutablePath } : {})
|
|
}
|
|
}
|
|
|
|
export const defaultApplicationSettings: ApplicationSettings = {
|
|
checkUpdatesOnStartup: true,
|
|
desktopNotificationsEnabled: true,
|
|
transparentFrostedEffectEnabled: false,
|
|
updateSource: 'github',
|
|
modelDownloadSource: 'modelscope',
|
|
localToolEnvironment: defaultLocalToolEnvironmentSettings,
|
|
conversationHtmlRenderingEnabled: true,
|
|
remoteProjectsEnabled: false,
|
|
applicationNavigation: defaultApplicationNavigation,
|
|
localInferenceEnabled: true,
|
|
magicNotesEnabled: true,
|
|
heartbeatEnabled: false,
|
|
heartbeatReportTimeoutSeconds: defaultSupervisionTimeoutSeconds,
|
|
supervisorOrganizeTimeoutSeconds: defaultSupervisionTimeoutSeconds,
|
|
supervisorModelConcurrency: defaultSupervisorModelConcurrency,
|
|
magicNotesShowIncompleteTodoCount: true,
|
|
magicNoteCommentMode: 'immediate',
|
|
magicNoteCommentFormat: 'combined',
|
|
magicNoteCanvasPageCount: 1
|
|
}
|
|
|
|
export class ApplicationSettingsStore {
|
|
private settings?: StoredApplicationSettings
|
|
private settingsLoad?: Promise<StoredApplicationSettings>
|
|
private warnings: SettingsWarning[] = []
|
|
private updateQueue: Promise<void> = Promise.resolve()
|
|
private readonly listeners = new Set<(settings: ApplicationSettings) => void>()
|
|
|
|
constructor(
|
|
private readonly filePath: string,
|
|
private readonly legacyLocalToolEnvironmentResolver: LegacyLocalToolEnvironmentResolver = resolveLegacyLocalToolEnvironmentPaths
|
|
) {}
|
|
|
|
onChanged(listener: (settings: ApplicationSettings) => void): () => void {
|
|
this.listeners.add(listener)
|
|
return () => { this.listeners.delete(listener) }
|
|
}
|
|
|
|
private async migrateLegacy(
|
|
settings: Omit<
|
|
StoredApplicationSettings,
|
|
| 'conversationHtmlRenderingEnabled'
|
|
| 'desktopNotificationsEnabled'
|
|
| 'transparentFrostedEffectEnabled'
|
|
| 'localToolEnvironment'
|
|
| 'version'
|
|
| 'applicationNavigation'
|
|
| 'localInferenceEnabled'
|
|
| 'heartbeatEnabled'
|
|
| 'magicNoteCanvasPageCount'
|
|
> & {
|
|
conversationHtmlRenderingEnabled?: boolean
|
|
desktopNotificationsEnabled?: boolean
|
|
magicNoteCanvasPageCount?: number
|
|
version: number
|
|
}
|
|
): Promise<StoredApplicationSettings> {
|
|
const resolved = await this.legacyLocalToolEnvironmentResolver()
|
|
const selection = (
|
|
executablePath: string | undefined
|
|
): LocalToolEnvironmentSettings['node'] =>
|
|
executablePath &&
|
|
localToolExecutablePathSchema.safeParse(executablePath).success
|
|
? { source: 'custom', executablePath }
|
|
: { source: 'managed' }
|
|
const next: StoredApplicationSettings = {
|
|
...defaultApplicationSettings,
|
|
...settings,
|
|
conversationHtmlRenderingEnabled:
|
|
settings.conversationHtmlRenderingEnabled ?? true,
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
localToolEnvironment: {
|
|
node: selection(resolved.nodeExecutablePath),
|
|
python: selection(resolved.pythonExecutablePath),
|
|
artifactDownloadSource: 'native'
|
|
}
|
|
}
|
|
await this.persist(next)
|
|
return next
|
|
}
|
|
|
|
private async isolateCorruptFile(): Promise<void> {
|
|
await isolateCorruptSettingsFile(
|
|
this.filePath,
|
|
'Application settings are corrupt and could not be isolated'
|
|
)
|
|
}
|
|
|
|
private async loadStored(): Promise<StoredApplicationSettings> {
|
|
if (this.settings) {
|
|
return this.settings
|
|
}
|
|
if (!this.settingsLoad) {
|
|
this.settingsLoad = this.readStored().finally(() => {
|
|
this.settingsLoad = undefined
|
|
})
|
|
}
|
|
return this.settingsLoad
|
|
}
|
|
|
|
private async readStored(): Promise<StoredApplicationSettings> {
|
|
try {
|
|
const contents = await readFile(this.filePath, 'utf8')
|
|
let parsed: unknown
|
|
try {
|
|
parsed = JSON.parse(contents) as unknown
|
|
} catch {
|
|
await this.isolateCorruptFile()
|
|
this.warnings = [{ code: 'application-settings-recovered' }]
|
|
this.settings = {
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
lastSeenReleaseNotesVersion: null,
|
|
...defaultApplicationSettings
|
|
}
|
|
return this.settings
|
|
}
|
|
assertSupportedSettingsVersion(
|
|
parsed,
|
|
CURRENT_SETTINGS_VERSION,
|
|
(version) =>
|
|
`当前 GoodBuddy 不支持应用设置版本 ${version},请升级应用后重试`
|
|
)
|
|
const result = storedApplicationSettingsSchema.safeParse(parsed)
|
|
if (!result.success) {
|
|
const versionElevenResult = versionElevenStoredApplicationSettingsSchema.safeParse(parsed)
|
|
if (versionElevenResult.success) {
|
|
const next: StoredApplicationSettings = { ...versionElevenResult.data, version: CURRENT_SETTINGS_VERSION }
|
|
await this.persist(next)
|
|
return next
|
|
}
|
|
const versionTenResult =
|
|
versionTenStoredApplicationSettingsSchema.safeParse(parsed)
|
|
if (versionTenResult.success) {
|
|
const next: StoredApplicationSettings = {
|
|
...versionTenResult.data,
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
conversationHtmlRenderingEnabled: true
|
|
}
|
|
await this.persist(next)
|
|
return next
|
|
}
|
|
const versionNineResult =
|
|
versionNineStoredApplicationSettingsSchema.safeParse(parsed)
|
|
if (versionNineResult.success) {
|
|
return this.migrateLegacy(versionNineResult.data)
|
|
}
|
|
const versionEightResult =
|
|
versionEightStoredApplicationSettingsSchema.safeParse(parsed)
|
|
if (versionEightResult.success) {
|
|
return this.migrateLegacy({
|
|
...versionEightResult.data,
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
remoteProjectsEnabled: false
|
|
})
|
|
}
|
|
const versionSevenResult =
|
|
versionSevenStoredApplicationSettingsSchema.safeParse(parsed)
|
|
if (versionSevenResult.success) {
|
|
return this.migrateLegacy({
|
|
...versionSevenResult.data,
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
remoteProjectsEnabled: false,
|
|
magicNotesShowIncompleteTodoCount: true
|
|
})
|
|
}
|
|
const versionSixResult =
|
|
versionSixStoredApplicationSettingsSchema.safeParse(parsed)
|
|
if (versionSixResult.success) {
|
|
return this.migrateLegacy({
|
|
...versionSixResult.data,
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
modelDownloadSource: 'modelscope',
|
|
remoteProjectsEnabled: false,
|
|
magicNotesShowIncompleteTodoCount: true
|
|
})
|
|
}
|
|
const versionFiveResult =
|
|
versionFiveStoredApplicationSettingsSchema.safeParse(parsed)
|
|
if (versionFiveResult.success) {
|
|
return this.migrateLegacy({
|
|
...versionFiveResult.data,
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
updateSource: 'github',
|
|
modelDownloadSource: 'modelscope',
|
|
remoteProjectsEnabled: false,
|
|
magicNotesShowIncompleteTodoCount: true
|
|
})
|
|
}
|
|
const versionFourResult =
|
|
versionFourStoredApplicationSettingsSchema.safeParse(parsed)
|
|
if (versionFourResult.success) {
|
|
return this.migrateLegacy({
|
|
...versionFourResult.data,
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
updateSource: 'github',
|
|
modelDownloadSource: 'modelscope',
|
|
remoteProjectsEnabled: false,
|
|
magicNotesShowIncompleteTodoCount: true,
|
|
lastSeenReleaseNotesVersion: null
|
|
})
|
|
}
|
|
const versionThreeResult =
|
|
versionThreeStoredApplicationSettingsSchema.safeParse(parsed)
|
|
if (versionThreeResult.success) {
|
|
return this.migrateLegacy({
|
|
...versionThreeResult.data,
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
updateSource: 'github',
|
|
modelDownloadSource: 'modelscope',
|
|
remoteProjectsEnabled: false,
|
|
magicNotesShowIncompleteTodoCount: true,
|
|
magicNoteCommentFormat: 'combined',
|
|
lastSeenReleaseNotesVersion: null
|
|
})
|
|
}
|
|
const versionTwoResult =
|
|
versionTwoStoredApplicationSettingsSchema.safeParse(parsed)
|
|
if (versionTwoResult.success) {
|
|
return this.migrateLegacy({
|
|
...versionTwoResult.data,
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
updateSource: 'github',
|
|
modelDownloadSource: 'modelscope',
|
|
remoteProjectsEnabled: false,
|
|
magicNotesShowIncompleteTodoCount: true,
|
|
magicNoteCommentMode: 'immediate',
|
|
magicNoteCommentFormat: 'combined',
|
|
lastSeenReleaseNotesVersion: null
|
|
})
|
|
}
|
|
const legacyResult =
|
|
legacyStoredApplicationSettingsSchema.safeParse(parsed)
|
|
if (legacyResult.success) {
|
|
return this.migrateLegacy({
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
checkUpdatesOnStartup: legacyResult.data.checkUpdatesOnStartup,
|
|
updateSource: 'github',
|
|
modelDownloadSource: 'modelscope',
|
|
remoteProjectsEnabled: false,
|
|
magicNotesEnabled: true,
|
|
magicNotesShowIncompleteTodoCount: true,
|
|
magicNoteCommentMode: 'immediate',
|
|
magicNoteCommentFormat: 'combined',
|
|
lastSeenReleaseNotesVersion: null
|
|
})
|
|
}
|
|
await this.isolateCorruptFile()
|
|
this.warnings = [{ code: 'application-settings-recovered' }]
|
|
this.settings = {
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
lastSeenReleaseNotesVersion: null,
|
|
...defaultApplicationSettings
|
|
}
|
|
return this.settings
|
|
}
|
|
this.settings = result.data
|
|
} catch (error) {
|
|
if (error instanceof UnsupportedSettingsVersionError) {
|
|
throw error
|
|
}
|
|
if (!isMissingFileError(error)) {
|
|
throw new Error('Application settings could not be read', {
|
|
cause: error
|
|
})
|
|
}
|
|
this.settings = {
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
lastSeenReleaseNotesVersion: null,
|
|
...defaultApplicationSettings
|
|
}
|
|
}
|
|
return this.settings
|
|
}
|
|
|
|
async get(): Promise<ApplicationSettings> {
|
|
const stored = await this.loadStored()
|
|
return {
|
|
checkUpdatesOnStartup: stored.checkUpdatesOnStartup,
|
|
desktopNotificationsEnabled: stored.desktopNotificationsEnabled,
|
|
transparentFrostedEffectEnabled: stored.transparentFrostedEffectEnabled,
|
|
updateSource: stored.updateSource,
|
|
modelDownloadSource: stored.modelDownloadSource,
|
|
localToolEnvironment: stored.localToolEnvironment,
|
|
conversationHtmlRenderingEnabled:
|
|
stored.conversationHtmlRenderingEnabled,
|
|
remoteProjectsEnabled: stored.remoteProjectsEnabled,
|
|
applicationNavigation: stored.applicationNavigation,
|
|
localInferenceEnabled: stored.localInferenceEnabled,
|
|
magicNotesEnabled: stored.magicNotesEnabled,
|
|
heartbeatEnabled: stored.heartbeatEnabled ?? false,
|
|
heartbeatReportTimeoutSeconds: stored.heartbeatReportTimeoutSeconds ?? defaultSupervisionTimeoutSeconds,
|
|
supervisorOrganizeTimeoutSeconds: stored.supervisorOrganizeTimeoutSeconds ?? defaultSupervisionTimeoutSeconds,
|
|
supervisorModelConcurrency: stored.supervisorModelConcurrency ?? defaultSupervisorModelConcurrency,
|
|
supervisionReview: stored.supervisionReview,
|
|
magicNotesShowIncompleteTodoCount:
|
|
stored.magicNotesShowIncompleteTodoCount,
|
|
magicNoteCommentMode: stored.magicNoteCommentMode,
|
|
magicNoteCommentFormat: stored.magicNoteCommentFormat,
|
|
magicNoteCanvasPageCount: stored.magicNoteCanvasPageCount,
|
|
...(this.warnings.length > 0 ? { warnings: [...this.warnings] } : {})
|
|
}
|
|
}
|
|
|
|
async getLastSeenReleaseNotesVersion(): Promise<string | null> {
|
|
return (await this.loadStored()).lastSeenReleaseNotesVersion
|
|
}
|
|
|
|
private async persist(next: StoredApplicationSettings): Promise<void> {
|
|
await writeJsonFileAtomically(this.filePath, next)
|
|
this.settings = next
|
|
}
|
|
|
|
update(input: unknown): Promise<ApplicationSettings> {
|
|
const operation = this.updateQueue.then(async () => {
|
|
const updates = applicationSettingsUpdateSchema.parse(input)
|
|
const current = await this.loadStored()
|
|
const next: StoredApplicationSettings = {
|
|
...current,
|
|
...updates,
|
|
version: CURRENT_SETTINGS_VERSION
|
|
}
|
|
await this.persist(next)
|
|
this.warnings = []
|
|
const settings: ApplicationSettings = {
|
|
checkUpdatesOnStartup: next.checkUpdatesOnStartup,
|
|
desktopNotificationsEnabled: next.desktopNotificationsEnabled,
|
|
transparentFrostedEffectEnabled: next.transparentFrostedEffectEnabled,
|
|
updateSource: next.updateSource,
|
|
modelDownloadSource: next.modelDownloadSource,
|
|
localToolEnvironment: next.localToolEnvironment,
|
|
conversationHtmlRenderingEnabled:
|
|
next.conversationHtmlRenderingEnabled,
|
|
remoteProjectsEnabled: next.remoteProjectsEnabled,
|
|
applicationNavigation: next.applicationNavigation,
|
|
localInferenceEnabled: next.localInferenceEnabled,
|
|
magicNotesEnabled: next.magicNotesEnabled,
|
|
heartbeatEnabled: next.heartbeatEnabled ?? false,
|
|
heartbeatReportTimeoutSeconds: next.heartbeatReportTimeoutSeconds ?? defaultSupervisionTimeoutSeconds,
|
|
supervisorOrganizeTimeoutSeconds: next.supervisorOrganizeTimeoutSeconds ?? defaultSupervisionTimeoutSeconds,
|
|
supervisorModelConcurrency: next.supervisorModelConcurrency ?? defaultSupervisorModelConcurrency,
|
|
supervisionReview: next.supervisionReview,
|
|
magicNotesShowIncompleteTodoCount:
|
|
next.magicNotesShowIncompleteTodoCount,
|
|
magicNoteCommentMode: next.magicNoteCommentMode,
|
|
magicNoteCommentFormat: next.magicNoteCommentFormat,
|
|
magicNoteCanvasPageCount: next.magicNoteCanvasPageCount
|
|
}
|
|
for (const listener of this.listeners) listener(settings)
|
|
return settings
|
|
})
|
|
this.updateQueue = operation.then(
|
|
() => undefined,
|
|
() => undefined
|
|
)
|
|
return operation
|
|
}
|
|
|
|
setLastSeenReleaseNotesVersion(version: unknown): Promise<void> {
|
|
const operation = this.updateQueue.then(async () => {
|
|
const parsedVersion = releaseVersionSchema.parse(version)
|
|
const current = await this.loadStored()
|
|
if (current.lastSeenReleaseNotesVersion === parsedVersion) {
|
|
return
|
|
}
|
|
await this.persist({
|
|
...current,
|
|
version: CURRENT_SETTINGS_VERSION,
|
|
lastSeenReleaseNotesVersion: parsedVersion
|
|
})
|
|
})
|
|
this.updateQueue = operation.then(
|
|
() => undefined,
|
|
() => undefined
|
|
)
|
|
return operation
|
|
}
|
|
}
|