feat(settings): replicate the WorkBuddy agents panel and persist team-management preference (TODO 15)
This commit is contained in:
1 parent
b3be45715e
commit
143516b3a8
33 files changed
+1071
-21
No files matched your search
@@ -452,11 +452,32 @@
|
||||
|
||||
### 15. 智能体页面:团队管理
|
||||
|
||||
- [ ] 拆解原版智能体设置页面,本轮仅对接“智能体团队管理”。
|
||||
- [ ] 根据 REA 证据梳理团队列表、详情及管理操作,将需要的真实存储/配置链路接入。
|
||||
- [ ] 验收:仅展示已确定范围;管理操作实际生效、重启后可恢复;团队管理与真实子代理调度的完成度分别记录。
|
||||
- [x] 拆解原版智能体设置页面,本轮仅对接“智能体团队管理”。
|
||||
- [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. 移除助理与设备连接
|
||||
|
||||
@@ -589,7 +610,7 @@
|
||||
## 推进顺序
|
||||
|
||||
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 保留为待拆解,后续单独展开。
|
||||
|
||||
## 每项交付记录模板
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@
|
||||
"rea:css:prepare": "node scripts/rea/prepare-css-runtime.mjs",
|
||||
"rea:css:capture": "node scripts/rea/capture-css.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: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",
|
||||
|
||||
@@ -19,6 +19,8 @@ try {
|
||||
const main={frame_id:'main',file_path:await fs.realpath(html)},frame={id:'main',url:page}
|
||||
const png='iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jKXcAAAAASUVORK5CYII='
|
||||
const bytes=Buffer.from(png,'base64'),digest=crypto.createHash('sha256').update(bytes).digest('hex')
|
||||
const svgBytes=Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7"/></svg>')
|
||||
const svgContent=svgBytes.toString('base64'),svgDigest=crypto.createHash('sha256').update(svgBytes).digest('hex')
|
||||
const makeSnapshot=()=>({strings:[page,'main','HTML','IMG','class','selected','src','fallback.png',resourceUrl,'contenteditable','true','14px'],documents:[{
|
||||
documentURL:0,frameId:1,scrollOffsetX:0,scrollOffsetY:0,
|
||||
nodes:{nodeName:[2,3],parentIndex:[-1,0],backendNodeId:[10,20],attributes:[[],[4,5,6,7]],currentSourceURL:{index:[1],value:[8]}},
|
||||
@@ -34,10 +36,10 @@ try {
|
||||
if(method==='DOM.describeNode')return {node:{nodeName:mode==='not-img'?'DIV':'IMG',backendNodeId:mode==='foreign-backend'?999:20}}
|
||||
if(method==='Page.getLayoutMetrics')return {cssLayoutViewport:{clientWidth:100,clientHeight:80,pageX:0,pageY:0}}
|
||||
if(method==='DOM.getNodeForLocation')return {backendNodeId:mode==='covered'?999:mode==='partly-covered'&¶ms.y>20?999:20}
|
||||
if(method==='Page.getResourceTree')return {frameTree:{frame,resources:mode==='not-loaded'?[]:[{url:selectedResourceUrl,type:'Image',mimeType:mode==='svg'?'image/svg+xml':'image/png',...(mode==='declared-oversize'?{contentSize:2097153}:{})}],childFrames:[{frame:{id:'foreign',url:'https://foreign.invalid'},resources:[{url:resourceUrl,type:'Image',mimeType:'image/png'}]}]}}
|
||||
if(method==='Page.getResourceTree')return {frameTree:{frame,resources:mode==='not-loaded'?[]:[{url:selectedResourceUrl,type:'Image',mimeType:mode==='svg'?'image/svg+xml':mode==='unsupported'?'image/bmp':'image/png',...(mode==='declared-oversize'?{contentSize:2097153}:{})}],childFrames:[{frame:{id:'foreign',url:'https://foreign.invalid'},resources:[{url:resourceUrl,type:'Image',mimeType:'image/png'}]}]}}
|
||||
if(method==='Page.getResourceContent'){
|
||||
if(mode==='content-unavailable')throw Error('private-url-or-cookie-must-not-leak')
|
||||
return {base64Encoded:mode!=='not-binary',content:mode==='invalid-base64'?'not-base64':mode==='signature'?'YWJj':mode==='oversize'?Buffer.alloc(2097153).toString('base64'):png}
|
||||
return {base64Encoded:mode!=='not-binary',content:mode==='invalid-base64'?'not-base64':mode==='signature'?'YWJj':mode==='oversize'?Buffer.alloc(2097153).toString('base64'):mode==='svg'?svgContent:png}
|
||||
}
|
||||
if(method==='DOMSnapshot.captureSnapshot'){const copy=structuredClone(snapshot);for(const doc of copy.documents)doc.layout.styles=doc.layout.nodeIndex.map(()=>params.computedStyles.map(()=>11));return copy}
|
||||
if(method==='Page.captureScreenshot')return {data:png}
|
||||
@@ -58,12 +60,17 @@ try {
|
||||
assert.equal(result.total_bytes,bytes.length)
|
||||
assert.equal(calls.find(c=>c.method==='Page.getResourceContent').params.url,resourceUrl,'currentSrc wins over the fallback src attribute')
|
||||
assert.equal(result.items[0].backend_node_id,20)
|
||||
mode='svg';result=await capture()
|
||||
assert.equal(result.items[0].status,'exported')
|
||||
assert.equal(result.items[0].artifact.mime_type,'image/svg+xml')
|
||||
assert.equal(result.items[0].artifact.sha256,svgDigest)
|
||||
assert.equal(result.items[0].artifact.data_base64,svgContent)
|
||||
mode='covered';result=await capture();assert.equal(result.items[0].status,'exported');assert.equal(result.items[0].visibility.status,'covered');assert.equal(result.items[0].selected_node.current_source_url,resourceUrl);
|
||||
mode='partly-covered';assert.equal((await capture()).items[0].status,'exported','partial occlusion may still export actual loaded full image bytes')
|
||||
for(const [test,reason] of [['absent','selector_absent'],['ambiguous','selector_ambiguous'],['not-img','selected_node_not_img'],['foreign-backend','snapshot_identity_unavailable'],['not-loaded','selected_source_not_loaded_in_main_frame'],['svg','unsupported_image_type'],['declared-oversize','asset_byte_budget_exceeded'],['oversize','asset_byte_budget_exceeded'],['signature','image_signature_mismatch'],['invalid-base64','invalid_base64'],['not-binary','binary_content_unavailable'],['content-unavailable','protocol_resource_unavailable'],['navigation-after-content','frame_changed']]){
|
||||
for(const [test,reason] of [['absent','selector_absent'],['ambiguous','selector_ambiguous'],['not-img','selected_node_not_img'],['foreign-backend','snapshot_identity_unavailable'],['not-loaded','selected_source_not_loaded_in_main_frame'],['unsupported','unsupported_image_type'],['declared-oversize','asset_byte_budget_exceeded'],['oversize','asset_byte_budget_exceeded'],['signature','image_signature_mismatch'],['invalid-base64','invalid_base64'],['not-binary','binary_content_unavailable'],['content-unavailable','protocol_resource_unavailable'],['navigation-after-content','frame_changed']]){
|
||||
mode=test;result=await capture();assert.equal(result.items[0].status,'unknown',test);assert.equal(result.items[0].reason,reason,test);assert.equal(result.total_bytes,0,test)
|
||||
assert.equal(result.items[0].artifact,undefined,test)
|
||||
if(['not-loaded','svg','declared-oversize'].includes(test))assert.equal(calls.some(c=>c.method==='Page.getResourceContent'),false,test+' must reject before the read')
|
||||
if(['not-loaded','unsupported','declared-oversize'].includes(test))assert.equal(calls.some(c=>c.method==='Page.getResourceContent'),false,test+' must reject before the read')
|
||||
assert.equal(JSON.stringify(result).includes('private-url-or-cookie'),false)
|
||||
}
|
||||
mode='ok';snapshot=makeSnapshot();snapshot.documents[0].nodes.attributes[0]=[9,10]
|
||||
@@ -98,5 +105,5 @@ try {
|
||||
assert.equal(optedOut.visuals.media_assets,undefined)
|
||||
assert.equal(JSON.stringify(optedOut.visuals).includes('fallback.png'),false)
|
||||
assert.equal(listener,undefined)
|
||||
console.log('REA selected IMG resource regression passed: opt-in/schema/Evidence, snapshot currentSrc/backend attribution, covered bytes with separate visibility, scope, binary budgets/signatures, URL credential redaction, unknowns and default URL omission')
|
||||
console.log('REA selected IMG resource regression passed: opt-in/schema/Evidence, snapshot currentSrc/backend attribution, raster/SVG export, covered bytes with separate visibility, scope, binary budgets/signatures, URL credential redaction, unknowns and default URL omission')
|
||||
}finally{await fs.rm(temporary,{recursive:true,force:true})}
|
||||
@@ -4,13 +4,15 @@ import { decodeCanonicalBase64 } from '../domain/webScreenshot.js';
|
||||
|
||||
const MAX_ASSETS=4, MAX_ASSET_BYTES=2*1024*1024, MAX_TOTAL_BYTES=8*1024*1024;
|
||||
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
||||
const types=new Set(['image/png','image/jpeg','image/gif','image/webp']);
|
||||
const types=new Set(['image/png','image/jpeg','image/gif','image/webp','image/svg+xml']);
|
||||
const mime=value=>value==='image/jpg'?'image/jpeg':value;
|
||||
function sniff(bytes){
|
||||
if(bytes.length>=24&&bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))return 'image/png';
|
||||
if(bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255)return 'image/jpeg';
|
||||
if(bytes.length>=10&&/^GIF8[79]a$/.test(bytes.toString('ascii',0,6)))return 'image/gif';
|
||||
if(bytes.length>=12&&bytes.toString('ascii',0,4)==='RIFF'&&bytes.toString('ascii',8,12)==='WEBP')return 'image/webp';
|
||||
const text=bytes.toString('utf8',0,Math.min(bytes.length,4096)).replace(/^\uFEFF/,'').trimStart();
|
||||
if(/^(?:<\?xml[\s\S]*?\?>\s*)?<svg(?:\s|>)/i.test(text))return 'image/svg+xml';
|
||||
}
|
||||
function safeSourceUrl(value){
|
||||
try{const u=new URL(value);if(!['file:','http:','https:'].includes(u.protocol))return null;u.username='';u.password='';u.search='';u.hash='';const result=u.href;return result.length<=2048?result:null;}catch{return null;}
|
||||
@@ -22,7 +24,7 @@ export async function captureElectronMediaAssets(snapshot,frames,selectors,send)
|
||||
const result={items:[],total_bytes:0,limits:{max_assets:MAX_ASSETS,max_asset_bytes:MAX_ASSET_BYTES,max_total_bytes:MAX_TOTAL_BYTES},limitations:[
|
||||
'Opt-in IMG resources only: exact unique selectors, authorized main file frame and snapshot backend identity. Visibility is reported separately and does not authorize new requests.',
|
||||
'Only Page.getResourceTree resources already loaded by the selected frame are read with Page.getResourceContent. No HTTP fetch, cookies, headers, input values or renderer JavaScript are used.',
|
||||
'PNG/JPEG/GIF/WebP only; SVG, canvas, video, CSS backgrounds and resources missing from the loaded frame tree remain unknown. Covered loaded images can be exported; bytes do not establish that this image is the currently visible layer.',
|
||||
'PNG/JPEG/GIF/WebP/SVG only; canvas, video, CSS backgrounds and resources missing from the loaded frame tree remain unknown. Covered loaded images can be exported; bytes do not establish that this image is the currently visible layer.',
|
||||
'Resource URLs in metadata omit credentials, query strings and fragments; URL SHA-256 identifies the exact selected URL. DOM and resource observations are sequential.'
|
||||
]};
|
||||
const main=frames[0],strings=snapshot.strings??[],str=index=>strings[index]??'';
|
||||
|
||||
@@ -32,7 +32,7 @@ const mediaIdentity = {
|
||||
};
|
||||
export const electronMediaAssetsSchema = z.object({
|
||||
items:z.array(z.discriminatedUnion('status',[
|
||||
z.object({...mediaIdentity,status:z.literal('exported'),artifact:z.object({mime_type:z.enum(['image/png','image/jpeg','image/gif','image/webp']),bytes:z.number().int().positive().max(2097152),sha256:z.string().regex(/^[a-f0-9]{64}$/),data_base64:z.string().max(2796204)})}),
|
||||
z.object({...mediaIdentity,status:z.literal('exported'),artifact:z.object({mime_type:z.enum(['image/png','image/jpeg','image/gif','image/webp','image/svg+xml']),bytes:z.number().int().positive().max(2097152),sha256:z.string().regex(/^[a-f0-9]{64}$/),data_base64:z.string().max(2796204)})}),
|
||||
z.object({...mediaIdentity,status:z.literal('unknown'),reason:z.string()})
|
||||
])).max(4),
|
||||
total_bytes:z.number().int().min(0).max(8388608),
|
||||
|
||||
@@ -135,7 +135,7 @@ await patch('dist/cli/electronCommands.js', text => once(text,
|
||||
' include_script_sources: options.includeScriptSources,\n include_style_sources: options.includeStyleSources,\n include_visuals: options.includeVisuals,\n ...(options.visualProbe ? {visual_probe: JSON.parse(options.visualProbe)} : {}),'))
|
||||
await patch('dist/contracts/javascript/electronToolContracts.js', text => once(text,
|
||||
'Returns frames, DOM structure, resource paths, and scripts without evaluating renderer JavaScript or invoking Electron APIs.',
|
||||
'Returns frames, DOM structure, resource paths, scripts and optional attributed CSS sources without evaluating renderer JavaScript or invoking Electron APIs. Default inspection is passive. include_style_sources selects CSS text; include_visuals selects viewport PNG and layout. Optional visual_probe declares native hover/click/scroll/key/viewport actions, sequential PNG samples, and actual platform fonts; UI actions can change application state and restoration is limited. visual_probe.media_selectors explicitly selects at most four unique main-frame IMG nodes and their already loaded PNG/JPEG/GIF/WebP resources, with byte budgets, SHA/base64 and visibility reported separately; no HTTP fetch is performed. These are local REA extensions; inspect coverage, budgets, restoration and limitations.'))
|
||||
'Returns frames, DOM structure, resource paths, scripts and optional attributed CSS sources without evaluating renderer JavaScript or invoking Electron APIs. Default inspection is passive. include_style_sources selects CSS text; include_visuals selects viewport PNG and layout. Optional visual_probe declares native hover/click/scroll/key/viewport actions, sequential PNG samples, and actual platform fonts; UI actions can change application state and restoration is limited. visual_probe.media_selectors explicitly selects at most four unique main-frame IMG nodes and their already loaded PNG/JPEG/GIF/WebP/SVG resources, with byte budgets, SHA/base64 and visibility reported separately; no HTTP fetch is performed. These are local REA extensions; inspect coverage, budgets, restoration and limitations.'))
|
||||
|
||||
const styleModule = await fs.readFile(path.join(patchRoot, 'CdpElectronStyles.js'), 'utf8')
|
||||
await fs.writeFile(path.join(output, 'dist/browser/CdpElectronStyles.js'), styleModule)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"media_selectors": [
|
||||
"#settings-browser-extension-chrome img",
|
||||
"#settings-browser-extension-edge img",
|
||||
"#settings-browser-extension-qq-browser img"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "wait", "step_id": "observe-loaded-browser-extension-images", "duration_ms": 250, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -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 }
|
||||
]
|
||||
}
|
||||
@@ -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))
|
||||
@@ -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}`)
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -125,10 +125,10 @@ if (!process.versions.electron) {
|
||||
// 原版 4 组;about 组按组件约定渲染为分隔线而不是标题。
|
||||
check('nav keeps the four recovered groups', [nav.groups.length, nav.dividers], [3, 1])
|
||||
// 通用(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 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 read(`(()=>{const input=document.querySelector('.settings-navigation input[type="search"]');
|
||||
|
||||
@@ -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
@@ -10,7 +10,7 @@
|
||||
import { app, safeStorage } from 'electron'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import type { GatewayConfig, GatewayConfigView, PersonalizationSettings } from '@shared/types'
|
||||
import type { AgentSettings, GatewayConfig, GatewayConfigView, PersonalizationSettings } from '@shared/types'
|
||||
|
||||
interface StoredConfig {
|
||||
speechBaseUrl?: string
|
||||
@@ -28,6 +28,8 @@ interface StoredConfig {
|
||||
autoLaunch?: boolean
|
||||
/** 设置 → 个性化(身份、回复风格、指令、人设、长期记忆)。 */
|
||||
personalization?: Partial<PersonalizationSettings>
|
||||
/** 设置 → 智能体(智能体团队管理偏好)。 */
|
||||
agentSettings?: Partial<AgentSettings>
|
||||
}
|
||||
|
||||
const DEFAULTS: StoredConfig = {
|
||||
@@ -131,6 +133,16 @@ export async function setStoredPersonalization(value: Partial<PersonalizationSet
|
||||
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> {
|
||||
const stored = await load()
|
||||
|
||||
+4
-1
@@ -11,7 +11,7 @@ import { BrowserWindow, app, dialog, ipcMain, shell, type OpenDialogOptions } fr
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { unlink, writeFile } from 'node:fs/promises'
|
||||
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 type { ConfigPatchLike, EditAction, SendPayload, WorkspaceAttachResult } from '@shared/ipc'
|
||||
import { applyAssistantEvent } from '@shared/message-parts'
|
||||
@@ -35,6 +35,7 @@ import * as models from './models'
|
||||
import * as workspaces from './workspaces'
|
||||
import * as generalSettings from './general-settings'
|
||||
import * as personalization from './personalization'
|
||||
import * as agentSettings from './agent-settings'
|
||||
import { prepareOmpModel } from './omp-model'
|
||||
import type { CustomModelInput, ResolvedModel } from '@shared/models'
|
||||
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('personalization:get', () => personalization.getPersonalization())
|
||||
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 }) => {
|
||||
if (patch.preventSleep !== undefined) {
|
||||
await config.setPreventSleep(patch.preventSleep)
|
||||
|
||||
@@ -117,6 +117,10 @@ const api: YCodeApi = {
|
||||
get: () => ipcRenderer.invoke('personalization:get'),
|
||||
update: (patch) => ipcRenderer.invoke('personalization:update', patch)
|
||||
},
|
||||
agents: {
|
||||
get: () => ipcRenderer.invoke('agents:get'),
|
||||
update: (patch) => ipcRenderer.invoke('agents:update', patch)
|
||||
},
|
||||
workspace: {
|
||||
get: () => ipcRenderer.invoke('workspace:get'),
|
||||
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 PersonalizationSettingsPanel from './PersonalizationSettingsPanel'
|
||||
import AboutHelpSettingsPanel from './AboutHelpSettingsPanel'
|
||||
import AgentsSettingsPanel from './AgentsSettingsPanel'
|
||||
import SourceSettingsNavigation from './SourceSettingsNavigation'
|
||||
import { useSharedTheme } from '../useSharedTheme'
|
||||
import { SOURCE_WINDOWS_SETTINGS_TABS } from './SourceSettingsNavigation'
|
||||
@@ -56,7 +57,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi
|
||||
return () => window.removeEventListener('keydown', escape)
|
||||
}, [onClose])
|
||||
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>
|
||||
<div className="settings-modal__content"><div className="settings-modal__panel">
|
||||
{error && <p role="alert">{error}</p>}
|
||||
@@ -67,6 +68,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi
|
||||
{section === 'keyboardShortcuts' && <ShortcutsSettingsPanel />}
|
||||
{section === 'personalization' && <PersonalizationSettingsPanel />}
|
||||
{(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>}
|
||||
</div></div>
|
||||
</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
@@ -15,7 +15,9 @@ import type {
|
||||
GeneralSettingsPatch,
|
||||
GeneralSettingsView,
|
||||
PersonalizationPatch,
|
||||
PersonalizationSettings
|
||||
PersonalizationSettings,
|
||||
AgentSettings,
|
||||
AgentSettingsPatch
|
||||
} from './types'
|
||||
import type { DownloadPayload, DownloadResult, ResourceEntry, ResourceRange, WorkspaceResource } from './resources'
|
||||
import type { ChatAttachment, InteractionResponse, MessageQuote, PermissionMode } from './conversation'
|
||||
@@ -120,6 +122,11 @@ export interface YCodeApi {
|
||||
get(): Ipc<PersonalizationSettings>
|
||||
update(patch: PersonalizationPatch): Ipc<PersonalizationSettings>
|
||||
}
|
||||
/** 设置 → 智能体:智能体团队管理偏好。 */
|
||||
agents: {
|
||||
get(): Ipc<AgentSettings>
|
||||
update(patch: AgentSettingsPatch): Ipc<AgentSettings>
|
||||
}
|
||||
workspace: {
|
||||
get(): Ipc<WorkspaceInfo>
|
||||
choose(): Ipc<WorkspaceInfo & { changed: boolean }>
|
||||
|
||||
@@ -172,6 +172,17 @@ export interface 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 } & (
|
||||
| { runId: string; type: 'started'; messageId: string }
|
||||
|
||||
Reference in new issue
Block a user