wip: 主题系统与设置面板改动 checkpoint
This commit is contained in:
@@ -147,6 +147,11 @@ export function registerIpc(): AgentService {
|
|||||||
return pass(() => service.setSystemPrompt(text))
|
return pass(() => service.setSystemPrompt(text))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('settings:setTheme', (_event, id: unknown) => {
|
||||||
|
if (typeof id !== 'string' || !id.trim()) throw new Error('invalid theme id')
|
||||||
|
service.setThemeColor(id.trim())
|
||||||
|
})
|
||||||
|
|
||||||
// ---- 工作区 ----
|
// ---- 工作区 ----
|
||||||
ipcMain.handle('workspaces:list', () =>
|
ipcMain.handle('workspaces:list', () =>
|
||||||
service.listWorkspaces().catch((err: unknown) => {
|
service.listWorkspaces().catch((err: unknown) => {
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ export class AgentService {
|
|||||||
: undefined,
|
: undefined,
|
||||||
// 历史版本存过 autoEdit/plan,统一收敛回 confirm
|
// 历史版本存过 autoEdit/plan,统一收敛回 confirm
|
||||||
permissionMode:
|
permissionMode:
|
||||||
raw.permissionMode === 'full' ? 'full' : 'confirm'
|
raw.permissionMode === 'full' ? 'full' : 'confirm',
|
||||||
|
themeColor: typeof raw.themeColor === 'string' ? raw.themeColor : undefined
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
this.settings = { systemPrompt: '', permissionMode: this.permissionMode }
|
this.settings = { systemPrompt: '', permissionMode: this.permissionMode }
|
||||||
@@ -649,13 +650,14 @@ export class AgentService {
|
|||||||
|
|
||||||
async listSessions(): Promise<SessionMeta[]> {
|
async listSessions(): Promise<SessionMeta[]> {
|
||||||
const infos = await SessionManager.list(this.cwd)
|
const infos = await SessionManager.list(this.cwd)
|
||||||
|
// 空会话(一条消息都没发过)不上列表:第一句发出之后才在侧栏可见
|
||||||
const metas = infos
|
const metas = infos
|
||||||
|
.filter((info) => info.messageCount > 0)
|
||||||
.sort((a, b) => b.modified.getTime() - a.modified.getTime())
|
.sort((a, b) => b.modified.getTime() - a.modified.getTime())
|
||||||
.map((info) => toSessionMeta(info))
|
.map((info) => toSessionMeta(info))
|
||||||
// 新会话在首条消息写入前没有 .jsonl 文件,SessionManager.list 扫不到;
|
// 未落盘的活跃会话兜底合并进列表(同样要求已有消息)
|
||||||
// 把当前活跃会话兜底合并进列表,避免侧栏“新建后不出现”
|
|
||||||
const session = this.runtime?.session
|
const session = this.runtime?.session
|
||||||
if (session && !metas.some((m) => samePath(m.path, session.sessionFile ?? ''))) {
|
if (session && session.messages.length > 0 && !metas.some((m) => samePath(m.path, session.sessionFile ?? ''))) {
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
metas.unshift({
|
metas.unshift({
|
||||||
id: session.sessionId,
|
id: session.sessionId,
|
||||||
@@ -876,6 +878,12 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 主题色偏好(纯外观,仅持久化;渲染层负责应用) */
|
||||||
|
setThemeColor(id: string): void {
|
||||||
|
this.settings = { ...this.settings, themeColor: id }
|
||||||
|
this.saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
private async rebuildRuntime(): Promise<void> {
|
private async rebuildRuntime(): Promise<void> {
|
||||||
const old = this.runtime
|
const old = this.runtime
|
||||||
this.runtime = null
|
this.runtime = null
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const api = {
|
|||||||
logoutProvider: (providerId: string): Promise<void> => ipcRenderer.invoke('auth:logout', providerId),
|
logoutProvider: (providerId: string): Promise<void> => ipcRenderer.invoke('auth:logout', providerId),
|
||||||
getSettings: (): Promise<AppSettings> => ipcRenderer.invoke('settings:get'),
|
getSettings: (): Promise<AppSettings> => ipcRenderer.invoke('settings:get'),
|
||||||
setSystemPrompt: (text: string): Promise<void> => ipcRenderer.invoke('settings:setPrompt', text),
|
setSystemPrompt: (text: string): Promise<void> => ipcRenderer.invoke('settings:setPrompt', text),
|
||||||
|
setThemeColor: (id: string): Promise<void> => ipcRenderer.invoke('settings:setTheme', id),
|
||||||
resolveApproval: (id: string, approved: boolean): Promise<void> =>
|
resolveApproval: (id: string, approved: boolean): Promise<void> =>
|
||||||
ipcRenderer.invoke('approval:resolve', id, approved),
|
ipcRenderer.invoke('approval:resolve', id, approved),
|
||||||
onEvent: (callback: (event: AgentUiEvent) => void): (() => void) => {
|
onEvent: (callback: (event: AgentUiEvent) => void): (() => void) => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { BottomSlot } from './components/BottomSlot'
|
|||||||
import { Composer } from './components/Composer'
|
import { Composer } from './components/Composer'
|
||||||
import { SettingsModal } from './components/SettingsModal'
|
import { SettingsModal } from './components/SettingsModal'
|
||||||
import { Search } from './components/Search'
|
import { Search } from './components/Search'
|
||||||
|
import { applyThemeColor } from './lib/theme'
|
||||||
|
|
||||||
export default function App(): JSX.Element {
|
export default function App(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
@@ -31,6 +32,14 @@ export default function App(): JSX.Element {
|
|||||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||||
const [searchOpen, setSearchOpen] = useState(false)
|
const [searchOpen, setSearchOpen] = useState(false)
|
||||||
|
|
||||||
|
// 启动时应用持久化的主题色
|
||||||
|
useEffect(() => {
|
||||||
|
void window.api
|
||||||
|
.getSettings()
|
||||||
|
.then((s) => applyThemeColor(s.themeColor))
|
||||||
|
.catch(() => undefined)
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 全局快捷键:Ctrl+N 新会话 / Ctrl+K 搜索 / Ctrl+, 设置 / Esc 关闭弹窗
|
// 全局快捷键:Ctrl+N 新会话 / Ctrl+K 搜索 / Ctrl+, 设置 / Esc 关闭弹窗
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKey = (e: KeyboardEvent): void => {
|
const onKey = (e: KeyboardEvent): void => {
|
||||||
@@ -110,6 +119,8 @@ export default function App(): JSX.Element {
|
|||||||
disabled={!state.ready}
|
disabled={!state.ready}
|
||||||
model={state.sessionState?.model}
|
model={state.sessionState?.model}
|
||||||
permissionMode={state.sessionState?.permissionMode ?? 'confirm'}
|
permissionMode={state.sessionState?.permissionMode ?? 'confirm'}
|
||||||
|
sessionKey={state.sessionState?.sessionId}
|
||||||
|
sessionMessageCount={state.sessionState?.messageCount}
|
||||||
onSend={send}
|
onSend={send}
|
||||||
onStop={stop}
|
onStop={stop}
|
||||||
onNewSession={newSession}
|
onNewSession={newSession}
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ export function ApprovalCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="-mr-0.5 flex items-center gap-1.5">
|
<div className="-mr-0.5 flex items-center gap-1.5">
|
||||||
<Button variant="ghost" size="sm" onClick={() => settle(false, '已拒绝')}>
|
<Button variant="quiet" size="sm" onClick={() => settle(false, '已拒绝')}>
|
||||||
跳过
|
跳过
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -11,26 +11,19 @@ const SUGGESTIONS = [
|
|||||||
'查询最近一次上架任务的执行结果'
|
'查询最近一次上架任务的执行结果'
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 空状态:大标题 + 建议词(beautifului Streaming Text 的 follow-ups 行样式) */
|
/** 空状态:一行小标题 + 三条建议(纯文本行,点击填入输入框) */
|
||||||
function EmptyState({ onPick }: { onPick: (text: string) => void }): ReactNode {
|
function EmptyState({ onPick }: { onPick: (text: string) => void }): ReactNode {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-center px-8 text-center">
|
<div className="flex h-full flex-col items-center justify-center gap-4 px-8 text-center">
|
||||||
<h2 className="mb-2 text-[32px] font-semibold leading-normal text-ink">今天想做点什么?</h2>
|
<h2 className="text-[19px] font-medium text-accent-ink">今天想做点什么?</h2>
|
||||||
<p className="mb-8 max-w-md text-base leading-normal text-ink-3">
|
<div className="flex flex-col items-stretch">
|
||||||
描述你的目标,Agent 会按 SOP 调用工具完成;也可以随时切为手动逐步执行。
|
|
||||||
</p>
|
|
||||||
<div className="grid w-full max-w-md flex-col">
|
|
||||||
{SUGGESTIONS.map((s) => (
|
{SUGGESTIONS.map((s) => (
|
||||||
<button
|
<button
|
||||||
key={s}
|
key={s}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onPick(s)}
|
onClick={() => onPick(s)}
|
||||||
className="-mx-1.5 flex items-center gap-2 rounded-[7px] border-b border-line px-1.5 py-1.5 text-left text-[12.5px] text-ink transition-colors duration-100 hover:bg-hover-2"
|
className="rounded-control px-2 py-1 text-[13px] text-ink-3 transition-colors duration-100 hover:bg-hover-2 hover:text-ink-2"
|
||||||
>
|
>
|
||||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--ink-3)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
|
|
||||||
<path d="M9 10l-5 5 5 5" />
|
|
||||||
<path d="M20 4v7a4 4 0 0 1-4 4H4" />
|
|
||||||
</svg>
|
|
||||||
{s}
|
{s}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -23,6 +23,24 @@ interface LocalCommand {
|
|||||||
/** 图片附件大小上限(与主进程 pickAttachments 一致) */
|
/** 图片附件大小上限(与主进程 pickAttachments 一致) */
|
||||||
const MAX_IMAGE_BYTES = 8 * 1024 * 1024
|
const MAX_IMAGE_BYTES = 8 * 1024 * 1024
|
||||||
|
|
||||||
|
// ---- 输入草稿:未发送的输入实时保存;新建的空会话自动复用,发送后清除 ----
|
||||||
|
const DRAFT_KEY = 'aso-composer-draft'
|
||||||
|
function loadDraft(): string {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(DRAFT_KEY) ?? ''
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function saveDraft(text: string): void {
|
||||||
|
try {
|
||||||
|
if (text) localStorage.setItem(DRAFT_KEY, text)
|
||||||
|
else localStorage.removeItem(DRAFT_KEY)
|
||||||
|
} catch {
|
||||||
|
// 存储不可用时静默放弃(草稿是尽力而为)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 输入框锚定菜单:portal 到 body 固定定位(BottomSlot 的 overflow-hidden 会裁剪普通弹层) */
|
/** 输入框锚定菜单:portal 到 body 固定定位(BottomSlot 的 overflow-hidden 会裁剪普通弹层) */
|
||||||
function ComposerMenu({
|
function ComposerMenu({
|
||||||
anchor,
|
anchor,
|
||||||
@@ -64,6 +82,8 @@ export function Composer({
|
|||||||
disabled,
|
disabled,
|
||||||
model,
|
model,
|
||||||
permissionMode = 'confirm',
|
permissionMode = 'confirm',
|
||||||
|
sessionKey,
|
||||||
|
sessionMessageCount,
|
||||||
onSend,
|
onSend,
|
||||||
onStop,
|
onStop,
|
||||||
onNewSession,
|
onNewSession,
|
||||||
@@ -78,6 +98,10 @@ export function Composer({
|
|||||||
disabled: boolean
|
disabled: boolean
|
||||||
model?: ModelInfo
|
model?: ModelInfo
|
||||||
permissionMode?: PermissionMode
|
permissionMode?: PermissionMode
|
||||||
|
/** 当前会话 id(切换时触发草稿复用) */
|
||||||
|
sessionKey?: string
|
||||||
|
/** 当前会话消息数(0 = 新建未发言的空会话) */
|
||||||
|
sessionMessageCount?: number
|
||||||
onSend: (text: string, images?: { mimeType: string; data: string }[]) => void
|
onSend: (text: string, images?: { mimeType: string; data: string }[]) => void
|
||||||
onStop: () => void
|
onStop: () => void
|
||||||
onNewSession: () => void
|
onNewSession: () => void
|
||||||
@@ -358,6 +382,24 @@ export function Composer({
|
|||||||
}
|
}
|
||||||
}, [prefillRef])
|
}, [prefillRef])
|
||||||
|
|
||||||
|
// 草稿实时保存(发送后输入框清空,草稿随之清除)
|
||||||
|
useEffect(() => {
|
||||||
|
saveDraft(value)
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
// 切换到空(新建未发言)会话且输入为空时,复用上次未发送的草稿
|
||||||
|
const prevSessionRef = useRef(sessionKey)
|
||||||
|
useEffect(() => {
|
||||||
|
const prev = prevSessionRef.current
|
||||||
|
prevSessionRef.current = sessionKey
|
||||||
|
if (prev === sessionKey) return
|
||||||
|
if ((sessionMessageCount ?? 0) !== 0) return
|
||||||
|
if (value.trim()) return
|
||||||
|
const draft = loadDraft()
|
||||||
|
if (draft) setValue(draft)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [sessionKey, sessionMessageCount])
|
||||||
|
|
||||||
// 自适应高度
|
// 自适应高度
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = textareaRef.current
|
const el = textareaRef.current
|
||||||
|
|||||||
@@ -16,15 +16,18 @@ import {
|
|||||||
LuSettings2,
|
LuSettings2,
|
||||||
LuSparkles,
|
LuSparkles,
|
||||||
LuTerminal,
|
LuTerminal,
|
||||||
|
LuTextQuote,
|
||||||
LuWebhook,
|
LuWebhook,
|
||||||
LuX
|
LuX
|
||||||
} from 'react-icons/lu'
|
} from 'react-icons/lu'
|
||||||
import type { IconType } from 'react-icons'
|
import type { IconType } from 'react-icons'
|
||||||
import type { CustomProviderConfig, ModelCatalog, ModelMeta, ProviderCatalogEntry, ResourceDetails, SessionState } from '@shared/types'
|
import type { CustomProviderConfig, ModelCatalog, ModelMeta, ProviderCatalogEntry, ResourceDetails, SessionState } from '@shared/types'
|
||||||
|
import { THEME_PRESETS, applyThemeColor } from '../lib/theme'
|
||||||
|
|
||||||
type Tab =
|
type Tab =
|
||||||
| 'general'
|
| 'general'
|
||||||
| 'model'
|
| 'model'
|
||||||
|
| 'prompt'
|
||||||
| 'memory'
|
| 'memory'
|
||||||
| 'subagent'
|
| 'subagent'
|
||||||
| 'plugins'
|
| 'plugins'
|
||||||
@@ -39,7 +42,8 @@ const NAV: Array<{ group: string; items: Array<{ id: Tab; label: string; icon: I
|
|||||||
group: '基础设置',
|
group: '基础设置',
|
||||||
items: [
|
items: [
|
||||||
{ id: 'general', label: '常规', icon: LuSettings2 },
|
{ id: 'general', label: '常规', icon: LuSettings2 },
|
||||||
{ id: 'model', label: '模型设置', icon: LuCpu }
|
{ id: 'model', label: '模型设置', icon: LuCpu },
|
||||||
|
{ id: 'prompt', label: '系统提示词', icon: LuTextQuote }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -65,8 +69,9 @@ const PLACEHOLDER: Record<string, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TAB_META: Record<Tab, { title: string; desc: string }> = {
|
const TAB_META: Record<Tab, { title: string; desc: string }> = {
|
||||||
general: { title: '常规', desc: '管理 Agent 的系统提示词与基础行为。' },
|
general: { title: '常规', desc: '主题色等外观偏好。' },
|
||||||
model: { title: '模型设置', desc: '' },
|
model: { title: '模型设置', desc: '' },
|
||||||
|
prompt: { title: '系统提示词', desc: '覆盖 Agent 的系统提示词,保存后重建运行时。' },
|
||||||
memory: { title: '记忆', desc: PLACEHOLDER.memory },
|
memory: { title: '记忆', desc: PLACEHOLDER.memory },
|
||||||
subagent: { title: '子智能体', desc: PLACEHOLDER.subagent },
|
subagent: { title: '子智能体', desc: PLACEHOLDER.subagent },
|
||||||
plugins: { title: '插件', desc: PLACEHOLDER.plugins },
|
plugins: { title: '插件', desc: PLACEHOLDER.plugins },
|
||||||
@@ -618,6 +623,7 @@ export function SettingsModal({
|
|||||||
const [keyValue, setKeyValue] = useState('')
|
const [keyValue, setKeyValue] = useState('')
|
||||||
const [prompt, setPrompt] = useState('')
|
const [prompt, setPrompt] = useState('')
|
||||||
const [promptDirty, setPromptDirty] = useState(false)
|
const [promptDirty, setPromptDirty] = useState(false)
|
||||||
|
const [theme, setTheme] = useState('default')
|
||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
const [notice, setNotice] = useState<{ kind: 'ok' | 'err'; text: string } | null>(null)
|
const [notice, setNotice] = useState<{ kind: 'ok' | 'err'; text: string } | null>(null)
|
||||||
const [resources, setResources] = useState<ResourceDetails | null>(null)
|
const [resources, setResources] = useState<ResourceDetails | null>(null)
|
||||||
@@ -701,6 +707,7 @@ export function SettingsModal({
|
|||||||
.then((s) => {
|
.then((s) => {
|
||||||
setPrompt(s.systemPrompt)
|
setPrompt(s.systemPrompt)
|
||||||
setPromptDirty(false)
|
setPromptDirty(false)
|
||||||
|
setTheme(s.themeColor ?? 'default')
|
||||||
})
|
})
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
setNotice(null)
|
setNotice(null)
|
||||||
@@ -782,6 +789,36 @@ export function SettingsModal({
|
|||||||
|
|
||||||
<div ref={scrollRef} className="min-h-0 flex-1 overflow-y-auto px-6 pb-4">
|
<div ref={scrollRef} className="min-h-0 flex-1 overflow-y-auto px-6 pb-4">
|
||||||
{tab === 'general' && (
|
{tab === 'general' && (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-[12.5px] font-medium text-zinc-700">主题色</div>
|
||||||
|
<div className="mt-1 text-[11.5px] text-zinc-400">影响链接、选中态、焦点环等强调色,点击即时生效。</div>
|
||||||
|
<div className="mt-3 flex flex-wrap items-center gap-3">
|
||||||
|
{THEME_PRESETS.map((p) => (
|
||||||
|
<button
|
||||||
|
key={p.id}
|
||||||
|
type="button"
|
||||||
|
title={p.label}
|
||||||
|
aria-label={`主题色:${p.label}`}
|
||||||
|
onClick={() => {
|
||||||
|
setTheme(p.id)
|
||||||
|
applyThemeColor(p.id)
|
||||||
|
void window.api.setThemeColor(p.id).catch(() => undefined)
|
||||||
|
}}
|
||||||
|
className={`flex size-8 items-center justify-center rounded-full transition-transform hover:scale-105 ${
|
||||||
|
theme === p.id ? 'ring-2 ring-zinc-900 ring-offset-2' : 'ring-1 ring-zinc-200'
|
||||||
|
}`}
|
||||||
|
style={{ background: p.accent }}
|
||||||
|
>
|
||||||
|
{theme === p.id && <LuCheck size={14} className="text-white drop-shadow" />}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === 'prompt' && (
|
||||||
<div className="flex h-full min-h-0 flex-col">
|
<div className="flex h-full min-h-0 flex-col">
|
||||||
<textarea
|
<textarea
|
||||||
value={prompt}
|
value={prompt}
|
||||||
@@ -905,7 +942,7 @@ export function SettingsModal({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab !== 'general' && tab !== 'model' && tab !== 'skills' && tab !== 'commands' && (
|
{tab !== 'general' && tab !== 'model' && tab !== 'prompt' && tab !== 'skills' && tab !== 'commands' && (
|
||||||
<ComingSoon icon={NAV[1].items.find((i) => i.id === tab)?.icon ?? LuPuzzle} label={meta.title} />
|
<ComingSoon icon={NAV[1].items.find((i) => i.id === tab)?.icon ?? LuPuzzle} label={meta.title} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -311,7 +311,9 @@ export function useAgent(): UseAgent {
|
|||||||
dispatch({ type: 'event', event })
|
dispatch({ type: 'event', event })
|
||||||
// 会话集合可能在后台变化(新建/删除/重命名),切换后刷新列表;
|
// 会话集合可能在后台变化(新建/删除/重命名),切换后刷新列表;
|
||||||
// 每个回合结束(stats)也刷新一次,让新会话首条消息落盘后的元数据及时更新
|
// 每个回合结束(stats)也刷新一次,让新会话首条消息落盘后的元数据及时更新
|
||||||
if (event.type === 'session_switched' || event.type === 'stats') {
|
const firstUserLanded =
|
||||||
|
event.type === 'message_end' && readRole(event.message) === 'user'
|
||||||
|
if (event.type === 'session_switched' || event.type === 'stats' || firstUserLanded) {
|
||||||
void window.api
|
void window.api
|
||||||
.listSessions()
|
.listSessions()
|
||||||
.then(setSessions)
|
.then(setSessions)
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ declare global {
|
|||||||
logoutProvider: (providerId: string) => Promise<void>
|
logoutProvider: (providerId: string) => Promise<void>
|
||||||
getSettings: () => Promise<AppSettings>
|
getSettings: () => Promise<AppSettings>
|
||||||
setSystemPrompt: (text: string) => Promise<void>
|
setSystemPrompt: (text: string) => Promise<void>
|
||||||
|
setThemeColor: (id: string) => Promise<void>
|
||||||
resolveApproval: (id: string, approved: boolean) => Promise<void>
|
resolveApproval: (id: string, approved: boolean) => Promise<void>
|
||||||
onEvent: (callback: (event: AgentUiEvent) => void) => () => void
|
onEvent: (callback: (event: AgentUiEvent) => void) => () => void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 主题色(accent):运行时覆盖 :root 的 --accent / --accent-ink / --accent-tint 三个变量,
|
||||||
|
* Tailwind 的 bg-accent / text-accent-ink / bg-accent-tint 等全部随之生效。
|
||||||
|
* accent-ink 为加深版(保证链接/可交互文本 AA 对比度),tint 为低透明度选中底。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ThemePreset {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
accent: string
|
||||||
|
ink: string
|
||||||
|
tint: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const THEME_PRESETS: ThemePreset[] = [
|
||||||
|
{ id: 'default', label: '默认灰紫', accent: '#8e8ea0', ink: '#707084', tint: 'rgba(142, 142, 160, 0.12)' },
|
||||||
|
{ id: 'blue', label: '蓝', accent: '#5b8def', ink: '#3a6cd4', tint: 'rgba(91, 141, 239, 0.12)' },
|
||||||
|
{ id: 'green', label: '绿', accent: '#48a878', ink: '#2f7d57', tint: 'rgba(72, 168, 120, 0.13)' },
|
||||||
|
{ id: 'orange', label: '橙', accent: '#e08a3c', ink: '#b05f1a', tint: 'rgba(224, 138, 60, 0.14)' },
|
||||||
|
{ id: 'red', label: '红', accent: '#d95c5c', ink: '#ad3636', tint: 'rgba(217, 92, 92, 0.12)' },
|
||||||
|
{ id: 'purple', label: '紫', accent: '#8e7cd8', ink: '#6a56b8', tint: 'rgba(142, 124, 216, 0.13)' },
|
||||||
|
{ id: 'cyan', label: '青', accent: '#38a3a8', ink: '#257b80', tint: 'rgba(56, 163, 168, 0.12)' },
|
||||||
|
{ id: 'black', label: '墨黑', accent: '#3a3a3f', ink: '#17171a', tint: 'rgba(23, 23, 26, 0.08)' }
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 应用主题预设(undefined/未知 id = 恢复默认,移除内联覆盖) */
|
||||||
|
export function applyThemeColor(id: string | undefined): void {
|
||||||
|
const preset = THEME_PRESETS.find((p) => p.id === id)
|
||||||
|
const root = document.documentElement
|
||||||
|
if (!preset || preset.id === 'default') {
|
||||||
|
root.style.removeProperty('--accent')
|
||||||
|
root.style.removeProperty('--accent-ink')
|
||||||
|
root.style.removeProperty('--accent-tint')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
root.style.setProperty('--accent', preset.accent)
|
||||||
|
root.style.setProperty('--accent-ink', preset.ink)
|
||||||
|
root.style.setProperty('--accent-tint', preset.tint)
|
||||||
|
}
|
||||||
@@ -92,6 +92,8 @@ export interface AppSettings {
|
|||||||
customWorkspaces?: string[]
|
customWorkspaces?: string[]
|
||||||
/** 权限模式(缺省 confirm) */
|
/** 权限模式(缺省 confirm) */
|
||||||
permissionMode?: PermissionMode
|
permissionMode?: PermissionMode
|
||||||
|
/** 主题色预设 id(缺省 default,见 renderer lib/theme.ts) */
|
||||||
|
themeColor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 权限模式:confirm 变更前确认 / full 完全访问 */
|
/** 权限模式:confirm 变更前确认 / full 完全访问 */
|
||||||
|
|||||||
Reference in New Issue
Block a user