feat: bundle private Windows runtimes with settings switches

This commit is contained in:
Codex committed 2026-10-21 16:31:24 +09:00
1 parent 759df7060d
commit bef87520c2
28 files changed
+902 -10

No files matched your search

+3 -1
View File
@@ -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),
+13 -3
View File
@@ -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) => {
+3
View File
@@ -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())
+94
View File
@@ -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
}
+57
View File
@@ -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'))
}
+3 -1
View File
@@ -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,
+4
View File
@@ -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); }
+1
View File
@@ -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
+27
View File
@@ -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>
}