feat: redesign coding default entry

This commit is contained in:
inman
2026-09-06 15:07:19 +08:00
parent f8eee430f4
commit de8ce283dd
12 changed files with 334 additions and 28 deletions

View File

@@ -0,0 +1,99 @@
# Task: Redesign Coding default page
## Identity
- Task ID: 20260906-coding-default-page-a7c42e
- Mode: Feature
- Branch: codex/20260906-coding-default-page-a7c42e-coding-default-page
- Worktree: /Users/inmanx/Documents/makelore-task-coding-default-page-a7c42e
- Base commit: f8eee430f4f4167dc2ecc682ece96c652cdde14b
- Owner: codex
- Status: Ready for Integration
## Scope
- 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.
- Keep project creation reachable from the landing surface and update focused
Renderer/Electron coverage plus README current-state wording.
## Intent And Constraints
- Reuse `src/assets/logo.svg`; do not copy or approximate the Codex mark and do
not generate a new brand asset.
- 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.
- Existing projects, Agent selection, Conversation creation, Composer behavior,
model/runtime controls, and accepted/uncertain mutation semantics remain
unchanged.
- Completed peer tasks for Plugin rehoming and removal of the project setup gate
overlap in Code-entry files but have no committed branch tips to consume. This
task therefore stays isolated on the recorded base and keeps its changes small
enough for later semantic integration.
## Outcome
- 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.
- 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
navigation regression were updated. A side-by-side visual QA pass is recorded
in project-root `design-qa.md` with `final result: passed`.
## Verification
- `corepack pnpm install --frozen-lockfile` — passed with pinned pnpm `10.33.4`.
- `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.
- `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`.
- `corepack pnpm run build:vite` — Renderer, Electron Main, Preload, and utility
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.
- `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.
## Follow-ups
- During integration, semantically reconcile `App.tsx`, `Sidebar.tsx`, and the
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.
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
redirect or a second project-creation implementation.
Human confirmation: not required for intent because the user explicitly
requested this entry behavior; integration still must reconcile the overlapping
peer tasks semantically.

View File

@@ -16,6 +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 编程历史与配额反馈Pi 上下文压缩不删除用户可见的 active-branch 历史,压缩摘要保持 Main-privateRenderer 首次渲染最近 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 不受影响。各模块的后台连接、轮询和子进程必须通过同一生命周期入口登记。

47
design-qa.md Normal file
View File

@@ -0,0 +1,47 @@
**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.
**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.
**Open Questions**
- None. The source does not include Makelore's persistent sidebar or its required project-selection state; those differences are intentional and necessary.
**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.
**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.
**Comparison History**
- Pass 1: no actionable P0/P1/P2 findings. No visual fixes were required after the combined comparison.
**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.
**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.
final result: passed

View File

