feat(settings): replicate the WorkBuddy agents panel and persist team-management preference (TODO 15)

This commit is contained in:
xiaohan.yuan committed 2026-10-17 12:58:25 +08:00
1 parent b3be45715e
commit 143516b3a8
33 files changed
+1071 -21

No files matched your search

+12 -5
View File
@@ -19,6 +19,8 @@ try {
const main={frame_id:'main',file_path:await fs.realpath(html)},frame={id:'main',url:page}
const png='iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jKXcAAAAASUVORK5CYII='
const bytes=Buffer.from(png,'base64'),digest=crypto.createHash('sha256').update(bytes).digest('hex')
const svgBytes=Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7"/></svg>')
const svgContent=svgBytes.toString('base64'),svgDigest=crypto.createHash('sha256').update(svgBytes).digest('hex')
const makeSnapshot=()=>({strings:[page,'main','HTML','IMG','class','selected','src','fallback.png',resourceUrl,'contenteditable','true','14px'],documents:[{
documentURL:0,frameId:1,scrollOffsetX:0,scrollOffsetY:0,
nodes:{nodeName:[2,3],parentIndex:[-1,0],backendNodeId:[10,20],attributes:[[],[4,5,6,7]],currentSourceURL:{index:[1],value:[8]}},
@@ -34,10 +36,10 @@ try {
if(method==='DOM.describeNode')return {node:{nodeName:mode==='not-img'?'DIV':'IMG',backendNodeId:mode==='foreign-backend'?999:20}}
if(method==='Page.getLayoutMetrics')return {cssLayoutViewport:{clientWidth:100,clientHeight:80,pageX:0,pageY:0}}
if(method==='DOM.getNodeForLocation')return {backendNodeId:mode==='covered'?999:mode==='partly-covered'&&params.y>20?999:20}
if(method==='Page.getResourceTree')return {frameTree:{frame,resources:mode==='not-loaded'?[]:[{url:selectedResourceUrl,type:'Image',mimeType:mode==='svg'?'image/svg+xml':'image/png',...(mode==='declared-oversize'?{contentSize:2097153}:{})}],childFrames:[{frame:{id:'foreign',url:'https://foreign.invalid'},resources:[{url:resourceUrl,type:'Image',mimeType:'image/png'}]}]}}
if(method==='Page.getResourceTree')return {frameTree:{frame,resources:mode==='not-loaded'?[]:[{url:selectedResourceUrl,type:'Image',mimeType:mode==='svg'?'image/svg+xml':mode==='unsupported'?'image/bmp':'image/png',...(mode==='declared-oversize'?{contentSize:2097153}:{})}],childFrames:[{frame:{id:'foreign',url:'https://foreign.invalid'},resources:[{url:resourceUrl,type:'Image',mimeType:'image/png'}]}]}}
if(method==='Page.getResourceContent'){
if(mode==='content-unavailable')throw Error('private-url-or-cookie-must-not-leak')
return {base64Encoded:mode!=='not-binary',content:mode==='invalid-base64'?'not-base64':mode==='signature'?'YWJj':mode==='oversize'?Buffer.alloc(2097153).toString('base64'):png}
return {base64Encoded:mode!=='not-binary',content:mode==='invalid-base64'?'not-base64':mode==='signature'?'YWJj':mode==='oversize'?Buffer.alloc(2097153).toString('base64'):mode==='svg'?svgContent:png}
}
if(method==='DOMSnapshot.captureSnapshot'){const copy=structuredClone(snapshot);for(const doc of copy.documents)doc.layout.styles=doc.layout.nodeIndex.map(()=>params.computedStyles.map(()=>11));return copy}
if(method==='Page.captureScreenshot')return {data:png}
@@ -58,12 +60,17 @@ try {
assert.equal(result.total_bytes,bytes.length)
assert.equal(calls.find(c=>c.method==='Page.getResourceContent').params.url,resourceUrl,'currentSrc wins over the fallback src attribute')
assert.equal(result.items[0].backend_node_id,20)
mode='svg';result=await capture()
assert.equal(result.items[0].status,'exported')
assert.equal(result.items[0].artifact.mime_type,'image/svg+xml')
assert.equal(result.items[0].artifact.sha256,svgDigest)
assert.equal(result.items[0].artifact.data_base64,svgContent)
mode='covered';result=await capture();assert.equal(result.items[0].status,'exported');assert.equal(result.items[0].visibility.status,'covered');assert.equal(result.items[0].selected_node.current_source_url,resourceUrl);
mode='partly-covered';assert.equal((await capture()).items[0].status,'exported','partial occlusion may still export actual loaded full image bytes')
for(const [test,reason] of [['absent','selector_absent'],['ambiguous','selector_ambiguous'],['not-img','selected_node_not_img'],['foreign-backend','snapshot_identity_unavailable'],['not-loaded','selected_source_not_loaded_in_main_frame'],['svg','unsupported_image_type'],['declared-oversize','asset_byte_budget_exceeded'],['oversize','asset_byte_budget_exceeded'],['signature','image_signature_mismatch'],['invalid-base64','invalid_base64'],['not-binary','binary_content_unavailable'],['content-unavailable','protocol_resource_unavailable'],['navigation-after-content','frame_changed']]){
for(const [test,reason] of [['absent','selector_absent'],['ambiguous','selector_ambiguous'],['not-img','selected_node_not_img'],['foreign-backend','snapshot_identity_unavailable'],['not-loaded','selected_source_not_loaded_in_main_frame'],['unsupported','unsupported_image_type'],['declared-oversize','asset_byte_budget_exceeded'],['oversize','asset_byte_budget_exceeded'],['signature','image_signature_mismatch'],['invalid-base64','invalid_base64'],['not-binary','binary_content_unavailable'],['content-unavailable','protocol_resource_unavailable'],['navigation-after-content','frame_changed']]){
mode=test;result=await capture();assert.equal(result.items[0].status,'unknown',test);assert.equal(result.items[0].reason,reason,test);assert.equal(result.total_bytes,0,test)
assert.equal(result.items[0].artifact,undefined,test)
if(['not-loaded','svg','declared-oversize'].includes(test))assert.equal(calls.some(c=>c.method==='Page.getResourceContent'),false,test+' must reject before the read')
if(['not-loaded','unsupported','declared-oversize'].includes(test))assert.equal(calls.some(c=>c.method==='Page.getResourceContent'),false,test+' must reject before the read')
assert.equal(JSON.stringify(result).includes('private-url-or-cookie'),false)
}
mode='ok';snapshot=makeSnapshot();snapshot.documents[0].nodes.attributes[0]=[9,10]
@@ -98,5 +105,5 @@ try {
assert.equal(optedOut.visuals.media_assets,undefined)
assert.equal(JSON.stringify(optedOut.visuals).includes('fallback.png'),false)
assert.equal(listener,undefined)
console.log('REA selected IMG resource regression passed: opt-in/schema/Evidence, snapshot currentSrc/backend attribution, covered bytes with separate visibility, scope, binary budgets/signatures, URL credential redaction, unknowns and default URL omission')
console.log('REA selected IMG resource regression passed: opt-in/schema/Evidence, snapshot currentSrc/backend attribution, raster/SVG export, covered bytes with separate visibility, scope, binary budgets/signatures, URL credential redaction, unknowns and default URL omission')
}finally{await fs.rm(temporary,{recursive:true,force:true})}
@@ -4,13 +4,15 @@ import { decodeCanonicalBase64 } from '../domain/webScreenshot.js';
const MAX_ASSETS=4, MAX_ASSET_BYTES=2*1024*1024, MAX_TOTAL_BYTES=8*1024*1024;
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
const types=new Set(['image/png','image/jpeg','image/gif','image/webp']);
const types=new Set(['image/png','image/jpeg','image/gif','image/webp','image/svg+xml']);
const mime=value=>value==='image/jpg'?'image/jpeg':value;
function sniff(bytes){
if(bytes.length>=24&&bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))return 'image/png';
if(bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255)return 'image/jpeg';
if(bytes.length>=10&&/^GIF8[79]a$/.test(bytes.toString('ascii',0,6)))return 'image/gif';
if(bytes.length>=12&&bytes.toString('ascii',0,4)==='RIFF'&&bytes.toString('ascii',8,12)==='WEBP')return 'image/webp';
const text=bytes.toString('utf8',0,Math.min(bytes.length,4096)).replace(/^\uFEFF/,'').trimStart();
if(/^(?:<\?xml[\s\S]*?\?>\s*)?<svg(?:\s|>)/i.test(text))return 'image/svg+xml';
}
function safeSourceUrl(value){
try{const u=new URL(value);if(!['file:','http:','https:'].includes(u.protocol))return null;u.username='';u.password='';u.search='';u.hash='';const result=u.href;return result.length<=2048?result:null;}catch{return null;}
@@ -22,7 +24,7 @@ export async function captureElectronMediaAssets(snapshot,frames,selectors,send)
const result={items:[],total_bytes:0,limits:{max_assets:MAX_ASSETS,max_asset_bytes:MAX_ASSET_BYTES,max_total_bytes:MAX_TOTAL_BYTES},limitations:[
'Opt-in IMG resources only: exact unique selectors, authorized main file frame and snapshot backend identity. Visibility is reported separately and does not authorize new requests.',
'Only Page.getResourceTree resources already loaded by the selected frame are read with Page.getResourceContent. No HTTP fetch, cookies, headers, input values or renderer JavaScript are used.',
'PNG/JPEG/GIF/WebP only; SVG, canvas, video, CSS backgrounds and resources missing from the loaded frame tree remain unknown. Covered loaded images can be exported; bytes do not establish that this image is the currently visible layer.',
'PNG/JPEG/GIF/WebP/SVG only; canvas, video, CSS backgrounds and resources missing from the loaded frame tree remain unknown. Covered loaded images can be exported; bytes do not establish that this image is the currently visible layer.',
'Resource URLs in metadata omit credentials, query strings and fragments; URL SHA-256 identifies the exact selected URL. DOM and resource observations are sequential.'
]};
const main=frames[0],strings=snapshot.strings??[],str=index=>strings[index]??'';
@@ -32,7 +32,7 @@ const mediaIdentity = {
};
export const electronMediaAssetsSchema = z.object({
items:z.array(z.discriminatedUnion('status',[
z.object({...mediaIdentity,status:z.literal('exported'),artifact:z.object({mime_type:z.enum(['image/png','image/jpeg','image/gif','image/webp']),bytes:z.number().int().positive().max(2097152),sha256:z.string().regex(/^[a-f0-9]{64}$/),data_base64:z.string().max(2796204)})}),
z.object({...mediaIdentity,status:z.literal('exported'),artifact:z.object({mime_type:z.enum(['image/png','image/jpeg','image/gif','image/webp','image/svg+xml']),bytes:z.number().int().positive().max(2097152),sha256:z.string().regex(/^[a-f0-9]{64}$/),data_base64:z.string().max(2796204)})}),
z.object({...mediaIdentity,status:z.literal('unknown'),reason:z.string()})
])).max(4),
total_bytes:z.number().int().min(0).max(8388608),
+1 -1
View File
@@ -135,7 +135,7 @@ await patch('dist/cli/electronCommands.js', text => once(text,
' include_script_sources: options.includeScriptSources,\n include_style_sources: options.includeStyleSources,\n include_visuals: options.includeVisuals,\n ...(options.visualProbe ? {visual_probe: JSON.parse(options.visualProbe)} : {}),'))
await patch('dist/contracts/javascript/electronToolContracts.js', text => once(text,
'Returns frames, DOM structure, resource paths, and scripts without evaluating renderer JavaScript or invoking Electron APIs.',
'Returns frames, DOM structure, resource paths, scripts and optional attributed CSS sources without evaluating renderer JavaScript or invoking Electron APIs. Default inspection is passive. include_style_sources selects CSS text; include_visuals selects viewport PNG and layout. Optional visual_probe declares native hover/click/scroll/key/viewport actions, sequential PNG samples, and actual platform fonts; UI actions can change application state and restoration is limited. visual_probe.media_selectors explicitly selects at most four unique main-frame IMG nodes and their already loaded PNG/JPEG/GIF/WebP resources, with byte budgets, SHA/base64 and visibility reported separately; no HTTP fetch is performed. These are local REA extensions; inspect coverage, budgets, restoration and limitations.'))
'Returns frames, DOM structure, resource paths, scripts and optional attributed CSS sources without evaluating renderer JavaScript or invoking Electron APIs. Default inspection is passive. include_style_sources selects CSS text; include_visuals selects viewport PNG and layout. Optional visual_probe declares native hover/click/scroll/key/viewport actions, sequential PNG samples, and actual platform fonts; UI actions can change application state and restoration is limited. visual_probe.media_selectors explicitly selects at most four unique main-frame IMG nodes and their already loaded PNG/JPEG/GIF/WebP/SVG resources, with byte budgets, SHA/base64 and visibility reported separately; no HTTP fetch is performed. These are local REA extensions; inspect coverage, budgets, restoration and limitations.'))
const styleModule = await fs.readFile(path.join(patchRoot, 'CdpElectronStyles.js'), 'utf8')
await fs.writeFile(path.join(output, 'dist/browser/CdpElectronStyles.js'), styleModule)
@@ -0,0 +1,12 @@
{
"include_fonts": false,
"include_message_content": false,
"media_selectors": [
"#settings-browser-extension-chrome img",
"#settings-browser-extension-edge img",
"#settings-browser-extension-qq-browser img"
],
"actions": [
{ "kind": "wait", "step_id": "observe-loaded-browser-extension-images", "duration_ms": 250, "frames": 1 }
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"include_fonts": false,
"include_message_content": false,
"track_selectors": [
".settings-navigation",
".settings-navigation__item",
".settings-navigation__item--active",
".settings-modal__panel"
],
"actions": [
{ "kind": "scroll", "step_id": "reveal-function-group-navigation-labels", "selector": ".settings-navigation", "delta_y": 500, "frames": 1 },
{ "kind": "wait", "step_id": "confirm-agents-navigation-label-and-order", "duration_ms": 400, "frames": 1 }
]
}
@@ -0,0 +1,14 @@
{
"include_fonts": false,
"include_message_content": false,
"track_selectors": [
".settings-navigation__item--active",
".settings-modal__panel",
".settings-navigation",
".settings-navigation__item"
],
"actions": [
{ "kind": "click", "step_id": "open-agents-by-observed-nav-index-7-in-dark-theme", "selector": ".settings-navigation__item", "index": 7, "frames": 1 },
{ "kind": "wait", "step_id": "verify-agents-dark-page-stable", "duration_ms": 800, "frames": 2 }
]
}
@@ -0,0 +1,14 @@
{
"include_fonts": false,
"include_message_content": false,
"track_selectors": [
".settings-navigation__item--active",
".settings-modal__panel",
".settings-navigation",
".settings-navigation__item"
],
"actions": [
{ "kind": "click", "step_id": "open-agents-by-observed-nav-index-7", "selector": ".settings-navigation__item", "index": 7, "frames": 1 },
{ "kind": "wait", "step_id": "verify-agents-page-stable", "duration_ms": 800, "frames": 4 }
]
}
@@ -0,0 +1,14 @@
{
"include_fonts": false,
"include_message_content": false,
"track_selectors": [
".settings-modal__panel",
".settings-navigation__item--active",
".appearance-card--light",
".appearance-card--dark"
],
"actions": [
{ "kind": "click", "step_id": "open-appearance-by-observed-nav-index-3-in-dark-theme", "selector": ".settings-navigation__item", "index": 3, "frames": 1 },
{ "kind": "wait", "step_id": "verify-dark-appearance-before-restoring-light", "duration_ms": 500, "frames": 1 }
]
}
@@ -0,0 +1,14 @@
{
"include_fonts": false,
"include_message_content": false,
"track_selectors": [
".settings-modal__panel",
".settings-navigation__item--active",
".appearance-card--light",
".appearance-card--dark"
],
"actions": [
{ "kind": "click", "step_id": "open-appearance-by-observed-nav-index-3", "selector": ".settings-navigation__item", "index": 3, "frames": 1 },
{ "kind": "wait", "step_id": "confirm-light-appearance-state", "duration_ms": 500, "frames": 1 }
]
}
@@ -0,0 +1,13 @@
{
"include_fonts": false,
"include_message_content": false,
"track_selectors": [
".settings-modal__panel",
".appearance-card--light",
".appearance-card--dark"
],
"actions": [
{ "kind": "click", "step_id": "restore-light-theme-after-agent-settings-sample", "selector": ".appearance-card--light", "index": 0, "frames": 1 },
{ "kind": "wait", "step_id": "wait-for-light-theme-render", "duration_ms": 800, "frames": 1 }
]
}
@@ -0,0 +1,16 @@
{
"include_fonts": false,
"include_message_content": false,
"track_selectors": [
".settings-modal__panel",
".general-settings-panel.extensions-settings-panel",
"#settings-agent-teams",
"#settings-browser-control"
],
"actions": [
{ "kind": "scroll", "step_id": "check-for-lower-agent-settings-content", "selector": ".settings-modal__panel", "delta_y": 700, "frames": 1 },
{ "kind": "wait", "step_id": "confirm-panel-content-end", "duration_ms": 250, "frames": 1 },
{ "kind": "scroll", "step_id": "return-agent-settings-to-top", "selector": ".settings-modal__panel", "delta_y": -700, "frames": 1 },
{ "kind": "wait", "step_id": "confirm-agent-settings-top", "duration_ms": 250, "frames": 1 }
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"include_fonts": false,
"include_message_content": false,
"track_selectors": [
".settings-modal__panel",
".settings-navigation__item--active",
".appearance-card--light",
".appearance-card--dark"
],
"actions": [
{ "kind": "click", "step_id": "select-dark-theme-for-agent-settings-sample", "selector": ".appearance-card--dark", "index": 0, "frames": 1 },
{ "kind": "wait", "step_id": "wait-for-dark-theme-render", "duration_ms": 800, "frames": 1 }
]
}
@@ -0,0 +1,15 @@
{
"include_fonts": false,
"include_message_content": false,
"track_selectors": [
".settings-modal__panel",
".wb-select",
".wb-select__value",
".wb-select__option"
],
"actions": [
{ "kind": "click", "step_id": "open-agent-team-setting-options-without-changing-value", "selector": ".wb-select", "index": 0, "frames": 1 },
{ "kind": "wait", "step_id": "observe-agent-team-setting-options", "duration_ms": 250, "frames": 1 },
{ "kind": "key", "step_id": "close-agent-team-setting-options-with-escape", "key": "Escape", "frames": 1 }
]
}
@@ -0,0 +1,73 @@
// Recover only the three browser-extension images retained by REA Evidence.
// This script reads the archived Evidence JSON and never accesses the WorkBuddy install.
import { createHash } from 'node:crypto'
import fs from 'node:fs/promises'
import path from 'node:path'
const evidenceId = 'ev_9542a063514b1a980665e504a89126c7a17f00b42f0c6dff9cead085a929f5c7'
const evidencePath = path.resolve(`.tmp/rea-css-evidence/${evidenceId}.json`)
const outputDirectory = path.resolve('src/renderer/src/components/agents-assets')
const expected = [
{
selector: '#settings-browser-extension-chrome img',
mimeType: 'image/svg+xml',
sha256: 'bde4af5a5d1150d3ec3108c29a5d746a00892196f3eaf1db4c4dc99aac7ba266',
output: 'workbuddy-browser-chrome.svg'
},
{
selector: '#settings-browser-extension-edge img',
mimeType: 'image/svg+xml',
sha256: '968cf9d827bfeaf47e4e53da55ec144bbc9565e5cb7a65cdb8dd0284fb3a5d11',
output: 'workbuddy-browser-edge.svg'
},
{
selector: '#settings-browser-extension-qq-browser img',
mimeType: 'image/png',
sha256: '186c82828c04b5bfe300c201ecd75cefb7984d97ea01bd491e0cfbdeb5df15a1',
output: 'workbuddy-browser-qq.png'
}
]
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex')
const evidence = JSON.parse(await fs.readFile(evidencePath, 'utf8'))
if (evidence.evidence_id !== evidenceId) throw new Error(`REA identity mismatch: ${evidence.evidence_id}`)
if (evidence.completeness?.truncated_sections?.length) throw new Error('REA Evidence is truncated')
if (!/renderer[\\/]index\.html$/i.test(evidence.normalized_result?.target?.file_path ?? '')) {
throw new Error('REA Evidence target is not the authorized WorkBuddy renderer')
}
const media = evidence.normalized_result?.visuals?.media_assets
if (!media?.items || media.items.length !== expected.length) throw new Error('Unexpected REA media item count')
const decoded = []
for (const item of expected) {
const matches = media.items.filter((candidate) => candidate.selector === item.selector)
if (matches.length !== 1) throw new Error(`Expected one REA item for ${item.selector}`)
const asset = matches[0]
if (asset.status !== 'exported' || asset.visibility?.status !== 'observed') {
throw new Error(`REA did not export a visible image for ${item.selector}: ${asset.reason}`)
}
if (asset.artifact?.mime_type !== item.mimeType || asset.artifact.sha256 !== item.sha256) {
throw new Error(`REA artifact identity changed for ${item.selector}`)
}
const bytes = Buffer.from(asset.artifact.data_base64, 'base64')
if (bytes.length !== asset.artifact.bytes || digest(bytes) !== item.sha256) {
throw new Error(`REA artifact bytes changed for ${item.selector}`)
}
if (item.mimeType === 'image/svg+xml') {
const source = bytes.toString('utf8')
if (!/^<svg\s/.test(source) || !/\bviewBox=['"]/i.test(source)) throw new Error(`Invalid SVG source for ${item.output}`)
if (/<script\b|<foreignObject\b|\b(?:href|xlink:href)=['"](?:https?:|file:|data:)/i.test(source)) {
throw new Error(`Unsafe external or active SVG content for ${item.output}`)
}
} else if (item.mimeType === 'image/png') {
if (bytes.subarray(0, 8).toString('hex') !== '89504e470d0a1a0a') throw new Error(`Invalid PNG source for ${item.output}`)
if (bytes.readUInt32BE(16) !== 128 || bytes.readUInt32BE(20) !== 128) throw new Error('QQ browser icon is not 128x128')
}
decoded.push({ bytes, output: item.output, sha256: item.sha256 })
}
await fs.mkdir(outputDirectory, { recursive: true })
for (const asset of decoded) await fs.writeFile(path.join(outputDirectory, asset.output), asset.bytes)
console.log(JSON.stringify({ evidenceId, outputDirectory, assets: decoded.map(({ output, sha256, bytes }) => ({
output, sha256, bytes: bytes.length
})) }, null, 2))
+123
View File
@@ -0,0 +1,123 @@
// Recover only retained REA CSS; never access the installed application here.
// Sources: REA light/dark Evidence for WorkBuddy 5.8.3 Settings -> Agents.
// The lazy module contains the browser-extension status rules for this page.
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 evidenceIds = {
light: 'ev_1cc896e04c0cc9ceab39b0a252f4982c019305609195ad44804ae9b5fb7379a8',
dark: 'ev_3384aa03af8a1ccdffb8e025dbd4a354636d8d6b0c207abd21e12fc5eb2ae719'
}
const expectedModuleSha = 'cbd4b37c6061381943d96ca454176b2f083e49eb8e04c34cb5d31eef3dbcb8f5'
const expectedTokensSha = '13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696'
const moduleName = 'ExtensionsSettingsPanel-BnsXh3oa.css'
const loadModule = async evidenceId => {
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: ${evidenceId}`)
const module = evidence.normalized_result.styles.items.find(item => item.file_path.endsWith(moduleName))
if (!module) throw new Error(`Agent settings module CSS is absent from ${evidenceId}`)
const moduleSha = sha(module.source.artifact.text)
if (moduleSha !== expectedModuleSha || module.source.artifact.sha256 !== expectedModuleSha) {
throw new Error(`Agent settings module CSS changed in ${evidenceId}`)
}
return { filePath: module.file_path, css: module.source.artifact.text, sha: moduleSha }
}
const lightModule = await loadModule(evidenceIds.light)
const darkModule = await loadModule(evidenceIds.dark)
if (lightModule.filePath !== darkModule.filePath) throw new Error('Agent settings module path changed across captures')
const tokens = await fs.readFile('.tmp/rea-css-recovered/tokens-Df-i-zsO.css', 'utf8')
const tokensSha = sha(tokens)
if (tokensSha !== expectedTokensSha) 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 = /\.extensions-settings-panel__browser-extension-status(?:[\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, mode = 'all') {
if (node.type === 'rule') {
const selectors = []
for (const selector of node.selectors) {
if (!relatedSelector.test(selector)) continue
const isDark = darkScope.test(selector)
if (mode === 'shared' && isDark) continue
if (mode === 'dark' && !isDark) continue
const mapped = isDark ? selector.replace(darkScope, '[data-theme=dark]') : selector
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(child => project(child, mode)).filter(Boolean)
return nodes.length ? node.clone({ nodes }) : null
}
return null
}
const sharedRoot = css => postcss.root({ nodes: postcss.parse(css).nodes.map(node => project(node, 'shared')).filter(Boolean) })
const lightRoot = sharedRoot(lightModule.css)
const darkSharedRoot = sharedRoot(darkModule.css)
const darkSpecificRoot = postcss.root({ nodes: postcss.parse(darkModule.css).nodes.map(node => project(node, 'dark')).filter(Boolean) })
const normalizeRules = root => {
const rules = []
root.walkRules(rule => {
const selectors = rule.selectors.map(selector => selector.replace(/\s+/g, ' ').trim()).join(',')
const declarations = (rule.nodes ?? [])
.filter(node => node.type === 'decl')
.map(node => `${node.prop}:${node.value.replace(/\b0px\b/g, '0').replace(/\s+/g, ' ').trim()}`)
.join(';')
rules.push(`${selectors}{${declarations}}`)
})
return rules.join('\n')
}
if (normalizeRules(lightRoot) !== normalizeRules(darkSharedRoot)) throw new Error('Light/dark agent module rules differ beyond serialization')
const root = postcss.root({ nodes: [...lightRoot.nodes, ...darkSpecificRoot.nodes] })
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('.extensions-settings-panel', pick(lightTokens))}\n${scope('[data-theme=dark] .extensions-settings-panel', pick(darkTokens))}\n`
const evidenceList = Object.entries(evidenceIds).map(([name, evidenceId]) => `${name}=${evidenceId}`).join('; ')
const css = `/* REA ${evidenceList}; ${moduleName} sha256=${lightModule.sha}; tokens sha256=${tokensSha}. */\n${root.toString()}\n${scopedTokens}`
await fs.writeFile('src/renderer/src/components/source-agents-panels-recovered.css', css)
const rules = []
root.walkRules(rule => rules.push(rule.selector))
const darkRules = []
darkSpecificRoot.walkRules(rule => darkRules.push(rule.selector))
console.log(`Recovered ${rules.length} agent settings rules (${Buffer.byteLength(css)} bytes); ${darkRules.length} dark-specific module rules; scoped ${pick(lightTokens).size} light / ${pick(darkTokens).size} dark tokens`)
console.log(rules.join('\n'))
if (rules.length !== 4) throw new Error(`Expected 4 agent settings module rules, found ${rules.length}`)