diff --git a/.project-docs/30-worklog/tasks/20260906-coding-default-page-a7c42e.md b/.project-docs/30-worklog/tasks/20260906-coding-default-page-a7c42e.md index f820e15..de163d9 100644 --- a/.project-docs/30-worklog/tasks/20260906-coding-default-page-a7c42e.md +++ b/.project-docs/30-worklog/tasks/20260906-coding-default-page-a7c42e.md @@ -15,10 +15,13 @@ - Replace the no-project Code entry redirect with a dedicated conversation-first landing state on `/chat`. - Restyle both the no-project landing and the empty Conversation timeline around - the existing Makelore M brand mark, a large centered prompt, and the existing - bottom-composer hierarchy shown in the supplied Codex reference. + the existing Makelore M brand mark, a large centered prompt, and the spacious + vertical hierarchy shown in the supplied Codex reference. - Keep project creation reachable from the landing surface and update focused Renderer/Electron coverage plus README current-state wording. +- Apply the user's follow-up correction: the lower landing surface is a standalone + `新增项目` CTA followed by horizontally arranged existing-project cards, not a + composer-shaped project starter. ## Intent And Constraints @@ -27,7 +30,8 @@ - Preserve the single Makelore light visual system, bundled typography, Chinese UI, project-local `.makelore` authority, and all Main-owned runtime/API seams. - The no-project call to action must open the existing Sidebar project-creation - flow rather than introducing a second creation implementation. + flow rather than introducing a second creation implementation; existing-project + cards must likewise reuse the Sidebar's validated project-entry behavior. - Existing projects, Agent selection, Conversation creation, Composer behavior, model/runtime controls, and accepted/uncertain mutation semantics remain unchanged. @@ -40,12 +44,16 @@ - Code now resolves an empty local project list to `/chat` instead of redirecting to Project Configuration. -- The no-project view uses the existing Makelore M vector asset, a large centered - guide sentence, and a low, wide project starter modeled on the supplied Codex - composition while retaining the Makelore sidebar and light visual system. -- Every project-starter action reuses the Sidebar's existing project-creation - dialog through a renderer-local event bridge; the secondary action opens the - existing project/settings route. +- The no-project view uses the existing Makelore M vector asset and a large + centered guide sentence while retaining the Makelore sidebar and light visual + system. Its lower surface now uses the user's clarified standalone orange + `新增项目` CTA followed by an overflow-safe horizontal row of existing projects. +- Project creation reuses the Sidebar's existing dialog through a renderer-local + event bridge; project-card selection uses the same bridge to invoke the + Sidebar's validated entry flow instead of duplicating project activation. +- Each project card is backed by the real local project catalog and exposes the + project name and last-opened date; an empty catalog shows only the CTA and does + not fabricate sample projects. - Empty Conversations reuse the same branded hero above the real editable Composer and automatically yield to the normal timeline after the first entry. - README current-state wording, focused unit coverage, and the Code-entry Electron @@ -58,7 +66,10 @@ - `corepack pnpm run typecheck` — passed. - `corepack pnpm exec vitest run tests/unit/coding-chat-panel.test.tsx tests/unit/coding-conversation-timeline.test.tsx tests/unit/module-navigation.test.tsx tests/unit/app-module-provider-gate.test.tsx --maxWorkers=4` — 4 files, 58 tests passed. -- `corepack pnpm test` — 222 files passed; 1,861 tests passed and 3 skipped. +- Follow-up focused run: + `corepack pnpm exec vitest run tests/unit/coding-chat-panel.test.tsx tests/unit/coding-project-entry.test.ts tests/unit/module-navigation.test.tsx tests/unit/app-module-provider-gate.test.tsx --maxWorkers=4` + — 4 files, 48 tests passed. +- `corepack pnpm test` — 223 files passed; 1,863 tests passed and 3 skipped. - `corepack pnpm run lint:check` — passed with 0 errors and the repository's 5 pre-existing warnings in `src/pages/Home/index.tsx` and `src/pages/Makelore/index.tsx`. @@ -66,12 +77,16 @@ worker production builds passed; only existing Browserslist, mixed-import, and chunk-size warnings were emitted. - `corepack pnpm exec playwright test tests/e2e/main-navigation.spec.ts --grep - "returns to the module chooser"` — passed against the built Electron app, - including `/chat`, branded hero, starter visibility, and project-dialog launch. + "offers a create CTA|returns to the module chooser"` — 2 tests passed against + the built Electron app, covering empty and remembered-project entry states, + horizontal card order, direct card entry, `/chat`, and project-dialog launch. - `git diff --check` — passed. - Visual comparison: - `/Users/inmanx/.codex/visualizations/2026/09/06/01a0756a-b6f5-7892-a11c-1c1608ebe592/codex-reference-vs-makelore.png` - — no actionable P0/P1/P2 differences. + `/Users/inmanx/.codex/visualizations/2026/09/06/01a0756a-b6f5-7892-a11c-1c1608ebe592/codex-reference-vs-makelore-v2.png` + plus the focused lower crop + `/Users/inmanx/.codex/visualizations/2026/09/06/01a0756a-b6f5-7892-a11c-1c1608ebe592/codex-reference-vs-makelore-v2-bottom.png` + — the user-identified composer/CTA mismatch is resolved and no actionable + P0/P1/P2 differences remain. ## Follow-ups @@ -79,17 +94,16 @@ Code-entry tests with the concurrently completed but uncommitted Plugin-rehoming and project-gate-removal tasks; their branch tips contain no commits to merge or cherry-pick from this feature worktree. -- Recheck the starter's `项目与插件设置` label after those peer changes establish - their final settings terminology. ## Promotion Candidates - Target: `.project-docs/30-worklog/current-state.md` and `.project-docs/40-domain/business-rules.md`. Proposal: record that entering Code with no active local project remains on - `/chat`, presents the branded project starter, and routes all creation actions - through the single existing Sidebar flow; empty Conversations share the same - brand hero until their first entry. + `/chat`, presents the branded hero, a standalone `新增项目` CTA, and real local + projects as a horizontal card row. Creation and project-card entry route through + the single existing Sidebar flows; empty Conversations share the same brand + hero until their first entry. Evidence: focused unit coverage, the passing Electron navigation regression, production build, and the passed side-by-side visual QA. Future impact: future routing or setup work must not restore the configuration diff --git a/README.md b/README.md index 2487f76..48f2aaa 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Makelore 是一个面向软件、视觉创作与智能机器人的 AI 桌面工 - 桌面技术栈:Electron、React 19、Vite、TypeScript、Zustand、Tailwind CSS。 - 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`。尚未选择本地项目时,页面以麦洛 M 标识和居中引导语建立焦点,下方提供独立的“新增项目”主操作以及可横向浏览、直接进入的已有项目卡片;项目创建和进入仍复用侧栏现有流程。已有项目的新 Conversation 在可编辑 Composer 上方沿用同一品牌引导,第一条消息出现后切换为正常时间线。 - AI 编程历史与配额反馈:Pi 上下文压缩不删除用户可见的 active-branch 历史,压缩摘要保持 Main-private;Renderer 首次渲染最近 120 个节点,向上滚动时按 100 个节点追加更早内容并保持阅读位置。Main 精确识别 `token_point_balance_exhausted` / `词元点数余额不足`,只向 Renderer 投影不可重试的稳定配额错误和安全中文提示,重开会话后仍可见且不暴露上游原始错误或 request id。 - 桌面性能策略:应用窗口使用不透明浅色表面并默认保留硬件合成;仅在显式安全模式或短时间内重复 GPU 进程崩溃时启用软件渲染,并把故障原因保存在用户数据目录。启动关键路径只创建本地应用壳,认证、代理、同步、更新和遥测在首帧后延迟初始化;开发版可通过 `app:performance` 快照观察 GPU、进程、WebContents、事件循环与 Renderer Long Task 聚合指标。 - 后台生命周期:Main 统一维护模块活动状态与任务租约。隐藏窗口、离开模块和关闭开发浏览器会释放非必要连接;生成、Code 执行、下载与发布构建持有租约并在完成后释放。已写入 Pi 的 prompt 或上下文整理即使确认超过 10 秒,也会继续持有运行所有权与后台租约,直到迟到响应、权威事件、明确失败或用户清理使其收敛;此时同一 Conversation 不接受重叠 mutation,其他 Conversation 不受影响。各模块的后台连接、轮询和子进程必须通过同一生命周期入口登记。 diff --git a/design-qa.md b/design-qa.md index c437c73..e50fe73 100644 --- a/design-qa.md +++ b/design-qa.md @@ -1,47 +1,52 @@ **Comparison Target** -- Source visual truth: `/var/folders/m7/x9ry3xgx4x50fxb20_0108tr0000gn/T/codex-clipboard-58e2cc35-7a17-4951-8bbc-53fee12d5246.png` -- Implementation screenshot: `/Users/inmanx/.codex/visualizations/2026/09/06/01a0756a-b6f5-7892-a11c-1c1608ebe592/makelore-coding-default.png` -- Combined comparison: `/Users/inmanx/.codex/visualizations/2026/09/06/01a0756a-b6f5-7892-a11c-1c1608ebe592/codex-reference-vs-makelore.png` -- State: reference start screen and Makelore Code with no active local project. -- Viewport: implementation CSS viewport 1280 x 800 at device scale factor 1. The source is 908 x 835 pixels with unknown capture density. For full-view comparison, the source was aspect-fit onto a 1280 x 800 white canvas and the implementation was kept at its native 1280 x 800 size; both canvases were then placed side by side. +- Source visual truth: `/var/folders/m7/x9ry3xgx4x50fxb20_0108tr0000gn/T/codex-clipboard-58e2cc35-7a17-4951-8bbc-53fee12d5246.png`, superseded at the lower action area by the user's explicit follow-up: use a standalone `新增项目` CTA and horizontally arranged existing-project cards, not a conversation box. +- Implementation screenshot: `/Users/inmanx/.codex/visualizations/2026/09/06/01a0756a-b6f5-7892-a11c-1c1608ebe592/makelore-coding-default-v2.png` +- Full-view comparison: `/Users/inmanx/.codex/visualizations/2026/09/06/01a0756a-b6f5-7892-a11c-1c1608ebe592/codex-reference-vs-makelore-v2.png` +- Focused lower-region comparison: `/Users/inmanx/.codex/visualizations/2026/09/06/01a0756a-b6f5-7892-a11c-1c1608ebe592/codex-reference-vs-makelore-v2-bottom.png` +- State: Codex reference start screen and Makelore Code with two remembered local projects but no active project. +- Viewport: implementation CSS viewport 1280 x 800 at device scale factor 1. The source is 908 x 835 pixels with unknown capture density. For full-view comparison, the source was aspect-fit onto a 1280 x 800 white canvas and the implementation remained at its native 1280 x 800 size. The focused comparison uses a 400-pixel-tall lower crop from each normalized content region. **Findings** -- No actionable P0, P1, or P2 differences remain. -- Fonts and typography: both screens use a restrained system sans-serif hierarchy. The Makelore prompt has the same single-line, medium-weight emphasis and remains comfortably readable without wrapping at the verified viewport. -- Spacing and layout rhythm: the M mark and guide sentence form one centered hero group, while the project starter stays anchored near the bottom with proportions close to the reference composer. The persistent Makelore sidebar is an intentional product-shell constraint rather than design drift. -- Colors and visual tokens: the implementation keeps Makelore's existing light background, muted neutral surfaces, dark foreground, and subtle elevation. It does not introduce a conflicting theme or decorative palette. -- Image quality and asset fidelity: the hero uses the repository's existing vector Makelore M asset, with no placeholder, CSS drawing, or reconstructed logo. -- Copy and content: the guide sentence is adapted to Makelore, and the bottom surface explains the required no-project action while preserving the reference's prompt-first reading order. +- No actionable P0, P1, or P2 differences remain after applying the user's correction. +- Fonts and typography: the M hero keeps the reference's restrained system-sans hierarchy and single-line display copy. The CTA, section label, project names, and recency metadata form a clear descending hierarchy without crowding or truncation at the verified viewport. +- Spacing and layout rhythm: the hero retains generous white space. The standalone CTA is visually separated from the existing-project row, and fixed-width cards create a true horizontal browsing pattern without increasing page height as the catalog grows. +- Colors and visual tokens: the CTA uses the existing Makelore brand orange and the cards use existing neutral surface tokens, preserving the single light product system and one-accent discipline. +- Image quality and asset fidelity: the hero uses the repository's existing vector Makelore M asset. Project cards use the product's installed icon library; no placeholder image, CSS drawing, or reconstructed logo is present. +- Copy and content: `新增项目`, `已有项目`, and `选择一个项目继续` state the two available paths directly. Project cards use real project names and last-opened timestamps from the local catalog. +- Interaction and accessibility: the CTA opens the existing creation dialog; each card has a project-specific accessible name, keyboard focus treatment, pressed feedback, horizontal overflow, and routes through the Sidebar's existing validated project-entry behavior. **Open Questions** -- None. The source does not include Makelore's persistent sidebar or its required project-selection state; those differences are intentional and necessary. +- None. The source screenshot's composer remains useful only for overall composition; the user's later instruction is authoritative for the lower interaction model. **Full-view Comparison Evidence** -- The combined comparison shows matching visual priority: small centered mark, large central guide copy, substantial white space, and a low, wide action surface. -- The implementation preserves the reference's quiet density while making the unavailable-chat state actionable through the existing project creation flow. +- The combined view preserves the reference's primary visual order: small centered mark, large guide sentence, ample whitespace, then the entry action area. +- The Makelore-specific CTA and project cards occupy the same quiet lower zone without resembling an editable conversation surface. **Focused Region Comparison Evidence** -- A separate crop was not needed: at native 1x capture, the M mark, guide sentence, starter metadata, CTA copy, icons, radii, and shadows are all legible in the full-view evidence. +- The focused comparison confirms that the earlier composer-shaped block is gone. The orange CTA is isolated and unmistakably actionable, while the two project cards align on one horizontal baseline with consistent icon, title, date, radius, and elevation treatment. **Comparison History** -- Pass 1: no actionable P0/P1/P2 findings. No visual fixes were required after the combined comparison. +- Pass 1: the user identified a P1 interaction-model mismatch: the lower surface looked like a default conversation box and omitted horizontally arranged existing projects. +- Fix: replaced the composer-shaped starter with a standalone `新增项目` CTA; added a horizontal, overflow-safe row backed by the real project catalog; connected card clicks to the existing Sidebar entry path; added unit and Electron coverage. +- Pass 2: the revised full-view and focused comparisons show the corrected CTA/card hierarchy with no remaining actionable P0/P1/P2 findings. **Implementation Checklist** -- [x] Replace the old project-configuration default with the Code conversation entry. -- [x] Use the real Makelore M asset in the centered hero. -- [x] Add a large guide sentence and bottom project starter. -- [x] Connect the starter controls to the existing project dialog and settings route. -- [x] Verify the Electron entry interaction and capture the no-project state. +- [x] Keep the Makelore M mark and large centered guide sentence. +- [x] Remove the composer-shaped no-project starter. +- [x] Add one standalone `新增项目` primary CTA. +- [x] Render real existing projects as horizontal cards. +- [x] Reuse the existing creation and validated project-entry flows. +- [x] Verify both the empty catalog and remembered-project states in Electron. **Follow-up Polish** -- P3: after the concurrent project-dialog simplification lands, recheck the starter's secondary label so it reflects the final project/settings terminology exactly. +- None required for this correction. final result: passed diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index ab02f53..78ca8fe 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -31,7 +31,10 @@ import { import { getAiModuleForPath } from '@/lib/ai-modules'; import { invokeIpc } from '@/lib/api-client'; import { getAuthUserDisplayName } from '@/lib/auth-user-display'; -import { subscribeCodingProjectCreationRequest } from '@/lib/coding-project-entry'; +import { + subscribeCodingProjectCreationRequest, + subscribeCodingProjectOpenRequest, +} from '@/lib/coding-project-entry'; import { WORKS_SQUARE_TOKEN_USAGE_STALE_EVENT } from '@/lib/works-square-usage-events'; import { fetchWorksTokenUsage, type WorksTokenUsage } from '@/lib/works-square'; import { isWorksTokenUsageExhausted } from '@/lib/works-square-token-usage'; @@ -431,11 +434,11 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi setCreateProjectError(null); }; - const showProjectEntryError = (project: CodingProjectSummary, message: string) => { + const showProjectEntryError = useCallback((project: CodingProjectSummary, message: string) => { setProjectEntryError({ project, message }); - }; + }, []); - const enterProject = async (project: CodingProjectSummary) => { + const enterProject = useCallback(async (project: CodingProjectSummary) => { const result = await loadProjectConfig(project.id); if (result.status !== 'valid' || !result.config) { showProjectEntryError(project, result.status === 'missing' ? '项目缺少必要的配置文件' : result.error ?? '项目配置无效'); @@ -445,16 +448,16 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi navigate(result.config.initialized ? '/chat' : projectConfigPath); - }; + }, [loadProjectConfig, navigate, setActiveProject, showProjectEntryError]); - const readAndEnterProject = async (project: CodingProjectSummary) => { + const readAndEnterProject = useCallback(async (project: CodingProjectSummary) => { setProjectEntryError(null); try { await enterProject(project); } catch (error) { showProjectEntryError(project, error instanceof Error ? error.message : String(error)); } - }; + }, [enterProject, showProjectEntryError]); const confirmCreateProject = async () => { @@ -495,9 +498,17 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi } }; - const openProject = async (project: CodingProjectSummary) => { + const openProject = useCallback(async (project: CodingProjectSummary) => { await readAndEnterProject(project); - }; + }, [readAndEnterProject]); + + useEffect(() => { + if (!isProgrammingModule) return undefined; + return subscribeCodingProjectOpenRequest((projectId) => { + const project = visibleProjects.find((candidate) => candidate.id === projectId); + if (project) void openProject(project); + }); + }, [isProgrammingModule, openProject, visibleProjects]); const requestRemoveUnavailableProject = () => { if (!projectEntryError) return; diff --git a/src/lib/coding-project-entry.ts b/src/lib/coding-project-entry.ts index b8c5993..4a737ec 100644 --- a/src/lib/coding-project-entry.ts +++ b/src/lib/coding-project-entry.ts @@ -1,4 +1,5 @@ const CODING_PROJECT_CREATE_REQUEST_EVENT = 'makelore:coding-project-create-request'; +const CODING_PROJECT_OPEN_REQUEST_EVENT = 'makelore:coding-project-open-request'; export function requestCodingProjectCreation(): void { if (typeof window === 'undefined') return; @@ -10,3 +11,22 @@ export function subscribeCodingProjectCreationRequest(listener: () => void): () window.addEventListener(CODING_PROJECT_CREATE_REQUEST_EVENT, listener); return () => window.removeEventListener(CODING_PROJECT_CREATE_REQUEST_EVENT, listener); } + +export function requestCodingProjectOpen(projectId: string): void { + if (typeof window === 'undefined') return; + window.dispatchEvent(new CustomEvent(CODING_PROJECT_OPEN_REQUEST_EVENT, { + detail: { projectId }, + })); +} + +export function subscribeCodingProjectOpenRequest( + listener: (projectId: string) => void, +): () => void { + if (typeof window === 'undefined') return () => undefined; + const handleRequest = (event: Event) => { + const projectId = (event as CustomEvent<{ projectId?: unknown }>).detail?.projectId; + if (typeof projectId === 'string' && projectId.trim()) listener(projectId); + }; + window.addEventListener(CODING_PROJECT_OPEN_REQUEST_EVENT, handleRequest); + return () => window.removeEventListener(CODING_PROJECT_OPEN_REQUEST_EVENT, handleRequest); +} diff --git a/src/pages/Chat/CodingChatPanel.tsx b/src/pages/Chat/CodingChatPanel.tsx index 4f27fe4..32d1440 100644 --- a/src/pages/Chat/CodingChatPanel.tsx +++ b/src/pages/Chat/CodingChatPanel.tsx @@ -1,11 +1,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { - ArrowUp, + ChevronRight, CircleAlert, FolderKanban, Plus, RefreshCw, - Settings2, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { @@ -36,6 +35,7 @@ import type { } from '@/types/coding-conversation'; import type { CodingConversationMetadata, + CodingProjectSummary, } from '@/types/coding-project'; import { CodingComposer } from './CodingComposer'; import { CodingChangesSummary } from './CodingChangesSummary'; @@ -49,6 +49,7 @@ import { createLocalConversationSnapshot } from './coding-chat-snapshot'; export interface CodingChatPanelProps { navigationDraft?: string; onCreateProject?(): void; + onOpenProject?(projectId: string): void; onOpenProjectSettings?(): void; } @@ -112,11 +113,24 @@ function acceptedPromptCount( )).length; } +const PROJECT_DATE_FORMATTER = new Intl.DateTimeFormat('zh-CN', { + month: 'short', + day: 'numeric', +}); + +function projectLastOpenedLabel(project: CodingProjectSummary): string { + const timestamp = Date.parse(project.lastOpenedAt); + if (!Number.isFinite(timestamp)) return '本地项目'; + return `最近打开 ${PROJECT_DATE_FORMATTER.format(timestamp)}`; +} + export function CodingChatPanel({ navigationDraft, onCreateProject, + onOpenProject, onOpenProjectSettings, }: CodingChatPanelProps) { + const projects = useCodingWorkspaceStore((state) => state.projects); const activeProject = useCodingWorkspaceStore((state) => state.activeProject); const config = useCodingWorkspaceStore((state) => state.config); const conversations = useCodingWorkspaceStore((state) => state.conversations); @@ -604,7 +618,7 @@ export function CodingChatPanel({ > -
+
{workspaceError ? (
) : null} -
- + -
- - -
- - {onOpenProjectSettings ? ( - - ) : null} - + {projects.length > 0 ? ( +
+
+

+ 已有项目 +

+

选择一个项目继续

-
-
+ +
+ {projects.map((project) => ( + + ))} +
+ + ) : null}
); diff --git a/src/pages/Chat/index.tsx b/src/pages/Chat/index.tsx index 2871104..6aa4e10 100644 --- a/src/pages/Chat/index.tsx +++ b/src/pages/Chat/index.tsx @@ -1,6 +1,9 @@ import { CodingChatPanel } from './CodingChatPanel'; import { useNavigate } from 'react-router-dom'; -import { requestCodingProjectCreation } from '@/lib/coding-project-entry'; +import { + requestCodingProjectCreation, + requestCodingProjectOpen, +} from '@/lib/coding-project-entry'; function getNavigationDraft(state: unknown): string | undefined { if (!state || typeof state !== 'object') return undefined; @@ -26,6 +29,7 @@ export function Chat() { navigate('/project-config')} />
diff --git a/tests/e2e/main-navigation.spec.ts b/tests/e2e/main-navigation.spec.ts index 50b78e9..7a0e036 100644 --- a/tests/e2e/main-navigation.spec.ts +++ b/tests/e2e/main-navigation.spec.ts @@ -1,6 +1,84 @@ +import { mkdir, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron'; +async function seedInactiveCodingProjects(resources: { rootDir: string; userDataDir: string }) { + const now = '2026-09-06T08:00:00.000Z'; + const older = '2026-09-04T08:00:00.000Z'; + const projects = [ + { + id: 'project-moonlight', + name: '月光小游戏', + path: join(resources.rootDir, '月光小游戏'), + createdAt: older, + updatedAt: now, + lastOpenedAt: now, + projectId: '11111111-1111-4111-8111-111111111111', + }, + { + id: 'project-storybook', + name: '故事绘本', + path: join(resources.rootDir, '故事绘本'), + createdAt: older, + updatedAt: older, + lastOpenedAt: older, + projectId: '22222222-2222-4222-8222-222222222222', + }, + ]; + + for (const project of projects) { + await mkdir(join(project.path, '.makelore'), { recursive: true }); + await mkdir(join(project.path, 'knowledge'), { recursive: true }); + await writeFile(join(project.path, '.makelore', 'project.json'), JSON.stringify({ + schemaVersion: 2, + projectType: 'custom', + projectId: project.projectId, + initialized: true, + agents: [], + knowledgeDirectory: 'knowledge', + createdAt: project.createdAt, + updatedAt: project.updatedAt, + })); + } + + await writeFile(join(resources.userDataDir, 'makelore-projects.json'), JSON.stringify({ + projects: { + projects: Object.fromEntries(projects.map(({ projectId: _projectId, ...project }) => [project.id, project])), + activeProjectId: null, + }, + })); +} + test.describe('Makelore module navigation without setup flow', () => { + test('offers a create CTA and horizontal cards for remembered inactive Code projects', async ({ + electronTestResources, + launchElectronApp, + }) => { + await seedInactiveCodingProjects(electronTestResources); + const app = await launchElectronApp({ skipSetup: true }); + + try { + const page = await getStableWindow(app); + await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); + await page.getByTestId('ai-module-option-programming').click(); + + await expect(page).toHaveURL(/\/chat$/); + await expect(page.getByRole('button', { name: '新增项目' })).toBeVisible(); + const projectCards = page.getByTestId('coding-existing-projects'); + await expect(projectCards).toBeVisible(); + await expect(projectCards.getByRole('button')).toHaveCount(2); + await expect(projectCards.getByRole('button').nth(0)).toContainText('月光小游戏'); + await expect(projectCards.getByRole('button').nth(1)).toContainText('故事绘本'); + + await projectCards.getByRole('button', { name: '进入项目 故事绘本' }).click(); + 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'); + } finally { + await closeElectronApp(app); + } + }); + test('keeps the three module entries in a proportional vertical stack while resizing the window', async ({ launchElectronApp }) => { const app = await launchElectronApp({ skipSetup: true }); @@ -143,7 +221,9 @@ test.describe('Makelore module navigation without setup flow', () => { await expect(page.getByText('请先新建项目')).toHaveCount(0); await expect(page.getByRole('heading', { name: '你想让麦洛和你一起构建什么?' })).toBeVisible(); await expect(page.getByRole('img', { name: '麦洛 M 标识' })).toBeVisible(); - await expect(page.getByTestId('coding-project-starter')).toBeVisible(); + await expect(page.getByRole('button', { name: '新增项目' })).toBeVisible(); + await expect(page.getByTestId('coding-project-starter')).toHaveCount(0); + await expect(page.getByTestId('coding-existing-projects')).toHaveCount(0); await expect(page.getByText('一念成光,万物可创。')).toHaveCount(0); await expect(page.getByRole('img', { name: 'Makelore logo' })).toBeVisible(); await expect(page.getByTestId('sidebar-module-switcher-trigger')).toContainText('编程 Code'); diff --git a/tests/unit/coding-chat-panel.test.tsx b/tests/unit/coding-chat-panel.test.tsx index 2a07666..d9e24fc 100644 --- a/tests/unit/coding-chat-panel.test.tsx +++ b/tests/unit/coding-chat-panel.test.tsx @@ -60,6 +60,12 @@ const project: CodingProjectSummary = { updatedAt: '2026-08-23T00:00:00.000Z', lastOpenedAt: '2026-08-23T00:00:00.000Z', }; +const secondProject: CodingProjectSummary = { + ...project, + id: 'project-2', + name: 'Second local project', + lastOpenedAt: '2026-08-22T00:00:00.000Z', +}; const agent: CodingProjectAgent = { id: 'agent-1', avatarId: 'default', @@ -199,15 +205,17 @@ describe('CodingChatPanel first Conversation', () => { vi.resetModules(); }); - it('shows a branded project starter before any local project is selected', async () => { - projectApi.list.mockResolvedValue({ projects: [], activeProjectId: null }); + it('shows a branded create CTA and horizontal existing-project cards before a project is selected', async () => { + projectApi.list.mockResolvedValue({ projects: [project, secondProject], activeProjectId: null }); const onCreateProject = vi.fn(); + const onOpenProject = vi.fn(); const onOpenProjectSettings = vi.fn(); const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel'); render( , ); @@ -215,13 +223,18 @@ describe('CodingChatPanel first Conversation', () => { expect(await screen.findByTestId('coding-chat-empty-project')).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '你想让麦洛和你一起构建什么?' })).toBeInTheDocument(); expect(screen.getByRole('img', { name: '麦洛 M 标识' })).toBeInTheDocument(); - expect(screen.getByTestId('coding-project-starter')).toHaveTextContent('尚未选择项目'); + expect(screen.getByRole('button', { name: '新增项目' })).toBeInTheDocument(); + expect(screen.getByTestId('coding-existing-projects')).toHaveTextContent('Local project'); + expect(screen.getByTestId('coding-existing-projects')).toHaveTextContent('Second local project'); + expect(screen.queryByTestId('coding-project-starter')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: '项目与插件设置' })).not.toBeInTheDocument(); fireEvent.click(screen.getByTestId('coding-start-project-button')); expect(onCreateProject).toHaveBeenCalledOnce(); - fireEvent.click(screen.getByRole('button', { name: '项目与插件设置' })); - expect(onOpenProjectSettings).toHaveBeenCalledOnce(); + fireEvent.click(screen.getByRole('button', { name: '进入项目 Second local project' })); + expect(onOpenProject).toHaveBeenCalledWith(secondProject.id); + expect(onOpenProjectSettings).not.toHaveBeenCalled(); }); it('makes the first-Conversation textarea editable while runtime metadata is held', async () => { diff --git a/tests/unit/coding-project-entry.test.ts b/tests/unit/coding-project-entry.test.ts new file mode 100644 index 0000000..f2db7c5 --- /dev/null +++ b/tests/unit/coding-project-entry.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + requestCodingProjectCreation, + requestCodingProjectOpen, + subscribeCodingProjectCreationRequest, + subscribeCodingProjectOpenRequest, +} from '@/lib/coding-project-entry'; + +describe('coding project entry events', () => { + it('delivers project creation requests and removes the listener on unsubscribe', () => { + const listener = vi.fn(); + const unsubscribe = subscribeCodingProjectCreationRequest(listener); + + requestCodingProjectCreation(); + expect(listener).toHaveBeenCalledOnce(); + + unsubscribe(); + requestCodingProjectCreation(); + expect(listener).toHaveBeenCalledOnce(); + }); + + it('delivers the selected project id and ignores requests after unsubscribe', () => { + const listener = vi.fn(); + const unsubscribe = subscribeCodingProjectOpenRequest(listener); + + requestCodingProjectOpen('project-2'); + expect(listener).toHaveBeenCalledWith('project-2'); + + unsubscribe(); + requestCodingProjectOpen('project-3'); + expect(listener).toHaveBeenCalledOnce(); + }); +});