diff --git a/apps/desktop/src/main/ipc.ts b/apps/desktop/src/main/ipc.ts index 9b7422c..99f1a7a 100644 --- a/apps/desktop/src/main/ipc.ts +++ b/apps/desktop/src/main/ipc.ts @@ -147,6 +147,11 @@ export function registerIpc(): AgentService { return pass(() => service.setSystemPrompt(text)) }) + ipcMain.handle('settings:setTheme', (_event, id: unknown) => { + if (typeof id !== 'string' || !id.trim()) throw new Error('invalid theme id') + service.setThemeColor(id.trim()) + }) + // ---- 工作区 ---- ipcMain.handle('workspaces:list', () => service.listWorkspaces().catch((err: unknown) => { diff --git a/apps/desktop/src/main/pi/agent-service.ts b/apps/desktop/src/main/pi/agent-service.ts index d977250..e4d1fb0 100644 --- a/apps/desktop/src/main/pi/agent-service.ts +++ b/apps/desktop/src/main/pi/agent-service.ts @@ -89,7 +89,8 @@ export class AgentService { : undefined, // 历史版本存过 autoEdit/plan,统一收敛回 confirm permissionMode: - raw.permissionMode === 'full' ? 'full' : 'confirm' + raw.permissionMode === 'full' ? 'full' : 'confirm', + themeColor: typeof raw.themeColor === 'string' ? raw.themeColor : undefined } } catch { this.settings = { systemPrompt: '', permissionMode: this.permissionMode } @@ -649,13 +650,14 @@ export class AgentService { async listSessions(): Promise { const infos = await SessionManager.list(this.cwd) + // 空会话(一条消息都没发过)不上列表:第一句发出之后才在侧栏可见 const metas = infos + .filter((info) => info.messageCount > 0) .sort((a, b) => b.modified.getTime() - a.modified.getTime()) .map((info) => toSessionMeta(info)) - // 新会话在首条消息写入前没有 .jsonl 文件,SessionManager.list 扫不到; - // 把当前活跃会话兜底合并进列表,避免侧栏“新建后不出现” + // 未落盘的活跃会话兜底合并进列表(同样要求已有消息) const session = this.runtime?.session - if (session && !metas.some((m) => samePath(m.path, session.sessionFile ?? ''))) { + if (session && session.messages.length > 0 && !metas.some((m) => samePath(m.path, session.sessionFile ?? ''))) { const now = new Date().toISOString() metas.unshift({ id: session.sessionId, @@ -876,6 +878,12 @@ export class AgentService { } } + /** 主题色偏好(纯外观,仅持久化;渲染层负责应用) */ + setThemeColor(id: string): void { + this.settings = { ...this.settings, themeColor: id } + this.saveSettings() + } + private async rebuildRuntime(): Promise { const old = this.runtime this.runtime = null diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 5426d86..e93c747 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -63,6 +63,7 @@ const api = { logoutProvider: (providerId: string): Promise => ipcRenderer.invoke('auth:logout', providerId), getSettings: (): Promise => ipcRenderer.invoke('settings:get'), setSystemPrompt: (text: string): Promise => ipcRenderer.invoke('settings:setPrompt', text), + setThemeColor: (id: string): Promise => ipcRenderer.invoke('settings:setTheme', id), resolveApproval: (id: string, approved: boolean): Promise => ipcRenderer.invoke('approval:resolve', id, approved), onEvent: (callback: (event: AgentUiEvent) => void): (() => void) => { diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index c6eadc7..98726c4 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -8,6 +8,7 @@ import { BottomSlot } from './components/BottomSlot' import { Composer } from './components/Composer' import { SettingsModal } from './components/SettingsModal' import { Search } from './components/Search' +import { applyThemeColor } from './lib/theme' export default function App(): JSX.Element { const { @@ -31,6 +32,14 @@ export default function App(): JSX.Element { const [settingsOpen, setSettingsOpen] = useState(false) const [searchOpen, setSearchOpen] = useState(false) + // 启动时应用持久化的主题色 + useEffect(() => { + void window.api + .getSettings() + .then((s) => applyThemeColor(s.themeColor)) + .catch(() => undefined) + }, []) + // 全局快捷键:Ctrl+N 新会话 / Ctrl+K 搜索 / Ctrl+, 设置 / Esc 关闭弹窗 useEffect(() => { const onKey = (e: KeyboardEvent): void => { @@ -110,6 +119,8 @@ export default function App(): JSX.Element { disabled={!state.ready} model={state.sessionState?.model} permissionMode={state.sessionState?.permissionMode ?? 'confirm'} + sessionKey={state.sessionState?.sessionId} + sessionMessageCount={state.sessionState?.messageCount} onSend={send} onStop={stop} onNewSession={newSession} diff --git a/apps/desktop/src/renderer/src/components/ApprovalCard.tsx b/apps/desktop/src/renderer/src/components/ApprovalCard.tsx index 5bb2694..c4158c3 100644 --- a/apps/desktop/src/renderer/src/components/ApprovalCard.tsx +++ b/apps/desktop/src/renderer/src/components/ApprovalCard.tsx @@ -222,7 +222,7 @@ export function ApprovalCard({
- ))} diff --git a/apps/desktop/src/renderer/src/components/Composer.tsx b/apps/desktop/src/renderer/src/components/Composer.tsx index 4762c7d..df202d6 100644 --- a/apps/desktop/src/renderer/src/components/Composer.tsx +++ b/apps/desktop/src/renderer/src/components/Composer.tsx @@ -23,6 +23,24 @@ interface LocalCommand { /** 图片附件大小上限(与主进程 pickAttachments 一致) */ const MAX_IMAGE_BYTES = 8 * 1024 * 1024 +// ---- 输入草稿:未发送的输入实时保存;新建的空会话自动复用,发送后清除 ---- +const DRAFT_KEY = 'aso-composer-draft' +function loadDraft(): string { + try { + return localStorage.getItem(DRAFT_KEY) ?? '' + } catch { + return '' + } +} +function saveDraft(text: string): void { + try { + if (text) localStorage.setItem(DRAFT_KEY, text) + else localStorage.removeItem(DRAFT_KEY) + } catch { + // 存储不可用时静默放弃(草稿是尽力而为) + } +} + /** 输入框锚定菜单:portal 到 body 固定定位(BottomSlot 的 overflow-hidden 会裁剪普通弹层) */ function ComposerMenu({ anchor, @@ -64,6 +82,8 @@ export function Composer({ disabled, model, permissionMode = 'confirm', + sessionKey, + sessionMessageCount, onSend, onStop, onNewSession, @@ -78,6 +98,10 @@ export function Composer({ disabled: boolean model?: ModelInfo permissionMode?: PermissionMode + /** 当前会话 id(切换时触发草稿复用) */ + sessionKey?: string + /** 当前会话消息数(0 = 新建未发言的空会话) */ + sessionMessageCount?: number onSend: (text: string, images?: { mimeType: string; data: string }[]) => void onStop: () => void onNewSession: () => void @@ -358,6 +382,24 @@ export function Composer({ } }, [prefillRef]) + // 草稿实时保存(发送后输入框清空,草稿随之清除) + useEffect(() => { + saveDraft(value) + }, [value]) + + // 切换到空(新建未发言)会话且输入为空时,复用上次未发送的草稿 + const prevSessionRef = useRef(sessionKey) + useEffect(() => { + const prev = prevSessionRef.current + prevSessionRef.current = sessionKey + if (prev === sessionKey) return + if ((sessionMessageCount ?? 0) !== 0) return + if (value.trim()) return + const draft = loadDraft() + if (draft) setValue(draft) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sessionKey, sessionMessageCount]) + // 自适应高度 useEffect(() => { const el = textareaRef.current diff --git a/apps/desktop/src/renderer/src/components/SettingsModal.tsx b/apps/desktop/src/renderer/src/components/SettingsModal.tsx index 16110de..0c6cdcb 100644 --- a/apps/desktop/src/renderer/src/components/SettingsModal.tsx +++ b/apps/desktop/src/renderer/src/components/SettingsModal.tsx @@ -16,15 +16,18 @@ import { LuSettings2, LuSparkles, LuTerminal, + LuTextQuote, LuWebhook, LuX } from 'react-icons/lu' import type { IconType } from 'react-icons' import type { CustomProviderConfig, ModelCatalog, ModelMeta, ProviderCatalogEntry, ResourceDetails, SessionState } from '@shared/types' +import { THEME_PRESETS, applyThemeColor } from '../lib/theme' type Tab = | 'general' | 'model' + | 'prompt' | 'memory' | 'subagent' | 'plugins' @@ -39,7 +42,8 @@ const NAV: Array<{ group: string; items: Array<{ id: Tab; label: string; icon: I group: '基础设置', items: [ { id: 'general', label: '常规', icon: LuSettings2 }, - { id: 'model', label: '模型设置', icon: LuCpu } + { id: 'model', label: '模型设置', icon: LuCpu }, + { id: 'prompt', label: '系统提示词', icon: LuTextQuote } ] }, { @@ -65,8 +69,9 @@ const PLACEHOLDER: Record = { } const TAB_META: Record = { - general: { title: '常规', desc: '管理 Agent 的系统提示词与基础行为。' }, + general: { title: '常规', desc: '主题色等外观偏好。' }, model: { title: '模型设置', desc: '' }, + prompt: { title: '系统提示词', desc: '覆盖 Agent 的系统提示词,保存后重建运行时。' }, memory: { title: '记忆', desc: PLACEHOLDER.memory }, subagent: { title: '子智能体', desc: PLACEHOLDER.subagent }, plugins: { title: '插件', desc: PLACEHOLDER.plugins }, @@ -618,6 +623,7 @@ export function SettingsModal({ const [keyValue, setKeyValue] = useState('') const [prompt, setPrompt] = useState('') const [promptDirty, setPromptDirty] = useState(false) + const [theme, setTheme] = useState('default') const [busy, setBusy] = useState(false) const [notice, setNotice] = useState<{ kind: 'ok' | 'err'; text: string } | null>(null) const [resources, setResources] = useState(null) @@ -701,6 +707,7 @@ export function SettingsModal({ .then((s) => { setPrompt(s.systemPrompt) setPromptDirty(false) + setTheme(s.themeColor ?? 'default') }) .catch(() => undefined) setNotice(null) @@ -782,6 +789,36 @@ export function SettingsModal({
{tab === 'general' && ( +
+
+
主题色
+
影响链接、选中态、焦点环等强调色,点击即时生效。
+
+ {THEME_PRESETS.map((p) => ( + + ))} +
+
+
+ )} + + {tab === 'prompt' && (