diff --git a/.project-docs/30-worklog/tasks/20261014-sidebar-simplify-a6d32f90.md b/.project-docs/30-worklog/tasks/20261014-sidebar-simplify-a6d32f90.md new file mode 100644 index 00000000..ec2d308d --- /dev/null +++ b/.project-docs/30-worklog/tasks/20261014-sidebar-simplify-a6d32f90.md @@ -0,0 +1,40 @@ +# Task: Simplify Code brand area and Canvas project labels + +## Identity + +- Task ID: 20261014-sidebar-simplify-a6d32f90 +- Mode: Feature +- Branch: codex/20261014-sidebar-simplify-a6d32f90-sidebar-simplify +- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261014-sidebar-simplify-a6d32f90 +- Base commit: 049e34cc77338f3778d82323539e9c0f6066a09a +- Owner: codex +- Status: In Progress + +## Scope + +- Remove the Code rail brand/logo/caption, align New Project at the former brand top and My Projects/list at the former New Project top; apply consistently to chat/configuration via the shared rail. Rename Canvas desktop rail heading to My Projects and show only project names, without version/date metadata. + +## Intent And Constraints + +- User explicitly supersedes prior Code brand retention. Preserve top return/collapse controls including Linux navigation, shared sidebar width/collapse, Code gallery/Alice footer, configuration content/guards, APIs/project identities/menu actions, Canvas sort/selection/loading. Desktop left rail only; compact Canvas project sheet/status presentation is outside the supplied screenshot scope. Preserve actual trial Main40930/PID93092 and all 18 inherited features; no Main restart or real business writes. + +## Outcome + +- Removed only the Code sidebar brand block/import; the center welcome artwork and top navigation remain. Removed 10px content top padding as well as the 46px brand block, placing New Project at the prior brand top and My Projects heading at the prior New Project top. Chat/configuration continue sharing one rail; bottom gallery/account rules unchanged. +- Canvas desktop rail is My Projects, with each row showing its real title only. Removed the date formatter; kept updatedAt ordering, selection identity and all existing project actions/loading states. +- README and focused existing tests updated; trial update pending. + +## Verification + +- Official ownership and Planning Gate passed after 66 peers/198 scope/intent/promotion sections were inspected read-only, no blocking semantic conflicts. +- Standard typecheck, scoped ESLint and default full `build:vite` passed. +- 31 Code/layout/navigation/community tests and 6 Canvas sidebar tests passed. The Canvas selection test also verifies recent-first order and exact identity after hiding metadata. +- Focused Electron chat/configuration geometry/navigation tests running; immutable trial update pending. + +## Follow-ups + +- None recorded. + +## Promotion Candidates + +- None recorded. diff --git a/README.md b/README.md index 4d84e8fe..6851d50a 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智 Agents 侧栏的「渠道」按每个微信一张卡展示备注、当前智能体与使用状态;一个微信同时只绑定一个智能体,多个微信可以使用同一智能体。常用操作是“更换智能体”,在同一个选择窗口中确认;暂停自动回复和解除微信绑定放在“更多操作”。暂停停止当前任务并保留登录、绑定和历史;更换保留登录与原启停状态;解除微信绑定是完整操作,停止任务、清除目标并退出微信登录,历史保留,再使用需扫码。掉线显示重新扫码入口,暂停显示继续回复,未知或未同步状态显示正在确认,不宣称可用。添加流程为选择已开始使用的智能体、扫码、自动完成绑定;只有这次用户发起的绑定在扫码确认、目标正确、启用成功且最终状态回读可用后才显示完成,不因页面打开、旧记录或验证码提交自动启用。无可用智能体时说明创建和“开始使用”的前提,加载失败不冒充空态,已有微信仍可管理或解绑。Main 持有凭据与原操作恢复;解除需要配套 Yuxi 用户级 `/channel-accounts/{id}/unbind`,旧后端不会通过改按钮文案或组合未确认操作假装完成。变更后以最新读取状态为准,未完成操作保留原身份供显式恢复;失败在当前窗口可见,结果未知不自动重放。服务端显示微信退出仍在处理中时,禁止新的连接、启停和绑定变更,保留刷新与原操作恢复;管理写入前再次读取当前状态。客户端已知目标智能体停用时明确显示不可用,并禁止恢复自动回复,仍可更换目标或安全暂停、解绑。旧请求无法安全恢复、账号关系不一致或目标不可用时提示先核对,保留原记录并避免盲目重试;账号关系或目标智能体问题可显式刷新状态后再次确认原操作,刷新本身不重放操作;“不再提醒”只移除本机提醒,不取消服务端仍在处理的操作。最新 Yuxi 的退出保护条件和持久恢复由服务端负责,发布需配套 Core/Adapter 与 Yuxi business=13、knowledge=2 数据库版本。渠道页不展示微信聊天或活动,既有历史、费用、内容隔离与应用 API 保持原边界。实际微信身份及重复扫码识别仍由云端负责,不从备注推测。 -- `Makelore Canvas|AI 绘画`:每个设计项目(Workspace)维护一份从创建起就存在的 Living Form。左侧项目栏沿用 Agents 的图标徽章与文字入口,新建、切换和项目操作菜单集中管理 Workspace。工作区统一项目标题、设计版本与制作状态,创作对话和作品区支持拖动分栏、键盘调宽及单栏展开;对话以 Markdown 呈现,底部输入器保留草稿、发送状态与 Enter / Shift+Enter 快捷键。右侧通过“当前作品 / 历史作品”切换,在大图或视频预览旁编辑唯一制作方案、确认费用和制作;历史作品可回到工作区查看,切换面板保留未保存的制作参数。紧凑窗口以“创作对话 / 作品与方案”切换面板,通过项目抽屉管理 Workspace。图片支持适应窗口、原始尺寸及 Esc 关闭,视频支持预览和保存到电脑。参考图从本地上传后以 `@图片N` 绑定,具体用法只写在创作提示词中。 +- `Makelore Canvas|AI 绘画`:每个设计项目(Workspace)维护一份从创建起就存在的 Living Form。左侧项目栏沿用 Agents 的图标徽章与文字入口,分组标题为“我的项目”,桌面项目列表仅显示名称,隐藏名称下的设计版本与更新时间;新建、切换和项目操作菜单集中管理 Workspace。工作区统一项目标题、设计版本与制作状态,创作对话和作品区支持拖动分栏、键盘调宽及单栏展开;对话以 Markdown 呈现,底部输入器保留草稿、发送状态与 Enter / Shift+Enter 快捷键。右侧通过“当前作品 / 历史作品”切换,在大图或视频预览旁编辑唯一制作方案、确认费用和制作;历史作品可回到工作区查看,切换面板保留未保存的制作参数。紧凑窗口以“创作对话 / 作品与方案”切换面板,通过项目抽屉管理 Workspace。图片支持适应窗口、原始尺寸及 Esc 关闭,视频支持预览和保存到电脑。参考图从本地上传后以 `@图片N` 绑定,具体用法只写在创作提示词中。 - `Makelore Robot|AI 机器`:管理机器人智能体、设备激活绑定、智能体配置与设备分配;机器人工作台的智能体位于 Robot 全局侧栏,选中后在内容区先查看绑定设备、再查看基础设置,当前智能体通过 URL 参数保持可分享选择;绑定设备时默认先选择“引导配网”或“已有激活码”。在 Windows 与 macOS 的引导路径中,Makelore 可在弹窗内扫描并连接附近开放的 `Xiaozhi-*` 配网热点,失败时仍可通过系统 Wi-Fi 手动连接;后续继续复用机器人现有热点配网页面,不修改固件,也不由 Makelore 接收 Wi-Fi 密码。 应用启动默认进入 AI 模块入口选择页。入口页可在未登录状态浏览;未登录用户点击已开通模块时进入客户端原生登录页,可使用账号密码或手机号短信验证码登录。密码登录可选“记住密码”:正式安装包仅由 Electron Main 使用系统受保护凭据存储加密保存和回填账号密码,不写入 Renderer 持久状态,未打包开发版或系统安全存储不可用时禁用该选项。登录请求由 Renderer 经 Host API 交给 Electron Main,再由 Main 调用 Works Square;成功后回到入口选择页。已登录时,Electron Main 会从 Works Square `/api/auth/me` 读取当前账号,只向 Renderer 投影用户名、账号/租户/部门标识、权限名列表与四个模块布尔开关,不透传上游资料或凭据。工作区门禁同时要求有效 Token 和完整用户身份;旧状态缺失身份时会先尝试从 Main 恢复,仍无法确认则清除残留会话并返回登录页。被管理员关闭的模块会在入口页置灰且无法点击,直接访问其工作区路径也会返回入口页。旧服务端未返回策略或缺少单项字段时默认开放;这个客户端门禁不替代服务端 API 授权。 @@ -41,7 +41,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智 - macOS 窗口按钮:红色关闭当前窗口,全屏时先退出全屏再关闭;黄色最小化,绿色保留原生缩放/全屏行为。关窗后应用和进行中的后台任务继续运行,点击 Dock、托盘或菜单可重新打开窗口;`Cmd+Q` 退出应用。 - 桌面文本右键菜单:主 Renderer 窗口由 Electron Main 提供原生“剪切、复制、粘贴、全选”操作;菜单只在可编辑区域或已有文本选择时出现,普通空白区域不显示,也不向 Renderer 暴露剪贴板 IPC。 - AI 编程核心对话运行时:Electron Main 按需启动一个长驻 Pi Agent Server,同一进程内为每条 Conversation 建立独立的 Runtime、Session、凭证视图、扩展上下文和 JSONL 通道,同时复用 Pi 模块与进程启动成本;Server 异常退出时所有旧通道统一失效,下一次恢复只重启一个 Server。正式包中的 Agent Server 从明确传入的 `resources/pi-runtime` 根定位依赖清单与包入口,不从脚本所在目录或应用 `node_modules` 猜测解析。Renderer 不直接启动或调用 Pi,也不读取其 wire 类型。项目与 Conversation 元数据先从本地读取,选中对话后才按需准备对应逻辑线程;输入框不等待运行时准备完成。同一助手工具批次中,会取得项目写租约的 `bash`、`edit`、`write` 与动态产品工具按顺序执行,避免批次在命令启动前自锁;纯只读工具仍可并行。子 Agent 仍使用独立的短生命周期进程,以保留并发配额、故障和写入边界;macOS 上两类后台进程都使用 Electron 自带的 Helper,不在 Dock 中创建额外应用图标。 -- AI 编程默认入口:进入 Code 后保持在 `/chat`。顶部返回箭头与侧栏收起图标同排,与右侧项目/会话名称对齐;折叠时保留返回和展开入口。macOS 为原生窗口按钮保留安全空间,窄侧栏仍可使用。下方麦洛品牌与“操作对话/作品”标签行对齐;“作品广场”与“新建项目”沿用 Agent 的彩色图标徽章+文字样式,不再使用整行白色块与厚阴影,其中“新建项目”位于品牌下方原广场入口的位置,“作品广场”移至底部、紧邻账户按钮上方;“我的项目”和项目列表紧接新建项目,列表沿用图标与文字布局及浅色选中反馈;进入“会话编程设置”后的项目配置页及插件资源页复用相同的左侧工作栏,品牌、返回与收起按钮、项目和会话列表、底部作品广场与账户的位置保持一致,并沿用当前宽度和收起状态;配置正文、顶部项目名称与配置页自身的返回操作保留。尚未选择本地项目时,页面以麦洛 M 标识和居中引导语建立焦点,下方提供独立的“新增项目”主操作以及可横向浏览、直接进入的已有项目卡片;项目创建和进入仍复用侧栏现有流程。已有项目的新 Conversation 在可编辑 Composer 上方沿用同一品牌引导,第一条消息出现后切换为正常时间线。 +- AI 编程默认入口:进入 Code 后保持在 `/chat`。顶部返回箭头与侧栏收起图标同排,与右侧项目/会话名称对齐;折叠时保留返回和展开入口。macOS 为原生窗口按钮保留安全空间,窄侧栏仍可使用。左侧不再展示麦洛品牌与说明语,“新建项目”置于原品牌区,“我的项目”与列表上移到原新建项目区域;“作品广场”与“新建项目”沿用 Agent 的彩色图标徽章+文字样式,不再使用整行白色块与厚阴影,其中“新建项目”位于品牌下方原广场入口的位置,“作品广场”移至底部、紧邻账户按钮上方;“我的项目”和项目列表紧接新建项目,列表沿用图标与文字布局及浅色选中反馈;进入“会话编程设置”后的项目配置页及插件资源页复用相同的左侧工作栏,返回与收起按钮、项目和会话列表、底部作品广场与账户的位置保持一致,并沿用当前宽度和收起状态;配置正文、顶部项目名称与配置页自身的返回操作保留。尚未选择本地项目时,页面以麦洛 M 标识和居中引导语建立焦点,下方提供独立的“新增项目”主操作以及可横向浏览、直接进入的已有项目卡片;项目创建和进入仍复用侧栏现有流程。已有项目的新 Conversation 在可编辑 Composer 上方沿用同一品牌引导,第一条消息出现后切换为正常时间线。 - AI 编程历史与配额反馈:Pi 上下文压缩不删除用户可见的 active-branch 历史,压缩摘要保持 Main-private;Renderer 首次渲染最近 120 个节点,向上滚动时按 100 个节点追加更早内容并保持阅读位置。Main 精确识别 `token_point_balance_exhausted` / `词元点数余额不足`,只向 Renderer 投影不可重试的稳定配额错误和安全中文提示,重开会话后仍可见且不暴露上游原始错误或 request id。 - 账号词元点数:左下角账户菜单显示本人永久余额和预占;“词元点数与充值”入口暂时隐藏,充值、充值订单与分页收支记录的既有实现保留。真正新注册赠送 100 点,1 元充值 50 点,永久有效;旧账户不补送,旧会员/订阅/周额度/重置卡入口已移除。家长为孩子充值时登录孩子的账号,页面显示当前昵称和账号,点数进入该账号自己的钱包;无需将孩子身份改为家长。当前有效账号可给本人充值,青少年可查看自己的精确余额;使用他人共享钱包时仅显示可用状态,不显示付款人的余额或记录。Main 持有凭据并转发充值请求;付款结果未知时沿用原请求并核查原订单,只有服务端确认才显示到账。扫码信息缺失时可到网页账户页继续原订单,付款来源也由网页账户页管理,充值不会自动切换共享/个人来源。需配套部署 Works Square 当前账号充值资格更新(本次无需新迁移,原永久点数基础为 0094);本地模拟验收不代表真实支付通过。 - 桌面性能策略:应用窗口使用不透明浅色表面并默认保留硬件合成;仅在显式安全模式或短时间内重复 GPU 进程崩溃时启用软件渲染,并把故障原因保存在用户数据目录。启动关键路径只创建本地应用壳,认证、代理、同步、更新和遥测在首帧后延迟初始化;开发版可通过 `app:performance` 快照观察 GPU、进程、WebContents、事件循环与 Renderer Long Task 聚合指标。 diff --git a/src/components/layout/ImageWorkspaceSidebar.tsx b/src/components/layout/ImageWorkspaceSidebar.tsx index ad7bd91b..637cd34f 100644 --- a/src/components/layout/ImageWorkspaceSidebar.tsx +++ b/src/components/layout/ImageWorkspaceSidebar.tsx @@ -34,17 +34,6 @@ type ProjectDialogState = | { mode: 'create'; project: null } | { mode: 'rename'; project: DesignWorkspaceSummary }; -function formatUpdatedAt(value: string): string { - const timestamp = Date.parse(value); - if (!Number.isFinite(timestamp)) return ''; - const date = new Date(timestamp); - const now = new Date(); - if (date.toDateString() === now.toDateString()) { - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } - return date.toLocaleDateString([], { month: 'numeric', day: 'numeric' }); -} - export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSidebarProps) { const authenticated = useAuthStore((state) => state.isAuthenticated()); const navigate = useNavigate(); @@ -178,7 +167,7 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
- 设计项目 + 我的项目 {(status === 'error' || status === 'unavailable') && (
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 7223d4cf..fe0f6836 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,4 +1,3 @@ -import classroomMark from '@/assets/consultations/mark.svg'; import { CodingProjectConversations } from '@/pages/Chat/CodingProjectConversations'; import { useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; @@ -442,10 +441,6 @@ export function Sidebar({ {workspaceLayout && !['darwin', 'win32'].includes(window.electron?.platform ?? '') &&
} - {workspaceLayout && } {!workspaceLayout && }
} diff --git a/src/pages/Chat/classroom-workspace.css b/src/pages/Chat/classroom-workspace.css index f1c2a902..cc9f2b36 100644 --- a/src/pages/Chat/classroom-workspace.css +++ b/src/pages/Chat/classroom-workspace.css @@ -18,7 +18,7 @@ [data-testid="sidebar"][data-classroom="true"] .classroom-brand:hover { background: #eaf1f8; } .classroom-brand-mark { display: grid; place-items: center; width: 40px; height: 42px; flex-shrink: 0; border: 1px solid #f1d1aa; border-radius: 12px; background: #fff0dd; box-shadow: 0 3px 0 #ecdbc5; } .classroom-brand-caption { display: block; margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: #64758a; } -[data-testid="sidebar"][data-classroom="true"] .classroom-sidebar-content { padding: 10px 12px 14px; } +[data-testid="sidebar"][data-classroom="true"] .classroom-sidebar-content { padding: 0 12px 14px; } [data-testid="sidebar"][data-classroom="true"] [data-testid="sidebar-create-project"] { min-height: 48px; gap: 8px; padding: 8px; border: 0; border-radius: 8px; background: transparent; color: #26384d; box-shadow: none; font-size: 14px; font-weight: 600; } [data-testid="sidebar"][data-classroom="true"] [data-testid="sidebar-create-project"] svg { color: #c44b23; } [data-testid="sidebar"][data-classroom="true"] [data-testid="sidebar-create-project"]:hover { background: #fff0e6; } diff --git a/tests/e2e/main-navigation.spec.ts b/tests/e2e/main-navigation.spec.ts index 2ee0f717..8f433dd3 100644 --- a/tests/e2e/main-navigation.spec.ts +++ b/tests/e2e/main-navigation.spec.ts @@ -410,21 +410,16 @@ test.describe('Makelore module navigation without setup flow', () => { await expect(moduleReturn).toHaveAttribute('aria-label', '返回首页'); await expect(moduleReturn).toHaveAttribute('title', '返回首页'); await expect(page.getByTestId('sidebar-module-return-icon')).toBeVisible(); - const brandBox = await sidebar.locator('.classroom-brand').boundingBox(); + await expect(sidebar.locator('.classroom-brand')).toHaveCount(0); + await expect(sidebar).not.toContainText('把脑洞变成作品'); const returnBox = await moduleReturn.boundingBox(); const collapseBox = await page.getByRole('button', { name: '折叠侧栏' }).boundingBox(); const sidebarBox = await sidebar.boundingBox(); - expect(brandBox).not.toBeNull(); expect(returnBox).not.toBeNull(); expect(collapseBox).not.toBeNull(); - expect(returnBox!.y + returnBox!.height).toBeLessThanOrEqual(brandBox!.y); + expect(returnBox!.y + returnBox!.height).toBeLessThanOrEqual(sidebarBox!.y); const headerBox = await page.getByTestId('coding-conversation-header').boundingBox(); - const tabsBox = await page.getByRole('tablist', { name: '当前工作' }).boundingBox(); - const toolbarBox = await page.locator('.classroom-workspace-toolbar').boundingBox(); expect(Math.abs(returnBox!.y + returnBox!.height / 2 - headerBox!.y - headerBox!.height / 2)).toBeLessThan(1); - expect(Math.abs(brandBox!.y + brandBox!.height / 2 - tabsBox!.y - tabsBox!.height / 2)).toBeLessThan(1); - expect(brandBox!.y - sidebarBox!.y).toBe(0); - expect(brandBox!.height).toBe(toolbarBox!.height); expect(returnBox!.width).toBe(32); expect(Math.abs(returnBox!.y + returnBox!.height / 2 - collapseBox!.y - collapseBox!.height / 2)).toBeLessThan(1); expect(returnBox!.x + returnBox!.width).toBeLessThanOrEqual(collapseBox!.x); @@ -436,7 +431,8 @@ test.describe('Makelore module navigation without setup flow', () => { const createBox = await sidebar.getByTestId('sidebar-create-project').boundingBox(); const headingBox = await sidebar.locator('.classroom-projects-heading').boundingBox(); const accountBox = await sidebar.getByTestId('sidebar-member-menu-trigger').boundingBox(); - expect(createBox!.y - brandBox!.y - brandBox!.height).toBe(10); + expect(createBox!.y).toBe(sidebarBox!.y); + expect(headingBox!.y - sidebarBox!.y).toBe(56); expect(communityBox!.x).toBe(createBox!.x); expect(communityBox!.y).toBeGreaterThan(headingBox!.y + headingBox!.height); expect(accountBox!.y - communityBox!.y - communityBox!.height).toBeCloseTo(0, 0); @@ -460,7 +456,7 @@ test.describe('Makelore module navigation without setup flow', () => { const fullscreenReturn = await moduleReturn.boundingBox(); expect(fullscreenReturn!.y).toBe(returnBox!.y); expect(await page.getByRole('button', { name: '折叠侧栏' }).boundingBox()).toEqual(collapseBox); - expect(await sidebar.locator('.classroom-brand').boundingBox()).toEqual(brandBox); + expect(await sidebar.getByTestId('sidebar-create-project').boundingBox()).toEqual(createBox); await page.screenshot({ path: test.info().outputPath('chat-fullscreen-return-aligned.png') }); await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].emit('leave-full-screen')); await expect.poll(async () => (await moduleReturn.boundingBox())!.x).toBe(returnBox!.x); @@ -486,13 +482,13 @@ test.describe('Makelore module navigation without setup flow', () => { await expect(sidebarResizer).toHaveAttribute('aria-valuenow', '171'); const readCodeChrome = () => page.evaluate(() => { + if (document.querySelector('[data-testid="sidebar"] .classroom-brand')) throw new Error('Removed Code brand reappeared'); const selectors = { sidebar: '[data-testid="sidebar"]', titlebar: '[data-testid="titlebar-sidebar-surface"]', navigation: '[data-testid="classroom-titlebar-navigation"]', home: '[data-testid="sidebar-module-switcher-trigger"]', toggle: '[data-testid="titlebar-sidebar-toggle"]', - brand: '[data-testid="sidebar"] .classroom-brand', create: '[data-testid="sidebar-create-project"]', heading: '[data-testid="sidebar"] .classroom-projects-heading', community: '[data-testid="sidebar"] .community-link[data-variant="sidebar"]', @@ -752,7 +748,7 @@ test.describe('Makelore module navigation without setup flow', () => { await expect(page.getByTestId('coding-existing-projects')).toHaveCount(0); await expect(page.getByText('一念成光,万物可创。')).toHaveCount(0); await expect(page.getByRole('img', { name: 'Makelore logo' })).toHaveCount(0); - await expect(page.getByTestId('sidebar').locator('.classroom-brand')).toBeVisible(); + await expect(page.getByTestId('sidebar').locator('.classroom-brand')).toHaveCount(0); await expect(page.getByTestId('sidebar-module-switcher-trigger')).toHaveText(''); await expect(page.getByTestId('sidebar-module-return-icon')).toBeVisible(); await expect(page.getByTestId('sidebar-create-project')).toBeVisible(); diff --git a/tests/unit/image-workspace-sidebar.test.tsx b/tests/unit/image-workspace-sidebar.test.tsx index a9d3a86c..5f29ba5a 100644 --- a/tests/unit/image-workspace-sidebar.test.tsx +++ b/tests/unit/image-workspace-sidebar.test.tsx @@ -189,12 +189,23 @@ describe('ImageWorkspaceSidebar', () => { expect(actions.renameProject).not.toHaveBeenCalled(); }); - it('selects a project by its workspace identity from the project row', () => { - const { target, actions } = prepareProjects(); + it('shows only project titles under My Projects and keeps recent-first selection by workspace identity', () => { + const { workspace, target, actions } = prepareProjects(); renderSidebar(); const sidebar = screen.getByTestId('sidebar-image-workspace'); - fireEvent.click(within(sidebar).getByRole('button', { name: new RegExp(`^${target.title}`) })); + expect(within(sidebar).getByText('我的项目', { exact: true })).toBeInTheDocument(); + expect(within(sidebar).queryByText('设计项目', { exact: true })).not.toBeInTheDocument(); + expect(sidebar).not.toHaveTextContent('设计版本'); + const activeProject = within(sidebar).getByRole('button', { name: workspace.workspace.title, exact: true }); + const newerProject = within(sidebar).getByRole('button', { name: target.title, exact: true }); + expect(activeProject.textContent).toBe(workspace.workspace.title); + expect(newerProject.textContent).toBe(target.title); + expect(activeProject).toHaveAttribute('aria-current', 'page'); + // The September project is newer than the active August project, despite + // arriving second in bootstrap. Removing its date must not change order. + expect(newerProject.compareDocumentPosition(activeProject)).toBe(Node.DOCUMENT_POSITION_FOLLOWING); + fireEvent.click(newerProject); expect(actions.selectProject).toHaveBeenCalledExactlyOnceWith(target.workspaceId); expect(screen.getByLabelText('当前页面')).toHaveTextContent('/image-canvas');