Files
ycode/scripts/rea/recover-composer-add-menu.mjs
T

95 lines
18 KiB
JavaScript

/** Recovery from fixed retained REA only; no original filesystem or runtime access. */
import fs from 'node:fs'
import crypto from 'node:crypto'
import {parse} from '@babel/parser'
import postcss from 'postcss'
const sha=value=>crypto.createHash('sha256').update(value).digest('hex'),archive=JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json','utf8')),cssManifest=JSON.parse(fs.readFileSync('.tmp/rea-css-recovered/manifest.json','utf8'))
const manifest={version:'5.8.2',rootSha256:'035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918',evidence:[archive.evidence_id,cssManifest.sourceEvidenceId],sources:[],bodies:[],adaptations:['Source AddMenuImpl parent, Popover, SubMenu, menu items, SVGs and tooltip bodies recovered with React/JSX singleton bindings.','Original provider-specific submenu content is replaced at one explicit boundary by typed host slots; entries/actions/availability are host data. No unavailable provider or service is simulated.','Host open state supports controlled open/onOpenChange; scopeKey unmount isolates owner. Source parent opening/closing and submenu state remain in the recovered body.','Default trigger retains source WBButton body and adds wb-composer__add-button plus aria-label 添加 for existing YCode automation.','MenuItem adds only data-add-menu-id and optional disabled metadata. Parent keyboard traversal/focus-first is intentionally absent as in current cr source; SubMenu arrow/Enter/touch/delays retain original source.','FloatingUI uses the existing pinned YCode host dependency; bundled source version is unknown. Portals/CSS are scoped; light theme selectors also map YCode data-theme.']}
function index(name,pin){const source=archive.items.find(item=>item.file_path.endsWith(name))?.source.artifact;if(!source||source.sha256!==pin||sha(source.text)!==pin)throw Error('REA source identity mismatch '+name);manifest.sources.push({file:name,sha256:pin});const nodes=new Map(),ast=parse(source.text,{sourceType:'module'});function visit(node){if(!node||typeof node!=='object')return;if(node.type==='FunctionDeclaration')nodes.set(node.id.name,node);if(node.type==='AssignmentExpression'&&node.left.type==='Identifier')nodes.set(node.left.name,node);for(const[key,value]of Object.entries(node)){if(key==='loc'||key==='comments')continue;if(Array.isArray(value))value.forEach(visit);else if(value&&typeof value==='object')visit(value)}}visit(ast.program);return{...source,nodes,ast}}
const core=index('src-DbE1_mgM.js','6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4'),button=index('src-DPDj7_qD.js','747e41d215375e1214c366213aea7e077d85421923029c61cbb6e9562be58de0')
function body(source,name){const node=source.nodes.get(name);if(!node)throw Error('Missing body '+name);const original=source.text.slice(node.start,node.end);manifest.bodies.push({file:manifest.sources.find(item=>item.sha256===source.sha256).file,name,start:node.start,end:node.end,sha256:sha(original)});return(node.type==='AssignmentExpression'?'const ':'')+original.replace(/\(0,[\w$]+\.(jsx|jsxs)\)/g,' JSX.$1')+';'}
const labels={};function visitLabels(node){if(!node||typeof node!=='object')return;if(node.type==='ObjectProperty'&&/^(?:chatInput\.(?:addMenu\.|attachment\.|mode\.|triggerSearch\.loading)|common\.(?:search|clearSearch)$)/.test(node.key.value??'')){const value=node.value.value??node.value.quasis?.[0]?.value.cooked;if(typeof value==='string'&&/[\u3400-\u9fff]/.test(value)&&!labels[node.key.value])labels[node.key.value]=value}for(const[key,value]of Object.entries(node)){if(key==='loc'||key==='comments')continue;if(Array.isArray(value))value.forEach(visitLabels);else if(value&&typeof value==='object')visitLabels(value)}}visitLabels(core.ast.program);if(!labels['chatInput.addMenu.title']||!labels['chatInput.mode.plan.label'])throw Error('Missing source labels')
let js=`import * as React from 'react';import * as JSX from 'react/jsx-runtime';import{useFloating,offset as D,flip as E,shift as O,size as b,arrow as k,autoUpdate as A,useInteractions,useDismiss,useHover,useTransitionStyles,FloatingPortal as NativeFloatingPortal}from'@floating-ui/react';
import{useInputProvider,useInputBoxStore,useInputBoxSelector,useAddMenuItems,useRenderHostSubmenu,useHostOpenState,useCrConfig,useRenderIcon}from'./runtime';import{SourceAddMenuConfigContext}from'./context';
const pU=React,Gi=React,Vi=React,la=React,Ei=React,mi=React,ci=React,Gt=React,Di=JSX,ua=JSX,pUFragment=React.Fragment,io={1:12,2:16},li=9990,ui=10,di='default',fi='__ycodeComposerAddMenuLayers__',gi=3,_i=8,yi=4,bi=1000,xi=.1,Si='data-pointer-press-transparent',wi=new Set(['SVG','IMG','VIDEO','CANVAS','AUDIO']),Oi=new Map(),qi=React.createContext(null),$i=React.createContext(true),pa=React.createContext(null),yt=SourceAddMenuConfigContext,da='textarea,input:not([type]),input[type=text],input[type=search],input[type=email],input[type=url],input[type=tel],input[type=password],input[type=number],[contenteditable=true],[contenteditable=plaintext-only]',fa='[data-cr-sub-menu-overlay=true],[data-cr-submenu-overlay=true]',zi='[data-cr-submenu-overlay=true],.cr-sub-menu__overlay',Ui='[data-floating-ui-inert]';let Ci=null;
const usePrefixCls=()=>name=>'cr-'+name,useT=()=>key=>(${JSON.stringify(labels)})[key]??key,useRenderDiag=()=>{},FloatingPortal=({children,root})=>JSX.jsx(NativeFloatingPortal,{root,children:JSX.jsx('div',{className:'yc-source-composer-add-menu','data-cr-theme':children?.props?.['data-cr-theme'],children})}),floatingAutoUpdateWithAnimationFrame=(n,a,c)=>A(n,a,c,{animationFrame:true});
`
// Tooltip is the same cr implementation used by this AddMenu, not the unrelated legacy tooltip.
for(const name of ['r$1','clsx','getFloatingLayerState','getGroupStack','nextZIndex','useFloatingLayer','TooltipLayer','isPressTransparent','isHoverSuppressedByPress','suppressHoverUntilPointerMove','isElementTruncated','isSubtreeTruncated','normalizeVisibleText','collectVisibleText','hasVisibleTriggerText','hasTruncationStyle','shouldGateOnTruncation','isEmptyContent','resolveTooltipThemeMode','isVisibleFocus','Tooltip$1','resolvePopoverWhileElementsMounted','getNearestStackingZIndex','PopoverLayer','usePopoverContext','Popover','PopoverTrigger','PopoverContent','Menu','MenuItem','SubMenu','MenuDivider','ChevronRightIcon','AddFilesIcon','ReferenceFilesIcon','ModeIcon','ExpertsIcon','SkillsIcon','ConnectorsIcon','PlusIcon'])js+=body(core,name)+'\n'
js+=body(button,'ButtonLoadingIcon')+'\n'+body(button,'qt')+'\nconst fe=qt;\n'
let parent=body(core,'AddMenuImpl')
const slotsStart=parent.indexOf('return w=a&&_?'),slotsEnd=parent.indexOf(',(0,mU.jsxs)(pU.Fragment',slotsStart)
// JSX aliases have already been normalized by body().
const normalizedEnd=parent.indexOf(', JSX.jsxs(pU.Fragment',slotsStart)
const boundaryEnd=normalizedEnd>=0?normalizedEnd:slotsEnd
if(slotsStart<0||boundaryEnd<slotsStart)throw Error('Provider submenu source boundary changed')
parent=parent.slice(0,slotsStart)+'return w=renderHostSubmenu(n.id,{closeMenu:()=>k(!1)},d,handleSubmenuItemClick)'+parent.slice(boundaryEnd)
if(!parent.includes('[C,w]=(0,pU.useState)(!1)'))throw Error('Open state source boundary changed')
parent=parent.replace('[C,w]=(0,pU.useState)(!1)','[C,w]=useHostOpenState()')
parent=parent.replace('let f=usePrefixCls(),p=useT()','let renderHostSubmenu=useRenderHostSubmenu(),f=usePrefixCls(),p=useT()')
parent=parent.replace('className:clsx(f(`add-menu__trigger`),C&&f(`add-menu__trigger--open`))','className:clsx(f(`add-menu__trigger`),C&&f(`add-menu__trigger--open`),`wb-composer__add-button`)').replace('"aria-label":p(`chatInput.addMenu.title`)','"aria-label":`添加`')
const row='icon:n.icon,onClick:()=>handleItemClick(n),children:n.label'
if(parent.split(row).length!==2)throw Error('Parent row metadata boundary changed')
parent=parent.replaceAll(row,'icon:n.icon,disabled:n.disabled,itemId:n.id,onClick:()=>handleItemClick(n),children:n.label')
parent=parent.replace('icon:n.icon,extra: JSX.jsx(ChevronRightIcon','icon:n.icon,disabled:n.disabled,itemId:n.id,extra: JSX.jsx(ChevronRightIcon')
parent=parent.replace('open:T===n.id,openDelay:0','open:!n.disabled&&T===n.id,openDelay:0').replace('onOpenChange:a=>{a&&T!==n.id','onOpenChange:a=>{if(n.disabled)return;a&&T!==n.id')
parent=parent.replace('icon:n.icon,onClick:()=>handleSubmenuItemClick(n.id),children:n.label','icon:n.icon,disabled:n.disabled,itemId:n.id,onClick:()=>handleSubmenuItemClick(n.id),children:n.label')
// MenuItem body gets only host metadata and keyboard reachability; source classes/SVG structure stay unchanged.
js=js.replace('onClick:f,className:p,style:m})','onClick:f,className:p,style:m,itemId})').replace('"aria-disabled":u,tabIndex:-1,onClick:u?void 0:f','"aria-disabled":u,"data-add-menu-id":itemId,tabIndex:-1,onClick:u?void 0:f')
js+='const dU={FILES:"add-files",REFERENCE_FILES:"reference-files",MODE:"mode",EXPERT:"experts",SKILL:"skills",CONNECTOR:"connectors"};\n'+parent+'\nexport{AddMenuImpl,Menu,MenuItem,MenuDivider,AddFilesIcon,ReferenceFilesIcon,ModeIcon,ExpertsIcon,SkillsIcon,ConnectorsIcon,PlusIcon};\n'
js+=`const Ea=React,RH=React,CH=React,bH=React,ka=JSX,UH=JSX,wH=JSX,P_=React,HP=React,dL=React,hL=React,hH=React,_H=React,_n=React,yn={sm:14,md:16,lg:20,xl:24},useI18n=()=>({t:useT()}),getA11yLabel=()=>(${JSON.stringify(labels)})['common.clearSearch'],renderProviderIcon=icon=>icon,TH='cr-add-menu-connector-row';\n`
for(const name of ['UploadIcon','LoadingSpinnerIcon','JumpIcon','ConnectLinkIcon','HighlightText','matchesKeyword','itemKey','isItemConfigured','isInputConnectorOn','isInputConnectorRepairing','useConnectorSubmenuSearch','ConnectorIcon','ConnectorSuffix','Toggle','ConnectorsSubmenu','resolveModeOptions','mH','hH','ModeSubmenu','ModeToggleRow','ModeToggle','splitFilename','ReferenceFilesSubmenu']){
if(name==='hH'){const text=body(core,name).replace('const hH=','const sourceModeOptions=');js+=text+'\n';continue}js+=body(core,name)+'\n'
}
// The only host file provider currently available is local-file picking. Remote rows are not fabricated.
js+='const IH=[{id:"local-files",labelKey:"chatInput.attachment.localFiles",icon:JSX.jsx(UploadIcon,{}),group:"own"}],BH={"local-files":"onPickLocalFiles"};\n'+body(core,'FilesSubmenu')+'\n'
js=js.replace('n?.presentation===`add-menu`?hH:mH','n?.presentation===`add-menu`?sourceModeOptions:mH')
for(const name of ['resolveSize','createIcon','bn','I_','WP','pL','useControllableInputValue','_L','useSwitchMotion','yH'])js+=body(button,name)+'\n'
js+='const GP=createIcon(WP),de=_L,me=createIcon(I_),Te=yH;\n'+body(core,'SearchInput')+'\nexport{FilesSubmenu,ModeSubmenu,ConnectorsSubmenu,ReferenceFilesSubmenu,SearchInput,UploadIcon};\n'
// Error-label maps are immutable source data, not service status fabrication.
js+=body(core,'EH')+'\n'+body(core,'DH')+'\n'
manifest.adaptations.push('FilesSubmenu body is original but its immutable catalog is restricted to the available local-file handler; extraItems accept ReactNode icons. No source remote-library service entries are manufactured. Reference-file icon callback maps to existing recovered WorkbenchFileIcon/ArtifactFolderSolidIcon. Mode setMode is a local projection; actual mutation is the explicit provider.onChange host. Optional connector views keep source searching/state callbacks and do not create or run connectors.')
js+=`if(typeof document!=='undefined'){document.addEventListener('pointerdown',n=>{Ci=isPressTransparent(n.target)?null:{x:n.clientX,y:n.clientY,at:Date.now()}},true);document.addEventListener('pointermove',n=>{if(Ci&&(Math.abs(n.clientX-Ci.x)>yi||Math.abs(n.clientY-Ci.y)>yi))Ci=null},true)}\n`
let css='/* Fixed REA current cr AddMenu, Menu/SubMenu/Popover, WB trigger, tooltip; scoped selectors. */\n'
const scope='.yc-source-composer-add-menu'
manifest.tokenSelectorMappings=[]
function scopedTokenSelectors(rule){
const mapped=[]
for(const selector of rule.selectors){
if(selector===':root'){mapped.push(scope);continue}
if(selector.includes('.cr-theme')){
mapped.push(selector.replace(/\.cr-theme\b/g,scope));continue
}
// Keep each original theme/platform condition. In particular, dark selectors must never become an unconditional scope rule.
mapped.push(selector+' '+scope)
if(/(?:data-theme=dark|\.cb-dark\b|IDE\\ Night|vscode-dark|\.dark\b)/.test(selector))mapped.push(scope+'[data-cr-theme=dark]')
if(/IDE\\ Light/.test(selector))mapped.push(scope+'[data-cr-theme=light]')
}
// The original cr light alias is a class; map the host's explicit data-theme too, without overriding an explicit dark instance.
if(rule.selectors.includes('.light .cr-theme'))mapped.push('[data-theme=light] '+scope+':not([data-cr-theme=dark])')
return [...new Set(mapped)].join(',')
}
function appendScopedRule(rule,originalParent){
let node=rule
// Preserve all enclosing media/supports conditions as well as the selector itself.
for(let parent=originalParent;parent?.type==='atrule';parent=parent.parent){const wrapper=parent.clone({nodes:[]});wrapper.append(node);node=wrapper}
css+=node.toString()+'\n'
}
for(const[file,pin]of Object.entries({'src-CBaV_A3S.css':'2090ed21045c8f8b0fc388d76424c8751bf18fcd065cbbd5d0ef485163a2e36c','src-CyXHA0IC.css':'a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe','src-Bh3YZ35H.css':'88dbdc1cd3f59d0e8b75824512a465a2be714bc7af481d790fe3a9484f85e01a'})){
const text=fs.readFileSync('.tmp/rea-css-recovered/'+file,'utf8');if(sha(text)!==pin||cssManifest.files.find(item=>item.file===file)?.sha256!==pin)throw Error('CSS identity mismatch '+file);manifest.sources.push({file,sha256:pin});const tree=postcss.parse(text)
tree.walkRules(rule=>{
if(file==='src-CBaV_A3S.css'){const declarations=(rule.nodes??[]).filter(node=>node.type==='decl'&&(/^--cr-/.test(node.prop)||/^--wb-/.test(node.prop)));if(!declarations.length)return;const clone=rule.clone({nodes:declarations.map(node=>node.clone())});clone.selector=scopedTokenSelectors(rule);manifest.tokenSelectorMappings.push({file,original:rule.selector,scoped:clone.selector,declarations:declarations.length});appendScopedRule(clone,rule.parent);return}
if(!/\.cr-(?:add-menu|files-submenu|reference-files-submenu|mode-submenu|connectors-submenu|search-input|menu\b|menu-|sub-menu|popover\b|tooltip(?:\b|__|--))|\.wb-(?:button|input|switch|icon)(?:\b|__|--)/.test(rule.selector)||/model-selector|address-tooltip/.test(rule.selector))return;const clone=rule.clone();clone.selector=clone.selectors.map(selector=>'.yc-source-composer-add-menu '+selector).join(',');if(rule.parent.type==='atrule'){const parent=rule.parent.clone({nodes:[]});parent.append(clone);css+=parent.toString()+'\n'}else css+=clone.toString()+'\n'
});tree.walkAtRules('keyframes',rule=>{if(['cr-sub-menu-pop-in','wb-button-loading-spin','cr-input-spin','wb-switch-spin'].includes(rule.params))css+=rule.toString()+'\n'})
}
manifest.adaptations.push('Theme recovery preserves every original :root/html/body/theme selector condition and enclosing at-rule. WB dark token groups remain conditional; component-local custom properties stay on their original scoped component selectors rather than leaking to the root scope. Explicit data-cr-theme maps the host instance mode.')
const observedPath='.tmp/rea-add-menu-open-settled-20261016/layout.json',observed=JSON.parse(fs.readFileSync(observedPath,'utf8')),observedId='ev_330f3e738f84f51e0bc5beb5a2ee5647a8bd567407048d1ff239384929ba324f'
if(observed.evidenceId!==observedId)throw Error('Runtime override evidence changed')
const observedShadow=observed.layout.documents.flatMap(document=>document.nodes).find(node=>node.attributes?.class==='cr-menu')?.styles['box-shadow']
if(observedShadow!=='color(srgb 0 0 0 / 0.04) 0px 4px 12px -4px, color(srgb 0 0 0 / 0.04) 0px 3px 6px -8px')throw Error('Observed current light menu shadow changed')
css+='/* Current light runtime overrides the generic cr theme shadow; retained REA computed evidence, not a recovered default-token declaration. */\n.yc-source-composer-add-menu[data-cr-theme=light] .cr-add-menu__popover .cr-menu{box-shadow:'+observedShadow+'}\n'
manifest.runtimeOverrides=[{evidenceId:observedId,retained:observedPath,sha256:sha(fs.readFileSync(observedPath)),selector:'cr-menu',property:'box-shadow',value:observedShadow,theme:'light',limitation:'Original runtime override producer not located; generic cr dark theme declaration is preserved.'}]
const dts=`import type{ComponentType,ReactNode}from'react';export const AddMenuImpl:ComponentType<{onClose?():void;onAction?(id:string):void;renderTrigger?(state:{isOpen:boolean}):ReactNode;className?:string;disabled?:boolean}>;export const Menu:ComponentType<{children?:ReactNode;className?:string}>;export const MenuItem:ComponentType<{children?:ReactNode;icon?:ReactNode;disabled?:boolean;itemId?:string;onClick?():void}>;export const MenuDivider:ComponentType;export const AddFilesIcon:ComponentType;export const ReferenceFilesIcon:ComponentType;export const ModeIcon:ComponentType;export const ExpertsIcon:ComponentType;export const SkillsIcon:ComponentType;export const ConnectorsIcon:ComponentType;export const PlusIcon:ComponentType;export const UploadIcon:ComponentType;\n`
const submenuTypes=`import type{FilesProvider,ModeProvider,ConnectorProvider,ReferenceFilesProvider}from'./SourceSubmenus';export const FilesSubmenu:ComponentType<{provider:FilesProvider;onCloseMenu?():void}>;export const ModeSubmenu:ComponentType<{provider:ModeProvider}>;export const ConnectorsSubmenu:ComponentType<{provider:ConnectorProvider;onCloseMenu?():void}>;export const ReferenceFilesSubmenu:ComponentType<{provider:ReferenceFilesProvider;onCloseMenu?():void}>;export const SearchInput:ComponentType<{value:string;onChange(value:string):void;placeholder?:string}>;\n`
const folder='src/renderer/src/workbuddy/composer/add-menu/',outputs=[[folder+'source.mjs',js],[folder+'source.d.mts',dts+submenuTypes],[folder+'source.css',css]];manifest.outputs=outputs.map(([file,text])=>({file,sha256:sha(text),bytes:Buffer.byteLength(text)}));outputs.push([folder+'manifest.json',JSON.stringify(manifest,null,2)+'\n']);for(const[file,text]of outputs){if(process.argv.includes('--check')){if(fs.readFileSync(file,'utf8')!==text)throw Error('Recovered output drift '+file)}else fs.writeFileSync(file,text)}console.log(JSON.stringify({result:'PASS',sources:manifest.sources.length,bodies:manifest.bodies.length,outputs:outputs.length}))