84 lines
5.8 KiB
JavaScript
84 lines
5.8 KiB
JavaScript
// Project rules from already-exported REA CSS. Never accesses WorkBuddy.
|
|
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import crypto from 'node:crypto'
|
|
import postcss from 'postcss'
|
|
|
|
const directory = path.resolve('.tmp/rea-css-recovered')
|
|
const manifest = JSON.parse(await fs.readFile(path.join(directory, 'manifest.json'), 'utf8'))
|
|
const destination = path.resolve('src/renderer/src/workbuddy/conversation/source-styles.css')
|
|
const families = /\.(?:cr-(?:frame|agent|self-|user-content|collapse|reasoning|tool-exp|tool-head|message-list|document|toolbar|icon-button|text-block)|cb-plan-|cb-exploration-status|conversation-timeline|conversation-high-credit-approval)/
|
|
const output = postcss.root()
|
|
const selected = []
|
|
for (const filename of ['src-CBaV_A3S.css', 'src-CyXHA0IC.css', 'contexts-BkjwYSZT.css']) {
|
|
const metadata = manifest.files.find(file => file.file === filename)
|
|
if (!metadata) throw new Error(`REA CSS not captured: ${filename}`)
|
|
const bytes = await fs.readFile(path.join(directory, filename))
|
|
if (crypto.createHash('sha256').update(bytes).digest('hex') !== metadata.sha256) throw new Error(`REA CSS digest mismatch: ${filename}`)
|
|
const source = postcss.parse(bytes, { from: filename })
|
|
let rules = 0
|
|
function project(node) {
|
|
if (node.type === 'rule') {
|
|
const selectors = node.selectors ?? []
|
|
const actionMap = { '._assistantFeedback_1jhpu_1': '.msg--assistant .wb-message-actions', '._copyButton_1ncbp_1': '.wb-message-action[data-action=copy]', '._ratingButton_13zum_7': '.wb-message-action[aria-pressed]', '._actionButton_jnle9_7': '.wb-message-action[data-action=regenerate]', '._iconOnly_jnle9_27': '.wb-message-action' }
|
|
const actionSelectors = selectors.filter(selector => /\._(?:assistantFeedback_1jhpu_1|copyButton_1ncbp_1|ratingButton_13zum_7|actionButton_jnle9_7)/.test(selector)).map(selector => {
|
|
for (const [from, to] of Object.entries(actionMap)) selector = selector.replaceAll(from, to)
|
|
return selector
|
|
})
|
|
if (actionSelectors.length) { rules++; return node.clone({ selector: actionSelectors.join(',') }) }
|
|
const userProjection = {
|
|
'._editable_1cxd2_1': '.chat-pane .yc-user-editable',
|
|
'._editable_1cxd2_1 *': '.chat-pane .yc-user-editable *',
|
|
'._editable_1cxd2_1 p': '.chat-pane .yc-user-editable p',
|
|
'._editable_1cxd2_1 span': '.chat-pane .yc-user-editable span',
|
|
'._editable_1cxd2_1 [data-slate-node=text]': '.chat-pane .yc-user-leaf',
|
|
'._editable_1cxd2_1[contenteditable=false] p': '.chat-pane .yc-user-editable p',
|
|
'._editable_1cxd2_1[contenteditable=false] [data-slate-node=text]': '.chat-pane .yc-user-leaf',
|
|
'._editable_1cxd2_1[contenteditable=false] [data-slate-leaf=true]': '.chat-pane .yc-user-leaf > span',
|
|
'._editable_1cxd2_1[contenteditable=false] [data-slate-string=true]': '.chat-pane .yc-user-leaf > span',
|
|
}
|
|
const projected = selectors.filter(selector => userProjection[selector]).map(selector => userProjection[selector])
|
|
if (projected.length) { rules++; return node.clone({ selector: [...new Set(projected)].join(',') }) }
|
|
let theme
|
|
if (selectors.includes(':root')) theme = '.chat-pane.cr-theme'
|
|
else if (selectors.includes('.light') || selectors.includes(':root.light') || selectors.includes('.cr-theme[data-cr-theme=light]')) theme = '.chat-pane.cr-theme[data-cr-theme=light]'
|
|
else if (selectors.includes('.dark') || selectors.includes(':root.dark') || selectors.includes('.cr-theme[data-cr-theme=dark]')) theme = '.chat-pane.cr-theme[data-cr-theme=dark]'
|
|
if (theme) {
|
|
const declarations = node.nodes.filter(d => d.type === 'decl' && d.prop.startsWith('--') && !/^--(?:wb-font-(?:size|line-height)-|cb-md-root$)/.test(d.prop))
|
|
if (!declarations.length) return
|
|
rules++
|
|
return postcss.rule({ selector: theme, nodes: declarations.map(d => d.clone()) })
|
|
}
|
|
const relevant = selectors.filter(selector => families.test(selector) && !/hydration|virtual-|share-mode|loading-more/.test(selector))
|
|
if (!relevant.length) return
|
|
rules++
|
|
return node.clone({ selector: relevant.join(',') })
|
|
}
|
|
if (node.type === 'atrule' && /keyframes$/.test(node.name)) return
|
|
if (node.type === 'atrule' && node.nodes) {
|
|
const children = node.nodes.map(project).filter(Boolean)
|
|
if (children.length) return node.clone({ nodes: children })
|
|
}
|
|
}
|
|
for (const node of source.nodes) {
|
|
const result = project(node)
|
|
if (result) output.append(result)
|
|
}
|
|
const animations = []
|
|
output.walkDecls(d => { if (/^animation(?:-name)?$/.test(d.prop)) animations.push(d.value) })
|
|
source.walkAtRules(/keyframes$/, node => {
|
|
if (animations.some(value => value.split(/[\s,]+/).includes(node.params))) output.append(node.clone())
|
|
})
|
|
selected.push({ file: filename, sha256: metadata.sha256, asarComparison: metadata.asarComparison?.status ?? 'not-compared', rules })
|
|
}
|
|
const header = `/* Generated by recover-conversation-styles.mjs from REA ${manifest.sourceEvidenceId}.
|
|
* Original selected rule bodies and conditional ancestors are preserved.
|
|
* Readonly user-editor selectors are projected onto static paragraph/leaf wrappers.
|
|
* Global theme declarations are scoped to .chat-pane; font-size controls remain inherited.
|
|
* Runtime/ASAR mismatches remain recorded in the source manifest.
|
|
*/\n`
|
|
await fs.writeFile(destination, header + output.toString() + '\n')
|
|
const record = { evidenceId: manifest.sourceEvidenceId, selected, destination, transformations: ['Theme selector scope only; custom-property values preserved.', 'Only listed conversation families selected; unimplemented virtualization excluded.', 'Conditional at-rule ancestors and animation keyframes retained.'] }
|
|
await fs.writeFile(path.join(directory, 'conversation-projection.json'), JSON.stringify(record, null, 2) + '\n')
|
|
console.log(JSON.stringify(record, null, 2))
|