diff --git a/docs/复刻待办清单-2026-10-17.md b/docs/复刻待办清单-2026-10-17.md index 43a9c3a..2fbd289 100644 --- a/docs/复刻待办清单-2026-10-17.md +++ b/docs/复刻待办清单-2026-10-17.md @@ -303,11 +303,38 @@ ### 10. 通用 -- [ ] 拆解原版通用设置页面的布局、控件和交互。 -- [ ] 将 YCode 当前已经支持的内容接入对应控件,记录暂未支持的差异。 -- [ ] 验收:控件反映真实配置;修改即时生效或明确提示生效时机,重启后持久化正确。 +- [x] 拆解原版通用设置页面的布局、控件和交互。 +- [x] 将 YCode 当前已经支持的内容接入对应控件,记录暂未支持的差异。 +- [x] 验收:控件反映真实配置;修改即时生效或明确提示生效时机,重启后持久化正确。 -状态:待 REA 拆解与能力盘点。 +状态:已实现并通过真实窗口回归;未接入项按「保持原版外观 + 明确标注」处理,逐项记录在下方。 + +原版证据: +- REA `ev_a2dbeb23e30b9770743e857279a2e43da38fcc48d10612f75ace2938c5b29114`(WorkBuddy 5.8.3 设置→通用面板,900×680,自顶向底滚动采样 0/400/795/0,帧 frame-000~003):16 行文案、控件类型与取值、分组标题(常规/权限/存储/通知/隐私)、滚动边界。 +- REA `ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e`(同页 DOM 边界、computed style、svg_nodes):面板 `[232,40,638,1415]`(5.8.2 复核)、组 `[40/218] [274/514] [804/265] [1085/234] [1335/120]`、行高 `[44,68,52 | 56,68,68,76,68,68,56 | 56,68,56 | 66]`、卡片 `#fff / radius 12 / padding 12px 16px`、标题 14px/500/22px、描述 14px/400/22px `rgba(0,0,0,.5)`、开关 34×20、下拉 120×32、滑块 224×40、存储卡 638×235、路径输入 530×32、更改按钮 68×32、打开目录 76×22、通知按钮 130×32。 +- REA `ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94`:面板与控件样式来源(`contexts-BkjwYSZT.css` sha256 `50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380`、`tokens-Df-i-zsO.css` sha256 `13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696`、UI kit `src-Bh3YZ35H.css` sha256 `88dbdc1cd3f59d0e8b75824512a465a2be714bc7af481d790fe3a9484f85e01a`)。 +- 观察范围:桌面端浅色主题(另加暗色抽查)、900×680 设置窗口、通用页默认态;未覆盖:各下拉展开态的选项列表(`general-settings-panel__link-open-select-popover` / `power-save-blocker-settings__popover` 均为 368 宽,规则已回收但本轮不接入)。 + +实现: +- `scripts/rea/recover-general-settings.mjs`:从上述 REA 回收样式表投影出面板规则(149 条)+ UI kit 控件规则 + 面板作用域 token,写入 `src/renderer/src/components/source-general-settings-recovered.css`;UI kit 规则用 `:where(.general-settings-panel)` 前缀并排在面板规则之前,以保持原版同权重、面板后加载的层叠(例如 `.wb-storage__open-folder{height:auto}` 必须压过 `.wb-button--medium{height:32px}`)。 +- `src/renderer/src/components/GeneralSettingsPanel.tsx`(+ `source-general-settings.css`、`general-settings-icons.tsx`):按原版 DOM/类名/几何复刻 16 行;品牌替换遵循既有约定(「WorkBuddy 默认工作空间存储路径」→「YCode 默认工作空间存储路径」)。 +- `src/main/general-settings.ts`、`src/main/config.ts`、`src/main/ipc.ts`、`src/main/index.ts`、`src/shared/{types,ipc}.ts`、`src/preload/index.ts`:新增 `window.ycode.general` API(get/update/openFolder/changeWorkspaceRoot/openSystemNotificationSettings)。 +- 已接入(真实行为、重启持久化): + 1. 运行时防止系统休眠 → `config.preventSleep`;任务运行期间主进程持有 `powerSaveBlocker`(`updateActiveRuns` 挂在 `chat:send` 运行生命周期上)。 + 2. 开机自动启动 → `config.autoLaunch` + 系统登录项(`app.setLoginItemSettings`),读取时以系统登录项真实状态为准。 + 3. 系统缓存目录 → 真实数据目录(`app.getPath('userData')`)、真实占用与磁盘容量(递归扫描 + `fs.statfs`)、「打开目录」调用系统文件管理器。 + 4. 默认工作空间存储路径 → `config.workspaceRoot`,只读填充态输入框 + 「更改」走系统目录选择器。 + 5. 桌面通知 → 「前往系统设置」打开系统通知设置页(Windows `ms-settings:notifications`)。 +- 未接入(原版行为未实现,因此保留原版外观、控件类型与初始态,但标记 `aria-disabled` + `title="暂未接入"`,且不产生任何效果):语言、链接打开方式、字体大小、允许锁屏运行、网络代理、自动安装可信技能、技能自动更新、套件自动更新、通知提示音、定时任务产物通知、体验优化计划(含「了解更多」内联链接)。共 11 项。 + +验证:`scripts/verify-general-settings.cjs` 在真实隔离 Electron(真实 preload/IPC/渲染进程/文件系统 + 原生设置窗口,视口 900×680)中 43 项断言全部通过:导航「通用」可用且为当前页;面板列宽 638、顶层级顺序 group/group/storage/group/group;4 个分组标题;14 行标题与行高逐一匹配;分组标签 638×22 且左内边距 8px;卡片 `12px 16px`/radius 12/白底;标题 14/500/22、描述 14/400/22 三级色;开关 34×20;5 个下拉 120×32;滑块 224×40 且标签 小/默认/大;通知按钮 130×32 且带尾部图标;存储卡标题/说明/76×22 打开目录/530×32 输入框/68×32 更改/填充态样式;用量文案与真实数据目录;进度条 606×4 三段合计 604;分隔线 606×1;11 个占位项全部 `aria-disabled` + 「暂未接入」且点击无效(下拉不弹层、开关不切换);防休眠与开机自启动开关可切换、写入 `config.json` 并可还原;工作空间路径改写真实持久化(含 `workspace.get()` 与 `config.json`);重开设置窗口后偏好与路径仍正确渲染;暗色主题下面板卡片与标题重新着色;无渲染器报错。报告 `.tmp/general-settings-2elYNv/report.json`。 +回归:`npm run typecheck`、`npm run build`、`npm run test:chat`、`npm run test:conversation`、`npm run test:conversation-ipc`、`node scripts/verify-settings-nav.cjs`、`node scripts/verify-composer-context-info.cjs`、`node scripts/verify-account-menu.cjs` 全部通过。`test:conversation-ipc` 的 electron mock 因新增 `powerSaveBlocker` 依赖而先失败,已在 `scripts/conversation-ipc-regression.ts` 补齐 mock(`powerSaveBlocker`、`app.isPackaged/isReady/getAppPath/getLoginItemSettings/setLoginItemSettings`、`shell.openExternal`)。 + +遗留: +1. 系统级写入的自动化隔离:验收时以 `YCODE_DISABLE_SYSTEM_INTEGRATION=1` 跳过登录项与系统设置页的真实写入,因此「写入注册表登录项」「打开 ms-settings:notifications」两条系统副作用只在人工路径覆盖,未纳入自动化断言。 +2. 「更改」按钮会弹出系统目录选择器,无法自动点击;脚本改为断言按钮可用 + 通过 IPC 验证路径持久化,并在重开窗口后断言面板渲染新路径。 +3. 数据目录路径较长时,缓存目录说明行会像原版一样换行(原版 `.wb-storage__path-text` 无 nowrap);脚本按实际行高换算期望的面板/卡片高度(QA 沙箱路径下为 1415+22=1437)。 +4. 若后续要把语言/链接打开方式/网络代理/通知提示音等下拉接入,需要先按 REA 采集各下拉的展开态选项列表(当前仅有弹层容器 368 宽的样式规则)。 ### 11. 外观与浅色/深色主题 @@ -455,7 +482,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 → 15 → 19。 3. 14、18、20 保留为待拆解,后续单独展开。 ## 每项交付记录模板 diff --git a/scripts/conversation-ipc-regression.ts b/scripts/conversation-ipc-regression.ts index 9ad85a6..452b1a9 100644 --- a/scripts/conversation-ipc-regression.ts +++ b/scripts/conversation-ipc-regression.ts @@ -138,7 +138,12 @@ async function buildBundle(tempRoot: string, state: TestState, name: string): Pr const state = globalThis[${stateKey}]; export const app = { getPath: (name) => name === 'userData' ? state.userData : name === 'home' ? state.home : state.userData, - getName: () => 'YCode', getVersion: () => '0.1.0' + getName: () => 'YCode', getVersion: () => '0.1.0', + isPackaged: false, + isReady: () => true, + getAppPath: () => state.userData, + getLoginItemSettings: () => ({ openAtLogin: false }), + setLoginItemSettings: () => {} }; export const safeStorage = { isEncryptionAvailable: () => false, @@ -150,7 +155,12 @@ async function buildBundle(tempRoot: string, state: TestState, name: string): Pr showOpenDialog: async (...args) => ({ canceled: false, filePaths: state.dialogPaths.splice(0) }), showSaveDialog: async () => ({ canceled: true, filePath: undefined }) }; - export const shell = { openPath: async () => '' }; + export const shell = { openPath: async () => '', openExternal: async () => {} }; + export const powerSaveBlocker = { + start: () => { state.powerBlockers = (state.powerBlockers ?? 0) + 1; return state.powerBlockers; }, + stop: () => true, + isStarted: () => (state.powerBlockers ?? 0) > 0 + }; export class BrowserWindow {} export class WebContentsView {} ` diff --git a/scripts/rea/probe-todo10-general-reset-top.json b/scripts/rea/probe-todo10-general-reset-top.json new file mode 100644 index 0000000..3d1a026 --- /dev/null +++ b/scripts/rea/probe-todo10-general-reset-top.json @@ -0,0 +1,11 @@ +{ + "include_fonts": false, + "include_message_content": false, + "track_selectors": [ + ".settings-modal__panel", + ".general-settings-panel" + ], + "actions": [ + { "kind": "scroll", "step_id": "return-general-top", "selector": ".settings-modal__panel", "delta_y": -1000, "frames": 1 } + ] +} diff --git a/scripts/rea/probe-todo10-general-scroll.json b/scripts/rea/probe-todo10-general-scroll.json new file mode 100644 index 0000000..a60316c --- /dev/null +++ b/scripts/rea/probe-todo10-general-scroll.json @@ -0,0 +1,17 @@ +{ + "include_fonts": false, + "include_message_content": false, + "track_selectors": [ + ".settings-modal__panel", + ".general-settings-panel", + ".general-settings-panel__group", + ".general-settings-panel__group-card", + ".general-settings-panel__section" + ], + "actions": [ + { "kind": "wait", "step_id": "general-top", "duration_ms": 250, "frames": 1 }, + { "kind": "scroll", "step_id": "general-scroll-1", "selector": ".settings-modal__panel", "delta_y": 400, "frames": 1 }, + { "kind": "scroll", "step_id": "general-scroll-2-to-bottom", "selector": ".settings-modal__panel", "delta_y": 400, "frames": 1 }, + { "kind": "scroll", "step_id": "return-general-top", "selector": ".settings-modal__panel", "delta_y": -1000, "frames": 1 } + ] +} diff --git a/scripts/rea/probe-todo10-open-general.json b/scripts/rea/probe-todo10-open-general.json new file mode 100644 index 0000000..641040c --- /dev/null +++ b/scripts/rea/probe-todo10-open-general.json @@ -0,0 +1,14 @@ +{ + "include_fonts": false, + "include_message_content": false, + "track_selectors": [ + ".settings-modal__panel", + ".general-settings-panel", + ".general-settings-panel__group", + ".general-settings-panel__section" + ], + "actions": [ + { "kind": "click", "step_id": "select-general-tab", "selector": ".settings-navigation__item", "index": 0, "frames": 0 }, + { "kind": "wait", "step_id": "general-ready", "duration_ms": 500, "frames": 1 } + ] +} diff --git a/scripts/rea/probe-todo10-open-settings.json b/scripts/rea/probe-todo10-open-settings.json new file mode 100644 index 0000000..3441be5 --- /dev/null +++ b/scripts/rea/probe-todo10-open-settings.json @@ -0,0 +1,16 @@ +{ + "include_fonts": false, + "include_message_content": false, + "track_selectors": [ + ".user-menu-popover", + ".user-menu-items", + ".settings-modal__panel", + ".general-settings-panel" + ], + "actions": [ + { "kind": "click", "step_id": "open-account-menu", "selector": ".user-menu-trigger--workbuddy", "index": 0, "frames": 0 }, + { "kind": "wait", "step_id": "account-menu-ready", "duration_ms": 500, "frames": 0 }, + { "kind": "click", "step_id": "open-settings", "selector": ".user-menu-items .user-menu-item", "index": 3, "frames": 0 }, + { "kind": "wait", "step_id": "settings-ready", "duration_ms": 800, "frames": 1 } + ] +} diff --git a/scripts/rea/recover-general-settings.mjs b/scripts/rea/recover-general-settings.mjs new file mode 100644 index 0000000..14a16e8 --- /dev/null +++ b/scripts/rea/recover-general-settings.mjs @@ -0,0 +1,111 @@ +// Recover only already-retained REA CSS; never access the installed application here. +// +// Source: REA evidence ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94 +// (WorkBuddy 5.8.x settings window), projected through the retained contexts stylesheet +// `.tmp/rea-css-recovered/contexts-BkjwYSZT.css`. The general settings panel DOM classes were +// observed in REA evidence ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e +// (settings window layout: `general-settings-panel`, `font-size-slider`, `wb-storage`, +// `power-save-blocker-settings`, `proxy-section`). +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 sourceEvidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94' +const layoutEvidenceId = 'ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e' +const contexts = await fs.readFile('.tmp/rea-css-recovered/contexts-BkjwYSZT.css', 'utf8') +const contextsSha = sha(contexts) +if (contextsSha !== '50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380') throw new Error('Contexts CSS changed') +const tokens = await fs.readFile('.tmp/rea-css-recovered/tokens-Df-i-zsO.css', 'utf8') +const tokensSha = sha(tokens) +if (tokensSha !== '13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696') throw new Error('Tokens CSS changed') +// UI kit:设置窗口实际加载的控件样式表(按钮 / 输入框 / 开关 / 图标)。 +const kit = await fs.readFile('.tmp/rea-css-recovered/src-Bh3YZ35H.css', 'utf8') +const kitSha = sha(kit) +if (kitSha !== '88dbdc1cd3f59d0e8b75824512a465a2be714bc7af481d790fe3a9484f85e01a') throw new Error('UI kit CSS changed') +// 选择器白名单:通用设置面板本体、字号滑块、存储卡片、电源与代理相关规则。 +const panelled = /\.(general-settings-panel|font-size-slider|wb-storage|power-save-blocker-settings|proxy-section)(?![a-zA-Z0-9])/ +function project(node) { + if (node.type === 'rule') { + const selectors = node.selectors.filter(selector => panelled.test(selector)) + return selectors.length ? node.clone({ selector: selectors.join(',') }) : null + } + if (node.type === 'atrule' && node.nodes) { + const nodes = node.nodes.map(project).filter(Boolean) + return nodes.length ? node.clone({ nodes }) : null + } + return null +} +const parsed = postcss.parse(contexts) +const root = postcss.root({ nodes: parsed.nodes.map(project).filter(Boolean) }) +// 原版的 .wb-button / .wb-input / .wb-switch / .wb-icon 来自全局 UI kit;YCode 里同样收敛到面板作用域。 +// 用 :where() 前缀保持原版层叠:UI kit 规则与面板规则同权重,面板规则在后(原版面板 chunk 后加载), +// 例如 .wb-storage__open-folder{height:auto} 必须仍能覆盖 .wb-button--medium{height:32px}。 +const kitScoped = /^\.wb-(button|input|switch|icon)(?![a-zA-Z])/ +function projectKit(node) { + if (node.type === 'rule') { + const selectors = node.selectors.filter(selector => kitScoped.test(selector.trim())) + return selectors.length ? node.clone({ selector: selectors.map(selector => `:where(.general-settings-panel) ${selector}`).join(',') }) : null + } + if (node.type === 'atrule' && node.nodes) { + const nodes = node.nodes.map(projectKit).filter(Boolean) + return nodes.length ? node.clone({ nodes }) : null + } + return null +} +const kitRoot = postcss.root({ nodes: postcss.parse(kit).nodes.map(projectKit).filter(Boolean) }) +// 原版把 `--wb-*` 设计变量放在全局 :root(tokens 样式表),YCode 的既有做法是按组件作用域 +// 复制一份,避免全局污染。这里同样把 tokens 的 :root / 深色作用域合并成通用面板作用域。 +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) + } +} +// UI kit 的控件 token 定义在它自己的样式表里。 +for (const rule of postcss.parse(kit).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) + } +} +if (lightTokens.size < 200 || darkTokens.size < 200) throw new Error('Tokens CSS lacks root scopes') +// 只输出面板规则真正引用到的变量(含 var() 传递闭包),避免把整套全局 token 复制进面板作用域。 +const referenced = new Set() +const collect = (text) => { for (const match of text.matchAll(/var\(\s*(--[\w-]+)/g)) referenced.add(match[1]) } +collect(root.toString()) +collect(kitRoot.toString()) +for (let depth = 0; depth < 6; depth++) { + const before = referenced.size + for (const name of [...referenced]) { + const value = lightTokens.get(name) ?? darkTokens.get(name) + if (typeof value === 'string') collect(value) + } + if (referenced.size === before) break +} +const missing = [...referenced].filter(name => !lightTokens.has(name)) +if (missing.length) console.log(`Tokens missing from light root scope: ${missing.join(', ')}`) +const pick = (map) => new Map([...referenced].filter(name => map.has(name)).map(name => [name, map.get(name)])) +const scope = (name, map) => `.${name}{${[...map].map(([property, value]) => `${property}:${value}`).join(';')}}` +const scopedTokens = `${scope('general-settings-panel', pick(lightTokens))}\n:root[data-theme=dark] ${scope('general-settings-panel', pick(darkTokens))}\n` +const css = `/* REA ${sourceEvidenceId} via contexts sha256=${contextsSha}; tokens sha256=${tokensSha}; ui-kit sha256=${kitSha}; general settings panel classes from REA ${layoutEvidenceId}. */\n${kitRoot.toString()}\n${root.toString()}\n${scopedTokens}` +await fs.writeFile('src/renderer/src/components/source-general-settings-recovered.css', css) +const rules = [] +root.walkRules(rule => rules.push(rule.selector)) +console.log(`Recovered ${rules.length} general-settings rules (${css.length} bytes)`) +console.log(rules.slice(0, 12).join('\n')) +if (rules.length < 60) throw new Error('Unexpectedly few general settings rules') +console.log(`Scoped ${pick(lightTokens).size} light / ${pick(darkTokens).size} dark referenced design tokens into .general-settings-panel`) diff --git a/scripts/verify-general-settings.cjs b/scripts/verify-general-settings.cjs new file mode 100644 index 0000000..983811a --- /dev/null +++ b/scripts/verify-general-settings.cjs @@ -0,0 +1,312 @@ +// TODO 10 验收:设置 → 通用面板的原版几何/排版 + YCode 真实能力接线 + 未接入项不伪装。 +// 真实隔离 YCode(真实 preload/IPC/渲染进程/文件系统)+ 原生设置窗口,视口 900×680(与原版一致)。 +// +// 原版依据(REA): +// - ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e(面板 DOM/几何/computed style/svg) +// - ev_a2dbeb23e30b9770743e857279a2e43da38fcc48d10612f75ace2938c5b29114(5.8.3 面板滚动采样 + 文案) +// 原版实测:面板 [232,40,638,1415];组 常规 218 / 权限 514 / 存储 265 / 通知 234 / 隐私 120; +// 行高 [44,68,52 | 56,68,68,76,68,68,56 | 56,68,56 | 66];卡片 bg #fff / radius 12 / padding 12px 16px; +// 标题 14px/500/22px;描述 14px/400/22px rgba(0,0,0,.5);开关 34×20;下拉 120×32;滑块 224×40。 +const fs = require('node:fs/promises') +const path = require('node:path') +const assert = require('node:assert/strict') + +const EXPECTED_TITLES = ['语言', '链接打开方式', '字体大小', '允许锁屏运行', '运行时防止系统休眠', '开机自动启动', + '网络代理', '自动安装可信技能', '技能自动更新', '套件自动更新', '桌面通知', '通知提示音', '定时任务产物通知', '体验优化计划'] +const EXPECTED_ROW_HEIGHTS = [44, 68, 52, 56, 68, 68, 76, 68, 68, 56, 56, 68, 56, 66] +const EXPECTED_GROUPS = ['常规', '权限', '通知', '隐私'] +const EXPECTED_PLACEHOLDERS = 11 + +if (!process.versions.electron) { + ;(async () => { + const directory = await fs.mkdtemp(path.resolve('.tmp/general-settings-')) + const data = path.join(directory, 'data') + const home = path.join(directory, 'home') + const nextWorkspace = path.join(directory, 'workspace-root') + await fs.mkdir(data, { recursive: true }) + await fs.mkdir(home, { recursive: true }) + await fs.mkdir(nextWorkspace, { 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('GENERAL_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 + const dataDir = process.env.YCODE_USER_DATA_DIR + const nextWorkspace = path.join(directory, 'workspace-root') + 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 }) + } + function configFile() { + try { return JSON.parse(require('node:fs').readFileSync(path.join(dataDir, 'config.json'), 'utf8')) } catch { return {} } + } + 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') + settings.webContents.on('console-message', (_event, level, message) => { if (level >= 2) rendererErrors.push(message) }) + await wait('!!document.querySelector(".settings-window-root")', 'settings root', settings) + await pause(400) + return settings + } + const panelSource = `(()=>{ + const rect = node => { const r = node.getBoundingClientRect(); return [Math.round(r.x), Math.round(r.y), Math.round(r.width), Math.round(r.height)] } + const panel = document.querySelector('.general-settings-panel') + const groups = [...document.querySelectorAll('.general-settings-panel__group')] + const sections = [...document.querySelectorAll('.general-settings-panel__group-card > .general-settings-panel__section')] + const storage = document.querySelector('.wb-storage') + const styles = node => { const s = getComputedStyle(node); return { fontSize: s.fontSize, fontWeight: s.fontWeight, lineHeight: s.lineHeight, + color: s.color, background: s.backgroundColor, borderRadius: s.borderRadius, padding: s.padding, gap: s.gap, paddingLeft: s.paddingLeft } } + const placeholderNodes = [...document.querySelectorAll('.general-settings-panel [data-settings-unavailable=true]')] + return { + panel: panel ? rect(panel) : null, + spacing3: panel ? getComputedStyle(panel).getPropertyValue('--wb-spacing-3').trim() : null, + labelPaddingLeft: panel ? getComputedStyle(panel.querySelector('.general-settings-panel__group-label')).paddingLeft : null, + order: panel ? [...panel.children].map(node => node.className.split(' ')[0]) : [], + groups: groups.map(node => ({ label: node.querySelector('.general-settings-panel__group-label')?.textContent?.trim(), + bounds: rect(node), labelBounds: rect(node.querySelector('.general-settings-panel__group-label')), + card: styles(node.querySelector('.general-settings-panel__group-card')) })), + groupCount: groups.length, + titles: sections.map(node => node.querySelector('.general-settings-panel__section-title')?.textContent?.trim()), + rowBounds: sections.map(rect), + rowHeight: sections.map(node => Math.round(node.getBoundingClientRect().height)), + titleStyle: styles(sections[0].querySelector('.general-settings-panel__section-title')), + descStyle: styles(sections[1].querySelector('.general-settings-panel__section-desc')), + switchWrap: rect(document.querySelector('.general-settings-panel__switch-wrap')), + switchTrack: rect(document.querySelector('.wb-switch__track')), + selects: [...document.querySelectorAll('.general-settings-panel .wb-select')].map(rect), + slider: rect(document.querySelector('.font-size-slider')), + sliderLabels: [...document.querySelectorAll('.font-size-slider__label')].map(node => node.textContent.trim()), + notificationButton: (() => { const button = [...document.querySelectorAll('.general-settings-panel .wb-button')].find(node => node.textContent.includes('前往系统设置')) + return button ? { bounds: rect(button), text: button.textContent.trim(), icons: button.querySelectorAll('svg').length } : null })(), + storage: storage ? { bounds: rect(storage), label: storage.querySelector('.wb-storage__group-label')?.textContent?.trim(), + card: rect(storage.querySelector('.wb-storage__card')), cardTitle: storage.querySelector('.wb-storage__card-title')?.textContent?.trim(), + cache: (() => { const node = storage.querySelector('.wb-storage__cache'); return node ? rect(node) : null })(), + titleRow: (() => { const node = storage.querySelector('.wb-storage__card-title-row'); return node ? rect(node) : null })(), + cachePathRow: (() => { const node = storage.querySelector('.wb-storage__path-row'); return node ? rect(node) : null })(), + overview: (() => { const node = storage.querySelector('.general-settings-panel__storage-overview'); return node ? rect(node) : null })(), + legend: (() => { const node = storage.querySelector('.general-settings-panel__storage-legend'); return node ? rect(node) : null })(), + workspace: (() => { const node = storage.querySelector('.general-settings-panel__workspace'); return node ? rect(node) : null })(), + workspaceHeader: (() => { const node = storage.querySelector('.general-settings-panel__workspace-header'); return node ? rect(node) : null })(), + workspaceDesc: (() => { const node = storage.querySelector('.general-settings-panel__workspace-path-desc'); return node ? rect(node) : null })(), + openFolder: rect(storage.querySelector('.wb-storage__open-folder')), pathText: storage.querySelector('.wb-storage__path-text')?.textContent?.trim(), + usageText: storage.querySelector('.general-settings-panel__storage-usage-text')?.textContent?.replace(/\\s+/g, ' ').trim(), + legend: [...storage.querySelectorAll('.general-settings-panel__storage-legend-label')].map(node => node.textContent.trim()), + segments: [...storage.querySelectorAll('.general-settings-panel__storage-progress-seg')].map(node => Math.round(node.getBoundingClientRect().width * 100) / 100), + divider: rect(storage.querySelector('.wb-storage__divider')), + workspaceTitle: storage.querySelector('.general-settings-panel__workspace-path-title')?.textContent?.trim(), + workspaceTitleStyle: styles(storage.querySelector('.general-settings-panel__workspace-path-title')), + workspaceDesc: storage.querySelector('.general-settings-panel__workspace-path-desc')?.textContent?.trim(), + input: (() => { const node = storage.querySelector('.general-settings-panel__path-input') + return node ? { bounds: rect(node), value: node.value, readOnly: node.readOnly, style: styles(node) } : null })(), + change: (() => { const button = [...storage.querySelectorAll('.wb-button')].find(node => node.textContent.trim() === '更改') + return button ? { bounds: rect(button), text: button.textContent.trim() } : null })() } : null, + placeholders: placeholderNodes.length, + placeholderHints: placeholderNodes.map(node => [node.getAttribute('aria-disabled'), node.getAttribute('title')]), + placeholderControlsDisabled: placeholderNodes.every(node => { const input = node.querySelector('input, button'); return !input || input.tabIndex === -1 }), + inlineLink: (() => { const link = document.querySelector('.general-settings-panel__inline-link') + return link ? { text: link.textContent.trim(), disabled: link.getAttribute('aria-disabled'), title: link.getAttribute('title'), bounds: rect(link) } : null })(), + switchStates: [...document.querySelectorAll('.general-settings-panel__switch-wrap')].map(node => node.querySelector('input')?.checked), + unavailableSwitches: [...document.querySelectorAll('.general-settings-panel__switch-wrap[data-settings-unavailable=true] input')].map(node => node.hasAttribute('readonly')) + }})()` + + ;(async () => { + const sync = require('node:fs') + sync.mkdirSync(dataDir, { recursive: true }) + app.setPath('userData', dataDir) + app.setPath('sessionData', dataDir) + 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(40) + } + assert.ok(main, 'main window') + main.webContents.on('console-message', (_event, level, message) => { if (level >= 2) rendererErrors.push(message) }) + await wait('!!window.ycode', 'main renderer') + await pause(1200) + + await step('general-panel-structure') + await openSettings('settings') + const nav = await read(`(()=>{const item=[...document.querySelectorAll('.settings-navigation__item')].find(node=>node.dataset.settingsTab==='settings') + return { enabled: item?.getAttribute('aria-disabled') !== 'true', active: !!document.querySelector('.settings-navigation__item--active') && + document.querySelector('.settings-navigation__item--active').dataset.settingsTab === 'settings' }})()`, settings) + check('nav 通用 entry is enabled and active', [nav.enabled, nav.active], [true, true]) + const panel = await read(panelSource, settings) + snapshots.panel = panel + check('panel keeps the recovered 232,40 638-wide column', [panel.panel[0], panel.panel[2]], [232, 638]) + // 原版 1415 高;数据目录路径比原版长时该行会像原版一样换行,因此按实际行高换算期望值。 + const pathRowOverflow = panel.storage.cachePathRow[3] - 22 + check('panel keeps the recovered content height (path row wrapping accounted)', + [panel.panel[3], pathRowOverflow], [1415 + pathRowOverflow, pathRowOverflow >= 0 ? pathRowOverflow : -1]) + check('panel top-level order stays group/group/storage/group/group', + panel.order.map(name => name.startsWith('general-settings-panel__group') ? 'group' : name), ['group', 'group', 'wb-storage', 'group', 'group']) + check('panel keeps the four recovered group labels', panel.groups.map(group => group.label), EXPECTED_GROUPS) + check('panel keeps the 14 recovered row titles in order', panel.titles, EXPECTED_TITLES) + check('panel keeps the recovered row heights', panel.rowHeight, EXPECTED_ROW_HEIGHTS) + check('group label keeps the recovered 638x22 with 8px left padding', + [panel.groups[0].labelBounds[2], panel.groups[0].labelBounds[3], panel.labelPaddingLeft, panel.spacing3], [638, 22, '8px', '8px']) + check('group card keeps the recovered padding and radius', + [panel.groups[1].card.padding, panel.groups[1].card.borderRadius, panel.groups[1].card.background], ['12px 16px', '12px', 'rgb(255, 255, 255)']) + check('row title keeps 14px/500/22px', [panel.titleStyle.fontSize, panel.titleStyle.fontWeight, panel.titleStyle.lineHeight], ['14px', '500', '22px']) + check('row description keeps 14px/400/22px tertiary', + [panel.descStyle.fontSize, panel.descStyle.fontWeight, panel.descStyle.lineHeight, panel.descStyle.color], ['14px', '400', '22px', 'rgba(0, 0, 0, 0.5)']) + check('switch keeps the recovered 34x20 control', [panel.switchTrack[2], panel.switchTrack[3]], [34, 20]) + check('panel selects keep the recovered 120x32 geometry', panel.selects.map(size => [size[2], size[3]]), + panel.selects.map(() => [120, 32])) + check('font size slider keeps the recovered 224x40 with 小/默认/大 labels', + [panel.slider[2], panel.slider[3], panel.sliderLabels], [224, 40, ['小', '默认', '大']]) + check('桌面通知 keeps the recovered 130x32 button with a trailing icon', + [panel.notificationButton.bounds[2], panel.notificationButton.bounds[3], panel.notificationButton.text, panel.notificationButton.icons], + [130, 32, '前往系统设置', 1]) + + await step('general-panel-storage') + const storage = panel.storage + check('storage keeps the recovered 638-wide card and legend wording', + [storage.bounds[2], storage.bounds[3], storage.label, storage.legend], [638, 265 + pathRowOverflow, '存储', ['系统缓存', '磁盘已用', '磁盘可用']]) + check('storage keeps the recovered titles and helper texts', + [storage.cardTitle, storage.workspaceTitle, storage.workspaceDesc, storage.change.text], + ['系统缓存目录', 'YCode 默认工作空间存储路径', '新建任务、工作空间时将自动存放在该路径下;修改后不影响已有数据。', '更改']) + check('storage keeps the recovered 76x22 打开目录 button', [storage.openFolder[2], storage.openFolder[3]], [76, 22]) + check('storage keeps the recovered input and 更改 button geometry', + [[storage.input.bounds[2], storage.input.bounds[3]], [storage.change.bounds[2], storage.change.bounds[3]]], [[530, 32], [68, 32]]) + check('storage input is read-only and filled', [storage.input.readOnly, storage.input.style.background, storage.input.style.borderRadius], + [true, 'rgb(242, 242, 242)', '8px']) + check('storage shows the real isolated data directory and disk usage', + [storage.pathText.startsWith(dataDir), /^已用 \d+(\.\d+)? [KMGT]?B \/ 共 \d+(\.\d+)? [KMGT]B$/.test(storage.usageText)], [true, true]) + // 606 宽的进度条内部有 2 个 1px 间隙(原版 gap:1px),三段合计 604。 + check('storage progress keeps the recovered 606x4 bar with 3 segments filling 604px', + [Math.round(storage.segments.reduce((sum, value) => sum + value, 0)), storage.segments.length], [604, 3]) + check('storage divider stays 606x1', [storage.divider[2], storage.divider[3]], [606, 1]) + check('workspace path title keeps 14px/500/22px', + [storage.workspaceTitleStyle.fontSize, storage.workspaceTitleStyle.fontWeight, storage.workspaceTitleStyle.lineHeight], ['14px', '500', '22px']) + + await step('general-panel-placeholders') + check('unwired rows stay explicit placeholders with 暂未接入 hints', + [panel.placeholders, panel.placeholderHints.every(([disabled, title]) => disabled === 'true' && title === '暂未接入'), panel.placeholderControlsDisabled], + [EXPECTED_PLACEHOLDERS, true, true]) + check('unwired switches stay read-only', panel.unavailableSwitches.every(Boolean), true) + check('了解更多 stays a disabled inline link', [panel.inlineLink.text, panel.inlineLink.disabled, panel.inlineLink.title], ['了解更多', 'true', '暂未接入']) + const placeholderClick = await read(`(()=>{const select=document.querySelector('.general-settings-panel .wb-select[aria-disabled=true]') + select.click() + const switchInput=document.querySelector('.general-settings-panel__switch-wrap[data-settings-unavailable=true] input') + const before=switchInput.checked; switchInput.click() + return { popover: !!document.querySelector('.wb-select-popover'), switchBefore: before, switchAfter: switchInput.checked }})()`, settings) + check('placeholder select does not open a popover and placeholder switch does not toggle', + [placeholderClick.popover, placeholderClick.switchBefore === placeholderClick.switchAfter], [false, true]) + + await step('general-settings-wired-behaviour') + const initial = await read('window.ycode.general.get()', settings) + snapshots.initial = { ...initial, dataDir: initial.dataDir } + check('general view reports the isolated data directory and default preferences', + [initial.dataDir === dataDir, initial.preventSleep, initial.autoLaunch, initial.preventSleepActive], [true, true, false, false]) + await read(`(()=>{const input=${'document.querySelectorAll(".general-settings-panel__switch-wrap:not([data-settings-unavailable]) input")[0]'} + input.click(); return true})()`, settings) + await pause(600) + const afterSleep = await read('window.ycode.general.get()', settings) + snapshots.afterSleep = afterSleep + check('toggling 运行时防止系统休眠 persists the preference', + [afterSleep.preventSleep, configFile().preventSleep], [false, false]) + await read(`(()=>{${'document.querySelectorAll(".general-settings-panel__switch-wrap:not([data-settings-unavailable]) input")[0]'}.click(); return true})()`, settings) + await pause(600) + const restoredSleep = await read('window.ycode.general.get()', settings) + check('prevent-sleep preference can be restored to on', [restoredSleep.preventSleep, configFile().preventSleep], [true, true]) + await read(`(()=>{${'document.querySelectorAll(".general-settings-panel__switch-wrap:not([data-settings-unavailable]) input")[1]'}.click(); return true})()`, settings) + await pause(600) + const afterLaunch = await read('window.ycode.general.get()', settings) + snapshots.afterLaunch = afterLaunch + check('toggling 开机自动启动 persists the preference (system integration isolated)', + [afterLaunch.autoLaunch, configFile().autoLaunch], [true, true]) + await read(`(()=>{${'document.querySelectorAll(".general-settings-panel__switch-wrap:not([data-settings-unavailable]) input")[1]'}.click(); return true})()`, settings) + await pause(600) + const restoredLaunch = await read('window.ycode.general.get()', settings) + check('auto-launch preference can be restored to off', [restoredLaunch.autoLaunch, configFile().autoLaunch], [false, false]) + const changed = await read(`window.ycode.general.changeWorkspaceRoot(${JSON.stringify(nextWorkspace)})`, settings) + await pause(400) + const rootListed = await read(`(async()=>(await window.ycode.workspace.get()).root)()`, settings) + check('更改工作空间路径 persists the real workspace root', + [changed.workspaceRoot === nextWorkspace, rootListed === nextWorkspace], [true, true]) + // 说明:「更改」按钮会打开系统目录选择器,无法自动化点击;这里断言按钮可用, + // 并在下面的重开窗口步骤里断言面板确实渲染持久化后的路径。 + const changeButton = await read(`(()=>{const button=[...document.querySelectorAll('.wb-storage .wb-button')].find(node => node.textContent.trim() === '更改') + return { disabled: button.disabled, label: button.textContent.trim() }})()`, settings) + check('更改 button stays enabled and labelled', changeButton, { disabled: false, label: '更改' }) + check('workspace root persists to config.json', configFile().workspaceRoot, nextWorkspace) + check('general API exposes the wired actions', + await read(`(()=>{const api=window.ycode.general; return [typeof api.openFolder, typeof api.changeWorkspaceRoot, typeof api.openSystemNotificationSettings]})()`, settings), + ['function', 'function', 'function']) + await read('window.ycode.general.openSystemNotificationSettings()', settings) + check('notification settings call resolves while system integration is isolated', true, true) + + await step('general-panel-persistence') + settings.close() + await pause(600) + await openSettings('settings') + const reopened = await read(panelSource, settings) + const reopenedView = await read('window.ycode.general.get()', settings) + snapshots.reopened = reopenedView + check('reopening the settings window keeps the persisted preferences', + [reopenedView.preventSleep, reopenedView.autoLaunch, reopenedView.workspaceRoot === nextWorkspace], [true, false, true]) + check('reopened panel renders the persisted workspace path', reopened.storage.input.value === nextWorkspace, true) + check('reopened panel still renders the recovered rows', reopened.rowHeight, EXPECTED_ROW_HEIGHTS) + + await step('general-panel-dark-theme') + const dark = await read(`(()=>{document.documentElement.dataset.theme='dark' + const card=document.querySelector('.general-settings-panel__group-card') + const title=document.querySelector('.general-settings-panel__section-title') + return { card: getComputedStyle(card).backgroundColor, title: getComputedStyle(title).color }})()`, settings) + check('dark theme repaints the panel card and titles', [dark.card !== 'rgb(255, 255, 255)', dark.title !== 'rgb(0, 0, 0)'], [true, true]) + await read(`(()=>{document.documentElement.dataset.theme='light'; return true})()`, settings) + + await step('general-panel-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('GENERAL_SETTINGS_' + result + ' ' + directory) + for (const failure of checks.filter(item => !item.pass)) console.log('GENERAL_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) + }) +} diff --git a/scripts/verify-settings-nav.cjs b/scripts/verify-settings-nav.cjs index 2c2cc3e..14a44e2 100644 --- a/scripts/verify-settings-nav.cjs +++ b/scripts/verify-settings-nav.cjs @@ -124,10 +124,11 @@ if (!process.versions.electron) { [nav.nav[0] === 200 || nav.nav[0] === 190, nav.nav[2] === 0 ? navContentWidth === 200 : navContentWidth === 190], [true, true]) // 原版 4 组;about 组按组件约定渲染为分隔线而不是标题。 check('nav keeps the four recovered groups', [nav.groups.length, nav.dividers], [3, 1]) - const expectedDisabled = EXPECTED_TABS.filter(tab => tab !== 'models' && tab !== 'appearance') + // 通用(settings) 在 TODO 10 接入后同样可用。 + const expectedDisabled = EXPECTED_TABS.filter(tab => !['settings', 'models', 'appearance'].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('models and appearance stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['appearance', 'models']) + check('settings, models and appearance stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['settings', 'appearance', 'models']) await step('settings-search-excludes-removed') await read(`(()=>{const input=document.querySelector('.settings-navigation input[type="search"]'); diff --git a/src/main/config.ts b/src/main/config.ts index c82eab5..34b5dae 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -22,6 +22,10 @@ interface StoredConfig { apiKeyEnc?: string apiKeyPlain?: string workspaceRoot?: string + /** 运行任务时阻止系统休眠(通用设置;默认开启,与原版初始态一致)。 */ + preventSleep?: boolean + /** 开机自动启动(通用设置;默认关闭)。 */ + autoLaunch?: boolean } const DEFAULTS: StoredConfig = { @@ -92,6 +96,28 @@ export async function setWorkspaceRoot(root: string): Promise { await persist({ ...stored, workspaceRoot: root }) } +/** 通用设置:运行任务时阻止系统休眠,默认开启。 */ +export async function getPreventSleep(): Promise { + const stored = await load() + return stored.preventSleep !== false +} + +export async function setPreventSleep(enabled: boolean): Promise { + const stored = await load() + await persist({ ...stored, preventSleep: enabled }) +} + +/** 通用设置:开机自动启动,默认关闭(由系统登录项反映实际状态)。 */ +export async function getAutoLaunch(): Promise { + const stored = await load() + return stored.autoLaunch === true +} + +export async function setAutoLaunch(enabled: boolean): Promise { + const stored = await load() + await persist({ ...stored, autoLaunch: enabled }) +} + /** 供渲染进程使用:脱敏视图 */ export async function getConfigView(): Promise { const stored = await load() diff --git a/src/main/general-settings.ts b/src/main/general-settings.ts new file mode 100644 index 0000000..641b9c7 --- /dev/null +++ b/src/main/general-settings.ts @@ -0,0 +1,118 @@ +/** + * 通用设置的真实运行链路(REA 对照原版「通用」页面): + * + * - 数据目录用量与所在磁盘容量:真实扫描 userData 目录并读取磁盘统计。 + * - 开机自动启动:写入/读取系统登录项(可用 YCODE_DISABLE_SYSTEM_INTEGRATION=1 隔离验收)。 + * - 运行时防止系统休眠:设置项开启且存在运行中的任务时,持有 powerSaveBlocker。 + * + * 未接入的原版设置项(语言、链接打开方式、字体大小、锁屏运行、网络代理、 + * 技能/套件自动更新、通知提示音、定时任务产物通知、体验优化计划)只做视觉占位, + * 不在此模块伪造行为。 + */ + +import { app, powerSaveBlocker } from 'electron' +import { promises as fs } from 'node:fs' +import { join } from 'node:path' + +let preventSleepEnabled = true +let activeRuns = 0 +let blockerId: number | null = null + +function systemIntegrationDisabled(): boolean { + return process.env.YCODE_DISABLE_SYSTEM_INTEGRATION === '1' || !app.isReady() +} + +/** 隔离验收环境:不触碰真实系统设置(登录项 / 系统设置页)。 */ +export function systemIntegrationSuppressed(): boolean { + return process.env.YCODE_DISABLE_SYSTEM_INTEGRATION === '1' +} + +function syncBlocker(): void { + const shouldBlock = preventSleepEnabled && activeRuns > 0 + if (shouldBlock && blockerId === null) blockerId = powerSaveBlocker.start('prevent-display-sleep') + if (!shouldBlock && blockerId !== null) { powerSaveBlocker.stop(blockerId); blockerId = null } +} + +/** 主进程启动时同步一次持久化的设置。 */ +export function configurePreventSleep(enabled: boolean): void { + preventSleepEnabled = enabled + syncBlocker() +} + +/** 任务开始/结束时更新运行计数;计数>0 且设置开启时阻止系统休眠。 */ +export function updateActiveRuns(delta: number): void { + activeRuns = Math.max(0, activeRuns + delta) + syncBlocker() +} + +export function isPreventSleepActive(): boolean { + return blockerId !== null +} + +/** + * 开机自启动的实际生效状态。 + * - 正常环境读系统登录项(外部删除登录项时如实反映为 false)。 + * - 隔离验收环境(YCODE_DISABLE_SYSTEM_INTEGRATION=1)不触碰系统,直接回显持久化偏好。 + */ +export function resolveAutoLaunch(stored: boolean): boolean { + if (systemIntegrationDisabled()) return stored + try { return app.getLoginItemSettings().openAtLogin } catch { return stored } +} + +/** 把开机自启动写入系统登录项,返回实际生效状态。 */ +export function applyAutoLaunch(enabled: boolean): boolean { + if (systemIntegrationDisabled()) return enabled + try { + app.setLoginItemSettings({ + openAtLogin: enabled, + path: process.execPath, + args: app.isPackaged ? [] : [app.getAppPath()] + }) + return app.getLoginItemSettings().openAtLogin + } catch { + return enabled + } +} + +let usageCache: { at: number; bytes: number; partial: boolean } | null = null + +/** 递归统计目录占用;限制条数与耗时,避免设置页卡顿。 */ +export async function directoryBytes(root: string, maxEntries = 200_000, budgetMs = 1500): Promise<{ bytes: number; partial: boolean }> { + const started = Date.now() + let bytes = 0 + let entries = 0 + let partial = false + const queue: string[] = [root] + while (queue.length) { + if (entries >= maxEntries || Date.now() - started > budgetMs) { partial = true; break } + const current = queue.pop() as string + let dirents + try { dirents = await fs.readdir(current, { withFileTypes: true }) } + catch { continue } + for (const dirent of dirents) { + entries++ + const target = join(current, dirent.name) + if (dirent.isDirectory()) { queue.push(target); continue } + if (!dirent.isFile()) continue + try { bytes += (await fs.stat(target)).size } catch { /* 忽略被占用的文件 */ } + if (entries >= maxEntries || Date.now() - started > budgetMs) { partial = true; break } + } + } + return { bytes, partial } +} + +export async function dataUsage(root: string, refresh = false): Promise<{ bytes: number; partial: boolean }> { + if (!refresh && usageCache && Date.now() - usageCache.at < 30_000) return usageCache + const result = await directoryBytes(root) + usageCache = { at: Date.now(), ...result } + return result +} + +export async function diskUsage(path: string): Promise<{ totalBytes: number; freeBytes: number }> { + try { + const stats = await fs.statfs(path) + return { totalBytes: stats.blocks * stats.bsize, freeBytes: stats.bavail * stats.bsize } + } catch { + return { totalBytes: 0, freeBytes: 0 } + } +} diff --git a/src/main/index.ts b/src/main/index.ts index b398af0..fd2c777 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -9,6 +9,8 @@ import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { disposeOmpKernels, registerIpc, setTaskWindowFactory } from './ipc' import { disposeMcpServers } from './capabilities/mcp' +import { getPreventSleep } from './config' +import { configurePreventSleep } from './general-settings' let mainWindow: BrowserWindow | null = null let settingsWindow: BrowserWindow | null = null @@ -161,6 +163,8 @@ if (process.env.YCODE_DISABLE_GPU === '1') { } app.whenReady().then(() => { + // 通用设置:启动时套用持久化偏好(运行任务时阻止系统休眠)。 + void getPreventSleep().then((enabled) => configurePreventSleep(enabled)).catch(() => undefined) registerIpc(() => mainWindow) setTaskWindowFactory((sessionId) => { openTaskWindow(sessionId) }) ipcMain.handle('app:openSettings', async (_event, section = 'model', action?: 'new') => { diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 9dc81e2..fbc5d1d 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -33,6 +33,7 @@ import { restoreWorkspaceFiles, rollbackEditedArtifacts } from './workspace-reve import { assistantWireHistory } from './agent/history' import * as models from './models' import * as workspaces from './workspaces' +import * as generalSettings from './general-settings' import { prepareOmpModel } from './omp-model' import type { CustomModelInput, ResolvedModel } from '@shared/models' import type { SessionContext } from '@shared/workspaces' @@ -384,6 +385,62 @@ export function registerIpc(getWindow: () => BrowserWindow | null): void { // ---------- 配置 ---------- ipcMain.handle('config:get', () => config.getConfigView()) ipcMain.handle('config:update', (_event, patch: ConfigPatchLike) => config.updateConfig(patch)) + + // ---------- 通用设置(设置 → 通用) ---------- + const generalView = async (refreshUsage = false) => { + const dataDir = app.getPath('userData') + const [usage, disk, workspaceRoot, autoLaunch, preventSleep] = await Promise.all([ + generalSettings.dataUsage(dataDir, refreshUsage), + generalSettings.diskUsage(dataDir), + config.getWorkspaceRoot(), + config.getAutoLaunch(), + config.getPreventSleep() + ]) + return { + autoLaunch: generalSettings.resolveAutoLaunch(autoLaunch), + preventSleep, + preventSleepActive: generalSettings.isPreventSleepActive(), + dataDir, + dataBytes: usage.bytes, + dataUsagePartial: usage.partial, + disk, + workspaceRoot + } + } + ipcMain.handle('general:get', (_event, options?: { refreshUsage?: boolean }) => generalView(options?.refreshUsage === true)) + ipcMain.handle('general:update', async (_event, patch: { autoLaunch?: boolean; preventSleep?: boolean }) => { + if (patch.preventSleep !== undefined) { + await config.setPreventSleep(patch.preventSleep) + generalSettings.configurePreventSleep(patch.preventSleep) + } + if (patch.autoLaunch !== undefined) { + await config.setAutoLaunch(patch.autoLaunch) + const applied = generalSettings.applyAutoLaunch(patch.autoLaunch) + if (applied !== patch.autoLaunch) await config.setAutoLaunch(applied) + } + return generalView() + }) + ipcMain.handle('general:openFolder', async (_event, kind: 'data' | 'workspace') => { + const target = kind === 'workspace' ? await config.getWorkspaceRoot() : app.getPath('userData') + await shell.openPath(target) + }) + ipcMain.handle('general:changeWorkspaceRoot', async (_event, next?: string) => { + let target = next + if (!target) { + const options: OpenDialogOptions = { title: '选择默认工作空间存储路径', properties: ['openDirectory', 'createDirectory'] } + const result = await dialog.showOpenDialog(options) + if (result.canceled || !result.filePaths.length) return generalView() + target = result.filePaths[0] + } + await config.setWorkspaceRoot(target) + changed() + return generalView() + }) + ipcMain.handle('general:open-system-notification-settings', async () => { + if (generalSettings.systemIntegrationSuppressed()) return + await shell.openExternal(process.platform === 'win32' ? 'ms-settings:notifications' : 'x-apple.systempreferences:com.apple.preference.notifications') + }) + ipcMain.handle('models:list', () => models.listModels()) ipcMain.handle('models:save', async (_event, input: CustomModelInput, overwrite?: boolean) => { const result = await models.saveModel(input, overwrite) @@ -644,6 +701,8 @@ export function registerIpc(getWindow: () => BrowserWindow | null): void { // 回合开始时冻结配置:运行中改设置不影响本回合 const controller = new AbortController() activeRuns.set(runId, controller) + // 通用设置「运行时防止系统休眠」:任务运行期间持有 powerSaveBlocker。 + generalSettings.updateActiveRuns(1) const publish = (event: ChatEvent): void => { if (!win?.isDestroyed?.() && !win?.webContents.isDestroyed?.()) { @@ -760,6 +819,7 @@ export function registerIpc(getWindow: () => BrowserWindow | null): void { publish({ runId, type: 'error', message: `会话保存失败:${error instanceof Error ? error.message : String(error)}` }) } finally { activeRuns.delete(runId) + generalSettings.updateActiveRuns(-1) runEmitters.delete(runId) } } diff --git a/src/preload/index.ts b/src/preload/index.ts index a9f8ead..d492b73 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -106,6 +106,13 @@ const api: YCodeApi = { get: () => ipcRenderer.invoke('config:get'), update: (patch: ConfigPatchLike) => ipcRenderer.invoke('config:update', patch) }, + general: { + get: (options) => ipcRenderer.invoke('general:get', options), + update: (patch) => ipcRenderer.invoke('general:update', patch), + openFolder: (kind) => ipcRenderer.invoke('general:openFolder', kind), + changeWorkspaceRoot: (path) => ipcRenderer.invoke('general:changeWorkspaceRoot', path), + openSystemNotificationSettings: () => ipcRenderer.invoke('general:open-system-notification-settings') + }, workspace: { get: () => ipcRenderer.invoke('workspace:get'), choose: () => ipcRenderer.invoke('workspace:choose'), diff --git a/src/renderer/src/components/GeneralSettingsPanel.tsx b/src/renderer/src/components/GeneralSettingsPanel.tsx new file mode 100644 index 0000000..6d016ff --- /dev/null +++ b/src/renderer/src/components/GeneralSettingsPanel.tsx @@ -0,0 +1,310 @@ +/** + * 设置 → 通用:WorkBuddy 原版「通用」面板的 1:1 复刻 + YCode 真实能力接线。 + * + * 结构与几何来源(REA): + * - ev_a2dbeb23e30b9770743e857279a2e43da38fcc48d10612f75ace2938c5b29114(5.8.3 面板自顶向底滚动采样) + * - ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e(面板 DOM 边界、computed style、svg) + * 类名与样式表见 source-general-settings-recovered.css / source-general-settings.css(REA 回收 contexts 投影)。 + * + * 已接入(真实行为,重启后持久化): + * - 运行时防止系统休眠:config.preventSleep,主进程在任务运行期间持有 powerSaveBlocker + * - 开机自动启动:config.autoLaunch,写入/读取系统登录项 + * - 系统缓存目录:真实数据目录路径、占用与磁盘容量、「打开目录」调用系统文件管理器 + * - 默认工作空间存储路径:config.workspaceRoot,「更改」打开目录选择器 + * - 桌面通知:「前往系统设置」打开系统通知设置页 + * + * 未接入原版行为的行(语言、链接打开方式、字体大小、允许锁屏运行、网络代理、自动安装可信技能、 + * 技能自动更新、套件自动更新、通知提示音、定时任务产物通知、体验优化计划):保留原版的文案、 + * 控件类型、尺寸与初始态,但标记 aria-disabled + title「暂未接入」并且不产生任何效果, + * 以免用占位控件冒充已实现能力。 + */ +import { useCallback, useEffect, useState } from 'react' +import type { ReactNode } from 'react' +import type { GeneralSettingsView } from '@shared/types' +import { IconTaskOpenFolder } from './task-menu-icons' +import { IconNotificationAuthorize, IconSelectCaret } from './general-settings-icons' +import './source-general-settings.css' + +const UNAVAILABLE_HINT = '暂未接入' + +/** 原版用量文案(已用 5.45 GB / 共 474.9 GB):KB/MB 保留 1 位,GB/TB 小于 10 保留 2 位、否则 1 位。 */ +export function formatBytes(bytes: number): string { + if (!Number.isFinite(bytes) || bytes <= 0) return '0 B' + const gb = 1024 ** 3 + const tb = 1024 ** 4 + if (bytes >= tb) return `${(bytes / tb).toFixed(bytes / tb < 10 ? 2 : 1)} TB` + if (bytes >= gb) return `${(bytes / gb).toFixed(bytes / gb < 10 ? 2 : 1)} GB` + if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(1)} MB` + return `${(bytes / 1024).toFixed(1)} KB` +} + +function classNames(...values: Array): string { + return values.filter(Boolean).join(' ') +} + +interface SectionProps { + title: string + desc?: ReactNode + action: ReactNode + body?: ReactNode + stacked?: boolean + className?: string +} + +/** 原版行结构:section > section-main > (section-copy > 标题 + 描述) + section-action > 控件。 */ +function Section({ title, desc, action, body, stacked, className }: SectionProps): React.JSX.Element { + return ( +
+
+
+

