feat: bundle private Windows runtimes with settings switches
This commit is contained in:
1 parent
759df7060d
commit
bef87520c2
28 files changed
+902
-10
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { privateRuntimeEnvironment } from '../../private-runtimes'
|
||||
import { app } from 'electron'
|
||||
import { existsSync, mkdirSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
@@ -362,6 +363,7 @@ export class ClaudeKernel {
|
||||
}
|
||||
|
||||
async ensureStarted(): Promise<void> {
|
||||
const childEnvironment = await privateRuntimeEnvironment(modelEnvironment(this.options.model))
|
||||
await this.permissionUpdates
|
||||
await this.draining
|
||||
if (this.policyError) throw this.policyError
|
||||
@@ -384,7 +386,7 @@ export class ClaudeKernel {
|
||||
cwd: this.options.workspaceRoot,
|
||||
model: this.options.model.config.model,
|
||||
pathToClaudeCodeExecutable: executable,
|
||||
env: modelEnvironment(this.options.model),
|
||||
env: childEnvironment,
|
||||
abortController: processAbort,
|
||||
includePartialMessages: true,
|
||||
permissionMode: permissionModeFor(this.permissionMode, this.agentMode),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { privateRuntimeEnvironment } from '../private-runtimes'
|
||||
import { app, safeStorage } from 'electron'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { join, isAbsolute } from 'node:path'
|
||||
import { createHash, randomUUID } from 'node:crypto'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
||||
import type { McpServerInput, McpServerView, McpToolView, McpStatus } from '@shared/capabilities'
|
||||
@@ -183,12 +184,21 @@ export async function connectMcpServer(id: string): Promise<McpServerView> {
|
||||
if (existing?.status === 'connected' && existing.configHash === server.configHash) return view(server)
|
||||
if (existing?.flight && existing.configHash === server.configHash) return existing.flight
|
||||
if (existing) await closeRuntime(id)
|
||||
const values = readSecrets(server)
|
||||
const childEnvironment = server.transport === 'stdio'
|
||||
? await privateRuntimeEnvironment({ ...getDefaultEnvironment(), ...values.env }) : undefined
|
||||
await writes
|
||||
const current = requireServer(await load(), id)
|
||||
if (current.configHash !== server.configHash || !current.enabled || current.trustedHash !== current.configHash) throw new Error('MCP 配置已变化,请重试')
|
||||
const values = readSecrets(server), client = new Client({ name: 'YCode', version: '0.1.0' })
|
||||
// Another connect may have completed while private settings / registry were loading.
|
||||
const concurrent = runtimes.get(id)
|
||||
if (concurrent?.configHash === server.configHash) {
|
||||
if (concurrent.status === 'connected') return view(server)
|
||||
if (concurrent.flight) return concurrent.flight
|
||||
}
|
||||
const client = new Client({ name: 'YCode', version: '0.1.0' })
|
||||
const transport: Transport = server.transport === 'stdio'
|
||||
? new StdioClientTransport({ command: server.command!, args: server.args, cwd: server.cwd, env: values.env, stderr: 'ignore' })
|
||||
? new StdioClientTransport({ command: server.command!, args: server.args, cwd: server.cwd, env: childEnvironment, stderr: 'ignore' })
|
||||
: new StreamableHTTPClientTransport(new URL(server.url!), { requestInit: { headers: values.headers, redirect: 'error' },
|
||||
// Keep credentials on the configured origin. GET streams also use this guarded fetch.
|
||||
fetch: async (input, init) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getPrivateRuntimes, updatePrivateRuntimes } from './private-runtimes'
|
||||
/**
|
||||
* IPC 注册层:渲染进程与主进程之间唯一的契约边界。
|
||||
*
|
||||
@@ -422,6 +423,8 @@ export function registerIpc(getWindow: () => BrowserWindow | null): void {
|
||||
ipcMain.handle('general:get', (_event, options?: { refreshUsage?: boolean }) => generalView(options?.refreshUsage === true))
|
||||
ipcMain.handle('personalization:get', () => personalization.getPersonalization())
|
||||
ipcMain.handle('personalization:update', (_event, patch: PersonalizationPatch) => personalization.updatePersonalization(patch))
|
||||
ipcMain.handle('private-runtimes:get', () => getPrivateRuntimes())
|
||||
ipcMain.handle('private-runtimes:update', (_event, patch) => updatePrivateRuntimes(patch))
|
||||
ipcMain.handle('agents:get', () => agentSettings.getAgentSettings())
|
||||
ipcMain.handle('agents:update', (_event, patch: AgentSettingsPatch) => agentSettings.updateAgentSettings(patch))
|
||||
ipcMain.handle('memory:get', () => memorySettings.getMemorySettings())
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { win32 } from 'node:path'
|
||||
import type { PrivateRuntimeId, PrivateRuntimePatch, PrivateRuntimePreferences, PrivateRuntimesView } from '../shared/private-runtimes'
|
||||
|
||||
export const PRIVATE_RUNTIME_IDS: PrivateRuntimeId[] = ['node', 'python', 'git']
|
||||
export function normalizePrivateRuntimePreferences(value: unknown): PrivateRuntimePreferences {
|
||||
const source = value && typeof value === 'object' ? value as PrivateRuntimePatch : {}
|
||||
return { enabled: source.enabled !== false, tools: {
|
||||
node: source.tools?.node !== false, python: source.tools?.python !== false, git: source.tools?.git !== false
|
||||
} }
|
||||
}
|
||||
export function validatePrivateRuntimePatch(value: unknown): PrivateRuntimePatch {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('运行环境设置无效')
|
||||
const input = value as Record<string, unknown>, patch: PrivateRuntimePatch = {}
|
||||
if (Object.keys(input).some(key => key !== 'enabled' && key !== 'tools')) throw new Error('未知运行环境设置')
|
||||
if ('enabled' in input) {
|
||||
if (typeof input.enabled !== 'boolean') throw new Error('运行环境开关无效')
|
||||
patch.enabled = input.enabled
|
||||
}
|
||||
if ('tools' in input) {
|
||||
if (!input.tools || typeof input.tools !== 'object' || Array.isArray(input.tools)) throw new Error('工具设置无效')
|
||||
patch.tools = {}
|
||||
for (const [key, enabled] of Object.entries(input.tools)) {
|
||||
if (!PRIVATE_RUNTIME_IDS.includes(key as PrivateRuntimeId) || typeof enabled !== 'boolean') throw new Error('工具开关无效')
|
||||
patch.tools[key as PrivateRuntimeId] = enabled
|
||||
}
|
||||
}
|
||||
return patch
|
||||
}
|
||||
|
||||
/** Fixed application-owned layout, never a renderer-supplied executable or PATH. */
|
||||
export function privateRuntimeView(platform: string, root: string, preferences: PrivateRuntimePreferences,
|
||||
isFile: (path: string) => boolean): PrivateRuntimesView {
|
||||
const required: Record<PrivateRuntimeId, string[]> = {
|
||||
node: ['node.exe', 'npm.cmd', 'npx.cmd'],
|
||||
python: ['python.exe', 'Scripts/pip.cmd'],
|
||||
git: ['cmd/git.exe', 'bin/bash.exe']
|
||||
}
|
||||
const supported = platform === 'win32'
|
||||
return { platform, supported, preferences, tools: PRIVATE_RUNTIME_IDS.map(id => {
|
||||
const path = win32.join(root, id)
|
||||
const available = supported && required[id].every(file => isFile(win32.join(path, file)))
|
||||
const enabled = preferences.tools[id]
|
||||
return { id, available, enabled, active: supported && available && preferences.enabled && enabled, path: available ? path : null }
|
||||
}) }
|
||||
}
|
||||
|
||||
/** Pure clone: keep callers' credential policy and never mutate process.env or system PATH. */
|
||||
export function applyPrivateRuntimeEnvironment(environment: NodeJS.ProcessEnv, view: PrivateRuntimesView, dataRoot?: string): NodeJS.ProcessEnv {
|
||||
const env = { ...environment }
|
||||
if (!view.supported || !view.preferences.enabled) return env
|
||||
const active = view.tools.filter(tool => tool.active && tool.path)
|
||||
if (!active.length) return env
|
||||
const paths = active.flatMap(tool => tool.id === 'python' ? [tool.path!, win32.join(tool.path!, 'Scripts')]
|
||||
: tool.id === 'git' ? [win32.join(tool.path!, 'cmd'), win32.join(tool.path!, 'bin')] : [tool.path!])
|
||||
if (dataRoot) {
|
||||
const set = (key: string, value: string): void => {
|
||||
for (const name of Object.keys(env)) if (name.toLowerCase() === key.toLowerCase()) delete env[name]
|
||||
env[key] = value
|
||||
}
|
||||
if (active.some(tool => tool.id === 'node')) {
|
||||
const prefix = win32.join(dataRoot, 'node', 'global')
|
||||
set('NPM_CONFIG_PREFIX', prefix)
|
||||
set('NPM_CONFIG_CACHE', win32.join(dataRoot, 'node', 'cache'))
|
||||
paths.push(prefix) // Windows npm global executable shims live directly in prefix.
|
||||
}
|
||||
if (active.some(tool => tool.id === 'python')) {
|
||||
const userBase = win32.join(dataRoot, 'python', 'user')
|
||||
set('PYTHONUSERBASE', userBase)
|
||||
set('PIP_CACHE_DIR', win32.join(dataRoot, 'python', 'cache'))
|
||||
// Bundled CPython 3.14 uses the Windows nt_user scheme. Do not set
|
||||
// PYTHONHOME/PIP_TARGET/PIP_USER: those would interfere with project venvs.
|
||||
paths.push(win32.join(userBase, 'Python314', 'Scripts'))
|
||||
}
|
||||
}
|
||||
const pathKeys = Object.keys(env).filter(key => key.toLowerCase() === 'path')
|
||||
// Windows treats these names case-insensitively; only emit one to avoid Node's ambiguous selection.
|
||||
const key = 'PATH' // Also overrides the MCP SDK's inherited uppercase PATH on its final merge.
|
||||
const original = pathKeys.map(name => env[name]).filter((value): value is string => !!value).join(';')
|
||||
for (const name of pathKeys) delete env[name]
|
||||
const seen = new Set<string>()
|
||||
env[key] = [...paths, ...original.split(';')].filter(part => {
|
||||
if (!part) return false
|
||||
const normalized = part.replace(/\\+$/u, '').toLowerCase()
|
||||
if (seen.has(normalized)) return false
|
||||
seen.add(normalized)
|
||||
return true
|
||||
}).join(';')
|
||||
const git = active.find(tool => tool.id === 'git')
|
||||
if (git) {
|
||||
for (const name of Object.keys(env)) if (name.toLowerCase() === 'claude_code_git_bash_path') delete env[name]
|
||||
env.CLAUDE_CODE_GIT_BASH_PATH = win32.join(git.path!, 'bin', 'bash.exe')
|
||||
}
|
||||
return env
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { app } from 'electron'
|
||||
import { statSync, promises as fs } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import type { PrivateRuntimePatch, PrivateRuntimePreferences, PrivateRuntimesView } from '../shared/private-runtimes'
|
||||
import { applyPrivateRuntimeEnvironment, normalizePrivateRuntimePreferences, privateRuntimeView, validatePrivateRuntimePatch } from './private-runtime-environment'
|
||||
|
||||
// Kept separate from gateway config so unrelated preference writes cannot overwrite this policy.
|
||||
function preferencesFile(): string { return join(app.getPath('userData'), 'private-runtimes.json') }
|
||||
async function getStoredPrivateRuntimes(): Promise<unknown> {
|
||||
try {
|
||||
const value = validatePrivateRuntimePatch(JSON.parse(await fs.readFile(preferencesFile(), 'utf8')))
|
||||
if (typeof value.enabled !== 'boolean' || !value.tools
|
||||
|| ['node', 'python', 'git'].some(id => typeof value.tools?.[id as keyof typeof value.tools] !== 'boolean')) {
|
||||
throw new Error('私有运行环境设置文件不完整,请修复 private-runtimes.json')
|
||||
}
|
||||
return value
|
||||
}
|
||||
catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
|
||||
throw error
|
||||
}
|
||||
}
|
||||
async function setStoredPrivateRuntimes(value: PrivateRuntimePreferences): Promise<void> {
|
||||
const file = preferencesFile(), temporary = `${file}.tmp`
|
||||
await fs.mkdir(join(file, '..'), { recursive: true })
|
||||
try {
|
||||
await fs.writeFile(temporary, JSON.stringify(value, null, 2), 'utf8')
|
||||
await fs.rename(temporary, file)
|
||||
} finally { await fs.rm(temporary, { force: true }).catch(() => undefined) }
|
||||
}
|
||||
|
||||
export async function getPrivateRuntimes(): Promise<PrivateRuntimesView> {
|
||||
const preferences = normalizePrivateRuntimePreferences(await getStoredPrivateRuntimes())
|
||||
const resources = app.isPackaged ? process.resourcesPath : join(app.getAppPath(), 'resources')
|
||||
const root = join(resources, 'runtimes', `win32-${process.arch}`)
|
||||
return privateRuntimeView(process.platform, root, preferences, path => {
|
||||
try { return statSync(path).isFile() } catch { return false }
|
||||
})
|
||||
}
|
||||
let writes: Promise<unknown> = Promise.resolve()
|
||||
export async function updatePrivateRuntimes(input: PrivateRuntimePatch): Promise<PrivateRuntimesView> {
|
||||
const patch = validatePrivateRuntimePatch(input)
|
||||
const operation = writes.then(async () => {
|
||||
const current = normalizePrivateRuntimePreferences(await getStoredPrivateRuntimes())
|
||||
await setStoredPrivateRuntimes({ enabled: patch.enabled ?? current.enabled, tools: { ...current.tools, ...patch.tools } })
|
||||
return getPrivateRuntimes()
|
||||
})
|
||||
writes = operation.catch(() => undefined)
|
||||
return operation
|
||||
}
|
||||
export function privateRuntimeEnvironment(environment: Record<string, string>): Promise<Record<string, string>>
|
||||
export function privateRuntimeEnvironment(environment: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv>
|
||||
export async function privateRuntimeEnvironment(environment: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv> {
|
||||
// Preserve non-Windows startup behavior and avoid needless configuration I/O.
|
||||
if (process.platform !== 'win32') return { ...environment }
|
||||
return applyPrivateRuntimeEnvironment(environment, await getPrivateRuntimes(), join(app.getPath('userData'), 'runtime-data'))
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { privateRuntimeEnvironment } from './private-runtimes'
|
||||
import type { BrowserWindow } from 'electron'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { isAbsolute, join, resolve } from 'node:path'
|
||||
@@ -94,6 +95,7 @@ export async function createTerminal(owner: BrowserWindow, cwd: string, sessionI
|
||||
if (typeof cwd !== 'string' || !isAbsolute(cwd)) throw new Error('终端需要有效的绝对工作目录')
|
||||
const root = resolve(cwd)
|
||||
if (!(await fs.stat(root).catch(() => null))?.isDirectory()) throw new Error('工作空间文件夹不存在')
|
||||
const childEnvironment = terminalEnvironment(await privateRuntimeEnvironment(terminalEnvironment()))
|
||||
validOwner(owner)
|
||||
const existing = [...terminals.values()]
|
||||
const active = existing.filter(entry => !entry.view.exited)
|
||||
@@ -112,7 +114,7 @@ export async function createTerminal(owner: BrowserWindow, cwd: string, sessionI
|
||||
? join(process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe') : '/bin/bash'
|
||||
let pty: IPty
|
||||
try { pty = spawn(shell, process.platform === 'win32' ? ['-NoLogo', '-NoProfile'] : ['--noprofile', '--norc'], {
|
||||
name: 'xterm-256color', cols: columns, rows: lines, cwd: root, env: terminalEnvironment(),
|
||||
name: 'xterm-256color', cols: columns, rows: lines, cwd: root, env: childEnvironment,
|
||||
...(process.platform === 'win32' ? { useConpty: true, useConptyDll: true } : { encoding: 'utf8' })
|
||||
}) } catch { throw new Error('无法启动本地终端,请检查 PowerShell 和系统终端支持') }
|
||||
const view: TerminalHostView = { id: randomUUID(), sessionId: ownerSession, cwd: root, title: 'PowerShell', shell,
|
||||
|
||||
@@ -10,6 +10,10 @@ import type { DownloadPayload, ResourceRange } from '@shared/resources'
|
||||
import type { InteractionResponse } from '@shared/conversation'
|
||||
|
||||
const api: YCodeApi = {
|
||||
privateRuntimes: {
|
||||
get: () => ipcRenderer.invoke('private-runtimes:get'),
|
||||
update: patch => ipcRenderer.invoke('private-runtimes:update', patch)
|
||||
},
|
||||
updates: {
|
||||
getState: () => ipcRenderer.invoke('updates:get-state'),
|
||||
check: () => ipcRenderer.invoke('updates:check'),
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { PrivateRuntimeId, PrivateRuntimePatch, PrivateRuntimesView } from '@shared/private-runtimes'
|
||||
import { Switch } from '../ui/Switch'
|
||||
import { Button } from '../ui/Button'
|
||||
import './private-runtimes-settings.css'
|
||||
|
||||
const TOOLS: { id: PrivateRuntimeId; name: string; description: string; symbol: string }[] = [
|
||||
{ id: 'node', name: 'Node.js', description: '基于 Chrome V8 引擎的 JavaScript 运行时,用于服务端开发。', symbol: 'JS' },
|
||||
{ id: 'python', name: 'Python', description: '通用编程语言,适用于脚本编写、自动化和数据处理。', symbol: 'Py' },
|
||||
{ id: 'git', name: 'Git Bash', description: '在 Windows 上提供 Git 和 Bash Shell 的类 Unix 命令行环境。', symbol: '⑂' }
|
||||
]
|
||||
|
||||
/** Only the user-selected runtime card; no unrelated security controls. */
|
||||
export default function PrivateRuntimesSettingsPanel(): React.JSX.Element {
|
||||
const [view, setView] = useState<PrivateRuntimesView | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const mounted = useRef(false)
|
||||
const busy = useRef(false)
|
||||
const generation = useRef(0)
|
||||
|
||||
const refresh = async (): Promise<void> => {
|
||||
if (busy.current) return
|
||||
const request = ++generation.current
|
||||
setLoading(true); setError('')
|
||||
try {
|
||||
const next = await window.ycode.privateRuntimes.get()
|
||||
if (mounted.current && request === generation.current) setView(next)
|
||||
} catch (reason) {
|
||||
if (mounted.current && request === generation.current) setError(`无法读取内置运行时:${String((reason as Error)?.message ?? reason)}`)
|
||||
} finally {
|
||||
if (mounted.current && request === generation.current) setLoading(false)
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
mounted.current = true
|
||||
void refresh()
|
||||
return () => { mounted.current = false; ++generation.current }
|
||||
}, [])
|
||||
|
||||
const update = async (patch: PrivateRuntimePatch): Promise<void> => {
|
||||
if (busy.current || loading || !view?.supported) return
|
||||
busy.current = true; setSaving(true); setError('')
|
||||
try {
|
||||
// The checkmarks represent persisted choices, never optimistic success.
|
||||
const next = await window.ycode.privateRuntimes.update(patch)
|
||||
if (mounted.current) setView(next)
|
||||
} catch (reason) {
|
||||
if (mounted.current) setError(`保存失败,请重新读取确认当前状态:${String((reason as Error)?.message ?? reason)}`)
|
||||
} finally {
|
||||
busy.current = false
|
||||
if (mounted.current) setSaving(false)
|
||||
}
|
||||
}
|
||||
const disabled = loading || saving || !view?.supported
|
||||
return <div className="private-runtimes-settings">
|
||||
<section className="private-runtimes-settings__card" aria-label="内置运行时" aria-busy={loading || saving}>
|
||||
<div className="private-runtimes-settings__row private-runtimes-settings__header">
|
||||
<div><h2>内置运行时</h2><p>允许使用随包提供的运行时工具。</p></div>
|
||||
<Switch size="lg" aria-label="启用内置运行时" checked={view?.preferences.enabled ?? false} disabled={disabled} onCheckedChange={enabled => void update({ enabled })} />
|
||||
</div>
|
||||
{TOOLS.map(tool => {
|
||||
const status = view?.tools.find(item => item.id === tool.id)
|
||||
const state = !view ? (loading ? '正在检测' : '无法读取状态') : !view.supported ? '当前平台不支持' : !status?.available ? '未随包提供或文件不完整' : status.active ? '已启用(新进程生效)' : view.preferences.enabled ? '已停用' : '总开关已关闭(保留单项选择)'
|
||||
return <div className="private-runtimes-settings__row" key={tool.id} data-runtime={tool.id}>
|
||||
<div className="private-runtimes-settings__copy">
|
||||
<h3><span className={`private-runtimes-settings__icon private-runtimes-settings__icon--${tool.id}`} aria-hidden="true">{tool.symbol}</span>{tool.name}</h3>
|
||||
<p>{tool.description}</p>
|
||||
<small title={status?.path ?? undefined}>{state}</small>
|
||||
</div>
|
||||
<Switch size="lg" aria-label={`启用 ${tool.name}`} checked={view?.preferences.tools[tool.id] ?? false} disabled={disabled || !view?.preferences.enabled || !status?.available} onCheckedChange={enabled => void update({ tools: { [tool.id]: enabled } })} />
|
||||
</div>
|
||||
})}
|
||||
</section>
|
||||
{loading && <p role="status">正在读取内置运行时…</p>}
|
||||
{saving && <p role="status">正在保存…</p>}
|
||||
{error && <div role="alert"><p>{error}</p><Button size="sm" disabled={loading || saving} onClick={() => void refresh()}>重新读取</Button></div>}
|
||||
{view && !view.supported && <p role="status">内置运行时仅支持 Windows x64;当前平台不使用这些随包工具。</p>}
|
||||
<p className="private-runtimes-settings__note">更改仅对之后启动的进程生效,请重新打开终端或启动新会话。关闭后不再注入随包工具,系统已安装的工具仍可能可用;不会修改系统 PATH 或应用权限。</p>
|
||||
</div>
|
||||
}
|
||||
@@ -24,6 +24,7 @@ const ShortcutsSettingsPanel = loadSettingsPanel(() => import('./ShortcutsSettin
|
||||
const PersonalizationSettingsPanel = loadSettingsPanel(() => import('./PersonalizationSettingsPanel'))
|
||||
const MemorySettingsPanel = loadSettingsPanel(() => import('./MemorySettingsPanel'))
|
||||
const AboutHelpSettingsPanel = loadSettingsPanel(() => import('./AboutHelpSettingsPanel'))
|
||||
const PrivateRuntimesSettingsPanel = loadSettingsPanel(() => import('./PrivateRuntimesSettingsPanel'))
|
||||
const AgentsSettingsPanel = loadSettingsPanel(() => import('./AgentsSettingsPanel'))
|
||||
|
||||
interface SettingsNavigationRequest { section: string; action?: 'new'; requestId: number }
|
||||
@@ -77,7 +78,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi
|
||||
return () => window.removeEventListener('keydown', escape)
|
||||
}, [onClose])
|
||||
const modal = <section className={`settings-modal${nativeWindow ? ' settings-modal--window' : ''}`} role="dialog" aria-label="设置" aria-modal={!nativeWindow}>
|
||||
<div className="settings-modal__nav"><SourceSettingsNavigation activeTab={section} supportedTabs={['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization', 'memory', 'extensions', 'helpFeedback', 'getHelp']} visibleTabs={VISIBLE_SETTINGS_TABS}
|
||||
<div className="settings-modal__nav"><SourceSettingsNavigation activeTab={section} supportedTabs={['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization', 'memory', 'extensions', 'securityCenter', 'helpFeedback', 'getHelp']} visibleTabs={VISIBLE_SETTINGS_TABS}
|
||||
onTabChange={(id) => { setError(''); setSection(id === 'models' ? 'model' : id) }} onUnavailable={() => setError('该设置尚未接入')} /></div>
|
||||
<div className="settings-modal__content"><div className="settings-modal__panel">
|
||||
{error && <p role="alert">{error}</p>}
|
||||
@@ -89,6 +90,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi
|
||||
{section === 'personalization' && <PersonalizationSettingsPanel />}
|
||||
{section === 'memory' && <MemorySettingsPanel />}
|
||||
{(section === 'helpFeedback' || section === 'getHelp') && <AboutHelpSettingsPanel section={section} />}
|
||||
{section === 'securityCenter' && <PrivateRuntimesSettingsPanel />}
|
||||
{section === 'extensions' && <AgentsSettingsPanel />}
|
||||
{section === 'about' && <div className="settings-window-panel"><h2>YCode</h2><p>{settings.appInfo?.version}</p></div>}
|
||||
</div></div>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/* User-provided highlighted runtime card; scoped styling, not a recovered security page. */
|
||||
.private-runtimes-settings { color: var(--yc-text-primary); font-size: var(--yc-text-body-size); }
|
||||
.private-runtimes-settings__card { background: var(--yc-bg-primary); border-radius: var(--yc-radius-xl); padding: var(--yc-space-5); }
|
||||
.private-runtimes-settings__row { display: flex; align-items: center; justify-content: space-between; gap: var(--yc-space-5); padding-top: var(--yc-space-5); }
|
||||
.private-runtimes-settings__header { padding-top: 0; padding-bottom: var(--yc-space-5); border-bottom: var(--yc-border-width) solid var(--yc-border-secondary); }
|
||||
.private-runtimes-settings h2, .private-runtimes-settings h3, .private-runtimes-settings p { margin: 0; }
|
||||
.private-runtimes-settings h2 { font-size: var(--yc-font-size-6); font-weight: var(--yc-font-weight-500); line-height: var(--yc-line-height-6); }
|
||||
.private-runtimes-settings h3 { display: flex; align-items: center; gap: var(--yc-space-3); font-size: var(--yc-font-size-5); font-weight: var(--yc-font-weight-500); line-height: var(--yc-line-height-5-2); }
|
||||
.private-runtimes-settings p { color: var(--yc-text-secondary); line-height: var(--yc-line-height-5-2); }
|
||||
.private-runtimes-settings small { color: var(--yc-text-tertiary); font-size: var(--yc-font-size-3); line-height: var(--yc-line-height-4-2); }
|
||||
.private-runtimes-settings__copy { min-width: 0; overflow-wrap: anywhere; }
|
||||
.private-runtimes-settings .yc-switch { flex-shrink: 0; }
|
||||
.private-runtimes-settings__icon { display: inline-flex; align-items: center; justify-content: center; width: var(--yc-size-4); height: var(--yc-size-4); font-size: var(--yc-font-size-1); font-weight: var(--yc-font-weight-600); color: var(--yc-status-success); }
|
||||
.private-runtimes-settings__icon--python { color: var(--yc-status-info); }
|
||||
.private-runtimes-settings__icon--git { color: var(--yc-status-warning); font-size: var(--yc-font-size-7); }
|
||||
.private-runtimes-settings > p, .private-runtimes-settings > [role=alert] { margin: var(--yc-space-4) var(--yc-space-5) 0; }
|
||||
.private-runtimes-settings [role=alert] p { color: var(--yc-status-error); }
|
||||
.private-runtimes-settings .private-runtimes-settings__note { font-size: var(--yc-font-size-3); line-height: var(--yc-line-height-5); }
|
||||
@@ -110,6 +110,7 @@ export interface WorkspaceAttachResult extends WorkspaceRegistryView {
|
||||
|
||||
/** preload 暴露给渲染进程的 API 形状 */
|
||||
export interface YCodeApi {
|
||||
privateRuntimes: import('./private-runtimes').PrivateRuntimesApi
|
||||
updates: UpdatesApi
|
||||
automation: AutomationApi
|
||||
library: LibraryApi
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/** Private Windows tools apply only to newly created YCode child processes. */
|
||||
export type PrivateRuntimeId = 'node' | 'python' | 'git'
|
||||
export interface PrivateRuntimePreferences {
|
||||
enabled: boolean
|
||||
tools: Record<PrivateRuntimeId, boolean>
|
||||
}
|
||||
export interface PrivateRuntimePatch {
|
||||
enabled?: boolean
|
||||
tools?: Partial<Record<PrivateRuntimeId, boolean>>
|
||||
}
|
||||
export interface PrivateRuntimeStatus {
|
||||
id: PrivateRuntimeId
|
||||
available: boolean
|
||||
enabled: boolean
|
||||
active: boolean
|
||||
path: string | null
|
||||
}
|
||||
export interface PrivateRuntimesView {
|
||||
platform: string
|
||||
supported: boolean
|
||||
preferences: PrivateRuntimePreferences
|
||||
tools: PrivateRuntimeStatus[]
|
||||
}
|
||||
export interface PrivateRuntimesApi {
|
||||
get(): Promise<PrivateRuntimesView>
|
||||
update(patch: PrivateRuntimePatch): Promise<PrivateRuntimesView>
|
||||
}
|
||||
Reference in new issue
Block a user