From 163f2627539a95b4ae78669f7c40b510ee2c4039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=A8=E9=B1=BC=E8=BE=A3=E6=A4=92?= Date: Tue, 29 Sep 2026 23:25:41 +0800 Subject: [PATCH] fix: align Code return and sidebar collapse in the top row --- .../20260929-code-back-entry-33d87a81.md | 16 +++++++++++--- README.md | 2 +- src/components/layout/ModuleSwitcher.tsx | 10 +++++---- src/components/layout/Sidebar.tsx | 16 +++++++++++++- src/components/layout/TitleBar.tsx | 2 +- tests/e2e/main-navigation.spec.ts | 22 +++++++++++++++++-- tests/unit/title-bar.test.tsx | 4 ++-- 7 files changed, 58 insertions(+), 14 deletions(-) diff --git a/.project-docs/30-worklog/tasks/20260929-code-back-entry-33d87a81.md b/.project-docs/30-worklog/tasks/20260929-code-back-entry-33d87a81.md index 48eb0f52..cc9efe70 100644 --- a/.project-docs/30-worklog/tasks/20260929-code-back-entry-33d87a81.md +++ b/.project-docs/30-worklog/tasks/20260929-code-back-entry-33d87a81.md @@ -8,11 +8,11 @@ - Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20260929-empty-work-merge-39c633d0 - Base commit: a77ddbd6813bb71d490544edfef6e1f35240070f - Owner: codex -- Status: Ready for Integration +- Status: Planning ## Scope -- Move the existing “编程 Code” module-home return entry from the project configuration sidebar into the Coding chat/workspace sidebar below the brand. Retain its `/module-select` target and the configuration page's own back action. +- Move the existing “编程 Code” module-home return entry from the project configuration sidebar into the Coding chat/workspace sidebar top row above the brand. Retain its `/module-select` target and the configuration page's own back action. ## Intent And Constraints @@ -43,7 +43,7 @@ ## Promotion Candidates -- Target: canonical classroom navigation when next integrated. Proposal: Code module-home return is visible below the chat sidebar brand and omitted from project configuration/resource sidebars; original configuration back remains. Evidence: explicit user confirmation, existing component reuse, actual Electron navigation and screenshots. No architecture or module-target change; no further product confirmation required. +- Target: canonical classroom navigation when next integrated. Proposal: Code module-home return is visible at the upper left above the chat sidebar brand, aligned with the right-side collapse control and omitted from project configuration/resource sidebars; original configuration back remains. Evidence: explicit user confirmation, existing component reuse, actual Electron navigation and screenshots. No architecture or module-target change; no further product confirmation required. ## Follow-up: Update The Temporary Renderer @@ -59,3 +59,13 @@ - Final generated source: `code-back-entry-33d87a81/source-with-group-intro` under the trial root; running JS `index-Dh1e8q7U.js`, CSS `index-CA1PKQZ_.css`. Typecheck, scoped lint and Renderer build passed again on this composite source. Launch/guidance metadata and build-source record explicitly retain Main/base/overlay provenance for subsequent updates; old assets and pre-update entry/metadata are retained for rollback. - Live verification: Code return button visible on `/chat`; same account, active conversation, selected teacher, teacher content, current Work tab and all 14 consultation drafts. All persisted conversation-history hashes unchanged; one generated runtime prompt manifest refreshed during normal conversation rehydration. Visible virtualized message count changed after rehydration, so full on-screen text equality was not claimed. No model prompt was sent for validation. Configuration and nested-route absence were covered by the preceding isolated Electron E2E. - Used the normal lifecycle lease and conversation-selection API to recover an idle runtime after Renderer reload, then released the lease and restored background throttling. Final live state has zero alerts and unchanged visibility/focus. A native screenshot attempt timed out; actual DOM/button presence, source checks and previous isolated visual/E2E evidence were used instead. Verification summary is in the trial update directory's `verification.json`; private snapshots are mode 0600 and never needed for handoff. + + +## Follow-up: Align Return And Collapse Controls + +- User asks for the Code return at the sidebar upper left and collapse icon at the right of the same row, then update the trial. Same task resumed with exact feature/worktree/branch/base ownership verified. Shared context has no changes since dcaa7ba; active task, entry and all peer scopes reread. Teacher-composer-actions is updating the trial separately; preserve its latest published source when staging. Other ready peers do not create a navigation conflict. +- Project Context Loaded: current light Electron workspace, Main-owned APIs and local project/conversation state remain authoritative. Sidebar default/minimum widths are 171/128px, so macOS traffic-light reservation of 88px cannot accommodate the full return label plus a collapse button in the native row. Use one compact row at the top of the Sidebar above the brand; preserve native titlebar chrome and the stable collapsed/peek expand control. Configuration routes continue to hide return. Narrow sidebars truncate only the visual label, keeping accessible navigation. Linux retains its existing non-collapsible behavior. +- Planning Gate: Passed. Update Sidebar/TitleBar and compact ModuleSwitcher presentation, adapt the existing navigation/layout checks, build, and refresh the latest trial Renderer while preserving conversation state. No main merge, push or formal-app replacement. + +- Implementation: reused ModuleSwitcher in a 40px top sidebar row, removed its vertical offset for this toolbar variant, and aligned a 32px collapse button to the right. TitleBar keeps the existing stable expand/peek button only when the Code sidebar is collapsed. Other module layouts and configuration-page absence remain unchanged. +- Verification: typecheck, changed-file lint and full production build passed. Relevant unit coverage totals 26 passing tests (two old TitleBar assertions updated to the new Sidebar-owned expanded control). Existing Electron navigation scenario passes with actual same-row/right-edge geometry, no duplicate toggle, 128px narrow width without overlap, config/nested absence and collapsed-hover return. Reviewed default-width screenshot. Independent implementation review passed. Temporary Renderer staging is in progress and includes the separately published teacher-composer-actions overlay. diff --git a/README.md b/README.md index 812c0653..d1cf157f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,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`。左侧麦洛品牌区下方提供“← 编程 Code”按钮,点击返回模块选择首页;项目配置及其资源子页不再重复显示此侧栏入口,保留配置页自身的返回操作。尚未选择本地项目时,页面以麦洛 M 标识和居中引导语建立焦点,下方提供独立的“新增项目”主操作以及可横向浏览、直接进入的已有项目卡片;项目创建和进入仍复用侧栏现有流程。已有项目的新 Conversation 在可编辑 Composer 上方沿用同一品牌引导,第一条消息出现后切换为正常时间线。 +- AI 编程默认入口:进入 Code 后保持在 `/chat`。左侧栏顶部在品牌区上方提供同排导航:左侧“← 编程 Code”返回模块选择首页,右侧收起侧栏;窄侧栏仅省略溢出的文字,折叠后的展开入口保持在标题栏;项目配置及其资源子页不再重复显示此侧栏入口,保留配置页自身的返回操作。尚未选择本地项目时,页面以麦洛 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 永久点数 API 与 0094 迁移;本地模拟验收不代表真实支付通过。 - 桌面性能策略:应用窗口使用不透明浅色表面并默认保留硬件合成;仅在显式安全模式或短时间内重复 GPU 进程崩溃时启用软件渲染,并把故障原因保存在用户数据目录。启动关键路径只创建本地应用壳,认证、代理、同步、更新和遥测在首帧后延迟初始化;开发版可通过 `app:performance` 快照观察 GPU、进程、WebContents、事件循环与 Renderer Long Task 聚合指标。 diff --git a/src/components/layout/ModuleSwitcher.tsx b/src/components/layout/ModuleSwitcher.tsx index 2c7cb199..d1833127 100644 --- a/src/components/layout/ModuleSwitcher.tsx +++ b/src/components/layout/ModuleSwitcher.tsx @@ -3,7 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { cn } from '@/lib/utils'; import { AI_MODULE_SELECTION_PATH, aiModules, getAiModuleForPath } from '@/lib/ai-modules'; -export function ModuleSwitcher({ sidebarCollapsed, compact = false }: { sidebarCollapsed: boolean; compact?: boolean }) { +export function ModuleSwitcher({ sidebarCollapsed, compact = false, toolbar = false }: { sidebarCollapsed: boolean; compact?: boolean; toolbar?: boolean }) { const location = useLocation(); const navigate = useNavigate(); const activeModuleId = getAiModuleForPath(location.pathname); @@ -14,7 +14,7 @@ export function ModuleSwitcher({ sidebarCollapsed, compact = false }: { sidebarC const compactTrigger = compact && !sidebarCollapsed; return ( -
+
); diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index d6e37bff..f6a4ad26 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -11,6 +11,7 @@ import { FolderKanban, Gamepad2, LogOut, + PanelLeftClose, Plus, RadioTower, Settings as SettingsIcon, @@ -90,6 +91,7 @@ export function Sidebar({ }: SidebarProps = {}) { const { t } = useTranslation('common'); const storedSidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed); + const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed); const sidebarWidth = useSettingsStore((state) => normalizeClassroomSidebarWidth(state.classroomSidebarWidth)); const sidebarPinnedCollapsed = !forceExpanded && storedSidebarCollapsed; const sidebarCollapsed = sidebarPinnedCollapsed && !sidebarPeekOpen; @@ -412,11 +414,23 @@ export function Sidebar({ )} >
+ {workspaceLayout &&
+ + {!sidebarPinnedCollapsed && ['darwin', 'win32'].includes(window.electron?.platform ?? '') && } +
} {workspaceLayout && } - + {!workspaceLayout && }
} diff --git a/src/components/layout/TitleBar.tsx b/src/components/layout/TitleBar.tsx index c73241ec..fbceacfe 100644 --- a/src/components/layout/TitleBar.tsx +++ b/src/components/layout/TitleBar.tsx @@ -178,7 +178,7 @@ function ProductTitleBar({ onPointerEnter={sidebarPreviewTitlebarVisible ? () => handleSidebarPeekEnter('titlebar') : undefined} onPointerLeave={sidebarPreviewTitlebarVisible ? () => handleSidebarPeekLeave('titlebar') : undefined} /> - {typeof document !== 'undefined' ? createPortal(sidebarTogglePortal, document.body) : sidebarTogglePortal} + {(!workspaceLayout || sidebarCollapsed) && (typeof document !== 'undefined' ? createPortal(sidebarTogglePortal, document.body) : sidebarTogglePortal)} ) : null} {integrated ? ( diff --git a/tests/e2e/main-navigation.spec.ts b/tests/e2e/main-navigation.spec.ts index d5fef1e5..af306976 100644 --- a/tests/e2e/main-navigation.spec.ts +++ b/tests/e2e/main-navigation.spec.ts @@ -83,14 +83,32 @@ test.describe('Makelore module navigation without setup flow', () => { await expect(moduleReturn).toContainText('编程 Code'); await expect(moduleReturn).toHaveAttribute('aria-label', '返回首页'); await expect(sidebar.getByTestId('sidebar-module-return-icon')).toBeVisible(); - await expect(moduleReturn).toHaveCSS('padding-top', '8px'); const brandBox = await sidebar.locator('.classroom-brand').boundingBox(); const returnBox = await moduleReturn.boundingBox(); + const collapseBox = await sidebar.getByRole('button', { name: '折叠侧栏' }).boundingBox(); + const sidebarBox = await sidebar.boundingBox(); expect(brandBox).not.toBeNull(); expect(returnBox).not.toBeNull(); - expect(returnBox!.y).toBeGreaterThanOrEqual(brandBox!.y + brandBox!.height); + expect(collapseBox).not.toBeNull(); + expect(returnBox!.y + returnBox!.height).toBeLessThanOrEqual(brandBox!.y); + expect(Math.abs(returnBox!.y + returnBox!.height / 2 - collapseBox!.y - collapseBox!.height / 2)).toBeLessThan(1); + expect(returnBox!.x + returnBox!.width).toBeLessThanOrEqual(collapseBox!.x); + expect(sidebarBox!.x + sidebarBox!.width - collapseBox!.x - collapseBox!.width).toBeLessThanOrEqual(14); + await expect(page.getByTestId('titlebar-sidebar-toggle')).toHaveCount(1); await page.screenshot({ path: test.info().outputPath('chat-code-return-entry.png') }); + const sidebarResizer = sidebar.getByRole('separator', { name: '调整左侧功能栏宽度' }); + await sidebarResizer.press('Home'); + await expect(sidebarResizer).toHaveAttribute('aria-valuenow', '128'); + const narrowReturn = await moduleReturn.boundingBox(); + const narrowToggle = await sidebar.getByRole('button', { name: '折叠侧栏' }).boundingBox(); + expect(narrowReturn!.x + narrowReturn!.width).toBeLessThanOrEqual(narrowToggle!.x); + await expect(moduleReturn).toBeVisible(); + await expect(sidebar.getByRole('button', { name: '折叠侧栏' })).toBeVisible(); + await page.screenshot({ path: test.info().outputPath('chat-narrow-navigation-row.png') }); + await sidebarResizer.dblclick(); + await expect(sidebarResizer).toHaveAttribute('aria-valuenow', '171'); + await sidebar.getByRole('button', { name: '项目设置', exact: true }).click(); await expect(page).toHaveURL(/\/project-config$/); await expect(page.getByTestId('project-configuration-page')).toBeVisible(); diff --git a/tests/unit/title-bar.test.tsx b/tests/unit/title-bar.test.tsx index 11e67efd..49c9ad30 100644 --- a/tests/unit/title-bar.test.tsx +++ b/tests/unit/title-bar.test.tsx @@ -91,7 +91,7 @@ describe('TitleBar platform behavior', () => { expect(screen.queryByTestId('titlebar-logo')).not.toBeInTheDocument(); expect(screen.queryByRole('img', { name: 'Makelore logo' })).not.toBeInTheDocument(); expect(screen.queryByTestId('titlebar-project-context')).not.toBeInTheDocument(); - expect(screen.getByRole('button', { name: '折叠侧栏' })).toBeEnabled(); + expect(screen.queryByRole('button', { name: '折叠侧栏' })).not.toBeInTheDocument(); }); it('renders the painting title bar as a transparent overlay with the logo at the window edge', () => { @@ -195,7 +195,7 @@ describe('TitleBar platform behavior', () => { fireEvent.click(pinButton); expect(useSettingsStore.getState().sidebarCollapsed).toBe(false); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveStyle({ width: '171px', minWidth: '171px', flexBasis: '171px' }); - expect(screen.getByRole('button', { name: '折叠侧栏' })).toBeEnabled(); + expect(screen.queryByRole('button', { name: '折叠侧栏' })).not.toBeInTheDocument(); }); it('extends the ordinary titlebar sidebar surface during a hover preview', () => {