diff --git a/.project-docs/30-worklog/tasks/20260906-project-setup-ux-90fe6cf2.md b/.project-docs/30-worklog/tasks/20260906-project-setup-ux-90fe6cf2.md
index 6692645..4f54993 100644
--- a/.project-docs/30-worklog/tasks/20260906-project-setup-ux-90fe6cf2.md
+++ b/.project-docs/30-worklog/tasks/20260906-project-setup-ux-90fe6cf2.md
@@ -36,59 +36,72 @@
## Outcome
-- Confirmed that the screenshot's “项目智能体” is a persisted project-owned parent Agent,
- not a runtime child Agent. ADR-006 and ADR-008 deliberately keep child Agents ephemeral
- and without inherited project Plugin resources.
-- Confirmed a state-invariant gap: Project Configuration validates only the Agents that
- exist, so an empty Agent array passes; `submit()` then saves `initialized: true`.
- `ProjectChatRoute` and `MainLayout` gate only on that boolean, despite the gate copy
- claiming that at least one manually configured Agent is required.
-- Confirmed the resulting dead-end presentation: Coding chat has no selected Agent, clears
- the Conversation selection, renders only a small “当前项目还没有可用智能体” sidebar
- message, leaves the main canvas empty, and disables the Composer without a recovery CTA.
-- Confirmed the navigation gap: Project Configuration uses history-relative
- `navigate(-1)`. The destination depends on how the page was entered and can even be a
- visually identical Project Configuration history entry; saving does not navigate to
- the Conversation page.
-- Confirmed an additional supported-path gap: first-run Setup does not configure a model.
- With zero model options, the create-Agent icon shows a toast and opens a read-only model
- drawer that contains no direct action to `/models`, so stronger Agent guidance alone
- would still lead some users into a dead end.
-- Recommended the “required but escapable” flow: an incomplete project may exist and the
- user may leave, but completion/Conversation readiness requires at least one enabled,
- unarchived, fully configured project Agent. In the empty state, replace the icon-only
- instruction with a prominent text CTA; after the first Agent is valid, make the primary
- action “保存并进入对话” with deterministic `/chat` navigation. Keep a full chat recovery
- state for legacy/archived-all data and route it directly back to Agent creation.
-- Recommended treating zero models as an explicit prerequisite state with a “先配置模型”
- CTA and a return intent, and replacing history-relative back behavior with context-aware,
- labelled destinations such as “返回对话” or “稍后设置”.
-- No product behavior or source code was changed.
+- Confirmed that the screenshot's “项目智能体” is the persisted project-owned parent
+ Agent, not an ephemeral runtime child Agent, and retained that terminology throughout.
+- Added one shared Conversation-readiness rule: `initialized` must be true and the project
+ must contain at least one enabled, unarchived Agent with a non-empty name, avatar, role,
+ responsibility, and resolved model. Project list entry, direct `/chat` resolution,
+ the programming-module gate, and Coding chat now use the same rule instead of trusting
+ `initialized` alone.
+- Reworked the zero-ready-Agent configuration state into a prominent required-step card.
+ With no model it offers “先配置模型” and a direct model-settings action; otherwise it
+ creates the first Agent or resumes an enabled incomplete Agent. Disabled-only state
+ intentionally offers a new Agent because the edit dialog has no enable control.
+- The first Agent is persisted immediately and then navigates to `/chat`. Later Agent
+ additions and edits retain the existing page-draft plus explicit-save behavior.
+- Replaced history-relative back navigation with labelled, deterministic “返回对话” or
+ “稍后设置” destinations. Unsaved Agent changes are confirmed before any page exit,
+ including the model-settings prerequisite path.
+- Archiving the last ready Agent now explains that saving will return the project to a
+ pending state; saving that state clears `initialized` and leaves the unusable chat route.
+- Added a full Coding chat recovery state with a direct project-configuration action and no
+ Composer or Conversation/runtime creation when no usable Agent exists.
+- Updated README with the accepted readiness and onboarding behavior.
## Verification
-- Inspected the supplied 1493×891 Project Configuration screenshot at original detail.
-- Read the project memory startup set, relevant product/domain rules, ADR-006, ADR-008,
- and related concurrent task scopes after passing the ownership and planning gates.
-- Inspected `src/pages/ProjectConfiguration/index.tsx`, `src/App.tsx`,
- `src/components/layout/MainLayout.tsx`, `src/components/layout/Sidebar.tsx`,
- `src/pages/Chat/CodingChatPanel.tsx`, `CodingConversationSidebar.tsx`,
- `AgentCreationDialog.tsx`, the project configuration schema/service, Setup, README,
- focused tests, and relevant Git blame/history.
-- Queried the UI/UX reference for onboarding, empty-state, primary-action, keyboard, and
- predictable-back guidance. Its generated dark palette was rejected because Makelore's
- repository contract requires the existing single light visual system.
-- No automated test was run because this task intentionally made no behavior change; a
- future implementation requires focused unit coverage and the shared Electron E2E flow.
+- Focused Vitest: 5 files, 63 tests passed, covering readiness, App routing, MainLayout
+ gating, Project Configuration onboarding/navigation, and Coding chat recovery.
+- Full `pnpm test`: 230 files / 1928 tests passed with 2 skipped, followed by the isolated
+ pressure test passing 1/1.
+- `pnpm run typecheck`: passed after the final interaction changes.
+- `pnpm run lint:check`: passed with 0 errors; it retained 5 pre-existing warnings in
+ `src/pages/Home/index.tsx` and `src/pages/Makelore/index.tsx`.
+- `pnpm run build:vite`: passed after the final interaction changes; only the repository's
+ existing Browserslist, dynamic-import, and chunk-size warnings were emitted.
+- Electron E2E `project-configuration-skills.spec.ts`: passed 1/1, including zero-model
+ guidance, legacy `initialized: true` plus zero-Agent recovery, and labelled return to chat.
+- Playwright trace inspection showed an initial E2E failure was caused by that spec's later
+ restrictive Host API stub, not product navigation; the navigation assertion now runs
+ before the stub and passes.
+- Independent read-only review found the disabled-only recovery loop; after the targeted
+ correction and test, the reviewer returned PASS.
+- `git diff --check`: passed.
+- Task Documentation Gate remains blocked because merging current `main` introduced the
+ unchanged foreign task record
+ `.project-docs/30-worklog/tasks/20260906-agent-browser-failure-a7c91e4d.md` relative to
+ this task's older recorded base. The record was not modified by this task.
## Follow-ups
-- If the recommended direction is accepted, implement one shared readiness predicate and
- cover zero Agents, all Agents archived, no configured model, first Agent completion,
- deterministic return/continue navigation, unsaved changes, and chat recovery.
-- Update README only with the accepted final interaction behavior, not this discussion or
- a historical worklog.
+- Obtain explicit human confirmation for the project-doc recovery required to adopt the
+ unchanged foreign task record brought in by the already-created, unpushed `main` merge;
+ then rerun `check_doc_drift.py`, set this record to Ready for Integration, commit the
+ result, and run `task_context.py complete`.
## Promotion Candidates
-- None recorded.
+- Target: `.project-docs/30-worklog/current-state.md`.
+ Proposal: record that Coding Conversation readiness requires both persisted
+ initialization and at least one enabled, unarchived, fully configured project Agent;
+ all project-entry and chat-recovery surfaces share this invariant, and the first Agent
+ setup is the explicit completion path.
+ Evidence: shared readiness tests, Project Configuration onboarding tests, App/MainLayout
+ route tests, Coding chat recovery tests, Electron E2E, successful full unit suite,
+ typecheck, lint, production build, and independent review.
+ Future impact: future project-entry, archival, or Agent-edit work must reuse the shared
+ predicate rather than treating `initialized` as sufficient on its own.
+ Semantic conflicts: none known; the rule preserves the existing no-default-Agent and
+ project-owned Agent decisions.
+ Human confirmation required: no, unless integration discovers a conflicting accepted
+ product decision.
diff --git a/README.md b/README.md
index 2e441ef..d7c2327 100644
--- a/README.md
+++ b/README.md
@@ -126,7 +126,8 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p
- 新项目默认没有智能体;用户在项目内创建智能体时必须填写名称、预设头像或本地头像、职责说明和精确的 Provider 账号与默认模型。该选择以 `{ accountId, modelId, thinkingLevel }` 保存,不依赖运行时私有 Provider id。本地头像会自动裁剪为 256×256,并优先压缩为 WebP 后随项目配置保存;系统指令与技能属于高级设置。
- Makelore 为每个项目智能体附加平台管理的中文语言规则:自然语言默认使用简体中文,包括可见的思考过程、进度说明、工具调用说明和最终回复;代码、命令、路径、日志、错误原文与必要引用保留原始语言。用户或项目智能体系统指令明确要求其他语言时按该要求执行;模型仍不遵循时视为 Provider 模型行为。
-- 项目配置页的新增与已有智能体编辑统一使用居中弹窗;编辑保存先更新当前页面草稿,点击底部“保存项目配置”后统一持久化。“可用模型”抽屉只展示已配置模型、提供方和文本/多模态能力,不提供项目级选择。智能体的默认模型只作为新对话的初始值;核心聊天页可为当前对话独立切换模型和思考强度,切换不会改写智能体默认值或其他对话。
+- 新项目可以暂时保留为待配置状态,但只有至少一个已启用、未归档且名称、头像、默认模型、角色与职责完整的项目智能体时,项目才可进入对话;旧配置即使错误保留了 `initialized: true`,也会回到项目配置完成这一步。配置页用顶部必需步骤和主操作强引导创建智能体,无模型时直接引导到模型设置;左上角提供带文字的确定性返回操作。聊天页遇到无可用智能体时显示可恢复空态,不展示不可用的输入框。
+- 项目配置页的新增与已有智能体编辑统一使用居中弹窗;首个智能体创建成功后会立即持久化并进入项目对话,后续新增与编辑先更新当前页面草稿,再点击底部“保存项目配置”统一持久化。“可用模型”抽屉只展示已配置模型、提供方和文本/多模态能力,不提供项目级选择。智能体的默认模型只作为新对话的初始值;核心聊天页可为当前对话独立切换模型和思考强度,切换不会改写智能体默认值或其他对话。
- 项目智能体只保存自身的精确默认模型;未完成模型选择的智能体保持待配置状态,不从项目级 `defaultModel` 或运行时私有配置回退。
- “可用技能”入口使用模块图标;点击技能后先展示目录结构,再展示主文件 `SKILL.md` 原文,并支持返回技能列表。
- 一个智能体可以拥有多条互相独立的 Pi Session;智能体和对话元数据分别由 `.makelore/project.json` 与 `.makelore/conversations.json` 保存,稳定的智能体与对话 id 保持本地历史连续。
diff --git a/shared/coding-project-contracts.ts b/shared/coding-project-contracts.ts
index b2f1fce..12e9c9a 100644
--- a/shared/coding-project-contracts.ts
+++ b/shared/coding-project-contracts.ts
@@ -56,3 +56,22 @@ export interface CodingProjectConfigSnapshot {
config: CodingProjectConfig;
knowledgeFiles: string[];
}
+
+export function isCodingProjectAgentReady(agent: CodingProjectAgent): boolean {
+ return agent.enabled
+ && agent.archivedAt === null
+ && agent.name.trim().length > 0
+ && agent.avatarId.trim().length > 0
+ && agent.roleName.trim().length > 0
+ && agent.responsibility.mission.trim().length > 0
+ && agent.modelResolution === 'resolved'
+ && agent.model !== null;
+}
+
+export function hasReadyCodingProjectAgent(config: CodingProjectConfig): boolean {
+ return config.agents.some(isCodingProjectAgentReady);
+}
+
+export function isCodingProjectConversationReady(config: CodingProjectConfig): boolean {
+ return config.initialized && hasReadyCodingProjectAgent(config);
+}
diff --git a/src/App.tsx b/src/App.tsx
index c97a8d8..921dda8 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -26,6 +26,7 @@ import { subscribeHostEvent } from '@/lib/host-events';
import { reportDesktopActivity, type DesktopActivityModule } from '@/lib/host-api';
import { installRendererPerformanceDiagnostics } from '@/lib/performance-diagnostics';
import { resolveSupportedLanguage } from '../shared/language';
+import { isCodingProjectConversationReady } from '../shared/coding-project-contracts';
import { LegacyPluginRedirect } from './pages/Plugins/legacy-plugin-redirect';
const Models = lazy(() => import('./pages/Models').then(({ Models: component }) => ({ default: component })));
@@ -229,7 +230,12 @@ function ProjectChatRoute() {
const configResult = await loadProjectConfig(project.id);
if (!cancelled) {
setResolvedProjectKey(project.id);
- setResolvedRoute(configResult.status === 'valid' && Boolean(configResult.config?.initialized) ? 'chat' : 'config');
+ setResolvedRoute(
+ configResult.status === 'valid'
+ && Boolean(configResult.config && isCodingProjectConversationReady(configResult.config))
+ ? 'chat'
+ : 'config',
+ );
}
} catch {
if (!cancelled) {
diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx
index 06c2037..b103f45 100644
--- a/src/components/layout/MainLayout.tsx
+++ b/src/components/layout/MainLayout.tsx
@@ -14,6 +14,7 @@ import { useProjectConfigStore } from '@/stores/project-config';
import { useSettingsStore } from '@/stores/settings';
import { cn } from '@/lib/utils';
import type { SidebarPeekSource } from './sidebar-peek';
+import { isCodingProjectConversationReady } from '../../../shared/coding-project-contracts';
const SIDEBAR_PEEK_CLOSE_DELAY_MS = 180;
@@ -88,7 +89,11 @@ export function MainLayout() {
if (activeProject && isProgrammingModule) void load(activeProject.id).catch(() => undefined);
}, [activeProject, isProgrammingModule, load]);
- const initializationBlocked = Boolean(activeProject && (!config || !config.initialized) && !isInitializationSafeRoute);
+ const initializationBlocked = Boolean(
+ activeProject
+ && (!config || !isCodingProjectConversationReady(config))
+ && !isInitializationSafeRoute,
+ );
return (
{/* Title bar: drag region on macOS, icon + controls on Windows */}
@@ -121,8 +126,8 @@ export function MainLayout() {
-
项目尚未初始化
-
请先在项目配置中完成至少一个手动 Agent 的名称、模型和职责设置。
+
项目还差一步
+
请先在项目配置中创建或恢复一个项目智能体,并完成名称、模型和职责设置。
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index 6637de7..4e2aa95 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -53,7 +53,10 @@ import { isCanonicalCodingProjectId } from '@/lib/coding-projects';
import type { CodingProjectSummary } from '@/types/coding-project';
import { useProjectConfigStore } from '@/stores/project-config';
import type { ProjectType } from '../../../shared/project-config';
-import type { ProjectIdentityChoice } from '../../../shared/coding-project-contracts';
+import {
+ isCodingProjectConversationReady,
+ type ProjectIdentityChoice,
+} from '../../../shared/coding-project-contracts';
import { toast } from 'sonner';
type OpenDialogResult = {
@@ -438,7 +441,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
return;
}
await setActiveProject(project.id);
- navigate(result.config.initialized
+ navigate(isCodingProjectConversationReady(result.config)
? '/chat'
: projectConfigPath);
};
diff --git a/src/pages/Chat/CodingChatPanel.tsx b/src/pages/Chat/CodingChatPanel.tsx
index 52b1a01..85919c1 100644
--- a/src/pages/Chat/CodingChatPanel.tsx
+++ b/src/pages/Chat/CodingChatPanel.tsx
@@ -41,7 +41,12 @@ import { CodingConversationSidebar } from './CodingConversationSidebar';
import { CodingConversationHeader } from './CodingConversationHeader';
import { CodingConversationTimeline } from './CodingConversationTimeline';
import { CodingInteractionPanel } from './CodingInteractionPanel';
+import { CodingProjectSetupEmptyState } from './CodingProjectSetupEmptyState';
import { createLocalConversationSnapshot } from './coding-chat-snapshot';
+import {
+ isCodingProjectAgentReady,
+ isCodingProjectConversationReady,
+} from '../../../shared/coding-project-contracts';
export interface CodingChatPanelProps {
navigationDraft?: string;
@@ -158,8 +163,9 @@ export function CodingChatPanel({
const submissionFlightsRef = useRef(new Set());
const agents = useMemo(() => (
- config?.agents.filter((agent) => agent.enabled && !agent.archivedAt) ?? []
+ config?.agents.filter(isCodingProjectAgentReady) ?? []
), [config]);
+ const conversationReady = Boolean(config && isCodingProjectConversationReady(config));
const selectedAgent = agents.find((agent) => agent.id === selectedAgentId) ?? null;
const selectedConversation = conversations.find((conversation) => (
conversation.id === selectedConversationId
@@ -615,6 +621,10 @@ export function CodingChatPanel({
);
}
+ if (activeProject && config && !conversationReady && workspaceLoadState !== 'loading') {
+ return ;
+ }
+
return (
+
+
+
+
+
创建项目智能体后开始对话
+
+ 当前项目还没有可用的项目智能体。完成名称、模型和职责设置后,就可以发送消息。
+
+
+
+
+ );
+}
diff --git a/src/pages/ProjectConfiguration/index.tsx b/src/pages/ProjectConfiguration/index.tsx
index 1e86629..5459d14 100644
--- a/src/pages/ProjectConfiguration/index.tsx
+++ b/src/pages/ProjectConfiguration/index.tsx
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
-import { ArrowLeft, Blocks, Check, Cpu, FileText, Files, FolderOpen, Plus, Trash2, Upload, X } from 'lucide-react';
+import { ArrowLeft, Blocks, Bot, Check, Cpu, FileText, Files, FolderOpen, Plus, Trash2, Upload, X } from 'lucide-react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -20,10 +20,16 @@ import { useCodingWorkspaceStore } from '@/stores/coding-workspace';
import { useProjectConfigStore } from '@/stores/project-config';
import { useProviderStore } from '@/stores/providers';
import type { CodingProjectAgent, CodingProjectConfig } from '@/types/coding-project';
-import type { ProjectIdentityChoice } from '../../../shared/coding-project-contracts';
+import {
+ hasReadyCodingProjectAgent,
+ isCodingProjectAgentReady,
+ isCodingProjectConversationReady,
+ type ProjectIdentityChoice,
+} from '../../../shared/coding-project-contracts';
const EMPTY_FILES: string[] = [];
type DrawerMode = 'models' | 'knowledge' | 'skills' | null;
+type ExitDestination = '/chat' | '/module-select' | '/models';
type ProjectIdentityKind = ProjectIdentityChoice['kind'];
type SkillStructureEntry = { path: string; type: 'file' | 'directory' };
type SkillInfo = {
@@ -93,7 +99,8 @@ function SkillDetail({ skill }: { skill: SkillInfo }) {
}
function AgentCard({ agent, onOpen }: { agent: CodingProjectAgent; onOpen: () => void }) {
- return ;
+ const ready = isCodingProjectAgentReady(agent);
+ return ;
}
function ModelCard({ model }: { model: CodingModelOption }) {
@@ -111,10 +118,20 @@ function ModelCard({ model }: { model: CodingModelOption }) {