Files
ycode/scripts/rea/recover-personalization-panel.mjs
T
xiaohan.yuan 0dc088f1f7 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.
2026-10-17 10:46:40 +08:00

86 lines
4.5 KiB
JavaScript

// Recover only already-retained REA CSS; never access the installed application here.
// Source: REA evidence ev_af920edf33af5e811d58f05dad15210b29cd891f28e0fa74f6b18d63212241b1
// (WorkBuddy 5.8.3 设置 → 个性化), PersonalizationPanel-D16rRGjy.css.
import fs from 'node:fs/promises'
import { createHash } from 'node:crypto'
import postcss from 'postcss'
const sha = value => createHash('sha256').update(value).digest('hex')
const evidenceId = 'ev_af920edf33af5e811d58f05dad15210b29cd891f28e0fa74f6b18d63212241b1'
const evidence = JSON.parse(await fs.readFile(`.tmp/rea-css-evidence/${evidenceId}.json`, 'utf8'))
if (evidence.evidence_id !== evidenceId) throw new Error('REA identity mismatch')
const module = evidence.normalized_result.styles.items.find(item => /PersonalizationPanel-D16rRGjy\.css$/.test(item.file_path))
if (!module) throw new Error('Personalization panel module CSS is absent from the evidence')
const moduleCss = module.source.artifact.text
const moduleSha = sha(moduleCss)
if (moduleSha !== '318664f96253adace047d7b1d2d213fbf6139461c10d80ae12135998b7b98f3d') throw new Error('Personalization panel module CSS changed')
const tokens = await fs.readFile('.tmp/rea-css-recovered/tokens-Df-i-zsO.css', 'utf8')
const tokensSha = sha(tokens)
if (tokensSha !== '13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696') throw new Error('Tokens CSS changed')
const lightTokens = new Map()
const darkTokens = new Map()
for (const rule of postcss.parse(tokens).nodes) {
if (rule.type !== 'rule') continue
const selectors = rule.selectors ?? []
const dark = selectors.some(selector => selector.includes('[data-theme=dark]'))
const light = selectors.some(selector => selector.trim() === ':root')
if (!dark && !light) continue
for (const node of rule.nodes ?? []) {
if (node.type !== 'decl' || !node.prop.startsWith('--')) continue
if (dark) darkTokens.set(node.prop, node.value)
if (light) lightTokens.set(node.prop, node.value)
}
}
const relatedSelector = /\.(?:personalization-panel|local-memory)(?:[\w-]*)?(?![\w-])/i
const darkScope = /^\s*(?:\[data-theme=["']?dark["']?\]|body\[data-vscode-theme-name=["']IDE Night["']\]|body\.vscode-dark|\.vscode-dark|body\.dark|\.dark)(?=\s|$)/i
function project(node) {
if (node.type === 'rule') {
const selectors = []
for (const selector of node.selectors) {
if (!relatedSelector.test(selector)) continue
const mapped = selector.replace(darkScope, '[data-theme=dark]')
if (!selectors.includes(mapped)) selectors.push(mapped)
}
return selectors.length ? node.clone({ selector: selectors.join(',') }) : null
}
if (node.type === 'atrule' && node.nodes) {
const nodes = node.nodes.map(project).filter(Boolean)
return nodes.length ? node.clone({ nodes }) : null
}
return null
}
const root = postcss.root({ nodes: postcss.parse(moduleCss).nodes.map(project).filter(Boolean) })
const referenced = new Set()
const collect = text => {
for (const match of text.matchAll(/var\(\s*(--[\w-]+)/g)) referenced.add(match[1])
}
collect(root.toString())
for (let depth = 0; depth < 8; depth++) {
const before = referenced.size
for (const name of [...referenced]) {
for (const value of [lightTokens.get(name), darkTokens.get(name)]) {
if (typeof value === 'string') collect(value)
}
}
if (referenced.size === before) break
}
const missingLight = [...referenced].filter(name => !lightTokens.has(name))
if (missingLight.length) throw new Error(`Tokens missing from light root scope: ${missingLight.join(', ')}`)
const pick = map => new Map([...referenced].map(name => [name, map.get(name) ?? lightTokens.get(name)]))
const scope = (selector, map) => `${selector}{${[...map].map(([property, value]) => `${property}:${value}`).join(';')}}`
const scopedTokens = `${scope('.general-settings-panel.personalization-panel', pick(lightTokens))}\n${scope(':root[data-theme=dark] .general-settings-panel.personalization-panel', pick(darkTokens))}\n`
const css = `/* REA ${evidenceId}; PersonalizationPanel-D16rRGjy.css sha256=${moduleSha}; tokens sha256=${tokensSha}. */\n${root.toString()}\n${scopedTokens}`
await fs.writeFile('src/renderer/src/components/source-personalization-panel-recovered.css', css)
const rules = []
root.walkRules(rule => rules.push(rule.selector))
console.log(`Recovered ${rules.length} personalization rules (${Buffer.byteLength(css)} bytes); scoped ${pick(lightTokens).size} light / ${pick(darkTokens).size} dark tokens`)
console.log(rules.slice(0, 16).join('\n'))
if (rules.length < 30) throw new Error('Unexpectedly few personalization rules')