@@ -218,7 +218,7 @@ function ProjectChatRoute() {
if (!project) {
if (!cancelled) {
setResolvedProjectKey(null);
setResolvedRoute('config');
setResolvedRoute('chat');
}
return;
}

View File

@@ -31,6 +31,7 @@ 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 { 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';
@@ -393,7 +394,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
}, [authUser, refreshTokenUsage]);
const openCreateProject = () => {
const openCreateProject = useCallback(() => {
setNewProjectName('');
setNewProjectSelectedPath('');
setNewProjectDirectoryMode('use-selected-directory');
@@ -402,7 +403,12 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
setNewProjectIdentityProjectId('');
setCreateProjectError(null);
setCreateDialogOpen(true);
};
}, []);
useEffect(() => {
if (!isProgrammingModule) return undefined;
return subscribeCodingProjectCreationRequest(openCreateProject);
}, [isProgrammingModule, openCreateProject]);
const closeCreateProject = () => {
if (creatingProject) return;

View File

@@ -0,0 +1,12 @@
const CODING_PROJECT_CREATE_REQUEST_EVENT = 'makelore:coding-project-create-request';
export function requestCodingProjectCreation(): void {
if (typeof window === 'undefined') return;
window.dispatchEvent(new Event(CODING_PROJECT_CREATE_REQUEST_EVENT));
}
export function subscribeCodingProjectCreationRequest(listener: () => void): () => void {
if (typeof window === 'undefined') return () => undefined;
window.addEventListener(CODING_PROJECT_CREATE_REQUEST_EVENT, listener);
return () => window.removeEventListener(CODING_PROJECT_CREATE_REQUEST_EVENT, listener);
}

View File

@@ -1,8 +1,11 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Bot,
ArrowUp,
CircleAlert,
FolderKanban,
Plus,
RefreshCw,
Settings2,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
@@ -40,10 +43,12 @@ import { CodingConversationSidebar } from './CodingConversationSidebar';
import { CodingConversationHeader } from './CodingConversationHeader';
import { CodingConversationTimeline } from './CodingConversationTimeline';
import { CodingInteractionPanel } from './CodingInteractionPanel';
import { CodingWelcomeHero } from './CodingWelcomeHero';
import { createLocalConversationSnapshot } from './coding-chat-snapshot';
export interface CodingChatPanelProps {
navigationDraft?: string;
onCreateProject?(): void;
onOpenProjectSettings?(): void;
}
@@ -109,6 +114,7 @@ function acceptedPromptCount(
export function CodingChatPanel({
navigationDraft,
onCreateProject,
onOpenProjectSettings,
}: CodingChatPanelProps) {
const activeProject = useCodingWorkspaceStore((state) => state.activeProject);
@@ -592,18 +598,87 @@ export function CodingChatPanel({
if (!activeProject && workspaceLoadState !== 'loading') {
return (
<section className="flex min-h-0 flex-1 items-center justify-center bg-background p-6" data-testid="coding-chat-empty-project">
<div className="max-w-md text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-surface-subtle shadow-[0_0_0_1px_rgba(0,0,0,0.06)]">
<Bot className="h-5 w-5 text-muted-foreground" aria-hidden="true" />
<section
className="flex min-h-0 flex-1 flex-col overflow-y-auto bg-background px-4 pb-3 pt-4 text-foreground sm:px-6 sm:pb-5"
data-testid="coding-chat-empty-project"
>
<CodingWelcomeHero className="min-h-[18rem] flex-1 pb-8 pt-6 sm:pb-12" />
<div className="mx-auto w-full max-w-[46rem] shrink-0">
{workspaceError ? (
<div className="mb-2 flex min-h-10 items-center gap-2 rounded-xl bg-destructive/5 px-3 py-2 text-xs text-destructive">
<CircleAlert className="h-4 w-4 shrink-0" aria-hidden="true" />
<p className="min-w-0 flex-1 text-pretty" role="alert">{workspaceError}</p>
<Button
type="button"
variant="ghost"
className="min-h-8 shrink-0 rounded-lg px-2.5 text-destructive"
onClick={() => void loadWorkspace().catch(() => undefined)}
>
<RefreshCw className="mr-1.5 h-4 w-4" aria-hidden="true" />
</Button>
</div>
) : null}
<div className="overflow-hidden rounded-[1.35rem] bg-surface-subtle shadow-[0_12px_36px_rgba(35,43,56,0.07)]" data-testid="coding-project-starter">
<button
type="button"
className="focus-ring flex min-h-10 w-full items-center gap-2.5 px-4 py-2 text-left text-xs font-medium text-muted-foreground transition-colors duration-150 hover:bg-surface-tertiary hover:text-foreground disabled:cursor-default disabled:hover:bg-transparent disabled:hover:text-muted-foreground"
onClick={onCreateProject}
disabled={!onCreateProject}
>
<FolderKanban className="h-4 w-4 shrink-0" aria-hidden="true" />
<span className="font-semibold text-foreground"></span>
<span aria-hidden="true">·</span>
<span></span>
</button>
<div className="chat-composer-surface border p-3 sm:p-3.5">
<button
type="button"
data-testid="coding-start-project-button"
className="focus-ring block min-h-[4.5rem] w-full rounded-xl px-1 pb-4 pt-1 text-left text-[15px] leading-6 text-muted-foreground transition-colors duration-150 hover:text-foreground disabled:cursor-default disabled:hover:text-muted-foreground"
onClick={onCreateProject}
disabled={!onCreateProject}
>
</button>
<div className="flex min-h-8 items-center gap-1">
<Button
type="button"
variant="ghost"
className="h-8 rounded-lg px-2.5 text-xs font-medium"
onClick={onCreateProject}
disabled={!onCreateProject}
>
<Plus className="mr-1.5 h-4 w-4 text-brand" aria-hidden="true" />
</Button>
{onOpenProjectSettings ? (
<Button
type="button"
variant="ghost"
className="h-8 rounded-lg px-2.5 text-xs font-medium text-muted-foreground"
onClick={onOpenProjectSettings}
>
<Settings2 className="mr-1.5 h-4 w-4" aria-hidden="true" />
</Button>
) : null}
<Button
type="button"
className="ml-auto h-8 w-8 shrink-0 rounded-full bg-foreground p-0 text-background shadow-soft hover:bg-foreground/90"
aria-label="新建或打开项目"
onClick={onCreateProject}
disabled={!onCreateProject}
>
<ArrowUp className="h-4 w-4" aria-hidden="true" />
</Button>
</div>
</div>
</div>
<h1 className="mt-4 text-balance text-lg font-semibold"></h1>
<p className="mt-2 text-pretty text-sm leading-6 text-muted-foreground">
Pi
</p>
<Button className="mt-5 min-h-10 rounded-xl" onClick={onOpenProjectSettings}>
</Button>
</div>
</section>
);

View File

@@ -11,7 +11,6 @@ import {
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import {
Bot,
BookOpen,
Check,
CheckCircle2,
@@ -64,6 +63,7 @@ import type {
SubagentDetailsV1,
} from '../../../shared/coding-conversation-contracts';
import { CodingAttachmentPreview } from './CodingAttachmentPreview';
import { CodingWelcomeHero } from './CodingWelcomeHero';
const EMPTY_NODES: ConversationNode[] = [];
const IDLE_RUN: ConversationRunState = { status: 'idle' };
@@ -1610,7 +1610,10 @@ export const CodingConversationTimeline = memo(function CodingConversationTimeli
if (element.scrollTop <= 48) loadEarlier();
}}
>
<div className="mx-auto flex w-full max-w-[50rem] flex-col gap-8">
<div className={cn(
'mx-auto flex w-full max-w-[50rem] flex-col gap-8',
nodes.length === 0 && 'min-h-full',
)}>
{windowStart > 0 && (
<Button
type="button"
@@ -1632,15 +1635,10 @@ export const CodingConversationTimeline = memo(function CodingConversationTimeli
/>
))}
{nodes.length === 0 && (
<div className="mx-auto flex max-w-md flex-col items-center px-6 py-16 text-center">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-surface-subtle">
<Bot className="h-4.5 w-4.5 text-muted-foreground" aria-hidden="true" />
</div>
<h2 className="mt-4 text-balance text-base font-semibold"></h2>
<p className="mt-1.5 text-pretty text-sm leading-6 text-muted-foreground">
Pi
</p>
</div>
<CodingWelcomeHero
headingLevel="h2"
className="min-h-[18rem] flex-1 px-6 pb-12 pt-6 sm:pb-16"
/>
)}
</div>
</div>

View File

@@ -0,0 +1,29 @@
import makeloreMark from '@/assets/logo.svg';
import { cn } from '@/lib/utils';
export function CodingWelcomeHero({
className,
headingLevel = 'h1',
}: {
className?: string;
headingLevel?: 'h1' | 'h2';
}) {
const Heading = headingLevel;
return (
<div
data-testid="coding-welcome-hero"
className={cn('flex flex-col items-center justify-center text-center', className)}
>
<img
src={makeloreMark}
alt="麦洛 M 标识"
className="h-auto w-12 select-none sm:w-[3.25rem]"
draggable={false}
/>
<Heading className="mt-6 max-w-2xl text-balance text-[clamp(1.65rem,2.45vw,2.1rem)] font-medium leading-[1.28] tracking-[-0.04em] text-foreground">
</Heading>
</div>
);
}

View File

@@ -1,5 +1,6 @@
import { CodingChatPanel } from './CodingChatPanel';
import { useNavigate } from 'react-router-dom';
import { requestCodingProjectCreation } from '@/lib/coding-project-entry';
function getNavigationDraft(state: unknown): string | undefined {
if (!state || typeof state !== 'object') return undefined;
@@ -24,6 +25,7 @@ export function Chat() {
<div data-testid="chat-operation-page" className="flex h-[calc(100%_+_3rem)] min-h-0 -m-6 overflow-hidden bg-background">
<CodingChatPanel
navigationDraft={navigationDraft}
onCreateProject={requestCodingProjectCreation}
onOpenProjectSettings={() => navigate('/project-config')}
/>
</div>

View File

@@ -139,8 +139,12 @@ test.describe('Makelore module navigation without setup flow', () => {
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('main-layout')).toBeVisible();
await expect(page).toHaveURL(/\/chat$/);
await expect(page.getByText('请先新建项目')).toHaveCount(0);
await expect(page.getByText('一念成光,万物可创。')).toBeVisible();
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.getByText('一念成光,万物可创。')).toHaveCount(0);
await expect(page.getByRole('img', { name: 'Makelore logo' })).toBeVisible();
await expect(page.getByTestId('sidebar-module-switcher-trigger')).toContainText('编程 Code');
await expect(page.getByTestId('sidebar-module-return-icon')).toBeVisible();
@@ -154,6 +158,12 @@ test.describe('Makelore module navigation without setup flow', () => {
await expect(page.getByTestId('game-asset-browser')).toHaveCount(0);
await expect(page.getByTestId('game-asset-review-summary')).toHaveCount(0);
await page.getByTestId('coding-start-project-button').click();
const createProjectDialog = page.getByRole('dialog', { name: '新建项目' });
await expect(createProjectDialog).toBeVisible();
await createProjectDialog.getByRole('button', { name: '取消' }).first().click();
await expect(page.getByRole('dialog', { name: '新建项目' })).toHaveCount(0);
await expect(page.getByTestId('sidebar-module-switcher')).toBeVisible();
const moduleHomeTrigger = page.getByTestId('sidebar-module-switcher-trigger');
await expect(moduleHomeTrigger).toHaveAttribute('aria-label', '返回首页');

View File

@@ -199,6 +199,31 @@ 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 });
const onCreateProject = vi.fn();
const onOpenProjectSettings = vi.fn();
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
render(
<CodingChatPanel
onCreateProject={onCreateProject}
onOpenProjectSettings={onOpenProjectSettings}
/>,
);
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('尚未选择项目');
fireEvent.click(screen.getByTestId('coding-start-project-button'));
expect(onCreateProject).toHaveBeenCalledOnce();
fireEvent.click(screen.getByRole('button', { name: '项目与插件设置' }));
expect(onOpenProjectSettings).toHaveBeenCalledOnce();
});
it('makes the first-Conversation textarea editable while runtime metadata is held', async () => {
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
projectApi.config.mockResolvedValue({ project, config });
@@ -215,6 +240,8 @@ describe('CodingChatPanel first Conversation', () => {
const view = render(<CodingChatPanel />);
const textbox = await screen.findByRole('textbox');
expect(screen.getByRole('heading', { name: '你想让麦洛和你一起构建什么?' })).toBeInTheDocument();
expect(screen.getByRole('img', { name: '麦洛 M 标识' })).toBeInTheDocument();
expect(textbox).toBeEnabled();
expect(projectApi.create).toHaveBeenCalledOnce();
expect(conversationApi.snapshot).toHaveBeenCalledWith(conversation.id);
@@ -922,7 +949,7 @@ describe('CodingChatPanel first Conversation', () => {
target: { files },
});
expect(await screen.findByText('每条消息最多添加 16 张图片。')).toBeInTheDocument();
expect(screen.getAllByRole('img')).toHaveLength(16);
expect(screen.getAllByRole('img', { name: /^image-\d+\.png$/u })).toHaveLength(16);
expect(codingConversationStore.getState().entriesByConversationId[conversation.id])
.toMatchObject({ loadState: 'live', error: null });
await waitFor(() => expect(screen.getByRole('button', { name: '发送' })).toBeEnabled());