feat: 原生系统提示词只读展示、会话末尾 fork、深浅色主题
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user