feat(settings): replicate the WorkBuddy personalization panel and wire identity, tone and prompts (TODO 13)

Recover the WorkBuddy 5.8.3 个性化 page from REA evidence (PersonalizationPanel-D16rRGjy.css + tokens): 45 module rules plus scoped light/dark tokens, the five-card 1160px layout (identity card 638x100 with inline editors, reply preference card 638x204, three 638x264 long-text cards with 598x160 previews) and the 640x520 editor modal.

Persist identity, tone, welcome tips, custom instructions, persona and long-term memory in config.json, expose personalization:get/update over IPC, and append the resulting personalization context to each turn's prompt for both the OMP and built-in kernels. Wire 欢迎语 to the streaming loading tips and keep 显示文件修改过程 visibly disabled because YCode has no incremental file-change renderer yet.

Verify with scripts/verify-personalization-settings.cjs: 29 assertions covering geometry, tone options, switches, inline identity editing with blur-discard, editor modal save/cancel, config persistence, reopen, dark surfaces and no renderer errors. Add a pure prompt-builder regression and keep every existing settings regression green.
This commit is contained in:
xiaohan.yuan committed 2026-10-17 10:46:40 +08:00
1 parent 19460f0b13
commit 0dc088f1f7
54 files changed
+2038 -248

No files matched your search

+14 -1
View File
@@ -10,7 +10,7 @@
import { app, safeStorage } from 'electron'
import { promises as fs } from 'node:fs'
import { join } from 'node:path'
import type { GatewayConfig, GatewayConfigView } from '@shared/types'
import type { GatewayConfig, GatewayConfigView, PersonalizationSettings } from '@shared/types'
interface StoredConfig {
speechBaseUrl?: string
@@ -26,6 +26,8 @@ interface StoredConfig {
preventSleep?: boolean
/** 开机自动启动(通用设置;默认关闭)。 */
autoLaunch?: boolean
/** 设置 → 个性化(身份、回复风格、指令、人设、长期记忆)。 */
personalization?: Partial<PersonalizationSettings>
}
const DEFAULTS: StoredConfig = {
@@ -118,6 +120,17 @@ export async function setAutoLaunch(enabled: boolean): Promise<void> {
await persist({ ...stored, autoLaunch: enabled })
}
/** 设置 → 个性化的原始持久化值(缺省字段由 personalization.ts 补齐)。 */
export async function getStoredPersonalization(): Promise<Partial<PersonalizationSettings>> {
const stored = await load()
return stored.personalization ?? {}
}
export async function setStoredPersonalization(value: Partial<PersonalizationSettings>): Promise<void> {
const stored = await load()
await persist({ ...stored, personalization: { ...(stored.personalization ?? {}), ...value } })
}
/** 供渲染进程使用:脱敏视图 */
export async function getConfigView(): Promise<GatewayConfigView> {
const stored = await load()
+7 -2
View File
@@ -11,7 +11,7 @@ import { BrowserWindow, app, dialog, ipcMain, shell, type OpenDialogOptions } fr
import { randomUUID } from 'node:crypto'
import { unlink, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import type { ChatEvent, ChatMessage, GitBranchInfo, WindowState } from '@shared/types'
import type { ChatEvent, ChatMessage, GitBranchInfo, PersonalizationPatch, WindowState } from '@shared/types'
import { freezeAssistantModelMetadata } from '@shared/assistant-model'
import type { ConfigPatchLike, EditAction, SendPayload, WorkspaceAttachResult } from '@shared/ipc'
import { applyAssistantEvent } from '@shared/message-parts'
@@ -34,6 +34,7 @@ import { assistantWireHistory } from './agent/history'
import * as models from './models'
import * as workspaces from './workspaces'
import * as generalSettings from './general-settings'
import * as personalization from './personalization'
import { prepareOmpModel } from './omp-model'
import type { CustomModelInput, ResolvedModel } from '@shared/models'
import type { SessionContext } from '@shared/workspaces'
@@ -408,6 +409,8 @@ export function registerIpc(getWindow: () => BrowserWindow | null): void {
}
}
ipcMain.handle('general:get', (_event, options?: { refreshUsage?: boolean }) => generalView(options?.refreshUsage === true))
ipcMain.handle('personalization:get', () => personalization.getPersonalization())
ipcMain.handle('personalization:update', (_event, patch: PersonalizationPatch) => personalization.updatePersonalization(patch))
ipcMain.handle('general:update', async (_event, patch: { autoLaunch?: boolean; preventSleep?: boolean }) => {
if (patch.preventSleep !== undefined) {
await config.setPreventSleep(patch.preventSleep)
@@ -740,7 +743,9 @@ export function registerIpc(getWindow: () => BrowserWindow | null): void {
const interactionBroker = createInteractionBroker(runId, controller.signal, emit)
// 对话中引用的内容块(原版 message-quote)随消息一起交给模型;表格 markdown 原样附带。
const quoteText = (payload.quotes ?? []).map((quote) => quote.markdown.trim()).filter(Boolean).join('\n\n')
const promptText = [text, quoteText, promptAttachments.text, references.length ? `引用工作区资源:\n${references.join('\n')}` : '', capabilitySnapshot.context].filter(Boolean).join('\n\n')
// 设置 → 个性化的身份/风格/指令/人设/长期记忆随每轮请求发送,OMP 与自建循环共用同一段文本。
const personalizationPrompt = personalization.buildPersonalizationPrompt(await personalization.getPersonalization())
const promptText = [text, quoteText, promptAttachments.text, references.length ? `引用工作区资源:\n${references.join('\n')}` : '', capabilitySnapshot.context, personalizationPrompt].filter(Boolean).join('\n\n')
// 回合在后台推进,立刻把 runId 还给渲染进程
const initialMessages = payload.action ? structuredClone([...history, userMessage, assistant]) : undefined
+28
View File
@@ -0,0 +1,28 @@
/**
* 设置 → 个性化(TODO 13)的持久化与运行链路。
*
* REA 依据(WorkBuddy 5.8.3,设置窗口 900×680):
* - ev_af920edf33af5e811d58f05dad15210b29cd891f28e0fa74f6b18d63212241b1(浅色顶部 DOM/layout/computed style + 模块 CSS)
* - ev_88af0774005c02e9c75284ade2be45de4943ee5aaf73fe5b13ac8a38b85be786(深色同页)
* - ev_4f4f76054a930a186199ee28a5f0169f7de48a05a295b8e9eb630c796f07be4a(最终浅色顶部)
*
* 选项表、默认人设与个性化上下文拼装放在 @shared/personalization(纯函数,可离线回归);
* 本模块只负责读写 config.json 的 personalization 字段。
*/
import type { PersonalizationPatch, PersonalizationSettings } from '@shared/types'
import { normalizePersonalization } from '@shared/personalization'
import { getStoredPersonalization, setStoredPersonalization } from './config'
export { buildPersonalizationPrompt, DEFAULT_PERSONA, DEFAULT_PERSONALIZATION, TONE_OPTIONS } from '@shared/personalization'
export { normalizePersonalization } from '@shared/personalization'
export async function getPersonalization(): Promise<PersonalizationSettings> {
return normalizePersonalization(await getStoredPersonalization())
}
export async function updatePersonalization(patch: PersonalizationPatch): Promise<PersonalizationSettings> {
const current = await getPersonalization()
const next = normalizePersonalization({ ...current, ...patch })
await setStoredPersonalization(next)
return next
}