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 new file mode 100644 index 00000000..2c10fdb8 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260929-code-back-entry-33d87a81.md @@ -0,0 +1,154 @@ +# Task: Move Code return navigation into the coding workspace + +## Identity + +- Task ID: 20260929-code-back-entry-33d87a81 +- Mode: Feature +- Branch: codex/20260929-code-back-entry-33d87a81-code-back-entry +- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20260929-empty-work-merge-39c633d0 +- Base commit: a77ddbd6813bb71d490544edfef6e1f35240070f +- Owner: codex +- Status: Planning + +## Scope + +- Move the existing “编程 Code” module-home return entry from the project configuration sidebar into the Coding workspace titlebar as an icon-only arrow aligned with the conversation header, with the brand aligned to the tabs below. Retain its `/module-select` target and the configuration page's own back action. + +## Intent And Constraints + +- User first requested understanding only, confirmed the two-page interpretation, then explicitly approved implementation. Preserve existing light styling, projects, conversation drafts/history, module access and other module navigation. Initial implementation excluded live-app updates. The follow-up explicitly authorizes the temporary Renderer update; main merge, push and formal-app deployment remain outside this change. +- Reused the clean, completed prior merge checkout after releasing its ready task ownership; old branch/commits remain. Created a new branch at current main a77ddbd, then successfully claimed this new task and verified exact task/mode/worktree/branch/base/reservation. No source from occupied foreign worktrees adopted. + +## Project Context Loaded + +- Task identity: as above, feature mode; ownership verified. Read active task and entry/memory index. Previously loaded positioning, current-state, decision-index, teacher ADR, architecture/domain, evidence, commitments and stale items are unchanged from 5aabaaf, verified by Git diff. Current AGENTS still requires existing light system, local project state and Main-owned APIs. +- Product/context: current main includes empty-work behavior, teacher voice/model controls and compact published shortcuts. This is a small navigation relocation within the existing sidebar; no new visual system is needed. Inspected screenshots, Sidebar, ModuleSwitcher, MainLayout, module route definitions, layout CSS and existing navigation tests. +- Peer scopes reviewed read-only. Active teacher-group-intro and child-language tasks have undefined scope, so coordination context is unknown; neither supplies a known dependency on module-return navigation. Existing teacher/UI tasks are ready. This task owns Sidebar navigation plus focused test/README updates only; no shared canonical docs. +- Relevant behavior: original “编程 Code” ModuleSwitcher navigates to `/module-select`, not a project reset. `/chat` uses the classroom sidebar; project configuration and nested resource pages use the generic sidebar. Retain the original target as the user confirmed. +- Gate: Passed. Plan: show the existing module-return control below the Code workspace brand, omit it on `/project-config` and nested configuration routes, keep a compact accessible layout, adjust the existing navigation regression, run typecheck/lint/build and focused UI verification, review and commit. + +## Outcome + +- Reused the existing ModuleSwitcher as an icon-only arrow in the `/chat` titlebar, aligned with the collapse control and the right-side conversation header, retaining its accessible return-home label and `/module-select` action. Project configuration and nested resource pages omit the entire sidebar header block, so no empty placeholder remains. Existing configuration back action, sidebar resizing/peek and other module controls are unchanged. README synchronized. + +## Verification + +- pnpm 10.33.4 standard typecheck, changed-file ESLint and production Renderer/Main/Preload/utility build passed. Existing sidebar/peek/module gate and module-navigation unit coverage: 23 tests across four files passed. +- Extended one existing navigation scenario; isolated Electron E2E 1/1 passed. It now checks icon-only return above the brand, no sidebar entry on configuration or plugin subpage, configuration back to chat, and collapsed-sidebar hover followed by real navigation to `/module-select`. Used an independent test profile/ports; no real user application or conversations were changed. Test command: `corepack pnpm exec playwright test tests/e2e/main-navigation.spec.ts --grep 'opens remembered Code projects' --reporter=list`. +- Inspected chat and configuration screenshots; delegated review also checked collapsed peek. Artifacts are under `test-results/main-navigation-Makelore-m-9ce56-instead-of-project-settings/`. Independent code review passed; no additional regression found. Whitespace and task-doc drift checks passed. + +## Follow-ups + +- Source is ready for integration. The temporary app now includes this navigation change; main and remote remain unchanged. + +## Promotion Candidates + +- Target: canonical classroom navigation when next integrated. Proposal: the icon-only module-home return and collapse control align with the conversation header, with branding aligned to the tabs below 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 + +- User explicitly requested the temporary-app update. Same-task ownership resumed and verified. Existing context is unchanged; active peer scopes read. Two other chats are working on teacher presentation changes; their task records and read-only status show no newly published live version yet. No messages sent to other chats. Recheck launch manifest and process identity immediately before applying this narrowly scoped update; adapt to any newer publication rather than replacing it. +- Current trial PID 32583 / Host API 54599 uses composer Main 78cfde6 with empty-work overlay, Renderer efd1894 and latest small-card stylesheet fef3b3a. Prepare the navigation patch against this exact generated Renderer source and preserve current Main/profile. Snapshot drafts/auth/project/teacher selection and histories privately, refresh only the Renderer from its existing served directory, and verify the new navigation plus state preservation. Main merge, remote push and formal installed-app replacement remain outside this follow-up. +- Planning Gate: Passed; only the generated trial artifact and this task record are writable. Keep rollback assets and truthful base/overlay metadata, and never start a model prompt for validation. + + +### Temporary Update Outcome + +- Updated the existing trial Renderer in place at `/var/folders/mx/ltbgwsks5_n8zllj69p_wbcw0000gn/T/makelore-trial-tdq_nuah/teacher-composer-4f829ac1/source/dist`. Main PID 32583, Host API 54599, isolated profile and native window visibility/focus are unchanged. Formal `/Applications/Makelore.app` was not modified. +- Detected another chat publishing teacher-group introductions during preparation. Discarded the earlier candidate and rebuilt from its actual published source, preserving fef3b3a prompt/reply styling, 0167d36 empty-work behavior and 7458c37 grouped introductions. Only source difference is `src/components/layout/Sidebar.tsx`, byte-identical to product commit 10b179f; independently reviewed. +- 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. + +- Temporary app updated successfully from the newest built teacher-composer-actions source (8af0ca7), retaining grouped introductions, empty-work behavior and all earlier overlays. Running JS is `index-BSBCYLkH.js`, CSS `index-CA1PKQZ_.css`; generated source is `/var/folders/mx/ltbgwsks5_n8zllj69p_wbcw0000gn/T/makelore-trial-tdq_nuah/code-back-toprow-33d87a81/source`. Main PID 32583/profile remain unchanged. +- Actual live geometry confirms exactly one collapse control, same horizontal center, no overlap, 13px inset from the sidebar right edge and navigation above the brand. Current account, rendered messages, teacher text/panel, Work tab and all 14 drafts match the pre-refresh snapshot; all 34 recorded data files have unchanged hashes. Zero alerts. Normal runtime lease released, prior background throttling restored and the inspector opened for this refresh closed. Launch/guidance provenance now points at the combined built Renderer, including the new 163f262 overlay; separate temporary CSS injection is no longer needed for the preserved teacher-controls update. No paid validation prompt, main merge, push or formal-install change. + + +## Follow-up: Brand First + +- User corrects the order using a brand screenshot: the Makelore logo/name/caption must be at the top. Keep the return/collapse row directly beneath it with left/right alignment. This supersedes the previous interpretation that navigation comes before branding, and includes updating the same trial. +- Project Context Loaded / Planning Gate: Passed. Same-task feature ownership, branch/worktree/base verified. Entry, positioning, integrated state, decisions, architecture/domain and other previously loaded context are unchanged from 1329d23; active record and peer scopes read again. The teacher-controls task is now ready and already preserved in the trial. No semantic conflict or new shared-memory write. Only reorder existing Sidebar markup, align the existing E2E/README description, verify and refresh the latest generated Renderer with existing state preservation. + +- Reordered only the existing Sidebar brand/control blocks. Updated README and the existing Electron scenario to require brand-first positioning while keeping the same-row/right-edge, narrow-width, collapsed return and configuration-route checks. Typecheck, scoped lint, production build and the complete existing navigation scenario passed (1/1); inspected the actual default-width screenshot. No new behavior or dependency was introduced. + +- Updated the same live trial to `index-C-y4CmSj.js` / `index-CA1PKQZ_.css` from `code-brand-first-33d87a81/source`, preserving all published overlays. Independent source review confirms Sidebar is the sole product delta; composite typecheck/lint/build passed. Live DOM confirms brand-first at the top of the Sidebar, controls directly below, identical horizontal centers, no overlap and a 13px right inset. Current account, 14 drafts, messages, teacher panel/content and pre-refresh Operation Chat tab were restored and compare equal; all 34 recorded data hashes are unchanged and no alerts remain. Main PID 32583 is unchanged; refresh lease released and task-opened inspector closed. Launch/guidance metadata include ae646f6. No merge, remote push or formal-app changes. + + +## Follow-up: Group Navigation And Personal Projects + +- User confirmed the hierarchy: move the brand/navigation group upward, then Code return, Works Square, a group gap, New Project, My Projects and its list. Brand stays first; collapse remains aligned on the right of Code. Public discovery belongs with navigation; creation belongs directly with personal project management. +- Project Context Loaded / Planning Gate: Passed. Same-task ownership and recorded feature branch/worktree/base are verified. Previously read entry, positioning, decisions, integrated architecture/domain and commitments remain unchanged from 41901ee. Active record and peer scopes reread; the only new peer merged teacher controls into main without touching the app. The current trial remains our combined brand-first source and all overlays. No conflicting navigation work identified. +- Plan: reduce chat-only header/brand top padding, place the existing CommunityLink directly below the Code control row, keep New Project immediately above My Projects with a larger gap between the two groups, preserve other module/config layouts and behavior. Update the existing layout/navigation verification and README, build against the current live source, and refresh the same temporary Renderer/profile. + +- Implemented the public navigation group in the non-scrolling chat header (brand, Code/collapse, Works Square), followed by a 20px group gap before New Project and an 8px projects-section margin. Reduced the chat header top padding from 8px to 0 and the brand padding from 10px/12px to 0/8px, moving its visible content upward while retaining native chrome. Other routes retain their previous ordering/styles. +- Validation: typecheck, changed-file lint, full production build and 26 relevant unit tests passed. The existing Electron navigation scenario passed with brand-at-top, Code/Works adjacency, gap before creation, New Project/My Projects adjacency, 128px no-overflow, unchanged config routes and collapsed return. Inspected screenshot; independent source review passed. Composite live-source build also passed and differs only in Sidebar and scoped classroom CSS. + +- Trial updated in place to `index-DUwPUKit.js` / `index-CA1PKQZ_.css`, generated under `sidebar-groups-33d87a81/source`; existing Main PID 32583, user profile and published overlays retained. Live checks confirm brand at top, Code/collapse aligned, one Works Square link directly beneath Code in the header, 20px separation before New Project and My Projects immediately after creation. Current account, conversation/messages, 14 drafts, closed teacher panel state and Operation Chat tab match pre-refresh; all 34 recorded data hashes unchanged, no alerts. Released refresh lease/restored background throttling; pre-existing inspector was left as found. Launch/guidance finalization was subsequently blocked by a concurrently replaced Renderer entry; pending composite publication below. No main merge/push or formal-app change. + +- Publication race detected: another chat replaced the served entry with teacher-text-shortcuts `index-8Lk2MctW.js` before our metadata finalization. The precondition blocked metadata writes; do not claim final completion until a composite preserves that new CSS/prompt rendering behavior. Resumed exact same task, retained snapshots, and prepare a fresh candidate from the actually served teacher-text-shortcuts source plus only this task's two-file patch. Existing product context and scope unchanged; Planning Gate remains Passed. + +- Final composite publication completed after the peer finalized fd0b720. Current source is `sidebar-groups-33d87a81/source-with-text-shortcuts`, JS `index-Cyy0gXYn.js`, common CSS `index-CA1PKQZ_.css`, classroom route CSS `index-BwzfBikL.css`. Applied only the two-file sidebar patch to the published teacher-text-shortcuts source; composite typecheck/lint/build passed and full source comparison confirmed no unrelated delta. +- Final live verification confirms both changes together: exact sidebar grouping/20px separation and teacher shortcut text has transparent background, no shadow/border and the new route stylesheet loaded. Account, current conversation/messages, 14 drafts, open teacher panel/content and Work tab match this final pre-refresh snapshot; all 34 data hashes unchanged and no alerts. This final user-visible panel/tab state supersedes the earlier snapshot because the user/peer changed it during concurrent work. Main remains PID 32583. Finalization updated launch/guidance/build provenance with fd0b720 + 774f1b7, released the lease, restored throttling and closed the inspector opened for this second refresh. No required work remains for the temporary-app request. + + +## Follow-up: Icon-Only Return Above Branding + +- User now explicitly supersedes the prior brand-first ordering: remove the visible Code return text, retain only its arrow, and place the control row above Makelore at the top. Keep the right-side collapse icon aligned and preserve the public-navigation/personal-project grouping. +- Project Context Loaded / Planning Gate: Passed. Exact same-task feature ownership, worktree, branch and base verified. Shared context unchanged from a57d073; active record and all peer scopes reread. Teacher shortcut task is active and teacher composer height work is active but scoped away from navigation. Use current published Renderer, check for concurrent publication and retain existing overlays. +- Plan: add an icon-only presentation to the existing ModuleSwitcher for this Code toolbar only, move the toolbar ahead of the brand, preserve accessible label/tooltip/return target, adapt existing layout/navigation verification, build and update the same trial/profile. No main merge, remote push or formal-app replacement. + +- Implemented an icon-only 32px ModuleSwitcher in the top Code toolbar above branding. Existing return target, accessible name/tooltip, right collapse alignment, Works Square and personal-project grouping are retained. Updated README and the existing navigation scenario. Typecheck, scoped lint, production build and 26 unit tests passed; the existing Electron scenario passed (1/1) with icon text absence, 32px geometry, above-brand/top placement, 128px width, config/nested absence and collapsed hover return. Default-width screenshot inspected; independent review passed. +- Trial preparation detected a newer teacher-text-depth publication while our candidate was being validated. Rebase the generated candidate onto the actually served source before updating; no live files changed by this task yet. + +- Final temporary publication preserves the newer colored-shortcut update b934945 (including depth update 83a19fb). A second publication precondition correctly rejected finalization after the peer replaced the served entry; rebuilt from its actual published source and only reapplied Sidebar + ModuleSwitcher. Final generated source is `sidebar-icon-top-33d87a81/source-with-text-colors` under the trial root, running/next JS `index-CKAsiRqM.js`, common CSS `index-CA1PKQZ_.css`, route CSS `index-D03AC39p.css`. Composite typecheck/lint/build and full source comparison passed. Launch/guidance now consistently point at this source and include 19d55e8 plus the preserved style overlays. +- Live geometry verifies icon-only 32px return above branding at the Sidebar top, accessible name/tooltip “返回首页”, one right collapse control with equal horizontal centers and a 13px right inset, Works Square beneath branding, and the unchanged 20px separation before New Project/My Projects. Current account, active conversation, 14 consultation drafts, teacher panel/content and Work tab match the pre-refresh snapshot. Virtualization renders the last 14 of the previous 22 message elements; that suffix is exactly equal and all 34 persisted data-file hashes are unchanged. No alert or model request. Main PID 32583, isolated profile, native visibility/focus and formal installation are unchanged. Refresh lease released, original background throttling restored, task-opened inspector closed. +- Coordination lesson / promotion candidate: future temporary-app publishing should validate both served index and manifest before replacing either, then finalize their provenance together. A peer can hot-swap CSS and overwrite next-load assets while leaving running-asset metadata stale; candidate preparation must use the actual served source and preserve all published overlays. Evidence: both guarded finalization attempts in this follow-up and the final successful composite; impacts only future deployment tooling, no canonical write or policy change performed here. + + +## Follow-up: Sidebar Spacing And Works Square Surface + +- User accepts icon-only ordering and asks to move the control row upward without moving the Makelore brand, then add a filled Works Square entry and place it centrally between the brand and New Project. Temporary-app authorization continues from this UI iteration. +- Project Context Loaded / Planning Gate: Passed. Task 20260929-code-back-entry-33d87a81, feature mode, same branch/worktree/base as Identity; official resume and matching status verified. Entry, memory index, active task and peer scopes read; positioning, integrated state, decisions, architecture/domain, evidence/reflection and commitments unchanged from 7830b7f. The retired shortcut task has an unreadable record but its replacement merge task confirms source b934945 and no live-app edits; coordination state is provisional and unrelated to sidebar decisions. Composer-height task may update the trial, so preserve the newest served source and compare entry/manifest before publishing. No shared canonical writes. +- Existing product remains the single light classroom system. The design skill database's dark-theme suggestion conflicts with the current product and is not adopted; retain established green community tokens and native controls. Review confirms double overflow clipping permits a 4px upward move of the full 32px controls within the existing 40px slot. Keep that slot and the brand fixed, align controls to its top. Add 10px above Works Square and reduce project-content top padding from 20px to 10px, keeping New Project fixed with equal gaps around the entry. Only scoped classroom CSS and existing E2E/README need changes. All presentation changes can be applied through the existing route stylesheet, enabling a state-preserving live stylesheet swap. No navigation, prompt, data, main-merge or formal-install changes. + +- Implementation is one scoped CSS change: top-align the existing 32px controls in the unchanged 40px slot; use a pale-green filled/bordered Works Square button with 10px above and below. Brand and New Project coordinates stay fixed. Typecheck, changed-file lint and production build passed. The existing Electron navigation scenario passed (1/1), checking exact top/brand coordinates, equal 10px gaps, fill color, 128px no-overflow, configuration exclusion and collapsed return; default and narrow screenshots inspected. No new tests added. Preparing the same CSS-only change against the now-published composer-height trial source. + +- Updated the live trial using a route-stylesheet swap, with no Renderer reload or Main restart. Candidate based on the finalized composer-height source 66cad87, retaining colored shortcuts and the full icon-only navigation. Sole source difference is the sidebar CSS from b4c06e8; composite typecheck/PostCSS production build and independent code review passed. Final source: `sidebar-spacing-33d87a81/source`; active route CSS `index-CjmETxm0.css`, existing running JS `index-CEjIKC5e.js`, next-load JS `index-BU5aR8dW.js`, common CSS `index-CDJ7LmpW.css`. Served index and launch/guidance provenance are updated together, distinguishing running JS from prepared next-load JS. +- Live before/after geometry confirms both controls moved up exactly 4px, brand and New Project boxes identical, Works Square down 10px with 10px gaps above/below and the requested filled background. Controls remain aligned and unclipped. All local storage, messages, input drafts, teacher content, selected tab, DOM focus, native visibility/focus and 34 recorded data-file hashes are unchanged; zero alerts. Main PID 32583/profile retained; inspector opened for this update was closed. Temporary app is ready; no main merge, push or formal-app modification. + + +## Follow-up: Align Sidebar With The Two Workspace Header Rows + +- User now explicitly asks for the return/collapse icons to align with the right-side project/conversation header, the Makelore brand to align with Operation Chat/Work tabs, and a lighter Works Square button matching New Project styling with equal spacing. This supersedes keeping the brand at its former lower position. Continue the authorized temporary-app update. +- Project Context Loaded / Planning Gate: Passed. Official same-task feature resume verified exact Identity fields; active record, entry/memory index and peer scopes read. Shared positioning, decisions, architecture/domain, evidence/reflection and commitments remain unchanged from 9d47cd2. Composer height is now published and ready; a separate merge task touches only main/cleanup and excludes live-app updates. Current published trial source is sidebar-spacing-33d87a81/source with all 13 overlays, so no semantic conflict. Task owns navigation presentation and focused regression/docs only; preserve light theme, native window controls, local projects, conversations and teacher features. +- Plan: move chat return/collapse into the native-titlebar-height navigation rail, using shared safe-width math for the right conversation header. macOS reserves 88px for traffic lights and a minimum 168px for the full pair; Windows minimum 88px. Keep both controls accessible while collapsed and stable during peek; the sidebar itself still supports 128px. Remove the redundant chat-sidebar row on platforms with a custom titlebar, retain Linux fallback, align a 46px brand block to the 46px tabs, and match Works Square to New Project's 41px rounded/bordered button with a very pale fill and equal 10px gaps. Validate platform controls, collapse/peek, narrow widths and actual two-row geometry, then build the latest composite and update the existing trial. + +- Implemented the two-control titlebar rail and shared safe-width helper; removed the redundant sidebar row on macOS/Windows while keeping Linux fallback. The brand is 46px tall, aligned to the tabs. Works Square is a 41px very pale green button matching New Project border/radius/padding/font/shadow, with equal 10px gaps. Keyboard focus moves between the titlebar buttons without closing the sidebar preview. +- Verification: typecheck, scoped ESLint and production build passed. Titlebar/platform/header/peek unit coverage: 33 tests in 3 files passed. Existing Electron navigation E2E passed (1/1), verifying both horizontal alignments, exact matching button styles, 128px sidebar with reserved topbar space, actual return hit testing, configuration/nested absence, stable collapsed hover geometry and real return navigation. Default and narrow screenshots inspected. Composite build/typecheck/lint passed; full source comparison confirms only the five intended product files changed and all teacher/composer overlays retained. Temporary publication pending below. + +- Temporary app updated in place from the current composite, preserving all 13 prior overlays and adding 883ae1d. Final source: `sidebar-rails-33d87a81/source`; live/next JS `index-pmc1zOQa.js`, common CSS `index-D1Sa4hBY.css`, route CSS `index-CEKmXNFU.css`. Launch/guidance/build metadata finalized consistently. Main PID 32583/profile/native visibility and focus are unchanged; no formal installation or main/remote update. +- Live checks confirm return and collapse centers align with the project/conversation header, the 46px brand aligns with the 46px tab row, native traffic lights and breadcrumb hit areas remain clear, and Works Square/New Project have matching height, border, radius, padding, typography and shadow, with the requested pale fill and equal 10px gaps. An unfocused-window color transition was paused at time zero; completed that button's six pending visual transitions before comparing final computed styles. No navigation or data action was triggered by this check. +- Account, current conversation/messages, all 14 consultation drafts, closed teacher panel/content and Operation Chat selection match the pre-refresh snapshot; all 34 recorded data hashes unchanged, zero alerts. Released refresh lease, restored background throttling and closed the task-opened inspector. Temporary app ready; working branch retains the source for later integration. + + +## Follow-up: Left-Align Return In Native Fullscreen + +- User asks to align the return button's left edge with New Project. Live read-only inspection confirms the current macOS trial is in native fullscreen, while DOM fullscreen and display-mode media queries are both false; the existing 88px native-traffic-light reservation is unnecessary in that state. Keep current vertical alignment and collapse placement; fullscreen and Windows return inset is 12px, while macOS windowed mode retains the native safe inset. +- Project Context Loaded / Planning Gate: Passed. Exact same-task feature identity and official resume verified. Active task, entry and all peer scopes reread; previously loaded memory/architecture/domain/context unchanged from 80b0cb9, no new peer scope conflict. Current trial source sidebar-rails-33d87a81/source is consistent with served and has all 14 overlays. Preserve native window behavior and Main ownership: add only a read-only fullscreen query/event bridge, subscribe before querying with race protection, then adapt the existing titlebar. No screen-size heuristic or native-button hiding. +- Plan: small reusable Main fullscreen IPC binding plus Preload allowlists, renderer subscription adapter and conditional 12px titlebar padding, update focused fullscreen/reopened-window/race tests and existing E2E with simulated native events in the isolated test process. For the existing trial, install only this read-only Main binding and allowlist addition alongside the new Renderer, preserve the current Main/profile/window state, and record the startup overlay for next launch. No main merge/push or formal-app change. + +- Implemented native fullscreen query/event binding with window-reopen support, scoped preload allowlists, a race-safe Renderer observer and 12px fullscreen/Windows return inset. macOS windowed traffic-light space, rail widths, collapse coordinates and brand position are unchanged. The observer also keeps the safe inset when an older preload rejects the event. 27 focused unit tests, typecheck, scoped lint and production build passed; the existing Electron navigation E2E passed including enter/leave native events, exact return/New Project left alignment and unchanged brand/collapse boxes. Fullscreen screenshot inspected. Final candidate includes the fallback fix and passes typecheck/lint/build. +- Temporary composite preserves all previous overlays and differs in exactly five product files. Its older Main adapter retains single-window semantics; the live update will install only the standalone read-only fullscreen module and two preload allowlist additions, not replace the Main bundle. Publication pending. + +- Updated the same temporary app in place. Source `sidebar-left-33d87a81/source` includes product commit 343f88c over the prior 14 overlays; live/next JS `index-BgI05aQO.js`, common CSS `index-CDJ7LmpW.css`, route CSS `index-CEKmXNFU.css`. Added the standalone fullscreen module to the running Main and startup wrapper, and patched only the two compiled Preload allowlist entries. Full Main bundle was preserved and Main was not restarted. Launch/guidance/build provenance records the native bridge and old composite adapter explicitly. +- Live verification: native fullscreen is true; return and New Project both start at x=12px. Return/collapse remain aligned with the conversation header, brand remains aligned with tabs; button styles and equal 10px gaps remain correct. Same route, account, conversation, 14 messages, 14 drafts, teacher panel and selected tab; all 34 recorded data-file hashes unchanged, zero alerts. Main PID 32583, native fullscreen, visibility and focus unchanged. Released refresh lease, restored throttling and closed the task-opened inspector. No formal app, main branch or remote changes. diff --git a/README.md b/README.md index 6398491e..1ca2f79d 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`。尚未选择本地项目时,页面以麦洛 M 标识和居中引导语建立焦点,下方提供独立的“新增项目”主操作以及可横向浏览、直接进入的已有项目卡片;项目创建和进入仍复用侧栏现有流程。已有项目的新 Conversation 在可编辑 Composer 上方沿用同一品牌引导,第一条消息出现后切换为正常时间线。 +- AI 编程默认入口:进入 Code 后保持在 `/chat`。顶部返回箭头与侧栏收起图标同排,与右侧项目/会话名称对齐;折叠时保留返回和展开入口。macOS 为原生窗口按钮保留安全空间,窄侧栏仍可使用。下方麦洛品牌与“操作对话/作品”标签行对齐;更浅底色的“作品广场”与“新建项目”采用相同高度、圆角、边框与阴影,并位于品牌和新建项目之间,上下各留 10px;“我的项目”和项目列表紧接新建项目;项目配置及其资源子页不再重复显示此侧栏入口,保留配置页自身的返回操作。尚未选择本地项目时,页面以麦洛 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/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 3408fc0a..6fb9f366 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -6,6 +6,7 @@ import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron'; import { registerHostApiProxyHandlers } from './ipc/host-api-proxy'; import { registerTranscriptExportHandler } from './ipc/transcript-export'; import { registerConversationLinkContextMenuHandler } from './ipc/conversation-link-context-menu'; +import { registerWindowFullscreenHandlers } from './ipc/window-fullscreen'; import { applyProxySettings } from './proxy'; import { syncLaunchAtStartupSettingFromStore } from './launch-at-startup'; import { getAllSettings, getSetting, resetSettings, setSetting, type AppSettings } from '../utils/store'; @@ -254,6 +255,7 @@ export function registerIpcHandlers( if (!window || window.isDestroyed()) throw new Error('Main window is unavailable'); return window; }; + const bindWindowFullscreen = registerWindowFullscreenHandlers(requireRequestWindow); registerHostApiProxyHandlers(hostApiContext); registerTranscriptExportHandler(getMainWindow); registerConversationLinkContextMenuHandler(getMainWindow); @@ -272,7 +274,10 @@ export function registerIpcHandlers( return leases; }; const bindWindow = (window: BrowserWindow): void => { - if (!window.isDestroyed() && !window.webContents.isDestroyed()) bindRenderer(window.webContents); + if (!window.isDestroyed() && !window.webContents.isDestroyed()) { + bindRenderer(window.webContents); + bindWindowFullscreen(window); + } }; const initialWindow = getMainWindow(); if (initialWindow) bindWindow(initialWindow); diff --git a/electron/main/ipc/window-fullscreen.ts b/electron/main/ipc/window-fullscreen.ts new file mode 100644 index 00000000..89d1806d --- /dev/null +++ b/electron/main/ipc/window-fullscreen.ts @@ -0,0 +1,20 @@ +import { ipcMain, type BrowserWindow } from 'electron'; + +export function registerWindowFullscreenHandlers( + requireWindow: (event: Electron.IpcMainInvokeEvent) => BrowserWindow, +): (window: BrowserWindow) => void { + ipcMain.handle('window:isFullScreen', (event) => requireWindow(event).isFullScreen()); + + const boundWindows = new WeakSet(); + return (window: BrowserWindow): void => { + if (window.isDestroyed() || window.webContents.isDestroyed() || boundWindows.has(window)) return; + boundWindows.add(window); + + const notify = (fullscreen: boolean): void => { + if (window.isDestroyed() || window.webContents.isDestroyed()) return; + window.webContents.send('window:fullscreen-changed', fullscreen); + }; + window.on('enter-full-screen', () => notify(true)); + window.on('leave-full-screen', () => notify(false)); + }; +} diff --git a/electron/preload/index.ts b/electron/preload/index.ts index a500010c..5ce8a706 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -33,6 +33,7 @@ const validInvokeChannels = [ 'window:maximize', 'window:close', 'window:isMaximized', + 'window:isFullScreen', 'settings:get', 'settings:set', 'settings:setMany', @@ -64,6 +65,7 @@ const validInvokeChannels = [ ]; const validEventChannels = [ + 'window:fullscreen-changed', 'navigate', 'cloud-agent-link', 'update:status-changed', diff --git a/src/components/layout/ModuleSwitcher.tsx b/src/components/layout/ModuleSwitcher.tsx index 2c7cb199..29cd6e6c 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, iconOnly = false }: { sidebarCollapsed: boolean; compact?: boolean; toolbar?: boolean; iconOnly?: 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 730bb70d..874529f0 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -133,6 +133,7 @@ export function Sidebar({ const isProgrammingModule = activeModule === 'programming'; const isPaintingModule = activeModule === 'painting'; const isRobotModule = activeModule === 'robot'; + const isProjectConfigPage = location.pathname === '/project-config' || location.pathname.startsWith('/project-config/'); const visibleProjects = projects; const selectedProjectFolderName = getFolderName(newProjectSelectedPath); const accountName = userProfile?.displayName?.trim() || getAuthUserDisplayName(authUser) || '未登录用户'; @@ -403,20 +404,26 @@ export function Sidebar({ )} > {workspaceLayout && !sidebarPinnedCollapsed && } -
- {workspaceLayout ? : } + } + {workspaceLayout && } + {!workspaceLayout && }
-
+
}
{isPaintingModule ? ( @@ -436,7 +443,7 @@ export function Sidebar({ {!sidebarCollapsed && '新建项目'} - + {!workspaceLayout && }
@@ -110,10 +117,23 @@ function ProductTitleBar({ data-testid="titlebar-sidebar-toggle-layer" className={cn( 'no-drag pointer-events-auto fixed left-0 top-0 z-[400] h-10', - nativeTrafficLights ? 'w-[132px]' : 'w-[52px]', + !workspaceLayout && (nativeTrafficLights ? 'w-[132px]' : 'w-[52px]'), )} + style={workspaceLayout ? { width: classroomTitlebarWidth } : undefined} + onPointerEnter={workspaceLayout ? () => handleSidebarPeekEnter('titlebar') : undefined} + onPointerLeave={workspaceLayout ? () => handleSidebarPeekLeave('titlebar') : undefined} + onFocusCapture={workspaceLayout ? () => handleSidebarPeekEnter('focus') : undefined} + onBlurCapture={workspaceLayout ? (event) => { + if (!event.currentTarget.contains(event.relatedTarget as Node | null)) handleSidebarPeekLeave('focus'); + } : undefined} > - {sidebarToggle} + {workspaceLayout ?
+ + {sidebarToggle} +
: sidebarToggle}
); const titlebarLogo = ( @@ -155,16 +175,16 @@ function ProductTitleBar({ <>
void): () => void { + let active = true; + let revision = 0; + let unsubscribe: (() => void) | void; + try { + unsubscribe = window.electron.ipcRenderer.on('window:fullscreen-changed', (fullscreen) => { + if (!active || typeof fullscreen !== 'boolean') return; + revision += 1; + onChange(fullscreen); + }); + } catch { + // Older preloads reject unknown channels synchronously. Keep the safe inset. + return () => {}; + } + const queryRevision = revision; + void invokeIpc('window:isFullScreen').then((fullscreen) => { + // A transition received during the query is more recent than its snapshot. + if (active && revision === queryRevision && typeof fullscreen === 'boolean') onChange(fullscreen); + }).catch(() => { + // Keep the windowed safe inset when an older host lacks the query. + }); + return () => { + active = false; + unsubscribe?.(); + }; +} diff --git a/src/pages/Chat/CodingConversationHeader.tsx b/src/pages/Chat/CodingConversationHeader.tsx index 3e077f17..140c3b21 100644 --- a/src/pages/Chat/CodingConversationHeader.tsx +++ b/src/pages/Chat/CodingConversationHeader.tsx @@ -14,7 +14,7 @@ import { CodingConversationRenameDialog } from './CodingConversationRenameDialog import { cn } from '@/lib/utils'; import { useSettingsStore } from '@/stores/settings'; import { normalizeClassroomSidebarWidth } from '@/lib/classroom-layout'; -import { WINDOWS_TITLEBAR_CONTROLS_WIDTH } from '@/components/layout/titlebar-metrics'; +import { getClassroomTitlebarWidth, WINDOWS_TITLEBAR_CONTROLS_WIDTH } from '@/components/layout/titlebar-metrics'; import type { ConversationSnapshot } from '@/types/coding-conversation'; import type { CodingConversationMetadata } from '@/types/coding-project'; @@ -189,7 +189,7 @@ export function CodingConversationHeader({ className="pointer-events-none fixed right-0 top-0 z-[80] h-10" data-testid="coding-conversation-header-titlebar" style={{ - left: `${sidebarCollapsed ? (platform === 'darwin' ? 132 : 52) : Math.max(sidebarWidth, platform === 'darwin' ? 132 : 0)}px`, + left: `${getClassroomTitlebarWidth(platform, sidebarCollapsed, sidebarWidth)}px`, // Keep both actions and their hit area outside the product chrome. right: platform === 'win32' ? WINDOWS_TITLEBAR_CONTROLS_WIDTH : 0, }} diff --git a/src/pages/Chat/classroom-workspace.css b/src/pages/Chat/classroom-workspace.css index 1836eebc..1aed527a 100644 --- a/src/pages/Chat/classroom-workspace.css +++ b/src/pages/Chat/classroom-workspace.css @@ -5,15 +5,19 @@ } [data-testid="main-layout"][data-classroom="true"] [data-testid="titlebar-sidebar-surface"] { background: #f3f7fb; border-color: #dde5ee; box-shadow: none; backdrop-filter: none; } [data-testid="sidebar"][data-classroom="true"] { background: #f3f7fb; border-color: #dde5ee; box-shadow: none; backdrop-filter: none; color: #26384d; } -[data-testid="sidebar"][data-classroom="true"] .classroom-brand { gap: 10px; padding: 10px 2px 12px; } +[data-testid="sidebar"][data-classroom="true"] .classroom-sidebar-header { padding-top: 0; padding-bottom: 0; } +[data-testid="sidebar"][data-classroom="true"] [data-testid="classroom-sidebar-navigation"] { align-items: center; } +[data-testid="sidebar"][data-classroom="true"] .classroom-brand { height: 46px; gap: 10px; padding: 0 2px; } +[data-testid="sidebar"][data-classroom="true"] .classroom-sidebar-header .community-link[data-variant="sidebar"] { min-height: 41px; margin-top: 10px; gap: 9px; padding: 8px 11px; border: 1px solid #d5e1ed; border-radius: 11px; background: #f8faf5; box-shadow: 0 3px 0 #e3eaf3; font-size: 14px; font-weight: 500; line-height: 20px; white-space: normal; } +[data-testid="sidebar"][data-classroom="true"] .classroom-sidebar-header .community-link[data-variant="sidebar"]:hover:not(:disabled) { background: #eef3e7; border-color: #b7c6a4; } [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: 4px 12px 14px; } +[data-testid="sidebar"][data-classroom="true"] .classroom-sidebar-content { padding: 10px 12px 14px; } [data-testid="sidebar"][data-classroom="true"] [data-testid="sidebar-create-project"] { min-height: 41px; gap: 9px; padding: 8px 11px; border: 1px solid #d5e1ed; border-radius: 11px; background: #fff; color: #304968; box-shadow: 0 3px 0 #e3eaf3; } [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: #fff7ef; border-color: #ecc5a5; } -[data-testid="sidebar"][data-classroom="true"] .classroom-projects { margin-top: 20px; } +[data-testid="sidebar"][data-classroom="true"] .classroom-projects { margin-top: 8px; } [data-testid="sidebar"][data-classroom="true"] .classroom-projects-heading { padding: 6px 3px; color: #64758a; } [data-testid="sidebar"][data-classroom="true"] .classroom-projects-heading > svg { display: none; } [data-testid="sidebar"][data-classroom="true"] .classroom-project-card { min-height: 56px; gap: 9px; padding: 8px; border: 1px solid transparent; border-radius: 13px; } diff --git a/tests/e2e/main-navigation.spec.ts b/tests/e2e/main-navigation.spec.ts index 7cef5807..2a9da6e7 100644 --- a/tests/e2e/main-navigation.spec.ts +++ b/tests/e2e/main-navigation.spec.ts @@ -50,7 +50,7 @@ async function seedInactiveCodingProjects(resources: { rootDir: string; userData } test.describe('Makelore module navigation without setup flow', () => { - test('offers a create CTA and horizontal cards for remembered inactive Code projects', async ({ + test('opens remembered Code projects and keeps the module return entry in chat instead of project settings', async ({ electronTestResources, launchElectronApp, }) => { @@ -76,6 +76,125 @@ test.describe('Makelore module navigation without setup flow', () => { await expect(page.getByTestId('coding-chat-panel')).toBeVisible(); await expect(page.getByTestId('coding-existing-projects')).toHaveCount(0); await expect(page.getByTestId('sidebar-course-project-project-storybook')).toHaveAttribute('aria-current', 'page'); + + const sidebar = page.getByTestId('sidebar'); + const moduleReturn = page.getByTestId('sidebar-module-switcher-trigger'); + await expect(moduleReturn).toBeVisible(); + await expect(moduleReturn).toHaveText(''); + 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(); + 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); + const headerBox = await page.getByTestId('coding-conversation-header').boundingBox(); + const tabsBox = await page.getByRole('tablist', { name: '当前工作' }).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(tabsBox!.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); + expect(sidebarBox!.x + sidebarBox!.width - collapseBox!.x - collapseBox!.width).toBeLessThanOrEqual(14); + await expect(page.getByTestId('titlebar-sidebar-toggle')).toHaveCount(1); + const community = sidebar.getByRole('button', { name: '作品广场', exact: true }); + await expect(community).toHaveCount(1); + const communityBox = await community.boundingBox(); + const createBox = await sidebar.getByTestId('sidebar-create-project').boundingBox(); + const headingBox = await sidebar.locator('.classroom-projects-heading').boundingBox(); + expect(communityBox!.y).toBeGreaterThanOrEqual(brandBox!.y + brandBox!.height); + expect(communityBox!.y - brandBox!.y - brandBox!.height).toBe(10); + expect(createBox!.y - communityBox!.y - communityBox!.height).toBe(10); + expect(createBox!.y - brandBox!.y - brandBox!.height).toBe(61); + expect(communityBox!.height).toBe(createBox!.height); + await expect(community).toHaveCSS('background-color', 'rgb(248, 250, 245)'); + const buttonStyles = await page.evaluate(() => { + const styles = (selector: string) => { + const s = getComputedStyle(document.querySelector(selector)!); + return [s.borderRadius, s.borderWidth, s.borderColor, s.boxShadow, s.padding, s.fontSize, s.fontWeight]; + }; + return { community: styles('[data-testid=sidebar] .community-link'), create: styles('[data-testid=sidebar-create-project]') }; + }); + expect(buttonStyles.community).toEqual(buttonStyles.create); + expect(headingBox!.y).toBeGreaterThanOrEqual(createBox!.y + createBox!.height); + expect(headingBox!.y - createBox!.y - createBox!.height).toBeLessThanOrEqual(14); + await page.screenshot({ path: test.info().outputPath('chat-code-return-entry.png') }); + + // Exercise native state events without switching the test app into a macOS Space. + await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].emit('enter-full-screen')); + await expect.poll(async () => (await moduleReturn.boundingBox())!.x).toBe(createBox!.x); + 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); + 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); + + 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 page.getByRole('button', { name: '折叠侧栏' }).boundingBox(); + expect(narrowReturn!.x + narrowReturn!.width).toBeLessThanOrEqual(narrowToggle!.x); + await expect(moduleReturn).toBeVisible(); + await expect(page.getByRole('button', { name: '折叠侧栏' })).toBeVisible(); + const narrowHeader = await page.getByTestId('coding-conversation-header').boundingBox(); + expect(narrowToggle!.x + narrowToggle!.width).toBeLessThanOrEqual(narrowHeader!.x); + expect(await moduleReturn.evaluate(el => { + const b = el.getBoundingClientRect(); + return el.contains(document.elementFromPoint(b.x + b.width / 2, b.y + b.height / 2)); + })).toBe(true); + await expect(community).toBeVisible(); + expect(await community.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true); + 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(); + await expect(sidebar.getByTestId('sidebar-module-switcher')).toHaveCount(0); + const projectBack = page.getByTestId('project-configuration-back-button'); + await expect(projectBack).toBeVisible(); + await page.screenshot({ path: test.info().outputPath('project-config-without-module-return.png') }); + await projectBack.click(); + await expect(page).toHaveURL(/\/chat$/); + await expect(moduleReturn).toBeVisible(); + + await sidebar.getByRole('button', { name: '项目设置', exact: true }).click(); + await page.getByTestId('resource-card-plugins').click(); + await expect(page).toHaveURL(/\/project-config\/plugins\?/); + const plugins = page.getByTestId('project-plugins-sheet'); + await expect(plugins).toBeVisible(); + await expect(sidebar.getByTestId('sidebar-module-switcher')).toHaveCount(0); + await page.screenshot({ path: test.info().outputPath('project-config-plugins-without-module-return.png') }); + await plugins.getByRole('button', { name: '关闭', exact: true }).click(); + await expect(page).toHaveURL(/\/project-config$/); + await expect(projectBack).toBeVisible(); + await sidebar.getByTestId('sidebar-course-project-project-storybook').click(); + await expect(page).toHaveURL(/\/chat$/); + + const sidebarToggle = page.getByTestId('titlebar-sidebar-toggle'); + await sidebarToggle.click(); + await page.getByTestId('coding-chat-panel').click({ position: { x: 500, y: 200 } }); + await expect(sidebar).toHaveAttribute('aria-hidden', 'true'); + const collapsedToggleBox = await sidebarToggle.boundingBox(); + await expect(moduleReturn).toBeVisible(); + await sidebarToggle.hover(); + await expect(sidebar).toHaveAttribute('aria-hidden', 'false'); + expect(await sidebarToggle.boundingBox()).toEqual(collapsedToggleBox); + await expect(moduleReturn).toBeVisible(); + await page.screenshot({ path: test.info().outputPath('chat-collapsed-hover-code-return.png') }); + await moduleReturn.click(); + await expect(page).toHaveURL(/\/module-select$/); + await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); } finally { await closeElectronApp(app); } diff --git a/tests/unit/coding-feature-ui.test.tsx b/tests/unit/coding-feature-ui.test.tsx index 23d1b72b..f5766686 100644 --- a/tests/unit/coding-feature-ui.test.tsx +++ b/tests/unit/coding-feature-ui.test.tsx @@ -267,7 +267,7 @@ describe('PI-130 feature-complete Coding UI', () => { ); expect(screen.getByTestId('coding-conversation-header-titlebar')).toHaveClass('fixed', 'top-0', 'h-10'); - expect(screen.getByTestId('coding-conversation-header-titlebar')).toHaveStyle({ left: '132px' }); + expect(screen.getByTestId('coding-conversation-header-titlebar')).toHaveStyle({ left: '168px' }); expect(screen.queryByTestId('coding-conversation-sidebar')).not.toBeInTheDocument(); window.electron.platform = originalPlatform; diff --git a/tests/unit/main-window-reopen-ipc.test.ts b/tests/unit/main-window-reopen-ipc.test.ts index 532456c6..f57306ed 100644 --- a/tests/unit/main-window-reopen-ipc.test.ts +++ b/tests/unit/main-window-reopen-ipc.test.ts @@ -9,7 +9,7 @@ function createWindow(id: number) { isDestroyed: vi.fn(() => false), send: vi.fn(), }); - return { + return Object.assign(new EventEmitter(), { id, webContents: contents, isDestroyed: vi.fn(() => false), @@ -17,8 +17,9 @@ function createWindow(id: number) { maximize: vi.fn(), unmaximize: vi.fn(), isMaximized: vi.fn(() => false), + isFullScreen: vi.fn(() => false), close: vi.fn(), - }; + }); } async function setup() { @@ -104,6 +105,74 @@ describe('IPC after closing and reopening the main window', () => { expect(ctx.second.webContents.listenerCount('destroyed')).toBe(1); }); + it('queries fullscreen for the requesting window before and after reopening', async () => { + const ctx = await setup(); + ctx.first.isFullScreen.mockReturnValue(true); + expect(ctx.invoke('window:isFullScreen', ctx.first)).toBe(true); + expect(ctx.invoke('window:isFullScreen', ctx.second)).toBe(false); + + ctx.reopen(); + expect(ctx.invoke('window:isFullScreen', ctx.second)).toBe(false); + ctx.second.isFullScreen.mockReturnValue(true); + expect(ctx.invoke('window:isFullScreen', ctx.second)).toBe(true); + expect(() => ctx.invoke('window:isFullScreen', ctx.first)).toThrow('Main window is unavailable'); + }); + + it('notifies each bound renderer once when its window enters or leaves fullscreen', async () => { + const ctx = await setup(); + ctx.bindings.bindWindow(ctx.first as unknown as BrowserWindow); + expect(ctx.first.listenerCount('enter-full-screen')).toBe(1); + expect(ctx.first.listenerCount('leave-full-screen')).toBe(1); + ctx.first.emit('enter-full-screen'); + ctx.first.emit('leave-full-screen'); + expect(ctx.first.webContents.send.mock.calls).toEqual([ + ['window:fullscreen-changed', true], + ['window:fullscreen-changed', false], + ]); + expect(ctx.second.webContents.send).not.toHaveBeenCalled(); + + const registeredCount = ctx.handle.mock.calls.length; + ctx.reopen(); + ctx.bindings.bindWindow(ctx.second as unknown as BrowserWindow); + expect(ctx.second.listenerCount('enter-full-screen')).toBe(1); + expect(ctx.second.listenerCount('leave-full-screen')).toBe(1); + ctx.second.emit('enter-full-screen'); + ctx.second.emit('leave-full-screen'); + expect(ctx.second.webContents.send.mock.calls).toEqual([ + ['window:fullscreen-changed', true], + ['window:fullscreen-changed', false], + ]); + expect(ctx.first.webContents.send).toHaveBeenCalledTimes(2); + expect(ctx.handle).toHaveBeenCalledTimes(registeredCount); + }); + + it('does not send fullscreen events after a window or renderer is destroyed', async () => { + const ctx = await setup(); + ctx.reopen(); + ctx.first.emit('enter-full-screen'); + ctx.first.emit('leave-full-screen'); + expect(ctx.first.webContents.send).not.toHaveBeenCalled(); + + ctx.second.webContents.isDestroyed.mockReturnValue(true); + ctx.second.emit('enter-full-screen'); + ctx.second.emit('leave-full-screen'); + expect(ctx.second.webContents.send).not.toHaveBeenCalled(); + }); + + it('does not bind fullscreen events to an already destroyed window or renderer', async () => { + const ctx = await setup(); + const closed = createWindow(3); + closed.isDestroyed.mockReturnValue(true); + ctx.bindings.bindWindow(closed as unknown as BrowserWindow); + expect(closed.listenerCount('enter-full-screen')).toBe(0); + expect(closed.listenerCount('leave-full-screen')).toBe(0); + + ctx.second.webContents.isDestroyed.mockReturnValue(true); + ctx.bindings.bindWindow(ctx.second as unknown as BrowserWindow); + expect(ctx.second.listenerCount('enter-full-screen')).toBe(0); + expect(ctx.second.listenerCount('leave-full-screen')).toBe(0); + }); + it('parents dialogs and link menus to the reopened renderer', async () => { const ctx = await setup(); ctx.reopen(); diff --git a/tests/unit/title-bar.test.tsx b/tests/unit/title-bar.test.tsx index 11e67efd..beca3ead 100644 --- a/tests/unit/title-bar.test.tsx +++ b/tests/unit/title-bar.test.tsx @@ -1,8 +1,19 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import type { ReactElement } from 'react'; +import { act, fireEvent, render as renderComponent, screen, waitFor } from '@testing-library/react'; +import { MemoryRouter, useLocation } from 'react-router-dom'; import { TitleBar } from '@/components/layout/TitleBar'; +import { CodingConversationHeader } from '@/pages/Chat/CodingConversationHeader'; import { useSettingsStore } from '@/stores/settings'; +function render(ui: ReactElement) { + return renderComponent(ui, { wrapper: ({ children }) => {children} }); +} + +function LocationPath() { + return {useLocation().pathname}; +} + const invokeIpcMock = vi.hoisted(() => vi.fn()); vi.mock('@/lib/api-client', () => ({ @@ -13,6 +24,7 @@ describe('TitleBar platform behavior', () => { beforeEach(() => { invokeIpcMock.mockReset(); invokeIpcMock.mockResolvedValue(false); + vi.mocked(window.electron.ipcRenderer.on).mockReset(); useSettingsStore.setState({ sidebarCollapsed: false, classroomSidebarWidth: 171 }); }); @@ -73,7 +85,7 @@ describe('TitleBar platform behavior', () => { expect(container.querySelector('.drag-region')).toBeInTheDocument(); expect(screen.queryByTitle('Minimize')).not.toBeInTheDocument(); - expect(invokeIpcMock).not.toHaveBeenCalled(); + expect(invokeIpcMock).toHaveBeenCalledWith('window:isFullScreen'); }); it('aligns the classroom title bar with one project sidebar and no duplicate branding', () => { @@ -92,6 +104,53 @@ describe('TitleBar platform behavior', () => { 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.getByRole('button', { name: '返回首页' })).toBeEnabled(); + expect(screen.getByTestId('classroom-titlebar-navigation')).toHaveClass('pl-[88px]'); + expect(screen.getByTestId('titlebar-sidebar-toggle')).toHaveClass('right-2'); + }); + + it('aligns fullscreen return with project buttons and restores native window-control space on exit', async () => { + window.electron.platform = 'darwin'; + invokeIpcMock.mockResolvedValue(true); + const unsubscribe = vi.fn(); + let notify: (...args: unknown[]) => void = () => {}; + vi.mocked(window.electron.ipcRenderer.on).mockImplementation((channel, callback) => { + expect(channel).toBe('window:fullscreen-changed'); + notify = callback; + return unsubscribe; + }); + const { unmount } = render(); + const navigation = screen.getByTestId('classroom-titlebar-navigation'); + await waitFor(() => expect(navigation).toHaveClass('pl-3')); + act(() => notify(false)); + expect(navigation).toHaveClass('pl-[88px]'); + act(() => notify(true)); + expect(navigation).toHaveClass('pl-3'); + unmount(); + expect(unsubscribe).toHaveBeenCalledOnce(); + }); + + it('keeps a native fullscreen transition when an older initial query resolves later', async () => { + window.electron.platform = 'darwin'; + let resolveQuery: (value: boolean) => void = () => {}; + invokeIpcMock.mockReturnValue(new Promise((resolve) => { resolveQuery = resolve; })); + let notify: (...args: unknown[]) => void = () => {}; + vi.mocked(window.electron.ipcRenderer.on).mockImplementation((_channel, callback) => { + notify = callback; + return vi.fn(); + }); + render(); + act(() => notify(true)); + await act(async () => resolveQuery(false)); + expect(screen.getByTestId('classroom-titlebar-navigation')).toHaveClass('pl-3'); + }); + + it('keeps the native safe inset when an older preload rejects fullscreen events', () => { + window.electron.platform = 'darwin'; + vi.mocked(window.electron.ipcRenderer.on).mockImplementation(() => { throw new Error('Invalid IPC channel'); }); + render(); + expect(screen.getByTestId('classroom-titlebar-navigation')).toHaveClass('pl-[88px]'); + expect(invokeIpcMock).not.toHaveBeenCalled(); }); it('renders the painting title bar as a transparent overlay with the logo at the window edge', () => { @@ -179,14 +238,14 @@ describe('TitleBar platform behavior', () => { useSettingsStore.setState({ sidebarCollapsed: true }); const { rerender } = render(); - expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveClass('w-[132px]'); + expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveStyle({ width: '168px' }); expect(screen.queryByTestId('titlebar-conversation-surface')).not.toBeInTheDocument(); expect(screen.queryByTestId('titlebar-logo')).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: '展开侧栏' })).toHaveAttribute('aria-expanded', 'false'); rerender(); - expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveClass('w-[132px]'); + expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveStyle({ width: '168px' }); expect(screen.queryByTestId('titlebar-conversation-surface')).not.toBeInTheDocument(); expect(screen.queryByTestId('titlebar-logo')).not.toBeInTheDocument(); const pinButton = screen.getByRole('button', { name: '固定侧栏' }); @@ -198,6 +257,66 @@ describe('TitleBar platform behavior', () => { expect(screen.getByRole('button', { name: '折叠侧栏' })).toBeEnabled(); }); + it.each([ + ['darwin', 168, 168, 'pl-[88px]'], + ['win32', 128, 88, 'pl-3'], + ] as const)('keeps both %s navigation controls outside the conversation header at narrow and collapsed widths', (platform, narrowWidth, collapsedWidth, insetClass) => { + window.electron.platform = platform; + useSettingsStore.setState({ classroomSidebarWidth: 128 }); + render(<> + + + + ); + + const assertWidth = (width: number) => { + expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveStyle({ width: `${width}px` }); + expect(screen.getByTestId('titlebar-sidebar-toggle-layer')).toHaveStyle({ width: `${width}px` }); + expect(screen.getByTestId('coding-conversation-header-titlebar')).toHaveStyle({ left: `${width}px` }); + }; + assertWidth(narrowWidth); + const rail = screen.getByTestId('classroom-titlebar-navigation'); + expect(rail).toHaveClass(insetClass); + expect(rail).toContainElement(screen.getByRole('button', { name: '返回首页' })); + expect(rail).toContainElement(screen.getByRole('button', { name: '折叠侧栏' })); + expect(screen.getByRole('button', { name: '返回首页' })).toHaveTextContent(''); + expect(screen.getAllByTestId('titlebar-sidebar-toggle')).toHaveLength(1); + + act(() => useSettingsStore.setState({ classroomSidebarWidth: 256 })); + assertWidth(256); + fireEvent.click(screen.getByRole('button', { name: '折叠侧栏' })); + assertWidth(collapsedWidth); + fireEvent.click(screen.getByRole('button', { name: '返回首页' })); + expect(screen.getByTestId('location-path')).toHaveTextContent('/module-select'); + }); + + it('preserves keyboard preview when focus moves between the classroom controls', () => { + window.electron.platform = 'darwin'; + useSettingsStore.setState({ sidebarCollapsed: true }); + const onSidebarPeekChange = vi.fn(); + render(<> + + + ); + const toggle = screen.getByRole('button', { name: '固定侧栏' }); + const back = screen.getByRole('button', { name: '返回首页' }); + act(() => toggle.focus()); + expect(onSidebarPeekChange).toHaveBeenLastCalledWith(true, 'focus'); + onSidebarPeekChange.mockClear(); + act(() => back.focus()); + expect(onSidebarPeekChange).not.toHaveBeenCalledWith(false, 'focus'); + expect(onSidebarPeekChange).toHaveBeenLastCalledWith(true, 'focus'); + act(() => screen.getByRole('button', { name: 'Outside navigation' }).focus()); + expect(onSidebarPeekChange).toHaveBeenLastCalledWith(false, 'focus'); + + const layer = screen.getByTestId('titlebar-sidebar-toggle-layer'); + fireEvent.pointerEnter(layer); + fireEvent.pointerLeave(layer); + expect(onSidebarPeekChange).toHaveBeenCalledWith(true, 'titlebar'); + expect(onSidebarPeekChange).toHaveBeenLastCalledWith(false, 'titlebar'); + expect(layer).toHaveStyle({ width: '168px' }); + }); + it('extends the ordinary titlebar sidebar surface during a hover preview', () => { window.electron.platform = 'darwin'; useSettingsStore.setState({ sidebarCollapsed: true });