feat(settings): replicate the WorkBuddy general settings panel (TODO 10)
This commit is contained in:
1 parent
401926d7eb
commit
7b68d9eb25
21 files changed
+1150
-11
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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') => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 }>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in new issue
Block a user