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