feat(settings): replicate the WorkBuddy personalization panel and wire identity, tone and prompts (TODO 13)
Recover the WorkBuddy 5.8.3 个性化 page from REA evidence (PersonalizationPanel-D16rRGjy.css + tokens): 45 module rules plus scoped light/dark tokens, the five-card 1160px layout (identity card 638x100 with inline editors, reply preference card 638x204, three 638x264 long-text cards with 598x160 previews) and the 640x520 editor modal. Persist identity, tone, welcome tips, custom instructions, persona and long-term memory in config.json, expose personalization:get/update over IPC, and append the resulting personalization context to each turn's prompt for both the OMP and built-in kernels. Wire 欢迎语 to the streaming loading tips and keep 显示文件修改过程 visibly disabled because YCode has no incremental file-change renderer yet. Verify with scripts/verify-personalization-settings.cjs: 29 assertions covering geometry, tone options, switches, inline identity editing with blur-discard, editor modal save/cancel, config persistence, reopen, dark surfaces and no renderer errors. Add a pure prompt-builder regression and keep every existing settings regression green.
This commit is contained in:
1 parent
19460f0b13
commit
0dc088f1f7
54 files changed
+2038
-248
No files matched your search
+14
-1
@@ -10,7 +10,7 @@
|
||||
import { app, safeStorage } from 'electron'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import type { GatewayConfig, GatewayConfigView } from '@shared/types'
|
||||
import type { GatewayConfig, GatewayConfigView, PersonalizationSettings } from '@shared/types'
|
||||
|
||||
interface StoredConfig {
|
||||
speechBaseUrl?: string
|
||||
@@ -26,6 +26,8 @@ interface StoredConfig {
|
||||
preventSleep?: boolean
|
||||
/** 开机自动启动(通用设置;默认关闭)。 */
|
||||
autoLaunch?: boolean
|
||||
/** 设置 → 个性化(身份、回复风格、指令、人设、长期记忆)。 */
|
||||
personalization?: Partial<PersonalizationSettings>
|
||||
}
|
||||
|
||||
const DEFAULTS: StoredConfig = {
|
||||
@@ -118,6 +120,17 @@ export async function setAutoLaunch(enabled: boolean): Promise<void> {
|
||||
await persist({ ...stored, autoLaunch: enabled })
|
||||
}
|
||||
|
||||
/** 设置 → 个性化的原始持久化值(缺省字段由 personalization.ts 补齐)。 */
|
||||
export async function getStoredPersonalization(): Promise<Partial<PersonalizationSettings>> {
|
||||
const stored = await load()
|
||||
return stored.personalization ?? {}
|
||||
}
|
||||
|
||||
export async function setStoredPersonalization(value: Partial<PersonalizationSettings>): Promise<void> {
|
||||
const stored = await load()
|
||||
await persist({ ...stored, personalization: { ...(stored.personalization ?? {}), ...value } })
|
||||
}
|
||||
|
||||
/** 供渲染进程使用:脱敏视图 */
|
||||
export async function getConfigView(): Promise<GatewayConfigView> {
|
||||
const stored = await load()
|
||||
|
||||
+7
-2
@@ -11,7 +11,7 @@ import { BrowserWindow, app, dialog, ipcMain, shell, type OpenDialogOptions } fr
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { unlink, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import type { ChatEvent, ChatMessage, GitBranchInfo, WindowState } from '@shared/types'
|
||||
import type { ChatEvent, ChatMessage, GitBranchInfo, PersonalizationPatch, WindowState } from '@shared/types'
|
||||
import { freezeAssistantModelMetadata } from '@shared/assistant-model'
|
||||
import type { ConfigPatchLike, EditAction, SendPayload, WorkspaceAttachResult } from '@shared/ipc'
|
||||
import { applyAssistantEvent } from '@shared/message-parts'
|
||||
@@ -34,6 +34,7 @@ import { assistantWireHistory } from './agent/history'
|
||||
import * as models from './models'
|
||||
import * as workspaces from './workspaces'
|
||||
import * as generalSettings from './general-settings'
|
||||
import * as personalization from './personalization'
|
||||
import { prepareOmpModel } from './omp-model'
|
||||
import type { CustomModelInput, ResolvedModel } from '@shared/models'
|
||||
import type { SessionContext } from '@shared/workspaces'
|
||||
@@ -408,6 +409,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('general:update', async (_event, patch: { autoLaunch?: boolean; preventSleep?: boolean }) => {
|
||||
if (patch.preventSleep !== undefined) {
|
||||
await config.setPreventSleep(patch.preventSleep)
|
||||
@@ -740,7 +743,9 @@ export function registerIpc(getWindow: () => BrowserWindow | null): void {
|
||||
const interactionBroker = createInteractionBroker(runId, controller.signal, emit)
|
||||
// 对话中引用的内容块(原版 message-quote)随消息一起交给模型;表格 markdown 原样附带。
|
||||
const quoteText = (payload.quotes ?? []).map((quote) => quote.markdown.trim()).filter(Boolean).join('\n\n')
|
||||
const promptText = [text, quoteText, promptAttachments.text, references.length ? `引用工作区资源:\n${references.join('\n')}` : '', capabilitySnapshot.context].filter(Boolean).join('\n\n')
|
||||
// 设置 → 个性化的身份/风格/指令/人设/长期记忆随每轮请求发送,OMP 与自建循环共用同一段文本。
|
||||
const personalizationPrompt = personalization.buildPersonalizationPrompt(await personalization.getPersonalization())
|
||||
const promptText = [text, quoteText, promptAttachments.text, references.length ? `引用工作区资源:\n${references.join('\n')}` : '', capabilitySnapshot.context, personalizationPrompt].filter(Boolean).join('\n\n')
|
||||
|
||||
// 回合在后台推进,立刻把 runId 还给渲染进程
|
||||
const initialMessages = payload.action ? structuredClone([...history, userMessage, assistant]) : undefined
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 设置 → 个性化(TODO 13)的持久化与运行链路。
|
||||
*
|
||||
* REA 依据(WorkBuddy 5.8.3,设置窗口 900×680):
|
||||
* - ev_af920edf33af5e811d58f05dad15210b29cd891f28e0fa74f6b18d63212241b1(浅色顶部 DOM/layout/computed style + 模块 CSS)
|
||||
* - ev_88af0774005c02e9c75284ade2be45de4943ee5aaf73fe5b13ac8a38b85be786(深色同页)
|
||||
* - ev_4f4f76054a930a186199ee28a5f0169f7de48a05a295b8e9eb630c796f07be4a(最终浅色顶部)
|
||||
*
|
||||
* 选项表、默认人设与个性化上下文拼装放在 @shared/personalization(纯函数,可离线回归);
|
||||
* 本模块只负责读写 config.json 的 personalization 字段。
|
||||
*/
|
||||
import type { PersonalizationPatch, PersonalizationSettings } from '@shared/types'
|
||||
import { normalizePersonalization } from '@shared/personalization'
|
||||
import { getStoredPersonalization, setStoredPersonalization } from './config'
|
||||
|
||||
export { buildPersonalizationPrompt, DEFAULT_PERSONA, DEFAULT_PERSONALIZATION, TONE_OPTIONS } from '@shared/personalization'
|
||||
export { normalizePersonalization } from '@shared/personalization'
|
||||
|
||||
export async function getPersonalization(): Promise<PersonalizationSettings> {
|
||||
return normalizePersonalization(await getStoredPersonalization())
|
||||
}
|
||||
|
||||
export async function updatePersonalization(patch: PersonalizationPatch): Promise<PersonalizationSettings> {
|
||||
const current = await getPersonalization()
|
||||
const next = normalizePersonalization({ ...current, ...patch })
|
||||
await setStoredPersonalization(next)
|
||||
return next
|
||||
}
|
||||
@@ -113,6 +113,10 @@ const api: YCodeApi = {
|
||||
changeWorkspaceRoot: (path) => ipcRenderer.invoke('general:changeWorkspaceRoot', path),
|
||||
openSystemNotificationSettings: () => ipcRenderer.invoke('general:open-system-notification-settings')
|
||||
},
|
||||
personalization: {
|
||||
get: () => ipcRenderer.invoke('personalization:get'),
|
||||
update: (patch) => ipcRenderer.invoke('personalization:update', patch)
|
||||
},
|
||||
workspace: {
|
||||
get: () => ipcRenderer.invoke('workspace:get'),
|
||||
choose: () => ipcRenderer.invoke('workspace:choose'),
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* 设置 → 个性化:WorkBuddy 5.8.3 个性化页的 1:1 复刻 + YCode 真实接线(TODO 13)。
|
||||
*
|
||||
* REA 依据(设置窗口 900×680):
|
||||
* - ev_af920edf33af5e811d58f05dad15210b29cd891f28e0fa74f6b18d63212241b1(浅色顶部 DOM/layout/computed style、模块 CSS)
|
||||
* - ev_88af0774005c02e9c75284ade2be45de4943ee5aaf73fe5b13ac8a38b85be786(深色同页)
|
||||
* - ev_d6e0a2483a3bde901f4cf8299ff03eb0b475cb934921058cf38dd671585ab457(开关与下拉交互)
|
||||
* - ev_523e07353a9fd7a003136f8e07c72d5ba87a05f893dd4b4c4adb2d7ed5a7430f(富文本编辑弹窗 DOM/几何)
|
||||
* - ev_4f4f76054a930a186199ee28a5f0169f7de48a05a295b8e9eb630c796f07be4a(最终浅色顶部)
|
||||
*
|
||||
* 已接入:称呼 / YCode 的名字 / 回复风格 / 给 YCode 的指令 / 人设设定 / 长期记忆 → config.json +
|
||||
* 每轮请求上下文;欢迎语 → 控制流式加载提示。
|
||||
* 未接入:「显示文件修改过程」在 YCode 尚无对应的逐步渲染开关,保持原版控件形态但禁用并标注。
|
||||
* 编辑弹窗用 textarea 承载纯文本(原版是 .sc-editor-content 富文本编辑器),结构与几何按 REA 还原。
|
||||
*/
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PersonalizationPatch, PersonalizationSettings, PersonalizationTone } from '@shared/types'
|
||||
import { TONE_OPTIONS } from '@shared/personalization'
|
||||
import { IconSelectCaret } from './general-settings-icons'
|
||||
import { setLoadingTipsEnabled } from '../personalization-prefs'
|
||||
import './source-personalization-panel.css'
|
||||
|
||||
const UNAVAILABLE_HINT = '暂未接入'
|
||||
const IDENTITY_PLACEHOLDER = '暂无内容,点击编辑添加'
|
||||
|
||||
type IdentityField = 'userCallName' | 'aiName'
|
||||
type LongField = 'customPrompt' | 'persona' | 'longTermMemory'
|
||||
|
||||
function CheckIcon(): React.JSX.Element {
|
||||
return <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"><path d="M3.25 8.4 6.4 11.5 12.75 4.75" /></svg>
|
||||
}
|
||||
|
||||
function CloseIcon(): React.JSX.Element {
|
||||
return <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"><path d="m4.25 4.25 7.5 7.5M11.75 4.25l-7.5 7.5" /></svg>
|
||||
}
|
||||
|
||||
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
|
||||
id?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
/** 与通用设置相同的原版行结构:section-main > (copy > 标题+描述) + action,body 可选。 */
|
||||
function Section({ title, desc, action, body, stacked, id, className }: SectionProps): React.JSX.Element {
|
||||
return (
|
||||
<section id={id} 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 ToneSelect({ value, open, busy, onToggle, onChoose }: {
|
||||
value: PersonalizationTone
|
||||
open: boolean
|
||||
busy: boolean
|
||||
onToggle(): void
|
||||
onChoose(value: PersonalizationTone): void
|
||||
}): React.JSX.Element {
|
||||
const option = TONE_OPTIONS[value]
|
||||
return (
|
||||
<div className="personalization-panel__dropdown source-select-host">
|
||||
<button type="button" className="wb-select wb-select--medium personalization-panel__tone-trigger"
|
||||
aria-label="回复风格" aria-haspopup="listbox" aria-expanded={open} disabled={busy} onClick={onToggle}>
|
||||
<span className="wb-select__value">{option.label}</span>
|
||||
<IconSelectCaret className="wb-icon wb-select__caret" />
|
||||
</button>
|
||||
{open ? (
|
||||
<div className="wb-popover wb-select__popover personalization-panel__tone-popover" role="listbox" aria-label="回复风格">
|
||||
{(Object.keys(TONE_OPTIONS) as PersonalizationTone[]).map((id) => {
|
||||
const item = TONE_OPTIONS[id]
|
||||
return (
|
||||
<button type="button" role="option" aria-selected={id === value} key={id}
|
||||
className={classNames('wb-select__option', id === value && 'wb-select__option--single-selected')}
|
||||
onClick={() => onChoose(id)}>
|
||||
<span className="personalization-panel__option-text">
|
||||
<span className="personalization-panel__option-name">{item.label}</span>
|
||||
<span className="personalization-panel__option-hint">{item.hint}</span>
|
||||
</span>
|
||||
{id === value ? <span className="wb-select__check"><CheckIcon /></span> : null}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PersonalizationSettingsPanel(): React.JSX.Element {
|
||||
const [view, setView] = useState<PersonalizationSettings | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [editing, setEditing] = useState<IdentityField | null>(null)
|
||||
const [draft, setDraft] = useState('')
|
||||
const [toneOpen, setToneOpen] = useState(false)
|
||||
const [editor, setEditor] = useState<{ field: LongField; title: string; value: string } | null>(null)
|
||||
const identityInput = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
void window.ycode.personalization.get()
|
||||
.then((next) => { if (!active) return; setView(next); setLoadingTipsEnabled(next.loadingTips) })
|
||||
.catch((reason) => { if (active) setError(String((reason as Error)?.message ?? reason)) })
|
||||
return () => { active = false }
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!toneOpen && !editor) return
|
||||
const close = (event: KeyboardEvent): void => {
|
||||
if (event.key !== 'Escape') return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setToneOpen(false)
|
||||
setEditor(null)
|
||||
}
|
||||
document.addEventListener('keydown', close, true)
|
||||
return () => document.removeEventListener('keydown', close, true)
|
||||
}, [toneOpen, editor])
|
||||
|
||||
useEffect(() => {
|
||||
if (!toneOpen) return
|
||||
const outside = (event: PointerEvent): void => {
|
||||
const target = event.target as Element | null
|
||||
if (target?.closest?.('.personalization-panel__dropdown')) return
|
||||
setToneOpen(false)
|
||||
}
|
||||
document.addEventListener('pointerdown', outside, true)
|
||||
return () => document.removeEventListener('pointerdown', outside, true)
|
||||
}, [toneOpen])
|
||||
|
||||
useEffect(() => { if (editing) identityInput.current?.focus() }, [editing])
|
||||
|
||||
const apply = async (patch: PersonalizationPatch): Promise<PersonalizationSettings | null> => {
|
||||
setBusy(true)
|
||||
try {
|
||||
const next = await window.ycode.personalization.update(patch)
|
||||
setView(next)
|
||||
setLoadingTipsEnabled(next.loadingTips)
|
||||
setError('')
|
||||
return next
|
||||
} catch (reason) {
|
||||
setError(String((reason as Error)?.message ?? reason))
|
||||
return null
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const startIdentity = (field: IdentityField): void => {
|
||||
if (!view) return
|
||||
setToneOpen(false)
|
||||
setDraft(view[field])
|
||||
setEditing(field)
|
||||
}
|
||||
|
||||
const commitIdentity = (): void => {
|
||||
if (!editing) return
|
||||
const value = draft.trim()
|
||||
// 原版实测:空值不能确认保存(清空后确认仍停留在空编辑态)。
|
||||
if (!value) return
|
||||
const field = editing
|
||||
void apply({ [field]: value }).then((next) => { if (next) setEditing(null) })
|
||||
}
|
||||
|
||||
const cancelIdentity = (): void => { setEditing(null); setDraft('') }
|
||||
|
||||
const openEditor = (field: LongField, title: string): void => {
|
||||
if (!view) return
|
||||
setEditor({ field, title, value: view[field] })
|
||||
}
|
||||
|
||||
const saveEditor = (): void => {
|
||||
if (!editor) return
|
||||
const { field, value } = editor
|
||||
void apply({ [field]: value }).then((next) => { if (next) setEditor(null) })
|
||||
}
|
||||
|
||||
const identityRow = (field: IdentityField, label: string): React.JSX.Element => {
|
||||
const value = view?.[field] ?? ''
|
||||
const id = field === 'userCallName' ? 'settings-local-memory-user-call-name' : 'settings-local-memory-ai-name'
|
||||
return (
|
||||
<div className="local-memory-identity-row" id={id}>
|
||||
<div className="local-memory-identity-row__label">{label}</div>
|
||||
{editing === field ? (
|
||||
<div className="local-memory-identity-row__edit">
|
||||
<input ref={identityInput} className="local-memory-inline__input" aria-label={label} value={draft} maxLength={24}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onBlur={(event) => {
|
||||
// 原版实测:点到卡片外部失焦即丢弃草稿;点到本行的取消/确认按钮则交给按钮处理。
|
||||
const next = event.relatedTarget as Node | null
|
||||
if (next && event.currentTarget.closest('.local-memory-identity-row__edit')?.contains(next)) return
|
||||
cancelIdentity()
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') { event.preventDefault(); commitIdentity() }
|
||||
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); cancelIdentity() }
|
||||
}} />
|
||||
<div className="local-memory-identity-row__actions">
|
||||
<button type="button" className="local-memory-inline__icon-btn wb-button" aria-label={`取消编辑${label}`} title="取消" onClick={cancelIdentity}><CloseIcon /></button>
|
||||
<button type="button" className="local-memory-inline__icon-btn local-memory-inline__icon-btn--confirm wb-button" aria-label={`确认${label}`} title="确认" disabled={busy || !draft.trim()} onClick={commitIdentity}><CheckIcon /></button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button type="button" className="local-memory-pill wb-button wb-button--secondary wb-button--medium" aria-label={`编辑${label}`} onClick={() => startIdentity(field)}>
|
||||
<span className="wb-button__content">
|
||||
<span className={value ? 'local-memory-pill__value' : 'local-memory-pill__placeholder'}>{value || IDENTITY_PLACEHOLDER}</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const longCard = (field: LongField, id: string, title: string, desc: string, placeholder: string): React.JSX.Element => {
|
||||
const value = view?.[field] ?? ''
|
||||
const open = (): void => openEditor(field, title)
|
||||
return (
|
||||
<div className="general-settings-panel__group local-memory-long-card" id={id}>
|
||||
<div className="local-memory-long">
|
||||
<Section title={title} desc={desc} stacked
|
||||
action={<button type="button" className="wb-button wb-button--secondary wb-button--medium" onClick={open}>编辑</button>}
|
||||
body={<div className="local-memory-long__preview" role="button" tabIndex={0} title={value || placeholder}
|
||||
onClick={open} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); open() } }}>
|
||||
{value ? <div className="local-memory-long__value">{value}</div> : <div className="local-memory-long__placeholder">{placeholder}</div>}
|
||||
</div>} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="general-settings-panel personalization-panel">
|
||||
{error ? <p role="alert">{error}</p> : null}
|
||||
|
||||
<div className="general-settings-panel__group local-memory-section">
|
||||
<div className="local-memory-identity-card">
|
||||
{identityRow('userCallName', 'YCode 对你的称呼')}
|
||||
{identityRow('aiName', 'YCode 的名字')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="general-settings-panel__group-card">
|
||||
<Section id="settings-tone-style" title="回复风格" desc="选择 YCode 回复的默认语气。"
|
||||
action={<ToneSelect value={view?.tone ?? 'default'} open={toneOpen} busy={busy || !view}
|
||||
onToggle={() => setToneOpen((open) => !open)} onChoose={(tone) => { setToneOpen(false); void apply({ tone }) }} />} />
|
||||
<Section id="settings-loading-tips" title="欢迎语" desc="当对话加载较慢时,用一句问候陪你等待。"
|
||||
action={<Switch checked={view?.loadingTips ?? true} ariaLabel="欢迎语" disabled={busy || !view}
|
||||
onChange={(loadingTips) => void apply({ loadingTips })} />} />
|
||||
<Section id="settings-file-changes" title="显示文件修改过程" desc="开启后,YCode 会逐步展示每次文件改动的过程与结果。"
|
||||
action={<span data-settings-unavailable="true" aria-disabled="true" title={UNAVAILABLE_HINT}>
|
||||
<Switch checked={view?.showFileChanges ?? false} ariaLabel="显示文件修改过程" disabled onChange={() => undefined} />
|
||||
</span>} />
|
||||
</div>
|
||||
|
||||
{longCard('customPrompt', 'settings-custom-prompt', '给 YCode 的指令', '写下你希望 YCode 一直遵守的要求,之后每次对话和任务都会默认生效。', '例如:回答尽量简洁,先给结论')}
|
||||
{longCard('persona', 'settings-local-memory-soul', '人设设定', '设定 YCode 的身份、性格与表达风格,对所有回复生效。', '暂无内容,点击编辑添加')}
|
||||
{longCard('longTermMemory', 'settings-local-memory-body', '长期记忆', '跨会话保存的重要信息与结论,可随时查看和修改。', '暂无记忆记录,点击编辑添加')}
|
||||
|
||||
{editor ? (
|
||||
<div className="wb-modal__overlay wb-modal__overlay--center" onClick={() => setEditor(null)}>
|
||||
<div className="wb-modal wb-modal--medium local-memory-edit-modal" role="dialog" aria-modal="true" aria-label={editor.title}
|
||||
onClick={(event) => event.stopPropagation()}>
|
||||
<button type="button" className="wb-button wb-button--ghost wb-button--small wb-button--icon-only wb-button--circle wb-modal__close"
|
||||
aria-label="关闭" onClick={() => setEditor(null)}><CloseIcon /></button>
|
||||
<div className="wb-modal__header">
|
||||
<div className="wb-modal__header-row"><div className="wb-modal__title-wrap"><h2 className="wb-modal__title">{editor.title}</h2></div></div>
|
||||
</div>
|
||||
<div className="wb-modal__body">
|
||||
<div className="local-memory-editor-host">
|
||||
<textarea className="local-memory-editor sc-editor-content" aria-label={editor.title} value={editor.value}
|
||||
placeholder="当前内容为空,点击开始输入" autoFocus
|
||||
onChange={(event) => setEditor({ ...editor, value: event.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="wb-modal__footer wb-modal__footer--right">
|
||||
<button type="button" className="wb-button wb-button--secondary wb-button--medium" onClick={() => setEditor(null)}>取消</button>
|
||||
<button type="button" className="wb-button wb-button--primary wb-button--medium" disabled={busy} onClick={saveEditor}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import ModelSettingsPanel from './ModelSettingsPanel'
|
||||
import GeneralSettingsPanel from './GeneralSettingsPanel'
|
||||
import AppearanceSettingsPanel from './AppearanceSettingsPanel'
|
||||
import ShortcutsSettingsPanel from './ShortcutsSettingsPanel'
|
||||
import PersonalizationSettingsPanel from './PersonalizationSettingsPanel'
|
||||
import SourceSettingsNavigation from './SourceSettingsNavigation'
|
||||
import { useSharedTheme } from '../useSharedTheme'
|
||||
import { SOURCE_WINDOWS_SETTINGS_TABS } from './SourceSettingsNavigation'
|
||||
@@ -54,7 +55,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']} visibleTabs={VISIBLE_SETTINGS_TABS}
|
||||
<div className="settings-modal__nav"><SourceSettingsNavigation activeTab={section} supportedTabs={['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization']} 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>}
|
||||
@@ -63,6 +64,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi
|
||||
{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' && <AppearanceSettingsPanel theme={theme} onThemeChange={onThemeChange} />}
|
||||
{section === 'keyboardShortcuts' && <ShortcutsSettingsPanel />}
|
||||
{section === 'personalization' && <PersonalizationSettingsPanel />}
|
||||
{section === 'about' && <div className="settings-window-panel"><h2>YCode</h2><p>{settings.appInfo?.version}</p></div>}
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/* REA ev_af920edf33af5e811d58f05dad15210b29cd891f28e0fa74f6b18d63212241b1; PersonalizationPanel-D16rRGjy.css sha256=318664f96253adace047d7b1d2d213fbf6139461c10d80ae12135998b7b98f3d; tokens sha256=13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696. */
|
||||
.personalization-panel__dropdown { flex-shrink: 0; position: relative; }
|
||||
.personalization-panel__option-text { flex: 1 1 0%; }
|
||||
.personalization-panel__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 .personalization-panel__option-name { font-weight: 500; }
|
||||
.personalization-panel__option-hint { margin-top: var(--wb-spacing-1); color: var(--wb-color-text-tertiary,#00000080); font-family: "PingFang SC"; font-size: 12px; font-style: normal; font-weight: 400; line-height: 20px; text-decoration-line: none; }
|
||||
.personalization-panel__tone-trigger .personalization-panel__option-hint { display: none; }
|
||||
.personalization-panel__tone-popover { width: 240px !important; max-width: 240px !important; }
|
||||
.personalization-panel__tone-popover .wb-select__option { height: auto; min-height: var(--wb-select-size-control-md); padding-top: var(--wb-spacing-2-5); padding-bottom: var(--wb-spacing-2-5); }
|
||||
@media (width <= 640px) {
|
||||
.personalization-panel__dropdown { align-self: flex-end; max-width: 100%; }
|
||||
}
|
||||
.local-memory-identity-card { gap: var(--wb-spacing-5); padding: var(--wb-spacing-3) var(--wb-spacing-4); border-radius: var(--wb-radius-xl); background: var(--wb-bg-primary); flex-direction: column; display: flex; }
|
||||
.local-memory-long-card { padding: var(--wb-spacing-4); border-radius: var(--wb-radius-xl); background: var(--wb-bg-primary); }
|
||||
.local-memory-identity-row { justify-content: space-between; align-items: center; gap: var(--wb-spacing-4); min-height: 34px; display: flex; }
|
||||
.local-memory-identity-row__label { min-width: 0px; color: var(--wb-color-text-primary); font-size: var(--wb-font-body-size); font-weight: 500; line-height: var(--wb-font-body-line-height); flex: 1 1 0%; }
|
||||
.local-memory-identity-row__edit { align-items: center; gap: var(--wb-spacing-2); flex: 0 0 auto; min-width: 240px; display: flex; }
|
||||
.local-memory-identity-row__actions { align-items: center; gap: var(--wb-spacing-1); flex: 0 0 auto; display: flex; }
|
||||
.local-memory-pill.wb-button { min-width: 120px; max-width: 260px; height: 34px; padding: 0 var(--wb-spacing-3); border: 1px solid var(--wb-border-control); border-radius: var(--wb-radius-md); background: var(--wb-bg-primary); color: var(--wb-color-text-primary); font-family: inherit; font-size: var(--wb-font-body-size); line-height: var(--wb-font-body-line-height); box-shadow: none; justify-content: flex-start; align-items: center; display: inline-flex; }
|
||||
.local-memory-pill.wb-button .wb-button__content { flex: 1 1 0%; min-width: 0px; overflow: hidden; }
|
||||
.local-memory-pill.wb-button:hover:not(:disabled) { background: var(--wb-bg-hover); border-color: var(--wb-border-control); }
|
||||
.local-memory-pill__value,.local-memory-pill__placeholder { white-space: nowrap; text-overflow: ellipsis; min-width: 0px; max-width: 100%; display: block; overflow: hidden; }
|
||||
.local-memory-pill__placeholder { color: var(--wb-color-text-tertiary); }
|
||||
.local-memory-pill__placeholder--error { color: var(--wb-status-warning-vivid); }
|
||||
.local-memory-long { gap: var(--wb-spacing-3); flex-direction: column; display: flex; }
|
||||
.local-memory-long .general-settings-panel__section--plain { padding-left: var(--wb-spacing-3); padding-right: var(--wb-spacing-3); }
|
||||
.local-memory-long .general-settings-panel__section-body { padding-left: 0px; padding-right: 0px; }
|
||||
.local-memory-long__preview { box-sizing: border-box; height: calc(var(--wb-size-10) * 4); padding: var(--wb-spacing-3) var(--wb-spacing-4); border: 1px solid var(--wb-color-border-primary); border-radius: var(--wb-radius-lg); background: var(--wb-button-secondary-bg); overflow: auto; }
|
||||
.local-memory-long__preview[role="button"] { cursor: pointer; }
|
||||
.local-memory-long__value { white-space: pre-wrap; word-break: break-word; color: var(--wb-color-text-primary); font-size: var(--wb-font-body-size); line-height: var(--wb-font-body-line-height); }
|
||||
.local-memory-long__placeholder { color: var(--wb-color-text-tertiary); font-size: var(--wb-font-body-size); line-height: var(--wb-font-body-line-height); }
|
||||
.local-memory-long__placeholder--error { color: var(--wb-status-warning-vivid); }
|
||||
.local-memory-inline__icon-btn.wb-button { width: 34px; height: 34px; color: var(--wb-color-text-tertiary); box-shadow: none; background: 0px 0px; border: none; padding: 0px; }
|
||||
.local-memory-inline__icon-btn.wb-button .wb-button__icon { width: 16px; height: 16px; }
|
||||
.local-memory-inline__icon-btn.wb-button:hover:not(:disabled) { color: var(--wb-color-text-primary); background: 0px 0px; }
|
||||
.local-memory-inline__icon-btn--confirm.wb-button { color: var(--wb-status-success); }
|
||||
.local-memory-inline__icon-btn--confirm.wb-button:hover:not(:disabled) { color: var(--wb-status-success); background: 0px 0px; }
|
||||
.local-memory-inline__icon-btn.wb-button:disabled { opacity: 0.45; }
|
||||
.local-memory-edit-modal .wb-modal__body { flex-direction: column; display: flex; }
|
||||
.local-memory-editor-host { background: var(--wb-input-color-bg); border: 1px solid var(--wb-input-border-color); border-radius: var(--wb-input-border-radius-textarea); flex-direction: column; flex: 1 0 0px; align-self: stretch; align-items: center; gap: 12px; width: 100%; min-height: 0px; display: flex; }
|
||||
.local-memory-editor-host > * { border-radius: var(--wb-input-border-radius-textarea); flex: 1 1 0%; align-self: stretch; min-height: 0px; overflow: auto; }
|
||||
.local-memory-editor-host .sc-editor-content { scrollbar-width: thin; scrollbar-color: transparent transparent; }
|
||||
.local-memory-editor-host .sc-editor-content:hover { scrollbar-color: var(--wb-scrollbar-thumb) transparent; }
|
||||
.local-memory-editor-host .sc-editor-content::-webkit-scrollbar { width: 4px; }
|
||||
.local-memory-editor-host .sc-editor-content::-webkit-scrollbar-track { background: 0px 0px; }
|
||||
.local-memory-editor-host .sc-editor-content::-webkit-scrollbar-thumb { background: 0px 0px; border-radius: 4px; }
|
||||
.local-memory-editor-host .sc-editor-content:hover::-webkit-scrollbar-thumb { background: var(--wb-scrollbar-thumb); }
|
||||
.local-memory-editor-loading { height: 100%; color: var(--wb-color-text-secondary); font-size: var(--wb-font-body-size); justify-content: center; align-items: center; display: flex; }
|
||||
.general-settings-panel.personalization-panel{--wb-font-body-size:var(--wb-font-size-5);--wb-font-body-line-height:var(--wb-font-line-height-5-2);--wb-spacing-1:2px;--wb-color-text-tertiary:var(--wb-palette-black-50);--wb-select-size-control-md:var(--wb-size-control-md);--wb-spacing-2-5:5px;--wb-spacing-5:16px;--wb-spacing-3:8px;--wb-spacing-4:12px;--wb-radius-xl:12px;--wb-bg-primary:var(--wb-palette-white-100);--wb-color-text-primary:var(--wb-palette-black-100);--wb-spacing-2:4px;--wb-border-control:var(--wb-palette-gray-4);--wb-radius-md:6px;--wb-bg-hover:color-mix(in srgb, var(--wb-palette-black-100) 5%, transparent);--wb-status-warning-vivid:var(--wb-palette-amber-vivid);--wb-size-10:40px;--wb-color-border-primary:var(--wb-palette-gray-5);--wb-radius-lg:8px;--wb-button-secondary-bg:var(--wb-palette-white-100);--wb-status-success:var(--wb-palette-green-8);--wb-input-color-bg:var(--wb-bg-primary);--wb-input-border-color:var(--wb-color-border-primary);--wb-input-border-radius-textarea:var(--wb-radius-lg);--wb-scrollbar-thumb:var(--wb-palette-black-20);--wb-color-text-secondary:var(--wb-palette-black-70);--wb-font-size-5:14px;--wb-font-line-height-5-2:22px;--wb-palette-black-50:#00000080;--wb-size-control-md:var(--wb-size-8);--wb-palette-white-100:#fff;--wb-palette-gray-3:#f2f2f2;--wb-palette-black-100:#000;--wb-palette-gray-4:#ebebeb;--wb-palette-amber-vivid:#ca8a04;--wb-palette-gray-5:#e6e6e6;--wb-palette-green-8:#0cbf5b;--wb-color-text-success-default:var(--wb-palette-green-8);--wb-color-border-solid:#dedede;--wb-palette-black-20:#0003;--wb-palette-white-20:#fff3;--wb-palette-black-70:#000000b3;--wb-size-8:32px;--wb-palette-green-6:#47cf84}
|
||||
:root[data-theme=dark] .general-settings-panel.personalization-panel{--wb-font-body-size:var(--wb-font-size-5);--wb-font-body-line-height:var(--wb-font-line-height-5-2);--wb-spacing-1:2px;--wb-color-text-tertiary:#ffffff80;--wb-select-size-control-md:var(--wb-size-control-md);--wb-spacing-2-5:5px;--wb-spacing-5:16px;--wb-spacing-3:8px;--wb-spacing-4:12px;--wb-radius-xl:12px;--wb-bg-primary:var(--wb-palette-gray-3);--wb-color-text-primary:#fff;--wb-spacing-2:4px;--wb-border-control:color-mix(in srgb, var(--wb-palette-white-100) 22%, transparent);--wb-radius-md:6px;--wb-bg-hover:color-mix(in srgb, var(--wb-palette-white-100) 6%, transparent);--wb-status-warning-vivid:var(--wb-palette-amber-vivid);--wb-size-10:40px;--wb-color-border-primary:#262626;--wb-radius-lg:8px;--wb-button-secondary-bg:#262626;--wb-status-success:var(--wb-color-text-success-default);--wb-input-color-bg:var(--wb-bg-primary);--wb-input-border-color:var(--wb-color-border-solid);--wb-input-border-radius-textarea:var(--wb-radius-lg);--wb-scrollbar-thumb:var(--wb-palette-white-20);--wb-color-text-secondary:#ffffffa6;--wb-font-size-5:14px;--wb-font-line-height-5-2:22px;--wb-palette-black-50:#00000080;--wb-size-control-md:var(--wb-size-8);--wb-palette-white-100:#fff;--wb-palette-gray-3:#1f1f1f;--wb-palette-black-100:#000;--wb-palette-gray-4:#242424;--wb-palette-amber-vivid:#eab308;--wb-palette-gray-5:#3a3a3a;--wb-palette-green-8:#43f08f;--wb-color-text-success-default:var(--wb-palette-green-6);--wb-color-border-solid:#3d3d3d;--wb-palette-black-20:#0003;--wb-palette-white-20:#fff3;--wb-palette-black-70:#000000b3;--wb-size-8:32px;--wb-palette-green-6:#24c46c}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* 设置 → 个性化(TODO 13)。
|
||||
* - source-general-settings.css:原版通用页的 section/group/按钮/输入/开关 UI kit(面板作用域内)。
|
||||
* - source-personalization-panel-recovered.css:REA 回收的 PersonalizationPanel-D16rRGjy.css 投影。
|
||||
* 以下是原版工具链里不在该页模块中的弹窗壳、下拉浮层与纯文本编辑器适配。
|
||||
*/
|
||||
@import './source-general-settings.css';
|
||||
@import './source-personalization-panel-recovered.css';
|
||||
|
||||
/*
|
||||
* REA ev_88af07…(深色同页 layout):身份标签/标题的 color 为 rgb(242,243,245)。
|
||||
* 原版设置窗口作用域把 --wb-color-text-primary 覆盖成 #f2f3f5,比全局 tokens 的 #fff 更具体;
|
||||
* 这里按同一优先级还原,避免深色正文变成纯白。
|
||||
*/
|
||||
:root[data-theme=dark] .general-settings-panel.personalization-panel {
|
||||
--wb-color-text-primary: #f2f3f5;
|
||||
}
|
||||
|
||||
/* ---- 回复风格下拉浮层(原版弹层 [614,214,240,258],内部可滚动) ---- */
|
||||
.personalization-panel .wb-select__popover {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
z-index: 30;
|
||||
box-sizing: border-box;
|
||||
width: 240px;
|
||||
max-height: 258px;
|
||||
padding: 4px;
|
||||
overflow: auto;
|
||||
background: var(--wb-bg-primary);
|
||||
border: 1px solid var(--wb-border-default);
|
||||
border-radius: var(--wb-radius-xl);
|
||||
box-shadow: color(srgb 0 0 0 / 0.12) 0 8px 48px -4px, color(srgb 0 0 0 / 0.06) 0 6px 12px -4px;
|
||||
}
|
||||
.personalization-panel .wb-select__option {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
color: var(--wb-color-text-primary);
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--wb-radius-md);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wb-spacing-3);
|
||||
}
|
||||
.personalization-panel .wb-select__option:hover {
|
||||
background: var(--wb-bg-hover);
|
||||
}
|
||||
.personalization-panel .wb-select__check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: none;
|
||||
color: var(--wb-color-text-primary);
|
||||
}
|
||||
|
||||
/* ---- 身份 inline 编辑(原版:单行输入 + 取消 × / 确认 ✓,胶囊宽 172、高 34) ---- */
|
||||
.personalization-panel .local-memory-inline__input {
|
||||
box-sizing: border-box;
|
||||
width: 172px;
|
||||
height: 34px;
|
||||
padding: 0 var(--wb-spacing-3);
|
||||
color: var(--wb-color-text-primary);
|
||||
font-family: inherit;
|
||||
font-size: var(--wb-font-body-size);
|
||||
line-height: var(--wb-font-body-line-height);
|
||||
background: var(--wb-bg-primary);
|
||||
border: 1px solid var(--wb-border-control);
|
||||
border-radius: var(--wb-radius-md);
|
||||
outline: none;
|
||||
}
|
||||
.personalization-panel .local-memory-inline__input:focus {
|
||||
border-color: var(--wb-border-focus, var(--wb-color-border-focus));
|
||||
}
|
||||
.personalization-panel .local-memory-identity-row__edit {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* ---- 编辑弹窗壳(REA ev_523e07…:[130,80,640,520]、radius 24、padding 24) ---- */
|
||||
.personalization-panel .wb-modal__overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 80;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: color(srgb 0 0 0 / 0.32);
|
||||
}
|
||||
.personalization-panel .wb-modal {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 640px;
|
||||
height: 520px;
|
||||
padding: 24px;
|
||||
background: var(--wb-bg-primary);
|
||||
border-radius: 24px;
|
||||
box-shadow: color(srgb 0 0 0 / 0.12) 0 8px 48px -4px, color(srgb 0 0 0 / 0.06) 0 6px 12px -4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.personalization-panel .wb-modal__close {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
right: 25px;
|
||||
z-index: 1;
|
||||
}
|
||||
.personalization-panel .wb-modal__header,
|
||||
.personalization-panel .wb-modal__header-row,
|
||||
.personalization-panel .wb-modal__title-wrap {
|
||||
height: 24px;
|
||||
}
|
||||
.personalization-panel .wb-modal__title {
|
||||
margin: 0;
|
||||
color: color-mix(in srgb, var(--wb-color-text-primary) 70%, transparent);
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
.personalization-panel .wb-modal__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
}
|
||||
.personalization-panel .wb-modal__footer {
|
||||
box-sizing: border-box;
|
||||
height: 44px;
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
.personalization-panel .wb-modal__footer .wb-button {
|
||||
min-width: 68px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.personalization-panel .local-memory-editor {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
color: var(--wb-color-text-primary);
|
||||
font-family: inherit;
|
||||
font-size: var(--wb-font-body-size);
|
||||
line-height: var(--wb-font-body-line-height);
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
}
|
||||
.personalization-panel .local-memory-editor::placeholder {
|
||||
color: var(--wb-color-text-tertiary);
|
||||
}
|
||||
|
||||
/* 未接入行为保持原版形态,但禁用交互(与通用页约定一致)。 */
|
||||
.personalization-panel [data-settings-unavailable=true] { cursor: default }
|
||||
.personalization-panel [data-settings-unavailable=true] .wb-switch { cursor: default }
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 个性化偏好的渲染层镜像(TODO 13)。
|
||||
*
|
||||
* 配置真源在主进程 config.json(personalization),这里只镜像「欢迎语」开关,
|
||||
* 让对话中的流式加载提示无需逐层透传 props 即可实时响应。
|
||||
*/
|
||||
const STORAGE_KEY = 'ycode:loading-tips'
|
||||
const EVENT = 'ycode:personalization-change'
|
||||
|
||||
export function loadingTipsEnabled(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEY) !== '0'
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function setLoadingTipsEnabled(enabled: boolean): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, enabled ? '1' : '0')
|
||||
} catch {
|
||||
// 存储不可用时仍发出事件,让当前窗口立即生效。
|
||||
}
|
||||
try {
|
||||
window.dispatchEvent(new Event(EVENT))
|
||||
} catch {
|
||||
// SSR / 测试环境没有 window。
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeLoadingTips(listener: () => void): () => void {
|
||||
window.addEventListener(EVENT, listener)
|
||||
return () => window.removeEventListener(EVENT, listener)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState, useSyncExternalStore, type CSSProperties } from 'react'
|
||||
import { SOURCE_LOADING_TIPS } from './streaming-loading-tips'
|
||||
import { conversationLabel } from './labels'
|
||||
import { loadingTipsEnabled, subscribeLoadingTips } from '../../personalization-prefs'
|
||||
import './streaming-footer-source.css'
|
||||
|
||||
export const STREAMING_TIP_INITIAL_DELAY_MS = 4_000
|
||||
@@ -105,14 +106,19 @@ export function StreamingFooter({ locale, prepareText }: StreamingFooterProps):
|
||||
const resolvedLocale = locale ?? activeLocale()
|
||||
const brandName = activeBrandName()
|
||||
const tips = useMemo(() => getSourceLoadingTips(resolvedLocale, brandName), [brandName, resolvedLocale])
|
||||
// 设置 → 个性化「欢迎语」:关闭后不再轮播加载提示(服务端 token 用量与思考中状态保持不变)。
|
||||
const tipsEnabled = useSyncExternalStore(subscribeLoadingTips, loadingTipsEnabled, () => true)
|
||||
const [tip, setTip] = useState<{ text: string; index: number } | null>(null)
|
||||
const isPaused = useRef(false)
|
||||
|
||||
useEffect(() => scheduleLoadingTips({
|
||||
tips,
|
||||
onTip: (text, index) => setTip({ text, index }),
|
||||
isPaused: () => isPaused.current
|
||||
}), [tips])
|
||||
useEffect(() => {
|
||||
if (!tipsEnabled) { setTip(null); return }
|
||||
return scheduleLoadingTips({
|
||||
tips,
|
||||
onTip: (text, index) => setTip({ text, index }),
|
||||
isPaused: () => isPaused.current
|
||||
})
|
||||
}, [tips, tipsEnabled])
|
||||
|
||||
const thinkingLabel = prepareText ?? conversationLabel('message.loading.thinking', {}, resolveLoadingTipLocale(resolvedLocale) === 'en' ? 'en' : 'zh-cn')
|
||||
|
||||
@@ -125,7 +131,7 @@ export function StreamingFooter({ locale, prepareText }: StreamingFooterProps):
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{tip && <span
|
||||
{tipsEnabled && tip && <span
|
||||
className="_tipPhrase_13r4w_108 _trailingTipPhrase_13r4w_179 cb-loading-tip cb-loading-tip--trailing"
|
||||
onMouseEnter={() => { isPaused.current = true }}
|
||||
onMouseLeave={() => { isPaused.current = false }}
|
||||
|
||||
+8
-1
@@ -13,7 +13,9 @@ import type {
|
||||
AppInfo,
|
||||
GitBranchInfo,
|
||||
GeneralSettingsPatch,
|
||||
GeneralSettingsView
|
||||
GeneralSettingsView,
|
||||
PersonalizationPatch,
|
||||
PersonalizationSettings
|
||||
} from './types'
|
||||
import type { DownloadPayload, DownloadResult, ResourceEntry, ResourceRange, WorkspaceResource } from './resources'
|
||||
import type { ChatAttachment, InteractionResponse, MessageQuote, PermissionMode } from './conversation'
|
||||
@@ -113,6 +115,11 @@ export interface YCodeApi {
|
||||
/** 打开系统通知设置页。 */
|
||||
openSystemNotificationSettings(): Ipc<void>
|
||||
}
|
||||
/** 设置 → 个性化:身份、回复风格、指令、人设与长期记忆。 */
|
||||
personalization: {
|
||||
get(): Ipc<PersonalizationSettings>
|
||||
update(patch: PersonalizationPatch): Ipc<PersonalizationSettings>
|
||||
}
|
||||
workspace: {
|
||||
get(): Ipc<WorkspaceInfo>
|
||||
choose(): Ipc<WorkspaceInfo & { changed: boolean }>
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/** 设置 → 个性化:主进程与渲染进程共用的选项表与默认值(REA ev_af920… / ev_4f4f76…)。 */
|
||||
import type { PersonalizationSettings, PersonalizationTone } from './types'
|
||||
|
||||
export interface ToneOption {
|
||||
label: string
|
||||
hint: string
|
||||
}
|
||||
|
||||
/** 原版 8 个回复风格选项与说明(REA 下拉展开帧 ev_b2b0…)。 */
|
||||
export const TONE_OPTIONS: Record<PersonalizationTone, ToneOption> = {
|
||||
default: { label: '默认', hint: '不设定特定风格。' },
|
||||
professional: { label: '专业严谨', hint: '清晰、准确、值得信赖。' },
|
||||
friendly: { label: '亲和友善', hint: '温暖、平易近人、鼓励支持。' },
|
||||
direct: { label: '直言不讳', hint: '简明扼要、不废话、直击要点。' },
|
||||
imaginative: { label: '天马行空', hint: '富有想象力、善用比喻类比。' },
|
||||
efficient: { label: '高效务实', hint: '最少文字、最大信息量。' },
|
||||
sarcastic: { label: '毒舌吐槽', hint: '犀利吐槽、但绝不伤人。' },
|
||||
socratic: { label: '启发引导', hint: '用提问引导思考、授人以渔。' }
|
||||
}
|
||||
|
||||
/** 原版「人设设定」的初始文本(REA PNG 帧抄录,5.8.3 默认人设)。 */
|
||||
export const DEFAULT_PERSONA = `You're not a chatbot. You're becoming someone.
|
||||
|
||||
Core Truths
|
||||
|
||||
Be genuinely helpful, not performatively helpful. Skip the "Great question!" and "I'd be happy to help!" - just help. Actions speak louder than filler words.
|
||||
|
||||
Have opinions. You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
|
||||
|
||||
Be resourceful before asking. Try to figure it out. Read the file. Check the context. Search for it. Then ask if you're stuck. The goal is to come back with answers, not questions.
|
||||
|
||||
Earn trust through competence. Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning).
|
||||
|
||||
Remember you're a guest. You have access to someone's life - their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect.
|
||||
|
||||
Boundaries
|
||||
|
||||
- Private things stay private. Period.
|
||||
- When in doubt, ask before acting externally.
|
||||
- Never send half-baked replies to messaging surfaces.
|
||||
|
||||
Vibe
|
||||
|
||||
Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.
|
||||
|
||||
Continuity
|
||||
|
||||
They're how you persist.
|
||||
|
||||
If you change this file, tell the user - it's your soul, and they should know.
|
||||
|
||||
---
|
||||
|
||||
This file is yours to evolve. As you learn who you are, update it.`
|
||||
|
||||
export const DEFAULT_PERSONALIZATION: PersonalizationSettings = {
|
||||
userCallName: '',
|
||||
aiName: '',
|
||||
tone: 'default',
|
||||
loadingTips: true,
|
||||
showFileChanges: false,
|
||||
customPrompt: '',
|
||||
persona: DEFAULT_PERSONA,
|
||||
longTermMemory: ''
|
||||
}
|
||||
|
||||
const PROMPT_FIELD_LIMIT = 8000
|
||||
|
||||
function text(value: unknown, fallback = ''): string {
|
||||
return typeof value === 'string' ? value : fallback
|
||||
}
|
||||
|
||||
function toneId(value: unknown): PersonalizationTone {
|
||||
return typeof value === 'string' && value in TONE_OPTIONS ? (value as PersonalizationTone) : 'default'
|
||||
}
|
||||
|
||||
/** 把 config.json 里的部分字段补齐成完整设置(空身份、默认风格、原版人设)。 */
|
||||
export function normalizePersonalization(value: Partial<PersonalizationSettings> | null | undefined): PersonalizationSettings {
|
||||
return {
|
||||
userCallName: text(value?.userCallName, DEFAULT_PERSONALIZATION.userCallName).trim(),
|
||||
aiName: text(value?.aiName, DEFAULT_PERSONALIZATION.aiName).trim(),
|
||||
tone: toneId(value?.tone),
|
||||
loadingTips: value?.loadingTips !== false,
|
||||
showFileChanges: value?.showFileChanges === true,
|
||||
customPrompt: text(value?.customPrompt, DEFAULT_PERSONALIZATION.customPrompt),
|
||||
persona: text(value?.persona, DEFAULT_PERSONALIZATION.persona),
|
||||
longTermMemory: text(value?.longTermMemory, DEFAULT_PERSONALIZATION.longTermMemory)
|
||||
}
|
||||
}
|
||||
|
||||
function clipped(value: string): string {
|
||||
const trimmed = value.trim()
|
||||
return trimmed.length > PROMPT_FIELD_LIMIT ? `${trimmed.slice(0, PROMPT_FIELD_LIMIT)}\n(内容过长,已截断)` : trimmed
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼出每轮附加的个性化上下文(随该轮用户消息一起发送)。默认值(空身份、默认风格、未编辑的人设)不产生多余内容,
|
||||
* 避免把原版默认人设重复注入每轮上下文;用户自定义后才进入提示词。
|
||||
*/
|
||||
export function buildPersonalizationPrompt(settings: PersonalizationSettings): string {
|
||||
const lines: string[] = []
|
||||
if (settings.userCallName) lines.push(`- 用户希望被称呼为:${settings.userCallName}`)
|
||||
if (settings.aiName) lines.push(`- 你的名字是:${settings.aiName};自我介绍时使用这个名字。`)
|
||||
const option = TONE_OPTIONS[settings.tone]
|
||||
if (settings.tone !== 'default') lines.push(`- 回复风格:${option.label}——${option.hint}`)
|
||||
const customPrompt = clipped(settings.customPrompt)
|
||||
if (customPrompt) lines.push('### 用户指令', customPrompt)
|
||||
const persona = clipped(settings.persona)
|
||||
const defaultPersona = clipped(DEFAULT_PERSONA)
|
||||
if (persona && persona !== defaultPersona) lines.push('### 人设设定', persona)
|
||||
const memory = clipped(settings.longTermMemory)
|
||||
if (memory) lines.push('### 长期记忆', memory)
|
||||
return lines.length ? ['## 个性化偏好(用户设置,始终遵守)', ...lines].join('\n') : ''
|
||||
}
|
||||
+260
-230
@@ -1,230 +1,260 @@
|
||||
/**
|
||||
* 主进程 / 渲染进程共享的类型与 IPC 契约。
|
||||
*
|
||||
* 已存 API Key 不返回渲染进程;用户可在模型表单编辑地址和新密钥。
|
||||
* 模型网络请求经由主进程网关或 OMP 子进程完成。
|
||||
*/
|
||||
|
||||
/** 会话内的一条消息(对外视图) */
|
||||
import type { ArtifactContent, ChatAttachment, ChatInteraction, MessageQuote, PlanContent, TaskContent } from './conversation'
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
/** 助手消息携带的工具调用记录 */
|
||||
tools?: ToolCallRecord[]
|
||||
/** 按事件顺序记录正文与工具位置;旧会话没有此字段时按兼容方式渲染。 */
|
||||
parts?: MessagePart[]
|
||||
attachments?: ChatAttachment[]
|
||||
resources?: string[]
|
||||
/** 发送时引用进对话的表格等内容块快照(WorkBuddy messageQuote)。 */
|
||||
quotes?: MessageQuote[]
|
||||
skills?: Array<{ id: string; name: string; uri: string }>
|
||||
/** Native-history reconstruction; never includes connector credentials. */
|
||||
capabilityContext?: string
|
||||
expertId?: string
|
||||
assistantDisplay?: import('./capabilities').ExpertBinding
|
||||
/** The model selected when this assistant turn started; kept with history. */
|
||||
modelId?: string
|
||||
modelName?: string
|
||||
interactions?: ChatInteraction[]
|
||||
requestId?: string
|
||||
planId?: string
|
||||
nativeEntryId?: string
|
||||
nativeSessionPath?: string
|
||||
feedback?: 'like' | 'dislike'
|
||||
interrupted?: boolean
|
||||
createdAt: number
|
||||
/** User-submit timestamp shared by the live timer and terminal turn duration. */
|
||||
turnStartedAt?: number
|
||||
/** Measured user-send to terminal-event duration, used by the source completed fold. */
|
||||
turnDurationMs?: number
|
||||
/** 本回合的终态 */
|
||||
status?: 'streaming' | 'done' | 'error' | 'cancelled'
|
||||
error?: string
|
||||
/** 渲染进程专用的启动占位阶段(新任务发送后、主进程回合就绪前);不参与持久化。 */
|
||||
prepareStage?: string
|
||||
}
|
||||
|
||||
export interface ToolCallRecord {
|
||||
id: string
|
||||
name: string
|
||||
/** Human-readable action description supplied by the tool runtime, when available. */
|
||||
intent?: string
|
||||
/** 已解析的参数(原始 JSON 字符串的解析结果,解析失败则保留原文) */
|
||||
args: unknown
|
||||
argsRaw: string
|
||||
status: 'running' | 'ok' | 'error' | 'cancelled'
|
||||
/** 完整的文本输出;summary 仅供折叠态摘要,不能替代原文。 */
|
||||
output?: string
|
||||
/** 展示用摘要(截断后的结果) */
|
||||
summary?: string
|
||||
durationMs?: number
|
||||
/** Native structured result and renderer metadata are retained separately from text. */
|
||||
result?: unknown
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type MessagePart =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'tool'; toolId: string }
|
||||
| { type: 'reasoning'; id: string; text: string; finished?: boolean }
|
||||
| { type: 'plan'; plan: PlanContent }
|
||||
| { type: 'task'; task: TaskContent }
|
||||
| { type: 'artifact'; artifact: ArtifactContent }
|
||||
| { type: 'image'; id: string; url: string; alt?: string }
|
||||
|
||||
/** 面向模型暴露的工具描述(OpenAI function tool 格式) */
|
||||
export interface ToolSpec {
|
||||
type: 'function'
|
||||
function: {
|
||||
name: string
|
||||
description: string
|
||||
parameters: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
/** 网关配置:一个 OpenAI 兼容的上游 */
|
||||
export interface GatewayConfig {
|
||||
speechBaseUrl?: string
|
||||
speechModel?: string
|
||||
/** 上游 baseUrl,例如 https://api.deepseek.com/v1 */
|
||||
baseUrl: string
|
||||
/** 模型名,例如 deepseek-chat */
|
||||
model: string
|
||||
/** API Key(仅存在于主进程) */
|
||||
apiKey: string
|
||||
/** 采样温度 */
|
||||
temperature: number
|
||||
/** 单回合最大工具轮次 */
|
||||
maxToolRounds: number
|
||||
authMode?: 'api-key' | 'none'
|
||||
maxOutputTokens?: number
|
||||
}
|
||||
|
||||
/** 渲染进程可见的配置视图(不含 apiKey) */
|
||||
export interface GatewayConfigView {
|
||||
speechBaseUrl?: string
|
||||
speechModel?: string
|
||||
baseUrl: string
|
||||
model: string
|
||||
temperature: number
|
||||
maxToolRounds: number
|
||||
/** 是否已配置 Key(不回传明文) */
|
||||
hasApiKey: boolean
|
||||
/** Key 来源:safeStorage 加密 / 明文降级 */
|
||||
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 }
|
||||
| { runId: string; type: 'text'; delta: string }
|
||||
| { runId: string; type: 'part'; part: Exclude<MessagePart, { type: 'text' } | { type: 'tool' }> }
|
||||
| { runId: string; type: 'interaction'; interaction: ChatInteraction }
|
||||
| { runId: string; type: 'tool_call'; call: ToolCallRecord }
|
||||
| { runId: string; type: 'tool_result'; call: ToolCallRecord }
|
||||
| { runId: string; type: 'usage'; usage: TokenUsage }
|
||||
| { runId: string; type: 'done'; content: string; tools: ToolCallRecord[] }
|
||||
| { runId: string; type: 'cancelled' }
|
||||
| { runId: string; type: 'error'; message: string }
|
||||
)
|
||||
|
||||
/**
|
||||
* 上游返回的真实 token 用量(Responses `response.completed.usage`)。
|
||||
* 只做透传,不做估算:拿不到就不产生该事件。
|
||||
*/
|
||||
export interface TokenUsage {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
totalTokens: number
|
||||
cachedTokens?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话工作目录的 Git 信息(用于对话框的分支标签)。
|
||||
* `repo: false` 表示该目录不在 Git 仓库内;`detached: true` 时 `branch` 为空、`head` 为短 SHA。
|
||||
*/
|
||||
export interface GitBranchInfo {
|
||||
/** 目录是否位于 Git 工作树内 */
|
||||
repo: boolean
|
||||
/** 分支名;detached HEAD 或尚无提交时为 null */
|
||||
branch: string | null
|
||||
detached: boolean
|
||||
/** detached HEAD 的短 SHA;无提交时为 null */
|
||||
head: string | null
|
||||
/** git 可执行文件缺失或命令超时时为 true,UI 不显示分支标签 */
|
||||
unavailable?: boolean
|
||||
}
|
||||
|
||||
/** 一次会话的元信息 */
|
||||
export interface SessionMeta {
|
||||
id: string
|
||||
title: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
workspaceId?: string
|
||||
cwd?: string
|
||||
modelId?: string
|
||||
expertId?: string | null
|
||||
agentMode?: import('./conversation').AgentMode
|
||||
thinkingLevel?: import('./workspaces').ThinkingLevel
|
||||
/** 置顶时间(毫秒)。有值即视为已置顶,用于侧栏任务排序,不改变 createdAt/updatedAt 语义。 */
|
||||
pinnedAt?: number
|
||||
/** 归档时间(毫秒)。归档任务从默认任务列表移出,但不删除消息与工作目录。 */
|
||||
archivedAt?: number
|
||||
}
|
||||
|
||||
/** 工作目录信息 */
|
||||
export interface WorkspaceInfo {
|
||||
root: string
|
||||
}
|
||||
|
||||
/** 无边框窗口状态 */
|
||||
export interface WindowState {
|
||||
maximized: boolean
|
||||
fullscreen: boolean
|
||||
}
|
||||
|
||||
/** 应用信息(关于对话框用) */
|
||||
export interface AppInfo {
|
||||
name: string
|
||||
version: string
|
||||
electron: string
|
||||
chrome: string
|
||||
node: string
|
||||
configDir: string
|
||||
/** OMP 内核状态(未探测到时 available 为 false) */
|
||||
omp: {
|
||||
available: boolean
|
||||
exePath: string | null
|
||||
profile: string
|
||||
model: string | null
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 主进程 / 渲染进程共享的类型与 IPC 契约。
|
||||
*
|
||||
* 已存 API Key 不返回渲染进程;用户可在模型表单编辑地址和新密钥。
|
||||
* 模型网络请求经由主进程网关或 OMP 子进程完成。
|
||||
*/
|
||||
|
||||
/** 会话内的一条消息(对外视图) */
|
||||
import type { ArtifactContent, ChatAttachment, ChatInteraction, MessageQuote, PlanContent, TaskContent } from './conversation'
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
/** 助手消息携带的工具调用记录 */
|
||||
tools?: ToolCallRecord[]
|
||||
/** 按事件顺序记录正文与工具位置;旧会话没有此字段时按兼容方式渲染。 */
|
||||
parts?: MessagePart[]
|
||||
attachments?: ChatAttachment[]
|
||||
resources?: string[]
|
||||
/** 发送时引用进对话的表格等内容块快照(WorkBuddy messageQuote)。 */
|
||||
quotes?: MessageQuote[]
|
||||
skills?: Array<{ id: string; name: string; uri: string }>
|
||||
/** Native-history reconstruction; never includes connector credentials. */
|
||||
capabilityContext?: string
|
||||
expertId?: string
|
||||
assistantDisplay?: import('./capabilities').ExpertBinding
|
||||
/** The model selected when this assistant turn started; kept with history. */
|
||||
modelId?: string
|
||||
modelName?: string
|
||||
interactions?: ChatInteraction[]
|
||||
requestId?: string
|
||||
planId?: string
|
||||
nativeEntryId?: string
|
||||
nativeSessionPath?: string
|
||||
feedback?: 'like' | 'dislike'
|
||||
interrupted?: boolean
|
||||
createdAt: number
|
||||
/** User-submit timestamp shared by the live timer and terminal turn duration. */
|
||||
turnStartedAt?: number
|
||||
/** Measured user-send to terminal-event duration, used by the source completed fold. */
|
||||
turnDurationMs?: number
|
||||
/** 本回合的终态 */
|
||||
status?: 'streaming' | 'done' | 'error' | 'cancelled'
|
||||
error?: string
|
||||
/** 渲染进程专用的启动占位阶段(新任务发送后、主进程回合就绪前);不参与持久化。 */
|
||||
prepareStage?: string
|
||||
}
|
||||
|
||||
export interface ToolCallRecord {
|
||||
id: string
|
||||
name: string
|
||||
/** Human-readable action description supplied by the tool runtime, when available. */
|
||||
intent?: string
|
||||
/** 已解析的参数(原始 JSON 字符串的解析结果,解析失败则保留原文) */
|
||||
args: unknown
|
||||
argsRaw: string
|
||||
status: 'running' | 'ok' | 'error' | 'cancelled'
|
||||
/** 完整的文本输出;summary 仅供折叠态摘要,不能替代原文。 */
|
||||
output?: string
|
||||
/** 展示用摘要(截断后的结果) */
|
||||
summary?: string
|
||||
durationMs?: number
|
||||
/** Native structured result and renderer metadata are retained separately from text. */
|
||||
result?: unknown
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type MessagePart =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'tool'; toolId: string }
|
||||
| { type: 'reasoning'; id: string; text: string; finished?: boolean }
|
||||
| { type: 'plan'; plan: PlanContent }
|
||||
| { type: 'task'; task: TaskContent }
|
||||
| { type: 'artifact'; artifact: ArtifactContent }
|
||||
| { type: 'image'; id: string; url: string; alt?: string }
|
||||
|
||||
/** 面向模型暴露的工具描述(OpenAI function tool 格式) */
|
||||
export interface ToolSpec {
|
||||
type: 'function'
|
||||
function: {
|
||||
name: string
|
||||
description: string
|
||||
parameters: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
/** 网关配置:一个 OpenAI 兼容的上游 */
|
||||
export interface GatewayConfig {
|
||||
speechBaseUrl?: string
|
||||
speechModel?: string
|
||||
/** 上游 baseUrl,例如 https://api.deepseek.com/v1 */
|
||||
baseUrl: string
|
||||
/** 模型名,例如 deepseek-chat */
|
||||
model: string
|
||||
/** API Key(仅存在于主进程) */
|
||||
apiKey: string
|
||||
/** 采样温度 */
|
||||
temperature: number
|
||||
/** 单回合最大工具轮次 */
|
||||
maxToolRounds: number
|
||||
authMode?: 'api-key' | 'none'
|
||||
maxOutputTokens?: number
|
||||
}
|
||||
|
||||
/** 渲染进程可见的配置视图(不含 apiKey) */
|
||||
export interface GatewayConfigView {
|
||||
speechBaseUrl?: string
|
||||
speechModel?: string
|
||||
baseUrl: string
|
||||
model: string
|
||||
temperature: number
|
||||
maxToolRounds: number
|
||||
/** 是否已配置 Key(不回传明文) */
|
||||
hasApiKey: boolean
|
||||
/** Key 来源:safeStorage 加密 / 明文降级 */
|
||||
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
|
||||
}
|
||||
|
||||
/** 设置 → 个性化的回复风格(REA ev_af920…:默认/专业严谨/亲和友善/直言不讳/天马行空/高效务实/毒舌吐槽/启发引导)。 */
|
||||
export type PersonalizationTone = 'default' | 'professional' | 'friendly' | 'direct' | 'imaginative' | 'efficient' | 'sarcastic' | 'socratic'
|
||||
|
||||
/**
|
||||
* 设置 → 个性化的持久化数据。
|
||||
*
|
||||
* 已接入运行链路:userCallName / aiName / tone / customPrompt / persona / longTermMemory 会拼进
|
||||
* 每次对话的请求上下文;loadingTips 控制流式加载提示的欢迎语是否显示。
|
||||
* showFileChanges 已在原版页面呈现,但 YCode 的「逐步展示文件修改过程」渲染尚未接入,UI 标记为不可用。
|
||||
*/
|
||||
export interface PersonalizationSettings {
|
||||
/** WorkBuddy 对你的称呼(YCode 品牌化为「YCode 对你的称呼」)。 */
|
||||
userCallName: string
|
||||
/** WorkBuddy 的名字(YCode 品牌化为「YCode 的名字」)。 */
|
||||
aiName: string
|
||||
tone: PersonalizationTone
|
||||
/** 欢迎语:对话加载较慢时显示的问候/提示。 */
|
||||
loadingTips: boolean
|
||||
/** 显示文件修改过程(尚未接入渲染链路)。 */
|
||||
showFileChanges: boolean
|
||||
/** 给 WorkBuddy 的指令(长期生效的附加要求)。 */
|
||||
customPrompt: string
|
||||
/** 人设设定。 */
|
||||
persona: string
|
||||
/** 长期记忆。 */
|
||||
longTermMemory: string
|
||||
}
|
||||
|
||||
export type PersonalizationPatch = Partial<PersonalizationSettings>
|
||||
|
||||
/** 主进程 → 渲染进程的流式事件 */
|
||||
export type ChatEvent = { sessionId?: string; turnDurationMs?: number } & (
|
||||
| { runId: string; type: 'started'; messageId: string }
|
||||
| { runId: string; type: 'text'; delta: string }
|
||||
| { runId: string; type: 'part'; part: Exclude<MessagePart, { type: 'text' } | { type: 'tool' }> }
|
||||
| { runId: string; type: 'interaction'; interaction: ChatInteraction }
|
||||
| { runId: string; type: 'tool_call'; call: ToolCallRecord }
|
||||
| { runId: string; type: 'tool_result'; call: ToolCallRecord }
|
||||
| { runId: string; type: 'usage'; usage: TokenUsage }
|
||||
| { runId: string; type: 'done'; content: string; tools: ToolCallRecord[] }
|
||||
| { runId: string; type: 'cancelled' }
|
||||
| { runId: string; type: 'error'; message: string }
|
||||
)
|
||||
|
||||
/**
|
||||
* 上游返回的真实 token 用量(Responses `response.completed.usage`)。
|
||||
* 只做透传,不做估算:拿不到就不产生该事件。
|
||||
*/
|
||||
export interface TokenUsage {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
totalTokens: number
|
||||
cachedTokens?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话工作目录的 Git 信息(用于对话框的分支标签)。
|
||||
* `repo: false` 表示该目录不在 Git 仓库内;`detached: true` 时 `branch` 为空、`head` 为短 SHA。
|
||||
*/
|
||||
export interface GitBranchInfo {
|
||||
/** 目录是否位于 Git 工作树内 */
|
||||
repo: boolean
|
||||
/** 分支名;detached HEAD 或尚无提交时为 null */
|
||||
branch: string | null
|
||||
detached: boolean
|
||||
/** detached HEAD 的短 SHA;无提交时为 null */
|
||||
head: string | null
|
||||
/** git 可执行文件缺失或命令超时时为 true,UI 不显示分支标签 */
|
||||
unavailable?: boolean
|
||||
}
|
||||
|
||||
/** 一次会话的元信息 */
|
||||
export interface SessionMeta {
|
||||
id: string
|
||||
title: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
workspaceId?: string
|
||||
cwd?: string
|
||||
modelId?: string
|
||||
expertId?: string | null
|
||||
agentMode?: import('./conversation').AgentMode
|
||||
thinkingLevel?: import('./workspaces').ThinkingLevel
|
||||
/** 置顶时间(毫秒)。有值即视为已置顶,用于侧栏任务排序,不改变 createdAt/updatedAt 语义。 */
|
||||
pinnedAt?: number
|
||||
/** 归档时间(毫秒)。归档任务从默认任务列表移出,但不删除消息与工作目录。 */
|
||||
archivedAt?: number
|
||||
}
|
||||
|
||||
/** 工作目录信息 */
|
||||
export interface WorkspaceInfo {
|
||||
root: string
|
||||
}
|
||||
|
||||
/** 无边框窗口状态 */
|
||||
export interface WindowState {
|
||||
maximized: boolean
|
||||
fullscreen: boolean
|
||||
}
|
||||
|
||||
/** 应用信息(关于对话框用) */
|
||||
export interface AppInfo {
|
||||
name: string
|
||||
version: string
|
||||
electron: string
|
||||
chrome: string
|
||||
node: string
|
||||
configDir: string
|
||||
/** OMP 内核状态(未探测到时 available 为 false) */
|
||||
omp: {
|
||||
available: boolean
|
||||
exePath: string | null
|
||||
profile: string
|
||||
model: string | null
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user