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

+26 -5
View File
@@ -452,11 +452,32 @@
### 15. 智能体页面:团队管理 ### 15. 智能体页面:团队管理
- [ ] 拆解原版智能体设置页面,本轮仅对接“智能体团队管理”。 - [x] 拆解原版智能体设置页面,本轮仅对接“智能体团队管理”。
- [ ] 根据 REA 证据梳理团队列表、详情及管理操作,将需要的真实存储/配置链路接入。 - [x] 根据 REA 证据确认本页只有三个设置区块,没有团队列表、创建、详情或成员管理;接入“开启 / 禁用”偏好的真实存储与重启恢复链路。
- [ ] 验收:仅展示已确定范围;管理操作实际生效、重启后可恢复;团队管理与真实子代理调度的完成度分别记录。 - [x] 验收:仅展示原版已确定的三个区块;团队管理偏好实际持久化、重启后可恢复;团队管理与真实子代理调度分别记录。
状态:待 REA 拆解;范围限定为团队管理。 状态:已实现并通过真实窗口专项验收。团队管理偏好已持久化;多智能体自动拆分执行器未接入。
原版证据:WorkBuddy 5.8.3,设置窗口 900×680。REA Evidence `ev_bd0f4b5dac7d4e2b89b1b050db1896c65e03b4fd1c3d3d986a8958fd9c1a9883`(浅色布局、DOM、computed style 与 CSS 源)、`ev_3384aa03af8a1ccdffb8e025dbd4a354636d8d6b0c207abd21e12fc5eb2ae719`(深色)、`ev_01940b7925326ee8e7f7e4aba661be2a17a2f876d634d4d370d01b6616a4a7fd`(团队管理下拉只含“开启 / 禁用”)、`ev_1cc896e04c0cc9ceab39b0a252f4982c019305609195ad44804ae9b5fb7379a8`(最终浅色顶部)、`ev_9542a063514b1a980665e504a89126c7a17f00b42f0c6dff9cead085a929f5c7`(三枚浏览器扩展图标媒体字节)。页面根 `638×503 @232,40`,三个区块依次为智能体团队管理、经济模式、浏览器操控(Beta);浏览器卡片 `638×261`,扩展列表 `606×193`,三行高 `68/68/56`。经济模式开启轨道浅色 `rgb(0,194,154)`、深色 `rgb(34,203,168)`。独立懒加载 CSS 为 `ExtensionsSettingsPanel-BnsXh3oa.css`,sha256 `cbd4b37c6061381943d96ca454176b2f083e49eb8e04c34cb5d31eef3dbcb8f5`。浏览器图标分别为 Chrome SVG `bde4af5a…`、Edge SVG `968cf9d8…`、QQ PNG `186c8282…`。观察范围限当前设置窗口;本页没有团队列表、团队详情、创建或成员管理,未据此推断其他页面。
实现:
- `src/shared/types.ts`、`src/shared/ipc.ts`、`src/preload/index.ts`:新增 `AgentSettings { teamManagement }` 与 `window.ycode.agents.get/update` 契约。
- `src/main/config.ts`、`src/main/agent-settings.ts`、`src/main/ipc.ts`:默认开启,写入 `config.json.agentSettings.teamManagement`;`agents:get/update` 返回归一化后的完整视图。
- `src/renderer/src/components/AgentsSettingsPanel.tsx` + `source-agents-panels.css`:复刻三区块布局、团队下拉、经济模式 34×20 开关、Beta 徽标和三行浏览器扩展列表;团队管理下拉真实持久化,控件 title 明确“仅保存偏好;多智能体协作执行尚未接入”。
- `source-agents-panels-recovered.css` + `scripts/rea/recover-agents-panels.mjs`:仅从 REA Evidence 回收并投影原版 `ExtensionsSettingsPanel-BnsXh3oa.css` 与引用闭包内的主题 token。
- `scripts/rea/probe-todo15-browser-media.json` + `scripts/rea/recover-agents-browser-icons.mjs`:通过 REA 已加载 IMG 媒体选择器导出三枚真实浏览器图标,校验 Evidence 身份、MIME、SHA-256、可见性与资源字节后写入 `components/agents-assets/`;Chrome/Edge 为 SVG,QQ 为 128×128 PNG。
- REA 媒体扩展新增已加载 SVG 的被动读取与签名校验,回归覆盖 SVG 导出、非法 MIME、字节预算和 Evidence 序列化;`test:rea-css` 已纳入 `media-assets-regression.mjs`。
- `SettingsDialog.tsx`、`scripts/verify-settings-nav.cjs`:启用设置导航中的“智能体”并渲染面板。
验证:`scripts/verify-agents-settings.cjs` 在真实隔离 Electron + 原生设置窗口(900×680、真实 preload/IPC/文件系统)中 17 项断言全部通过:导航启用与选中;面板、三张卡片、主 section、下拉、开关、Beta、浏览器列表及按钮精确几何;原版文案;三枚图标均为已加载 `IMG`、36×36、自然尺寸有效且无占位标题;下拉仅“开启 / 禁用”并持久化为 `false`;关闭重开及 `config.json` 均恢复“禁用”;经济模式禁用且标注“暂未接入”,浅色/深色保持原版品牌绿轨道;浏览器安装入口禁用且标注“尚未配置”、无 `<a href>`;浅色/深色 surface 与 token;无渲染器错误。浅色和深色实测截图随报告保存在 `.tmp/agents-settings-OHo1QC/ycode-agents-light.png` / `ycode-agents-dark.png`,报告 `.tmp/agents-settings-OHo1QC/report.json`。回归串行执行 `npm run typecheck`、`npm run build`、`npm run test:rea-css`、`npm run test:chat`、`npm run test:conversation`、`npm run test:conversation-ipc`、`npm run test:personalization`、`node scripts/verify-settings-nav.cjs`、`node scripts/verify-shortcuts-settings.cjs`、`node scripts/verify-appearance-settings.cjs`、`node scripts/verify-general-settings.cjs`、`node scripts/verify-personalization-settings.cjs`、`node scripts/verify-about-help-settings.cjs`、`node scripts/verify-desktop-appearance.cjs`、`node scripts/verify-account-menu.cjs` 全部通过。
遗留:
1. 团队管理开关目前只保存偏好,复杂任务自动拆分与多智能体协作执行尚未接入;按用户要求,本轮不处理子代理调度和云端服务。
2. 经济模式只还原布局和禁用态,上下文自动压缩未接入,界面标注“暂未接入”。
3. 浏览器操控的“安装 / 前往安装”只还原布局和禁用态,Chrome、Edge、QQ 扩展安装与外跳未接入,界面标注“尚未配置”。
4. 视觉验收只覆盖当前 900×680 设置窗口;未覆盖其他窗口尺寸、扩展安装后的状态、真实团队列表或其他云端团队页面。
### 16. 移除助理与设备连接 ### 16. 移除助理与设备连接
@@ -589,7 +610,7 @@
## 推进顺序 ## 推进顺序
1. 对话与侧栏:01 → 02 → 03 → 04 → 05 → 06 → 07 → 08 → 09 → 21(均已实现并留下验收记录,未完成验收点在各条目内标注)。 1. 对话与侧栏:01 → 02 → 03 → 04 → 05 → 06 → 07 → 08 → 09 → 21(均已实现并留下验收记录,未完成验收点在各条目内标注)。
2. 设置:16、17 的入口范围与 10(通用)均已落实并验证;接着依次推进 11 → 12 → 13 → 15 → 19。 2. 设置:16、17 的入口范围与 10(通用)均已落实并验证;11(外观)、12(快捷键)、13(个性化)、19(关于与获取帮助)、15(智能体)均已实现并完成专项验收。
3. 14、18、20 保留为待拆解,后续单独展开。 3. 14、18、20 保留为待拆解,后续单独展开。
## 每项交付记录模板 ## 每项交付记录模板
+1 -1
View File
@@ -39,7 +39,7 @@
"rea:css:prepare": "node scripts/rea/prepare-css-runtime.mjs", "rea:css:prepare": "node scripts/rea/prepare-css-runtime.mjs",
"rea:css:capture": "node scripts/rea/capture-css.mjs", "rea:css:capture": "node scripts/rea/capture-css.mjs",
"recover:css-reference": "node scripts/rea/export-css-reference.mjs", "recover:css-reference": "node scripts/rea/export-css-reference.mjs",
"test:rea-css": "node scripts/rea/css-capture-regression.mjs && node scripts/rea/css-runtime-contract-regression.mjs && node scripts/rea/visual-capture-regression.mjs && node scripts/rea/visual-probe-regression.mjs && node scripts/rea/visual-probe-budget-regression.mjs && node scripts/rea/selected-artifact-text-regression.mjs && node scripts/rea/css-mcp-regression.mjs && node scripts/rea/css-runtime-integrity.mjs", "test:rea-css": "node scripts/rea/css-capture-regression.mjs && node scripts/rea/css-runtime-contract-regression.mjs && node scripts/rea/visual-capture-regression.mjs && node scripts/rea/visual-probe-regression.mjs && node scripts/rea/visual-probe-budget-regression.mjs && node scripts/rea/media-assets-regression.mjs && node scripts/rea/selected-artifact-text-regression.mjs && node scripts/rea/css-mcp-regression.mjs && node scripts/rea/css-runtime-integrity.mjs",
"test:rea-visual-live": "node scripts/rea/visual-probe-live-regression.mjs", "test:rea-visual-live": "node scripts/rea/visual-probe-live-regression.mjs",
"test:visual-pixels": "node scripts/rea/compare-visual-pixels.mjs --self-test", "test:visual-pixels": "node scripts/rea/compare-visual-pixels.mjs --self-test",
"test:font-platform": "node scripts/rea/capture-ycode-font-platform-fixture.mjs && node scripts/rea/verify-font-platform-parity.mjs .tmp/ycode-font-platform-selftest-reference.json .tmp/ycode-font-platform-selftest-result.json", "test:font-platform": "node scripts/rea/capture-ycode-font-platform-fixture.mjs && node scripts/rea/verify-font-platform-parity.mjs .tmp/ycode-font-platform-selftest-reference.json .tmp/ycode-font-platform-selftest-result.json",
+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 main={frame_id:'main',file_path:await fs.realpath(html)},frame={id:'main',url:page}
const png='iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jKXcAAAAASUVORK5CYII=' const png='iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jKXcAAAAASUVORK5CYII='
const bytes=Buffer.from(png,'base64'),digest=crypto.createHash('sha256').update(bytes).digest('hex') 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:[{ 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, 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]}}, 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==='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==='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==='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(method==='Page.getResourceContent'){
if(mode==='content-unavailable')throw Error('private-url-or-cookie-must-not-leak') 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==='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} if(method==='Page.captureScreenshot')return {data:png}
@@ -58,12 +60,17 @@ try {
assert.equal(result.total_bytes,bytes.length) 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(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) 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='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') 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) 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) 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) assert.equal(JSON.stringify(result).includes('private-url-or-cookie'),false)
} }
mode='ok';snapshot=makeSnapshot();snapshot.documents[0].nodes.attributes[0]=[9,10] 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(optedOut.visuals.media_assets,undefined)
assert.equal(JSON.stringify(optedOut.visuals).includes('fallback.png'),false) assert.equal(JSON.stringify(optedOut.visuals).includes('fallback.png'),false)
assert.equal(listener,undefined) 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})} }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 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 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; const mime=value=>value==='image/jpg'?'image/jpeg':value;
function sniff(bytes){ 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>=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>=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>=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'; 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){ 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;} 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:[ 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.', '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.', '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.' '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]??''; const main=frames[0],strings=snapshot.strings??[],str=index=>strings[index]??'';
@@ -32,7 +32,7 @@ const mediaIdentity = {
}; };
export const electronMediaAssetsSchema = z.object({ export const electronMediaAssetsSchema = z.object({
items:z.array(z.discriminatedUnion('status',[ 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()}) z.object({...mediaIdentity,status:z.literal('unknown'),reason:z.string()})
])).max(4), ])).max(4),
total_bytes:z.number().int().min(0).max(8388608), 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)} : {}),')) ' 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, 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, 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') const styleModule = await fs.readFile(path.join(patchRoot, 'CdpElectronStyles.js'), 'utf8')
await fs.writeFile(path.join(output, 'dist/browser/CdpElectronStyles.js'), styleModule) 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}`)
+243
View File
@@ -0,0 +1,243 @@
// TODO 15 验收:设置 → 智能体的 1:1 布局 + 团队管理偏好真实持久化 + 未接入项标注 + 深浅主题。
// 真实隔离 YCode(真实 preload/IPC/渲染进程/文件系统)+ 原生设置窗口,视口 900×680(与原版一致)。
//
// 原版依据(REA ev_bd0f4b…/ev_3384aa…/ev_01940b…/ev_1cc896…):
// 页面根 [232,40,638,503];团队管理卡 638×90(section 606×66、下拉 120×32 @734,69);
// 经济模式卡 638×90(开关 34×20 @820,181);浏览器操控标签 638×22 + Beta 39.391×20 @314,253;
// 浏览器卡片 638×261(主 section 606×44、安装 68×32 @786,294、列表 606×193、行 68/68/56、按钮 102×32 @752)。
const fs = require('node:fs/promises')
const path = require('node:path')
const assert = require('node:assert/strict')
if (!process.versions.electron) {
;(async () => {
const directory = await fs.mkdtemp(path.resolve('.tmp/agents-settings-'))
const data = path.join(directory, 'data'), home = path.join(directory, 'home')
await fs.mkdir(data, { recursive: true })
await fs.mkdir(home, { recursive: true })
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data, YCODE_OMP_HOME: home,
YCODE_DISABLE_GPU: '1', YCODE_DISABLE_SYSTEM_INTEGRATION: '1' }
delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL
const child = require('node:child_process').spawn(require('electron'),
['--no-sandbox', '--disable-gpu-sandbox', __filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
let output = ''
child.stdout.on('data', bytes => { output += bytes; if (bytes.toString().includes('AGENTS_SETTINGS_')) process.stdout.write(bytes) })
child.stderr.on('data', bytes => { output += bytes })
const timer = setTimeout(() => child.kill(), 300000)
child.once('exit', async code => {
clearTimeout(timer)
await fs.writeFile(path.join(directory, 'process.log'), output)
console.log(JSON.stringify({ directory, exitCode: code }))
if (code) console.error(output.slice(-4000))
process.exitCode = code ? 1 : 0
})
})().catch(error => { console.error(error); process.exitCode = 1 })
} else {
const { app, BrowserWindow } = require('electron')
const { pathToFileURL } = require('node:url')
const directory = process.env.YCODE_QA_DIRECTORY
app.setPath('home', path.join(directory, 'home'))
const checks = [], snapshots = {}, rendererErrors = []
let main, settings, phase = 'boot'
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
async function read(source, win = main) {
let timer
try {
return await Promise.race([win.webContents.executeJavaScript(source), new Promise((_, reject) => {
timer = setTimeout(() => reject(Error('Renderer timeout: ' + source.slice(0, 120))), 15000)
})])
} finally { clearTimeout(timer) }
}
async function wait(source, label, win = main) {
for (let index = 0; index < 300; index++) { const value = await read(source, win); if (value) return value; await pause(40) }
throw Error('Timeout: ' + label)
}
async function step(label) { phase = label; await fs.writeFile(path.join(directory, 'phase.txt'), label) }
function check(label, actual, expected) {
const pass = typeof expected === 'function' ? expected(actual) : JSON.stringify(actual) === JSON.stringify(expected)
checks.push({ label, pass, actual, expected: typeof expected === 'function' ? expected.toString() : expected })
}
const near = (value, target, tolerance = 1) => Math.abs(value - target) <= tolerance
function observe(win) {
win.webContents.on('console-message', event => { if (event.level === 'error' || event.level >= 3) rendererErrors.push({ windowId: win.id, message: event.message }) })
}
async function openSettings(section) {
await read(`window.ycode.app.openSettings(${JSON.stringify(section)})`)
for (let index = 0; index < 200; index++) {
settings = BrowserWindow.getAllWindows().find(win => win !== main)
if (settings && !settings.webContents.isLoading()) break
await pause(25)
}
assert.ok(settings, 'settings window')
await wait('!!document.querySelector(".settings-window-root")', 'settings root', settings)
await pause(400)
return settings
}
const layoutSource = `(()=>{
const round = value => Math.round(value * 1000) / 1000
const rect = node => { if (!node) return null; const r = node.getBoundingClientRect()
return { x: round(r.x), y: round(r.y), width: round(r.width), height: round(r.height) } }
const panel = document.querySelector('.extensions-settings-panel')
const cards = [...document.querySelectorAll('.extensions-settings-panel > .general-settings-panel__group-card')]
const sections = [...document.querySelectorAll('.extensions-settings-panel .general-settings-panel__group-card > .general-settings-panel__section')]
const buttons = [...document.querySelectorAll('.extensions-settings-panel__action-button')]
const rows = [...document.querySelectorAll('.extensions-settings-panel__browser-row')]
const select = document.querySelector('.extensions-settings-panel__team-dropdown .wb-select')
const switchInput = document.querySelector('#agents-economy-switch, .extensions-settings-panel .wb-switch__input')
return {
panel: rect(panel),
cards: cards.map(card => rect(card)),
sections: sections.map(section => ({
rect: rect(section),
title: section.querySelector('.general-settings-panel__section-title')?.textContent?.trim(),
desc: section.querySelector('.general-settings-panel__section-desc')?.textContent?.trim()
})),
select: select ? { rect: rect(select), value: select.querySelector('.wb-select__value')?.textContent?.trim(), title: select.getAttribute('title') } : null,
switch: switchInput ? (() => { const track = switchInput.closest('.wb-switch')?.querySelector('.wb-switch__track')
return { rect: rect(track), trackBackground: track ? getComputedStyle(track).backgroundColor : null,
trackOpacity: track ? getComputedStyle(track).opacity : null, thumb: rect(track?.querySelector('.wb-switch__thumb')),
checked: switchInput.checked, disabled: switchInput.disabled,
unavailable: switchInput.closest('.extensions-settings-panel__unavailable-control')?.getAttribute('title') } })() : null,
browserLabel: (() => { const node = document.querySelector('.extensions-settings-panel__browser-label'); return node ? { ...rect(node), text: node.firstChild?.textContent?.trim() } : null })(),
beta: (() => { const node = document.querySelector('.extensions-settings-panel__beta'); return node ? { ...rect(node), text: node.textContent.trim() } : null })(),
browserCard: rect(document.querySelector('.extensions-settings-panel__browser-card')),
browserList: rect(document.querySelector('.extensions-settings-panel__browser-list')),
rows: rows.map(row => ({
rect: rect(row),
title: row.querySelector('.extensions-settings-panel__browser-title')?.textContent?.trim(),
desc: row.querySelector('.extensions-settings-panel__browser-desc')?.textContent?.trim(),
icon: (() => { const node = row.querySelector('.extensions-settings-panel__browser-icon')
return node ? { ...rect(node), tagName: node.tagName, src: node.getAttribute('src'), placeholderTitle: node.getAttribute('title'),
complete: node.complete === true, naturalWidth: node.naturalWidth, naturalHeight: node.naturalHeight } : null })(),
button: (() => { const node = row.querySelector('.extensions-settings-panel__action-button')
return node ? { rect: rect(node), text: node.textContent.trim(), ariaDisabled: node.getAttribute('aria-disabled'), title: node.getAttribute('title'), arrow: !!node.querySelector('.wb-button__icon svg') } : null })()
})),
buttons: buttons.map(button => ({ text: button.textContent.trim(), rect: rect(button), ariaDisabled: button.getAttribute('aria-disabled'), title: button.getAttribute('title') })),
anchors: [...document.querySelectorAll('a')].map(anchor => anchor.getAttribute('href')).filter(Boolean),
cardBackground: cards[0] ? getComputedStyle(cards[0]).backgroundColor : null,
titleColor: (() => { const node = document.querySelector('.extensions-settings-panel .general-settings-panel__section-title'); return node ? getComputedStyle(node).color : null })(),
descColor: (() => { const node = document.querySelector('.extensions-settings-panel .general-settings-panel__section-desc'); return node ? getComputedStyle(node).color : null })(),
selectBackground: select ? getComputedStyle(select).backgroundColor : null,
selectBorder: select ? getComputedStyle(select).borderTopColor : null,
buttonBackground: buttons[0] ? getComputedStyle(buttons[0]).backgroundColor : null,
buttonBorder: buttons[0] ? getComputedStyle(buttons[0]).borderTopColor : null,
betaBackground: (() => { const node = document.querySelector('.extensions-settings-panel__beta'); return node ? getComputedStyle(node).backgroundColor : null })(),
theme: document.documentElement.dataset.theme ?? 'light'
}})()`
;(async () => {
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href); await app.whenReady()
for (let index = 0; index < 200; index++) {
main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode')
if (main && !main.webContents.isLoading()) break
await pause(25)
}
assert.ok(main, 'main window'); observe(main)
await wait('!!window.ycode', 'preload api')
await step('agents-settings')
await openSettings('extensions'); observe(settings)
await wait('!!document.querySelector(".extensions-settings-panel")', 'agents panel', settings)
const nav = await read(`(()=>{const item=document.querySelector('[data-settings-tab="extensions"]')
return { exists: !!item, disabled: item?.getAttribute('aria-disabled'), active: document.querySelector('.settings-navigation__item--active')?.dataset.settingsTab }})()`, settings)
check('智能体 nav entry is enabled and active', [nav.exists, nav.disabled !== 'true', nav.active], [true, true, 'extensions'])
const light = await read(layoutSource, settings)
snapshots.light = light
await fs.writeFile(path.join(directory, 'ycode-agents-light.png'), (await settings.webContents.capturePage()).toPNG())
check('agents panel keeps the recovered 638x503 box at 232,40',
[near(light.panel.x, 232), near(light.panel.y, 40), near(light.panel.width, 638), near(light.panel.height, 503)], [true, true, true, true])
check('team-management and economy cards keep 638x90 with 606x66 sections',
[light.cards.slice(0, 2).map(card => [near(card.width, 638), near(card.height, 90)].every(Boolean)),
light.sections.slice(0, 2).map(section => [near(section.rect.width, 606), near(section.rect.height, 66)].every(Boolean)),
near(light.cards[0].y, 40), near(light.cards[1].y, 146)], [[true, true], [true, true], true, true])
check('team-management keeps the recovered title, description and 120x32 select',
[light.sections[0].title, light.sections[0].desc,
near(light.select.rect.x, 734), near(light.select.rect.y, 69), near(light.select.rect.width, 120), near(light.select.rect.height, 32),
light.select.value, light.select.title],
['智能体团队管理', '开启后,复杂任务会自动拆分多个智能体协作完成。专家团队任务不受此设置影响。',
true, true, true, true, '开启', '仅保存偏好;多智能体协作执行尚未接入'])
check('economy mode keeps the recovered copy, 34x20 switch and disabled annotation',
[light.sections[1].title, light.sections[1].desc,
near(light.switch.rect.x, 820), near(light.switch.rect.y, 181), near(light.switch.rect.width, 34), near(light.switch.rect.height, 20),
light.switch.trackBackground, light.switch.checked, light.switch.disabled, light.switch.unavailable],
['经济模式', '自动在合适时机触发上下文压缩,减少长对话中重复传输的上下文,降低积分消耗',
true, true, true, true, 'rgb(0, 194, 154)', true, true, '暂未接入'])
check('browser group keeps the recovered label 638x22 and Beta badge',
[near(light.browserLabel.x, 232), near(light.browserLabel.y, 252), near(light.browserLabel.width, 638), near(light.browserLabel.height, 22),
light.browserLabel.text, near(light.beta.x, 314), near(light.beta.y, 253), near(light.beta.width, 39.391), near(light.beta.height, 20), light.beta.text],
[true, true, true, true, '浏览器操控', true, true, true, true, 'Beta'])
check('browser card keeps 638x261 with 606x44 main section and 68x32 install button',
[near(light.browserCard.y, 282), near(light.browserCard.width, 638), near(light.browserCard.height, 261),
near(light.sections[2].rect.y, 294), near(light.sections[2].rect.width, 606), near(light.sections[2].rect.height, 44),
light.sections[2].title, near(light.buttons[0].rect.x, 786), near(light.buttons[0].rect.width, 68), near(light.buttons[0].rect.height, 32),
light.buttons[0].ariaDisabled, light.buttons[0].title],
[true, true, true, true, true, true, '让 YCode 控制浏览器', true, true, true, 'true', '尚未配置'])
check('browser list keeps the recovered 606x193 with 68/68/56 rows',
[near(light.browserList.y, 338), near(light.browserList.width, 606), near(light.browserList.height, 193),
light.rows.map(row => near(row.rect.height, row.title.includes('QQ') ? 56 : 68)),
light.rows.every(row => row.icon && row.icon.tagName === 'IMG' && near(row.icon.width, 36) && near(row.icon.height, 36)
&& row.icon.complete && row.icon.naturalWidth > 0 && row.icon.naturalHeight > 0 && row.icon.src && row.icon.placeholderTitle === null)],
[true, true, true, [true, true, true], true])
check('browser rows keep the recovered titles, descriptions and disabled 102x32 actions',
[light.rows.map(row => row.title),
light.rows.every(row => row.button && near(row.button.rect.width, 102) && near(row.button.rect.height, 32) && row.button.text === '前往安装' && row.button.ariaDisabled === 'true' && row.button.title === '尚未配置' && row.button.arrow)],
[['YCode Chrome 扩展程序', 'YCode Edge 扩展程序', 'YCode QQ 浏览器扩展程序'], true])
check('agents page contains no anchor links', light.anchors, [])
await step('agents-team-toggle')
await read(`document.querySelector('.extensions-settings-panel__team-dropdown .wb-select').click()`, settings)
const popover = await wait(`(()=>{const node=document.querySelector('.extensions-settings-panel__team-popover')
return node?{options:[...node.querySelectorAll('[role=option]')].map(option=>option.textContent.trim())}:null})()`, 'team popover', settings)
check('team-management dropdown offers exactly 开启 / 禁用', popover.options.map(text => text.replace(/\s+/g, '')), ['开启', '禁用'])
await read(`[...document.querySelectorAll('.extensions-settings-panel__team-popover [role=option]')].find(option=>option.textContent.trim()==='禁用').click()`, settings)
await wait(`document.querySelector('.extensions-settings-panel__team-dropdown .wb-select__value')?.textContent?.trim()==='禁用'`, 'team management disabled', settings)
const stored = await read(`window.ycode.agents.get().then(view => view.teamManagement)`, settings)
const config = JSON.parse(await fs.readFile(path.join(directory, 'data', 'config.json'), 'utf8'))
snapshots.config = config.agentSettings
check('selecting 禁用 persists the team-management preference', [stored, config.agentSettings?.teamManagement], [false, false])
await step('agents-persistence')
settings.close()
await pause(500)
await openSettings('extensions')
const reopened = await read(layoutSource, settings)
check('reopened settings restores the 禁用 preference', reopened.select.value, '禁用')
await step('agents-dark')
await openSettings('appearance')
await wait('!!document.querySelector(".appearance-card")', 'appearance panel', settings)
await read(`document.querySelector('[aria-label="深色"].appearance-card').click()`, settings)
await wait(`document.documentElement.dataset.theme==='dark'`, 'dark theme', settings)
await openSettings('extensions')
await wait(`document.documentElement.dataset.theme==='dark'&&!!document.querySelector('.extensions-settings-panel')`, 'dark agents panel', settings)
const dark = await read(layoutSource, settings)
snapshots.dark = dark
await fs.writeFile(path.join(directory, 'ycode-agents-dark.png'), (await settings.webContents.capturePage()).toPNG())
check('dark theme keeps the recovered card, text, select and button surfaces',
[dark.cardBackground, dark.titleColor, dark.descColor, dark.selectBackground, dark.selectBorder, dark.buttonBackground, dark.buttonBorder, dark.betaBackground],
['rgb(31, 31, 31)', 'rgb(242, 243, 245)', 'rgba(255, 255, 255, 0.5)', 'rgb(31, 31, 31)', 'rgb(61, 61, 61)', 'rgb(38, 38, 38)', 'rgb(61, 61, 61)', 'rgb(31, 31, 31)'])
check('dark theme keeps every recovered geometry',
[near(dark.panel.width, 638), near(dark.panel.height, 503), near(dark.cards[0].height, 90),
near(dark.browserCard.height, 261), near(dark.browserList.height, 193)], [true, true, true, true, true])
check('dark economy switch keeps the recovered enabled brand track', dark.switch.trackBackground, 'rgb(34, 203, 168)')
await openSettings('appearance')
await wait('!!document.querySelector(".appearance-card")', 'appearance panel again', settings)
await read(`document.querySelector('[aria-label="浅色"].appearance-card').click()`, settings)
await wait(`document.documentElement.dataset.theme==='light'`, 'light restored', settings)
await step('agents-errors')
check('no renderer error logs', rendererErrors, [])
const result = checks.every(item => item.pass) ? 'PASS' : 'FAIL'
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result, phase, checks, snapshots }, null, 2))
console.log('AGENTS_SETTINGS_' + result + ' ' + directory)
for (const failure of checks.filter(item => !item.pass)) console.log('AGENTS_SETTINGS_FAILURE ' + JSON.stringify(failure))
app.exit(result === 'PASS' ? 0 : 1)
})().catch(async error => {
console.error(error)
try { await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'ERROR', phase, checks, snapshots, error: String(error?.stack ?? error) }, null, 2)) } catch { /* ignore */ }
app.exit(1)
})
}
+2 -2
View File
@@ -125,10 +125,10 @@ if (!process.versions.electron) {
// 原版 4 组;about 组按组件约定渲染为分隔线而不是标题。 // 原版 4 组;about 组按组件约定渲染为分隔线而不是标题。
check('nav keeps the four recovered groups', [nav.groups.length, nav.dividers], [3, 1]) check('nav keeps the four recovered groups', [nav.groups.length, nav.dividers], [3, 1])
// 通用(settings) 在 TODO 10 接入后同样可用。 // 通用(settings) 在 TODO 10 接入后同样可用。
const expectedDisabled = EXPECTED_TABS.filter(tab => !['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization', 'helpFeedback', 'getHelp'].includes(tab)) const expectedDisabled = EXPECTED_TABS.filter(tab => !['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization', 'extensions', 'helpFeedback', 'getHelp'].includes(tab))
check('unwired entries stay disabled', nav.tabs.filter((tab, index) => nav.disabled[index] === 'true'), expectedDisabled) check('unwired entries stay disabled', nav.tabs.filter((tab, index) => nav.disabled[index] === 'true'), expectedDisabled)
check('unwired entries keep the 暂未接入 hint', nav.titles.filter(title => (title ?? '').includes('暂未接入')).length, expectedDisabled.length) check('unwired entries keep the 暂未接入 hint', nav.titles.filter(title => (title ?? '').includes('暂未接入')).length, expectedDisabled.length)
check('settings, appearance, shortcuts, personalization, models, about and help stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['settings', 'appearance', 'keyboardShortcuts', 'personalization', 'models', 'helpFeedback', 'getHelp']) check('settings, appearance, shortcuts, personalization, agents, models, about and help stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['settings', 'appearance', 'keyboardShortcuts', 'personalization', 'extensions', 'models', 'helpFeedback', 'getHelp'])
await step('settings-search-excludes-removed') await step('settings-search-excludes-removed')
await read(`(()=>{const input=document.querySelector('.settings-navigation input[type="search"]'); await read(`(()=>{const input=document.querySelector('.settings-navigation input[type="search"]');
+30
View File
@@ -0,0 +1,30 @@
/**
* 设置 → 智能体(TODO 15):只持久化「智能体团队管理」偏好。
*
* REA 依据(WorkBuddy 5.8.3,设置窗口 900×680):
* - ev_bd0f4b5dac7d4e2b89b1b050db1896c65e03b4fd1c3d3d986a8958fd9c1a9883(浅色顶部 DOM/layout/computed style + CSS 源)
* - ev_3384aa03af8a1ccdffb8e025dbd4a354636d8d6b0c207abd21e12fc5eb2ae719(深色同页)
* - ev_01940b7925326ee8e7f7e4aba661be2a17a2f876d634d4d370d01b6616a4a7fd(团队管理下拉展开:开启 / 禁用)
* - ev_1cc896e04c0cc9ceab39b0a252f4982c019305609195ad44804ae9b5fb7379a8(最终浅色顶部)
*
* 用户限定:云端服务与子代理调度本轮不处理。因此这里只保存偏好(默认开启,与原版一致),
* 多智能体自动拆分的执行器尚未接入;面板用 title 明确说明,不把偏好伪装成已生效能力。
*/
import type { AgentSettings, AgentSettingsPatch } from '@shared/types'
import { getStoredAgentSettings, setStoredAgentSettings } from './config'
export const DEFAULT_AGENT_SETTINGS: AgentSettings = { teamManagement: true }
export function normalizeAgentSettings(value: Partial<AgentSettings> | null | undefined): AgentSettings {
return { teamManagement: value?.teamManagement !== false }
}
export async function getAgentSettings(): Promise<AgentSettings> {
return normalizeAgentSettings(await getStoredAgentSettings())
}
export async function updateAgentSettings(patch: AgentSettingsPatch): Promise<AgentSettings> {
const next = normalizeAgentSettings({ ...await getAgentSettings(), ...patch })
await setStoredAgentSettings(next)
return next
}
+13 -1
View File
@@ -10,7 +10,7 @@
import { app, safeStorage } from 'electron' import { app, safeStorage } from 'electron'
import { promises as fs } from 'node:fs' import { promises as fs } from 'node:fs'
import { join } from 'node:path' import { join } from 'node:path'
import type { GatewayConfig, GatewayConfigView, PersonalizationSettings } from '@shared/types' import type { AgentSettings, GatewayConfig, GatewayConfigView, PersonalizationSettings } from '@shared/types'
interface StoredConfig { interface StoredConfig {
speechBaseUrl?: string speechBaseUrl?: string
@@ -28,6 +28,8 @@ interface StoredConfig {
autoLaunch?: boolean autoLaunch?: boolean
/** 设置 → 个性化(身份、回复风格、指令、人设、长期记忆)。 */ /** 设置 → 个性化(身份、回复风格、指令、人设、长期记忆)。 */
personalization?: Partial<PersonalizationSettings> personalization?: Partial<PersonalizationSettings>
/** 设置 → 智能体(智能体团队管理偏好)。 */
agentSettings?: Partial<AgentSettings>
} }
const DEFAULTS: StoredConfig = { const DEFAULTS: StoredConfig = {
@@ -131,6 +133,16 @@ export async function setStoredPersonalization(value: Partial<PersonalizationSet
await persist({ ...stored, personalization: { ...(stored.personalization ?? {}), ...value } }) await persist({ ...stored, personalization: { ...(stored.personalization ?? {}), ...value } })
} }
export async function getStoredAgentSettings(): Promise<Partial<AgentSettings>> {
const stored = await load()
return stored.agentSettings ?? {}
}
export async function setStoredAgentSettings(value: Partial<AgentSettings>): Promise<void> {
const stored = await load()
await persist({ ...stored, agentSettings: { ...(stored.agentSettings ?? {}), ...value } })
}
/** 供渲染进程使用:脱敏视图 */ /** 供渲染进程使用:脱敏视图 */
export async function getConfigView(): Promise<GatewayConfigView> { export async function getConfigView(): Promise<GatewayConfigView> {
const stored = await load() const stored = await load()
+4 -1
View File
@@ -11,7 +11,7 @@ import { BrowserWindow, app, dialog, ipcMain, shell, type OpenDialogOptions } fr
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { unlink, writeFile } from 'node:fs/promises' import { unlink, writeFile } from 'node:fs/promises'
import { join } from 'node:path' import { join } from 'node:path'
import type { ChatEvent, ChatMessage, GitBranchInfo, PersonalizationPatch, WindowState } from '@shared/types' import type { AgentSettingsPatch, ChatEvent, ChatMessage, GitBranchInfo, PersonalizationPatch, WindowState } from '@shared/types'
import { freezeAssistantModelMetadata } from '@shared/assistant-model' import { freezeAssistantModelMetadata } from '@shared/assistant-model'
import type { ConfigPatchLike, EditAction, SendPayload, WorkspaceAttachResult } from '@shared/ipc' import type { ConfigPatchLike, EditAction, SendPayload, WorkspaceAttachResult } from '@shared/ipc'
import { applyAssistantEvent } from '@shared/message-parts' import { applyAssistantEvent } from '@shared/message-parts'
@@ -35,6 +35,7 @@ import * as models from './models'
import * as workspaces from './workspaces' import * as workspaces from './workspaces'
import * as generalSettings from './general-settings' import * as generalSettings from './general-settings'
import * as personalization from './personalization' import * as personalization from './personalization'
import * as agentSettings from './agent-settings'
import { prepareOmpModel } from './omp-model' import { prepareOmpModel } from './omp-model'
import type { CustomModelInput, ResolvedModel } from '@shared/models' import type { CustomModelInput, ResolvedModel } from '@shared/models'
import type { SessionContext } from '@shared/workspaces' import type { SessionContext } from '@shared/workspaces'
@@ -411,6 +412,8 @@ export function registerIpc(getWindow: () => BrowserWindow | null): void {
ipcMain.handle('general:get', (_event, options?: { refreshUsage?: boolean }) => generalView(options?.refreshUsage === true)) ipcMain.handle('general:get', (_event, options?: { refreshUsage?: boolean }) => generalView(options?.refreshUsage === true))
ipcMain.handle('personalization:get', () => personalization.getPersonalization()) ipcMain.handle('personalization:get', () => personalization.getPersonalization())
ipcMain.handle('personalization:update', (_event, patch: PersonalizationPatch) => personalization.updatePersonalization(patch)) ipcMain.handle('personalization:update', (_event, patch: PersonalizationPatch) => personalization.updatePersonalization(patch))
ipcMain.handle('agents:get', () => agentSettings.getAgentSettings())
ipcMain.handle('agents:update', (_event, patch: AgentSettingsPatch) => agentSettings.updateAgentSettings(patch))
ipcMain.handle('general:update', async (_event, patch: { autoLaunch?: boolean; preventSleep?: boolean }) => { ipcMain.handle('general:update', async (_event, patch: { autoLaunch?: boolean; preventSleep?: boolean }) => {
if (patch.preventSleep !== undefined) { if (patch.preventSleep !== undefined) {
await config.setPreventSleep(patch.preventSleep) await config.setPreventSleep(patch.preventSleep)
+4
View File
@@ -117,6 +117,10 @@ const api: YCodeApi = {
get: () => ipcRenderer.invoke('personalization:get'), get: () => ipcRenderer.invoke('personalization:get'),
update: (patch) => ipcRenderer.invoke('personalization:update', patch) update: (patch) => ipcRenderer.invoke('personalization:update', patch)
}, },
agents: {
get: () => ipcRenderer.invoke('agents:get'),
update: (patch) => ipcRenderer.invoke('agents:update', patch)
},
workspace: { workspace: {
get: () => ipcRenderer.invoke('workspace:get'), get: () => ipcRenderer.invoke('workspace:get'),
choose: () => ipcRenderer.invoke('workspace:choose'), choose: () => ipcRenderer.invoke('workspace:choose'),
@@ -0,0 +1,182 @@
/**
* 设置 → 智能体:WorkBuddy 5.8.3「智能体」页的 1:1 复刻(TODO 15)。
*
* REA 依据(设置窗口 900×680):
* - ev_bd0f4b5dac7d4e2b89b1b050db1896c65e03b4fd1c3d3d986a8958fd9c1a9883(浅色顶部 DOM/layout/computed style + CSS 源)
* - ev_3384aa03af8a1ccdffb8e025dbd4a354636d8d6b0c207abd21e12fc5eb2ae719(深色同页)
* - ev_01940b7925326ee8e7f7e4aba661be2a17a2f876d634d4d370d01b6616a4a7fd(团队管理下拉:开启 / 禁用)
* - ev_1cc896e04c0cc9ceab39b0a252f4982c019305609195ad44804ae9b5fb7379a8(最终浅色顶部)
*
* 页面只有三个区块:智能体团队管理(下拉)、经济模式(开关)、浏览器操控(Beta,三行扩展安装)。
* 用户限定:云端服务与子代理调度暂不处理。本轮只把「智能体团队管理」做成真实持久化偏好,
* 并在控件 title 说明多智能体执行器尚未接入;经济模式与浏览器操控保持原版布局、禁用并标注。
*/
import { useEffect, useRef, useState } from 'react'
import type { ReactNode } from 'react'
import type { AgentSettings } from '@shared/types'
import { IconSelectCaret } from './general-settings-icons'
import browserChromeIcon from './agents-assets/workbuddy-browser-chrome.svg'
import browserEdgeIcon from './agents-assets/workbuddy-browser-edge.svg'
import browserQqIcon from './agents-assets/workbuddy-browser-qq.png'
import './source-agents-panels.css'
const UNAVAILABLE_HINT = '暂未接入'
const UNSET_HINT = '尚未配置'
const BROWSER_ROWS: ReadonlyArray<{ key: string; icon: string; title: string; desc: string }> = [
{ key: 'chrome', icon: browserChromeIcon, title: 'YCode Chrome 扩展程序', desc: '在 Chrome 中安装此扩展程序,将 YCode 连接到你的浏览器。' },
{ key: 'edge', icon: browserEdgeIcon, title: 'YCode Edge 扩展程序', desc: '在 Edge 中安装此扩展程序,将 YCode 连接到你的浏览器。' },
{ key: 'qq', icon: browserQqIcon, title: 'YCode QQ 浏览器扩展程序', desc: '在 QQ 浏览器中安装此扩展程序,将 YCode 连接到你的浏览器。' }
]
function classNames(...values: Array<string | false | undefined>): string {
return values.filter(Boolean).join(' ')
}
function ExternalArrow(): React.JSX.Element {
return <svg className="wb-icon" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round"><path d="M5.25 3.75h7v7M12 4 4.25 11.75" /></svg>
}
function Section({ title, desc, action, stacked }: { title: string; desc?: ReactNode; action: ReactNode; stacked?: boolean }): React.JSX.Element {
return (
<section className={classNames('general-settings-panel__section', 'general-settings-panel__section--plain',
stacked ? 'general-settings-panel__section--stacked' : 'general-settings-panel__section--inline')}>
<div className="general-settings-panel__section-main">
<div className="general-settings-panel__section-copy">
<h3 className="general-settings-panel__section-title">{title}</h3>
{desc ? <p className="general-settings-panel__section-desc">{desc}</p> : null}
</div>
<div className="general-settings-panel__section-action">{action}</div>
</div>
</section>
)
}
function Switch({ checked }: { checked: boolean }): React.JSX.Element {
return (
<div className="general-settings-panel__switch-wrap extensions-settings-panel__unavailable-control"
data-settings-unavailable="true" aria-disabled="true" title={UNAVAILABLE_HINT}>
<label className="wb-switch wb-switch--large">
<span className="wb-switch__control">
<input className="wb-switch__input" type="checkbox" role="switch" aria-label="经济模式" checked={checked} disabled
data-settings-unavailable="true" />
<span className="wb-switch__track"><span className="wb-switch__thumb" /></span>
</span>
</label>
</div>
)
}
function UnavailableButton({ label, width, arrow = false }: { label: string; width: number; arrow?: boolean }): React.JSX.Element {
return (
<button type="button" className="wb-button wb-button--secondary wb-button--medium extensions-settings-panel__action-button"
style={{ minWidth: width }} aria-disabled="true" title={UNSET_HINT}
onClick={(event) => { event.preventDefault(); event.stopPropagation() }}>
<span className="wb-button__content">{label}</span>
{arrow ? <span className="wb-button__icon"><ExternalArrow /></span> : null}
</button>
)
}
function TeamSelect({ value, open, onToggle, onChoose }: {
value: boolean
open: boolean
onToggle(): void
onChoose(value: boolean): void
}): React.JSX.Element {
return (
<div className="extensions-settings-panel__team-dropdown">
<button type="button" className="wb-select wb-select--medium" aria-label="智能体团队管理" aria-haspopup="listbox"
aria-expanded={open} title="仅保存偏好;多智能体协作执行尚未接入" onClick={onToggle}>
<span className="wb-select__value">{value ? '开启' : '禁用'}</span>
<IconSelectCaret className="wb-icon wb-select__caret" />
</button>
{open ? (
<div className="wb-popover extensions-settings-panel__team-popover" role="listbox" aria-label="智能体团队管理">
{[true, false].map((option) => (
<button type="button" key={String(option)} role="option" aria-selected={option === value}
className={classNames('wb-select__option', option === value && 'wb-select__option--single-selected')}
onClick={() => onChoose(option)}>
<span className="extensions-settings-panel__option-text">{option ? '开启' : '禁用'}</span>
{option === value ? <span className="wb-select__check"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"><path d="M3.25 8.4 6.4 11.5 12.75 4.75" /></svg></span> : null}
</button>
))}
</div>
) : null}
</div>
)
}
export default function AgentsSettingsPanel(): React.JSX.Element {
const [view, setView] = useState<AgentSettings | null>(null)
const [error, setError] = useState('')
const [teamOpen, setTeamOpen] = useState(false)
const root = useRef<HTMLDivElement>(null)
useEffect(() => {
let active = true
void window.ycode.agents.get()
.then((next) => { if (active) setView(next) })
.catch((reason) => { if (active) setError(String((reason as Error)?.message ?? reason)) })
return () => { active = false }
}, [])
useEffect(() => {
if (!teamOpen) return
const outside = (event: PointerEvent): void => { if (!root.current?.contains(event.target as Node)) setTeamOpen(false) }
const escape = (event: KeyboardEvent): void => {
if (event.key !== 'Escape') return
event.preventDefault(); event.stopPropagation(); setTeamOpen(false)
}
document.addEventListener('pointerdown', outside, true)
document.addEventListener('keydown', escape, true)
return () => { document.removeEventListener('pointerdown', outside, true); document.removeEventListener('keydown', escape, true) }
}, [teamOpen])
const update = async (teamManagement: boolean): Promise<void> => {
try {
setView(await window.ycode.agents.update({ teamManagement }))
setError('')
} catch (reason) {
setError(String((reason as Error)?.message ?? reason))
}
}
return (
<div ref={root} className="general-settings-panel extensions-settings-panel">
{error ? <p role="alert">{error}</p> : null}
<div className="general-settings-panel__group-card">
<Section title="智能体团队管理"
desc="开启后,复杂任务会自动拆分多个智能体协作完成。专家团队任务不受此设置影响。"
action={<TeamSelect value={view?.teamManagement ?? true} open={teamOpen} onToggle={() => setTeamOpen((open) => !open)}
onChoose={(teamManagement) => { setTeamOpen(false); void update(teamManagement) }} />} />
</div>
<div className="general-settings-panel__group-card">
<Section title="经济模式"
desc="自动在合适时机触发上下文压缩,减少长对话中重复传输的上下文,降低积分消耗"
action={<Switch checked />} />
</div>
<div className="general-settings-panel__group extensions-settings-panel__browser-group">
<p className="general-settings-panel__group-label extensions-settings-panel__browser-label">浏览器操控<span className="extensions-settings-panel__beta">Beta</span></p>
<div className="general-settings-panel__group-card extensions-settings-panel__browser-card">
<Section title="让 YCode 控制浏览器" action={<UnavailableButton label="安装" width={68} />} />
<div className="extensions-settings-panel__browser-list">
{BROWSER_ROWS.map((row) => (
<div key={row.key} className={`extensions-settings-panel__browser-row extensions-settings-panel__browser-row--${row.key}`}>
<img className="wb-icon extensions-settings-panel__browser-icon" src={row.icon} alt="" aria-hidden="true"
width={36} height={36} draggable={false} />
<div className="extensions-settings-panel__browser-copy">
<div className="extensions-settings-panel__browser-title">{row.title}</div>
<div className="extensions-settings-panel__browser-desc">{row.desc}</div>
</div>
<UnavailableButton label="前往安装" width={102} arrow />
</div>
))}
</div>
</div>
</div>
</div>
)
}
@@ -6,6 +6,7 @@ import AppearanceSettingsPanel from './AppearanceSettingsPanel'
import ShortcutsSettingsPanel from './ShortcutsSettingsPanel' import ShortcutsSettingsPanel from './ShortcutsSettingsPanel'
import PersonalizationSettingsPanel from './PersonalizationSettingsPanel' import PersonalizationSettingsPanel from './PersonalizationSettingsPanel'
import AboutHelpSettingsPanel from './AboutHelpSettingsPanel' import AboutHelpSettingsPanel from './AboutHelpSettingsPanel'
import AgentsSettingsPanel from './AgentsSettingsPanel'
import SourceSettingsNavigation from './SourceSettingsNavigation' import SourceSettingsNavigation from './SourceSettingsNavigation'
import { useSharedTheme } from '../useSharedTheme' import { useSharedTheme } from '../useSharedTheme'
import { SOURCE_WINDOWS_SETTINGS_TABS } from './SourceSettingsNavigation' import { SOURCE_WINDOWS_SETTINGS_TABS } from './SourceSettingsNavigation'
@@ -56,7 +57,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi
return () => window.removeEventListener('keydown', escape) return () => window.removeEventListener('keydown', escape)
}, [onClose]) }, [onClose])
const modal = <section className={`settings-modal${nativeWindow ? ' settings-modal--window' : ''}`} role="dialog" aria-label="设置" aria-modal={!nativeWindow}> const modal = <section className={`settings-modal${nativeWindow ? ' settings-modal--window' : ''}`} role="dialog" aria-label="设置" aria-modal={!nativeWindow}>
<div className="settings-modal__nav"><SourceSettingsNavigation activeTab={section} supportedTabs={['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization', 'helpFeedback', 'getHelp']} visibleTabs={VISIBLE_SETTINGS_TABS} <div className="settings-modal__nav"><SourceSettingsNavigation activeTab={section} supportedTabs={['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization', 'extensions', 'helpFeedback', 'getHelp']} visibleTabs={VISIBLE_SETTINGS_TABS}
onTabChange={(id) => { setError(''); setSection(id === 'models' ? 'model' : id) }} onUnavailable={() => setError('该设置尚未接入')} /></div> onTabChange={(id) => { setError(''); setSection(id === 'models' ? 'model' : id) }} onUnavailable={() => setError('该设置尚未接入')} /></div>
<div className="settings-modal__content"><div className="settings-modal__panel"> <div className="settings-modal__content"><div className="settings-modal__panel">
{error && <p role="alert">{error}</p>} {error && <p role="alert">{error}</p>}
@@ -67,6 +68,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi
{section === 'keyboardShortcuts' && <ShortcutsSettingsPanel />} {section === 'keyboardShortcuts' && <ShortcutsSettingsPanel />}
{section === 'personalization' && <PersonalizationSettingsPanel />} {section === 'personalization' && <PersonalizationSettingsPanel />}
{(section === 'helpFeedback' || section === 'getHelp') && <AboutHelpSettingsPanel section={section} />} {(section === 'helpFeedback' || section === 'getHelp') && <AboutHelpSettingsPanel section={section} />}
{section === 'extensions' && <AgentsSettingsPanel />}
{section === 'about' && <div className="settings-window-panel"><h2>YCode</h2><p>{settings.appInfo?.version}</p></div>} {section === 'about' && <div className="settings-window-panel"><h2>YCode</h2><p>{settings.appInfo?.version}</p></div>}
</div></div> </div></div>
</section> </section>
@@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'><defs><linearGradient id='a' x1='3.2173' y1='15' x2='44.7812' y2='15' gradientUnits='userSpaceOnUse'><stop offset='0' stop-color='#d93025'/><stop offset='1' stop-color='#ea4335'/></linearGradient><linearGradient id='b' x1='20.7219' y1='47.6791' x2='41.5039' y2='11.6837' gradientUnits='userSpaceOnUse'><stop offset='0' stop-color='#fcc934'/><stop offset='1' stop-color='#fbbc04'/></linearGradient><linearGradient id='c' x1='26.5981' y1='46.5015' x2='5.8161' y2='10.506' gradientUnits='userSpaceOnUse'><stop offset='0' stop-color='#1e8e3e'/><stop offset='1' stop-color='#34a853'/></linearGradient></defs><circle cx='24' cy='23.9947' r='12' style='fill:#fff'/><path d='M3.2154,36A24,24,0,1,0,12,3.2154,24,24,0,0,0,3.2154,36ZM34.3923,18A12,12,0,1,1,18,13.6077,12,12,0,0,1,34.3923,18Z' style='fill:none'/><path d='M24,12H44.7812a23.9939,23.9939,0,0,0-41.5639.0029L13.6079,30l.0093-.0024A11.9852,11.9852,0,0,1,24,12Z' style='fill:url(#a)'/><circle cx='24' cy='24' r='9.5' style='fill:#1a73e8'/><path d='M34.3913,30.0029,24.0007,48A23.994,23.994,0,0,0,44.78,12.0031H23.9989l-.0025.0093A11.985,11.985,0,0,1,34.3913,30.0029Z' style='fill:url(#b)'/><path d='M13.6086,30.0031,3.218,12.006A23.994,23.994,0,0,0,24.0025,48L34.3931,30.0029l-.0067-.0068a11.9852,11.9852,0,0,1-20.7778.007Z' style='fill:url(#c)'/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'><defs><radialGradient id='b' cx='161.8' cy='68.9' r='95.4' gradientTransform='matrix(1 0 0 -.95 0 248.8)' gradientUnits='userSpaceOnUse'><stop offset='.7' stop-opacity='0'/><stop offset='.9' stop-opacity='.5'/><stop offset='1'/></radialGradient><radialGradient id='d' cx='-340.3' cy='63' r='143.2' gradientTransform='matrix(.15 -.99 -.8 -.12 176.6 -125.4)' gradientUnits='userSpaceOnUse'><stop offset='.8' stop-opacity='0'/><stop offset='.9' stop-opacity='.5'/><stop offset='1'/></radialGradient><radialGradient id='e' cx='113.4' cy='570.2' r='202.4' gradientTransform='matrix(-.04 1 2.13 .08 -1179.5 -106.7)' gradientUnits='userSpaceOnUse'><stop offset='0' stop-color='#35c1f1'/><stop offset='.1' stop-color='#34c1ed'/><stop offset='.2' stop-color='#2fc2df'/><stop offset='.3' stop-color='#2bc3d2'/><stop offset='.7' stop-color='#36c752'/></radialGradient><radialGradient id='f' cx='376.5' cy='568' r='97.3' gradientTransform='matrix(.28 .96 .78 -.23 -303.8 -148.5)' gradientUnits='userSpaceOnUse'><stop offset='0' stop-color='#66eb6e'/><stop offset='1' stop-color='#66eb6e' stop-opacity='0'/></radialGradient><linearGradient id='a' x1='63.3' y1='84' x2='241.7' y2='84' gradientTransform='matrix(1 0 0 -1 0 266)' gradientUnits='userSpaceOnUse'><stop offset='0' stop-color='#0c59a4'/><stop offset='1' stop-color='#114a8b'/></linearGradient><linearGradient id='c' x1='157.3' y1='161.4' x2='46' y2='40.1' gradientTransform='matrix(1 0 0 -1 0 266)' gradientUnits='userSpaceOnUse'><stop offset='0' stop-color='#1b9de2'/><stop offset='.2' stop-color='#1595df'/><stop offset='.7' stop-color='#0680d7'/><stop offset='1' stop-color='#0078d4'/></linearGradient></defs><path d='M235.7 195.5a93.7 93.7 0 0 1-10.6 4.7 101.9 101.9 0 0 1-35.9 6.4c-47.3 0-88.5-32.5-88.5-74.3a31.5 31.5 0 0 1 16.4-27.3c-42.8 1.8-53.8 46.4-53.8 72.5 0 74 68.1 81.4 82.8 81.4 7.9 0 19.8-2.3 27-4.6l1.3-.4a128.3 128.3 0 0 0 66.6-52.8 4 4 0 0 0-5.3-5.6Z' transform='translate(-4.6 -5)' style='fill:url(#a)'/><path d='M235.7 195.5a93.7 93.7 0 0 1-10.6 4.7 101.9 101.9 0 0 1-35.9 6.4c-47.3 0-88.5-32.5-88.5-74.3a31.5 31.5 0 0 1 16.4-27.3c-42.8 1.8-53.8 46.4-53.8 72.5 0 74 68.1 81.4 82.8 81.4 7.9 0 19.8-2.3 27-4.6l1.3-.4a128.3 128.3 0 0 0 66.6-52.8 4 4 0 0 0-5.3-5.6Z' transform='translate(-4.6 -5)' style='isolation:isolate;opacity:.35;fill:url(#b)'/><path d='M110.3 246.3A79.2 79.2 0 0 1 87.6 225a80.7 80.7 0 0 1 29.5-120c3.2-1.5 8.5-4.1 15.6-4a32.4 32.4 0 0 1 25.7 13 31.9 31.9 0 0 1 6.3 18.7c0-.2 24.5-79.6-80-79.6-43.9 0-80 41.6-80 78.2a130.2 130.2 0 0 0 12.1 56 128 128 0 0 0 156.4 67 75.5 75.5 0 0 1-62.8-8Z' transform='translate(-4.6 -5)' style='fill:url(#c)'/><path d='M110.3 246.3A79.2 79.2 0 0 1 87.6 225a80.7 80.7 0 0 1 29.5-120c3.2-1.5 8.5-4.1 15.6-4a32.4 32.4 0 0 1 25.7 13 31.9 31.9 0 0 1 6.3 18.7c0-.2 24.5-79.6-80-79.6-43.9 0-80 41.6-80 78.2a130.2 130.2 0 0 0 12.1 56 128 128 0 0 0 156.4 67 75.5 75.5 0 0 1-62.8-8Z' transform='translate(-4.6 -5)' style='opacity:.41;fill:url(#d);isolation:isolate'/><path d='M157 153.8c-.9 1-3.4 2.5-3.4 5.6 0 2.6 1.7 5.2 4.8 7.3 14.3 10 41.4 8.6 41.5 8.6a59.6 59.6 0 0 0 30.3-8.3 61.4 61.4 0 0 0 30.4-52.9c.3-22.4-8-37.3-11.3-43.9C228 28.8 182.3 5 132.6 5a128 128 0 0 0-128 126.2c.5-36.5 36.8-66 80-66 3.5 0 23.5.3 42 10a72.6 72.6 0 0 1 30.9 29.3c6.1 10.6 7.2 24.1 7.2 29.5s-2.7 13.3-7.8 19.9Z' transform='translate(-4.6 -5)' style='fill:url(#e)'/><path d='M157 153.8c-.9 1-3.4 2.5-3.4 5.6 0 2.6 1.7 5.2 4.8 7.3 14.3 10 41.4 8.6 41.5 8.6a59.6 59.6 0 0 0 30.3-8.3 61.4 61.4 0 0 0 30.4-52.9c.3-22.4-8-37.3-11.3-43.9C228 28.8 182.3 5 132.6 5a128 128 0 0 0-128 126.2c.5-36.5 36.8-66 80-66 3.5 0 23.5.3 42 10a72.6 72.6 0 0 1 30.9 29.3c6.1 10.6 7.2 24.1 7.2 29.5s-2.7 13.3-7.8 19.9Z' transform='translate(-4.6 -5)' style='fill:url(#f)'/></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -0,0 +1,7 @@
/* REA light=ev_1cc896e04c0cc9ceab39b0a252f4982c019305609195ad44804ae9b5fb7379a8; dark=ev_3384aa03af8a1ccdffb8e025dbd4a354636d8d6b0c207abd21e12fc5eb2ae719; ExtensionsSettingsPanel-BnsXh3oa.css sha256=cbd4b37c6061381943d96ca454176b2f083e49eb8e04c34cb5d31eef3dbcb8f5; tokens sha256=13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696. */
.extensions-settings-panel__browser-extension-status { box-sizing: border-box; min-width: 84px; height: 32px; padding: 0 var(--wb-spacing-4); color: var(--wb-color-text-secondary); font-size: var(--wb-font-body-size); font-weight: 400; line-height: var(--wb-font-body-line-height); white-space: nowrap; cursor: default; align-items: center; display: inline-grid; position: relative; }
.extensions-settings-panel__browser-extension-status::before { content: attr(data-install-label); visibility: hidden; padding-right: 20px; font-weight: 500; }
.extensions-settings-panel__browser-extension-status-content { justify-content: center; align-items: center; gap: var(--wb-spacing-3); display: flex; position: absolute; inset: 0px; }
.extensions-settings-panel__browser-extension-status-dot { background: var(--wb-status-success); border-radius: 50%; flex: 0 0 6px; width: 6px; height: 6px; }
.extensions-settings-panel{--wb-spacing-4:12px;--wb-color-text-secondary:var(--wb-palette-black-70);--wb-font-body-size:var(--wb-font-size-5);--wb-font-body-line-height:var(--wb-font-line-height-5-2);--wb-spacing-3:8px;--wb-status-success:var(--wb-palette-green-8);--wb-palette-black-70:#000000b3;--wb-font-size-5:14px;--wb-font-line-height-5-2:22px;--wb-palette-green-8:#0cbf5b;--wb-color-text-success-default:var(--wb-palette-green-8);--wb-palette-green-6:#47cf84}
[data-theme=dark] .extensions-settings-panel{--wb-spacing-4:12px;--wb-color-text-secondary:#ffffffa6;--wb-font-body-size:var(--wb-font-size-5);--wb-font-body-line-height:var(--wb-font-line-height-5-2);--wb-spacing-3:8px;--wb-status-success:var(--wb-color-text-success-default);--wb-palette-black-70:#000000b3;--wb-font-size-5:14px;--wb-font-line-height-5-2:22px;--wb-palette-green-8:#43f08f;--wb-color-text-success-default:var(--wb-palette-green-6);--wb-palette-green-6:#24c46c}
@@ -0,0 +1,181 @@
/*
* 设置 → 智能体(TODO 15)。
* - source-agents-panels-recovered.css:REA 回收的 ExtensionsSettingsPanel-BnsXh3oa.css 投影。
* - 卡片/section/按钮/开关所需的通用 UI kit 由 GeneralSettingsPanel 已加载的 source-general-settings.css 提供;
* 这里不再重复 @import,避免同一份通用样式表被打包多次改变层叠顺序。
*/
@import './source-agents-panels-recovered.css';
/*
* REA ev_3384aa…(深色):正文 color 为 rgb(242,243,245);原版设置窗口把 --wb-color-text-primary
* 覆盖为 #f2f3f5,比全局 tokens 的 #fff 更具体,这里按同一优先级还原。
*/
:root[data-theme=dark] .general-settings-panel.extensions-settings-panel {
--wb-color-text-primary: #f2f3f5;
}
/* ---- 智能体团队管理下拉 ---- */
.extensions-settings-panel__team-dropdown {
position: relative;
flex-shrink: 0;
}
.extensions-settings-panel__team-dropdown .wb-select {
width: 120px;
min-width: 120px;
height: 32px;
padding: 0 8px;
justify-content: space-between;
gap: 2px;
color: var(--wb-color-text-primary);
background: var(--wb-bg-primary);
border: 1px solid var(--wb-color-border-primary);
border-radius: 8px;
font-family: Arial, system-ui, sans-serif;
font-size: 14px;
line-height: 22px;
cursor: pointer;
display: inline-flex;
align-items: center;
}
:root[data-theme=dark] .extensions-settings-panel__team-dropdown .wb-select {
border-color: var(--wb-color-border-solid);
}
.extensions-settings-panel__team-popover {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 30;
box-sizing: border-box;
width: 120px;
padding: 4px;
background: var(--wb-bg-primary);
border: 1px solid var(--wb-border-default);
border-radius: 12px;
box-shadow: color(srgb 0 0 0 / 0.12) 0 8px 48px -4px, color(srgb 0 0 0 / 0.06) 0 6px 12px -4px;
}
.extensions-settings-panel__team-popover .wb-select__option {
box-sizing: border-box;
width: 100%;
height: 32px;
padding: 0 8px;
color: var(--wb-color-text-primary);
font: inherit;
font-size: 14px;
text-align: left;
background: none;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.extensions-settings-panel__team-popover .wb-select__option:hover {
background: var(--wb-bg-hover);
}
.extensions-settings-panel__team-popover .wb-select__check {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
}
/* ---- 浏览器操控(Beta) ---- */
.extensions-settings-panel__browser-label {
display: flex;
align-items: center;
gap: 4px;
}
.extensions-settings-panel__beta {
box-sizing: border-box;
padding: 0 8px;
color: var(--wb-color-text-tertiary);
background: var(--wb-bg-primary);
border: 1px solid var(--wb-border-default);
border-radius: 4px;
font-size: 11px;
line-height: 18px;
font-weight: 400;
}
.extensions-settings-panel__browser-card {
display: flex;
flex-direction: column;
}
.extensions-settings-panel__browser-card > .general-settings-panel__section .extensions-settings-panel__action-button {
width: 68px;
min-width: 68px;
}
.extensions-settings-panel__action-button {
flex-shrink: 0;
}
.extensions-settings-panel__browser-list {
box-sizing: border-box;
width: 100%;
height: 193px;
padding-top: 1px;
overflow: hidden;
border-radius: 8px;
box-shadow: inset 0 0 0 1px var(--wb-border-subtle);
}
.extensions-settings-panel__browser-row {
box-sizing: border-box;
width: 100%;
height: 68px;
display: flex;
align-items: center;
gap: 12px;
}
.extensions-settings-panel__browser-row:not(:last-child) {
border-bottom: 1px solid var(--wb-border-subtle);
}
.extensions-settings-panel__browser-row--qq {
height: 56px;
align-items: flex-start;
padding-top: 12px;
}
.extensions-settings-panel__browser-row--qq .extensions-settings-panel__action-button {
margin-top: 6px;
}
.extensions-settings-panel__browser-icon {
width: 36px;
height: 36px;
flex-shrink: 0;
display: block;
object-fit: contain;
}
.extensions-settings-panel__browser-copy {
flex: 1;
min-width: 0;
}
.extensions-settings-panel__browser-title {
color: var(--wb-color-text-primary);
font-size: 14px;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.extensions-settings-panel__browser-desc {
color: var(--wb-color-text-secondary);
font-size: 14px;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 未接入行为保持原版形态但禁用(与通用页约定一致)。 */
.extensions-settings-panel [data-settings-unavailable=true] { cursor: default }
.extensions-settings-panel [data-settings-unavailable=true] .wb-switch { cursor: default }
.general-settings-panel.extensions-settings-panel .extensions-settings-panel__unavailable-control
.wb-switch:has(.wb-switch__input:checked:disabled) .wb-switch__track {
background-color: #00c29a;
border-color: #00c29a;
}
:root[data-theme=dark] .general-settings-panel.extensions-settings-panel .extensions-settings-panel__unavailable-control
.wb-switch:has(.wb-switch__input:checked:disabled) .wb-switch__track {
background-color: #22cba8;
border-color: #22cba8;
}
+8 -1
View File
@@ -15,7 +15,9 @@ import type {
GeneralSettingsPatch, GeneralSettingsPatch,
GeneralSettingsView, GeneralSettingsView,
PersonalizationPatch, PersonalizationPatch,
PersonalizationSettings PersonalizationSettings,
AgentSettings,
AgentSettingsPatch
} from './types' } from './types'
import type { DownloadPayload, DownloadResult, ResourceEntry, ResourceRange, WorkspaceResource } from './resources' import type { DownloadPayload, DownloadResult, ResourceEntry, ResourceRange, WorkspaceResource } from './resources'
import type { ChatAttachment, InteractionResponse, MessageQuote, PermissionMode } from './conversation' import type { ChatAttachment, InteractionResponse, MessageQuote, PermissionMode } from './conversation'
@@ -120,6 +122,11 @@ export interface YCodeApi {
get(): Ipc<PersonalizationSettings> get(): Ipc<PersonalizationSettings>
update(patch: PersonalizationPatch): Ipc<PersonalizationSettings> update(patch: PersonalizationPatch): Ipc<PersonalizationSettings>
} }
/** 设置 → 智能体:智能体团队管理偏好。 */
agents: {
get(): Ipc<AgentSettings>
update(patch: AgentSettingsPatch): Ipc<AgentSettings>
}
workspace: { workspace: {
get(): Ipc<WorkspaceInfo> get(): Ipc<WorkspaceInfo>
choose(): Ipc<WorkspaceInfo & { changed: boolean }> choose(): Ipc<WorkspaceInfo & { changed: boolean }>
+11
View File
@@ -172,6 +172,17 @@ export interface PersonalizationSettings {
export type PersonalizationPatch = Partial<PersonalizationSettings> export type PersonalizationPatch = Partial<PersonalizationSettings>
/**
* 设置 → 智能体(TODO 15)。
* 本轮只接入「智能体团队管理」这一个偏好;经济模式与浏览器操控仅按原版布局呈现并明确标注未接入。
*/
export interface AgentSettings {
/** 开启后复杂任务自动拆分多个智能体协作完成(原版默认开启)。 */
teamManagement: boolean
}
export type AgentSettingsPatch = Partial<AgentSettings>
/** 主进程 → 渲染进程的流式事件 */ /** 主进程 → 渲染进程的流式事件 */
export type ChatEvent = { sessionId?: string; turnDurationMs?: number } & ( export type ChatEvent = { sessionId?: string; turnDurationMs?: number } & (
| { runId: string; type: 'started'; messageId: string } | { runId: string; type: 'started'; messageId: string }