Refine Agent sidebar icons and manual expansion

This commit is contained in:
鲨鱼辣椒 committed 2026-10-12 20:52:14 +08:00
1 parent a340c0fd94
commit ad1d2f261d
7 files changed
+221 -20

No files matched your search

@@ -0,0 +1,81 @@
# Task: Keep Agent sidebar collapsed until explicit expansion
## Identity
- Task ID: 20261012-agent-sidebar-manual-67c1a8e4
- Mode: Feature
- Branch: codex/20261012-agent-sidebar-manual-67c1a8e4-agent-sidebar-manual
- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261012-agent-sidebar-manual-67c1a8e4
- Base commit: a340c0fd94ff5711f00a08312e80db4b148ff101
- Owner: codex
- Status: Ready for Integration
## Scope
- Remove the advanced-settings resource policy helper sentence. Keep the Agent sidebar collapsed on pointer/focus arrival; explicit toggle remains the only expansion action. Refresh the “我的智能体” and “渠道” SVG icons in the existing visual language, preserving labels and routes.
## Intent And Constraints
- User confirmed the requested icons are the sidebar “我的智能体” and “渠道”, not the top return/collapse controls. Product-language communication; no temporary app update. Preserve return/collapse alignment, cards-first entry, model/resource choices and permission enforcement. Restrict changed hover policy to cloud_agents; Code and other modules keep existing peek. No real cloud saves/publish/model requests during checks; only isolated fixtures.
## Project Context Loaded
Task context:
- Task ID: 20261012-agent-sidebar-manual-67c1a8e4; Feature; owner codex.
- Branch: codex/20261012-agent-sidebar-manual-67c1a8e4-agent-sidebar-manual.
- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261012-agent-sidebar-manual-67c1a8e4.
- Base: a340c0fd94ff5711f00a08312e80db4b148ff101, completed conversation polish including cards-first navigation.
- Bundled check/start/status passed and ownership/reservation exactly match. 37 peer records read-only: 36 concrete scopes, one old child-language record undefined. No blocking semantic conflict. Relevant overlaps: entry-nav same-row return/toggle and cards-first behavior, config-simplify model/resource controls and existing permission semantics, Code hover/resize behavior outside Agents.
Read:
- AGENTS.md; 05-agent-entry read-before-planning, memory-index and planning-gate; own task record.
- 00-brief positioning/success; 20-architecture community-collaboration; task-relevant current-state integrated snapshot, decision-index/system-overview and full personal-cloud-agents ADR.
- Task-relevant domain/evidence/reflection/commitments indexes; scopes/constraints/promotion candidates of every peer.
- Current MainLayout, Sidebar, TitleBar, ConfigurationFields and focused navigation/peek checks. Design skill search; adapt its friendly/icon consistency guidance to the existing cream/pastel/orange product language.
Relevant understanding:
- Project serves students/creators with creation and AI, excludes courses in the client. Main/WS/Yuxi own authorization, resources, accounting and run state; removing text does not change policy.
- Integrated snapshot predates current simplified Agents UI. Human-approved base is authoritative for this feature; no canonical docs are edited in Feature mode.
- Existing collapse preview listens across toggle, titlebar, focus and sidebar zones. Agents must have no transient preview, including a preview left over from another route. Explicit toggle and keyboard activation remain accessible.
- Navigation labels/routes, current-page indication, return control/native window safe spaces and non-Agent preview remain intact. Personal cloud resource selections/enforcement unchanged.
- Evidence commitments distinguish fixture/compiled source from real account, paid provider or installed app acceptance. User expressly forbids trial update; no live app operation is needed.
- Likely files: MainLayout hover policy, Sidebar scoped SVG presentation, ConfigurationFields one sentence, README and focused behavior checks.
- Unknowns resolved: user clarified both sidebar icon positions; no prescribed replacement artwork, use consistent existing SVG set. Old undefined peer has no concrete dependency.
Gate result:
- Passed.
## Plan
1. Scope sidebar-preview suppression to Agents and clear stale transient state across route changes; preserve explicit mouse/keyboard toggle and other modules.
2. Delete only the resource helper sentence and refresh the two sidebar icons without changing navigation or policy.
3. Verify hover/toggle/route transitions, resource UI and relevant Electron fixture, typecheck/lint/build; update task-scoped evidence and save changes. Do not update the temporary app.
## Outcome
- Removed only the advanced-settings resource-policy helper sentence. Model requirements, resource selection and underlying access enforcement are unchanged.
- Agents, including channels and conversation/configuration routes, now disable transient sidebar preview. Hovering the toggle, titlebar or left edge and focusing the toggle keep a collapsed sidebar hidden. Clicking the toggle or activating it with the keyboard still changes the persisted collapsed preference.
- Entering/leaving the Agents module resets preview sources and cancels pending close timers. A preview from Code cannot appear in Agents or revive after returning; Code preview and the fixed expanded Canvas sidebar remain available.
- Replaced the two confirmed sidebar icons with a pale-purple conversation robot for “我的智能体” and a pale-blue connection-node icon for “渠道”. Labels, current-page indication and destinations are preserved. Retained the separate Bot icon used by the robot module.
- README and focused checks reflect the final behavior. No temporary application, runtime pointer, manifest, user profile or cloud data was updated.
## Verification
- `typecheck`: passed after preserving the robot module's shared Bot import. The initial check caught that missing import; it was corrected before the final build and UI check.
- Scoped ESLint for MainLayout, Sidebar, ConfigurationFields, the sidebar-preview unit check and the Electron check: passed. `git diff --check`: passed.
- `build:vite`: passed on the final source. Build log: `/tmp/makelore-agent-sidebar-manual-build.log`.
- `cloud-agents-page.test.tsx`: 31 checks passed. MainLayout sidebar-preview and module-gate checks: 18 passed. Covers both Agent routes, all four preview sources, explicit toggle, module transitions, pending timers, unchanged Code preview and Canvas expansion.
- Production Electron isolated fixture (`tests/e2e/cloud-agents.spec.ts`): 1 scenario passed. Actual shell checks confirm hovering the toggle/titlebar/left edge and focusing do not open the sidebar; Enter and click do. The removed helper is absent; existing create/configure/resource/save/start-chat checks still pass.
- Visually reviewed `cloud-agent-sidebar-icons.png` and `cloud-agent-advanced-model.png` from `.tmp/agent-sidebar-manual-electron/cloud-agents-personal-clou-c4180--published-chat-in-Electron/`. Both sidebar icons are visible with distinct pastel badges and unchanged readable labels.
- Verification uses an isolated test profile and fixture-owned Host API responses. This is source/fixture evidence, not acceptance in the user's logged-in temporary app or a real cloud/model call. User expressly requested no temporary app update.
- Read-only peer review found no actionable issues: route boundary covers Agent subpages, sources/timers are cleared, explicit toggle remains available, other modules and shared robot icon are preserved.
- Bundled `check_doc_drift.py --task-id 20261012-agent-sidebar-manual-67c1a8e4`: passed; changes remain inside Feature ownership boundaries.
## Follow-ups
- Preserve this task branch/worktree for integration. Include its recorded base and preceding Agent page changes when integrating; do not replace another active checkout.
- Temporary app update remains deferred until the user requests it. No cloud service change is needed for this task.
## Promotion Candidates
- Target: integrated current state and Agent navigation/UI guidance. Proposal: Agents alone use explicit sidebar expansion; other modules retain their existing preview behavior. Record the removed resource-policy helper as a presentation change, with policy enforcement unchanged. Evidence: user's explicit request, confirmed icon locations, 18 MainLayout checks and production Electron scenario. Future impact: subsequent shared-sidebar changes should preserve this per-module interaction. Semantic conflicts: older generic hover-preview descriptions need the Agents exception during serialized integration. Human confirmation: the behavior is already authorized by this conversation; canonical edits still require Integration mode.
+1 -1
View File
@@ -12,7 +12,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智
- `Makelore Code|AI 编程`:管理本地项目、项目智能体、对话、文件上下文、代码变更和运行时。 - `Makelore Code|AI 编程`:管理本地项目、项目智能体、对话、文件上下文、代码变更和运行时。
会话工作台采用浅蓝项目栏、彩色通用手柄图标与卡片选中态;中间对话/作品使用圆角标签,作品预览的后退、前进、刷新与网址输入统一放在标签右侧;只在作品页显示,窄窗口自动换行。右侧智能体咨询使用奶油色标题区。沿用真实项目和会话入口、侧栏伸缩与账户菜单,样式只作用于 Code 会话工作台。可编辑的空对话在输入区上方的可用区域中央显示童趣欢迎语;「去作品广场逛逛」入口紧贴输入框上方,两者一起位于底部。老师介绍以固定宽度浮在右上角,不占用中间布局;欢迎语和底部输入区始终相对项目栏右侧的整个内容区域居中;小窗口的欢迎内容独立滚动;发送首条消息后收起欢迎区。侧栏保留「作品广场」入口,只有点击后才在系统浏览器打开公共广场,不请求、摘取或内嵌展示广场作品。打开广场保留当前项目、对话、草稿和附件,不自动发送消息;老师求助按钮保留临时应用的最新文案「帮我整体看🧠」。 会话工作台采用浅蓝项目栏、彩色通用手柄图标与卡片选中态;中间对话/作品使用圆角标签,作品预览的后退、前进、刷新与网址输入统一放在标签右侧;只在作品页显示,窄窗口自动换行。右侧智能体咨询使用奶油色标题区。沿用真实项目和会话入口、侧栏伸缩与账户菜单,样式只作用于 Code 会话工作台。可编辑的空对话在输入区上方的可用区域中央显示童趣欢迎语;「去作品广场逛逛」入口紧贴输入框上方,两者一起位于底部。老师介绍以固定宽度浮在右上角,不占用中间布局;欢迎语和底部输入区始终相对项目栏右侧的整个内容区域居中;小窗口的欢迎内容独立滚动;发送首条消息后收起欢迎区。侧栏保留「作品广场」入口,只有点击后才在系统浏览器打开公共广场,不请求、摘取或内嵌展示广场作品。打开广场保留当前项目、对话、草稿和附件,不自动发送消息;老师求助按钮保留临时应用的最新文案「帮我整体看🧠」。
- `Makelore Agents|AI 智能体`:从模块首页进入时先打开“我的智能体”卡片列表,保留此前对话与操作恢复记录;返回首页箭头与侧栏折叠按钮同排,折叠后仍可返回或展开,macOS 原生窗口按钮留出安全空间。首页围绕自己的智能体和创建入口,显示童趣问候“Hi,今天想创造还是想聊聊?”,不再显示更多、收到的分享、活动或归档筛选。卡片显示名称、可为空的一句话介绍及状态,支持聊天、调整、继续制作和确认删除;删除沿用云端归档机制,移出列表并停用新调用,历史和费用保留。创建先填写名称并选择平台像素头像或上传头像,下一步进入独立配置。顶部名称有编辑图标,头像可点击修改;未设置头像时使用同一张默认像素头像,从创建到配置、改名、保存和重新打开保持一致。基础页为“介绍与提示词”,一句话介绍可留空,完整提示词保留原始文本;模型作为高级设置第一项必选,未选择时不能开始使用,草稿可分步保存,不自动选模型。介绍与提示词采用浅蓝介绍卡和浅紫提示词卡;高级设置按模型、工具箱(Tool)、做事手册(Skill)、MCP 服务、子智能体和知识库纵向呈现,不再设内部分类切换;不展示 Tool/Skill 解释、云端能力管理、费用上限或运行限制卡片,保存其他配置时保留既有运行限制,费用规则不变。顶层页签切换保留搜索、资源选择、知识表单及未保存输入。配置页移除聊天、自动任务和使用与接入导航,独立聊天仍从卡片进入,微信账号仍在侧栏渠道管理。草稿明确保存(Ctrl/⌘+S);保存只更新草稿,“应用最新修改”使新配置生效,失败重试保持原操作身份;点击“开始使用”完成必要的私有启用后直接打开聊天,不再展示二次确认页;未保存修改、缺少模型、冲突或尚未确认的请求会留在配置页处理,打开聊天不自动发送消息。独立对话沿用奶油底色和浅蓝、浅紫、薄荷色:空对话展示当前智能体头像与名称,示例只填入草稿;用户消息和智能体回复区分呈现,长 Markdown、表格和代码在消息区内滚动,底部输入框、附件、停止和历史入口保留;短窗口中,配置完全生效且没有待处理问题时收起重复的已保存/已应用提示和禁用按钮,修改或待确认时恢复;“调整智能体”与客户端/微信来源同排,窄窗口会换行,未发送内容的离开保护不变。头像上传、Main-owned Host API、访问隔离、费用与不可变版本规则保持;介绍留空需要配套 Yuxi DraftCreate/DraftSave 的空字符串契约。 - `Makelore Agents|AI 智能体`:从模块首页进入时先打开“我的智能体”卡片列表,保留此前对话与操作恢复记录;返回首页箭头与侧栏折叠按钮同排,折叠后仍可返回;智能体模块的侧栏只通过按钮点击或键盘激活展开,鼠标经过左侧、收纳按钮或顶栏不会展开,切换模块不沿用旧悬停预览;其他模块仍保留原预览方式。“我的智能体”使用浅紫对话机器人图标,“渠道”使用浅蓝连接节点图标。macOS 原生窗口按钮留出安全空间。首页围绕自己的智能体和创建入口,显示童趣问候“Hi,今天想创造还是想聊聊?”,不再显示更多、收到的分享、活动或归档筛选。卡片显示名称、可为空的一句话介绍及状态,支持聊天、调整、继续制作和确认删除;删除沿用云端归档机制,移出列表并停用新调用,历史和费用保留。创建先填写名称并选择平台像素头像或上传头像,下一步进入独立配置。顶部名称有编辑图标,头像可点击修改;未设置头像时使用同一张默认像素头像,从创建到配置、改名、保存和重新打开保持一致。基础页为“介绍与提示词”,一句话介绍可留空,完整提示词保留原始文本;模型作为高级设置第一项必选,未选择时不能开始使用,草稿可分步保存,不自动选模型。介绍与提示词采用浅蓝介绍卡和浅紫提示词卡;高级设置按模型、工具箱(Tool)、做事手册(Skill)、MCP 服务、子智能体和知识库纵向呈现,不再设内部分类切换;不展示 Tool/Skill 解释、资源开放规则提示、云端能力管理、费用上限或运行限制卡片,保存其他配置时保留既有运行限制,费用规则不变。顶层页签切换保留搜索、资源选择、知识表单及未保存输入。配置页移除聊天、自动任务和使用与接入导航,独立聊天仍从卡片进入,微信账号仍在侧栏渠道管理。草稿明确保存(Ctrl/⌘+S);保存只更新草稿,“应用最新修改”使新配置生效,失败重试保持原操作身份;点击“开始使用”完成必要的私有启用后直接打开聊天,不再展示二次确认页;未保存修改、缺少模型、冲突或尚未确认的请求会留在配置页处理,打开聊天不自动发送消息。独立对话沿用奶油底色和浅蓝、浅紫、薄荷色:空对话展示当前智能体头像与名称,示例只填入草稿;用户消息和智能体回复区分呈现,长 Markdown、表格和代码在消息区内滚动,底部输入框、附件、停止和历史入口保留;短窗口中,配置完全生效且没有待处理问题时收起重复的已保存/已应用提示和禁用按钮,修改或待确认时恢复;“调整智能体”与客户端/微信来源同排,窄窗口会换行,未发送内容的离开保护不变。头像上传、Main-owned Host API、访问隔离、费用与不可变版本规则保持;介绍留空需要配套 Yuxi DraftCreate/DraftSave 的空字符串契约。
云知识库的 embedding 模型只由平台管理员在 Yuxi 后台维护:在「智能体管理 → 模型供应商」启用供应商,按供应商要求填写有效的服务地址和 API Key,添加并启用 embedding 模型并填写实际向量维度;embedding 向量化费用由平台承担,不计入用户用量、词元点数或智能体费用上限;one-api 不参与 embedding 配置,Works Square 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。 云知识库的 embedding 模型只由平台管理员在 Yuxi 后台维护:在「智能体管理 → 模型供应商」启用供应商,按供应商要求填写有效的服务地址和 API Key,添加并启用 embedding 模型并填写实际向量维度;embedding 向量化费用由平台承担,不计入用户用量、词元点数或智能体费用上限;one-api 不参与 embedding 配置,Works Square 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。
+9 -8
View File
@@ -31,7 +31,7 @@ export function MainLayout() {
const isCloudAgents = activeModule === 'cloud_agents'; const isCloudAgents = activeModule === 'cloud_agents';
const handleSidebarPeekChange = useCallback((open: boolean, source: SidebarPeekSource) => { const handleSidebarPeekChange = useCallback((open: boolean, source: SidebarPeekSource) => {
if (!sidebarCollapsed) return; if (!sidebarCollapsed || isCloudAgents) return;
const sources = sidebarPeekSourcesRef.current; const sources = sidebarPeekSourcesRef.current;
if (sources[source] === open) return; if (sources[source] === open) return;
@@ -55,19 +55,20 @@ export function MainLayout() {
setSidebarPeekOpen(false); setSidebarPeekOpen(false);
sidebarPeekCloseTimerRef.current = null; sidebarPeekCloseTimerRef.current = null;
}, SIDEBAR_PEEK_CLOSE_DELAY_MS); }, SIDEBAR_PEEK_CLOSE_DELAY_MS);
}, [sidebarCollapsed]); }, [sidebarCollapsed, isCloudAgents]);
useEffect(() => { useEffect(() => {
sidebarPeekSourcesRef.current = { toggle: false, focus: false, sidebar: false, titlebar: false }; sidebarPeekSourcesRef.current = { toggle: false, focus: false, sidebar: false, titlebar: false };
// This transient state mirrors the external persisted sidebar preference; // This transient state mirrors the external persisted sidebar preference;
// reset it when that preference changes so a stale preview cannot reopen. // reset it on preference changes and both edges of the Agents boundary so
// a previous module's preview or timer cannot reopen the manual-only sidebar.
// eslint-disable-next-line react-hooks/set-state-in-effect // eslint-disable-next-line react-hooks/set-state-in-effect
setSidebarPeekOpen(false); setSidebarPeekOpen(false);
if (sidebarPeekCloseTimerRef.current) { if (sidebarPeekCloseTimerRef.current) {
clearTimeout(sidebarPeekCloseTimerRef.current); clearTimeout(sidebarPeekCloseTimerRef.current);
sidebarPeekCloseTimerRef.current = null; sidebarPeekCloseTimerRef.current = null;
} }
}, [sidebarCollapsed]); }, [sidebarCollapsed, isCloudAgents]);
useEffect(() => () => { useEffect(() => () => {
if (sidebarPeekCloseTimerRef.current) clearTimeout(sidebarPeekCloseTimerRef.current); if (sidebarPeekCloseTimerRef.current) clearTimeout(sidebarPeekCloseTimerRef.current);
@@ -83,8 +84,8 @@ export function MainLayout() {
workspaceLayout={isChatWorkspace} workspaceLayout={isChatWorkspace}
overlay={isPaintingModule} overlay={isPaintingModule}
showSidebarControls={!isCanvasWorkspace} showSidebarControls={!isCanvasWorkspace}
sidebarPeekOpen={isCanvasWorkspace ? false : sidebarPeekOpen} sidebarPeekOpen={isCanvasWorkspace || isCloudAgents ? false : sidebarPeekOpen}
onSidebarPeekChange={isCanvasWorkspace ? undefined : handleSidebarPeekChange} onSidebarPeekChange={isCanvasWorkspace || isCloudAgents ? undefined : handleSidebarPeekChange}
/> />
{/* Below the title bar: sidebar + content */} {/* Below the title bar: sidebar + content */}
@@ -93,8 +94,8 @@ export function MainLayout() {
workspaceLayout={isChatWorkspace} workspaceLayout={isChatWorkspace}
hideOnCompact={isCanvasWorkspace} hideOnCompact={isCanvasWorkspace}
forceExpanded={isCanvasWorkspace} forceExpanded={isCanvasWorkspace}
sidebarPeekOpen={isCanvasWorkspace ? false : sidebarPeekOpen} sidebarPeekOpen={isCanvasWorkspace || isCloudAgents ? false : sidebarPeekOpen}
onSidebarPeekChange={isCanvasWorkspace ? undefined : handleSidebarPeekChange} onSidebarPeekChange={isCanvasWorkspace || isCloudAgents ? undefined : handleSidebarPeekChange}
/> />
<main <main
data-testid="main-content" data-testid="main-content"
+8 -3
View File
@@ -6,15 +6,16 @@ import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { import {
Bot, Bot,
BotMessageSquare,
Cpu, Cpu,
ChevronRight, ChevronRight,
FolderKanban, FolderKanban,
Gamepad2, Gamepad2,
LogOut, LogOut,
Plus, Plus,
RadioTower,
Settings as SettingsIcon, Settings as SettingsIcon,
UserCircle2, UserCircle2,
Waypoints,
} from 'lucide-react'; } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { ConfirmDialog } from '@/components/ui/confirm-dialog';
@@ -505,13 +506,17 @@ export function Sidebar({
<button type="button" aria-label="我的智能体" onClick={() => navigate('/cloud-agents?view=mine')} <button type="button" aria-label="我的智能体" onClick={() => navigate('/cloud-agents?view=mine')}
aria-current={location.pathname === '/cloud-agents' ? 'page' : undefined} aria-current={location.pathname === '/cloud-agents' ? 'page' : undefined}
className={cn('flex min-h-10 w-full items-center gap-2 rounded-lg px-2 py-2 text-left text-sm font-semibold hover:bg-surface-tertiary', location.pathname === '/cloud-agents' && 'bg-brand-soft', sidebarCollapsed && 'justify-center px-0')}> className={cn('flex min-h-10 w-full items-center gap-2 rounded-lg px-2 py-2 text-left text-sm font-semibold hover:bg-surface-tertiary', location.pathname === '/cloud-agents' && 'bg-brand-soft', sidebarCollapsed && 'justify-center px-0')}>
<Bot className="h-4 w-4 shrink-0 text-brand" /> <span aria-hidden="true" className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl border border-[#e2d7ef] bg-[#f2ebfa] text-[#785b9b]">
<BotMessageSquare className="h-5 w-5" strokeWidth={1.8} />
</span>
{!sidebarCollapsed && <span>我的智能体</span>} {!sidebarCollapsed && <span>我的智能体</span>}
</button> </button>
<button type="button" aria-label="渠道" onClick={() => navigate('/cloud-agents/channels')} <button type="button" aria-label="渠道" onClick={() => navigate('/cloud-agents/channels')}
aria-current={location.pathname.startsWith('/cloud-agents/channels') ? 'page' : undefined} aria-current={location.pathname.startsWith('/cloud-agents/channels') ? 'page' : undefined}
className={cn('mt-1 flex min-h-10 w-full items-center gap-2 rounded-lg px-2 py-2 text-left text-sm font-semibold hover:bg-surface-tertiary', location.pathname.startsWith('/cloud-agents/channels') && 'bg-brand-soft', sidebarCollapsed && 'justify-center px-0')}> className={cn('mt-1 flex min-h-10 w-full items-center gap-2 rounded-lg px-2 py-2 text-left text-sm font-semibold hover:bg-surface-tertiary', location.pathname.startsWith('/cloud-agents/channels') && 'bg-brand-soft', sidebarCollapsed && 'justify-center px-0')}>
<RadioTower className="h-4 w-4 shrink-0 text-brand" /> <span aria-hidden="true" className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl border border-[#d7e4ec] bg-[#edf4fa] text-[#55788e]">
<Waypoints className="h-5 w-5" strokeWidth={1.8} />
</span>
{!sidebarCollapsed && <span>渠道</span>} {!sidebarCollapsed && <span>渠道</span>}
</button> </button>
</div> : <div data-testid="sidebar-robot-navigation" className="px-2 py-3"> </div> : <div data-testid="sidebar-robot-navigation" className="px-2 py-3">
@@ -76,6 +76,5 @@ export function ConfigurationFields({ slug, value, disabled, onChange, section =
onCreated={kb => { change('knowledges', [...new Set([...value.knowledges, kb.kb_id])]); setRefresh(v => v + 1); }} /></div>} onCreated={kb => { change('knowledges', [...new Set([...value.knowledges, kb.kb_id])]); setRefresh(v => v + 1); }} /></div>}
</fieldset>; </fieldset>;
})} })}
<p className="text-xs leading-5 text-muted-foreground">未选择的资源不会开放给此智能体。通过微信或 API 使用时也遵循本配置。</p>
</div>; </div>;
} }
+20 -2
View File
@@ -126,11 +126,28 @@ test('personal cloud Agent identity, configuration and separate published chat i
expect(collapseBounds).not.toBeNull(); expect(collapseBounds).not.toBeNull();
expect(Math.abs(returnBounds!.y - collapseBounds!.y)).toBeLessThanOrEqual(1); expect(Math.abs(returnBounds!.y - collapseBounds!.y)).toBeLessThanOrEqual(1);
expect(returnBounds!.x + returnBounds!.width).toBeLessThan(collapseBounds!.x); expect(returnBounds!.x + returnBounds!.width).toBeLessThan(collapseBounds!.x);
await page.screenshot({ path: testInfo.outputPath('cloud-agent-sidebar-icons.png') });
await navigation.getByRole('button', { name: '折叠侧栏', exact: true }).click(); await navigation.getByRole('button', { name: '折叠侧栏', exact: true }).click();
await page.mouse.move(700, 400); const sidebar = page.getByTestId('sidebar');
const sidebarToggle = page.getByTestId('titlebar-sidebar-toggle');
await sidebarToggle.hover();
await expect(sidebar).toHaveAttribute('aria-hidden', 'true');
await expect(sidebarToggle).toHaveAttribute('aria-expanded', 'false');
await page.getByTestId('titlebar-sidebar-toggle-layer').hover();
await expect(sidebar).toHaveAttribute('aria-hidden', 'true');
await page.mouse.move(3, 120);
await expect(sidebar).toHaveAttribute('aria-hidden', 'true');
await sidebarToggle.focus();
await expect(sidebar).toHaveAttribute('aria-hidden', 'true');
await expect(navigation.getByRole('button', { name: '返回首页', exact: true })).toBeVisible(); await expect(navigation.getByRole('button', { name: '返回首页', exact: true })).toBeVisible();
await navigation.getByRole('button', { name: /展开侧栏|固定侧栏/ }).click(); await expect(navigation.getByRole('button', { name: '展开侧栏', exact: true })).toBeVisible();
await sidebarToggle.press('Enter');
await expect(page.getByTestId('sidebar-cloud-agents-navigation')).toBeVisible(); await expect(page.getByTestId('sidebar-cloud-agents-navigation')).toBeVisible();
await expect(sidebarToggle).toHaveAttribute('aria-expanded', 'true');
await sidebarToggle.click();
await expect(sidebar).toHaveAttribute('aria-hidden', 'true');
await sidebarToggle.click();
await expect(sidebar).toHaveAttribute('aria-hidden', 'false');
const fixtureState = () => app.evaluate(() => Reflect.get(globalThis, '__cloudAgentUiFixture').snapshot()); const fixtureState = () => app.evaluate(() => Reflect.get(globalThis, '__cloudAgentUiFixture').snapshot());
const messageOperations = async () => (await fixtureState()).operations.filter((operation: string) => operation === 'preview' || operation === 'submit'); const messageOperations = async () => (await fixtureState()).operations.filter((operation: string) => operation === 'preview' || operation === 'submit');
await page.getByRole('button', { name: '创建我的第一个智能体', exact: true }).click(); await page.getByRole('button', { name: '创建我的第一个智能体', exact: true }).click();
@@ -192,6 +209,7 @@ test('personal cloud Agent identity, configuration and separate published chat i
await expect(page.getByText('选它做事时能用的工具。', { exact: true })).toHaveCount(0); await expect(page.getByText('选它做事时能用的工具。', { exact: true })).toHaveCount(0);
await expect(page.getByText('给它一本手册,照着方法做。', { exact: true })).toHaveCount(0); await expect(page.getByText('给它一本手册,照着方法做。', { exact: true })).toHaveCount(0);
await expect(page.getByText('管理我的云端能力', { exact: true })).toHaveCount(0); await expect(page.getByText('管理我的云端能力', { exact: true })).toHaveCount(0);
await expect(page.getByText('未选择的资源不会开放给此智能体。通过微信或 API 使用时也遵循本配置。', { exact: true })).toHaveCount(0);
await expect(page.getByRole('group', { name: /^知识库/ })).toBeVisible(); await expect(page.getByRole('group', { name: /^知识库/ })).toBeVisible();
await expect(page.getByRole('heading', { name: '费用上限', exact: true })).toHaveCount(0); await expect(page.getByRole('heading', { name: '费用上限', exact: true })).toHaveCount(0);
await expect(page.getByRole('heading', { name: '运行限制', exact: true })).toHaveCount(0); await expect(page.getByRole('heading', { name: '运行限制', exact: true })).toHaveCount(0);
+102 -5
View File
@@ -1,5 +1,5 @@
import { act, fireEvent, render, screen } from '@testing-library/react'; import { act, fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { MemoryRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MainLayout } from '@/components/layout/MainLayout'; import { MainLayout } from '@/components/layout/MainLayout';
import { codingWorkspaceStore } from '@/stores/coding-workspace'; import { codingWorkspaceStore } from '@/stores/coding-workspace';
@@ -8,6 +8,7 @@ import { useSettingsStore } from '@/stores/settings';
type PeekProps = { type PeekProps = {
sidebarPeekOpen?: boolean; sidebarPeekOpen?: boolean;
onSidebarPeekChange?: (open: boolean, source: 'toggle' | 'focus' | 'sidebar' | 'titlebar') => void; onSidebarPeekChange?: (open: boolean, source: 'toggle' | 'focus' | 'sidebar' | 'titlebar') => void;
forceExpanded?: boolean;
}; };
vi.mock('@/components/layout/TitleBar', () => ({ vi.mock('@/components/layout/TitleBar', () => ({
@@ -17,8 +18,12 @@ vi.mock('@/components/layout/TitleBar', () => ({
type="button" type="button"
data-testid="peek-trigger" data-testid="peek-trigger"
data-peek-open={sidebarPeekOpen ? 'true' : 'false'} data-peek-open={sidebarPeekOpen ? 'true' : 'false'}
aria-label={useSettingsStore.getState().sidebarCollapsed ? '展开侧栏' : '折叠侧栏'}
onClick={() => useSettingsStore.getState().setSidebarCollapsed(!useSettingsStore.getState().sidebarCollapsed)}
onPointerEnter={() => onSidebarPeekChange?.(true, 'toggle')} onPointerEnter={() => onSidebarPeekChange?.(true, 'toggle')}
onPointerLeave={() => onSidebarPeekChange?.(false, 'toggle')} onPointerLeave={() => onSidebarPeekChange?.(false, 'toggle')}
onFocus={() => onSidebarPeekChange?.(true, 'focus')}
onBlur={() => onSidebarPeekChange?.(false, 'focus')}
/> />
<div <div
data-testid="peek-titlebar" data-testid="peek-titlebar"
@@ -30,22 +35,33 @@ vi.mock('@/components/layout/TitleBar', () => ({
})); }));
vi.mock('@/components/layout/Sidebar', () => ({ vi.mock('@/components/layout/Sidebar', () => ({
Sidebar: ({ sidebarPeekOpen, onSidebarPeekChange }: PeekProps) => ( Sidebar: ({ sidebarPeekOpen, onSidebarPeekChange, forceExpanded }: PeekProps) => (
<aside <aside
data-testid="peek-sidebar" data-testid="peek-sidebar"
data-peek-open={sidebarPeekOpen ? 'true' : 'false'} data-peek-open={sidebarPeekOpen ? 'true' : 'false'}
data-force-expanded={forceExpanded ? 'true' : 'false'}
onPointerEnter={() => onSidebarPeekChange?.(true, 'sidebar')} onPointerEnter={() => onSidebarPeekChange?.(true, 'sidebar')}
onPointerLeave={() => onSidebarPeekChange?.(false, 'sidebar')} onPointerLeave={() => onSidebarPeekChange?.(false, 'sidebar')}
/> />
), ),
})); }));
function renderLayout() { function NavigationProbe() {
const navigate = useNavigate();
const location = useLocation();
return <>
<output data-testid="layout-location">{location.pathname}</output>
<button onClick={() => navigate('/cloud-agents')}>进入智能体</button>
<button onClick={() => navigate('/chat')}>进入编程</button>
</>;
}
function renderLayout(path = '/chat') {
render( render(
<MemoryRouter initialEntries={['/chat']}> <MemoryRouter initialEntries={[path]}>
<Routes> <Routes>
<Route element={<MainLayout />}> <Route element={<MainLayout />}>
<Route path="*" element={null} /> <Route path="*" element={<NavigationProbe />} />
</Route> </Route>
</Routes> </Routes>
</MemoryRouter>, </MemoryRouter>,
@@ -140,4 +156,85 @@ describe('MainLayout sidebar hover preview', () => {
}); });
expect(trigger).toHaveAttribute('data-peek-open', 'false'); expect(trigger).toHaveAttribute('data-peek-open', 'false');
}); });
it.each(['/cloud-agents', '/cloud-agents/channels'])('ignores every pointer and focus preview source on %s', (path) => {
renderLayout(path);
const trigger = screen.getByTestId('peek-trigger');
const sidebar = screen.getByTestId('peek-sidebar');
const titlebar = screen.getByTestId('peek-titlebar');
for (const target of [trigger, titlebar, sidebar]) {
fireEvent.pointerEnter(target);
expect(trigger).toHaveAttribute('data-peek-open', 'false');
expect(sidebar).toHaveAttribute('data-peek-open', 'false');
fireEvent.pointerLeave(target);
}
fireEvent.focus(trigger);
expect(trigger).toHaveAttribute('data-peek-open', 'false');
expect(sidebar).toHaveAttribute('data-peek-open', 'false');
fireEvent.blur(trigger);
act(() => vi.advanceTimersByTime(180));
expect(sidebar).toHaveAttribute('data-peek-open', 'false');
expect(useSettingsStore.getState().sidebarCollapsed).toBe(true);
expect(vi.getTimerCount()).toBe(0);
});
it.each(['/cloud-agents', '/cloud-agents/channels'])('allows explicit sidebar expansion and collapse on %s', (path) => {
renderLayout(path);
fireEvent.click(screen.getByRole('button', { name: '展开侧栏' }));
expect(useSettingsStore.getState().sidebarCollapsed).toBe(false);
expect(screen.getByRole('button', { name: '折叠侧栏' })).toHaveAttribute('data-peek-open', 'false');
fireEvent.click(screen.getByRole('button', { name: '折叠侧栏' }));
expect(useSettingsStore.getState().sidebarCollapsed).toBe(true);
fireEvent.pointerEnter(screen.getByTestId('peek-trigger'));
fireEvent.focus(screen.getByTestId('peek-trigger'));
expect(screen.getByTestId('peek-sidebar')).toHaveAttribute('data-peek-open', 'false');
});
it('clears active Code preview sources when entering Agents and does not restore them on return', () => {
renderLayout();
fireEvent.pointerEnter(screen.getByTestId('peek-trigger'));
fireEvent.pointerEnter(screen.getByTestId('peek-titlebar'));
fireEvent.pointerEnter(screen.getByTestId('peek-sidebar'));
fireEvent.focus(screen.getByTestId('peek-trigger'));
expect(screen.getByTestId('peek-sidebar')).toHaveAttribute('data-peek-open', 'true');
fireEvent.click(screen.getByRole('button', { name: '进入智能体' }));
expect(screen.getByTestId('layout-location')).toHaveTextContent('/cloud-agents');
expect(screen.getByTestId('peek-sidebar')).toHaveAttribute('data-peek-open', 'false');
fireEvent.click(screen.getByRole('button', { name: '进入编程' }));
expect(screen.getByTestId('peek-sidebar')).toHaveAttribute('data-peek-open', 'false');
fireEvent.pointerEnter(screen.getByTestId('peek-trigger'));
expect(screen.getByTestId('peek-sidebar')).toHaveAttribute('data-peek-open', 'true');
fireEvent.pointerLeave(screen.getByTestId('peek-trigger'));
act(() => vi.advanceTimersByTime(180));
expect(screen.getByTestId('peek-sidebar')).toHaveAttribute('data-peek-open', 'false');
});
it('cancels a stale Code close timer at the Agents boundary before Code starts a fresh preview', () => {
renderLayout();
fireEvent.pointerEnter(screen.getByTestId('peek-trigger'));
fireEvent.pointerLeave(screen.getByTestId('peek-trigger'));
expect(vi.getTimerCount()).toBe(1);
fireEvent.click(screen.getByRole('button', { name: '进入智能体' }));
expect(vi.getTimerCount()).toBe(0);
act(() => vi.advanceTimersByTime(180));
expect(screen.getByTestId('peek-sidebar')).toHaveAttribute('data-peek-open', 'false');
fireEvent.click(screen.getByRole('button', { name: '进入编程' }));
fireEvent.pointerEnter(screen.getByTestId('peek-titlebar'));
act(() => vi.advanceTimersByTime(180));
expect(screen.getByTestId('peek-sidebar')).toHaveAttribute('data-peek-open', 'true');
fireEvent.pointerLeave(screen.getByTestId('peek-titlebar'));
act(() => vi.advanceTimersByTime(180));
expect(screen.getByTestId('peek-sidebar')).toHaveAttribute('data-peek-open', 'false');
});
it('retains the fixed expanded Canvas workspace sidebar', () => {
renderLayout('/image-canvas');
expect(screen.getByTestId('peek-sidebar')).toHaveAttribute('data-force-expanded', 'true');
fireEvent.pointerEnter(screen.getByTestId('peek-trigger'));
fireEvent.focus(screen.getByTestId('peek-trigger'));
expect(screen.getByTestId('peek-sidebar')).toHaveAttribute('data-peek-open', 'false');
expect(screen.getByTestId('peek-sidebar')).toHaveAttribute('data-force-expanded', 'true');
});
}); });