diff --git a/AGENTS.md b/AGENTS.md index 90c65fc4..3565f810 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -198,6 +198,7 @@ 7. **带文字容器固定高度**:所有含内部文字的选项 / 按钮 / 标签 / 容器固定高度,禁止因文字多少被撑大撑高;过长文字用省略号或内部滚动处理。 8. **窗口设最大尺寸 + 内部滚动**:所有弹窗 / 面板 / 列表设置 `max-height` / `max-width`,超出由内部容器滚动,不顶大整个窗口;滚动条要么隐藏,要么做样式适配,不暴露原生粗滚动条。 9. **实体面板禁止半透明**:实体 UI 容器(对话区 / 侧栏 / 下拉/二级菜单 / 抽屉 / 对话框本体 / 状态条 / tooltip / 输入栏)背景必须不透明,且不加 `backdrop-filter` 磨砂。唯一例外:遮罩层 scrim(`--overlay-scrim`,`position:fixed;inset:0`)和刻意玻璃质感装饰保留半透明。 +10. **主题变体必须与基础定义同文件(禁止跨文件补丁覆盖)**:组件的 dark/light/classic 主题变体写在该组件自己的样式文件里(文件内统一的 `body[data-theme='…'] { … }` 块),禁止把某组件的主题样式写到别的文件去覆盖。新增主题样式前先确认该组件基础定义所在文件;发现存量跨文件覆盖应顺手回迁并删除原覆盖。`!important` 只准用于遮盖第三方库样式,禁止用于压过项目自有定义(出现这种需求说明应该合并定义,而不是加 `!important`)。审计脚本:`python3 cache/style_audit.py` + `python3 cache/theme_diff.py`。 ## 5.6) 工具结果显示规范(强制) @@ -362,9 +363,19 @@ AI 执行以下流程时,每一步都要向用户说明在做什么: - 开启后写入:`logs/approval_agent/` - 记录以累积 `messages` 为主,便于对齐主/子智能体会话格式 -## 11) 多智能体模式(multi-agent mode) +## 11) 多智能体对话类型(multi-agent conversation type) -> 对话级开关 `metadata.multi_agent_mode = true` 启用,URL 入口 `/multiagent/`,数据目录 `~/.astrion/astrion/host/mutiagents/`(保留原拼写)。完整设计与踩坑记录见项目记忆 `multi_agent_mode_design`,本节只列 Agent 改代码时必须知道的硬约束。 +> **2026-08 重构**:已废除「多智能体模式」全局状态,多智能体是**对话的不可变属性**(`metadata.multi_agent_mode = true`,创建时确定、不可变)。URL 统一为 `/`(旧 `/multiagent/*` 由前端 bootstrap 重定向到裸路径)。数据目录 `~/.astrion/astrion/host/mutiagents/`(保留原拼写)。完整设计与踩坑记录见项目记忆 `multi_agent_mode_design`,重构方案见 `docs/conversation_type_unification_plan.md`,本节只列 Agent 改代码时必须知道的硬约束。 + +### 11.0 前端对话类型模型(重构后) + +- 三个语义单一的状态,**互不复用**: + - `currentConversationType`(`static/src/app/state.ts`):当前已打开对话的类型(`'normal' | 'multi_agent' | null`),由 `enterConversation` 从对话 metadata 恢复(`bootstrap.ts`),空对话为 `null`。 + - `newConversationType`(同上):空对话时输入栏待创建类型(`'agent' | 'multi_agent'`),localStorage 持久化(`agents_new_conversation_type`),由输入栏底行 `+` 右侧的类型选择器修改(`InputComposer.vue` 的 `agent-type-switcher`)。 + - `sidebarConversationType`(`stores/conversation.ts`):侧边栏列表过滤器(`'normal' | 'multi_agent'`),localStorage 持久化(`agents_sidebar_conversation_type`),驱动列表/搜索的 `multi_agent_mode=0|1` 请求参数与侧边栏新建按钮的对话类型。 +- `stores/conversation.ts` 的 `multiAgentMode` 字段保留,但语义已变为「当前对话是否多智能体」,仅由 `enterConversation`(meta 落地)与空对话 watcher(复位 false)写入——`stores/subAgent.ts` 的读取点依赖它,不得删除。 +- 侧边栏类型切换 UI:平铺模式在「搜索对话」下方显示分段控件;分组模式在工作区标题行新建/更多按钮左侧显示单字切换按钮;列表区域用 `` 做左右滑动动画(切多智能体向左滑出,切回反向)。 +- 发送消息创建对话时按 `newConversationType` 选择 `/api/conversations` 或 `/api/multiagent/conversations`(`message/send.ts`);侧边栏新建按 `sidebarConversationType` 选择(`conversation/action.ts`)。 ### 11.1 角色与实例 diff --git a/static/src/App.vue b/static/src/App.vue index 36d3ebb3..dfef6d4a 100644 --- a/static/src/App.vue +++ b/static/src/App.vue @@ -95,6 +95,7 @@ @reveal-workspace="revealHostWorkspace" @rename-workspace="handleRenameWorkspaceFromSidebar" @pin-workspace="handlePinWorkspaceFromSidebar" + @conversation-type-change="handleSidebarConversationTypeChange" />
@@ -763,6 +767,7 @@ @reveal-workspace="revealHostWorkspace" @rename-workspace="handleRenameWorkspaceFromSidebar" @pin-workspace="handlePinWorkspaceFromSidebar" + @conversation-type-change="handleSidebarConversationTypeChange" /> diff --git a/static/src/app/methods/conversation/action.ts b/static/src/app/methods/conversation/action.ts index 74c83453..b3b2d99e 100644 --- a/static/src/app/methods/conversation/action.ts +++ b/static/src/app/methods/conversation/action.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { debugLog, traceLog } from '../common'; import { usePersonalizationStore } from '../../../stores/personalization'; +import { persistNewConversationType } from '../../state'; import { } from './shared'; @@ -71,7 +72,17 @@ export const actionMethods = { console.warn('读取新建对话按钮行为设置失败,按默认跳转处理:', error); } if (newChatBehavior !== 'blank') { - const target = this.multiAgentMode ? '/multiagent/new' : '/new'; + // 跳转 /new 前把侧边栏过滤器类型同步给输入栏选择器: + // 看着哪类对话列表点新建,默认就创建哪类(选择器仍可手动改)。 + try { + const { useConversationStore } = await import('../../../stores/conversation'); + this.newConversationType = + useConversationStore().sidebarConversationType === 'multi_agent' ? 'multi_agent' : 'agent'; + persistNewConversationType(this.newConversationType); + } catch (_e) { + // ignore + } + const target = '/new'; const normalize = (p) => String(p || '/').replace(/^\/+|\/+$/g, ''); if (normalize(window.location.pathname) !== normalize(target)) { window.location.href = target; @@ -144,7 +155,9 @@ export const actionMethods = { this.versioningInitializingBackupToastId = backupToastId; } - const isMultiAgent = Boolean(this.multiAgentMode); + // 侧边栏新建:类型跟随侧边栏过滤器(看着哪类列表就建哪类对话) + const { useConversationStore } = await import('../../../stores/conversation'); + const isMultiAgent = useConversationStore().sidebarConversationType === 'multi_agent'; const createUrl = isMultiAgent ? '/api/multiagent/conversations' : '/api/conversations'; // reasoning_effort 随创建权威写入新对话 meta(此处 this.reasoningEffort 已被上方 // 重置为个性化默认值,语义与“创建空对话写入默认值一次”一致) @@ -278,10 +291,18 @@ export const actionMethods = { if (!workspaceId) return; debugLog('在指定工作区创建新对话:', workspaceId); try { - const response = await fetch('/api/conversations', { + const { useConversationStore } = await import('../../../stores/conversation'); + const conversationStore = useConversationStore(); + // 项目旁新建:类型跟随侧边栏过滤器(看着哪类列表就建哪类对话) + const isMultiAgent = conversationStore.sidebarConversationType === 'multi_agent'; + const createUrl = isMultiAgent ? '/api/multiagent/conversations' : '/api/conversations'; + const createPayload = isMultiAgent + ? { workspace_id: workspaceId, preserve_mode: true } + : { workspace_id: workspaceId }; + const response = await fetch(createUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workspace_id: workspaceId }) + body: JSON.stringify(createPayload) }); const result = await response.json(); if (result.success) { diff --git a/static/src/app/methods/conversation/bootstrap.ts b/static/src/app/methods/conversation/bootstrap.ts index 4eaf9395..d99b071a 100644 --- a/static/src/app/methods/conversation/bootstrap.ts +++ b/static/src/app/methods/conversation/bootstrap.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { debugLog, traceLog } from '../common'; import { useQuickDockStore } from '../../../stores/quickDock'; +import { useConversationStore } from '../../../stores/conversation'; /** * 统一加载协议(方案 B):进入对话的单一入口。 @@ -17,7 +18,6 @@ export const bootstrapMethods = { * @param options.source 'refresh' | 'sidebar'(仅用于日志追踪) * @param options.workspaceId 目标工作区(host/docker 多工作区场景) * @param options.urlMode 'push' | 'replace' | 'none'(缺省 push) - * @param options.urlPrefix 显式 URL 前缀(缺省按 multiAgentMode 自适应) * @param options.preserveListPosition 保持侧边栏列表位置不置顶 * @param options.resetUI 渲染前 resetAllStates(sidebar 切换保留「清空→填入」语义) */ @@ -26,7 +26,6 @@ export const bootstrapMethods = { source = 'sidebar', workspaceId = '', urlMode = 'push', - urlPrefix = '', preserveListPosition = false, resetUI = false } = options; @@ -63,9 +62,10 @@ export const bootstrapMethods = { } // 恢复会话级推理强度档位(null = 默认,不传参) this.reasoningEffort = typeof meta.reasoning_effort === 'string' ? meta.reasoning_effort : null; - if (typeof meta.multi_agent_mode === 'boolean') { - this.multiAgentMode = meta.multi_agent_mode; - } + // 对话类型从 metadata 落地(创建时确定、不可变) + this.currentConversationType = meta.multi_agent_mode === true ? 'multi_agent' : 'normal'; + // conversationStore.multiAgentMode 语义 = 「当前对话是否多智能体」(subAgent store 读取) + useConversationStore().$patch({ multiAgentMode: meta.multi_agent_mode === true }); // 2. 当前对话状态(skip 标记阻止 currentConversationId watch 重复拉历史) this.skipConversationHistoryReload = true; @@ -76,12 +76,12 @@ export const bootstrapMethods = { this.promoteConversationToTop(normalizedId); } if (urlMode !== 'none') { - const prefix = urlPrefix || (this.multiAgentMode ? '/multiagent/' : '/'); + // 对话类型不再是路由概念,统一裸路径 / const stateMethod = urlMode === 'replace' ? 'replaceState' : 'pushState'; history[stateMethod]( { conversationId: normalizedId }, '', - `${prefix}${this.stripConversationPrefix(normalizedId)}` + `/${this.stripConversationPrefix(normalizedId)}` ); } this.skipConversationLoadedEvent = true; diff --git a/static/src/app/methods/conversation/load.ts b/static/src/app/methods/conversation/load.ts index a726b60c..fedcda40 100644 --- a/static/src/app/methods/conversation/load.ts +++ b/static/src/app/methods/conversation/load.ts @@ -6,6 +6,32 @@ import { } from './shared'; export const loadMethods = { + /** + * 侧边栏对话类型(普通/多智能体)切换: + * 重置平铺列表分页并重新加载,同时刷新所有已展开的工作区分组。 + * 由 ConversationSidebar 的 conversation-type-change 事件触发; + * store 中的 sidebarConversationType 已由组件先行写入。 + */ + async handleSidebarConversationTypeChange(_type?: 'normal' | 'multi_agent') { + this.conversationsOffset = 0; + this.hasMoreConversations = false; + await this.loadConversationsList(); + try { + const { useConversationStore } = await import('../../../stores/conversation'); + const conversationStore = useConversationStore(); + const groups = Array.isArray(conversationStore.workspaceGroups) + ? conversationStore.workspaceGroups + : []; + for (const group of groups) { + if (group && group.workspaceId) { + await this.loadWorkspaceConversations(group.workspaceId, { reset: true }); + } + } + } catch (error) { + console.error('刷新工作区分组对话失败:', error); + } + }, + async loadConversationsList() { const queryOffset = this.conversationsOffset; const queryLimit = this.conversationsLimit; @@ -14,8 +40,9 @@ export const loadMethods = { const requestSeq = ++this.conversationListRequestSeq; this.conversationsLoading = true; try { - // 多智能体模式下只加载多智能体对话记录;常规模式排除多智能体对话 - const maParam = this.multiAgentMode ? '&multi_agent_mode=1' : '&multi_agent_mode=0'; + // 列表过滤由侧边栏类型过滤器(普通/多智能体)决定 + const { useConversationStore } = await import('../../../stores/conversation'); + const maParam = useConversationStore().sidebarConversationType === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0'; const response = await fetch(`/api/conversations?limit=${queryLimit}&offset=${queryOffset}${maParam}`); const data = await response.json(); @@ -255,7 +282,8 @@ export const loadMethods = { if (group.loading || group.loadingMore) return; group.loading = true; try { - const maParam = this.multiAgentMode ? '&multi_agent_mode=1' : '&multi_agent_mode=0'; + const { useConversationStore: useConvStore } = await import('../../../stores/conversation'); + const maParam = useConvStore().sidebarConversationType === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0'; const response = await fetch(`/api/conversations?workspace_id=${encodeURIComponent(workspaceId)}&limit=${group.limit}&offset=${group.offset}${maParam}`); const data = await response.json(); if (data.success) { diff --git a/static/src/app/methods/message/send.ts b/static/src/app/methods/message/send.ts index 02c2da07..e1fc244f 100644 --- a/static/src/app/methods/message/send.ts +++ b/static/src/app/methods/message/send.ts @@ -240,7 +240,8 @@ export const sendMethods = { this.versioningInitializingBackupToastId = backupToastId; } - const isMultiAgent = Boolean(this.multiAgentMode); + // 首条消息创建:类型来自输入栏「智能体/多智能体」选择器(newConversationType) + const isMultiAgent = this.newConversationType === 'multi_agent'; const createUrl = isMultiAgent ? '/api/multiagent/conversations' : '/api/conversations'; // reasoning_effort 随创建权威写入新对话 meta:/new 页面用户可能已手动调整档位, // 不能依赖后端 terminal 值(会被 status 轮询的 prefs 应用覆盖) @@ -259,6 +260,14 @@ export const sendMethods = { throw new Error(createResult?.message || createResult?.error || '创建对话失败'); } targetConversationId = createResult.conversation_id; + // 创建即定型:落地对话类型(与后端 metadata 保持一致) + this.currentConversationType = isMultiAgent ? 'multi_agent' : 'normal'; + try { + const { useConversationStore } = await import('../../../stores/conversation'); + useConversationStore().$patch({ multiAgentMode: isMultiAgent }); + } catch (_e) { + // ignore + } this.skipConversationHistoryReload = true; this.currentConversationId = targetConversationId; this.currentConversationTitle = '新对话'; @@ -293,8 +302,8 @@ export const sendMethods = { } const pathFragment = this.stripConversationPrefix(targetConversationId); - const urlPrefix = this.multiAgentMode ? '/multiagent/' : '/'; - history.replaceState({ conversationId: targetConversationId }, '', `${urlPrefix}${pathFragment}`); + // 对话类型不再是路由概念,统一裸路径 / + history.replaceState({ conversationId: targetConversationId }, '', `/${pathFragment}`); } catch (error) { this.uiPushToast({ title: '发送失败', diff --git a/static/src/app/methods/search.ts b/static/src/app/methods/search.ts index 792fafc7..96a1735f 100644 --- a/static/src/app/methods/search.ts +++ b/static/src/app/methods/search.ts @@ -57,7 +57,9 @@ export const searchMethods = { this.searchResults = []; this.searchGroups = []; - const maParam = this.multiAgentMode ? '&multi_agent_mode=1' : '&multi_agent_mode=0'; + // 搜索范围跟随侧边栏类型过滤器(普通/多智能体) + const { useConversationStore } = await import('../../stores/conversation'); + const maParam = useConversationStore().sidebarConversationType === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0'; const grouped = this.isGroupedSidebarSearch(); const url = grouped ? `/api/conversations/search?q=${encodeURIComponent(trimmed)}&limit=${SEARCH_GROUP_LIMIT}&all_workspaces=1${maParam}` diff --git a/static/src/app/methods/taskPolling/sync.ts b/static/src/app/methods/taskPolling/sync.ts index e028a79a..6d228b76 100644 --- a/static/src/app/methods/taskPolling/sync.ts +++ b/static/src/app/methods/taskPolling/sync.ts @@ -134,8 +134,8 @@ export const syncMethods = { const pathFragment = this.stripConversationPrefix(data.conversation_id); const currentPath = window.location.pathname.replace(/^\/+/, ''); - const urlPrefix = this.multiAgentMode ? '/multiagent/' : '/'; - const targetPath = `${urlPrefix}${pathFragment}`; + // 对话类型不再是路由概念,统一裸路径 / + const targetPath = `/${pathFragment}`; if (data.created) { history.pushState({ conversationId: data.conversation_id }, '', targetPath); } else if (currentPath !== pathFragment) { diff --git a/static/src/app/methods/ui/menu.ts b/static/src/app/methods/ui/menu.ts index 3e2a067c..1f5502b6 100644 --- a/static/src/app/methods/ui/menu.ts +++ b/static/src/app/methods/ui/menu.ts @@ -56,7 +56,7 @@ export const menuMethods = { } }, handleClickOutsideQuickMenu(event) { - if (!this.quickMenuOpen && !this.permissionMenuOpen) { + if (!this.quickMenuOpen && !this.permissionMenuOpen && !this.agentTypeMenuOpen) { return; } const shell = diff --git a/static/src/app/methods/ui/mode.ts b/static/src/app/methods/ui/mode.ts index 33a4f5a0..88ac25a3 100644 --- a/static/src/app/methods/ui/mode.ts +++ b/static/src/app/methods/ui/mode.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { debugLog } from '../common'; +import { persistNewConversationType } from '../../state'; import { usePolicyStore } from '../../../stores/policy'; import { useModelStore } from '../../../stores/model'; import { usePersonalizationStore } from '../../../stores/personalization'; @@ -54,6 +55,7 @@ export const modeMethods = { this.inputCloseMenus(); this.modeMenuOpen = false; this.modelMenuOpen = false; + this.agentTypeMenuOpen = false; }, handleToggleGoalMode() { if (!this.isConnected) { @@ -75,14 +77,26 @@ export const modeMethods = { this.modeMenuOpen = false; this.modelMenuOpen = false; }, - handleToggleAgentMode() { - if (!this.isConnected) { + // 输入栏「智能体/多智能体」类型选择器:空对话态可选(写 newConversationType), + // 已有对话为禁用展示态(类型创建时确定、不可变)。 + handleToggleAgentTypeMenu() { + if (!this.isConnected || this.currentConversationId) { return; } - this.inputCloseMenus(); - // 多智能体模式 → 传统模式;传统模式 → 多智能体模式 - const target = this.multiAgentMode ? '/new' : '/multiagent/new'; - window.location.href = target; + const next = !this.agentTypeMenuOpen; + this.agentTypeMenuOpen = next; + if (next) { + this.permissionMenuOpen = false; + this.modelMenuOpen = false; + this.modeMenuOpen = false; + this.inputCloseMenus?.(); + } + }, + handleSelectNewConversationType(type) { + const normalized = type === 'multi_agent' ? 'multi_agent' : 'agent'; + this.newConversationType = normalized; + persistNewConversationType(normalized); + this.agentTypeMenuOpen = false; }, toggleModeMenu() { if (!this.isConnected || this.streamingMessage) { @@ -256,7 +270,7 @@ export const modeMethods = { } }, async handleStopAllSubAgents() { - const isMultiAgent = !!this.multiAgentMode; + const isMultiAgent = this.currentConversationType === 'multi_agent'; const mode = isMultiAgent ? 'soft_stop' : 'terminate'; const title = isMultiAgent ? '是否暂停所有子智能体?' : '是否终结所有子智能体?'; const message = isMultiAgent diff --git a/static/src/app/methods/ui/route.ts b/static/src/app/methods/ui/route.ts index 5848745b..dc194827 100644 --- a/static/src/app/methods/ui/route.ts +++ b/static/src/app/methods/ui/route.ts @@ -30,59 +30,18 @@ export const routeMethods = { this.titleReady = false; this.currentConversationTitle = ''; this.titleTypingText = ''; - const path = window.location.pathname.replace(/^\/+/, ''); - // 检查多智能体模式入口 + let path = window.location.pathname.replace(/^\/+/, ''); + // 兼容重定向:旧多智能体路由统一收敛到裸路径。 + // 对话类型是 metadata 属性(创建时确定),不再是路由概念。 if (path === 'multiagent/new' || path === 'multiagent') { - this.multiAgentMode = true; - this.currentConversationId = null; - this.currentConversationTitle = '多智能体模式'; - this.logMessageState('bootstrapRoute:clear-messages-for-multiagent-new'); - this.messages = []; - this.titleReady = true; - this.suppressTitleTyping = false; - this.startTitleTyping('多智能体模式', { animate: false }); - this.initialRouteResolved = true; - this.refreshBlankHeroState(); - // 进入多智能体模式时触发后端重建索引以补全 multi_agent_mode 字段 - try { - await fetch('/api/multiagent/rebuild-index', { method: 'POST' }); - } catch (_e) { - // 重建失败不阻断主流程 - } - await this.restoreComposerDraftState('bootstrap-route:multiagent'); - return; - } - // 当 URL 是 /multiagent/conv_xxx 时也走多智能体模式 - if (path.startsWith('multiagent/')) { - this.multiAgentMode = true; + history.replaceState({}, '', '/new'); + path = 'new'; + } else if (path.startsWith('multiagent/')) { const convPart = path.slice('multiagent/'.length); - const convId = convPart.startsWith('conv_') ? convPart : `conv_${convPart}`; - try { - // 统一加载协议:一次 bootstrap 拿元数据+历史+运行状态(纯只读,不切换后端上下文) - const result = await this.enterConversation(convId, { - source: 'refresh', - urlMode: 'replace', - urlPrefix: '/multiagent/' - }); - if (result.success) { - this.currentConversationTitle = result.title || '多智能体模式'; - this.titleReady = true; - this.suppressTitleTyping = false; - this.startTitleTyping(this.currentConversationTitle, { animate: false }); - this.initialRouteResolved = true; - await this.restoreComposerDraftState('bootstrap-route:multiagent-existing'); - return; - } - } catch (error) { - console.warn('[multiagent] 加载多智能体对话失败:', error); - } - // 加载失败回退到 multiagent/new 路径 - history.replaceState({}, '', '/multiagent/new'); - window.location.reload(); - return; + const bareId = convPart.startsWith('conv_') ? convPart.slice('conv_'.length) : convPart; + history.replaceState({}, '', `/${bareId}`); + path = bareId; } - // 非多智能体模式:清除标志 - this.multiAgentMode = false; if (!path || this.isExplicitNewConversationRoute()) { this.currentConversationId = null; this.currentConversationTitle = '新对话'; @@ -93,6 +52,15 @@ export const routeMethods = { this.startTitleTyping('新对话', { animate: false }); this.initialRouteResolved = true; this.refreshBlankHeroState(); + // 触发后端重建索引以补全旧对话的 multi_agent_mode 字段(每次页面加载最多一次) + if (!this.multiAgentIndexRebuildTriggered) { + this.multiAgentIndexRebuildTriggered = true; + try { + await fetch('/api/multiagent/rebuild-index', { method: 'POST' }); + } catch (_e) { + // 重建失败不阻断主流程 + } + } await this.restoreComposerDraftState('bootstrap-route:new'); return; } @@ -102,8 +70,7 @@ export const routeMethods = { // 统一加载协议:一次 bootstrap 拿元数据+历史+运行状态(纯只读,不切换后端上下文) const result = await this.enterConversation(convId, { source: 'refresh', - urlMode: 'replace', - urlPrefix: '/' + urlMode: 'replace' }); if (result.success) { this.currentConversationTitle = result.title || ''; @@ -166,7 +133,8 @@ export const routeMethods = { }, isExplicitNewConversationRoute() { const normalizedPath = window.location.pathname.replace(/^\/+|\/+$/g, ''); - return normalizedPath === 'new' || normalizedPath === 'multiagent/new' || normalizedPath === 'multiagent'; + // 多智能体旧路由在 bootstrapRoute 已被重定向到 /new,这里只判裸新建路由 + return normalizedPath === 'new'; }, stripConversationPrefix(conversationId) { if (!conversationId) return ''; diff --git a/static/src/app/state.ts b/static/src/app/state.ts index 2e6bdbb8..61650ab7 100644 --- a/static/src/app/state.ts +++ b/static/src/app/state.ts @@ -23,15 +23,44 @@ export const persistWorkspaceMode = (isHostMode: boolean): void => { } }; +/* 待创建对话类型缓存:/new 页输入栏「智能体/多智能体」选择器的上次选择, + 刷新后恢复,避免每次新建都要重新选。 */ +const NEW_CONVERSATION_TYPE_STORAGE_KEY = 'agents_new_conversation_type'; + +const loadNewConversationType = (): 'agent' | 'multi_agent' => { + try { + const v = window.localStorage.getItem(NEW_CONVERSATION_TYPE_STORAGE_KEY); + return v === 'multi_agent' ? 'multi_agent' : 'agent'; + } catch { + return 'agent'; + } +}; + +export const persistNewConversationType = (type: string): void => { + try { + window.localStorage.setItem(NEW_CONVERSATION_TYPE_STORAGE_KEY, type); + } catch { + /* ignore */ + } +}; + const cachedWorkspaceMode = loadCachedWorkspaceMode(); +const cachedNewConversationType = loadNewConversationType(); export function dataState() { return { // 路由相关 initialRouteResolved: false, dropToolEvents: false, - // 多智能体模式开关 - multiAgentMode: false, + // 当前打开对话的类型('normal' | 'multi_agent',创建时确定、不可变); + // 空对话态为 null。权威来源是对话 metadata.multi_agent_mode。 + currentConversationType: null, + // /new 页输入栏选择器的待创建类型('agent' | 'multi_agent',localStorage 持久化) + newConversationType: cachedNewConversationType, + // 输入栏类型选择器菜单开关 + agentTypeMenuOpen: false, + // 多智能体索引重建触发去重(每次页面加载最多一次) + multiAgentIndexRebuildTriggered: false, // 轮询模式标志(禁用 WebSocket 事件处理) usePollingMode: true, diff --git a/static/src/app/watchers.ts b/static/src/app/watchers.ts index 65839c50..38a1142d 100644 --- a/static/src/app/watchers.ts +++ b/static/src/app/watchers.ts @@ -5,9 +5,6 @@ import { useQuickDockStore } from '../stores/quickDock'; import { useFileStore } from '../stores/file'; export const watchers = { - multiAgentMode(newValue) { - useConversationStore().$patch({ multiAgentMode: !!newValue }); - }, inputMessage() { this.autoResizeInput(); if (typeof this.scheduleComposerDraftPersist === 'function') { @@ -74,6 +71,11 @@ export const watchers = { if (newValue !== oldValue) { // 同步到 conversationStore(QuickDock 等组件监听 store 侧的 id) useConversationStore().setCurrentConversationId(newValue || null); + if (!newValue) { + // 空对话态:对话类型复位(进入对话时由 enterConversation 从 metadata 落地) + this.currentConversationType = null; + useConversationStore().$patch({ multiAgentMode: false }); + } const quickDock = useQuickDockStore(); // 关闭详情/预览/菜单等瞬态(必须立即) quickDock.resetTransient(); diff --git a/static/src/components/chat/actions/toolRenderers.ts b/static/src/components/chat/actions/toolRenderers.ts index 1752a150..d101ad9c 100644 --- a/static/src/components/chat/actions/toolRenderers.ts +++ b/static/src/components/chat/actions/toolRenderers.ts @@ -1564,7 +1564,10 @@ function renderAnswerSubAgentQuestion(result: any, args: any): string { } function renderCreateCustomAgent(result: any, args: any): string { - const status = formatToolStatusLabel(result, '✓ 已创建', '✗ 创建失败'); + const overwritten = !!result?.overwritten; + const status = result?.success + ? (overwritten ? '✓ 已覆盖更新' : '✓ 已创建') + : '✗ 创建失败'; const roleId = args.role_id ?? result.role_id ?? ''; const name = args.name ?? result.name ?? roleId; diff --git a/static/src/components/input/InputComposer.vue b/static/src/components/input/InputComposer.vue index a5b3d7c8..8268cc3a 100644 --- a/static/src/components/input/InputComposer.vue +++ b/static/src/components/input/InputComposer.vue @@ -270,6 +270,7 @@
+
+
+ +
+ +
+
+
-
+
+ + +
+
+ +
运行中的任务
+
+
(); const uiStore = useUiStore(); const conversationStore = useConversationStore(); const personalizationStore = usePersonalizationStore(); +/** 侧边栏对话类型过滤器(普通/多智能体),唯一真相在 conversation store */ +const sidebarType = computed(() => conversationStore.sidebarConversationType); + +/** 列表切换动画方向:切到多智能体向左滑出/从右侧进入,切回反向 */ +const slideTransitionName = computed(() => + sidebarType.value === 'multi_agent' ? 'type-slide-left' : 'type-slide-right' +); + +const setSidebarType = (type: 'normal' | 'multi_agent') => { + if (conversationStore.sidebarConversationType === type) return; + conversationStore.setSidebarConversationType(type); + emit('conversation-type-change', type); +}; + +const toggleSidebarType = () => { + setSidebarType(sidebarType.value === 'multi_agent' ? 'normal' : 'multi_agent'); +}; + const { sidebarCollapsed: collapsed } = storeToRefs(uiStore); const { searchQuery, diff --git a/static/src/composables/useLegacySocket.ts b/static/src/composables/useLegacySocket.ts index 8f94572c..b68fd212 100644 --- a/static/src/composables/useLegacySocket.ts +++ b/static/src/composables/useLegacySocket.ts @@ -827,8 +827,8 @@ export async function initializeLegacySocket(ctx: any) { ctx.promoteConversationToTop(convId); const pathFragment = ctx.stripConversationPrefix(convId); const currentPath = window.location.pathname.replace(/^\/+/, ''); - const urlPrefix = ctx.multiAgentMode ? '/multiagent/' : '/'; - const targetPath = `${urlPrefix}${pathFragment}`; + // 对话类型不再是路由概念,统一裸路径 / + const targetPath = `/${pathFragment}`; if (data.created) { history.pushState({ conversationId: convId }, '', targetPath); } else if (currentPath !== pathFragment) { @@ -1706,7 +1706,7 @@ export async function initializeLegacySocket(ctx: any) { // 多智能体模式下,若仍有运行中的多智能体实例,保持对话运行态,继续接收后续消息; // 但此时主智能体已空闲,不应阻塞输入区(waitingForSubAgent=false)。 if (hasRunningMultiAgent) { - } else if (ctx.multiAgentMode || !data.has_running_sub_agents) { + } else if (ctx.currentConversationType === 'multi_agent' || !data.has_running_sub_agents) { if (ctx.waitingForSubAgent) { ctx.waitingForSubAgent = false; } @@ -1734,8 +1734,8 @@ export async function initializeLegacySocket(ctx: any) { } socketLog('等待子智能体完成:', data); - // 多智能体模式下,子智能体 idle/running 是常态,不阻塞主对话输入区 - if (!ctx.multiAgentMode) { + // 多智能体对话下,子智能体 idle/running 是常态,不阻塞主对话输入区 + if (ctx.currentConversationType !== 'multi_agent') { // 设置标志:有子智能体在运行,阻止状态重置 ctx.waitingForSubAgent = true; diff --git a/static/src/stores/conversation.ts b/static/src/stores/conversation.ts index 93f431c6..6010033e 100644 --- a/static/src/stores/conversation.ts +++ b/static/src/stores/conversation.ts @@ -1,5 +1,25 @@ import { defineStore } from 'pinia'; +/* 侧边栏对话类型过滤器缓存(普通/多智能体):记住用户上次查看的列表类型。 */ +const SIDEBAR_CONVERSATION_TYPE_STORAGE_KEY = 'agents_sidebar_conversation_type'; + +const loadSidebarConversationType = (): 'normal' | 'multi_agent' => { + try { + const v = window.localStorage.getItem(SIDEBAR_CONVERSATION_TYPE_STORAGE_KEY); + return v === 'multi_agent' ? 'multi_agent' : 'normal'; + } catch { + return 'normal'; + } +}; + +const persistSidebarConversationType = (type: string): void => { + try { + window.localStorage.setItem(SIDEBAR_CONVERSATION_TYPE_STORAGE_KEY, type); + } catch { + /* ignore */ + } +}; + export interface ConversationSummary { id: string; title: string; @@ -54,7 +74,10 @@ interface ConversationState { runningWorkspaceTasks: any[]; acknowledgedCompletedTaskIds: string[]; workspaceGroups: WorkspaceConversationGroup[]; + /** 当前打开对话是否为多智能体(由 enterConversation/空对话 watcher 写入,subAgent store 读取) */ multiAgentMode: boolean; + /** 侧边栏对话类型过滤器:'normal' 普通对话 | 'multi_agent' 多智能体对话(localStorage 持久化) */ + sidebarConversationType: 'normal' | 'multi_agent'; } export const useConversationStore = defineStore('conversation', { @@ -82,13 +105,21 @@ export const useConversationStore = defineStore('conversation', { runningWorkspaceTasks: [], acknowledgedCompletedTaskIds: [], workspaceGroups: [], - multiAgentMode: false + multiAgentMode: false, + sidebarConversationType: loadSidebarConversationType() }), actions: { /** 当前对话 id 同步(由 app watcher 在 this.currentConversationId 变化时写入) */ setCurrentConversationId(id: string | null) { this.currentConversationId = id; }, + /** 侧边栏对话类型过滤器切换(左右切换控件写入,驱动列表请求的服务端过滤) */ + setSidebarConversationType(type: 'normal' | 'multi_agent') { + const normalized = type === 'multi_agent' ? 'multi_agent' : 'normal'; + if (this.sidebarConversationType === normalized) return; + this.sidebarConversationType = normalized; + persistSidebarConversationType(normalized); + }, resetConversations() { this.conversations = []; this.searchResults = []; @@ -204,7 +235,8 @@ export const useConversationStore = defineStore('conversation', { const fetchOffset = refresh ? 0 : group.offset; group.loading = true; try { - const maParam = this.multiAgentMode ? '&multi_agent_mode=1' : '&multi_agent_mode=0'; + // 列表过滤由侧边栏类型过滤器(普通/多智能体)决定 + const maParam = this.sidebarConversationType === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0'; const response = await fetch( `/api/conversations?workspace_id=${encodeURIComponent(workspaceId)}&limit=${group.fetchLimit}&offset=${fetchOffset}${maParam}` ); @@ -251,8 +283,9 @@ export const useConversationStore = defineStore('conversation', { if (group.hasMore) { try { const fetchOffset = group.conversations.length; + const maParam = this.sidebarConversationType === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0'; const response = await fetch( - `/api/conversations?workspace_id=${encodeURIComponent(workspaceId)}&limit=${group.bufferLimit}&offset=${fetchOffset}` + `/api/conversations?workspace_id=${encodeURIComponent(workspaceId)}&limit=${group.bufferLimit}&offset=${fetchOffset}${maParam}` ); const data = await response.json(); if (data.success) { diff --git a/static/src/styles/components/chat/_chat-area.scss b/static/src/styles/components/chat/_chat-area.scss index 40f5a997..0a31bb0e 100644 --- a/static/src/styles/components/chat/_chat-area.scss +++ b/static/src/styles/components/chat/_chat-area.scss @@ -2428,3 +2428,98 @@ a.md-download-link { vertical-align: middle; line-height: 1; } + +/* ========== 深色主题变体(对话区组件) ========== */ +body[data-theme='dark'] { + /* 用户消息块 - 使用浅灰色 */ + .user-message .message-text { + background: var(--badge-bg); + box-shadow: none; + } + + /* AI 消息块 - 使用浅灰色 */ + .assistant-message .message-text { + background: var(--badge-bg); + border-left-color: var(--border-strong); + } + + /* 思考块和工具块 - 使用更深的颜色(类似选中状态) */ + .thinking-block, + .tool-block, + .collapsible-block { + background: var(--surface-panel); + } + + .collapsible-header { + background: var(--surface-panel); + } + + /* 代码块 - 代码区域纯黑底+白字 */ + .code-block-wrapper { + background: var(--badge-bg); + border-color: var(--border-card-strong); + } + + .text-output pre { + background: var(--surface-base) !important; + color: var(--text-primary); + } + + .text-output pre code { + background: transparent !important; + color: var(--text-primary); + } + + /* 表格 */ + .text-output .text-content table { + background: var(--badge-bg); + } + + .text-output .text-content thead { + background: var(--surface-panel); + } + + /* 滚动锁定按钮 */ + .scroll-lock-btn { + background: var(--badge-bg); + border-color: var(--border-card-strong); + box-shadow: none; + } + + .scroll-lock-btn:hover { + background: var(--surface-muted); + box-shadow: none; + } + + .scroll-lock-toggle.locked .scroll-lock-btn { + border-color: color-mix(in srgb, var(--on-accent) 20%, transparent); + box-shadow: none; + } + + .scroll-lock-toggle.locked .scroll-lock-btn img { + filter: brightness(0) invert(1); + } + + .scroll-lock-toggle.locked .scroll-lock-btn svg { + stroke: var(--text-primary); + } + + /* 顶部栏 - 黑色背景,白色字体 */ + .conversation-ribbon { + background: var(--surface-panel) !important; + } + + .conversation-ribbon::after { + background: var(--surface-panel) !important; + } + + /* 模型选择下拉菜单 - 和输入栏一样的底色 */ + .model-mode-dropdown { + background: var(--badge-bg) !important; + box-shadow: none !important; + } + + .dropdown-item:hover:not(.disabled) { + background: var(--surface-muted) !important; + } +} diff --git a/static/src/styles/components/input/_composer.scss b/static/src/styles/components/input/_composer.scss index 64837583..747a760c 100644 --- a/static/src/styles/components/input/_composer.scss +++ b/static/src/styles/components/input/_composer.scss @@ -339,6 +339,51 @@ body[data-theme='dark'] { .stadium-input-highlight .skill-md-link { color: var(--state-info); } + + /* 输入栏壳体 - 灰黑色(覆盖基础 --surface-raised) */ + .stadium-shell, + .stadium-shell.is-focused, + .stadium-shell.has-text { + background: var(--badge-bg); + border-color: var(--border-card-strong); + } + + .stadium-input::placeholder { + color: var(--text-tertiary); + } + + /* 发送按钮 - 白底黑三角,浅阴影做层次 */ + .stadium-btn.send-btn { + background: var(--text-primary); + color: var(--surface-raised); + box-shadow: 0 1px 3px var(--shadow-color); + } + + .stadium-btn.send-btn:hover:not(:disabled) { + background: var(--text-primary); + box-shadow: 0 1px 3px var(--shadow-color); + } + + .stadium-btn.send-btn .send-icon { + border-left-color: var(--surface-raised); + } + + .stadium-btn.send-btn .stop-icon { + background-color: var(--surface-raised); + } + + /* 快捷菜单/子菜单/对话类型菜单 - 与输入栏同一灰黑色 */ + .quick-menu, + .quick-submenu, + .agent-type-switcher__menu { + background: var(--badge-bg); + border-color: var(--border-card-strong); + } + + .menu-entry:hover:not(:disabled), + .menu-entry.active { + background: var(--tab-active); + } } body[data-theme='light'] { @@ -350,6 +395,34 @@ body[data-theme='light'] { .stadium-input-highlight .skill-md-link { color: var(--state-info); } + + /* 发送按钮 - 黑底白三角,浅阴影做层次 */ + .stadium-btn.send-btn { + background: var(--text-primary); + color: var(--surface-raised); + box-shadow: 0 1px 3px var(--shadow-color); + } + + .stadium-btn.send-btn:hover:not(:disabled) { + background: var(--text-primary); + box-shadow: 0 1px 3px var(--shadow-color); + } + + .stadium-btn.send-btn .send-icon { + border-left-color: var(--surface-raised); + } + + .stadium-btn.send-btn .stop-icon { + background-color: var(--surface-raised); + } + + .menu-entry:hover:not(:disabled) { + background: var(--chip-bg); + } + + .menu-entry.active { + background: var(--surface-muted); + } } .permission-switcher { @@ -1049,10 +1122,6 @@ body[data-theme='light'] { } /* Blank conversation hero */ -.chat-container { - position: relative; -} - .blank-hero-overlay { position: absolute; inset: 0; @@ -1427,3 +1496,122 @@ body[data-theme='light'] { align-items: center; justify-content: center; } + +/* ========== 对话类型选择器(输入栏底行 + 按钮右侧) ========== */ +.input-actions-left { + display: flex; + align-items: center; + gap: 6px; +} + +.agent-type-switcher { + position: relative; + display: inline-flex; + align-items: center; +} + +.agent-type-switcher__btn { + border: none; + background: transparent; + color: var(--text-primary); + border-radius: 7px; + padding: 4px 10px; + font-size: 13px; + line-height: 1.2; + height: 28px; + cursor: pointer; + box-shadow: none; + display: inline-flex; + align-items: center; + gap: 5px; + white-space: nowrap; + transition: + background 140ms ease, + color 140ms ease; +} + +.agent-type-switcher__btn:hover:not(:disabled) { + background: var(--hover-bg); +} + +.agent-type-switcher__btn:disabled { + opacity: 0.55; + cursor: default; +} + +.agent-type-switcher__icon { + color: var(--text-secondary); + flex-shrink: 0; +} + +.agent-type-switcher__caret { + font-size: 13px; + color: var(--text-secondary); + transform: rotate(90deg); + transition: transform 0.18s ease; + line-height: 1; +} + +.agent-type-switcher__caret.open { + transform: rotate(270deg); +} + +/* 菜单底色/边框/圆角与 + 菜单(.quick-menu)一致 */ +.agent-type-switcher__menu { + position: absolute; + left: 0; + bottom: calc(100% + 14px); + width: 220px; + max-height: 240px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px; + border: 1px solid var(--border-default); + border-radius: 18px; + background: var(--surface-soft); + box-shadow: none; + z-index: 40; +} + +.agent-type-switcher__menu-item { + border: none; + background: transparent; + text-align: left; + border-radius: 12px; + padding: 10px 12px; + min-height: 48px; + cursor: pointer; + color: var(--text-primary); + display: flex; + flex-direction: column; + gap: 2px; + justify-content: center; + transition: background 0.15s ease; +} + +.agent-type-switcher__menu-item:hover { + background: var(--hover-bg); +} + +.agent-type-switcher__menu-item.is-active { + background: var(--hover-bg); +} + +.agent-type-switcher__menu-label { + font-size: 13px; + font-weight: 600; +} + +.agent-type-switcher__menu-desc { + font-size: 11px; + color: var(--text-secondary); + line-height: 1.35; +} + +@media (max-width: 768px) { + .agent-type-switcher__menu { + width: min(220px, calc(100vw - 40px)); + } +} diff --git a/static/src/styles/components/overlays/_overlays.scss b/static/src/styles/components/overlays/_overlays.scss index 4fc4c971..66274bee 100644 --- a/static/src/styles/components/overlays/_overlays.scss +++ b/static/src/styles/components/overlays/_overlays.scss @@ -2869,41 +2869,6 @@ body[data-theme='light'] { box-shadow: none; } - .stadium-shell.is-focused, - .stadium-shell.has-text, - .stadium-shell.is-multiline.is-focused, - .stadium-shell.is-multiline.has-text { - box-shadow: none; - } - - /* 发送按钮 - 黑底白三角,浅阴影做层次 */ - .stadium-btn.send-btn { - background: var(--text-primary); - color: var(--surface-raised); - box-shadow: 0 1px 3px var(--shadow-color); - } - - .stadium-btn.send-btn:hover:not(:disabled) { - background: var(--text-primary); - box-shadow: 0 1px 3px var(--shadow-color); - } - - .stadium-btn.send-btn .send-icon { - border-left-color: var(--surface-raised); - } - - .stadium-btn.send-btn .stop-icon { - background-color: var(--surface-raised); - } - - .menu-entry:hover:not(:disabled) { - background: var(--chip-bg); - } - - .menu-entry.active { - background: var(--surface-muted); - } - /* 机器人图标保持黑色 */ .status-logo { color: var(--text-primary); @@ -2973,15 +2938,6 @@ body[data-theme='light'] { color: var(--text-secondary) !important; } - /* 删除按钮 - 使用深灰色 */ - .conversation-action-btn.delete-btn { - background: var(--text-secondary) !important; - } - - .conversation-action-btn.delete-btn:hover { - background: color-mix(in srgb, var(--text-secondary) 70%, var(--text-primary)) !important; - } - /* 个人空间 - consideration 删除按钮 */ .consideration-remove { color: var(--text-secondary) !important; @@ -3025,73 +2981,6 @@ body[data-theme='dark'] { box-shadow: none; } - .stadium-shell.is-focused, - .stadium-shell.has-text, - .stadium-shell.is-multiline.is-focused, - .stadium-shell.is-multiline.has-text { - box-shadow: none; - } - - /* 输入栏 - 改为灰黑色 */ - .stadium-shell { - background: var(--badge-bg); - border-color: var(--border-card-strong); - } - - .stadium-shell.is-focused, - .stadium-shell.has-text { - background: var(--badge-bg); - border-color: var(--border-card-strong); - } - - /* 输入框文字颜色 */ - .stadium-input { - color: var(--text-primary); - } - - .stadium-input::placeholder { - color: var(--text-tertiary); - } - - /* 发送按钮 - 白底黑三角,浅阴影做层次 */ - .stadium-btn.send-btn { - background: var(--text-primary); - color: var(--surface-raised); - box-shadow: 0 1px 3px var(--shadow-color); - } - - .stadium-btn.send-btn:hover:not(:disabled) { - background: var(--text-primary); - box-shadow: 0 1px 3px var(--shadow-color); - } - - .stadium-btn.send-btn .send-icon { - border-left-color: var(--surface-raised); - } - - .stadium-btn.send-btn .stop-icon { - background-color: var(--surface-raised); - } - - /* 快捷菜单 - 改为灰黑色 */ - .quick-menu { - background: var(--badge-bg); - border-color: var(--border-card-strong); - } - - .quick-submenu { - background: var(--badge-bg); - border-color: var(--border-card-strong); - } - - .menu-entry:hover:not(:disabled) { - background: var(--theme-tab-active); - } - - .menu-entry.active { - background: var(--theme-tab-active); - } - /* 机器人图标保持白色 */ .status-logo { color: var(--text-primary); @@ -3166,29 +3055,6 @@ body[data-theme='dark'] { color: var(--text-secondary) !important; } - /* 空对话欢迎语 - 白色 */ - .blank-hero-text { - color: var(--text-primary) !important; - } - - /* 删除按钮 - 使用浅灰色 */ - .conversation-action-btn.delete-btn { - background: var(--text-secondary) !important; - } - - .conversation-action-btn.delete-btn:hover { - background: color-mix(in srgb, var(--text-secondary) 70%, var(--text-primary)) !important; - } - - /* 顶部栏 - 黑色背景,白色字体 */ - .conversation-ribbon { - background: var(--surface-panel) !important; - } - - .conversation-ribbon::after { - background: var(--surface-panel) !important; - } - .mobile-topbar-title { color: var(--text-primary) !important; text-shadow: none !important; @@ -3269,62 +3135,6 @@ body[data-theme='dark'] { filter: brightness(0) invert(1) !important; } - /* 用量统计 - 数字白色 */ - .stat-value { - color: var(--text-primary) !important; - } - - /* 对话区域顶部的模型选择器 - 白色字体 */ - .conversation-ribbon__selector { - color: var(--text-primary) !important; - } - - .conversation-ribbon__selector:hover { - color: var(--text-primary) !important; - } - - .conversation-ribbon__selector.open { - color: var(--text-primary) !important; - } - - .selector-model { - color: var(--text-primary) !important; - } - - .selector-mode { - color: var(--text-secondary) !important; - } - - .conversation-ribbon__text { - color: var(--text-primary) !important; - } - - /* 模型选择下拉菜单 - 和输入栏一样的底色 */ - .model-mode-dropdown { - background: var(--badge-bg) !important; - box-shadow: none !important; - } - - .dropdown-title { - color: var(--text-secondary) !important; - } - - .dropdown-item { - color: var(--text-primary) !important; - } - - .dropdown-item:hover:not(.disabled) { - background: var(--surface-muted) !important; - } - - .item-label { - color: var(--text-primary) !important; - } - - .item-desc { - color: var(--text-secondary) !important; - } - /* 个人空间 - consideration 列表项的虚线边框 */ .consideration-item { border-color: var(--text-secondary) !important; @@ -3360,105 +3170,6 @@ body[data-theme='dark'] { background: var(--surface-panel) !important; } - /* 快捷菜单 - 和输入栏一样的底色 */ - .quick-menu { - background: var(--badge-bg) !important; - border-color: var(--border-card-strong) !important; - } - - .quick-submenu { - background: var(--badge-bg) !important; - border-color: var(--border-card-strong) !important; - } - - .menu-entry { - color: var(--text-primary) !important; - } - - .menu-entry:hover:not(:disabled) { - background: var(--theme-tab-active) !important; - } - - .menu-entry.active { - background: var(--theme-tab-active) !important; - } - - .menu-entry .entry-arrow { - color: var(--text-secondary) !important; - } - - .submenu-label { - color: var(--text-primary) !important; - } - - /* 用户消息块 - 使用浅灰色 */ - .user-message .message-text { - background: var(--badge-bg); - box-shadow: none; - } - - /* AI 消息块 - 使用浅灰色 */ - .assistant-message .message-text { - background: var(--badge-bg); - border-left-color: var(--border-strong); - } - - /* 思考块和工具块 - 使用更深的颜色(类似选中状态) */ - .thinking-block, - .tool-block, - .collapsible-block { - background: var(--surface-panel); - } - - .collapsible-header { - background: var(--surface-panel); - } - - .collapsible-header:hover { - background: var(--surface-panel); - } - - /* 代码块 - 代码区域纯黑底+白字 */ - .code-block-wrapper { - background: var(--badge-bg); - border-color: var(--border-card-strong); - } - - .text-output pre { - background: var(--surface-base) !important; - color: var(--text-primary); - } - - .text-output pre code { - background: transparent !important; - color: var(--text-primary); - } - - /* 滚动锁定按钮 */ - .scroll-lock-btn { - background: var(--badge-bg); - border-color: var(--border-card-strong); - box-shadow: none; - } - - .scroll-lock-btn:hover { - background: var(--surface-muted); - box-shadow: none; - } - - .scroll-lock-toggle.locked .scroll-lock-btn { - border-color: color-mix(in srgb, var(--on-accent) 20%, transparent); - box-shadow: none; - } - - .scroll-lock-toggle.locked .scroll-lock-btn img { - filter: brightness(0) invert(1); - } - - .scroll-lock-toggle.locked .scroll-lock-btn svg { - stroke: var(--text-primary); - } - /* 左侧面板头部(文件/待办事项/子智能体标签栏) */ .sidebar-header { background: var(--surface-rail); @@ -3475,25 +3186,6 @@ body[data-theme='dark'] { padding: 0 16px !important; } - .sidebar-manage-btn { - background: var(--badge-bg); - border-color: var(--border-card-strong); - } - - .sidebar-manage-btn:hover { - background: var(--surface-muted); - border-color: var(--border-strong); - } - - .sidebar-view-toggle { - background: var(--badge-bg); - border-color: var(--border-card-strong); - } - - .sidebar-view-toggle:hover { - background: var(--surface-muted); - } - .panel-menu { background: var(--badge-bg); border-color: var(--border-card-strong); @@ -3503,18 +3195,6 @@ body[data-theme='dark'] { background: var(--border-card-strong); } - .todo-empty, - .sub-agent-empty, - .file-tree-empty { - background: var(--surface-panel); - border-color: var(--border-card-strong); - } - - .todo-task { - background: var(--badge-bg); - border-color: var(--border-card-strong); - } - .folder-header, .file-node { background: transparent; @@ -3527,49 +3207,6 @@ body[data-theme='dark'] { .file-node.file-leaf:hover { background: var(--badge-bg); } - - /* 聚焦文件面板 */ - .file-tab { - background: var(--surface-panel); - border-color: var(--border-card-strong); - } - - .tab-header { - background: var(--badge-bg); - } - - .file-content { - background: var(--surface-base); - } - - .file-content code { - color: var(--text-primary); - } - - .focus-close-btn { - background: var(--badge-bg); - } - - .focus-close-btn:hover { - background: var(--surface-muted); - } - - .approval-close-btn { - background: var(--badge-bg); - } - - .approval-close-btn:hover { - background: var(--surface-muted); - } - - /* 表格 */ - .text-output .text-content table { - background: var(--badge-bg); - } - - .text-output .text-content thead { - background: var(--surface-panel); - } } .tutorial-page { diff --git a/static/src/styles/components/panels/_right-panel.scss b/static/src/styles/components/panels/_right-panel.scss index 89a27025..e697e0c2 100644 --- a/static/src/styles/components/panels/_right-panel.scss +++ b/static/src/styles/components/panels/_right-panel.scss @@ -826,3 +826,36 @@ body[data-theme='dark'] .git-change-file__open-icon { color: var(--theme-surface-strong); background: var(--accent-strong); } + +/* ========== 深色主题变体(右侧面板组件) ========== */ +body[data-theme='dark'] { + /* 聚焦文件面板 */ + .file-tab { + background: var(--surface-panel); + border-color: var(--border-card-strong); + } + + .tab-header { + background: var(--badge-bg); + } + + .file-content { + background: var(--surface-base); + } + + .focus-close-btn { + background: var(--badge-bg); + } + + .focus-close-btn:hover { + background: var(--surface-muted); + } + + .approval-close-btn { + background: var(--badge-bg); + } + + .approval-close-btn:hover { + background: var(--surface-muted); + } +} diff --git a/static/src/styles/components/sidebar/_conversation.scss b/static/src/styles/components/sidebar/_conversation.scss index bb919ecf..94d268b2 100644 --- a/static/src/styles/components/sidebar/_conversation.scss +++ b/static/src/styles/components/sidebar/_conversation.scss @@ -808,6 +808,7 @@ body[data-theme='dark'] .workspace-group-header.search-result-group-header:hover opacity: 1; } +.workspace-type-btn, .workspace-new-btn, .workspace-more-btn { width: 28px; @@ -824,6 +825,7 @@ body[data-theme='dark'] .workspace-group-header.search-result-group-header:hover color 140ms ease; } +.workspace-type-btn:hover, .workspace-new-btn:hover, .workspace-more-btn:hover, .workspace-group-actions.visible .workspace-more-btn[aria-expanded='true'] { @@ -831,6 +833,12 @@ body[data-theme='dark'] .workspace-group-header.search-result-group-header:hover color: var(--text-primary); } +.workspace-type-btn-label { + font-size: 12px; + font-weight: 600; + line-height: 1; +} + .workspace-more-btn span { position: relative; display: inline-block; @@ -1103,3 +1111,78 @@ body[data-theme='dark'] .conversation-sidebar:not(.collapsed) .personal-page-btn body[data-theme='dark'] .conversation-sidebar .load-more-btn:hover:not(:disabled) { background: var(--hover-bg) !important; } + +/* ========== 侧边栏对话类型切换(普通 / 多智能体) ========== */ +.conversation-type-switch { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 2px; + margin: 6px 8px 0; + padding: 2px; + height: 30px; + border-radius: 8px; + background: var(--surface-muted); +} + +.conversation-type-option { + flex: 1 1 0; + height: 26px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--text-secondary); + font-size: 12px; + line-height: 1; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: + background 140ms ease, + color 140ms ease; +} + +.conversation-type-option:hover:not(.active) { + color: var(--text-primary); +} + +.conversation-type-option.active { + background: var(--surface-soft); + color: var(--text-primary); + font-weight: 600; +} + +/* 列表区域整体左右滑动切换动画 */ +.conversation-list-pane { + min-height: 0; +} + +.type-slide-left-enter-active, +.type-slide-left-leave-active, +.type-slide-right-enter-active, +.type-slide-right-leave-active { + transition: + transform 200ms ease, + opacity 200ms ease; +} + +.type-slide-left-enter-from { + transform: translateX(100%); + opacity: 0; +} + +.type-slide-left-leave-to { + transform: translateX(-100%); + opacity: 0; +} + +.type-slide-right-enter-from { + transform: translateX(-100%); + opacity: 0; +} + +.type-slide-right-leave-to { + transform: translateX(100%); + opacity: 0; +}