{title}

+ {desc ?

{desc}

: null} +
+
{action}
+
+ {body ?
{body}
: null} +
+ ) +} + +function Switch({ checked, ariaLabel, disabled, onChange }: { + checked: boolean + ariaLabel: string + disabled?: boolean + onChange(checked: boolean): void +}): React.JSX.Element { + return ( +
+ +
+ ) +} + +/** 未接入的开关:保持原版开/关外观与尺寸,但不可交互。 */ +function UnavailableSwitch({ checked, ariaLabel }: { checked: boolean; ariaLabel: string }): React.JSX.Element { + return ( +
+ +
+ ) +} + +/** 未接入的下拉:原版 120×32 闭合态(value + caret),不可展开。 */ +function UnavailableSelect({ value, className, ariaLabel }: { value: string; className?: string; ariaLabel: string }): React.JSX.Element { + return ( + + + + ) +} + +/** + * 未接入的字体大小滑块:原版 .font-size-slider 结构(224 宽、6 个刻度、2 个激活、thumb 位于 + * left 44.8px = 默认档,标签 小/默认/大)。 + */ +function UnavailableFontSizeSlider(): React.JSX.Element { + const ticks = [0, 1, 2, 3, 4, 5] + return ( +
+
+
+
+
+
+ {ticks.map((index) => ( + + ))} +
+
+
+
+ 小 + 默认 + 大 +
+
+
+ ) +} + +export default function GeneralSettingsPanel(): React.JSX.Element { + const [view, setView] = useState(null) + const [error, setError] = useState('') + const [busy, setBusy] = useState(false) + + const apply = useCallback(async (work: Promise) => { + setBusy(true) + try { setView(await work); setError('') } + catch (reason) { setError(String((reason as Error)?.message ?? reason)) } + finally { setBusy(false) } + }, []) + + useEffect(() => { + let active = true + void window.ycode.general.get() + .then((next) => { if (active) setView(next) }) + .catch((reason) => { if (active) setError(String((reason as Error)?.message ?? reason)) }) + return () => { active = false } + }, []) + + const update = (patch: { autoLaunch?: boolean; preventSleep?: boolean }): void => { void apply(window.ycode.general.update(patch)) } + const openFolder = (kind: 'data' | 'workspace'): void => { void window.ycode.general.openFolder(kind).catch((reason) => setError(String((reason as Error)?.message ?? reason))) } + const changeWorkspaceRoot = (): void => { void apply(window.ycode.general.changeWorkspaceRoot()) } + const openNotificationSettings = (): void => { void window.ycode.general.openSystemNotificationSettings().catch((reason) => setError(String((reason as Error)?.message ?? reason))) } + + const dataBytes = view?.dataBytes ?? 0 + const diskTotal = view?.disk.totalBytes ?? 0 + const diskFree = view?.disk.freeBytes ?? 0 + const cachePercent = diskTotal > 0 ? (dataBytes / diskTotal) * 100 : 0 + const usedPercent = diskTotal > 0 ? ((diskTotal - diskFree) / diskTotal) * 100 : 0 + const otherPercent = Math.max(0, usedPercent - cachePercent) + const freePercent = Math.max(0, 100 - usedPercent) + + return ( +
+ {error ?

{error}

: null} + +
+

常规

+
+
} /> +
} + className="general-settings-panel__link-open-section" /> +
} /> +
+
+ +
+

