// 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))