feat: remove Code project prompt and Skills controls
This commit is contained in:
1 parent
350f7d740b
commit
140f6be8db
6 files changed
+240
-37
No files matched your search
@@ -0,0 +1,67 @@
|
|||||||
|
# Task: Remove project prompt and Skills editing controls
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- Task ID: 20261014-remove-project-agent-fields-6c2e8b14
|
||||||
|
- Mode: Feature
|
||||||
|
- Branch: codex/20261014-remove-project-agent-fields-6c2e8b14-remove-project-agent-fields
|
||||||
|
- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261014-remove-project-agent-fields-6c2e8b14
|
||||||
|
- Base commit: 350f7d740b0559a43c7b3f8ab6cd32401cc6f677
|
||||||
|
- Owner: codex
|
||||||
|
- Status: Ready for Integration
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Remove Code project prompt and project Skills controls from the configuration page and the unused Skill-list request; clarify default-model copy.
|
||||||
|
|
||||||
|
## Intent And Constraints
|
||||||
|
|
||||||
|
- User explicitly requested deleting both visible edit sections and asks how historical conversations can be affected. Explain past messages/results are immutable; later turns may use refreshed resources. No advanced replacement entry. Existing backend schema/Agent resources remain compatible; saving model settings must not silently reset hidden metadata. No live-app update, merge, push or worktree retirement requested.
|
||||||
|
|
||||||
|
## Project Context Loaded
|
||||||
|
|
||||||
|
Task context:
|
||||||
|
- Official task identity, feature mode, branch, absolute worktree and base350f7d7 verified against registry and Git.
|
||||||
|
- All59 peer records/177 Scope, Intent and Promotion sections read; no conflict. Child-language placeholder scope remains unknown without concrete dependency. New preview/gallery scopes do not own this configuration form.
|
||||||
|
|
||||||
|
Read:
|
||||||
|
- Own record, AGENTS, entry/read-before-planning/memory-index/planning-gate. Positioning/current-state/decision-index/overview, community boundary, domain/success and evidence/reflection/commitment context retained from just-completed assessment on identical base; canonical diff is empty.
|
||||||
|
- Current ProjectConfiguration and affected unit/Electron tests, README and prior runtime assessment.
|
||||||
|
|
||||||
|
Relevant understanding:
|
||||||
|
- Project-owned local Agent metadata is authoritative; runtime prompt/Skill contracts are separate from visible edit controls and teacher cloud configuration.
|
||||||
|
- User has now approved UI deletion, superseding the assessment suggestion of an advanced entry. No runtime resource migration or historical-message rewrite is requested.
|
||||||
|
- Files: ProjectConfiguration/index.tsx, README and focused configuration tests. Model save should preserve existing prompt/skillIds in the cloned draft.
|
||||||
|
- Unknown: live temporary-app revision; no update attempted.
|
||||||
|
|
||||||
|
Gate result: Passed.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Delete prompt/Skills controls and unused Skill-fetch state; narrow the explanation to model defaults.
|
||||||
|
2. Verify model-only saving preserves configuration data, and desktop settings contain no prompt/Skills edit controls.
|
||||||
|
3. Run focused checks/build, document and commit the result.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- Deleted the project prompt textarea and project Skills assignment section entirely, with no advanced replacement. Deleted the now-unused SkillInfo type, list state, fetch callback and reload requests.
|
||||||
|
- Default programming settings now expose only the model selector. Copy explicitly states new conversations use the default model while existing conversation models/messages remain unchanged; the no-project state no longer advertises prompt editing.
|
||||||
|
- README and existing desktop assertions updated. Historical messages and executed results are not rewritten by configuration refresh; the earlier assessment's 'affected history' phrasing meant future turns in that conversation, not past messages.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Product typecheck, build:vite, product/legacy-spec scoped ESLint and whitespace checks passed.
|
||||||
|
- Existing coding-feature-ui and project-config-store suites: 15 tests passed.
|
||||||
|
- Focused project-configuration-controls suite: 2 tests passed, covering default and historical Agent routes. Model saves preserve exact hidden prompt/skillIds and untouched Agent metadata without mutating the loaded config; no Skill-list request occurs.
|
||||||
|
- Isolated Electron project-menu/configuration scenario passed1/1 against fresh build: correct inactive target project/model-b, enabled model/save controls, new copy, no prompt/Skills inputs. Reviewed settings screenshot. Initial run hit an existing modal-close/pointer-leave race in the test; synchronized that transition with menu closure/archive readiness before rerunning, without product changes.
|
||||||
|
- Evidence: `/tmp/remove-project-agent-fields-electron-6c2e8b14-rerun/run.log` and `results/pi-coding-first-chat-proje-cfb75-gned-at-every-sidebar-width/project-settings-model-only.png`. Generated main entry restored byte-for-byte; isolated processes exited. No real account, project or running application modified.
|
||||||
|
- Updated existing legacy project-configuration-skills assertions without running that broader historical scenario. No full-suite claim; targeted scope verified.
|
||||||
|
- Independent read-only product review passed. Final changed-file ESLint/typecheck and task-aware document drift check passed.
|
||||||
|
|
||||||
|
## Follow-ups
|
||||||
|
|
||||||
|
- Running application update, integration, push and cleanup remain separate actions.
|
||||||
|
|
||||||
|
## Promotion Candidates
|
||||||
|
|
||||||
|
- Target: current-state / Code configuration summary. Proposal: Code default programming configuration only exposes model selection; prompt and project Skills fields are not user-editable in that page. Evidence: user directive, removed controls/request and focused verification. Future impact: do not reintroduce these controls or imply historical messages are rewritten. No conflict; user explicitly confirmed this behavior. Canonical promotion only during future integration.
|
||||||
@@ -156,7 +156,7 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p
|
|||||||
- 新项目自动建立一个内部默认编程智能体,`defaultAgentId` 固定其身份;创建项目不再要求创建或选择智能体。有可用的全局默认模型时复制精确账号与模型,没有时保留待选择状态。旧项目优先保留原默认或置顶/首个可用智能体,保留所有历史绑定;全部停用时新增默认配置,不复活旧配置。
|
- 新项目自动建立一个内部默认编程智能体,`defaultAgentId` 固定其身份;创建项目不再要求创建或选择智能体。有可用的全局默认模型时复制精确账号与模型,没有时保留待选择状态。旧项目优先保留原默认或置顶/首个可用智能体,保留所有历史绑定;全部停用时新增默认配置,不复活旧配置。
|
||||||
- Makelore 为每个项目智能体附加平台管理的中文语言规则:自然语言默认使用简体中文,包括可见的思考过程、进度说明、工具调用说明和最终回复;代码、命令、路径、日志、错误原文与必要引用保留原始语言。用户或项目智能体系统指令明确要求其他语言时按该要求执行;模型仍不遵循时视为 Provider 模型行为。
|
- Makelore 为每个项目智能体附加平台管理的中文语言规则:自然语言默认使用简体中文,包括可见的思考过程、进度说明、工具调用说明和最终回复;代码、命令、路径、日志、错误原文与必要引用保留原始语言。用户或项目智能体系统指令明确要求其他语言时按该要求执行;模型仍不遵循时视为 Provider 模型行为。
|
||||||
- 有效项目直接进入会话工作区。打开项目不创建空会话;首次发送或点击项目下的新建入口才创建本地元数据。首条消息无需等待运行时 Snapshot 返回。
|
- 有效项目直接进入会话工作区。打开项目不创建空会话;首次发送或点击项目下的新建入口才创建本地元数据。首条消息无需等待运行时 Snapshot 返回。
|
||||||
- 项目设置直接编辑默认编程配置的模型、系统指令和需要分配的 Skill;会话菜单可进入该历史会话绑定的编程配置。默认模型只影响新对话,现有对话独立切换模型与思考选项,原有配置编辑不会改写其他对话。
|
- 项目设置仅提供默认编程配置的模型选择,不展示项目提示词或项目 Skills 编辑入口;知识文件与插件仍在各自入口管理。会话菜单可进入该历史会话绑定的编程配置。默认模型只影响新对话,现有对话独立切换模型与思考选项,已有消息记录不会被改写;保存模型配置保留原有 Agent 元数据。
|
||||||
- 项目智能体只保存自身的精确默认模型;未完成模型选择的智能体保持待配置状态,不从项目级 `defaultModel` 或运行时私有配置回退。
|
- 项目智能体只保存自身的精确默认模型;未完成模型选择的智能体保持待配置状态,不从项目级 `defaultModel` 或运行时私有配置回退。
|
||||||
- 一个智能体可以拥有多条互相独立的 Pi Session;智能体和对话元数据分别由 `.makelore/project.json` 与 `.makelore/conversations.json` 保存,稳定的智能体与对话 id 保持本地历史连续。
|
- 一个智能体可以拥有多条互相独立的 Pi Session;智能体和对话元数据分别由 `.makelore/project.json` 与 `.makelore/conversations.json` 保存,稳定的智能体与对话 id 保持本地历史连续。
|
||||||
- 会话列表直接挂在左侧项目下,不展示中间智能体列;同一项目内不同历史智能体的会话统一列出,保留归档、改名、未读与运行状态。快速切换项目和会话时,迟到的请求与回复保持原归属。
|
- 会话列表直接挂在左侧项目下,不展示中间智能体列;同一项目内不同历史智能体的会话统一列出,保留归档、改名、未读与运行状态。快速切换项目和会话时,迟到的请求与回复保持原归属。
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { ArrowLeft, Check, Cpu, Files, Puzzle, Trash2, Upload, X } from 'lucide-react';
|
import { ArrowLeft, Check, Cpu, Files, Puzzle, Trash2, Upload, X } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -10,7 +10,6 @@ import { ProjectPublishAction } from '@/components/works/ProjectPublishAction';
|
|||||||
import { Plugins } from '@/pages/Plugins';
|
import { Plugins } from '@/pages/Plugins';
|
||||||
import { PROJECT_SETTINGS_PLUGINS_PATH } from '@/pages/Plugins/legacy-plugin-redirect';
|
import { PROJECT_SETTINGS_PLUGINS_PATH } from '@/pages/Plugins/legacy-plugin-redirect';
|
||||||
import { buildCodingModelOptions, parseCodingModelKey, type CodingModelOption } from '@/lib/coding-model-options';
|
import { buildCodingModelOptions, parseCodingModelKey, type CodingModelOption } from '@/lib/coding-model-options';
|
||||||
import { getCodingSkills } from '@/lib/coding-product-tools';
|
|
||||||
import { useCodingWorkspaceStore } from '@/stores/coding-workspace';
|
import { useCodingWorkspaceStore } from '@/stores/coding-workspace';
|
||||||
import { useProjectConfigStore } from '@/stores/project-config';
|
import { useProjectConfigStore } from '@/stores/project-config';
|
||||||
import { useProviderStore } from '@/stores/providers';
|
import { useProviderStore } from '@/stores/providers';
|
||||||
@@ -18,13 +17,6 @@ import type { CodingProjectAgent, CodingProjectConfig } from '@/types/coding-pro
|
|||||||
|
|
||||||
const EMPTY_FILES: string[] = [];
|
const EMPTY_FILES: string[] = [];
|
||||||
type DrawerMode = 'models' | 'knowledge' | null;
|
type DrawerMode = 'models' | 'knowledge' | null;
|
||||||
type SkillInfo = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
available: boolean;
|
|
||||||
effective: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const THINKING_LEVEL_LABELS = {
|
const THINKING_LEVEL_LABELS = {
|
||||||
off: '关闭',
|
off: '关闭',
|
||||||
@@ -122,24 +114,11 @@ export function ProjectConfiguration() {
|
|||||||
const [draft, setDraft] = useState<CodingProjectConfig | null>(null);
|
const [draft, setDraft] = useState<CodingProjectConfig | null>(null);
|
||||||
const [drawerMode, setDrawerMode] = useState<DrawerMode>(null);
|
const [drawerMode, setDrawerMode] = useState<DrawerMode>(null);
|
||||||
const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
|
const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
|
||||||
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const knowledgeInputRef = useRef<HTMLInputElement | null>(null);
|
const knowledgeInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const loadSkills = useCallback(() => {
|
|
||||||
void getCodingSkills()
|
|
||||||
.then((result) => setSkills(result.skills.map((skill) => ({
|
|
||||||
id: skill.id,
|
|
||||||
name: skill.name,
|
|
||||||
description: skill.description,
|
|
||||||
available: skill.available,
|
|
||||||
effective: skill.effective,
|
|
||||||
}))))
|
|
||||||
.catch(() => setSkills([]));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => { if (activeProject) void load(activeProject.id); }, [activeProject, load]);
|
useEffect(() => { if (activeProject) void load(activeProject.id); }, [activeProject, load]);
|
||||||
useEffect(() => { if (stored) setDraft(structuredClone(stored)); }, [stored]);
|
useEffect(() => { if (stored) setDraft(structuredClone(stored)); }, [stored]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -147,7 +126,6 @@ export function ProjectConfiguration() {
|
|||||||
if (!requestedAgentId || !draft?.agents.some((agent) => agent.id === requestedAgentId)) return;
|
if (!requestedAgentId || !draft?.agents.some((agent) => agent.id === requestedAgentId)) return;
|
||||||
setActiveAgentId(requestedAgentId);
|
setActiveAgentId(requestedAgentId);
|
||||||
}, [draft, location.search]);
|
}, [draft, location.search]);
|
||||||
useEffect(() => { loadSkills(); }, [loadSkills]);
|
|
||||||
useEffect(() => { void refreshProviderSnapshot(); }, [refreshProviderSnapshot]);
|
useEffect(() => { void refreshProviderSnapshot(); }, [refreshProviderSnapshot]);
|
||||||
const modelOptions = useMemo(
|
const modelOptions = useMemo(
|
||||||
() => buildCodingModelOptions(providerAccounts, providerVendors),
|
() => buildCodingModelOptions(providerAccounts, providerVendors),
|
||||||
@@ -162,11 +140,10 @@ export function ProjectConfiguration() {
|
|||||||
search: '?scope=all&source=all&state=available',
|
search: '?scope=all&source=all&state=available',
|
||||||
});
|
});
|
||||||
const closePluginWorkspace = () => {
|
const closePluginWorkspace = () => {
|
||||||
loadSkills();
|
|
||||||
navigate('/project-config', { replace: true });
|
navigate('/project-config', { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!activeProject) return <><main data-testid="project-configuration-empty-state" className="min-h-full bg-background p-8 text-foreground"><div className="mx-auto w-full max-w-7xl"><h1 className="text-xl font-semibold">项目配置</h1><p className="mt-4 text-xl font-semibold tracking-[-0.02em]">一念成光,万物可创。</p><p className="mt-2 text-sm font-medium text-muted-foreground">新建或打开一个项目后即可配置模型、提示词、知识与项目能力;插件目录仍可在这里查看。</p><div className="mt-6 max-w-sm"><ResourceCard id="plugins" icon={<Puzzle />} title="插件" subtitle="查看可选与已安装能力" onClick={openPluginWorkspace} /></div></div></main><PluginWorkspaceSheet open={pluginWorkspaceOpen} onClose={closePluginWorkspace} /></>;
|
if (!activeProject) return <><main data-testid="project-configuration-empty-state" className="min-h-full bg-background p-8 text-foreground"><div className="mx-auto w-full max-w-7xl"><h1 className="text-xl font-semibold">项目配置</h1><p className="mt-4 text-xl font-semibold tracking-[-0.02em]">一念成光,万物可创。</p><p className="mt-2 text-sm font-medium text-muted-foreground">新建或打开一个项目后即可配置模型、知识与项目能力;插件目录仍可在这里查看。</p><div className="mt-6 max-w-sm"><ResourceCard id="plugins" icon={<Puzzle />} title="插件" subtitle="查看可选与已安装能力" onClick={openPluginWorkspace} /></div></div></main><PluginWorkspaceSheet open={pluginWorkspaceOpen} onClose={closePluginWorkspace} /></>;
|
||||||
if (!draft) return <div className="p-8 text-center text-sm font-medium">正在读取项目配置…</div>;
|
if (!draft) return <div className="p-8 text-center text-sm font-medium">正在读取项目配置…</div>;
|
||||||
|
|
||||||
const updateAgent = (next: CodingProjectAgent) => setDraft((current) => current ? { ...current, agents: current.agents.map((item) => item.id === next.id ? { ...next, updatedAt: new Date().toISOString() } : item) } : current);
|
const updateAgent = (next: CodingProjectAgent) => setDraft((current) => current ? { ...current, agents: current.agents.map((item) => item.id === next.id ? { ...next, updatedAt: new Date().toISOString() } : item) } : current);
|
||||||
@@ -202,19 +179,12 @@ export function ProjectConfiguration() {
|
|||||||
<section data-testid="project-resource-row" className="grid grid-cols-3 gap-3"><ResourceCard id="models" icon={<Cpu />} title="可用模型" subtitle={`${modelOptions.length} 个可用模型`} onClick={() => setDrawerMode('models')} /><ResourceCard id="knowledge" icon={<Files />} title="知识文件" subtitle={`${knowledge.length} 个知识文件`} onClick={() => setDrawerMode('knowledge')} /><ResourceCard id="plugins" icon={<Puzzle />} title="插件" subtitle="管理云端与本机能力" onClick={openPluginWorkspace} /></section>
|
<section data-testid="project-resource-row" className="grid grid-cols-3 gap-3"><ResourceCard id="models" icon={<Cpu />} title="可用模型" subtitle={`${modelOptions.length} 个可用模型`} onClick={() => setDrawerMode('models')} /><ResourceCard id="knowledge" icon={<Files />} title="知识文件" subtitle={`${knowledge.length} 个知识文件`} onClick={() => setDrawerMode('knowledge')} /><ResourceCard id="plugins" icon={<Puzzle />} title="插件" subtitle="管理云端与本机能力" onClick={openPluginWorkspace} /></section>
|
||||||
{activeAgent && <section className="surface-card rounded-2xl border border-border/70 p-5" data-testid="project-default-settings">
|
{activeAgent && <section className="surface-card rounded-2xl border border-border/70 p-5" data-testid="project-default-settings">
|
||||||
<h2 className="text-lg font-semibold">{activeAgent.id === draft.defaultAgentId ? '默认编程配置' : '历史会话编程配置'}</h2>
|
<h2 className="text-lg font-semibold">{activeAgent.id === draft.defaultAgentId ? '默认编程配置' : '历史会话编程配置'}</h2>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">新会话使用项目默认配置;历史会话保留原来的配置与记录。</p>
|
<p className="mt-1 text-sm text-muted-foreground">默认模型用于新对话;已有对话的模型和消息记录保持不变。</p>
|
||||||
<label className="mt-4 block text-sm">默认模型<select className="mt-2 block w-full rounded-lg border bg-background p-2"
|
<label className="mt-4 block text-sm">默认模型<select className="mt-2 block w-full rounded-lg border bg-background p-2"
|
||||||
value={activeAgent.model ? JSON.stringify([activeAgent.model.accountId, activeAgent.model.modelId]) : ''}
|
value={activeAgent.model ? JSON.stringify([activeAgent.model.accountId, activeAgent.model.modelId]) : ''}
|
||||||
onChange={event => { const model = parseCodingModelKey(event.target.value); updateAgent({...activeAgent, model: model ? {...model, thinkingLevel: 'off', reasoningChoice: {mode: 'default'}} : null, modelResolution: model ? 'resolved' : 'required'}); }}>
|
onChange={event => { const model = parseCodingModelKey(event.target.value); updateAgent({...activeAgent, model: model ? {...model, thinkingLevel: 'off', reasoningChoice: {mode: 'default'}} : null, modelResolution: model ? 'resolved' : 'required'}); }}>
|
||||||
<option value="">发送前选择模型</option>{modelOptions.map(model => <option key={model.key} value={model.key}>{model.modelId}</option>)}
|
<option value="">发送前选择模型</option>{modelOptions.map(model => <option key={model.key} value={model.key}>{model.modelId}</option>)}
|
||||||
</select></label>
|
</select></label>
|
||||||
<label className="mt-4 block text-sm">项目提示词<textarea className="mt-2 min-h-36 w-full rounded-lg border bg-background p-3" value={activeAgent.prompt} onChange={event => updateAgent({...activeAgent,prompt:event.target.value})} /></label>
|
|
||||||
<h3 className="mt-4 text-sm font-medium">项目 Skills</h3>
|
|
||||||
<div className="mt-2 space-y-2">{skills.map(skill => <label key={skill.id} className="flex items-start gap-2 text-sm">
|
|
||||||
<input type="checkbox" checked={activeAgent.skillIds.includes(skill.id)} disabled={!skill.available}
|
|
||||||
onChange={event => updateAgent({...activeAgent,skillIds:event.target.checked ? [...activeAgent.skillIds,skill.id] : activeAgent.skillIds.filter(id=>id!==skill.id)})} />
|
|
||||||
<span>{skill.name}<span className="block text-xs text-muted-foreground">{skill.description}</span></span>
|
|
||||||
</label>)}</div>
|
|
||||||
</section>}
|
</section>}
|
||||||
<div data-testid="project-configuration-actions" className="glass-surface config-motion-actions sticky bottom-3 z-10 mt-auto flex shrink-0 flex-col gap-2 rounded-2xl border border-border/70 bg-background/85 p-2 shadow-float sm:flex-row sm:items-start sm:justify-between">
|
<div data-testid="project-configuration-actions" className="glass-surface config-motion-actions sticky bottom-3 z-10 mt-auto flex shrink-0 flex-col gap-2 rounded-2xl border border-border/70 bg-background/85 p-2 shadow-float sm:flex-row sm:items-start sm:justify-between">
|
||||||
<Button type="button" variant="outline" size="sm" onClick={() => setDeleteDialogOpen(true)} className="h-9 w-full border-brand/25 bg-background px-3 font-semibold text-foreground shadow-none hover:border-brand/40 hover:bg-brand-soft sm:w-auto"><Trash2 className="mr-2 h-4 w-4" />删除项目</Button>
|
<Button type="button" variant="outline" size="sm" onClick={() => setDeleteDialogOpen(true)} className="h-9 w-full border-brand/25 bg-background px-3 font-semibold text-foreground shadow-none hover:border-brand/40 hover:bg-brand-soft sm:w-auto"><Trash2 className="mr-2 h-4 w-4" />删除项目</Button>
|
||||||
|
|||||||
@@ -633,7 +633,8 @@ async function installCodingFirstChatHost(
|
|||||||
}
|
}
|
||||||
if (multipleProjects && path === `/api/coding/projects/config?projectId=${otherProject.id}`)
|
if (multipleProjects && path === `/api/coding/projects/config?projectId=${otherProject.id}`)
|
||||||
return respond({ snapshot: { project: otherProject,
|
return respond({ snapshot: { project: otherProject,
|
||||||
config: { ...config, defaultAgentId: agent.id, agents: [{ ...agent, prompt: 'Settings for Another project' }] }, knowledgeFiles: [] } });
|
config: { ...config, defaultAgentId: agent.id, agents: [{ ...agent,
|
||||||
|
model: { ...configuredModel, modelId: 'model-b' }, prompt: 'Settings for Another project' }] }, knowledgeFiles: [] } });
|
||||||
if (multipleProjects && path === `/api/coding/projects/conversations?projectId=${otherProject.id}`)
|
if (multipleProjects && path === `/api/coding/projects/conversations?projectId=${otherProject.id}`)
|
||||||
return respond({ conversations: [{ ...secondConversation, id: 'conversation-other-project', title: 'Other project conversation' }] });
|
return respond({ conversations: [{ ...secondConversation, id: 'conversation-other-project', title: 'Other project conversation' }] });
|
||||||
if (multipleProjects && path === '/api/coding/conversations/conversation-other-project/snapshot')
|
if (multipleProjects && path === '/api/coding/conversations/conversation-other-project/snapshot')
|
||||||
@@ -1114,6 +1115,10 @@ test('project menus target their project and keep new conversations aligned at e
|
|||||||
await archivedMenuItem.click({ trial: true });
|
await archivedMenuItem.click({ trial: true });
|
||||||
await page.screenshot({ path: test.info().outputPath('sidebar-project-menu-peek.png') });
|
await page.screenshot({ path: test.info().outputPath('sidebar-project-menu-peek.png') });
|
||||||
await archivedMenuItem.click();
|
await archivedMenuItem.click();
|
||||||
|
await expect(archivedMenuItem).toHaveCount(0);
|
||||||
|
await expect(conversations.getByRole('button', { name: '返回最近对话', exact: true })).toBeVisible();
|
||||||
|
// Resume pointer tracking after the modal portal has released the page.
|
||||||
|
await primaryMenu.hover();
|
||||||
await page.mouse.move(800, 500);
|
await page.mouse.move(800, 500);
|
||||||
await expect(rail).toHaveAttribute('aria-hidden', 'true');
|
await expect(rail).toHaveAttribute('aria-hidden', 'true');
|
||||||
await sidebarToggle.hover();
|
await sidebarToggle.hover();
|
||||||
@@ -1137,9 +1142,20 @@ test('project menus target their project and keep new conversations aligned at e
|
|||||||
await expect(otherMenu).toBeFocused();
|
await expect(otherMenu).toBeFocused();
|
||||||
await otherMenu.click();
|
await otherMenu.click();
|
||||||
await page.getByRole('menuitem', { name: '项目设置', exact: true }).click();
|
await page.getByRole('menuitem', { name: '项目设置', exact: true }).click();
|
||||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
const configuration = page.getByTestId('project-configuration-page');
|
||||||
await expect(page.getByRole('textbox', { name: '项目提示词', exact: true })).toHaveValue('Settings for Another project');
|
const defaultSettings = configuration.getByTestId('project-default-settings');
|
||||||
|
await expect(configuration).toBeVisible();
|
||||||
|
await expect(configuration.getByRole('heading', { name: '项目配置', exact: true })).toBeVisible();
|
||||||
await expect(otherProject).toHaveAttribute('aria-current', 'page');
|
await expect(otherProject).toHaveAttribute('aria-current', 'page');
|
||||||
|
const defaultModel = defaultSettings.getByRole('combobox', { name: '默认模型', exact: true });
|
||||||
|
await expect(defaultModel).toHaveValue(JSON.stringify(['account-e2e', 'model-b']));
|
||||||
|
await expect(defaultModel).toBeEnabled();
|
||||||
|
await expect(configuration.getByRole('button', { name: '保存项目配置', exact: true })).toBeEnabled();
|
||||||
|
await expect(defaultSettings).toContainText('默认模型用于新对话;已有对话的模型和消息记录保持不变。');
|
||||||
|
await expect(configuration.getByText('项目提示词', { exact: true })).toHaveCount(0);
|
||||||
|
await expect(configuration.getByText(/项目\s*Skills/)).toHaveCount(0);
|
||||||
|
await expect(defaultSettings.locator('textarea, input[type="checkbox"]')).toHaveCount(0);
|
||||||
|
await page.screenshot({ path: test.info().outputPath('project-settings-model-only.png') });
|
||||||
expect((await activationRequests()).at(-1)?.body).toEqual({ projectId: 'project-pi-other' });
|
expect((await activationRequests()).at(-1)?.body).toEqual({ projectId: 'project-pi-other' });
|
||||||
|
|
||||||
// The archive action also validates and activates an inactive project.
|
// The archive action also validates and activates an inactive project.
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ test.describe('Project configuration plugin capabilities', () => {
|
|||||||
}, null, 2));
|
}, null, 2));
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||||
|
await expect(page.getByRole('textbox', { name: '项目提示词', exact: true })).toHaveCount(0);
|
||||||
|
await expect(page.getByRole('heading', { name: '项目 Skills', exact: true })).toHaveCount(0);
|
||||||
await expect(page.getByTestId('project-identity-card')).toHaveCount(0);
|
await expect(page.getByTestId('project-identity-card')).toHaveCount(0);
|
||||||
await expect(page.getByTestId('legacy-project-identity-card')).toHaveCount(0);
|
await expect(page.getByTestId('legacy-project-identity-card')).toHaveCount(0);
|
||||||
await expect(page.getByText(/项目 ID|项目身份|独立副本/u)).toHaveCount(0);
|
await expect(page.getByText(/项目 ID|项目身份|独立副本/u)).toHaveCount(0);
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { ProjectConfiguration } from '@/pages/ProjectConfiguration';
|
||||||
|
import type { ProviderAccount, ProviderVendorInfo } from '@/lib/providers';
|
||||||
|
import type { CodingProjectAgent, CodingProjectConfig, CodingProjectSummary } from '@/types/coding-project';
|
||||||
|
|
||||||
|
const codingState = vi.hoisted(() => ({
|
||||||
|
activeProject: null as CodingProjectSummary | null,
|
||||||
|
load: vi.fn().mockResolvedValue(undefined),
|
||||||
|
removeProject: vi.fn(),
|
||||||
|
}));
|
||||||
|
const configState = vi.hoisted(() => ({
|
||||||
|
configsByProjectId: {} as Record<string, CodingProjectConfig>,
|
||||||
|
knowledgeByProjectId: {} as Record<string, string[]>,
|
||||||
|
load: vi.fn().mockResolvedValue(undefined),
|
||||||
|
save: vi.fn<(projectId: string, config: CodingProjectConfig) => Promise<void>>().mockResolvedValue(undefined),
|
||||||
|
uploadKnowledge: vi.fn(),
|
||||||
|
remove: vi.fn(),
|
||||||
|
}));
|
||||||
|
const providerState = vi.hoisted(() => ({
|
||||||
|
accounts: [] as ProviderAccount[],
|
||||||
|
vendors: [] as ProviderVendorInfo[],
|
||||||
|
refreshProviderSnapshot: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
const getCodingSkills = vi.hoisted(() => vi.fn().mockResolvedValue({ skills: [] }));
|
||||||
|
|
||||||
|
vi.mock('@/stores/coding-workspace', () => ({
|
||||||
|
useCodingWorkspaceStore: (selector: (state: typeof codingState) => unknown) => selector(codingState),
|
||||||
|
}));
|
||||||
|
vi.mock('@/stores/project-config', () => ({
|
||||||
|
useProjectConfigStore: (selector: (state: typeof configState) => unknown) => selector(configState),
|
||||||
|
}));
|
||||||
|
vi.mock('@/stores/providers', () => ({
|
||||||
|
useProviderStore: (selector: (state: typeof providerState) => unknown) => selector(providerState),
|
||||||
|
}));
|
||||||
|
vi.mock('@/lib/coding-product-tools', () => ({ getCodingSkills }));
|
||||||
|
vi.mock('@/pages/Plugins', () => ({ Plugins: () => null }));
|
||||||
|
vi.mock('@/components/works/ProjectPublishAction', () => ({ ProjectPublishAction: () => null }));
|
||||||
|
|
||||||
|
const now = '2026-10-14T00:00:00.000Z';
|
||||||
|
const project: CodingProjectSummary = {
|
||||||
|
id: 'project-config-controls',
|
||||||
|
name: 'Local project',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
lastOpenedAt: now,
|
||||||
|
};
|
||||||
|
const defaultAgent: CodingProjectAgent = {
|
||||||
|
id: 'default-agent',
|
||||||
|
avatarId: 'default',
|
||||||
|
roleName: '伙伴',
|
||||||
|
name: 'Builder',
|
||||||
|
builtIn: false,
|
||||||
|
enabled: true,
|
||||||
|
skillIds: ['project-skill', 'unknown-legacy-skill'],
|
||||||
|
responsibility: { mission: 'Build', owns: [], boundaries: [], collaborators: [], principles: [] },
|
||||||
|
prompt: ' 原有项目提示词\n保留换行与空格。 ',
|
||||||
|
archivedAt: null,
|
||||||
|
pinned: true,
|
||||||
|
model: { accountId: 'account-1', modelId: 'model-default', thinkingLevel: 'off' },
|
||||||
|
modelResolution: 'resolved',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
const historicalAgent: CodingProjectAgent = {
|
||||||
|
...defaultAgent,
|
||||||
|
id: 'historical-agent',
|
||||||
|
name: 'Historical builder',
|
||||||
|
prompt: '历史会话专用提示词\n不要替换为默认项目提示词。',
|
||||||
|
skillIds: ['historical-only-skill'],
|
||||||
|
model: { accountId: 'account-1', modelId: 'model-history', thinkingLevel: 'high' },
|
||||||
|
archivedAt: '2026-10-13T00:00:00.000Z',
|
||||||
|
pinned: false,
|
||||||
|
};
|
||||||
|
const config: CodingProjectConfig = {
|
||||||
|
schemaVersion: 2,
|
||||||
|
projectType: 'custom',
|
||||||
|
initialized: true,
|
||||||
|
defaultAgentId: defaultAgent.id,
|
||||||
|
agents: [defaultAgent, historicalAgent],
|
||||||
|
knowledgeDirectory: 'knowledge',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ProjectConfiguration model-only controls', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
codingState.activeProject = project;
|
||||||
|
configState.configsByProjectId = { [project.id]: structuredClone(config) };
|
||||||
|
providerState.accounts = [{
|
||||||
|
id: 'account-1',
|
||||||
|
vendorId: 'custom',
|
||||||
|
label: 'Work account',
|
||||||
|
authMode: 'api_key',
|
||||||
|
model: 'model-default',
|
||||||
|
fallbackModels: ['model-history', 'model-new'],
|
||||||
|
enabled: true,
|
||||||
|
isDefault: true,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ route: '/project-config', heading: '默认编程配置', target: defaultAgent },
|
||||||
|
{ route: '/project-config?agent=historical-agent', heading: '历史会话编程配置', target: historicalAgent },
|
||||||
|
])('changes only the selected Agent model at $route and preserves hidden configuration', async ({ route, heading, target }) => {
|
||||||
|
render(<MemoryRouter initialEntries={[route]}><ProjectConfiguration /></MemoryRouter>);
|
||||||
|
expect(await screen.findByRole('heading', { name: heading })).toBeVisible();
|
||||||
|
const settings = within(screen.getByTestId('project-default-settings'));
|
||||||
|
const model = settings.getByRole('combobox', { name: '默认模型' });
|
||||||
|
const save = screen.getByRole('button', { name: '保存项目配置' });
|
||||||
|
expect(model).toBeEnabled();
|
||||||
|
expect(model).toHaveValue(JSON.stringify([target.model!.accountId, target.model!.modelId]));
|
||||||
|
expect(save).toBeEnabled();
|
||||||
|
expect(settings.getByText('默认模型用于新对话;已有对话的模型和消息记录保持不变。')).toBeVisible();
|
||||||
|
expect(settings.getAllByRole('combobox')).toHaveLength(1);
|
||||||
|
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('项目提示词')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('项目 Skills')).not.toBeInTheDocument();
|
||||||
|
expect(getCodingSkills).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
fireEvent.change(model, { target: { value: JSON.stringify(['account-1', 'model-new']) } });
|
||||||
|
fireEvent.click(save);
|
||||||
|
|
||||||
|
await waitFor(() => expect(configState.save).toHaveBeenCalledOnce());
|
||||||
|
expect(configState.save).toHaveBeenCalledWith(project.id, {
|
||||||
|
...config,
|
||||||
|
agents: config.agents.map((agent) => agent.id === target.id ? {
|
||||||
|
...agent,
|
||||||
|
model: {
|
||||||
|
accountId: 'account-1',
|
||||||
|
modelId: 'model-new',
|
||||||
|
thinkingLevel: 'off',
|
||||||
|
reasoningChoice: { mode: 'default' },
|
||||||
|
},
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
} : agent),
|
||||||
|
});
|
||||||
|
await waitFor(() => expect(save).toBeEnabled());
|
||||||
|
expect(codingState.load).toHaveBeenCalledOnce();
|
||||||
|
expect(configState.configsByProjectId[project.id]).toEqual(config);
|
||||||
|
expect(getCodingSkills).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user