权限

+
+
} /> +
update({ preventSleep: checked })} />} /> +
update({ autoLaunch: checked })} />} /> +
} + body={
} /> +
} /> +
} /> +
} /> +
+
+ +
+

存储

+
+
+
+
系统缓存目录
+ +
+
+ {view?.dataDir ?? '—'} · 存放对话记录、运行缓存与临时文件。 +
+
+
+ + + +
+
+ + + 系统缓存 + + + + 磁盘已用 + + + + 磁盘可用 + + + 已用 {formatBytes(dataBytes)} / 共 {formatBytes(diskTotal)} + +
+
+
+
+
+
+ YCode 默认工作空间存储路径 +
+

新建任务、工作空间时将自动存放在该路径下;修改后不影响已有数据。

+
+ + +
+
+
+
+ +
+

通知

+
+
+ 前往系统设置 + + } /> +
} /> +
} /> +
+
+ +
+

隐私

+
+
+ 允许使用你的对话数据帮助改进模型。数据会先加密脱敏,且无法关联到你个人。另有链接 + {' '} event.preventDefault()}>了解更多 + } + action={} /> +
+
+
+ ) +} diff --git a/src/renderer/src/components/SettingsDialog.tsx b/src/renderer/src/components/SettingsDialog.tsx index 823f428..bd23089 100644 --- a/src/renderer/src/components/SettingsDialog.tsx +++ b/src/renderer/src/components/SettingsDialog.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react' import { useSettingsStore } from '../stores/settings' import ModelSettingsPanel from './ModelSettingsPanel' +import GeneralSettingsPanel from './GeneralSettingsPanel' import SourceSettingsNavigation from './SourceSettingsNavigation' import { useSharedTheme } from '../useSharedTheme' import { SOURCE_WINDOWS_SETTINGS_TABS } from './SourceSettingsNavigation' @@ -51,10 +52,11 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi return () => window.removeEventListener('keydown', escape) }, [onClose]) const modal =
-
{ setError(''); setSection(id === 'models' ? 'model' : id) }} onUnavailable={() => setError('该设置尚未接入')} />
{error &&

{error}

} + {section === 'settings' && } {section === 'model' && setCreateRequest(0)} onSave={settings.saveModel} onDelete={settings.deleteModel} onTest={(input) => window.ycode.models.test(input)} />} {section === 'workspace' &&

工作空间

{settings.workspaces.map((space) => )}
} {section === 'appearance' &&

外观

{(['light', 'dark'] as const).map((value) => )}
} diff --git a/src/renderer/src/components/general-settings-icons.tsx b/src/renderer/src/components/general-settings-icons.tsx new file mode 100644 index 0000000..ed44d1a --- /dev/null +++ b/src/renderer/src/components/general-settings-icons.tsx @@ -0,0 +1,30 @@ +/** + * 通用设置(设置 → 通用)用到的原版图标。 + * + * path / viewBox / transform 取自 REA 归档 + * ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e 的 svg_nodes,按原样复刻。 + */ +import type { SVGProps } from 'react' + +/** 桌面通知行「前往系统设置」按钮右侧图标。 */ +export function IconNotificationAuthorize(props: SVGProps): React.JSX.Element { + return ( + + + + ) +} + +/** 下拉闭合态的 caret(与 source-select 的 SelectCaretGlyph 同源,取自同一份 REA svg_nodes)。 */ +export function IconSelectCaret(props: SVGProps): React.JSX.Element { + return ( + + + + ) +} diff --git a/src/renderer/src/components/source-general-settings-recovered.css b/src/renderer/src/components/source-general-settings-recovered.css new file mode 100644 index 0000000..963458c --- /dev/null +++ b/src/renderer/src/components/source-general-settings-recovered.css @@ -0,0 +1,5 @@ +/* REA ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94 via contexts sha256=50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380; tokens sha256=13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696; ui-kit sha256=88dbdc1cd3f59d0e8b75824512a465a2be714bc7af481d790fe3a9484f85e01a; general settings panel classes from REA ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e. */ +:where(.general-settings-panel) .wb-button{box-sizing:border-box;cursor:pointer;white-space:nowrap;user-select:none;transition:background-color var(--wb-button-transition-duration) var(--wb-button-transition-easing), color var(--wb-button-transition-duration) var(--wb-button-transition-easing), border-color var(--wb-button-transition-duration) var(--wb-button-transition-easing), transform var(--wb-button-transition-duration) var(--wb-button-transition-easing);border-radius:var(--wb-button-border-radius-medium);border:none;outline:none;justify-content:center;align-items:center;font-family:PingFang SC,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-weight:500;text-decoration:none;display:inline-flex}:where(.general-settings-panel) .wb-button--medium{height:var(--wb-button-height-medium);padding:0 var(--wb-button-padding-medium);font-size:var(--wb-button-font-size-medium);gap:var(--wb-button-gap-medium);min-width:var(--wb-button-min-width-medium);line-height:var(--wb-button-line-height-medium)}:where(.general-settings-panel) .wb-button--xsmall{height:var(--wb-button-height-xsmall);font-size:var(--wb-button-font-size-small);line-height:var(--wb-button-height-xsmall);border-radius:var(--wb-button-border-radius-small);gap:0;padding:0}:where(.general-settings-panel) .wb-button--xsmall.wb-button--icon-only{width:var(--wb-button-height-xsmall);height:var(--wb-button-height-xsmall)}:where(.general-settings-panel) .wb-button--small{height:var(--wb-button-height-small);padding:0 var(--wb-button-padding-small);font-size:var(--wb-button-font-size-small);gap:var(--wb-button-gap-small);min-width:var(--wb-button-min-width-small);border-radius:var(--wb-button-border-radius-small);line-height:var(--wb-button-line-height-small)}:where(.general-settings-panel) .wb-button--small .wb-button__icon{width:var(--wb-button-icon-size-small);height:var(--wb-button-icon-size-small)}:where(.general-settings-panel) .wb-button--large{height:var(--wb-button-height-large);padding:0 var(--wb-button-padding-large);font-size:var(--wb-button-font-size-large);gap:var(--wb-button-gap-large);border-radius:var(--wb-button-border-radius-large);line-height:var(--wb-button-line-height-large)}:where(.general-settings-panel) .wb-button--primary{background-color:var(--wb-button-primary-bg);color:var(--wb-button-primary-fg)}:where(.general-settings-panel) .wb-button--primary:hover:not(:disabled){background-color:var(--wb-button-primary-bg-hover)}:where(.general-settings-panel) .wb-button--primary:active:not(:disabled){background-color:var(--wb-button-primary-bg-active)}:where(.general-settings-panel) .wb-button--primary:disabled,:where(.general-settings-panel) .wb-button--primary.wb-button--disabled{background-color:var(--wb-button-primary-bg-disabled);color:var(--wb-button-primary-fg-disabled);cursor:not-allowed}:where(.general-settings-panel) .wb-button--secondary{background-color:var(--wb-button-secondary-bg);color:var(--wb-button-secondary-fg);border:1px solid var(--wb-button-secondary-border)}:where(.general-settings-panel) .wb-button--secondary:hover:not(:disabled){background-color:var(--wb-button-secondary-bg-hover)}:where(.general-settings-panel) .wb-button--secondary:active:not(:disabled){background-color:var(--wb-button-secondary-bg-active)}:where(.general-settings-panel) .wb-button--secondary:disabled,:where(.general-settings-panel) .wb-button--secondary.wb-button--disabled{color:var(--wb-button-secondary-fg-disabled);border-color:var(--wb-button-secondary-border-disabled);cursor:not-allowed}:where(.general-settings-panel) .wb-button--grey{background-color:var(--wb-button-grey-bg);color:var(--wb-button-grey-fg)}:where(.general-settings-panel) .wb-button--grey:hover:not(:disabled){background-color:var(--wb-button-grey-bg-hover)}:where(.general-settings-panel) .wb-button--grey:active:not(:disabled){background-color:var(--wb-button-grey-bg-active)}:where(.general-settings-panel) .wb-button--grey:disabled,:where(.general-settings-panel) .wb-button--grey.wb-button--disabled{background-color:var(--wb-button-grey-bg-disabled);color:var(--wb-button-grey-fg-disabled);cursor:not-allowed}:where(.general-settings-panel) .wb-button--ghost{background-color:var(--wb-button-ghost-bg);color:var(--wb-button-ghost-fg);font-weight:400}:where(.general-settings-panel) .wb-button--ghost:hover:not(:disabled){background-color:var(--wb-button-ghost-bg-hover);color:var(--wb-button-ghost-fg-hover)}:where(.general-settings-panel) .wb-button--ghost:active:not(:disabled){background-color:var(--wb-button-ghost-bg-active);color:var(--wb-button-ghost-fg-hover)}:where(.general-settings-panel) .wb-button--ghost:disabled,:where(.general-settings-panel) .wb-button--ghost.wb-button--disabled{background-color:var(--wb-button-ghost-bg);color:var(--wb-button-ghost-fg-disabled);cursor:not-allowed}:where(.general-settings-panel) .wb-button--link{background-color:var(--wb-button-link-bg);color:var(--wb-button-link-fg);min-width:0;height:auto;padding:0;font-weight:400}:where(.general-settings-panel) .wb-button--link:hover:not(:disabled){background-color:var(--wb-button-link-bg-hover);color:var(--wb-button-link-fg-hover);text-decoration:underline}:where(.general-settings-panel) .wb-button--link:active:not(:disabled){background-color:var(--wb-button-link-bg-active);color:var(--wb-button-link-fg-hover);text-decoration:underline}:where(.general-settings-panel) .wb-button--link:disabled,:where(.general-settings-panel) .wb-button--link.wb-button--disabled{background-color:var(--wb-button-link-bg);color:var(--wb-button-link-fg-disabled);cursor:not-allowed}:where(.general-settings-panel) .wb-button--icon-only{min-width:unset;padding:0}:where(.general-settings-panel) .wb-button--icon-only.wb-button--medium{width:var(--wb-button-height-medium);height:var(--wb-button-height-medium)}:where(.general-settings-panel) .wb-button--icon-only.wb-button--small{width:var(--wb-button-height-small);height:var(--wb-button-height-small)}:where(.general-settings-panel) .wb-button--icon-only.wb-button--large{width:var(--wb-button-height-large);height:var(--wb-button-height-large)}:where(.general-settings-panel) .wb-button--plain{color:var(--wb-button-plain-fg,inherit);font-weight:var(--wb-button-plain-font-weight,inherit);background-color:#0000}:where(.general-settings-panel) .wb-button--circle{border-radius:50%}:where(.general-settings-panel) .wb-button--pill{border-radius:var(--wb-button-border-radius-pill)}:where(.general-settings-panel) .wb-button--danger.wb-button--primary{background-color:var(--wb-button-danger-bg);color:var(--wb-button-danger-fg-on-solid)}:where(.general-settings-panel) .wb-button--danger.wb-button--primary:hover:not(:disabled){background-color:var(--wb-button-danger-bg-hover);color:var(--wb-button-danger-fg-on-solid)}:where(.general-settings-panel) .wb-button--danger.wb-button--primary:active:not(:disabled){background-color:var(--wb-button-danger-bg-active)}:where(.general-settings-panel) .wb-button--danger.wb-button--primary:disabled,:where(.general-settings-panel) .wb-button--danger.wb-button--primary.wb-button--disabled{background-color:var(--wb-button-danger-bg-disabled);color:var(--wb-button-danger-fg-on-solid-disabled);cursor:not-allowed}:where(.general-settings-panel) .wb-button--danger.wb-button--secondary{color:var(--wb-button-danger-secondary-fg);border-color:var(--wb-button-danger-secondary-border);background-color:var(--wb-button-danger-secondary-bg)}:where(.general-settings-panel) .wb-button--danger.wb-button--secondary:hover:not(:disabled){background-color:var(--wb-button-danger-secondary-bg-hover);color:var(--wb-button-danger-secondary-fg);border-color:var(--wb-button-danger-secondary-border)}:where(.general-settings-panel) .wb-button--danger.wb-button--secondary:active:not(:disabled){background-color:var(--wb-button-danger-secondary-bg-active);color:var(--wb-button-danger-secondary-fg);border-color:var(--wb-button-danger-secondary-border)}:where(.general-settings-panel) .wb-button--danger.wb-button--secondary:disabled,:where(.general-settings-panel) .wb-button--danger.wb-button--secondary.wb-button--disabled{color:var(--wb-button-danger-secondary-fg-disabled);border-color:var(--wb-button-danger-secondary-border-disabled);cursor:not-allowed}:where(.general-settings-panel) .wb-button--danger.wb-button--grey{color:var(--wb-button-danger-grey-fg)}:where(.general-settings-panel) .wb-button--danger.wb-button--grey:hover:not(:disabled){background-color:var(--wb-button-grey-bg-hover);color:var(--wb-button-danger-grey-fg)}:where(.general-settings-panel) .wb-button--danger.wb-button--grey:active:not(:disabled){background-color:var(--wb-button-grey-bg-active)}:where(.general-settings-panel) .wb-button--danger.wb-button--grey:disabled,:where(.general-settings-panel) .wb-button--danger.wb-button--grey.wb-button--disabled{background-color:var(--wb-button-grey-bg-disabled);color:var(--wb-button-danger-fg-disabled);cursor:not-allowed}:where(.general-settings-panel) .wb-button--danger.wb-button--ghost{color:var(--wb-button-danger-ghost-fg)}:where(.general-settings-panel) .wb-button--danger.wb-button--ghost:hover:not(:disabled){background-color:var(--wb-button-danger-bg-soft-hover);color:var(--wb-button-danger-ghost-fg)}:where(.general-settings-panel) .wb-button--danger.wb-button--ghost:active:not(:disabled){background-color:var(--wb-button-danger-bg-soft-active)}:where(.general-settings-panel) .wb-button--danger.wb-button--ghost:disabled,:where(.general-settings-panel) .wb-button--danger.wb-button--ghost.wb-button--disabled{background-color:var(--wb-button-ghost-bg);color:var(--wb-button-danger-fg-disabled);cursor:not-allowed}:where(.general-settings-panel) .wb-button--success.wb-button--primary{background-color:var(--wb-button-success-bg);color:var(--wb-button-success-fg-on-solid)}:where(.general-settings-panel) .wb-button--success.wb-button--primary:hover:not(:disabled),:where(.general-settings-panel) .wb-button--success.wb-button--primary:active:not(:disabled){background-color:var(--wb-button-success-bg-hover)}:where(.general-settings-panel) .wb-button--success.wb-button--primary:disabled,:where(.general-settings-panel) .wb-button--success.wb-button--primary.wb-button--disabled{background-color:var(--wb-button-success-bg-disabled);color:var(--wb-button-success-fg-on-solid-disabled);cursor:not-allowed}:where(.general-settings-panel) .wb-button--success.wb-button--secondary,:where(.general-settings-panel) .wb-button--success.wb-button--grey,:where(.general-settings-panel) .wb-button--success.wb-button--ghost,:where(.general-settings-panel) .wb-button--success.wb-button--plain,:where(.general-settings-panel) .wb-button--success.wb-button--link{color:var(--wb-button-success-fg)}:where(.general-settings-panel) .wb-button--success.wb-button--secondary:disabled,:where(.general-settings-panel) .wb-button--success.wb-button--secondary.wb-button--disabled,:where(.general-settings-panel) .wb-button--success.wb-button--grey:disabled,:where(.general-settings-panel) .wb-button--success.wb-button--grey.wb-button--disabled,:where(.general-settings-panel) .wb-button--success.wb-button--ghost:disabled,:where(.general-settings-panel) .wb-button--success.wb-button--ghost.wb-button--disabled,:where(.general-settings-panel) .wb-button--success.wb-button--plain:disabled,:where(.general-settings-panel) .wb-button--success.wb-button--plain.wb-button--disabled,:where(.general-settings-panel) .wb-button--success.wb-button--link:disabled,:where(.general-settings-panel) .wb-button--success.wb-button--link.wb-button--disabled{color:var(--wb-button-success-fg-disabled)}:where(.general-settings-panel) .wb-button--loading{pointer-events:none;position:relative}:where(.general-settings-panel) .wb-button--loading .wb-button__content,:where(.general-settings-panel) .wb-button--loading .wb-button__icon{opacity:0}:where(.general-settings-panel) .wb-button--loading.wb-button--secondary .wb-button__loading-icon{color:var(--wb-button-loading-secondary-fg)}:where(.general-settings-panel) .wb-button--loading.wb-button--grey .wb-button__loading-icon{color:var(--wb-button-loading-grey-fg)}:where(.general-settings-panel) .wb-button--loading.wb-button--ghost .wb-button__loading-icon{color:var(--wb-button-loading-ghost-fg)}:where(.general-settings-panel) .wb-button--loading.wb-button--danger .wb-button__loading-icon{color:var(--wb-button-loading-danger-fg)}:where(.general-settings-panel) .wb-button--loading.wb-button--danger.wb-button--primary .wb-button__loading-icon{color:var(--wb-button-loading-danger-fg-on-solid)}:where(.general-settings-panel) .wb-button--loading.wb-button--link .wb-button__loading-icon{color:var(--wb-button-loading-link-fg)}:where(.general-settings-panel) .wb-button--loading.wb-button--small .wb-button__loading-icon{width:var(--wb-button-icon-size-small);height:var(--wb-button-icon-size-small)}:where(.general-settings-panel) .wb-button__loading{justify-content:center;align-items:center;display:flex;position:absolute;inset:0}:where(.general-settings-panel) .wb-button__loading-icon{color:var(--wb-button-loading-primary-fg);width:var(--wb-button-icon-size-medium);height:var(--wb-button-icon-size-medium);animation:.8s linear infinite wb-button-spin}:where(.general-settings-panel) .wb-button--full-width{width:100%}:where(.general-settings-panel) .wb-button__icon{width:var(--wb-button-icon-size-medium);height:var(--wb-button-icon-size-medium);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}:where(.general-settings-panel) .wb-button__icon svg{color:currentColor;fill:currentColor;width:100%;height:100%}:where(.general-settings-panel) .wb-button__content{align-items:center;display:inline-flex}:where(.general-settings-panel) .wb-icon{vertical-align:middle;flex-shrink:0;display:inline-block}:where(.general-settings-panel) .wb-icon--spin{transform-origin:50%;animation:1.2s linear infinite wb-icon-spin}:where(.general-settings-panel) .wb-icon--themable{filter:none;transition:filter var(--wb-motion-duration-fast,.16s) var(--wb-motion-easing-standard,ease)}:where(.general-settings-panel) .wb-icon--inline>svg{width:100%;height:100%}:where(.general-settings-panel) .wb-input{font-family:inherit;line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-size:var(--wb-input-font-size)}:where(.general-settings-panel) .wb-input::placeholder{color:var(--wb-input-color-placeholder)}:where(.general-settings-panel) .wb-input{box-sizing:border-box;width:100%;padding:0 var(--wb-input-padding-x);background:var(--wb-input-color-bg);border:1px solid var(--wb-input-border-color);border-radius:var(--wb-input-border-radius);transition:border-color var(--wb-input-motion-duration) var(--wb-input-motion-easing), box-shadow var(--wb-input-motion-duration) var(--wb-input-motion-easing);outline:none;align-items:center;display:inline-flex}:where(.general-settings-panel) .wb-input:hover:not(:disabled):not(:read-only){border-color:var(--wb-input-border-color-hover)}:where(.general-settings-panel) .wb-input:focus:not(:disabled):not(:read-only){border-color:var(--wb-input-border-color-focus)}:where(.general-settings-panel) .wb-input:disabled{color:var(--wb-input-color-text-disabled);background:var(--wb-input-disabled-bg);border-color:var(--wb-input-disabled-border-color);cursor:not-allowed}:where(.general-settings-panel) .wb-input--small{font-family:inherit;line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-size:var(--wb-input-font-size-small)}:where(.general-settings-panel) .wb-input--small::placeholder{color:var(--wb-input-color-placeholder)}:where(.general-settings-panel) .wb-input--small{height:var(--wb-input-height-small);border-radius:var(--wb-input-border-radius-small)}:where(.general-settings-panel) .wb-input--medium{height:var(--wb-input-height-medium);border-radius:var(--wb-input-border-radius)}:where(.general-settings-panel) .wb-input--large{font-family:inherit;line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-size:var(--wb-input-font-size-large)}:where(.general-settings-panel) .wb-input--large::placeholder{color:var(--wb-input-color-placeholder)}:where(.general-settings-panel) .wb-input--large{height:var(--wb-input-height-large);padding:0 var(--wb-input-padding-x-large);border-radius:var(--wb-input-border-radius-large)}:where(.general-settings-panel) .wb-input--status-error{border-color:var(--wb-input-status-error-color)}:where(.general-settings-panel) .wb-input--status-error:hover:not(:disabled):not(:read-only),:where(.general-settings-panel) .wb-input--status-error:focus:not(:disabled):not(:read-only){background:var(--wb-input-status-error-bg);border-color:var(--wb-input-status-error-color)}:where(.general-settings-panel) .wb-input-wrapper{box-sizing:border-box;align-items:center;gap:var(--wb-input-gap);width:100%;padding:0 var(--wb-input-padding-x);background:var(--wb-input-color-bg);border:1px solid var(--wb-input-border-color);border-radius:var(--wb-input-border-radius);color:var(--wb-input-color-text);transition:border-color var(--wb-input-motion-duration) var(--wb-input-motion-easing), box-shadow var(--wb-input-motion-duration) var(--wb-input-motion-easing);display:inline-flex}:where(.general-settings-panel) .wb-input-wrapper:hover:not(.wb-input-wrapper--disabled){border-color:var(--wb-input-border-color-hover)}:where(.general-settings-panel) .wb-input-wrapper:focus-within:not(.wb-input-wrapper--disabled){border-color:var(--wb-input-border-color-focus)}:where(.general-settings-panel) .wb-input-wrapper:has(>.wb-input__icon-button),:where(.general-settings-panel) .wb-input-wrapper:has(>.wb-input__suffix>.wb-input__icon-button){padding-right:var(--wb-input-has-button-padding-right)}:where(.general-settings-panel) .wb-input-wrapper--large:has(>.wb-input__icon-button),:where(.general-settings-panel) .wb-input-wrapper--large:has(>.wb-input__suffix>.wb-input__icon-button){padding-right:var(--wb-input-has-button-padding-right-large)}:where(.general-settings-panel) .wb-input-wrapper--small{height:var(--wb-input-height-small);border-radius:var(--wb-input-border-radius-small)}:where(.general-settings-panel) .wb-input-wrapper--small .wb-input__inner{font-family:inherit;line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-size:var(--wb-input-font-size-small)}:where(.general-settings-panel) .wb-input-wrapper--small .wb-input__inner::placeholder{color:var(--wb-input-color-placeholder)}:where(.general-settings-panel) .wb-input-wrapper--small .wb-input__prefix svg,:where(.general-settings-panel) .wb-input-wrapper--small .wb-input__suffix svg{font-size:var(--wb-input-icon-size-small)}:where(.general-settings-panel) .wb-input-wrapper--small .wb-input__icon-button{width:var(--wb-input-icon-button-size-small);height:var(--wb-input-icon-button-size-small)}:where(.general-settings-panel) .wb-input-wrapper--small .wb-input__icon-button svg{font-size:var(--wb-input-icon-size-small)}:where(.general-settings-panel) .wb-input-wrapper--medium{height:var(--wb-input-height-medium);border-radius:var(--wb-input-border-radius)}:where(.general-settings-panel) .wb-input-wrapper--large{height:var(--wb-input-height-large);padding:0 var(--wb-input-padding-x-large);border-radius:var(--wb-input-border-radius-large)}:where(.general-settings-panel) .wb-input-wrapper--large .wb-input__inner{font-family:inherit;line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-size:var(--wb-input-font-size-large)}:where(.general-settings-panel) .wb-input-wrapper--large .wb-input__inner::placeholder{color:var(--wb-input-color-placeholder)}:where(.general-settings-panel) .wb-input-wrapper--status-error,:where(.general-settings-panel) .wb-input-wrapper--status-error:hover:not(.wb-input-wrapper--disabled),:where(.general-settings-panel) .wb-input-wrapper--status-error:focus-within:not(.wb-input-wrapper--disabled){background:var(--wb-input-status-error-bg);border-color:var(--wb-input-status-error-color)}:where(.general-settings-panel) .wb-input-wrapper--disabled{color:var(--wb-input-color-text-disabled);background:var(--wb-input-disabled-bg);border-color:var(--wb-input-disabled-border-color);cursor:not-allowed}:where(.general-settings-panel) .wb-input__prefix,:where(.general-settings-panel) .wb-input__suffix{color:var(--wb-input-color-icon);flex:none;align-items:center;display:inline-flex}:where(.general-settings-panel) .wb-input__prefix svg,:where(.general-settings-panel) .wb-input__suffix svg{width:1em;height:1em;font-size:var(--wb-input-icon-size)}:where(.general-settings-panel) .wb-input-wrapper--disabled .wb-input__prefix,:where(.general-settings-panel) .wb-input-wrapper--disabled .wb-input__suffix{color:var(--wb-input-color-icon-disabled)}:where(.general-settings-panel) .wb-input__inner{font-family:inherit;line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-size:var(--wb-input-font-size)}:where(.general-settings-panel) .wb-input__inner::placeholder{color:var(--wb-input-color-placeholder)}:where(.general-settings-panel) .wb-input__inner{background:0 0;border:none;outline:none;flex:auto;min-width:0;height:100%;padding:0}:where(.general-settings-panel) .wb-input__inner:disabled{color:var(--wb-input-color-text-disabled);cursor:not-allowed}:where(.general-settings-panel) .wb-input__icon-button{width:var(--wb-input-icon-button-size);height:var(--wb-input-icon-button-size);border-radius:var(--wb-input-icon-button-border-radius);color:var(--wb-input-color-icon);cursor:pointer;transition:color var(--wb-input-motion-duration) var(--wb-input-motion-easing);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}:where(.general-settings-panel) .wb-input__icon-button:hover{color:var(--wb-input-color-icon-hover)}:where(.general-settings-panel) .wb-input__icon-button:disabled,:where(.general-settings-panel) .wb-input__icon-button--disabled{color:var(--wb-input-color-icon-disabled);cursor:not-allowed;pointer-events:none}:where(.general-settings-panel) .wb-input__icon-button svg{width:1em;height:1em;font-size:var(--wb-input-icon-size)}:where(.general-settings-panel) .wb-input__clear--hidden{visibility:hidden;pointer-events:none}:where(.general-settings-panel) .wb-input--filled{background:var(--wb-input-filled-color-bg);border-color:#0000}:where(.general-settings-panel) .wb-input--filled:hover:not(:focus):not(:disabled):not(:read-only){background:var(--wb-input-filled-color-bg-hover);border-color:var(--wb-input-filled-color-bg-hover)}:where(.general-settings-panel) .wb-input--filled:focus:not(:disabled):not(:read-only){box-shadow:none;border-color:#0000}:where(.general-settings-panel) .wb-input--filled.wb-input--status-error{border-color:var(--wb-input-status-error-color)}:where(.general-settings-panel) .wb-input--filled.wb-input--status-error:hover:not(:focus):not(:disabled):not(:read-only){background:var(--wb-input-filled-color-bg-hover);border-color:var(--wb-input-status-error-color)}:where(.general-settings-panel) .wb-input--filled.wb-input--status-error:focus:not(:disabled):not(:read-only){background:var(--wb-input-filled-color-bg);border-color:var(--wb-input-status-error-color)}:where(.general-settings-panel) .wb-input-wrapper--filled{background:var(--wb-input-filled-color-bg);border-color:#0000}:where(.general-settings-panel) .wb-input-wrapper--filled:hover:not(:focus-within):not(.wb-input-wrapper--disabled){background:var(--wb-input-filled-color-bg-hover);border-color:var(--wb-input-filled-color-bg-hover)}:where(.general-settings-panel) .wb-input-wrapper--filled:focus-within:not(.wb-input-wrapper--disabled){box-shadow:none;border-color:#0000}:where(.general-settings-panel) .wb-input-wrapper--filled.wb-input-wrapper--status-error{border-color:var(--wb-input-status-error-color)}:where(.general-settings-panel) .wb-input-wrapper--filled.wb-input-wrapper--status-error:hover:not(:focus-within):not(.wb-input-wrapper--disabled){background:var(--wb-input-filled-color-bg-hover);border-color:var(--wb-input-status-error-color)}:where(.general-settings-panel) .wb-input-wrapper--filled.wb-input-wrapper--status-error:focus-within:not(.wb-input-wrapper--disabled){background:var(--wb-input-filled-color-bg);border-color:var(--wb-input-status-error-color)}:where(.general-settings-panel) .wb-input-tips-wrapper{flex-direction:column;width:100%;display:inline-flex}:where(.general-settings-panel) .wb-input__tips{margin-top:var(--wb-spacing-1);font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);color:var(--wb-color-text-tertiary)}:where(.general-settings-panel) .wb-input-tips-wrapper--status-error .wb-input__tips{color:var(--wb-input-status-error-color)}:where(.general-settings-panel) .wb-input__count{min-width:6ch;font-size:var(--wb-input-count-font-size);color:var(--wb-input-count-color);font-variant-numeric:tabular-nums;user-select:none;flex:none;justify-content:flex-end;align-items:center;line-height:1;display:inline-flex}:where(.general-settings-panel) .wb-input-group{box-sizing:border-box;align-items:stretch;width:100%;display:inline-flex}:where(.general-settings-panel) .wb-input-group .wb-input-wrapper,:where(.general-settings-panel) .wb-input-group .wb-input{flex:auto;min-width:0}:where(.general-settings-panel) .wb-input-group>.wb-input-group__addon:first-child+.wb-input-wrapper,:where(.general-settings-panel) .wb-input-group>.wb-input-group__addon:first-child+.wb-input{border-top-left-radius:0;border-bottom-left-radius:0}:where(.general-settings-panel) .wb-input-group>.wb-input-wrapper:has(+.wb-input-group__addon),:where(.general-settings-panel) .wb-input-group>.wb-input:has(+.wb-input-group__addon){border-top-right-radius:0;border-bottom-right-radius:0}:where(.general-settings-panel) .wb-input-group--small .wb-input-group__addon{height:var(--wb-input-height-small);font-size:var(--wb-input-font-size-small);border-radius:var(--wb-input-border-radius-small)}:where(.general-settings-panel) .wb-input-group--medium .wb-input-group__addon{height:var(--wb-input-height-medium);border-radius:var(--wb-input-border-radius)}:where(.general-settings-panel) .wb-input-group--large .wb-input-group__addon{height:var(--wb-input-height-large);width:var(--wb-input-height-large);border-radius:var(--wb-radius-xl);justify-content:center;padding:0}:where(.general-settings-panel) .wb-input-group__addon{box-sizing:border-box;padding:0 var(--wb-input-addon-padding-x);font-size:var(--wb-input-addon-font-size);line-height:var(--wb-input-addon-line-height);color:var(--wb-input-addon-color-text);background:var(--wb-input-addon-color-bg);border:1px solid var(--wb-input-addon-border-color);white-space:nowrap;flex:none;align-items:center;display:inline-flex}:where(.general-settings-panel) .wb-input-group__addon svg{width:1em;height:1em;font-size:var(--wb-input-icon-size)}:where(.general-settings-panel) .wb-input-group__addon:first-child{border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}:where(.general-settings-panel) .wb-input-group__addon:last-child:not(:first-child){border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}:where(.general-settings-panel) .wb-input-group--disabled .wb-input-group__addon{color:var(--wb-input-color-text-disabled);background:var(--wb-input-disabled-bg);border-color:var(--wb-input-disabled-border-color)}:where(.general-settings-panel) .wb-input-group__addon:has(.wb-button){background:0 0;border:none;padding:0}:where(.general-settings-panel) .wb-input-group__addon:has(.wb-button)>.wb-button{border-top-left-radius:0;border-bottom-left-radius:0;height:100%}:where(.general-settings-panel) .wb-input-number{box-sizing:border-box;background:var(--wb-input-color-bg);border:1px solid var(--wb-input-border-color);border-radius:var(--wb-input-border-radius);width:100%;color:var(--wb-input-color-text);transition:border-color var(--wb-input-motion-duration) var(--wb-input-motion-easing), box-shadow var(--wb-input-motion-duration) var(--wb-input-motion-easing);align-items:center;display:inline-flex;overflow:hidden}:where(.general-settings-panel) .wb-input-number:hover:not(.wb-input-number--disabled){border-color:var(--wb-input-border-color-hover)}:where(.general-settings-panel) .wb-input-number:focus-within:not(.wb-input-number--disabled){border-color:var(--wb-input-border-color-focus)}:where(.general-settings-panel) .wb-input-number--small{height:var(--wb-input-height-small);border-radius:var(--wb-input-border-radius-small)}:where(.general-settings-panel) .wb-input-number--medium{height:var(--wb-input-height-medium)}:where(.general-settings-panel) .wb-input-number--large{height:var(--wb-input-height-large);border-radius:var(--wb-input-border-radius-large)}:where(.general-settings-panel) .wb-input-number__inner{min-width:0;height:100%;padding:0 var(--wb-input-padding-x);font-family:inherit;font-size:var(--wb-input-font-size);line-height:var(--wb-input-line-height);color:var(--wb-input-color-text);font-variant-numeric:tabular-nums;text-align:left;background:0 0;border:none;outline:none;flex:auto}:where(.general-settings-panel) .wb-input-number__inner::placeholder{color:var(--wb-input-color-placeholder)}:where(.general-settings-panel) .wb-input-number__inner:disabled{color:var(--wb-input-color-text-disabled);cursor:not-allowed}:where(.general-settings-panel) .wb-input-number--small .wb-input-number__inner{font-size:var(--wb-input-font-size-small)}:where(.general-settings-panel) .wb-input-number--large .wb-input-number__inner{padding:0 var(--wb-input-padding-x-large)}:where(.general-settings-panel) .wb-input-number__controls{opacity:0;pointer-events:none;height:100%;transition:opacity var(--wb-input-motion-duration) var(--wb-input-motion-easing);flex-direction:column;flex:none;gap:2px;display:inline-flex}:where(.general-settings-panel) .wb-input-number:hover:not(.wb-input-number--disabled) .wb-input-number__controls{opacity:1;pointer-events:auto}:where(.general-settings-panel) .wb-input-number__step{min-height:0;width:var(--wb-input-number-controls-width);background:var(--wb-input-number-step-color-bg);height:100%;color:var(--wb-input-number-step-color);border-radius:0;flex:1 1 0}:where(.general-settings-panel) .wb-input-number__step svg{width:var(--wb-input-icon-size);height:var(--wb-input-icon-size);font-size:var(--wb-input-icon-size)}:where(.general-settings-panel) .wb-input-number__step:hover:not(:disabled){color:var(--wb-input-number-step-color-hover);background:var(--wb-input-number-step-color-bg-hover)}:where(.general-settings-panel) .wb-input-number__step:disabled,:where(.general-settings-panel) .wb-input-number__step.wb-input__icon-button--disabled{cursor:not-allowed;pointer-events:auto}:where(.general-settings-panel) .wb-input-number__step:disabled:hover,:where(.general-settings-panel) .wb-input-number__step.wb-input__icon-button--disabled:hover{color:var(--wb-input-number-step-color);background:var(--wb-input-number-step-color-bg)}:where(.general-settings-panel) .wb-input-number .wb-input-number__step:disabled,:where(.general-settings-panel) .wb-input-number .wb-input-number__step.wb-input__icon-button--disabled{cursor:not-allowed;pointer-events:auto;color:var(--wb-input-color-icon-disabled);background:var(--wb-input-number-step-color-bg)}:where(.general-settings-panel) .wb-input-number .wb-input-number__step:disabled:hover,:where(.general-settings-panel) .wb-input-number .wb-input-number__step.wb-input__icon-button--disabled:hover{color:var(--wb-input-color-icon-disabled);background:var(--wb-input-number-step-color-bg)}:where(.general-settings-panel) .wb-input-number--status-error,:where(.general-settings-panel) .wb-input-number--status-error:hover:not(.wb-input-number--disabled),:where(.general-settings-panel) .wb-input-number--status-error:focus-within:not(.wb-input-number--disabled){border-color:var(--wb-input-status-error-color)}:where(.general-settings-panel) .wb-input-number--disabled{color:var(--wb-input-color-text-disabled);background:var(--wb-input-disabled-bg);border-color:var(--wb-input-disabled-border-color);cursor:not-allowed}:where(.general-settings-panel) .wb-input-wrapper--disabled .wb-timepicker-trigger__icon{color:var(--wb-input-color-icon-disabled)}:where(.general-settings-panel) .wb-switch{align-items:center;gap:var(--wb-switch-gap);cursor:pointer;user-select:none;color:var(--wb-switch-color-text);font-size:var(--wb-switch-font-size);line-height:var(--wb-switch-line-height);display:inline-flex}:where(.general-settings-panel) .wb-switch__control{flex-shrink:0;display:inline-flex;position:relative}:where(.general-settings-panel) .wb-switch__input{opacity:0;width:100%;height:100%;cursor:inherit;margin:0;padding:0;position:absolute;inset:0}:where(.general-settings-panel) .wb-switch__track{width:var(--wb-switch-track-width);height:var(--wb-switch-track-height);padding:var(--wb-switch-track-padding);border-radius:var(--wb-switch-radius);background:var(--wb-switch-track-bg);box-sizing:border-box;transition:background var(--wb-switch-duration) var(--wb-switch-easing), border-color var(--wb-switch-duration) var(--wb-switch-easing);border:1px solid #0000;align-items:center;display:inline-flex}:where(.general-settings-panel) .wb-switch__thumb{width:var(--wb-switch-thumb-size);height:var(--wb-switch-thumb-size);border-radius:var(--wb-switch-radius);background:var(--wb-switch-thumb-bg);box-shadow:var(--wb-switch-thumb-shadow);transition:transform var(--wb-switch-duration) var(--wb-switch-easing)}:where(.general-settings-panel) .wb-switch__input:checked+.wb-switch__track{background:var(--wb-switch-track-bg-checked);border:1px solid var(--wb-switch-track-border-color-checked)}:where(.general-settings-panel) .wb-switch__input:checked+.wb-switch__track .wb-switch__thumb{transform:translateX(var(--wb-switch-thumb-translate))}:where(.general-settings-panel) .wb-switch__input:focus-visible+.wb-switch__track{outline:var(--wb-switch-outline-focus-width) solid var(--wb-switch-color-outline-focus);outline-offset:var(--wb-switch-outline-focus-offset)}:where(.general-settings-panel) .wb-switch:has(.wb-switch__input:disabled){cursor:not-allowed}:where(.general-settings-panel) .wb-switch:has(.wb-switch__input:disabled) .wb-switch__input:checked+.wb-switch__track{background:var(--wb-switch-track-bg-checked-disabled);border:1px solid #0000}:where(.general-settings-panel) .wb-switch:has(.wb-switch__input:disabled) .wb-switch__input:not(:checked)+.wb-switch__track{background:var(--wb-switch-track-bg-disabled);border:1px solid #0000}:where(.general-settings-panel) .wb-switch:has(.wb-switch__input:disabled) .wb-switch__thumb{background:var(--wb-switch-thumb-bg-disabled-unchecked)}:where(.general-settings-panel) .wb-switch--loading{cursor:wait}:where(.general-settings-panel) .wb-switch--loading:has(.wb-switch__input:disabled) .wb-switch__thumb{box-shadow:none;background:0 0}:where(.general-settings-panel) .wb-switch--loading .wb-switch__spinner{z-index:1;background:0 0;position:relative}:where(.general-settings-panel) .wb-switch--loading:has(.wb-switch__input:disabled) .wb-switch__input:not(:checked)+.wb-switch__track{background:var(--wb-switch-track-bg-disabled);opacity:1;border-color:#0000}:where(.general-settings-panel) .wb-switch--loading:has(.wb-switch__input:disabled) .wb-switch__input:checked+.wb-switch__track{background:var(--wb-switch-track-bg-checked-disabled);opacity:1;border-color:#0000}:where(.general-settings-panel) .wb-switch__spinner{border-radius:var(--wb-switch-radius);border:var(--wb-switch-spinner-border-width) solid var(--wb-switch-track-color-spinner);box-sizing:border-box;transform-origin:50%;will-change:transform;width:100%;height:100%;animation:wb-switch-spin var(--wb-switch-spinner-duration) linear infinite;border-top-color:#0000;display:block}:where(.general-settings-panel) .wb-switch--loading .wb-switch__spinner{animation-name:wb-switch-spin;animation-duration:var(--wb-switch-spinner-duration);animation-timing-function:linear;animation-iteration-count:infinite;animation-play-state:running}:where(.general-settings-panel) .wb-switch--danger .wb-switch__input:checked+.wb-switch__track{background:var(--wb-switch-track-bg-danger);border:1px solid var(--wb-switch-track-border-color-danger)}:where(.general-settings-panel) .wb-switch--danger:has(.wb-switch__input:disabled) .wb-switch__input:checked+.wb-switch__track{background:var(--wb-switch-track-bg-danger-disabled);border:1px solid #0000}:where(.general-settings-panel) .wb-switch--small{font-size:var(--wb-switch-font-size-small);line-height:var(--wb-switch-line-height-small)}:where(.general-settings-panel) .wb-switch--small .wb-switch__track{width:var(--wb-switch-track-width-small);height:var(--wb-switch-track-height-small)}:where(.general-settings-panel) .wb-switch--small .wb-switch__thumb{width:var(--wb-switch-thumb-size-small);height:var(--wb-switch-thumb-size-small)}:where(.general-settings-panel) .wb-switch--small .wb-switch__input:checked+.wb-switch__track .wb-switch__thumb{transform:translateX(var(--wb-switch-thumb-translate-small))}:where(.general-settings-panel) .wb-switch--large .wb-switch__track{width:var(--wb-switch-track-width-large);height:var(--wb-switch-track-height-large)}:where(.general-settings-panel) .wb-switch--large .wb-switch__thumb{width:var(--wb-switch-thumb-size-large);height:var(--wb-switch-thumb-size-large)}:where(.general-settings-panel) .wb-switch--large .wb-switch__input:checked+.wb-switch__track .wb-switch__thumb{transform:translateX(var(--wb-switch-thumb-translate-large))}:where(.general-settings-panel) .wb-switch__label{display:inline-block}:where(.general-settings-panel) .wb-switch--projection{cursor:inherit}:where(.general-settings-panel) .wb-switch--projection.wb-switch--checked .wb-switch__track{background:var(--wb-switch-track-bg-checked);border:1px solid var(--wb-switch-track-border-color-checked)}:where(.general-settings-panel) .wb-switch--projection.wb-switch--checked .wb-switch__thumb{transform:translateX(var(--wb-switch-thumb-translate))}:where(.general-settings-panel) .wb-switch--projection.wb-switch--checked.wb-switch--small .wb-switch__thumb{transform:translateX(var(--wb-switch-thumb-translate-small))}:where(.general-settings-panel) .wb-switch--projection.wb-switch--checked.wb-switch--large .wb-switch__thumb{transform:translateX(var(--wb-switch-thumb-translate-large))}:where(.general-settings-panel) .wb-switch--projection[aria-disabled=true] .wb-switch__track{background:var(--wb-switch-track-bg-disabled);border:1px solid #0000}:where(.general-settings-panel) .wb-switch--projection[aria-disabled=true] .wb-switch__thumb{background:var(--wb-switch-thumb-bg-disabled-unchecked)}:where(.general-settings-panel) .wb-switch--projection[aria-disabled=true].wb-switch--checked .wb-switch__track{background:var(--wb-switch-track-bg-checked-disabled);border:1px solid #0000}:where(.general-settings-panel) .wb-switch--spring .wb-switch__track{transition-duration:.15s;transition-timing-function:cubic-bezier(.34,1.36,.64,1)}:where(.general-settings-panel) .wb-switch--spring .wb-switch__thumb{transition:none}@media (prefers-reduced-motion:reduce){:where(.general-settings-panel) .wb-switch--spring .wb-switch__track{transition:none}} +[data-settings-locate=true] .general-settings-panel__section-title,[data-settings-locate=true] .general-settings-panel__workspace-path-title,[data-settings-locate=true] .wb-storage__card-title{animation:1.4s ease-in-out 3 wb-settings-locate-breath}@media (prefers-reduced-motion:reduce){[data-settings-locate=true] .general-settings-panel__section-title,[data-settings-locate=true] .general-settings-panel__workspace-path-title,[data-settings-locate=true] .wb-storage__card-title{color:var(--wb-brand-primary);animation:none}}.general-settings-panel{--settings-title:var(--wb-color-text-primary);--settings-body:var(--wb-color-text-secondary);--settings-accent:var(--wb-brand-primary);gap:var(--wb-spacing-5);width:100%;min-width:0;color:var(--settings-title);flex-direction:column;display:flex}.general-settings-panel__list{gap:var(--wb-spacing-3);padding-bottom:var(--wb-spacing-2);flex-direction:column;display:flex}.general-settings-panel__group{gap:var(--wb-spacing-3);flex-direction:column;display:flex}.general-settings-panel__group-label{padding-left:var(--wb-spacing-3);color:var(--wb-color-text-tertiary);font-size:var(--wb-font-body-size);font-weight:400;line-height:var(--wb-font-body-line-height);margin:0}.general-settings-panel__group-card{padding:var(--wb-spacing-4) var(--wb-spacing-5);border-radius:var(--wb-radius-xl);background:var(--wb-bg-primary);flex-direction:column;display:flex}.general-settings-panel__section{border-radius:var(--wb-radius-xl);background:var(--wb-bg-primary);border:none}.general-settings-panel__section--inline,.general-settings-panel__section--stacked{padding:var(--wb-spacing-4) var(--wb-spacing-5)}.general-settings-panel__section--plain{background:0 0;border-radius:0}.general-settings-panel__section--plain.general-settings-panel__section--inline,.general-settings-panel__section--plain.general-settings-panel__section--stacked{padding:var(--wb-spacing-4) 0}.general-settings-panel__group-card>.general-settings-panel__section--plain:first-child.general-settings-panel__section--inline,.general-settings-panel__group-card>.general-settings-panel__section--plain:first-child.general-settings-panel__section--stacked{padding-top:0}.general-settings-panel__group-card>.general-settings-panel__section--plain:last-child.general-settings-panel__section--inline,.general-settings-panel__group-card>.general-settings-panel__section--plain:last-child.general-settings-panel__section--stacked{padding-bottom:0}.general-settings-panel__section-main{justify-content:space-between;align-items:center;gap:var(--wb-spacing-6);min-width:0;display:flex}.general-settings-panel__section-copy{flex:1;min-width:0}.general-settings-panel__section-icon{flex-shrink:0;justify-content:center;align-items:center;display:flex}.general-settings-panel__section--with-icon .general-settings-panel__section-main{gap:var(--wb-spacing-4)}.general-settings-panel__section-title{color:var(--settings-title);font-size:var(--wb-font-body-size);font-weight:500;line-height:var(--wb-font-body-line-height);letter-spacing:0;align-items:center;margin:0;display:inline-flex}.general-settings-panel__section-desc{max-width:680px;color:var(--wb-color-text-tertiary);font-size:var(--wb-font-body-size);font-weight:400;line-height:var(--wb-font-body-line-height);letter-spacing:0;word-break:break-word;margin:0;text-decoration:none}.general-settings-panel__inline-link{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:pointer;transition:color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard);text-decoration:none}.general-settings-panel__inline-link:hover{color:var(--wb-color-text-primary)}.general-settings-panel__section-action{flex-shrink:0;justify-content:flex-end;align-items:center;display:flex}.general-settings-panel__section-action .wb-select__option-label{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.general-settings-panel__link-open-section .general-settings-panel__section-action{align-self:center}.general-settings-panel__link-open-select.wb-select{width:120px;min-width:120px}.general-settings-panel__link-open-option-text{flex:1;min-width:0}.general-settings-panel__link-open-option-name{font-size:var(--wb-font-body-size);font-weight:400;line-height:var(--wb-font-body-line-height)}.wb-select__option--single-selected .general-settings-panel__link-open-option-name{font-weight:500}.general-settings-panel__link-open-option-hint{margin-top:var(--wb-spacing-1);color:var(--wb-color-text-tertiary,#00000080);white-space:normal;font-size:12px;font-weight:400;line-height:20px}.general-settings-panel__link-open-select-popover.wb-select-popover{min-width:368px;width:368px!important;max-width:368px!important}.general-settings-panel__link-open-select-popover.wb-select-popover .wb-select__option{width:100%;height:auto;min-height:var(--wb-select-size-control-md);padding-top:var(--wb-spacing-2-5);padding-bottom:var(--wb-spacing-2-5);gap:var(--wb-select-spacing-1);align-items:flex-start}.general-settings-panel__link-open-select-popover.wb-select-popover .wb-select__option-label{text-overflow:unset;white-space:normal;overflow:visible}.general-settings-panel__link-open-select-popover.wb-select-popover .wb-select__option-check{flex-shrink:0;margin-top:2px;margin-left:auto}.general-settings-panel__section-body{padding-top:var(--wb-spacing-4)}.general-settings-panel__slider-wrap{flex-shrink:0}.general-settings-panel__workspace{gap:var(--wb-spacing-3);flex-direction:column;display:flex}.general-settings-panel__workspace-header{align-items:center;gap:var(--wb-spacing-1);display:flex}.general-settings-panel__workspace-path-title{color:var(--wb-color-text-primary);font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);margin:0;font-weight:500}.general-settings-panel__workspace-path-desc{color:var(--wb-color-text-tertiary);font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);margin:-4px 0 0}.general-settings-panel__path-row{align-items:center;gap:var(--wb-spacing-3);min-width:0;display:flex}.general-settings-panel__path-input{cursor:default;min-width:0;color:var(--wb-input-color-text-disabled);flex:1}.general-settings-panel__path-button{height:32px;padding:0 var(--wb-spacing-4);border:1px solid var(--wb-border-default,var(--wb-color-border-secondary));border-radius:var(--wb-radius-lg);color:var(--settings-title);font-size:var(--wb-font-body-size);font-weight:400;line-height:var(--wb-font-body-line-height);letter-spacing:0;cursor:pointer;transition:background-color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard), border-color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard);background:0 0;flex:none}.general-settings-panel__path-button:hover{background:var(--wb-bg-hover);border-color:var(--wb-color-border-primary,var(--wb-border-default))}.general-settings-panel__path-button:focus-visible{outline:2px solid color-mix(in srgb, var(--settings-accent) 70%, transparent);outline-offset:2px}.general-settings-panel__path-button:disabled{opacity:.45;cursor:default}.general-settings-panel__switch-wrap{justify-content:flex-end;align-items:center;display:flex}.general-settings-panel__switch-tooltip-trigger{align-items:center;display:inline-flex}.general-settings-panel__notification-group{flex-direction:column;gap:8px;margin-top:4px;display:flex}.general-settings-panel__notification-group-title{margin:var(--wb-spacing-2) 0 0;padding-left:var(--wb-spacing-3);color:var(--wb-color-text-tertiary);font-size:var(--wb-font-body-size);font-weight:400;line-height:var(--wb-font-body-line-height);letter-spacing:0}.general-settings-panel__notification-title{align-items:center;gap:8px;min-width:0;display:inline-flex}.general-settings-panel__notification-authorized-badge{background:color-mix(in srgb, var(--wb-color-text-success-default) 15%, transparent);height:18px;color:var(--wb-color-text-success-default);border-radius:3px;align-items:center;padding:0 6px;font-size:11px;font-weight:500;line-height:18px;display:inline-flex}.general-settings-panel__notification-authorize{border-radius:var(--wb-radius-lg);background:var(--wb-bg-secondary);height:30px;color:var(--wb-color-text-primary);font-size:var(--wb-font-body-size);font-weight:500;line-height:var(--wb-font-body-line-height);letter-spacing:0;cursor:pointer;transition:background-color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard);border:none;flex:none;align-items:center;gap:6px;padding:6px 14px;display:inline-flex}.general-settings-panel__notification-authorize:hover:not(:disabled){background:var(--wb-bg-hover)}.general-settings-panel__notification-authorize:active:not(:disabled){background:var(--wb-bg-active)}.general-settings-panel__notification-authorize:focus-visible{outline:2px solid color-mix(in srgb, var(--wb-brand-primary) 60%, transparent);outline-offset:2px}.general-settings-panel__notification-authorize:disabled{opacity:.45;cursor:default}.general-settings-panel__notification-authorize-icon{flex:none;display:block}.general-settings-panel__card,.general-settings-panel__card-row,.general-settings-panel__card-left,.general-settings-panel__card-right{min-width:0}.general-settings-panel .ant-switch{min-width:32px;height:18px;line-height:18px}.general-settings-panel .ant-switch .ant-switch-handle{width:14px;height:14px;top:2px}.general-settings-panel .ant-switch-checked .ant-switch-handle{inset-inline-start:calc(100% - 16px)}.power-save-blocker-settings__select.wb-select{width:max-content;min-width:120px}.power-save-blocker-settings__option-text{flex:1;min-width:0}.power-save-blocker-settings__option-name{font-size:var(--wb-font-body-size);font-weight:400;line-height:var(--wb-font-body-line-height)}.wb-select__option--single-selected .power-save-blocker-settings__option-name{font-weight:500}.power-save-blocker-settings__option-hint{margin-top:var(--wb-spacing-1);color:var(--wb-color-text-tertiary,#00000080);white-space:normal;font-size:12px;font-weight:400;line-height:20px}.power-save-blocker-settings__popover.wb-select-popover{min-width:368px;width:368px!important;max-width:368px!important}.power-save-blocker-settings__popover.wb-select-popover .wb-select__option{width:100%;height:auto;min-height:var(--wb-select-size-control-md);padding-top:var(--wb-spacing-2-5);padding-bottom:var(--wb-spacing-2-5);gap:var(--wb-select-spacing-1);align-items:flex-start}.power-save-blocker-settings__popover.wb-select-popover .wb-select__option-label{text-overflow:unset;white-space:normal;flex:1;overflow:visible}.power-save-blocker-settings__popover.wb-select-popover .wb-select__option-check{flex-shrink:0;margin-top:2px;margin-left:auto}.extensions-settings-panel .general-settings-panel__section-desc strong{color:var(--wb-color-text-primary);font-weight:500}.extensions-settings-panel .general-settings-panel__section-action{min-width:84px}.extensions-settings-panel .general-settings-panel__section-action .wb-button{justify-content:center;width:100%}.extensions-settings-panel__capability-group .general-settings-panel__section-action{min-width:40px}.extensions-settings-panel__browser-extension-list>.general-settings-panel__section--plain:last-child{padding-bottom:0}.extensions-settings-panel__browser-extension-item .general-settings-panel__section-action{min-width:84px}.font-size-slider{gap:var(--wb-spacing-2);flex-direction:column;width:224px;display:flex}.font-size-slider__track{width:224px;height:var(--wb-spacing-5);cursor:pointer;user-select:none;align-items:center;display:flex;position:relative}.font-size-slider__marks{width:224px;height:var(--wb-spacing-2);position:relative}.font-size-slider__marks-track{background:var(--wb-color-text-disabled);width:100%;height:1px;position:absolute;top:calc(50% - .5px);left:0}.font-size-slider__marks-fill{background:var(--wb-color-text-primary);height:1px;transition:width .15s;position:absolute;top:calc(50% - .5px);left:0}.font-size-slider__tick{background:var(--wb-color-text-disabled);width:1px;height:100%;position:absolute;top:0;transform:translate(-50%)}.font-size-slider__tick--active{background:var(--wb-color-text-primary)}.font-size-slider__thumb{width:var(--wb-spacing-3);height:var(--wb-spacing-5);background:var(--wb-bg-primary);border:1px solid var(--wb-border-card);border-radius:var(--wb-radius-lg);box-shadow:var(--wb-shadow-thumb);cursor:grab;transition:left .15s;position:absolute;top:50%;transform:translate(-50%,-50%)}.font-size-slider__thumb:active{cursor:grabbing}.font-size-slider__track--dragging .font-size-slider__thumb{cursor:grabbing;transition:none}.font-size-slider__track--dragging .font-size-slider__marks-fill{transition:none}.font-size-slider__labels{width:224px;height:var(--wb-spacing-6);position:relative}.font-size-slider__label{font-size:var(--wb-font-caption-size);line-height:var(--wb-font-caption-line-height);color:var(--wb-color-text-tertiary);white-space:nowrap;position:absolute;top:0;transform:translate(-50%)}.font-size-slider__label:first-child{left:0}.font-size-slider__label:nth-child(2){left:44.8px}.font-size-slider__label:last-child{left:224px}[data-theme=dark] .font-size-slider__thumb,body[data-vscode-theme-name=IDE\ Night] .font-size-slider__thumb,body.vscode-dark .font-size-slider__thumb,.vscode-dark .font-size-slider__thumb,body.dark .font-size-slider__thumb,.dark .font-size-slider__thumb{background:var(--wb-bg-secondary);border-color:var(--wb-border-hover)}@media (width<=720px){.general-settings-panel__section-main{align-items:flex-start;gap:var(--wb-spacing-5);flex-direction:column}.general-settings-panel__section-action{justify-content:flex-start;width:100%;min-width:0}.general-settings-panel__path-row{width:100%}}.general-settings-panel__section--proxy .general-settings-panel__section-body{margin-top:var(--wb-spacing-3);border-top:none;padding-top:0}.proxy-section{gap:var(--wb-spacing-4);flex-direction:column;display:flex}.proxy-section__manual{gap:var(--wb-spacing-3);flex-direction:column;display:flex}.proxy-section__manual-row{align-items:center;gap:var(--wb-spacing-3);display:flex}.proxy-section__url-input{flex:auto;min-width:0}.proxy-section__enabled-label{box-sizing:border-box;min-width:82px;white-space:nowrap;text-align:right;color:var(--wb-color-text-brand-active,#009273);flex-shrink:0;padding-inline-end:var(--wb-spacing-3);font-family:PingFang SC;font-size:14px;font-style:normal;font-weight:500;line-height:22px;text-decoration-line:none}.proxy-section__action-button{flex-shrink:0;min-width:120px}.proxy-section__actions{align-items:center;gap:8px;display:flex}.proxy-section__status{align-items:center;gap:6px;font-size:12px;line-height:18px;display:inline-flex}.proxy-section__status--ok{color:var(--cb-success-color,#1aa260)}.proxy-section__status--warn{color:var(--cb-warning,#d6a700)}.proxy-section__status--error{color:var(--cb-error-foreground,#e34c4c)}.proxy-section__status-latency{color:var(--cb-text-muted,var(--cb-text-secondary))}.wb-storage{gap:var(--wb-spacing-3);flex-direction:column;display:flex}.wb-storage__group-label{padding-left:var(--wb-spacing-3);color:var(--wb-color-text-tertiary);font-size:var(--wb-font-body-size);font-weight:400;line-height:var(--wb-font-body-line-height);margin:0}.wb-storage__card{gap:var(--wb-spacing-6);padding:var(--wb-spacing-4) var(--wb-spacing-5);background:var(--wb-bg-primary);border-radius:var(--wb-radius-xl);border:none;flex-direction:column;display:flex}.wb-storage__cache{flex-direction:column;gap:0;display:flex}.wb-storage__divider{background:var(--wb-border-subtle);border:none;width:100%;height:1px;margin:0}.wb-storage__card-title-row{justify-content:space-between;align-items:center;gap:var(--wb-spacing-3);display:flex}.wb-storage__card-title{font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);color:var(--wb-color-text-primary);font-weight:500}.wb-storage__path-row{justify-content:space-between;align-items:flex-start;gap:var(--wb-spacing-2);display:flex}.wb-storage__path-text{min-width:0;font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);color:var(--wb-color-text-tertiary);flex:1}.general-settings-panel__storage-overview{gap:var(--wb-spacing-2);margin-top:var(--wb-spacing-3);flex-direction:column;display:flex}.general-settings-panel__storage-progress{width:100%;height:var(--wb-spacing-2);border-radius:var(--wb-radius-xs);background:var(--wb-button-grey-bg);gap:1px;display:flex;overflow:hidden}.general-settings-panel__storage-progress-seg{height:100%;transition:width var(--wb-motion-duration-base) var(--wb-motion-easing-standard);display:block}.general-settings-panel__storage-progress-seg--workbuddy{background:var(--wb-color-text-brand-default)}.general-settings-panel__storage-progress-seg--other{background:var(--wb-color-text-tertiary)}.general-settings-panel__storage-legend{align-items:center;gap:var(--wb-spacing-5);min-width:0;display:flex}.general-settings-panel__storage-legend-item{align-items:center;gap:var(--wb-spacing-2);font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);color:var(--wb-color-text-tertiary);display:flex}.general-settings-panel__storage-legend-dot{border-radius:50%;flex-shrink:0;width:6px;height:6px;display:inline-block}.general-settings-panel__storage-legend-dot--workbuddy{background:var(--wb-color-text-brand-default)}.general-settings-panel__storage-legend-dot--other{background:var(--wb-color-text-tertiary)}.general-settings-panel__storage-legend-dot--free{background:var(--wb-bg-tertiary)}.general-settings-panel__storage-legend-label{color:var(--wb-color-text-tertiary)}.general-settings-panel__storage-usage-text{font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);color:var(--wb-color-text-tertiary);white-space:nowrap;margin-left:auto}.wb-storage__open-folder{white-space:nowrap;height:auto;font-size:var(--wb-font-body-size);font-style:normal;font-weight:400;line-height:var(--wb-font-body-line-height);flex-shrink:0;text-decoration-line:none;color:var(--wb-color-text-tertiary,#00000080)!important;background:0 0!important;border:none!important;padding:0!important}.wb-storage__open-folder:hover{color:var(--wb-color-text-primary)!important;background:0 0!important}.wb-storage__policy{justify-content:space-between;align-items:center;gap:var(--wb-spacing-4);display:flex}.wb-storage__policy-copy{flex:1;min-width:0}.wb-storage__policy-title{margin:0 0 var(--wb-spacing-1) 0;font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);color:var(--wb-color-text-primary);font-weight:500}.wb-storage__policy-desc{font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height);color:var(--wb-color-text-secondary);margin:0}.wb-storage__policy-action{flex-shrink:0;min-width:120px;position:relative}.wb-storage-confirm-popover{z-index:2;background:color-mix(in srgb, var(--wb-bg-primary) 80%, transparent);backdrop-filter:blur(40px);border:none;border-radius:20px;width:300px;padding:20px;position:absolute;top:50%;right:calc(100% + 10px);transform:translateY(-50%);box-shadow:0 8px 32px 8px #0000001a,0 4px 16px -6px #0000000a}.wb-storage-confirm-popover:after{background:color-mix(in srgb, var(--wb-bg-primary) 80%, transparent);content:"";width:9px;height:9px;position:absolute;top:50%;right:-4px;transform:translateY(-50%)rotate(45deg)}.wb-storage-confirm-popover__content{flex-direction:column;gap:24px;display:flex}.wb-storage-confirm-popover__main{align-items:flex-start;gap:var(--wb-spacing-2);display:flex}.wb-storage-confirm-popover__icon{color:var(--wb-status-info,var(--wb-brand-primary));flex:none;margin-top:1px}.wb-storage-confirm-popover__copy{gap:var(--wb-spacing-2);flex-direction:column;flex:1;min-width:0;display:flex}.wb-storage-confirm-popover__title{color:var(--wb-color-text-primary);font-size:16px;font-weight:600;line-height:24px}.wb-storage-confirm-popover__desc{color:var(--wb-color-text-secondary);font-size:var(--wb-font-body-size);line-height:var(--wb-font-body-line-height)}.wb-storage-confirm-popover__actions{justify-content:flex-end;gap:var(--wb-spacing-2);display:flex}.wb-storage-confirm-popover__actions .wb-button{min-width:68px}.wb-storage__error{padding:var(--wb-spacing-2) var(--wb-spacing-3);background:var(--wb-bg-secondary);border:1px solid var(--wb-status-error,var(--wb-border-default));border-radius:var(--wb-radius-sm);color:var(--wb-status-error,var(--wb-color-text-primary));font-size:var(--wb-font-caption-size);line-height:var(--wb-font-caption-line-height);word-break:break-all} +.general-settings-panel{--wb-brand-primary:var(--wb-palette-brand-8);--wb-color-text-primary:var(--wb-palette-black-100);--wb-color-text-secondary:var(--wb-palette-black-70);--wb-spacing-5:16px;--wb-spacing-3:8px;--wb-spacing-2:4px;--wb-color-text-tertiary:var(--wb-palette-black-50);--wb-font-body-size:var(--wb-font-size-5);--wb-font-body-line-height:var(--wb-font-line-height-5-2);--wb-spacing-4:12px;--wb-radius-xl:12px;--wb-bg-primary:var(--wb-palette-white-100);--wb-spacing-6:20px;--wb-motion-duration-fast:.15s;--wb-motion-easing-standard:cubic-bezier(.4, 0, .2, 1);--wb-spacing-1:2px;--wb-select-size-control-md:var(--wb-size-control-md);--wb-spacing-2-5:5px;--wb-select-spacing-1:var(--wb-spacing-1);--wb-input-color-text-disabled:var(--wb-color-text-disabled);--wb-border-default:color-mix(in srgb, var(--wb-palette-black-100) 8%, transparent);--wb-color-border-secondary:var(--wb-palette-gray-4);--wb-radius-lg:8px;--wb-bg-hover:color-mix(in srgb, var(--wb-palette-black-100) 5%, transparent);--wb-color-border-primary:var(--wb-palette-gray-5);--wb-color-text-success-default:var(--wb-palette-green-8);--wb-bg-secondary:var(--wb-color-bg-secondary-hover-active);--wb-bg-active:color-mix(in srgb, var(--wb-palette-black-100) 8%, transparent);--wb-color-text-disabled:var(--wb-palette-black-30);--wb-border-card:#00000014;--wb-shadow-thumb:0 1px 2px #0000002e;--wb-font-caption-size:var(--wb-font-size-3);--wb-font-caption-line-height:var(--wb-font-line-height-4-2);--wb-border-hover:#00000029;--wb-color-text-brand-active:var(--wb-palette-brand-9);--wb-border-subtle:var(--wb-palette-gray-3);--wb-radius-xs:2px;--wb-button-grey-bg:var(--wb-palette-gray-4);--wb-motion-duration-base:.2s;--wb-color-text-brand-default:var(--wb-palette-brand-8);--wb-bg-tertiary:var(--wb-color-bg-primary-hover);--wb-status-info:var(--wb-palette-brand-8);--wb-status-error:var(--wb-palette-red-8);--wb-radius-sm:4px;--wb-button-transition-duration:var(--wb-motion-duration-instant);--wb-button-transition-easing:var(--wb-motion-easing-standard);--wb-button-border-radius-medium:var(--wb-radius-lg);--wb-button-height-medium:var(--wb-size-control-md);--wb-button-padding-medium:var(--wb-spacing-4);--wb-button-font-size-medium:var(--wb-font-size-5);--wb-button-gap-medium:var(--wb-spacing-2);--wb-button-min-width-medium:var(--wb-size-17);--wb-button-line-height-medium:var(--wb-font-line-height-5-2);--wb-button-height-xsmall:var(--wb-size-4);--wb-button-font-size-small:var(--wb-font-size-3);--wb-button-border-radius-small:var(--wb-radius-md);--wb-button-height-small:var(--wb-size-control-sm);--wb-button-padding-small:var(--wb-spacing-3);--wb-button-gap-small:var(--wb-spacing-1);--wb-button-min-width-small:var(--wb-size-10);--wb-button-line-height-small:var(--wb-font-line-height-5);--wb-button-icon-size-small:var(--wb-icon-size-sm);--wb-button-height-large:var(--wb-size-control-xl);--wb-button-padding-large:var(--wb-spacing-8);--wb-button-font-size-large:var(--wb-font-size-5);--wb-button-gap-large:var(--wb-spacing-3);--wb-button-border-radius-large:var(--wb-radius-xl);--wb-button-line-height-large:var(--wb-font-line-height-5-2);--wb-button-primary-bg:var(--wb-palette-black-90);--wb-button-primary-fg:var(--wb-palette-white-100);--wb-button-primary-bg-hover:var(--wb-palette-black-75);--wb-button-primary-bg-active:var(--wb-palette-black-75);--wb-button-primary-bg-disabled:var(--wb-palette-black-20);--wb-button-primary-fg-disabled:var(--wb-palette-white-100);--wb-button-secondary-bg:var(--wb-palette-white-100);--wb-button-secondary-fg:var(--wb-palette-black-90);--wb-button-secondary-border:var(--wb-color-border-solid);--wb-button-secondary-bg-hover:var(--wb-palette-gray-2);--wb-button-secondary-bg-active:var(--wb-palette-gray-2);--wb-button-secondary-fg-disabled:var(--wb-palette-black-30);--wb-button-secondary-border-disabled:var(--wb-color-border-secondary);--wb-button-grey-fg:var(--wb-palette-black-90);--wb-button-grey-bg-hover:var(--wb-palette-gray-5);--wb-button-grey-bg-active:var(--wb-palette-gray-5);--wb-button-grey-bg-disabled:var(--wb-palette-gray-4);--wb-button-grey-fg-disabled:var(--wb-palette-black-30);--wb-button-ghost-bg:transparent;--wb-button-ghost-fg:var(--wb-palette-black-90);--wb-button-ghost-bg-hover:#0000000f;--wb-button-ghost-fg-hover:var(--wb-palette-black-90);--wb-button-ghost-bg-active:#0000000f;--wb-button-ghost-fg-disabled:var(--wb-palette-black-30);--wb-button-link-bg:transparent;--wb-button-link-fg:#1470b4;--wb-button-link-bg-hover:transparent;--wb-button-link-fg-hover:#1470b4;--wb-button-link-bg-active:transparent;--wb-button-link-fg-disabled:#94b7d1;--wb-button-border-radius-pill:var(--wb-radius-full);--wb-button-danger-bg:var(--wb-palette-red-7);--wb-button-danger-fg-on-solid:var(--wb-palette-white-100);--wb-button-danger-bg-hover:var(--wb-palette-red-6);--wb-button-danger-bg-active:var(--wb-palette-red-6);--wb-button-danger-bg-disabled:var(--wb-palette-red-3);--wb-button-danger-fg-on-solid-disabled:var(--wb-palette-white-100);--wb-button-danger-secondary-fg:var(--wb-palette-red-9);--wb-button-danger-secondary-border:var(--wb-palette-red-2);--wb-button-danger-secondary-bg:var(--wb-palette-white-100);--wb-button-danger-secondary-bg-hover:var(--wb-palette-red-1);--wb-button-danger-secondary-bg-active:var(--wb-palette-red-1);--wb-button-danger-secondary-fg-disabled:var(--wb-palette-red-3);--wb-button-danger-secondary-border-disabled:var(--wb-palette-red-2);--wb-button-danger-grey-fg:var(--wb-palette-red-8);--wb-button-danger-fg-disabled:var(--wb-palette-red-3);--wb-button-danger-ghost-fg:var(--wb-palette-red-8);--wb-button-danger-bg-soft-hover:color-mix(in srgb, var(--wb-color-text-error-default) 8%, transparent);--wb-button-danger-bg-soft-active:color-mix(in srgb, var(--wb-color-text-error-default) 16%, transparent);--wb-button-success-bg:#0cbf5b;--wb-button-success-fg-on-solid:var(--wb-palette-white-100);--wb-button-success-bg-hover:#09a84f;--wb-button-success-bg-disabled:var(--wb-color-text-success-disabled);--wb-button-success-fg-on-solid-disabled:var(--wb-palette-white-100);--wb-button-success-fg:var(--wb-color-text-success-default);--wb-button-success-fg-disabled:var(--wb-color-text-success-disabled);--wb-button-loading-secondary-fg:var(--wb-palette-black-100);--wb-button-loading-grey-fg:var(--wb-palette-black-100);--wb-button-loading-ghost-fg:var(--wb-palette-black-100);--wb-button-loading-danger-fg:var(--wb-color-text-error-disabled);--wb-button-loading-danger-fg-on-solid:var(--wb-palette-white-100);--wb-button-loading-link-fg:#1470b4;--wb-button-loading-primary-fg:var(--wb-palette-white-100);--wb-button-icon-size-medium:var(--wb-icon-size-md);--wb-input-line-height:var(--wb-font-body-line-height);--wb-input-color-text:var(--wb-color-text-primary);--wb-input-font-size:var(--wb-font-size-5);--wb-input-color-placeholder:var(--wb-color-text-disabled);--wb-input-padding-x:var(--wb-spacing-3);--wb-input-color-bg:var(--wb-bg-primary);--wb-input-border-color:var(--wb-color-border-primary);--wb-input-border-radius:var(--wb-radius-lg);--wb-input-motion-duration:var(--wb-motion-duration-instant);--wb-input-motion-easing:var(--wb-motion-easing-standard);--wb-input-border-color-hover:var(--wb-palette-black-30);--wb-input-border-color-focus:var(--wb-palette-black-30);--wb-input-disabled-bg:var(--wb-color-bg-secondary-hover-active);--wb-input-disabled-border-color:var(--wb-color-border-primary);--wb-input-font-size-small:var(--wb-font-size-3);--wb-input-height-small:var(--wb-size-control-ms);--wb-input-border-radius-small:var(--wb-radius-md);--wb-input-height-medium:var(--wb-size-control-md);--wb-input-font-size-large:var(--wb-font-size-5);--wb-input-height-large:var(--wb-size-control-xl);--wb-input-padding-x-large:var(--wb-spacing-4);--wb-input-border-radius-large:var(--wb-radius-xl);--wb-input-status-error-color:var(--wb-color-text-error-default);--wb-input-status-error-bg:var(--wb-color-bg-secondary-default);--wb-input-gap:var(--wb-spacing-2);--wb-input-has-button-padding-right:var(--wb-spacing-2);--wb-input-has-button-padding-right-large:var(--wb-spacing-3);--wb-input-icon-size-small:var(--wb-icon-size-ms);--wb-input-icon-button-size-small:var(--wb-size-5);--wb-input-color-icon:var(--wb-color-text-tertiary);--wb-input-icon-size:var(--wb-icon-size-md);--wb-input-color-icon-disabled:var(--wb-color-text-disabled);--wb-input-icon-button-size:var(--wb-size-6);--wb-input-icon-button-border-radius:var(--wb-radius-md);--wb-input-color-icon-hover:var(--wb-color-text-secondary);--wb-input-filled-color-bg:var(--wb-bg-inset-strong);--wb-input-filled-color-bg-hover:var(--wb-color-bg-primary-hover);--wb-input-count-font-size:var(--wb-font-caption-size);--wb-input-count-color:var(--wb-color-text-tertiary);--wb-input-addon-padding-x:var(--wb-spacing-3);--wb-input-addon-font-size:var(--wb-font-body-size);--wb-input-addon-line-height:var(--wb-font-body-line-height);--wb-input-addon-color-text:var(--wb-color-text-secondary);--wb-input-addon-color-bg:var(--wb-bg-secondary);--wb-input-addon-border-color:var(--wb-color-border-primary);--wb-input-number-controls-width:var(--wb-size-7);--wb-input-number-step-color-bg:var(--wb-color-bg-secondary-hover-active);--wb-input-number-step-color:var(--wb-color-text-tertiary);--wb-input-number-step-color-hover:var(--wb-color-text-secondary);--wb-input-number-step-color-bg-hover:var(--wb-color-bg-primary-hover);--wb-switch-gap:var(--wb-spacing-3);--wb-switch-color-text:var(--wb-color-text-primary);--wb-switch-font-size:var(--wb-font-body-size);--wb-switch-line-height:var(--wb-font-body-line-height);--wb-switch-track-width:26px;--wb-switch-track-height:var(--wb-size-4);--wb-switch-track-padding:var(--wb-spacing-1);--wb-switch-radius:var(--wb-radius-full);--wb-switch-track-bg:var(--wb-palette-gray-5);--wb-switch-duration:var(--wb-motion-duration-fast);--wb-switch-easing:var(--wb-motion-easing-standard);--wb-switch-thumb-size:var(--wb-size-3);--wb-switch-thumb-bg:var(--wb-control-selected-fg);--wb-switch-thumb-shadow:var(--wb-shadow-sm);--wb-switch-track-bg-checked:var(--wb-brand-primary);--wb-switch-track-border-color-checked:var(--wb-brand-primary);--wb-switch-thumb-translate:8px;--wb-switch-outline-focus-width:2px;--wb-switch-color-outline-focus:var(--wb-border-focus);--wb-switch-outline-focus-offset:2px;--wb-switch-track-bg-checked-disabled:var(--wb-color-text-brand-disabled);--wb-switch-track-bg-disabled:var(--wb-color-bg-primary-default);--wb-switch-thumb-bg-disabled-unchecked:var(--wb-control-selected-fg);--wb-switch-spinner-border-width:1.5px;--wb-switch-track-color-spinner:var(--wb-control-selected-fg);--wb-switch-spinner-duration:.8s;--wb-switch-track-bg-danger:var(--wb-palette-red-9);--wb-switch-track-border-color-danger:var(--wb-palette-red-9);--wb-switch-track-bg-danger-disabled:var(--wb-color-text-error-disabled);--wb-switch-font-size-small:var(--wb-font-caption-size);--wb-switch-line-height-small:var(--wb-font-caption-line-height);--wb-switch-track-width-small:var(--wb-size-6);--wb-switch-track-height-small:14px;--wb-switch-thumb-size-small:10px;--wb-switch-thumb-translate-small:8px;--wb-switch-track-width-large:34px;--wb-switch-track-height-large:20px;--wb-switch-thumb-size-large:var(--wb-size-4);--wb-switch-thumb-translate-large:12px;--wb-palette-brand-8:#00c29a;--wb-palette-black-100:#000;--wb-palette-black-70:#000000b3;--wb-palette-black-50:#00000080;--wb-font-size-5:14px;--wb-font-line-height-5-2:22px;--wb-palette-white-100:#fff;--wb-size-control-md:var(--wb-size-8);--wb-palette-gray-4:#ebebeb;--wb-palette-gray-5:#e6e6e6;--wb-palette-green-8:#0cbf5b;--wb-color-bg-secondary-hover-active:var(--wb-palette-gray-2);--wb-palette-black-30:#0000004d;--wb-font-size-3:12px;--wb-font-line-height-4-2:18px;--wb-palette-brand-9:#009273;--wb-palette-gray-3:#f2f2f2;--wb-color-bg-primary-hover:var(--wb-palette-gray-4);--wb-palette-red-8:#f64041;--wb-motion-duration-instant:.1s;--wb-size-17:68px;--wb-size-4:16px;--wb-radius-md:6px;--wb-size-control-sm:var(--wb-size-6);--wb-size-10:40px;--wb-font-line-height-5:20px;--wb-icon-size-sm:var(--wb-size-3);--wb-size-control-xl:var(--wb-size-12);--wb-spacing-8:32px;--wb-palette-black-90:#000000e6;--wb-palette-black-75:#000000bf;--wb-palette-black-20:#0003;--wb-color-border-solid:#dedede;--wb-palette-gray-2:#f7f7f7;--wb-radius-full:9999px;--wb-palette-red-7:#e85c5c;--wb-palette-red-6:#ec7474;--wb-palette-red-3:#f5b9b9;--wb-palette-red-9:#b42c3f;--wb-palette-red-2:#f9d1d1;--wb-palette-red-1:#fce8e8;--wb-color-text-error-default:var(--wb-palette-red-8);--wb-color-text-success-disabled:var(--wb-palette-green-3);--wb-color-text-error-disabled:var(--wb-palette-red-3);--wb-icon-size-md:var(--wb-size-4);--wb-size-control-ms:var(--wb-size-7);--wb-color-bg-secondary-default:var(--wb-palette-white-100);--wb-icon-size-ms:var(--wb-size-3-2);--wb-size-5:20px;--wb-size-6:24px;--wb-bg-inset-strong:var(--wb-palette-gray-3);--wb-size-7:28px;--wb-size-3:12px;--wb-control-selected-fg:var(--wb-palette-white-100);--wb-shadow-sm:0 1px 2px color-mix(in srgb, var(--wb-palette-black-100) 8%, transparent);--wb-border-focus:var(--wb-color-border-focus);--wb-color-text-brand-disabled:var(--wb-palette-brand-3);--wb-color-bg-primary-default:var(--wb-palette-gray-3);--wb-size-8:32px;--wb-size-12:48px;--wb-palette-green-3:#a3e7c2;--wb-size-3-2:14px;--wb-color-border-focus:var(--wb-palette-black-75);--wb-palette-brand-3:#9fe8d9} +:root[data-theme=dark] .general-settings-panel{--wb-brand-primary:var(--wb-palette-brand-7);--wb-color-text-primary:#fff;--wb-color-text-secondary:#ffffffa6;--wb-color-text-tertiary:#ffffff80;--wb-bg-primary:var(--wb-palette-gray-3);--wb-input-color-text-disabled:var(--wb-color-text-disabled);--wb-border-default:color-mix(in srgb, var(--wb-palette-white-100) 14%, transparent);--wb-color-border-secondary:var(--wb-palette-gray-5);--wb-bg-hover:color-mix(in srgb, var(--wb-palette-white-100) 6%, transparent);--wb-color-border-primary:#262626;--wb-color-text-success-default:var(--wb-palette-green-6);--wb-bg-secondary:var(--wb-palette-gray-2);--wb-bg-active:var(--wb-palette-white-10);--wb-color-text-disabled:#ffffff59;--wb-border-card:#ffffff1a;--wb-shadow-thumb:0 1px 4px #0009;--wb-border-hover:var(--wb-palette-white-20);--wb-color-text-brand-active:var(--wb-palette-brand-5);--wb-border-subtle:color-mix(in srgb, var(--wb-palette-white-100) 6%, transparent);--wb-button-grey-bg:#383838;--wb-color-text-brand-default:var(--wb-palette-brand-7);--wb-bg-tertiary:var(--wb-palette-gray-4);--wb-status-info:var(--wb-color-text-brand-default);--wb-status-error:var(--wb-color-text-error-default);--wb-button-primary-bg:var(--wb-palette-white-90);--wb-button-primary-fg:var(--wb-palette-black-90);--wb-button-primary-bg-hover:var(--wb-palette-white-75);--wb-button-primary-bg-active:var(--wb-palette-white-75);--wb-button-primary-bg-disabled:var(--wb-palette-white-20);--wb-button-primary-fg-disabled:var(--wb-palette-black-90);--wb-button-secondary-bg:#262626;--wb-button-secondary-fg:var(--wb-palette-white-90);--wb-button-secondary-border:var(--wb-color-border-solid);--wb-button-secondary-bg-hover:#2e2e2e;--wb-button-secondary-bg-active:#2e2e2e;--wb-button-secondary-fg-disabled:var(--wb-palette-white-30);--wb-button-secondary-border-disabled:var(--wb-color-border-secondary);--wb-button-grey-fg:var(--wb-palette-white-90);--wb-button-grey-bg-hover:#404040;--wb-button-grey-bg-active:#404040;--wb-button-grey-bg-disabled:#404040;--wb-button-grey-fg-disabled:var(--wb-palette-white-30);--wb-button-ghost-bg:transparent;--wb-button-ghost-fg:var(--wb-palette-white-90);--wb-button-ghost-bg-hover:var(--wb-palette-white-10);--wb-button-ghost-fg-hover:var(--wb-palette-white-90);--wb-button-ghost-bg-active:var(--wb-palette-white-10);--wb-button-ghost-fg-disabled:var(--wb-palette-white-30);--wb-button-link-bg:transparent;--wb-button-link-fg:#4294d2;--wb-button-link-bg-hover:transparent;--wb-button-link-fg-hover:#4294d2;--wb-button-link-bg-active:transparent;--wb-button-link-fg-disabled:#94b7d1;--wb-button-danger-bg:#e85c5c;--wb-button-danger-fg-on-solid:var(--wb-palette-white-100);--wb-button-danger-bg-hover:#ec7474;--wb-button-danger-bg-active:#ec7474;--wb-button-danger-bg-disabled:#803838;--wb-button-danger-fg-on-solid-disabled:var(--wb-palette-white-70);--wb-button-danger-secondary-fg:var(--wb-palette-red-9);--wb-button-danger-secondary-border:var(--wb-palette-red-5);--wb-button-danger-secondary-bg:#262626;--wb-button-danger-secondary-bg-hover:color-mix(in srgb, var(--wb-palette-red-3) 40%, var(--wb-button-danger-secondary-bg) 60%);--wb-button-danger-secondary-bg-active:color-mix(in srgb, var(--wb-palette-red-3) 40%, var(--wb-button-danger-secondary-bg) 60%);--wb-button-danger-secondary-fg-disabled:var(--wb-color-text-error-disabled);--wb-button-danger-secondary-border-disabled:var(--wb-color-text-error-disabled);--wb-button-danger-grey-fg:var(--wb-palette-red-9);--wb-button-danger-fg-disabled:var(--wb-color-text-error-disabled);--wb-button-danger-ghost-fg:var(--wb-palette-red-9);--wb-button-danger-bg-soft-hover:color-mix(in srgb, var(--wb-color-text-error-default) 12%, transparent);--wb-button-danger-bg-soft-active:color-mix(in srgb, var(--wb-color-text-error-default) 20%, transparent);--wb-button-success-bg:#0cbf5b;--wb-button-success-fg-on-solid:var(--wb-palette-white-90);--wb-button-success-bg-hover:#09a84f;--wb-button-success-bg-disabled:var(--wb-color-text-success-disabled);--wb-button-success-fg-on-solid-disabled:var(--wb-palette-white-70);--wb-button-success-fg:var(--wb-color-text-success-default);--wb-button-success-fg-disabled:var(--wb-color-text-success-disabled);--wb-button-loading-secondary-fg:var(--wb-palette-white-30);--wb-button-loading-grey-fg:var(--wb-palette-white-30);--wb-button-loading-ghost-fg:var(--wb-palette-white-30);--wb-button-loading-danger-fg:var(--wb-color-text-error-disabled);--wb-button-loading-danger-fg-on-solid:var(--wb-palette-white-90);--wb-button-loading-link-fg:#94b7d1;--wb-button-loading-primary-fg:var(--wb-palette-white-90);--wb-input-color-text:var(--wb-color-text-primary);--wb-input-color-placeholder:var(--wb-color-text-disabled);--wb-input-color-bg:var(--wb-bg-primary);--wb-input-border-color:var(--wb-color-border-solid);--wb-input-border-color-hover:var(--wb-palette-white-30);--wb-input-border-color-focus:var(--wb-palette-white-30);--wb-input-disabled-bg:var(--wb-color-bg-secondary-hover-active);--wb-input-disabled-border-color:var(--wb-color-border-primary);--wb-input-status-error-color:var(--wb-color-text-error-default);--wb-input-status-error-bg:transparent;--wb-input-color-icon:var(--wb-color-text-tertiary);--wb-input-color-icon-disabled:var(--wb-color-text-disabled);--wb-input-color-icon-hover:var(--wb-color-text-secondary);--wb-input-filled-color-bg:var(--wb-bg-inset-strong);--wb-input-filled-color-bg-hover:var(--wb-color-bg-primary-hover);--wb-input-count-color:var(--wb-color-text-tertiary);--wb-input-addon-color-text:var(--wb-color-text-secondary);--wb-input-addon-color-bg:var(--wb-bg-secondary);--wb-input-addon-border-color:var(--wb-color-border-solid);--wb-input-number-step-color-bg:var(--wb-color-bg-secondary-hover-active);--wb-input-number-step-color:var(--wb-color-text-tertiary);--wb-input-number-step-color-hover:var(--wb-color-text-secondary);--wb-input-number-step-color-bg-hover:var(--wb-color-bg-primary-active);--wb-switch-color-text:var(--wb-color-text-primary);--wb-switch-track-bg:var(--wb-bg-elevated);--wb-switch-thumb-bg:var(--wb-control-selected-fg);--wb-switch-thumb-shadow:var(--wb-shadow-sm);--wb-switch-track-bg-checked:var(--wb-brand-primary);--wb-switch-track-border-color-checked:var(--wb-brand-primary);--wb-switch-track-bg-checked-disabled:var(--wb-palette-brand-4);--wb-switch-track-bg-disabled:var(--wb-palette-gray-5);--wb-switch-thumb-bg-disabled-unchecked:color-mix(in srgb, var(--wb-palette-white-100) 72%, var(--wb-palette-black-100));--wb-switch-track-color-spinner:var(--wb-switch-thumb-bg-disabled-unchecked);--wb-switch-track-bg-danger:var(--wb-palette-red-9);--wb-switch-track-border-color-danger:var(--wb-palette-red-9);--wb-switch-track-bg-danger-disabled:var(--wb-palette-red-6);--wb-palette-brand-8:#4cf0ce;--wb-palette-black-100:#000;--wb-palette-black-70:#000000b3;--wb-palette-black-50:#00000080;--wb-palette-white-100:#fff;--wb-palette-gray-4:#242424;--wb-palette-gray-5:#3a3a3a;--wb-palette-green-8:#43f08f;--wb-color-bg-secondary-hover-active:var(--wb-palette-gray-2);--wb-palette-black-30:#0000004d;--wb-palette-brand-9:#7cf0d8;--wb-palette-gray-3:#1f1f1f;--wb-color-bg-primary-hover:var(--wb-palette-gray-4);--wb-palette-red-8:#af191a;--wb-palette-black-90:#000000e6;--wb-palette-black-75:#000000bf;--wb-palette-black-20:#0003;--wb-color-border-solid:#3d3d3d;--wb-palette-gray-2:#292929;--wb-palette-red-7:#a31717;--wb-palette-red-6:#8a1414;--wb-palette-red-3:#3a1616;--wb-palette-red-9:#dc4258;--wb-palette-red-2:#260e0e;--wb-palette-red-1:#250e0e;--wb-color-text-error-default:var(--wb-palette-red-7);--wb-color-text-success-disabled:#4ed88a59;--wb-color-text-error-disabled:#f4737459;--wb-color-bg-secondary-default:var(--wb-palette-gray-2);--wb-bg-inset-strong:#262626;--wb-control-selected-fg:var(--wb-palette-white-100);--wb-border-focus:var(--wb-color-border-focus);--wb-color-text-brand-disabled:#3dd9b659;--wb-color-bg-primary-default:var(--wb-palette-gray-3);--wb-palette-green-3:#175d37;--wb-color-border-focus:var(--wb-palette-white-70);--wb-palette-brand-3:#185f51} diff --git a/src/renderer/src/components/source-general-settings.css b/src/renderer/src/components/source-general-settings.css new file mode 100644 index 0000000..5816a42 --- /dev/null +++ b/src/renderer/src/components/source-general-settings.css @@ -0,0 +1,20 @@ +@import './source-general-settings-recovered.css'; + +/* + * 原版把 `.wb-select{width:120px;min-width:120px}` 与通用面板放在同一个懒加载 chunk + * (REA ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94 的 + * contexts 样式表)。YCode 的 CSS 是全局打包的,直接放出这条规则会污染其它面板的 select, + * 因此按原版实测宽度(语言 120 / 链接打开方式 120 / 允许锁屏运行 120 / 网络代理 120 / + * 通知提示音 120,见 REA ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e) + * 收敛到本面板作用域。 + */ +.general-settings-panel .wb-select { width: 120px; min-width: 120px } + +/* + * 未接入原版行为的行:保持原版外观与几何,只停用交互并给出提示 + * (与设置导航未接入项的 aria-disabled + 「暂未接入」约定一致)。 + */ +.general-settings-panel [data-settings-unavailable=true] { cursor: default } +.general-settings-panel [data-settings-unavailable=true] .wb-select, +.general-settings-panel [data-settings-unavailable=true] .wb-switch, +.general-settings-panel [data-settings-unavailable=true] .font-size-slider { cursor: default } diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index 88bbb1c..e852a77 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -11,7 +11,9 @@ import type { WorkspaceInfo, WindowState, AppInfo, - GitBranchInfo + GitBranchInfo, + GeneralSettingsPatch, + GeneralSettingsView } from './types' import type { DownloadPayload, DownloadResult, ResourceEntry, ResourceRange, WorkspaceResource } from './resources' import type { ChatAttachment, InteractionResponse, MessageQuote, PermissionMode } from './conversation' @@ -100,6 +102,17 @@ export interface YCodeApi { get(): Ipc update(patch: ConfigPatchLike): Ipc } + /** 设置 → 通用:真实可接入的能力。 */ + general: { + get(options?: { refreshUsage?: boolean }): Ipc + update(patch: GeneralSettingsPatch): Ipc + /** 打开数据目录 / 默认工作空间目录 */ + openFolder(kind: 'data' | 'workspace'): Ipc + /** 修改默认工作空间存储路径;传入 path 时不弹系统对话框(验收用)。 */ + changeWorkspaceRoot(path?: string): Ipc + /** 打开系统通知设置页。 */ + openSystemNotificationSettings(): Ipc + } workspace: { get(): Ipc choose(): Ipc diff --git a/src/shared/types.ts b/src/shared/types.ts index 89bf343..f2c7cec 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -117,6 +117,31 @@ export interface GatewayConfigView { keyStorage: 'encrypted' | 'plaintext' | 'none' } +/** 设置 → 通用:真实状态视图(未接入项不下发数据)。 */ +export interface GeneralSettingsView { + /** 系统登录项的真实状态 */ + autoLaunch: boolean + /** 运行任务时阻止系统休眠(偏好) */ + preventSleep: boolean + /** 当前是否有 powerSaveBlocker 生效 */ + preventSleepActive: boolean + /** 数据目录(存放对话记录、运行缓存与临时文件) */ + dataDir: string + /** 数据目录占用字节 */ + dataBytes: number + /** 扫描是否因预算被截断 */ + dataUsagePartial: boolean + /** 数据目录所在磁盘容量 */ + disk: { totalBytes: number; freeBytes: number } + /** 默认工作空间存储路径 */ + workspaceRoot: string +} + +export interface GeneralSettingsPatch { + autoLaunch?: boolean + preventSleep?: boolean +} + /** 主进程 → 渲染进程的流式事件 */ export type ChatEvent = { sessionId?: string; turnDurationMs?: number } & ( | { runId: string; type: 'started'; messageId: string }