feat(settings): replicate the WorkBuddy general settings panel (TODO 10)

This commit is contained in:
xiaohan.yuan committed 2026-10-17 08:53:34 +08:00
1 parent 401926d7eb
commit 7b68d9eb25
21 files changed
+1150 -11

No files matched your search

+26
View File
@@ -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<void> {
await persist({ ...stored, workspaceRoot: root })
}
/** 通用设置:运行任务时阻止系统休眠,默认开启。 */
export async function getPreventSleep(): Promise<boolean> {
const stored = await load()
return stored.preventSleep !== false
}
export async function setPreventSleep(enabled: boolean): Promise<void> {
const stored = await load()
await persist({ ...stored, preventSleep: enabled })
}
/** 通用设置:开机自动启动,默认关闭(由系统登录项反映实际状态)。 */
export async function getAutoLaunch(): Promise<boolean> {
const stored = await load()
return stored.autoLaunch === true
}
export async function setAutoLaunch(enabled: boolean): Promise<void> {
const stored = await load()
await persist({ ...stored, autoLaunch: enabled })
}
/** 供渲染进程使用:脱敏视图 */
export async function getConfigView(): Promise<GatewayConfigView> {
const stored = await load()
+118
View File
@@ -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 }
}
}
+4
View File
@@ -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') => {
+60
View File
@@ -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)
}
}
+7
View File
@@ -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'),
@@ -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 | false | undefined>): 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 (
<section className={classNames('general-settings-panel__section', 'general-settings-panel__section--plain',
stacked ? 'general-settings-panel__section--stacked' : 'general-settings-panel__section--inline', className)}>
<div className="general-settings-panel__section-main">
<div className="general-settings-panel__section-copy">
<h3 className="general-settings-panel__section-title">{title}</h3>
{desc ? <p className="general-settings-panel__section-desc">{desc}</p> : null}
</div>
<div className="general-settings-panel__section-action">{action}</div>
</div>
{body ? <div className="general-settings-panel__section-body">{body}</div> : null}
</section>
)
}
function Switch({ checked, ariaLabel, disabled, onChange }: {
checked: boolean
ariaLabel: string
disabled?: boolean
onChange(checked: boolean): void
}): React.JSX.Element {
return (
<div className="general-settings-panel__switch-wrap">
<label className="wb-switch wb-switch--large">
<span className="wb-switch__control">
<input className="wb-switch__input" type="checkbox" role="switch" aria-label={ariaLabel} checked={checked}
disabled={disabled} onChange={(event) => onChange(event.target.checked)} />
<span className="wb-switch__track"><span className="wb-switch__thumb" /></span>
</span>
</label>
</div>
)
}
/** 未接入的开关:保持原版开/关外观与尺寸,但不可交互。 */
function UnavailableSwitch({ checked, ariaLabel }: { checked: boolean; ariaLabel: string }): React.JSX.Element {
return (
<div className="general-settings-panel__switch-wrap" data-settings-unavailable="true" aria-disabled="true" title={UNAVAILABLE_HINT}>
<label className="wb-switch wb-switch--large" aria-disabled="true">
<span className="wb-switch__control">
<input className="wb-switch__input" type="checkbox" role="switch" aria-label={ariaLabel} checked={checked}
readOnly tabIndex={-1} aria-disabled="true" />
<span className="wb-switch__track"><span className="wb-switch__thumb" /></span>
</span>
</label>
</div>
)
}
/** 未接入的下拉:原版 120×32 闭合态(value + caret),不可展开。 */
function UnavailableSelect({ value, className, ariaLabel }: { value: string; className?: string; ariaLabel: string }): React.JSX.Element {
return (
<span className="source-select-host" data-settings-unavailable="true" aria-disabled="true" title={UNAVAILABLE_HINT}>
<button type="button" className={classNames('wb-select', 'wb-select--medium', className)} aria-label={ariaLabel}
aria-disabled="true" title={UNAVAILABLE_HINT} tabIndex={-1}>
<span className="wb-select__value">{value}</span>
<IconSelectCaret className="wb-icon wb-select__caret" />
</button>
</span>
)
}
/**
* 未接入的字体大小滑块:原版 .font-size-slider 结构(224 宽、6 个刻度、2 个激活、thumb 位于
* left 44.8px = 默认档,标签 小/默认/大)。
*/
function UnavailableFontSizeSlider(): React.JSX.Element {
const ticks = [0, 1, 2, 3, 4, 5]
return (
<div className="general-settings-panel__slider-wrap" data-settings-unavailable="true" aria-disabled="true" title={UNAVAILABLE_HINT}>
<div className="font-size-slider" aria-disabled="true">
<div className="font-size-slider__track">
<div className="font-size-slider__marks">
<div className="font-size-slider__marks-track" />
<div className="font-size-slider__marks-fill" style={{ width: 44.796875 }} />
{ticks.map((index) => (
<span key={index} className={classNames('font-size-slider__tick', index <= 1 && 'font-size-slider__tick--active')}
style={{ left: index * 44.8 }} />
))}
</div>
<div className="font-size-slider__thumb" style={{ left: 44.8 }} />
</div>
<div className="font-size-slider__labels">
<span className="font-size-slider__label">小</span>
<span className="font-size-slider__label">默认</span>
<span className="font-size-slider__label">大</span>
</div>
</div>
</div>
)
}
export default function GeneralSettingsPanel(): React.JSX.Element {
const [view, setView] = useState<GeneralSettingsView | null>(null)
const [error, setError] = useState('')
const [busy, setBusy] = useState(false)
const apply = useCallback(async (work: Promise<GeneralSettingsView>) => {
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 (
<div className="general-settings-panel">
{error ? <p role="alert">{error}</p> : null}
<div className="general-settings-panel__group">
<p className="general-settings-panel__group-label">常规</p>
<div className="general-settings-panel__group-card">
<Section title="语言" action={<UnavailableSelect value="简体中文" ariaLabel="语言" />} />
<Section title="链接打开方式" desc="设置 YCode 对话中链接的默认打开策略。"
action={<UnavailableSelect value="智能打开" ariaLabel="链接打开方式" className="general-settings-panel__link-open-select" />}
className="general-settings-panel__link-open-section" />
<Section title="字体大小" action={<UnavailableFontSizeSlider />} />
</div>
</div>
<div className="general-settings-panel__group">
<p className="general-settings-panel__group-label">权限</p>
<div className="general-settings-panel__group-card">
<Section title="允许锁屏运行" desc="选择锁屏后的运行方式,保障远程控制与后台 Agent 任务持续执行。"
action={<UnavailableSelect value="关闭" ariaLabel="允许锁屏运行" className="power-save-blocker-settings__select" />} />
<Section title="运行时防止系统休眠" desc="在 YCode 运行任务时,让电脑保持唤醒状态。"
action={<Switch checked={view?.preventSleep ?? true} ariaLabel="运行时防止系统休眠" disabled={busy || !view}
onChange={(checked) => update({ preventSleep: checked })} />} />
<Section title="开机自动启动" desc="开启后 YCode 会在你登录电脑后自动启动。"
action={<Switch checked={view?.autoLaunch ?? false} ariaLabel="开机自动启动" disabled={busy || !view}
onChange={(checked) => update({ autoLaunch: checked })} />} />
<Section title="网络代理" desc="网络异常时可切换连接方式,修改后立即生效" stacked
className="general-settings-panel__section--proxy"
action={<UnavailableSelect value="直接连接" ariaLabel="网络代理" />}
body={<div className="proxy-section" />} />
<Section title="自动安装可信技能" desc="安全检测通过后自动安装,高风险项始终要求手动确认。"
action={<UnavailableSwitch checked={false} ariaLabel="自动安装可信技能" />} />
<Section title="技能自动更新" desc="开启后将自动更新已安装的技能为最新版本,不会更新你在 YCode 中编辑过的技能"
action={<UnavailableSwitch checked ariaLabel="技能自动更新" />} />
<Section title="套件自动更新" desc="开启后将自动更新已安装的套件为最新版本。"
action={<UnavailableSwitch checked ariaLabel="套件自动更新" />} />
</div>
</div>
<section className="wb-storage">
<p className="wb-storage__group-label">存储</p>
<div className="wb-storage__card">
<div className="wb-storage__cache">
<div className="wb-storage__card-title-row">
<div className="wb-storage__card-title">系统缓存目录</div>
<button type="button" className="wb-button wb-button--ghost wb-button--medium wb-storage__open-folder"
onClick={() => openFolder('data')}>
<span className="wb-button__icon wb-button__icon--left"><IconTaskOpenFolder className="wb-icon" /></span>
<span className="wb-button__content">打开目录</span>
</button>
</div>
<div className="wb-storage__path-row">
<span className="wb-storage__path-text">{view?.dataDir ?? '—'} · 存放对话记录、运行缓存与临时文件。</span>
</div>
<div className="general-settings-panel__storage-overview">
<div className="general-settings-panel__storage-progress">
<span className="general-settings-panel__storage-progress-seg general-settings-panel__storage-progress-seg--workbuddy"
style={{ width: `${cachePercent}%` }} />
<span className="general-settings-panel__storage-progress-seg general-settings-panel__storage-progress-seg--other"
style={{ width: `${otherPercent}%` }} />
<span className="general-settings-panel__storage-progress-seg" style={{ width: `${freePercent}%` }} />
</div>
<div className="general-settings-panel__storage-legend">
<span className="general-settings-panel__storage-legend-item">
<span className="general-settings-panel__storage-legend-dot general-settings-panel__storage-legend-dot--workbuddy" />
<span className="general-settings-panel__storage-legend-label">系统缓存</span>
</span>
<span className="general-settings-panel__storage-legend-item">
<span className="general-settings-panel__storage-legend-dot general-settings-panel__storage-legend-dot--other" />
<span className="general-settings-panel__storage-legend-label">磁盘已用</span>
</span>
<span className="general-settings-panel__storage-legend-item">
<span className="general-settings-panel__storage-legend-dot general-settings-panel__storage-legend-dot--free" />
<span className="general-settings-panel__storage-legend-label">磁盘可用</span>
</span>
<span className="general-settings-panel__storage-usage-text">
已用 {formatBytes(dataBytes)} / 共 {formatBytes(diskTotal)}
</span>
</div>
</div>
</div>
<hr className="wb-storage__divider" />
<div className="general-settings-panel__workspace">
<div className="general-settings-panel__workspace-header">
<span className="general-settings-panel__workspace-path-title">YCode 默认工作空间存储路径</span>
</div>
<p className="general-settings-panel__workspace-path-desc">新建任务、工作空间时将自动存放在该路径下;修改后不影响已有数据。</p>
<div className="general-settings-panel__path-row">
<input className="wb-input wb-input--medium wb-input--filled general-settings-panel__path-input"
aria-label="默认工作空间存储路径" value={view?.workspaceRoot ?? ''} readOnly />
<button type="button" className="wb-button wb-button--secondary wb-button--medium" disabled={busy || !view}
onClick={changeWorkspaceRoot}>
<span className="wb-button__content">更改</span>
</button>
</div>
</div>
</div>
</section>
<div className="general-settings-panel__group">
<p className="general-settings-panel__group-label">通知</p>
<div className="general-settings-panel__group-card">
<Section title="桌面通知" desc="任务完成或有新消息时,将会通过系统弹窗提醒你。"
action={<button type="button" className="wb-button wb-button--secondary wb-button--medium"
onClick={openNotificationSettings}>
<span className="wb-button__content">前往系统设置</span>
<span className="wb-button__icon wb-button__icon--right"><IconNotificationAuthorize /></span>
</button>} />
<Section title="通知提示音" desc="收到桌面或应用内通知时播放的提示音。"
action={<UnavailableSelect value="无音效" ariaLabel="通知提示音" />} />
<Section title="定时任务产物通知" desc="定时任务的产物生成后,将会通过系统弹窗提醒你。"
action={<UnavailableSwitch checked ariaLabel="定时任务产物通知" />} />
</div>
</div>
<div className="general-settings-panel__group">
<p className="general-settings-panel__group-label">隐私</p>
<div className="general-settings-panel__group-card">
<Section title="体验优化计划"
desc={<>
允许使用你的对话数据帮助改进模型。数据会先加密脱敏,且无法关联到你个人。另有链接
{' '}<a className="general-settings-panel__inline-link" href="#none" aria-disabled="true" title={UNAVAILABLE_HINT}
onClick={(event) => event.preventDefault()}>了解更多</a>
</>}
action={<UnavailableSwitch checked ariaLabel="体验优化计划" />} />
</div>
</div>
</div>
)
}
@@ -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 = <section className={`settings-modal${nativeWindow ? ' settings-modal--window' : ''}`} role="dialog" aria-label="设置" aria-modal={!nativeWindow}>
<div className="settings-modal__nav"><SourceSettingsNavigation activeTab={section} supportedTabs={['models', 'appearance']} visibleTabs={VISIBLE_SETTINGS_TABS}
<div className="settings-modal__nav"><SourceSettingsNavigation activeTab={section} supportedTabs={['settings', 'models', 'appearance']} visibleTabs={VISIBLE_SETTINGS_TABS}
onTabChange={(id) => { setError(''); setSection(id === 'models' ? 'model' : id) }} onUnavailable={() => setError('该设置尚未接入')} /></div>
<div className="settings-modal__content"><div className="settings-modal__panel">
{error && <p role="alert">{error}</p>}
{section === 'settings' && <GeneralSettingsPanel />}
{section === 'model' && <ModelSettingsPanel models={settings.models} createRequest={createRequest} onCreateHandled={() => setCreateRequest(0)} onSave={settings.saveModel} onDelete={settings.deleteModel} onTest={(input) => window.ycode.models.test(input)} />}
{section === 'workspace' && <div className="settings-window-panel"><h2>工作空间</h2>{settings.workspaces.map((space) => <button className="btn" key={space.id} title={space.root} onClick={() => void window.ycode.workspace.open(space.id).catch((error) => setError(String(error.message ?? error)))}>{space.name}</button>)}<button className="btn" onClick={() => void settings.attachWorkspace().catch((error) => setError(String(error.message ?? error)))}>打开已有文件夹</button></div>}
{section === 'appearance' && <div className="settings-window-panel"><h2>外观</h2><div className="segmented">{(['light', 'dark'] as const).map((value) => <button key={value} className="segmented__item" data-active={theme === value} onClick={() => onThemeChange(value)}>{value === 'light' ? '浅色' : '深色'}</button>)}</div></div>}
@@ -0,0 +1,30 @@
/**
* 通用设置(设置 → 通用)用到的原版图标。
*
* path / viewBox / transform 取自 REA 归档
* ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e 的 svg_nodes,按原样复刻。
*/
import type { SVGProps } from 'react'
/** 桌面通知行「前往系统设置」按钮右侧图标。 */
export function IconNotificationAuthorize(props: SVGProps<SVGSVGElement>): React.JSX.Element {
return (
<svg {...props} viewBox="0 0 16 16" fill="none" width="16" height="16">
<path
transform="matrix(1 0 0 1 2.57586 2.39996)"
d="M9.8242 6.6001L9.8242 5.109Q9.8243 3.305 9.7462 2.7323Q9.7081 2.4537 9.6375 2.2354L0.8485 11.0243L0 10.1758L8.789 1.3868Q8.5706 1.3162 8.292 1.2782Q7.7193 1.2 5.9153 1.2001L4.4243 1.2001L4.4243 0L5.9152 0Q7.8008 -0 8.4543 0.0892Q9.5761 0.2423 10.179 0.8453Q10.782 1.4482 10.9351 2.57Q11.0243 3.2235 11.0243 5.1091L11.0243 6.6001L9.8242 6.6001Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
)
}
/** 下拉闭合态的 caret(与 source-select 的 SelectCaretGlyph 同源,取自同一份 REA svg_nodes)。 */
export function IconSelectCaret(props: SVGProps<SVGSVGElement>): React.JSX.Element {
return (
<svg {...props} viewBox="0 0 14 14" fill="currentColor" width="16" height="16">
<path transform="translate(2.967, 4.717)" d="M7.2416 0L4.0333 3.2083L0.825 0L0 0.825L4.0333 4.8582L8.0666 0.825L7.2416 0Z" />
</svg>
)
}
@@ -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}:wLine truncated
[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(Line truncated
.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);--wbLine truncated
: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);--wbLine truncated
@@ -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 }
+14 -1
View File
@@ -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<GatewayConfigView>
update(patch: ConfigPatchLike): Ipc<GatewayConfigView>
}
/** 设置 → 通用:真实可接入的能力。 */
general: {
get(options?: { refreshUsage?: boolean }): Ipc<GeneralSettingsView>
update(patch: GeneralSettingsPatch): Ipc<GeneralSettingsView>
/** 打开数据目录 / 默认工作空间目录 */
openFolder(kind: 'data' | 'workspace'): Ipc<void>
/** 修改默认工作空间存储路径;传入 path 时不弹系统对话框(验收用)。 */
changeWorkspaceRoot(path?: string): Ipc<GeneralSettingsView>
/** 打开系统通知设置页。 */
openSystemNotificationSettings(): Ipc<void>
}
workspace: {
get(): Ipc<WorkspaceInfo>
choose(): Ipc<WorkspaceInfo & { changed: boolean }>
+25
View File
@@ -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 }