feat: 原生系统提示词只读展示、会话末尾 fork、深浅色主题

This commit is contained in:
Rick Ye
2026-09-09 03:16:13 +08:00
parent b9bb72ada9
commit bb0aedd27c
15 changed files with 376 additions and 219 deletions
+10 -2
View File
@@ -1,4 +1,4 @@
import { app, BrowserWindow, ipcMain, Menu, shell } from 'electron'
import { app, BrowserWindow, ipcMain, Menu, nativeTheme, shell } from 'electron'
import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { registerIpc } from './ipc'
@@ -32,13 +32,16 @@ function registerWindowControls(): void {
}
async function createWindow(): Promise<void> {
// 窗口底色跟随深浅色(加载/resize 时不闪白);构造函数里已按持久化偏好设置过
// nativeTheme.themeSource,这里用实际生效值
const dark = nativeTheme.shouldUseDarkColors
const win = new BrowserWindow({
width: 1280,
height: 840,
minWidth: 960,
minHeight: 640,
show: false,
backgroundColor: '#ffffff',
backgroundColor: dark ? '#212121' : '#ffffff',
title: 'Inkreach Agent',
// 窗口/任务栏图标(Windows/Linux;macOS 打包时由 build 配置提供 icns)
...(existsSync(APP_ICON) ? { icon: APP_ICON } : {}),
@@ -52,6 +55,11 @@ async function createWindow(): Promise<void> {
}
})
// 深浅色切换时同步窗口底色(避免暗色下 resize/加载闪白块)
nativeTheme.on('updated', () => {
win.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#212121' : '#ffffff')
})
// 最大化状态同步给渲染层(切换「最大化/还原」字形)
const sendWinState = (): void => win.webContents.send('win:state', win.isMaximized())
win.on('maximize', sendWinState)
+10 -4
View File
@@ -178,19 +178,25 @@ export function registerIpc(): AgentService {
)
})
// 会话末尾分叉为新会话(pi fork,原会话保留)
ipcMain.handle('session:fork', () => pass(() => service.forkSession()))
// ---- 应用设置 ----
ipcMain.handle('settings:get', () => service.getSettings())
ipcMain.handle('settings:setPrompt', (_event, text: unknown) => {
if (typeof text !== 'string') throw new Error('invalid prompt')
return pass(() => service.setSystemPrompt(text))
})
// 当前生效的系统提示词(pi 原生 + 技能注入,只读展示)
ipcMain.handle('settings:getSystemPrompt', () => service.getSystemPrompt())
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('settings:setThemeMode', (_event, mode: unknown) => {
if (mode !== 'light' && mode !== 'dark' && mode !== 'system') throw new Error('invalid theme mode')
service.setThemeMode(mode)
})
// ---- 工作区 ----
ipcMain.handle('workspaces:list', () =>
service.listWorkspaces().catch((err: unknown) => {
+45 -15
View File
@@ -1,4 +1,5 @@
import { app, dialog, shell } from 'electron'
import { nativeTheme } from 'electron'
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import {
@@ -39,6 +40,7 @@ import type {
SessionStatsInfo,
SkillDetail,
ThinkingLevel,
ThemeMode,
WorkspaceFile,
WorkspaceMeta
} from '@shared/types'
@@ -63,7 +65,7 @@ export class AgentService {
private flushTimer: NodeJS.Timeout | null = null
private attached: AgentSession | null = null
private unsubscribeAttached: (() => void) | null = null
private settings: AppSettings = { systemPrompt: '' }
private settings: AppSettings = {}
/** 当前工作区目录(agent cwd),会话按目录隔离 */
private workspaceDir = ''
private approvalsRegistered = false
@@ -73,6 +75,13 @@ export class AgentService {
private skillsEstimate: { cwd: string; value: number } | null = null
readonly approvals = new ApprovalService()
constructor() {
// 设置同步读入 + 应用持久化的深浅色偏好(nativeTheme 会影响渲染层
// prefers-color-scheme 与原生控件配色;窗口创建前生效可避免首帧闪错色)
this.loadSettings()
nativeTheme.themeSource = this.settings.themeMode ?? 'system'
}
/** 工作区根目录:文档/Inkreach,每个子目录 = 一个工作区 */
get workspacesRoot(): string {
return join(app.getPath('documents'), 'Inkreach')
@@ -94,7 +103,6 @@ export class AgentService {
try {
const raw = JSON.parse(readFileSync(this.settingsFile, 'utf8')) as Partial<AppSettings>
this.settings = {
systemPrompt: typeof raw.systemPrompt === 'string' ? raw.systemPrompt : '',
workspace: typeof raw.workspace === 'string' ? raw.workspace : undefined,
customWorkspaces: Array.isArray(raw.customWorkspaces)
? raw.customWorkspaces.filter((p): p is string => typeof p === 'string')
@@ -102,10 +110,11 @@ export class AgentService {
// 历史版本存过 autoEdit/plan,统一收敛回 confirm
permissionMode:
raw.permissionMode === 'full' ? 'full' : 'confirm',
themeColor: typeof raw.themeColor === 'string' ? raw.themeColor : undefined
themeColor: typeof raw.themeColor === 'string' ? raw.themeColor : undefined,
themeMode: raw.themeMode === 'light' || raw.themeMode === 'dark' ? raw.themeMode : 'system'
}
} catch {
this.settings = { systemPrompt: '', permissionMode: this.permissionMode }
this.settings = { permissionMode: this.permissionMode, themeMode: 'system' }
}
this.permissionMode = this.settings.permissionMode ?? 'confirm'
this.initWorkspaces()
@@ -258,7 +267,6 @@ export class AgentService {
cwd: targetCwd,
agentDir: targetAgentDir,
resourceLoaderOptions: {
...(this.settings.systemPrompt.trim() ? { systemPrompt: this.settings.systemPrompt } : {}),
extensionFactories: [
createApprovalExtension(this.approvals, TOOLS_REQUIRING_APPROVAL, () => this.permissionMode)
]
@@ -1054,17 +1062,45 @@ export class AgentService {
this.emitSwitched(session)
}
/** 在会话末尾分叉出新会话(pi fork:复制到新会话文件,原会话保留不动)。
* fork 后 runtime 重挂新会话,rebind 回调负责发 session_switched。 */
async forkSession(): Promise<void> {
const runtime = await this.getRuntime()
const session = runtime.session
if (session.isStreaming) {
await session.abort()
}
const leafId = session.sessionManager.getLeafId()
if (!leafId) {
throw new Error('会话还没有可分叉的内容')
}
const result = await runtime.fork(leafId, { position: 'at' })
if (result.cancelled) return
}
// ---- 应用设置 ----
getSettings(): AppSettings {
return { ...this.settings }
}
/** 更新系统提示词后重建运行时(continueRecent 重新挂载当前最近会话,历史保留) */
async setSystemPrompt(text: string): Promise<void> {
this.settings = { ...this.settings, systemPrompt: text }
/** 当前生效的系统提示词(pi 原生提示词 + 技能注入,只读展示用) */
async getSystemPrompt(): Promise<string> {
const runtime = await this.getRuntime()
return runtime.session.systemPrompt
}
/** 深浅色模式:写入 Electron nativeTheme(渲染层 prefers-color-scheme 随之变化)并持久化 */
setThemeMode(mode: ThemeMode): void {
this.settings = { ...this.settings, themeMode: mode }
this.saveSettings()
nativeTheme.themeSource = mode
}
/** 主题色偏好(纯外观,仅持久化;渲染层负责应用) */
setThemeColor(id: string): void {
this.settings = { ...this.settings, themeColor: id }
this.saveSettings()
await this.rebuildRuntime()
}
/** 切换权限模式:即时生效(审批扩展每笔工具调用实时读取),并广播给渲染层 */
@@ -1078,12 +1114,6 @@ export class AgentService {
}
}
/** 主题色偏好(纯外观,仅持久化;渲染层负责应用) */
setThemeColor(id: string): void {
this.settings = { ...this.settings, themeColor: id }
this.saveSettings()
}
private async rebuildRuntime(): Promise<void> {
const old = this.runtime
this.runtime = null
+4 -1
View File
@@ -18,6 +18,7 @@ import type {
SessionMeta,
SessionState,
ThinkingLevel,
ThemeMode,
WorkspaceFile,
WorkspaceMeta
} from '@shared/types'
@@ -76,9 +77,11 @@ const api = {
listForkPoints: (): Promise<ForkPoint[]> => ipcRenderer.invoke('session:forkPoints'),
navigateTree: (entryId: string, options?: NavigateTreeOptions): Promise<void> =>
ipcRenderer.invoke('session:navigate', entryId, options),
forkSession: (): Promise<void> => ipcRenderer.invoke('session:fork'),
getSettings: (): Promise<AppSettings> => ipcRenderer.invoke('settings:get'),
setSystemPrompt: (text: string): Promise<void> => ipcRenderer.invoke('settings:setPrompt', text),
getSystemPrompt: (): Promise<string> => ipcRenderer.invoke('settings:getSystemPrompt'),
setThemeColor: (id: string): Promise<void> => ipcRenderer.invoke('settings:setTheme', id),
setThemeMode: (mode: ThemeMode): Promise<void> => ipcRenderer.invoke('settings:setThemeMode', mode),
resolveApproval: (id: string, approved: boolean): Promise<void> =>
ipcRenderer.invoke('approval:resolve', id, approved),
onEvent: (callback: (event: AgentUiEvent) => void): (() => void) => {
+10 -3
View File
@@ -12,7 +12,7 @@ import { SettingsModal } from './components/SettingsModal'
import { Search } from './components/Search'
import { BrowserPanel } from './components/browser/BrowserPanel'
import { SidePanel, type PanelTabIcon } from './components/SidePanel'
import { applyThemeColor } from './lib/theme'
import { applyThemeColor, watchColorScheme } from './lib/theme'
// ---- 侧边面板(TabPanel):开合 / 标签 / 宽度,跨启动持久化 ----
@@ -63,6 +63,7 @@ export default function App(): JSX.Element {
workspaces,
forkPoints,
navigating,
forking,
send,
stop,
clearError,
@@ -80,7 +81,8 @@ export default function App(): JSX.Element {
abortCompaction,
setAutoCompaction,
dismissCompactionNotice,
navigateTree
navigateTree,
forkSession
} = useAgent()
const prefillRef = useRef<string | null>(null)
const [settingsOpen, setSettingsOpen] = useState(false)
@@ -133,12 +135,15 @@ export default function App(): JSX.Element {
}))
}, [])
// 启动时应用持久化的主题色
// 启动时应用持久化的主题色;深浅色由主进程 nativeTheme 决定 prefers-color-scheme,
// 这里监听并切换 .dark 类(设置里改模式 → nativeTheme 变化 → 媒体查询事件 → 生效)
useEffect(() => {
const stopWatching = watchColorScheme()
void window.api
.getSettings()
.then((s) => applyThemeColor(s.themeColor))
.catch(() => undefined)
return stopWatching
}, [])
// 全局快捷键:Ctrl+N 新会话 / Ctrl+K 搜索 / Ctrl+, 设置 / Esc 关闭弹窗
@@ -249,11 +254,13 @@ export default function App(): JSX.Element {
compacting={state.compacting}
compactionNotice={state.compactionNotice}
navigating={navigating}
forking={forking}
forkPoints={forkPoints}
onDismissError={clearError}
onDismissCompactionNotice={dismissCompactionNotice}
onAbortCompaction={abortCompaction}
onRewind={handleRewind}
onFork={forkSession}
onPickSuggestion={handlePickSuggestion}
onApplySelection={handleApplySelection}
/>
@@ -1,6 +1,6 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import type { ReactNode } from 'react'
import { LuFoldVertical, LuLoaderCircle, LuRotateCw, LuTriangleAlert, LuX } from 'react-icons/lu'
import { LuFoldVertical, LuGitFork, LuLoaderCircle, LuRotateCw, LuTriangleAlert, LuX } from 'react-icons/lu'
import type { ApprovalRequest, ForkPoint } from '@shared/types'
import type { CompactionNotice, ToolState, UiMessage } from '../hooks/useAgent'
import { formatTokens } from '../lib/utils'
@@ -147,11 +147,13 @@ export function ChatView({
compacting,
compactionNotice,
navigating,
forking,
forkPoints,
onDismissError,
onDismissCompactionNotice,
onAbortCompaction,
onRewind,
onFork,
onPickSuggestion,
onApplySelection
}: {
@@ -169,11 +171,15 @@ export function ChatView({
compactionNotice: CompactionNotice | null
/** 分支回溯/摘要生成进行中 */
navigating: boolean
/** 会话末尾分叉(pi fork)进行中 */
forking: boolean
forkPoints: ForkPoint[]
onDismissError: () => void
onDismissCompactionNotice: () => void
onAbortCompaction: () => void
onRewind: (entryId: string, summarize: boolean) => void
/** 在会话末尾分叉出新会话(原会话保留) */
onFork: () => void
onPickSuggestion: (text: string) => void
onApplySelection: (instruction: string, selection: string) => void
}): ReactNode {
@@ -374,6 +380,25 @@ export function ChatView({
countUserMessages(messages, 0, hidden),
hidden === 0
)}
{/* 会话结尾:从此处分叉为新会话(pi fork,原会话保留)。回合进行中不打断,收起入口 */}
{hasContent && !turnActive && !streaming && !compacting && !navigating && (
<div className="mt-1 flex">
<button
type="button"
disabled={forking}
onClick={onFork}
title="复制当前会话到新的会话文件,从这里继续探索(原会话保留不动)"
className="flex h-7 items-center gap-1.5 rounded-control px-2 text-[12px] text-ink-3 transition-colors hover:bg-hover hover:text-ink-2 disabled:cursor-default disabled:opacity-60"
>
{forking ? (
<LuLoaderCircle size={12} className="animate-spin" />
) : (
<LuGitFork size={12} className="rotate-180" />
)}
{forking ? '正在创建分支…' : '从此处分叉为新会话'}
</button>
</div>
)}
</div>
) : (
<div className="h-full">
@@ -23,6 +23,7 @@ import { createOnigurumaEngine } from 'shiki/engine/oniguruma'
import { bundledLanguages, type BundledLanguage } from 'shiki/langs'
import { bundledThemes } from 'shiki/themes'
import { CodeBlock, type ShikiLine } from './CodeBlock'
import { useDarkTheme } from '../lib/theme'
/**
* Markdown 渲染:基于 llm-ui(github.com/richardgill/llm-ui)做块级解析 ——
@@ -32,7 +33,8 @@ import { CodeBlock, type ShikiLine } from './CodeBlock'
* 代码块用 Shiki 做真实语法高亮(按需加载语言),保留本项目 CodeBlock 的行号/复制/diff 视图。
*/
/** Shiki 高亮器:github-light 主题 + 常用语言(grammar 在运行时按需动态加载) */
/** Shiki 高亮器:双主题注册(浅色/深色随 .dark 切换,切换时按块重新取词元),
* grammar 在运行时按需动态加载 */
const HIGHLIGHT_LANGS = [
'typescript',
'javascript',
@@ -63,7 +65,7 @@ const HIGHLIGHT_LANGS = [
]
const highlighterPromise: Promise<HighlighterCore> = createHighlighterCore({
themes: [bundledThemes['github-light']],
themes: [bundledThemes['github-light'], bundledThemes['github-dark']],
langs: HIGHLIGHT_LANGS.map((name) => bundledLanguages[name as BundledLanguage]),
engine: createOnigurumaEngine(import('shiki/wasm'))
})
@@ -220,6 +222,9 @@ const CodeBlockComponent: LLMOutputComponent = ({ blockMatch }) => {
const source = code ?? ''
const rawLang = (language ?? '').toLowerCase()
const shikiLang = LANG_ALIAS[rawLang] ?? (HIGHLIGHT_LANGS.includes(rawLang) ? rawLang : '')
// 主题随深浅色切换:切换时 effect 重跑,可见代码块重新取词元
const dark = useDarkTheme()
const shikiTheme = dark ? 'github-dark' : 'github-light'
const [tokens, setTokens] = useState<ShikiLine[] | null>(null)
useEffect(() => {
@@ -232,7 +237,7 @@ const CodeBlockComponent: LLMOutputComponent = ({ blockMatch }) => {
.then((highlighter) => {
if (!alive) return
try {
const result = highlighter.codeToTokens(source, { lang: shikiLang, theme: 'github-light' })
const result = highlighter.codeToTokens(source, { lang: shikiLang, theme: shikiTheme })
setTokens(result.tokens)
} catch {
setTokens(null)
@@ -242,7 +247,7 @@ const CodeBlockComponent: LLMOutputComponent = ({ blockMatch }) => {
return () => {
alive = false
}
}, [source, shikiLang])
}, [source, shikiLang, shikiTheme])
return (
<div className="my-2.5">
@@ -21,8 +21,8 @@ import {
LuX
} from 'react-icons/lu'
import type { IconType } from 'react-icons'
import type { CustomProviderConfig, ModelCatalog, ModelMeta, ProviderCatalogEntry, ResourceDetails, SessionState } from '@shared/types'
import { THEME_PRESETS, applyThemeColor } from '../lib/theme'
import type { CustomProviderConfig, ModelCatalog, ModelMeta, ProviderCatalogEntry, ResourceDetails, SessionState, ThemeMode } from '@shared/types'
import { THEME_MODES, THEME_PRESETS, applyThemeColor } from '../lib/theme'
type Tab =
| 'general'
@@ -71,7 +71,7 @@ const PLACEHOLDER: Record<string, string> = {
const TAB_META: Record<Tab, { title: string; desc: string }> = {
general: { title: '常规', desc: '主题色等外观偏好。' },
model: { title: '模型设置', desc: '' },
prompt: { title: '系统提示词', desc: '覆盖 Agent 的系统提示词,保存后重建运行时。' },
prompt: { title: '系统提示词', desc: '当前生效的 pi 原生系统提示词(含技能清单注入),只读。' },
memory: { title: '记忆', desc: PLACEHOLDER.memory },
subagent: { title: '子智能体', desc: PLACEHOLDER.subagent },
plugins: { title: '插件', desc: PLACEHOLDER.plugins },
@@ -88,7 +88,7 @@ function ProviderBadge({ provider }: { provider: ProviderCatalogEntry }): JSX.El
if (provider.isOAuth) {
return <span className="rounded-control bg-accent-tint px-1.5 py-0.5 text-[10px] text-accent-ink">OAuth</span>
}
return <span className="rounded-control bg-zinc-100 px-1.5 py-0.5 text-[10px] text-zinc-500">未配置</span>
return <span className="rounded-control bg-hover px-1.5 py-0.5 text-[10px] text-ink-3">未配置</span>
}
function formatContext(n: number): string {
@@ -99,12 +99,12 @@ function formatContext(n: number): string {
function ComingSoon({ icon: Icon, label }: { icon: IconType; label: string }): ReactNode {
return (
<div className="flex flex-col items-center justify-center gap-2 rounded-card border border-dashed border-zinc-200 py-16 text-center">
<span className="flex size-10 items-center justify-center rounded-full bg-zinc-100 text-zinc-400">
<div className="flex flex-col items-center justify-center gap-2 rounded-card border border-dashed border-line py-16 text-center">
<span className="flex size-10 items-center justify-center rounded-full bg-hover text-ink-3">
<Icon size={18} />
</span>
<div className="text-[13px] font-medium text-zinc-600">{label}</div>
<div className="text-[12px] text-zinc-400">该能力正在建设中,敬请期待</div>
<div className="text-[13px] font-medium text-ink-2">{label}</div>
<div className="text-[12px] text-ink-3">该能力正在建设中,敬请期待</div>
</div>
)
}
@@ -144,14 +144,14 @@ function ResourcePanel({
return (
<div className="flex min-h-0 flex-col">
<div className="flex items-center gap-2 pb-3">
<div className="flex items-center rounded-control bg-zinc-100 p-0.5">
<div className="flex items-center rounded-control bg-hover p-0.5">
{(['user', 'project'] as const).map((s) => (
<button
key={s}
type="button"
onClick={() => setScope(s)}
className={`h-6 rounded-[6px] px-2.5 text-[11.5px] transition-colors ${
scope === s ? 'bg-white font-medium text-zinc-900 shadow-sm' : 'text-zinc-500 hover:text-zinc-700'
scope === s ? 'bg-surface font-medium text-ink shadow-sm' : 'text-ink-3 hover:text-ink-2'
}`}
>
{s === 'user' ? '全局' : '项目'}
@@ -163,7 +163,7 @@ function ResourcePanel({
type="button"
title={dir}
onClick={() => onOpenDir(dir)}
className="ml-auto flex shrink-0 items-center gap-1 rounded-control px-1.5 py-1 text-[11.5px] text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
className="ml-auto flex shrink-0 items-center gap-1 rounded-control px-1.5 py-1 text-[11.5px] text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
>
<LuFolderOpen size={12} className="shrink-0" />
打开目录
@@ -172,9 +172,9 @@ function ResourcePanel({
</div>
{!loading && scoped.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-1 rounded-card border border-dashed border-zinc-200 py-12 text-center">
<div className="text-[13px] font-medium text-zinc-600">{emptyTitle}</div>
<div className="text-[12px] text-zinc-400">{hints[scope]}</div>
<div className="flex flex-col items-center justify-center gap-1 rounded-card border border-dashed border-line py-12 text-center">
<div className="text-[13px] font-medium text-ink-2">{emptyTitle}</div>
<div className="text-[12px] text-ink-3">{hints[scope]}</div>
</div>
) : (
<div className="space-y-1.5">
@@ -184,25 +184,25 @@ function ResourcePanel({
type="button"
title={`${item.filePath}(点击打开)`}
onClick={() => onOpen(item.filePath)}
className="block w-full rounded-card border border-zinc-200 px-3 py-2.5 text-left transition-colors hover:border-zinc-300 hover:bg-zinc-50"
className="block w-full rounded-card border border-line px-3 py-2.5 text-left transition-colors hover:border-line-strong hover:bg-inset"
>
<div className="flex items-center gap-2">
<span className="font-mono text-[12.5px] font-medium text-zinc-900">{item.title}</span>
<span className="font-mono text-[12.5px] font-medium text-ink">{item.title}</span>
{(item.badges ?? []).map((b) => (
<span
key={b.text}
className={`rounded-control px-1.5 py-0.5 text-[10px] ${
b.tone === 'accent' ? 'bg-accent-tint text-accent-ink' : 'bg-amber-50 text-amber-600'
b.tone === 'accent' ? 'bg-accent-tint text-accent-ink' : 'bg-orange-tint text-orange'
}`}
>
{b.text}
</span>
))}
<span className="ml-auto min-w-0 truncate font-mono text-[10.5px] text-zinc-400">
<span className="ml-auto min-w-0 truncate font-mono text-[10.5px] text-ink-3">
{item.hint || item.filePath}
</span>
</div>
{item.desc && <div className="mt-1 line-clamp-2 text-[12px] leading-5 text-zinc-500">{item.desc}</div>}
{item.desc && <div className="mt-1 line-clamp-2 text-[12px] leading-5 text-ink-3">{item.desc}</div>}
</button>
))}
</div>
@@ -235,7 +235,7 @@ const CUSTOM_API_OPTIONS: Array<{ value: string; label: string }> = [
]
const customInputCls =
'w-full rounded-control border border-zinc-300 bg-white px-2.5 py-1.5 text-[12px] text-zinc-900 outline-none placeholder:text-zinc-400 focus:border-zinc-500'
'w-full rounded-control border border-line-strong bg-surface px-2.5 py-1.5 text-[12px] text-ink outline-none placeholder:text-ink-3 focus:border-ink-3'
/** 模型设置:搜索/过滤 + @tanstack/react-virtual 虚拟列表(供应商多、模型上千时滚动不卡顿)。
* 卡片视觉按行拼装:provider 头行圆角上盖、模型行接左右边框、末行圆角下盖,行尾留组间距。
@@ -346,17 +346,17 @@ function ModelSettings({
switch (row.kind) {
case 'provider':
return (
<div className="rounded-t-[14px] border border-b-0 border-zinc-200 bg-white px-3 pb-2 pt-3">
<div className="rounded-t-[14px] border border-b-0 border-line bg-surface px-3 pb-2 pt-3">
<div className="flex items-center gap-2">
<span className="text-[12.5px] font-medium text-zinc-800">{row.provider.name}</span>
<span className="text-[12.5px] font-medium text-ink">{row.provider.name}</span>
<ProviderBadge provider={row.provider} />
{customProviders.some((c) => c.id === row.provider.id) && (
<span className="rounded-control bg-violet-100 px-1.5 py-0.5 text-[10px] text-violet-600">自定义</span>
<span className="rounded-control bg-accent-tint px-1.5 py-0.5 text-[10px] text-accent-ink">自定义</span>
)}
<button
type="button"
onClick={() => onEditKey(row.provider.id)}
className="ml-auto flex items-center gap-1 rounded-control px-1.5 py-0.5 text-[11px] text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
className="ml-auto flex items-center gap-1 rounded-control px-1.5 py-0.5 text-[11px] text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
>
<LuKeyRound size={11} />
{row.provider.hasApiKey ? '更换密钥' : '设置密钥'}
@@ -366,7 +366,7 @@ function ModelSettings({
type="button"
disabled={busy}
onClick={() => onLogout(row.provider.id)}
className="flex items-center gap-1 rounded-control px-1.5 py-0.5 text-[11px] text-zinc-500 transition-colors hover:bg-red-50 hover:text-red-600"
className="flex items-center gap-1 rounded-control px-1.5 py-0.5 text-[11px] text-ink-3 transition-colors hover:bg-red-tint hover:text-red"
>
<LuLogOut size={11} />
移除
@@ -377,7 +377,7 @@ function ModelSettings({
type="button"
disabled={busy}
onClick={() => onDeleteCustom(row.provider.id)}
className="flex items-center gap-1 rounded-control px-1.5 py-0.5 text-[11px] text-zinc-500 transition-colors hover:bg-red-50 hover:text-red-600"
className="flex items-center gap-1 rounded-control px-1.5 py-0.5 text-[11px] text-ink-3 transition-colors hover:bg-red-tint hover:text-red"
>
<LuX size={11} />
删除
@@ -388,20 +388,20 @@ function ModelSettings({
)
case 'key':
return (
<div className="border-x border-zinc-200 bg-zinc-50 px-3 py-2">
<div className="flex items-center gap-2 rounded-control border border-zinc-200 bg-white p-2">
<div className="border-x border-line bg-inset px-3 py-2">
<div className="flex items-center gap-2 rounded-control border border-line bg-surface p-2">
<input
type="password"
value={keyValue}
onChange={(e) => onKeyChange(e.target.value)}
placeholder="输入 API Key(保存到 ~/.pi/agent/auth.json,与 pi CLI 共享)"
className="flex-1 rounded-control border border-zinc-300 bg-white px-2.5 py-1.5 font-mono text-[12px] text-zinc-900 outline-none focus:border-zinc-500"
className="flex-1 rounded-control border border-line-strong bg-surface px-2.5 py-1.5 font-mono text-[12px] text-ink outline-none focus:border-ink-3"
/>
<button
type="button"
disabled={busy || !keyValue.trim()}
onClick={() => onSaveKey(row.provider.id)}
className="flex items-center gap-1 rounded-control bg-zinc-900 px-2.5 py-1.5 text-[12px] text-white transition-colors hover:bg-zinc-700 disabled:opacity-50"
className="flex items-center gap-1 rounded-control bg-ink px-2.5 py-1.5 text-[12px] text-page transition-opacity hover:opacity-90 disabled:opacity-50"
>
<LuCheck size={12} />
保存
@@ -409,7 +409,7 @@ function ModelSettings({
<button
type="button"
onClick={onKeyCancel}
className="rounded-control px-2 py-1.5 text-[12px] text-zinc-500 hover:bg-zinc-100"
className="rounded-control px-2 py-1.5 text-[12px] text-ink-3 hover:bg-hover"
>
取消
</button>
@@ -419,25 +419,25 @@ function ModelSettings({
case 'model': {
const selected = row.provider.id === currentProvider && row.model.id === currentModel
return (
<div className={`border-zinc-200 px-1.5 ${row.last ? 'border-x border-b pb-1.5' : 'border-x'}`}>
<div className={`border-line px-1.5 ${row.last ? 'border-x border-b pb-1.5' : 'border-x'}`}>
<button
type="button"
disabled={busy}
onClick={() => onSelectModel(row.provider.id, row.model)}
className={`flex w-full items-center gap-2 rounded-control px-2 py-1.5 text-left transition-colors ${
selected ? 'bg-accent-tint ring-1 ring-accent' : 'hover:bg-zinc-100'
selected ? 'bg-accent-tint ring-1 ring-accent' : 'hover:bg-hover'
}`}
>
{selected ? (
<LuCheck size={13} className="shrink-0 text-accent-ink" />
) : (
<span className="h-[13px] w-[13px] shrink-0 rounded-full border border-zinc-300" />
<span className="h-[13px] w-[13px] shrink-0 rounded-full border border-line-strong" />
)}
<span className={`text-[12.5px] ${selected ? 'font-medium text-zinc-900' : 'text-zinc-700'}`}>
<span className={`text-[12.5px] ${selected ? 'font-medium text-ink' : 'text-ink-2'}`}>
{row.model.name}
</span>
<span className="font-mono text-[10.5px] text-zinc-400">{row.model.id}</span>
<span className="ml-auto flex items-center gap-2 text-[10.5px] text-zinc-400">
<span className="font-mono text-[10.5px] text-ink-3">{row.model.id}</span>
<span className="ml-auto flex items-center gap-2 text-[10.5px] text-ink-3">
{row.model.reasoning && <span>推理</span>}
<span>{formatContext(row.model.contextWindow)} ctx</span>
</span>
@@ -447,7 +447,7 @@ function ModelSettings({
}
case 'empty':
return (
<div className="rounded-b-[14px] border border-t-0 border-zinc-200 px-3 pb-1.5 pt-0.5 text-[11.5px] text-zinc-400">
<div className="rounded-b-[14px] border border-t-0 border-line px-3 pb-1.5 pt-0.5 text-[11.5px] text-ink-3">
配置密钥后显示可用模型
</div>
)
@@ -458,16 +458,16 @@ function ModelSettings({
<div className="flex min-h-0 flex-col">
<div className="flex items-center gap-2 pb-3">
<div className="relative flex-1">
<LuSearch size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-zinc-400" />
<LuSearch size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-ink-3" />
<input
value={search}
onChange={(e) => onSearch(e.target.value)}
placeholder="搜索模型或提供商"
className="w-full rounded-control border border-zinc-300 bg-white py-1.5 pl-8 pr-3 text-[12.5px] text-zinc-900 outline-none placeholder:text-zinc-400 focus:border-zinc-500"
className="w-full rounded-control border border-line-strong bg-surface py-1.5 pl-8 pr-3 text-[12.5px] text-ink outline-none placeholder:text-ink-3 focus:border-ink-3"
/>
</div>
{loading && <span className="text-[11px] text-zinc-400">加载中…</span>}
<label className="flex shrink-0 cursor-pointer select-none items-center gap-1.5 text-[11.5px] text-zinc-500 hover:text-zinc-700">
{loading && <span className="text-[11px] text-ink-3">加载中…</span>}
<label className="flex shrink-0 cursor-pointer select-none items-center gap-1.5 text-[11.5px] text-ink-3 hover:text-ink-2">
<input
type="checkbox"
checked={onlyConfigured}
@@ -481,7 +481,7 @@ function ModelSettings({
onClick={() =>
setCustomForm({ id: '', name: '', baseUrl: '', apiKey: '', api: 'openai-completions', modelsText: '' })
}
className="flex shrink-0 items-center gap-1 rounded-control border border-zinc-200 bg-white px-2 py-1 text-[11.5px] text-zinc-600 transition-colors hover:border-zinc-300 hover:bg-zinc-50 hover:text-zinc-800"
className="flex shrink-0 items-center gap-1 rounded-control border border-line bg-surface px-2 py-1 text-[11.5px] text-ink-2 transition-colors hover:border-line-strong hover:bg-inset hover:text-ink"
>
<LuPlus size={12} />
自定义 API
@@ -489,10 +489,10 @@ function ModelSettings({
</div>
{customForm && (
<div className="mb-3 rounded-card border border-zinc-200 bg-zinc-50/70 p-3">
<div className="mb-3 rounded-card border border-line bg-inset/70 p-3">
<div className="grid grid-cols-2 gap-2">
<label className="flex flex-col gap-1">
<span className="text-[11px] text-zinc-500">ID(唯一标识)</span>
<span className="text-[11px] text-ink-3">ID(唯一标识)</span>
<input
value={customForm.id}
onChange={(e) => setCustomForm({ ...customForm, id: e.target.value })}
@@ -502,7 +502,7 @@ function ModelSettings({
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-[11px] text-zinc-500">显示名称(可空)</span>
<span className="text-[11px] text-ink-3">显示名称(可空)</span>
<input
value={customForm.name}
onChange={(e) => setCustomForm({ ...customForm, name: e.target.value })}
@@ -511,7 +511,7 @@ function ModelSettings({
/>
</label>
<label className="col-span-2 flex flex-col gap-1">
<span className="text-[11px] text-zinc-500">API 地址</span>
<span className="text-[11px] text-ink-3">API 地址</span>
<input
value={customForm.baseUrl}
onChange={(e) => setCustomForm({ ...customForm, baseUrl: e.target.value })}
@@ -521,7 +521,7 @@ function ModelSettings({
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-[11px] text-zinc-500">API Key</span>
<span className="text-[11px] text-ink-3">API Key</span>
<input
type="password"
value={customForm.apiKey}
@@ -531,7 +531,7 @@ function ModelSettings({
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-[11px] text-zinc-500">协议</span>
<span className="text-[11px] text-ink-3">协议</span>
<select
value={customForm.api}
onChange={(e) => setCustomForm({ ...customForm, api: e.target.value })}
@@ -545,7 +545,7 @@ function ModelSettings({
</select>
</label>
<label className="col-span-2 flex flex-col gap-1">
<span className="text-[11px] text-zinc-500">模型 ID(逗号或换行分隔)</span>
<span className="text-[11px] text-ink-3">模型 ID(逗号或换行分隔)</span>
<textarea
value={customForm.modelsText}
onChange={(e) => setCustomForm({ ...customForm, modelsText: e.target.value })}
@@ -560,7 +560,7 @@ function ModelSettings({
<button
type="button"
onClick={() => setCustomForm(null)}
className="rounded-control px-2.5 py-1.5 text-[12px] text-zinc-500 transition-colors hover:bg-zinc-100"
className="rounded-control px-2.5 py-1.5 text-[12px] text-ink-3 transition-colors hover:bg-hover"
>
取消
</button>
@@ -568,7 +568,7 @@ function ModelSettings({
type="button"
disabled={busy || !customForm.id.trim() || !customForm.baseUrl.trim() || !customForm.apiKey.trim()}
onClick={submitCustom}
className="flex items-center gap-1 rounded-control bg-zinc-900 px-3 py-1.5 text-[12px] text-white transition-colors hover:bg-zinc-700 disabled:cursor-not-allowed disabled:opacity-50"
className="flex items-center gap-1 rounded-control bg-ink px-3 py-1.5 text-[12px] text-page transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
>
<LuCheck size={12} />
保存
@@ -578,7 +578,7 @@ function ModelSettings({
)}
{rows.length === 0 ? (
<div className="py-10 text-center text-[12.5px] text-zinc-400">
<div className="py-10 text-center text-[12.5px] text-ink-3">
{loading ? '加载中…' : '没有匹配的模型'}
</div>
) : (
@@ -622,8 +622,9 @@ export function SettingsModal({
const [keyProvider, setKeyProvider] = useState<string | null>(null)
const [keyValue, setKeyValue] = useState('')
const [prompt, setPrompt] = useState('')
const [promptDirty, setPromptDirty] = useState(false)
const [promptLoading, setPromptLoading] = useState(false)
const [theme, setTheme] = useState('default')
const [themeMode, setThemeMode] = useState<ThemeMode>('system')
const [busy, setBusy] = useState(false)
const [notice, setNotice] = useState<{ kind: 'ok' | 'err'; text: string } | null>(null)
const [resources, setResources] = useState<ResourceDetails | null>(null)
@@ -705,15 +706,25 @@ export function SettingsModal({
void window.api
.getSettings()
.then((s) => {
setPrompt(s.systemPrompt)
setPromptDirty(false)
setTheme(s.themeColor ?? 'default')
setThemeMode(s.themeMode ?? 'system')
})
.catch(() => undefined)
setNotice(null)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])
// 系统提示词页:进入时读取当前生效的 pi 原生提示词(只读)
useEffect(() => {
if (!open || tab !== 'prompt') return
setPromptLoading(true)
window.api
.getSystemPrompt()
.then(setPrompt)
.catch(() => setPrompt(''))
.finally(() => setPromptLoading(false))
}, [open, tab])
// 技能 / 命令页:进入时加载详情
useEffect(() => {
if (!open || (tab !== 'skills' && tab !== 'commands')) return
@@ -743,15 +754,15 @@ export function SettingsModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/25 backdrop-blur-[2px]" onClick={onClose}>
<div
className="flex h-[640px] w-[920px] max-w-[94vw] overflow-hidden rounded-card border border-zinc-200 bg-white shadow-overlay"
className="flex h-[640px] w-[920px] max-w-[94vw] overflow-hidden rounded-card border border-line bg-surface shadow-overlay"
style={{ animation: 'pop-in 200ms cubic-bezier(0.23,1,0.32,1) both' }}
onClick={(e) => e.stopPropagation()}
>
{/* 左侧:分组导航 */}
<nav className="w-[196px] shrink-0 overflow-y-auto border-r border-zinc-200 bg-zinc-50/70 p-2.5">
<nav className="w-[196px] shrink-0 overflow-y-auto border-r border-line bg-inset/70 p-2.5">
{NAV.map((group) => (
<div key={group.group} className="mb-2">
<div className="px-2 pb-1 pt-2 text-[11px] font-medium text-zinc-400">{group.group}</div>
<div className="px-2 pb-1 pt-2 text-[11px] font-medium text-ink-3">{group.group}</div>
{group.items.map(({ id, label, icon: Icon }) => (
<button
key={id}
@@ -759,11 +770,11 @@ export function SettingsModal({
onClick={() => setTab(id)}
className={`flex h-8 w-full items-center gap-2 rounded-control px-2 text-left text-[12.5px] transition-colors ${
tab === id
? 'bg-zinc-200/80 font-medium text-zinc-900'
: 'text-zinc-600 hover:bg-zinc-100 hover:text-zinc-800'
? 'bg-hover-2/80 font-medium text-ink'
: 'text-ink-2 hover:bg-hover hover:text-ink'
}`}
>
<Icon size={14} className={tab === id ? 'text-zinc-700' : 'text-zinc-400'} />
<Icon size={14} className={tab === id ? 'text-ink-2' : 'text-ink-3'} />
{label}
</button>
))}
@@ -775,13 +786,13 @@ export function SettingsModal({
<div className="flex min-w-0 flex-1 flex-col">
<div className="flex items-start gap-3 px-6 pb-3 pt-5">
<div className="min-w-0 flex-1">
<div className="text-[16px] font-semibold text-zinc-900">{meta.title}</div>
{meta.desc && <div className="mt-0.5 text-[12px] leading-5 text-zinc-500">{meta.desc}</div>}
<div className="text-[16px] font-semibold text-ink">{meta.title}</div>
{meta.desc && <div className="mt-0.5 text-[12px] leading-5 text-ink-3">{meta.desc}</div>}
</div>
<button
type="button"
onClick={onClose}
className="rounded-control p-1.5 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
className="rounded-control p-1.5 text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
>
<LuX size={16} />
</button>
@@ -791,8 +802,29 @@ export function SettingsModal({
{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="text-[12.5px] font-medium text-ink-2">外观</div>
<div className="mt-1 text-[11.5px] text-ink-3">深浅色模式;跟随系统时随系统设置自动切换,即时生效。</div>
<div className="mt-2.5 flex w-fit items-center rounded-control border border-line bg-hover p-0.5">
{THEME_MODES.map((m) => (
<button
key={m.id}
type="button"
onClick={() => {
setThemeMode(m.id)
void window.api.setThemeMode(m.id).catch(() => undefined)
}}
className={`h-7 rounded-[6px] px-3 text-[12px] transition-colors ${
themeMode === m.id ? 'bg-surface font-medium text-ink shadow-card' : 'text-ink-3 hover:text-ink-2'
}`}
>
{m.label}
</button>
))}
</div>
</div>
<div>
<div className="text-[12.5px] font-medium text-ink-2">主题色</div>
<div className="mt-1 text-[11.5px] text-ink-3">影响链接、选中态、焦点环等强调色,点击即时生效。</div>
<div className="mt-3 flex flex-wrap items-center gap-3">
{THEME_PRESETS.map((p) => (
<button
@@ -806,7 +838,9 @@ export function SettingsModal({
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'
theme === p.id
? 'ring-2 ring-ink ring-offset-2 ring-offset-surface'
: 'ring-1 ring-line-strong'
}`}
style={{ background: p.accent }}
>
@@ -820,34 +854,8 @@ export function SettingsModal({
{tab === 'prompt' && (
<div className="flex h-full min-h-0 flex-col">
<textarea
value={prompt}
onChange={(e) => {
setPrompt(e.target.value)
setPromptDirty(true)
}}
spellCheck={false}
placeholder={'例如:你是店铺运营助手,负责热点词分析、商品文案生成与上架流程编排…'}
className="min-h-0 flex-1 resize-none rounded-card border border-zinc-300 bg-white p-3.5 font-mono text-[12.5px] leading-6 text-zinc-900 outline-none placeholder:text-zinc-400 focus:border-zinc-500"
/>
<div className="mt-3 flex items-center justify-end">
<button
type="button"
disabled={busy || !promptDirty}
onClick={() => {
if (streaming) {
setNotice({ kind: 'err', text: 'Agent 正在回复中,请先停止或等待完成后再保存(保存会重建运行时)' })
return
}
run('系统提示词已保存,运行时已重建', () => window.api.setSystemPrompt(prompt), () =>
setPromptDirty(false)
)
}}
className="flex items-center gap-1.5 rounded-control bg-zinc-900 px-3.5 py-1.5 text-[12.5px] text-white transition-colors hover:bg-zinc-700 disabled:cursor-not-allowed disabled:opacity-50"
>
<LuCheck size={13} />
保存并应用
</button>
<div className="min-h-0 flex-1 overflow-y-auto whitespace-pre-wrap rounded-card border border-line bg-inset p-3.5 font-mono text-[12px] leading-6 text-ink-2">
{promptLoading ? '载入中…' : prompt || '(空)'}
</div>
</div>
)}
@@ -953,7 +961,7 @@ export function SettingsModal({
className={`border-t px-6 py-2 text-[12px] ${
notice.kind === 'ok'
? 'border-green bg-green-tint text-green'
: 'border-red-200 bg-red-50 text-red-700'
: 'border-red/40 bg-red-tint text-red'
}`}
>
{notice.text}
@@ -111,7 +111,7 @@ export function SidePanel({
return (
<aside
className="relative flex h-full shrink-0 flex-col border-l border-zinc-200 bg-zinc-50"
className="relative flex h-full shrink-0 flex-col border-l border-line bg-inset"
style={{ width }}
aria-label="侧边面板"
>
@@ -124,7 +124,7 @@ export function SidePanel({
/>
{tabs.length > 0 && (
<div className="relative flex h-8 shrink-0 items-center gap-1 border-b border-zinc-200 bg-white px-1.5">
<div className="relative flex h-8 shrink-0 items-center gap-1 border-b border-line bg-surface px-1.5">
{/* 面板 tag 本身即下拉按钮(与网页 tab 条的 ˅ 列表按钮形态区分) */}
<div className="relative shrink-0">
<button
@@ -142,7 +142,7 @@ export function SidePanel({
className={`flex h-7 items-center gap-1.5 rounded-md border px-2 text-[12px] transition-colors ${
listOpen
? 'border-accent/50 bg-accent-tint/40 text-accent-ink'
: 'border-zinc-200 bg-white text-zinc-600 hover:border-accent/40 hover:text-zinc-800'
: 'border-line bg-surface text-ink-2 hover:border-accent/40 hover:text-ink'
}`}
>
{current && (
@@ -153,15 +153,15 @@ export function SidePanel({
)}
<LuChevronDown
size={11}
className={`shrink-0 text-zinc-400 transition-transform ${listOpen ? 'rotate-180' : ''}`}
className={`shrink-0 text-ink-3 transition-transform ${listOpen ? 'rotate-180' : ''}`}
/>
</button>
{listOpen && (
<>
<div className="fixed inset-0 z-20" onClick={() => setListOpen(false)} />
<div className="absolute left-0 top-8 z-30 w-56 rounded-lg border border-zinc-200 bg-white py-1.5 shadow-xl shadow-zinc-300/40">
<div className="px-3 pb-1 pt-1 text-[11px] text-zinc-400">打开的tab</div>
<div className="absolute left-0 top-8 z-30 w-56 rounded-lg border border-line bg-surface py-1.5 shadow-overlay">
<div className="px-3 pb-1 pt-1 text-[11px] text-ink-3">打开的tab</div>
{tabs.map((tab) => (
<div
key={tab.id}
@@ -169,10 +169,10 @@ export function SidePanel({
onSelectTab(tab.id)
setListOpen(false)
}}
className="group flex cursor-default items-center gap-2 px-2 py-1.5 hover:bg-zinc-100"
className="group flex cursor-default items-center gap-2 px-2 py-1.5 hover:bg-hover"
>
<tab.icon size={12} className="shrink-0 text-zinc-400" />
<span className="min-w-0 flex-1 truncate text-[12px] text-zinc-600">{tab.title}</span>
<tab.icon size={12} className="shrink-0 text-ink-3" />
<span className="min-w-0 flex-1 truncate text-[12px] text-ink-2">{tab.title}</span>
<button
type="button"
aria-label={`关闭${tab.title}标签`}
@@ -180,20 +180,20 @@ export function SidePanel({
e.stopPropagation()
onCloseTab(tab.id)
}}
className="shrink-0 rounded p-0.5 text-zinc-400 transition-colors hover:bg-zinc-200 hover:text-zinc-600"
className="shrink-0 rounded p-0.5 text-ink-3 transition-colors hover:bg-hover-2 hover:text-ink-2"
>
<LuX size={11} />
</button>
</div>
))}
<div className="my-1 border-t border-zinc-100" />
<div className="my-1 border-t border-line-soft" />
<button
type="button"
onClick={() => {
setListOpen(false)
onCollapse()
}}
className="w-full px-3 py-1.5 text-left text-[12px] text-zinc-600 transition-colors hover:bg-zinc-100"
className="w-full px-3 py-1.5 text-left text-[12px] text-ink-2 transition-colors hover:bg-hover"
>
收起面板
</button>
@@ -211,7 +211,7 @@ export function SidePanel({
title="打开新的标签"
onClick={() => setPlusOpen((v) => !v)}
className={`flex size-7 shrink-0 items-center justify-center rounded transition-colors ${
plusOpen ? 'bg-zinc-100 text-zinc-600' : 'text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600'
plusOpen ? 'bg-hover text-ink-2' : 'text-ink-3 hover:bg-hover hover:text-ink-2'
}`}
>
<LuPlus size={13} />
@@ -220,7 +220,7 @@ export function SidePanel({
{plusOpen && (
<>
<div className="fixed inset-0 z-20" onClick={() => setPlusOpen(false)} />
<div className="absolute left-0 top-8 z-30 w-40 rounded-lg border border-zinc-200 bg-white py-1 shadow-xl shadow-zinc-300/40">
<div className="absolute left-0 top-8 z-30 w-40 rounded-lg border border-line bg-surface py-1 shadow-overlay">
{available.map(({ id, title, icon: Icon }) => {
const opened = tabs.some((t) => t.id === id)
return (
@@ -232,11 +232,11 @@ export function SidePanel({
setPlusOpen(false)
onOpenTab(id)
}}
className="flex w-full items-center gap-2 px-2.5 py-1.5 text-left text-[12px] text-zinc-600 transition-colors hover:bg-zinc-100 disabled:cursor-default disabled:text-zinc-300 disabled:hover:bg-transparent"
className="flex w-full items-center gap-2 px-2.5 py-1.5 text-left text-[12px] text-ink-2 transition-colors hover:bg-hover disabled:cursor-default disabled:text-ink-3 disabled:hover:bg-transparent"
>
<Icon size={12} className="shrink-0" />
{title}
{opened && <span className="ml-auto text-[10px] text-zinc-300">已打开</span>}
{opened && <span className="ml-auto text-[10px] text-ink-3">已打开</span>}
</button>
)
})}
@@ -257,7 +257,7 @@ export function SidePanel({
}}
/>
<div
className="fixed z-40 w-40 rounded-lg border border-zinc-200 bg-white py-1 shadow-xl shadow-zinc-300/40"
className="fixed z-40 w-40 rounded-lg border border-line bg-surface py-1 shadow-overlay"
style={{ left: ctxMenu.x, top: ctxMenu.y }}
>
<button
@@ -266,7 +266,7 @@ export function SidePanel({
onCloseTab(ctxMenu.tabId)
setCtxMenu(null)
}}
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[12px] text-zinc-600 transition-colors hover:bg-zinc-100"
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[12px] text-ink-2 transition-colors hover:bg-hover"
>
<LuX size={11} className="shrink-0" />
关闭标签页
@@ -280,7 +280,7 @@ export function SidePanel({
}
setCtxMenu(null)
}}
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[12px] text-zinc-600 transition-colors hover:bg-zinc-100 disabled:cursor-default disabled:text-zinc-300 disabled:hover:bg-transparent"
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[12px] text-ink-2 transition-colors hover:bg-hover disabled:cursor-default disabled:text-ink-3 disabled:hover:bg-transparent"
>
<LuX size={11} className="shrink-0" />
关闭其他标签页
@@ -311,8 +311,8 @@ function EmptyState({
return (
<div className="flex h-full min-w-0 flex-1 flex-col items-center justify-center gap-6 px-8">
<div className="text-center">
<h2 className="text-[16px] font-medium text-zinc-700">打开标签页</h2>
<p className="mt-1.5 text-[12px] text-zinc-400">选择要在侧边面板中打开的标签。</p>
<h2 className="text-[16px] font-medium text-ink-2">打开标签页</h2>
<p className="mt-1.5 text-[12px] text-ink-3">选择要在侧边面板中打开的标签。</p>
</div>
<div className="grid grid-cols-2 gap-3">
{available.map(({ id, title, icon: Icon }) => (
@@ -320,7 +320,7 @@ function EmptyState({
key={id}
type="button"
onClick={() => onOpen(id)}
className="flex h-[76px] w-[112px] flex-col items-center justify-center gap-2 rounded-xl border border-zinc-200 bg-white text-zinc-500 transition-colors hover:border-accent/40 hover:bg-accent-tint/50 hover:text-accent-ink"
className="flex h-[76px] w-[112px] flex-col items-center justify-center gap-2 rounded-xl border border-line bg-surface text-ink-3 transition-colors hover:border-accent/40 hover:bg-accent-tint/50 hover:text-accent-ink"
>
<Icon size={18} />
<span className="text-[12px]">{title}</span>
@@ -22,8 +22,8 @@ export function TitleBar({
useEffect(() => window.api.onWindowState((maximized) => setMaximized(maximized)), [])
return (
<div className="relative z-20 flex h-10 shrink-0 select-none items-center border-b border-zinc-200 bg-white pl-4 [-webkit-app-region:drag]">
<div className="min-w-0 truncate text-[12.5px] font-medium text-zinc-800">{name || '新会话'}</div>
<div className="relative z-20 flex h-10 shrink-0 select-none items-center border-b border-line bg-surface pl-4 [-webkit-app-region:drag]">
<div className="min-w-0 truncate text-[12.5px] font-medium text-ink">{name || '新会话'}</div>
<div className="ml-auto flex h-full shrink-0 items-center gap-1 pr-1.5 [-webkit-app-region:no-drag]">
<button
type="button"
@@ -31,7 +31,7 @@ export function TitleBar({
title="侧边面板"
onClick={onTogglePanel}
className={`flex h-7 w-[30px] items-center justify-center rounded transition-colors ${
panelOpen ? 'bg-accent-tint text-accent-ink' : 'text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700'
panelOpen ? 'bg-accent-tint text-accent-ink' : 'text-ink-3 hover:bg-hover hover:text-ink-2'
}`}
>
<LuPanelRight size={14} />
@@ -42,7 +42,7 @@ export function TitleBar({
type="button"
aria-label="最小化"
onClick={() => void window.api.minimize()}
className="flex h-full w-[46px] items-center justify-center text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
className="flex h-full w-[46px] items-center justify-center text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
>
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden>
<path d="M0 5h10" stroke="currentColor" strokeWidth="1" />
@@ -52,7 +52,7 @@ export function TitleBar({
type="button"
aria-label={maximized ? '还原' : '最大化'}
onClick={() => void window.api.toggleMaximize()}
className="flex h-full w-[46px] items-center justify-center text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
className="flex h-full w-[46px] items-center justify-center text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
>
{maximized ? (
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden>
@@ -68,7 +68,7 @@ export function TitleBar({
type="button"
aria-label="关闭"
onClick={() => void window.api.close()}
className="flex h-full w-[46px] items-center justify-center text-zinc-500 transition-colors hover:bg-[#e81123] hover:text-white"
className="flex h-full w-[46px] items-center justify-center text-ink-3 transition-colors hover:bg-[#e81123] hover:text-page"
>
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden>
<path d="M0 0l10 10M10 0L0 10" stroke="currentColor" strokeWidth="1" />
@@ -129,9 +129,9 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
}, [hideCause, slotHidden, activeTab?.tabId])
return (
<div className="flex h-full min-w-0 flex-1 flex-col bg-zinc-50" aria-label="内置浏览器">
<div className="flex h-full min-w-0 flex-1 flex-col bg-inset" aria-label="内置浏览器">
{/* 页签栏:˅ 列表(打开/最近关闭)+ 页签(右键菜单)+ 新建 */}
<div className="flex h-9 shrink-0 items-center gap-1 border-b border-zinc-200 bg-white px-1.5">
<div className="flex h-9 shrink-0 items-center gap-1 border-b border-line bg-surface px-1.5">
<div className="relative shrink-0">
<button
type="button"
@@ -140,7 +140,7 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
title="标签页列表"
onClick={() => setTabListOpen((v) => !v)}
className={`flex size-7 items-center justify-center rounded transition-colors ${
tabListOpen ? 'bg-zinc-100 text-zinc-600' : 'text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600'
tabListOpen ? 'bg-hover text-ink-2' : 'text-ink-3 hover:bg-hover hover:text-ink-2'
}`}
>
<LuChevronDown size={13} />
@@ -149,23 +149,23 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
{tabListOpen && (
<>
<div className="fixed inset-0 z-20" onClick={() => setTabListOpen(false)} />
<div className="absolute left-0 top-9 z-30 max-h-[320px] w-60 overflow-y-auto rounded-lg border border-zinc-200 bg-white py-1.5 shadow-xl shadow-zinc-300/40">
<div className="px-3 pb-1 pt-1 text-[11px] text-zinc-400">打开的标签页</div>
<div className="absolute left-0 top-9 z-30 max-h-[320px] w-60 overflow-y-auto rounded-lg border border-line bg-surface py-1.5 shadow-overlay">
<div className="px-3 pb-1 pt-1 text-[11px] text-ink-3">打开的标签页</div>
{(state?.tabs ?? []).length === 0 && (
<div className="px-3 py-1 text-[11.5px] text-zinc-300">无打开的标签页</div>
<div className="px-3 py-1 text-[11.5px] text-ink-3">无打开的标签页</div>
)}
{(state?.tabs ?? []).map((tab) => (
<div
key={tab.tabId}
onClick={() => props.onSelectTab(tab.tabId)}
className="group flex cursor-default items-center gap-2 px-2 py-1.5 hover:bg-zinc-100"
className="group flex cursor-default items-center gap-2 px-2 py-1.5 hover:bg-hover"
>
{tab.loading ? (
<LuRotateCw size={12} className="shrink-0 animate-spin text-zinc-400" />
<LuRotateCw size={12} className="shrink-0 animate-spin text-ink-3" />
) : (
<LuGlobe size={12} className="shrink-0 text-zinc-400" />
<LuGlobe size={12} className="shrink-0 text-ink-3" />
)}
<span className="min-w-0 flex-1 truncate text-[12px] text-zinc-600">
<span className="min-w-0 flex-1 truncate text-[12px] text-ink-2">
{tab.title || tab.url || '新标签页'}
</span>
<button
@@ -175,7 +175,7 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
e.stopPropagation()
props.onCloseTab(tab.tabId)
}}
className="shrink-0 rounded p-0.5 text-zinc-400 transition-colors hover:bg-zinc-200 hover:text-zinc-600"
className="shrink-0 rounded p-0.5 text-ink-3 transition-colors hover:bg-hover-2 hover:text-ink-2"
>
<LuX size={11} />
</button>
@@ -189,7 +189,7 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
props.onCloseAllTabs()
setTabListOpen(false)
}}
className="mt-0.5 flex w-full items-center gap-2 px-2.5 py-1.5 text-left text-[12px] text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
className="mt-0.5 flex w-full items-center gap-2 px-2.5 py-1.5 text-left text-[12px] text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
>
<LuX size={11} className="shrink-0" />
关闭全部标签页
@@ -198,8 +198,8 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
{(state?.recentClosed?.length ?? 0) > 0 && (
<>
<div className="my-1 border-t border-zinc-100" />
<div className="px-3 pb-1 text-[11px] text-zinc-400">最近关闭的标签页</div>
<div className="my-1 border-t border-line-soft" />
<div className="px-3 pb-1 text-[11px] text-ink-3">最近关闭的标签页</div>
{state?.recentClosed.map((item, i) => (
<button
key={`${i}-${item.url}`}
@@ -208,10 +208,10 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
setTabListOpen(false)
void Promise.resolve(props.onNewTab(item.url)).catch(() => undefined)
}}
className="flex w-full items-center gap-2 px-2 py-1.5 text-left hover:bg-zinc-100"
className="flex w-full items-center gap-2 px-2 py-1.5 text-left hover:bg-hover"
>
<LuGlobe size={12} className="shrink-0 text-zinc-400" />
<span className="min-w-0 flex-1 truncate text-[12px] text-zinc-600">{item.title}</span>
<LuGlobe size={12} className="shrink-0 text-ink-3" />
<span className="min-w-0 flex-1 truncate text-[12px] text-ink-2">{item.title}</span>
</button>
))}
</>
@@ -238,7 +238,7 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
className={`group flex h-7 max-w-44 min-w-20 shrink-0 items-center gap-1 rounded px-2 text-left text-[11px] transition-colors ${
tab.tabId === state?.activeTabId
? 'bg-accent-tint text-accent-ink'
: 'text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700'
: 'text-ink-3 hover:bg-hover hover:text-ink-2'
}`}
>
{tab.loading && <LuRotateCw size={10} className="shrink-0 animate-spin" />}
@@ -251,7 +251,7 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
e.stopPropagation()
props.onCloseTab(tab.tabId)
}}
className="hidden shrink-0 rounded p-0.5 hover:bg-zinc-200 group-hover:block"
className="hidden shrink-0 rounded p-0.5 hover:bg-hover-2 group-hover:block"
>
<LuX size={10} />
</span>
@@ -262,7 +262,7 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
aria-label="新建标签页"
onClick={() => props.onNewTab()}
disabled={Boolean(riskBanner)}
className="flex size-7 shrink-0 items-center justify-center rounded text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 disabled:opacity-40"
className="flex size-7 shrink-0 items-center justify-center rounded text-ink-3 transition-colors hover:bg-hover hover:text-ink-2 disabled:opacity-40"
>
<LuPlus size={13} />
</button>
@@ -280,7 +280,7 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
}}
/>
<div
className="fixed z-40 w-40 rounded-lg border border-zinc-200 bg-white py-1 shadow-xl shadow-zinc-300/40"
className="fixed z-40 w-40 rounded-lg border border-line bg-surface py-1 shadow-overlay"
style={{ left: tabMenu.x, top: tabMenu.y }}
>
<button
@@ -289,7 +289,7 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
props.onCloseTab(tabMenu.tabId)
setTabMenu(null)
}}
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[12px] text-zinc-600 transition-colors hover:bg-zinc-100"
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[12px] text-ink-2 transition-colors hover:bg-hover"
>
<LuX size={11} className="shrink-0" />
关闭标签页
@@ -303,7 +303,7 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
}
setTabMenu(null)
}}
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[12px] text-zinc-600 transition-colors hover:bg-zinc-100 disabled:cursor-default disabled:text-zinc-300 disabled:hover:bg-transparent"
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[12px] text-ink-2 transition-colors hover:bg-hover disabled:cursor-default disabled:text-ink-3 disabled:hover:bg-transparent"
>
<LuX size={11} className="shrink-0" />
关闭其他标签页
@@ -314,12 +314,12 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
</div>
{/* 工具条 */}
<div className="flex h-10 shrink-0 items-center gap-1 border-b border-zinc-200 bg-white px-1.5">
<div className="flex h-10 shrink-0 items-center gap-1 border-b border-line bg-surface px-1.5">
<button
type="button"
aria-label="后退"
onClick={props.onGoBack}
className="flex size-7 items-center justify-center rounded text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
className="flex size-7 items-center justify-center rounded text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
>
<LuArrowLeft size={14} />
</button>
@@ -327,7 +327,7 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
type="button"
aria-label="前进"
onClick={props.onGoForward}
className="flex size-7 items-center justify-center rounded text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
className="flex size-7 items-center justify-center rounded text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
>
<LuArrowRight size={14} />
</button>
@@ -335,12 +335,12 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
type="button"
aria-label="刷新"
onClick={props.onReload}
className="flex size-7 items-center justify-center rounded text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
className="flex size-7 items-center justify-center rounded text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
>
<LuRotateCw size={13} />
</button>
<div className="mx-1 flex h-7 min-w-0 flex-1 items-center gap-1.5 rounded border border-zinc-200 bg-zinc-50 px-2 focus-within:border-accent focus-within:bg-white">
<LuGlobe size={11} className="shrink-0 text-zinc-400" />
<div className="mx-1 flex h-7 min-w-0 flex-1 items-center gap-1.5 rounded border border-line bg-inset px-2 focus-within:border-accent focus-within:bg-surface">
<LuGlobe size={11} className="shrink-0 text-ink-3" />
<input
ref={addressRef}
value={address}
@@ -351,14 +351,14 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
}}
placeholder="输入网址或搜索…"
spellCheck={false}
className="min-w-0 flex-1 bg-transparent text-[12px] text-zinc-700 outline-none placeholder:text-zinc-400"
className="min-w-0 flex-1 bg-transparent text-[12px] text-ink-2 outline-none placeholder:text-ink-3"
/>
</div>
<button
type="button"
onClick={() => setImportOpen(true)}
title="导入本机浏览器登录态"
className="flex size-7 items-center justify-center rounded text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
className="flex size-7 items-center justify-center rounded text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
>
<LuDownload size={13} />
</button>
@@ -366,7 +366,7 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
{/* 风险告知(首次使用门禁,Agent 与用户操作都会被拦截直到确认) */}
{riskBanner && (
<div className="flex shrink-0 items-start gap-2 border-b border-amber-200 bg-amber-50 px-3 py-2.5 text-[11.5px] leading-relaxed text-amber-800">
<div className="flex shrink-0 items-start gap-2 border-b border-orange/40 bg-orange-tint px-3 py-2.5 text-[11.5px] leading-relaxed text-orange">
<LuCircleAlert size={14} className="mt-0.5 shrink-0" />
<div className="min-w-0 flex-1">
<p>
@@ -375,7 +375,7 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
<button
type="button"
onClick={props.onAcknowledgeRisk}
className="mt-1.5 rounded bg-amber-600 px-2.5 py-1 text-[11px] font-medium text-white transition-colors hover:bg-amber-700"
className="mt-1.5 rounded bg-orange px-2.5 py-1 text-[11px] font-medium text-page transition-opacity hover:opacity-90"
>
我已了解,开始使用
</button>
@@ -385,26 +385,26 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
{/* 页面区域:原生 WebContentsView 对齐到这里的矩形。
* 让位期(菜单/浮层打开)以冻结快照占位,快照未就绪时短暂纯白 */}
<div className="relative min-h-0 flex-1 bg-white">
<div className="relative min-h-0 flex-1 bg-page">
{activeTab && !slotHidden ? (
<BrowserSlot key={activeTab.tabId} tabId={activeTab.tabId} />
) : activeTab ? (
frozen ? (
<img src={frozen} alt="" aria-hidden className="h-full w-full object-cover" draggable={false} />
) : (
<div className="h-full bg-white" />
<div className="h-full bg-surface" />
)
) : (
<div className="flex h-full flex-col items-center justify-center gap-3 px-8 text-center">
<LuGlobe size={28} className="text-zinc-300" />
{riskBanner && <p className="text-[12px] text-zinc-400">确认上方风险告知后即可打开网页</p>}
<LuGlobe size={28} className="text-ink-3" />
{riskBanner && <p className="text-[12px] text-ink-3">确认上方风险告知后即可打开网页</p>}
</div>
)}
</div>
{/* Agent 操作轨迹 */}
{state?.trace && !importOpen && (
<div className="shrink-0 truncate border-t border-zinc-200 bg-white px-3 py-1.5 text-[10.5px] text-zinc-400">
<div className="shrink-0 truncate border-t border-line bg-surface px-3 py-1.5 text-[10.5px] text-ink-3">
{state.trace}
</div>
)}
@@ -463,42 +463,42 @@ function ImportOverlay({
}
return (
<div className="absolute inset-0 z-20 flex flex-col bg-white/95 backdrop-blur-[2px]">
<div className="flex h-10 shrink-0 items-center justify-between border-b border-zinc-200 px-3">
<div className="text-[12.5px] font-medium text-zinc-700">导入本机浏览器登录态</div>
<div className="absolute inset-0 z-20 flex flex-col bg-surface/95 backdrop-blur-[2px]">
<div className="flex h-10 shrink-0 items-center justify-between border-b border-line px-3">
<div className="text-[12.5px] font-medium text-ink-2">导入本机浏览器登录态</div>
<button
type="button"
aria-label="关闭"
onClick={onClose}
className="flex size-6 items-center justify-center rounded text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600"
className="flex size-6 items-center justify-center rounded text-ink-3 hover:bg-hover hover:text-ink-2"
>
<LuX size={13} />
</button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-3 text-[12px] text-zinc-600">
<p className="mb-3 leading-relaxed text-zinc-500">
<div className="min-h-0 flex-1 overflow-y-auto p-3 text-[12px] text-ink-2">
<p className="mb-3 leading-relaxed text-ink-3">
选择本机浏览器的一个配置(Profile),把其中各网站的登录态(Cookie / Local Storage)导入到内置受管浏览器。
导入前请<span className="font-medium text-zinc-700">完全退出</span>该浏览器,并关闭本面板的所有标签页。
导入前请<span className="font-medium text-ink-2">完全退出</span>该浏览器,并关闭本面板的所有标签页。
</p>
{state?.imported && (
<div className="mb-3 rounded border border-zinc-200 bg-zinc-50 px-2.5 py-2 text-[11.5px] text-zinc-500">
<div className="mb-3 rounded border border-line bg-inset px-2.5 py-2 text-[11.5px] text-ink-3">
当前导入来源:{state.imported.browser} · {state.imported.profile}
{state.imported.cookies ? '(含 Cookie)' : ''}
</div>
)}
{sources === null && !error && <p className="py-6 text-center text-zinc-400">正在检测本机浏览器…</p>}
{sources?.length === 0 && <p className="py-6 text-center text-zinc-400">未检测到可导入的本机浏览器(支持 Chrome / Edge / Brave / Chromium)</p>}
{sources === null && !error && <p className="py-6 text-center text-ink-3">正在检测本机浏览器…</p>}
{sources?.length === 0 && <p className="py-6 text-center text-ink-3">未检测到可导入的本机浏览器(支持 Chrome / Edge / Brave / Chromium)</p>}
<div className="flex flex-col gap-2">
{sources?.map((source) => (
<div key={source.id} className="rounded border border-zinc-200">
<div className="flex items-center gap-1.5 px-2.5 py-2 text-[12px] font-medium text-zinc-700">
<LuChevronDown size={12} className="text-zinc-400" />
<div key={source.id} className="rounded border border-line">
<div className="flex items-center gap-1.5 px-2.5 py-2 text-[12px] font-medium text-ink-2">
<LuChevronDown size={12} className="text-ink-3" />
{source.name}
<span className="text-[10.5px] font-normal text-zinc-400">({source.profiles.length} 个配置)</span>
<span className="text-[10.5px] font-normal text-ink-3">({source.profiles.length} 个配置)</span>
</div>
<div className="flex flex-col gap-1 px-2 pb-2">
{source.profiles.map((profile) => {
@@ -509,12 +509,12 @@ function ImportOverlay({
type="button"
onClick={() => setSelected({ sourceId: source.id, profileDir: profile.dir })}
className={`flex items-center justify-between rounded px-2.5 py-1.5 text-left text-[11.5px] transition-colors ${
active ? 'bg-accent-tint text-accent-ink' : 'text-zinc-600 hover:bg-zinc-100'
active ? 'bg-accent-tint text-accent-ink' : 'text-ink-2 hover:bg-hover'
}`}
>
<span className="min-w-0 truncate">
{profile.name}
<span className="ml-1.5 text-[10.5px] text-zinc-400">{profile.dir}</span>
<span className="ml-1.5 text-[10.5px] text-ink-3">{profile.dir}</span>
</span>
{active && <span className="shrink-0 text-[10px]">已选择</span>}
</button>
@@ -526,12 +526,12 @@ function ImportOverlay({
</div>
{error && (
<div className="mt-3 rounded border border-red-200 bg-red-50 px-2.5 py-2 text-[11.5px] text-red-600">{error}</div>
<div className="mt-3 rounded border border-red/40 bg-red-tint px-2.5 py-2 text-[11.5px] text-red">{error}</div>
)}
{result && (
<div
className={`mt-3 rounded border px-2.5 py-2 text-[11.5px] leading-relaxed ${
result.ok ? 'border-green-200 bg-green-50 text-green-700' : 'border-red-200 bg-red-50 text-red-600'
result.ok ? 'border-green-200 bg-green-tint text-green' : 'border-red/40 bg-red-tint text-red'
}`}
>
{result.message}
@@ -539,11 +539,11 @@ function ImportOverlay({
)}
</div>
<div className="flex shrink-0 items-center justify-end gap-2 border-t border-zinc-200 px-3 py-2.5">
<div className="flex shrink-0 items-center justify-end gap-2 border-t border-line px-3 py-2.5">
<button
type="button"
onClick={onClose}
className="rounded px-3 py-1.5 text-[12px] text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
className="rounded px-3 py-1.5 text-[12px] text-ink-3 transition-colors hover:bg-hover hover:text-ink-2"
>
关闭
</button>
@@ -551,7 +551,7 @@ function ImportOverlay({
type="button"
disabled={!selected || busy}
onClick={runImport}
className="rounded bg-accent px-3.5 py-1.5 text-[12px] font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-40"
className="rounded bg-accent px-3.5 py-1.5 text-[12px] font-medium text-page transition-opacity hover:opacity-90 disabled:opacity-40"
>
{busy ? '导入中…' : '导入登录态'}
</button>
@@ -350,6 +350,8 @@ export interface UseAgent {
forkPoints: ForkPoint[]
/** 分支回溯(含分支摘要生成)进行中 */
navigating: boolean
/** 会话末尾分叉(pi fork)进行中 */
forking: boolean
send: (text: string, images?: SendImage[]) => void
stop: () => void
clearError: () => void
@@ -369,6 +371,8 @@ export interface UseAgent {
setAutoCompaction: (enabled: boolean) => void
dismissCompactionNotice: () => void
navigateTree: (entryId: string, options?: NavigateTreeOptions) => void
/** 在会话末尾分叉出新会话(pi fork,原会话保留) */
forkSession: () => void
}
export function useAgent(): UseAgent {
@@ -379,6 +383,8 @@ export function useAgent(): UseAgent {
const [forkPoints, setForkPoints] = useState<ForkPoint[]>([])
/** 分支回溯进行中(含分支摘要生成) */
const [navigating, setNavigating] = useState(false)
/** 会话末尾分叉进行中 */
const [forking, setForking] = useState(false)
const refreshWorkspaces = useCallback((): void => {
void window.api
@@ -565,6 +571,15 @@ export function useAgent(): UseAgent {
[dispatchError]
)
/** 在会话末尾分叉出新会话(pi fork:新会话文件,原会话保留);成功后 session_switched 自动切换 */
const forkSession = useCallback((): void => {
setForking(true)
window.api
.forkSession()
.catch(dispatchError)
.finally(() => setForking(false))
}, [dispatchError])
return useMemo(
() => ({
state,
@@ -572,6 +587,7 @@ export function useAgent(): UseAgent {
workspaces,
forkPoints,
navigating,
forking,
send,
stop,
clearError,
@@ -590,7 +606,8 @@ export function useAgent(): UseAgent {
abortCompaction,
setAutoCompaction,
dismissCompactionNotice,
navigateTree
navigateTree,
forkSession
}),
[
state,
@@ -598,6 +615,7 @@ export function useAgent(): UseAgent {
workspaces,
forkPoints,
navigating,
forking,
send,
stop,
clearError,
@@ -616,7 +634,8 @@ export function useAgent(): UseAgent {
abortCompaction,
setAutoCompaction,
dismissCompactionNotice,
navigateTree
navigateTree,
forkSession
]
)
}
+4 -1
View File
@@ -19,6 +19,7 @@ import type {
SessionState,
SessionStatsInfo,
ThinkingLevel,
ThemeMode,
WorkspaceFile,
WorkspaceMeta
} from '@shared/types'
@@ -79,9 +80,11 @@ declare global {
setAutoCompaction: (enabled: boolean) => Promise<void>
listForkPoints: () => Promise<ForkPoint[]>
navigateTree: (entryId: string, options?: NavigateTreeOptions) => Promise<void>
forkSession: () => Promise<void>
getSettings: () => Promise<AppSettings>
setSystemPrompt: (text: string) => Promise<void>
getSystemPrompt: () => Promise<string>
setThemeColor: (id: string) => Promise<void>
setThemeMode: (mode: ThemeMode) => Promise<void>
resolveApproval: (id: string, approved: boolean) => Promise<void>
onEvent: (callback: (event: AgentUiEvent) => void) => () => void
browserGetState: () => Promise<BrowserUiState>
@@ -4,6 +4,9 @@
* accent-ink 为加深版(保证链接/可交互文本 AA 对比度),tint 为低透明度选中底。
*/
import { useEffect, useState } from 'react'
import type { ThemeMode } from '@shared/types'
export interface ThemePreset {
id: string
label: string
@@ -23,6 +26,12 @@ export const THEME_PRESETS: ThemePreset[] = [
{ id: 'black', label: '墨黑', accent: '#3a3a3f', ink: '#17171a', tint: 'rgba(23, 23, 26, 0.08)' }
]
export const THEME_MODES: Array<{ id: ThemeMode; label: string }> = [
{ id: 'light', label: '浅色' },
{ id: 'dark', label: '深色' },
{ id: 'system', label: '跟随系统' }
]
/** 应用主题预设(undefined/未知 id = 恢复默认,移除内联覆盖) */
export function applyThemeColor(id: string | undefined): void {
const preset = THEME_PRESETS.find((p) => p.id === id)
@@ -37,3 +46,34 @@ export function applyThemeColor(id: string | undefined): void {
root.style.setProperty('--accent-ink', preset.ink)
root.style.setProperty('--accent-tint', preset.tint)
}
/**
* 深浅色模式应用:主进程把用户选择写入 Electron nativeTheme.themeSource,
* 渲染层的 prefers-color-scheme 媒体查询随之反映(light/dark/system 三种语义)。
* 这里监听媒体查询切换 <html> 的 .dark 类——beautifului.css 的 .dark 令牌块
* 接管全部设计令牌,组件无需感知模式。返回清理函数。
*/
export function watchColorScheme(): () => void {
const mq = window.matchMedia('(prefers-color-scheme: dark)')
const apply = (): void => {
document.documentElement.classList.toggle('dark', mq.matches)
}
apply()
mq.addEventListener('change', apply)
return () => {
mq.removeEventListener('change', apply)
document.documentElement.classList.remove('dark')
}
}
/** 当前是否处于深色(组件级感知:shiki 双主题、需要单独适配的局部样式等) */
export function useDarkTheme(): boolean {
const [dark, setDark] = useState<boolean>(() => window.matchMedia('(prefers-color-scheme: dark)').matches)
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)')
const onChange = (): void => setDark(mq.matches)
mq.addEventListener('change', onChange)
return () => mq.removeEventListener('change', onChange)
}, [])
return dark
}
+5 -2
View File
@@ -139,8 +139,6 @@ export interface CustomProviderConfig {
/** 应用级设置(存 userData/aso-settings.json) */
export interface AppSettings {
/** 覆盖 pi 默认系统提示词(空串 = 不覆盖) */
systemPrompt: string
/** 当前工作区目录(缺省 = 默认工作区 Default) */
workspace?: string
/** 通过目录选择器添加的根目录之外的工作区 */
@@ -149,8 +147,13 @@ export interface AppSettings {
permissionMode?: PermissionMode
/** 主题色预设 id(缺省 default,见 renderer lib/theme.ts) */
themeColor?: string
/** 深浅色模式(缺省 system 跟随系统;经 Electron nativeTheme 生效) */
themeMode?: ThemeMode
}
/** 深浅色模式:light / dark / system(跟随系统) */
export type ThemeMode = 'light' | 'dark' | 'system'
/** 权限模式:confirm 变更前确认 / full 完全访问 */
export type PermissionMode = 'confirm' | 'full'