fix(agents): make editor settings peer tabs

This commit is contained in:
2026-09-15 14:50:06 +08:00
parent 80e52496d2
commit 868ab54be6
5 changed files with 163 additions and 18 deletions

View File

@@ -0,0 +1,51 @@
# Task: 核对我的要求与高级设置的切换逻辑
## Identity
- Task ID: 20260915-agent-editor-tabs-8c46f6f5
- Mode: Feature
- Branch: codex/20260915-agent-editor-tabs-8c46f6f5-agent-editor-tabs
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260915-agent-editor-tabs-8c46f6f5
- Base commit: 80e52496d2696c3b579c911ee7c07f9588b368a6
- Owner: codex
- Status: Ready for Integration
## Scope
- Implement the user-approved peer “我的要求” / “高级设置” tabs in the personal cloud Agent editor.
- Own DraftEditor, focused unit/Electron regressions, README and this task record. No Main, API, persistence or billing changes.
## Intent And Constraints
- Source inspection established that the former advanced control only revealed category navigation and reset the content to instructions. The user accepted replacing it with a peer tab.
- Concurrent Task Gate passed through bundled check/start/status, first isolating from occupied main, then resuming this same task. Feature identity, absolute worktree, branch and recorded base match the ownership registry. No subagents were created.
- Planning Gate passed. Read the entry instructions, active record, memory index, positioning, integrated current state, decision index, system overview, planning gate, business rules, success criteria, personal cloud Agent ADR and guided UX source record.
- Project Context Loaded: Main-owned Electron product; integrated focus includes guided Agent requirements and knowledge processing. This task changes only local editor presentation. Relevant files are DraftEditor.tsx, cloud-agents-page.test.tsx, cloud-agents.spec.ts and README.md. No additional evidence, reflection or commitment dependency applies.
- Peer inspection used registered task records only. Guided UX and knowledge changes overlap in domain but are integrated; remaining channel diagnosis and historical reviews do not own this tab decision. Historical placeholder records and template positioning remain unknown coordination state, with no concrete dependency on this implementation.
- Preserve basic requirements as the default, saved/preview/published separation, unfinished requirements/resources/knowledge/budget input, independent budget saving and current preview. Use the existing light-system Tabs component.
- The user's approval supersedes the prior advanced-disclosure interaction for this scope; accepted canonical memory remains read-only in feature mode.
## Outcome
- Existing Radix-based Tabs provide peer selection, associated panel semantics, keyboard navigation and a grouped, clearly highlighted selection. Advanced settings initially opens capabilities; capability/knowledge/limit selection is remembered across top-level tab switches. Selecting the active tab does not collapse or reset it.
- One shared panel and ConfigurationFields instance remain mounted. Switching changes the visible category without discarding resource search, knowledge creation, independent budget edits, basic requirements or preview input; switching itself does not save or send.
- Changing the top-level tab resets the shared form scroll to the top so the previous category's scroll offset does not hide the new form heading. Category selection and entered values still remain.
- README describes the final peer-tab behavior. Focused unit and Electron specs cover selection, defaults, repeated selection, remembered categories, hidden navigation, keyboard movement, scroll reset, preserved inputs and existing save/preview flows.
## Verification
- pnpm 10.33.4 matches packageManager; frozen-lockfile install passed without dependency changes.
- Typecheck and scoped ESLint passed; production build:vite passed for Renderer, Main, Preload and utility. Repeated these checks after adding the scroll reset; the final Electron run also passed (1 test, 24.7 seconds), including the explicit real-scroll reset assertions.
- Four focused unit files: 46/47 initially passed; the new test used a string expectation for a numeric input, corrected to numeric 12.5. The affected page file then passed all 19 tests. Combined final focused coverage: 47 tests (19 page + 28 unchanged guidance/workflows/management).
- First Electron cloud-agents.spec.ts run passed with the real desktop shell and test-owned Host API fixture, including arrow-key switching, category memory and preservation of knowledge/budget input. Existing wide/narrow windows, 125%/150% zoom, save/retrial and independent costs/schedules paths passed.
- Visually inspected the advanced and requirements tab screenshots. Both use the existing light palette and grouped selection; the advanced view hides the requirements form.
- Working and committed changes were inspected from the recorded base; only the four intended product/document/test paths and this task record changed. Bundled documentation drift and whitespace checks passed.
- No full repository suite or live cloud/provider requests were needed for this local navigation change. Production deployment, installation and main integration are outside this task's result.
## Follow-ups
- Integrate this task branch through the normal Integration Gate; rebuild/install the client for the currently installed application to receive the change.
## Promotion Candidates
- Target: ADR-2026-09-11-personal-cloud-agents.md Desktop Workspace / Guided First Helper and canonical Agent UI description. Proposal: replace “advanced configuration expands below requirements” with two peer editor tabs; preserve basic-default guidance, advanced category memory, unsaved input and preview separation. Evidence: explicit user acceptance, source implementation, focused unit and Electron regressions. Future impact: later UI work should preserve this navigation model. Semantic conflict: replaces the earlier disclosure description only; Main/cloud/data semantics stay unchanged. Human confirmation: interaction change already approved, no additional product decision needed during normal integration.

View File

@@ -7,7 +7,7 @@
Makelore 是一个面向软件、视觉创作、智能机器人与个人云智能体的 AI 桌面工作台。当前版本为 `2.0.0`,源码提供四个模块入口;云智能体需配套配置 WS/Yuxi 服务。模块入口页按 Agents、Code、Canvas、Robot 排列统一采用横向插画卡片Agents 标语为“打造你想象中的AI助手”。工作区左上角入口点击后返回模块入口页
- `Makelore CodeAI 编程`:管理本地项目、项目智能体、对话、文件上下文、代码变更和运行时。
- `Makelore AgentsAI 智能体`:配置个人云智能体并试聊,发布后正式对话、按账号分享或创建应用 API Key。支持流式回复、审批、排队请求、附件与产物、自动任务和活动历史重新进入时恢复最近智能体与对话未确认操作可按原输入重试。聊天可提出日程建议由创建者核对能力、时间和费用后启用。知识库上传、替换及导入本人会话附件后自动解析并建立索引文档卡片区分排队、解析、索引、可用和失败持续刷新已加载文档失败可原地重试零文本内容不会显示为可用新文档可用后才移除被替换的旧文档。支持删除知识内容个人 MCP 凭据、Skills、子智能体可自助管理。支持智能体和会话归档、发布版本比较及恢复为草稿。除知识库 embedding 向量化外模型调用包括知识处理触发的对话模型调用仍按原规则由创建者个人词元点数支付可按智能体设置每次任务和每日上限费用按时间、来源、应用分页统计embedding 向量化由平台承担,不计入用户用量、词元点数或智能体费用上限;分享/API 调用者保有自己的内容空间。Main 管理云会话和本机文件Renderer 通过 Host API 操作。配套服务接入见 [Yuxi MakeLore 说明](https://xerrors.github.io/Yuxi/advanced/makelore-agents.html)。 编辑采用全高桌面分栏,支持拖动调整与展开、独立滚动、小窗口配置/试用切换。首次创建按选用途、说要求、试一试引导,提供科学问答、故事、英语和笔记整理起点;回答方式可多选,特别要求直接编辑,仍以草稿的 system_prompt 保存。新助手预选云端目录的第一个可用模型并明确显示,可自行更换;已有助手不自动换模型。能力、知识与限制折叠到高级设置。草稿明确保存Ctrl/⌘+S保存与切换页面保留当前试用及未发送文字“保存并重新试用”使用最新保存的要求保留上次试用供对比并可复用上次问题不自动发送。示例问题仅填入输入框有运行、排队、审批或未确认操作时先处理当前试用。工具确认先展示实际操作说明与参数完整技术详情可展开。确认“开始使用”会创建仅自己可用的发布版本并进入独立对话分享与应用仍单独开启。费用上限在限制中独立保存。Enter 发送、Shift+Enter 换行F6 切换面板焦点、Esc 恢复分栏。
- `Makelore AgentsAI 智能体`:配置个人云智能体并试聊,发布后正式对话、按账号分享或创建应用 API Key。支持流式回复、审批、排队请求、附件与产物、自动任务和活动历史重新进入时恢复最近智能体与对话未确认操作可按原输入重试。聊天可提出日程建议由创建者核对能力、时间和费用后启用。知识库上传、替换及导入本人会话附件后自动解析并建立索引文档卡片区分排队、解析、索引、可用和失败持续刷新已加载文档失败可原地重试零文本内容不会显示为可用新文档可用后才移除被替换的旧文档。支持删除知识内容个人 MCP 凭据、Skills、子智能体可自助管理。支持智能体和会话归档、发布版本比较及恢复为草稿。除知识库 embedding 向量化外模型调用包括知识处理触发的对话模型调用仍按原规则由创建者个人词元点数支付可按智能体设置每次任务和每日上限费用按时间、来源、应用分页统计embedding 向量化由平台承担,不计入用户用量、词元点数或智能体费用上限;分享/API 调用者保有自己的内容空间。Main 管理云会话和本机文件Renderer 通过 Host API 操作。配套服务接入见 [Yuxi MakeLore 说明](https://xerrors.github.io/Yuxi/advanced/makelore-agents.html)。 编辑采用全高桌面分栏,支持拖动调整与展开、独立滚动、小窗口配置/试用切换。首次创建按选用途、说要求、试一试引导,提供科学问答、故事、英语和笔记整理起点;回答方式可多选,特别要求直接编辑,仍以草稿的 system_prompt 保存。新助手预选云端目录的第一个可用模型并明确显示,可自行更换;已有助手不自动换模型。编辑区以“我的要求”和“高级设置”两个 Tab 切换,默认显示基础要求;高级设置包含能力、知识与限制,返回时保留上次分类,切换保留未保存的输入及当前试用。草稿明确保存Ctrl/⌘+S保存与切换页面保留当前试用及未发送文字“保存并重新试用”使用最新保存的要求保留上次试用供对比并可复用上次问题不自动发送。示例问题仅填入输入框有运行、排队、审批或未确认操作时先处理当前试用。工具确认先展示实际操作说明与参数完整技术详情可展开。确认“开始使用”会创建仅自己可用的发布版本并进入独立对话分享与应用仍单独开启。费用上限在限制中独立保存。Enter 发送、Shift+Enter 换行F6 切换面板焦点、Esc 恢复分栏。
云知识库的 embedding 模型只由平台管理员在 Yuxi 后台维护:在「智能体管理 → 模型供应商」启用供应商,按供应商要求填写有效的服务地址和 API Key添加并启用 embedding 模型并填写实际向量维度embedding 向量化费用由平台承担不计入用户用量、词元点数或智能体费用上限one-api 不参与 embedding 配置Works Square 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。

View File

@@ -4,6 +4,7 @@ import { ArrowLeft, Check, Loader2, Save } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog';
import { cloudAgentsApi } from '@/lib/cloud-agents-api';
import { EMPTY_CLOUD_CONFIGURATION, type CloudAgentDraft, type CloudRecent, type CloudThread, type CloudScheduleProposal } from '../../../shared/cloud-agents';
@@ -18,7 +19,7 @@ import { AgentRequirements } from './AgentRequirements';
import { agentExamples } from './agent-guidance';
type Tab = 'configuration' | 'chat' | 'tasks' | 'access';
type Category = 'instructions' | 'capabilities' | 'knowledge' | 'limits';
type AdvancedCategory = 'capabilities' | 'knowledge' | 'limits';
const hasDraftChanges = (draft: CloudAgentDraft, saved: CloudAgentDraft) => draft.name !== saved.name || draft.purpose !== saved.purpose || draft.system_prompt !== saved.system_prompt
|| JSON.stringify(draft.configuration) !== JSON.stringify(saved.configuration);
const errorMessage = (error: unknown) => error instanceof Error ? error.message : '暂时无法完成操作';
@@ -29,7 +30,8 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt }
}) {
const [saved, setSaved] = useState(initial);
const [draft, setDraft] = useState(() => startingPrompt === undefined ? initial : { ...initial, system_prompt: startingPrompt });
const [advanced, setAdvanced] = useState(false);
const [editorTab, setEditorTab] = useState('instructions');
const [advancedCategory, setAdvancedCategory] = useState<AdvancedCategory>('capabilities');
const [previewActivation, setPreviewActivation] = useState(0);
const [previewCreated, setPreviewCreated] = useState(Boolean(initial.configuration?.model));
const [remote, setRemote] = useState<CloudAgentDraft | null>(null);
@@ -39,7 +41,7 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt }
const initialTab = recent?.mode === 'preview' ? 'configuration' : initial.published_version ? 'chat' : 'configuration';
const [tab, setTab] = useState<Tab>(initialTab);
const [visited, setVisited] = useState<Set<Tab>>(() => new Set([initialTab]));
const [category, setCategory] = useState<Category>('instructions');
const category = editorTab === 'instructions' ? 'instructions' : advancedCategory;
const [openedThread, setOpenedThread] = useState<CloudThread>();
const [conversationSource, setConversationSource] = useState<'desktop' | 'wechat'>('desktop');
const [wechatVisited, setWechatVisited] = useState(false);
@@ -49,6 +51,7 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt }
const hasPending = pending.hasPending || formalPending.hasPending;
const alive = useRef(true);
const saving = useRef(false);
const configurationScroll = useRef<HTMLDivElement>(null);
const dirty = hasDraftChanges(draft, saved);
const canSave = dirty && !busy && !remote && Boolean(draft.name.trim() && draft.purpose.trim());
const navigate = (next: Tab) => { setVisited(current => new Set([...current, next])); setTab(next); };
@@ -128,14 +131,21 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt }
<Button variant="outline" onClick={() => { setSaved(remote); if (remote.configuration?.model) setPreviewCreated(true); setRemote(null); setError(''); }}></Button></div>
</div>}
<div hidden={tab !== 'configuration'} className="agent-workspace-view">
{visited.has('configuration') && <DesktopSplit previewActivation={previewActivation} configuration={<>
{visited.has('configuration') && <DesktopSplit previewActivation={previewActivation} configuration={<Tabs value={editorTab} onValueChange={next => {
setEditorTab(next);
if (configurationScroll.current) configurationScroll.current.scrollTop = 0;
}} className="flex min-h-0 flex-1 flex-col">
<div className="agent-config-tabs">
<Button variant={category === 'instructions' ? 'secondary' : 'ghost'} onClick={() => setCategory('instructions')}></Button>
<Button variant="ghost" aria-expanded={advanced} onClick={() => { setAdvanced(!advanced); setCategory('instructions'); }}></Button>
<TabsList aria-label="编辑设置" className="h-auto w-full justify-start">
<TabsTrigger value="instructions" className="flex-1 data-[state=active]:text-primary"></TabsTrigger>
<TabsTrigger value="advanced" className="flex-1 data-[state=active]:text-primary"></TabsTrigger>
</TabsList>
</div>
{advanced && <nav aria-label="配置分类" className="agent-config-tabs">{([['capabilities', '能力'], ['knowledge', '知识'], ['limits', '限制']] as const).map(([key, label]) =>
<Button key={key} variant={category === key ? 'secondary' : 'ghost'} aria-current={category === key ? 'page' : undefined} onClick={() => setCategory(key)}>{label}</Button>)}</nav>}
<div className="agent-config-scroll"><fieldset disabled={busy} className="min-w-0">
{/* Keep the shared form mounted so switching tabs preserves unfinished resource and budget edits. */}
<TabsContent value={editorTab} className="m-0 flex min-h-0 flex-1 flex-col">
{editorTab === 'advanced' && <nav aria-label="配置分类" className="agent-config-tabs pt-2">{([['capabilities', '能力'], ['knowledge', '知识'], ['limits', '限制']] as const).map(([key, label]) =>
<Button key={key} variant={advancedCategory === key ? 'secondary' : 'ghost'} aria-current={advancedCategory === key ? 'page' : undefined} onClick={() => setAdvancedCategory(key)}>{label}</Button>)}</nav>}
<div ref={configurationScroll} className="agent-config-scroll"><fieldset disabled={busy} className="min-w-0">
<div hidden={category !== 'instructions'} className="mb-6 space-y-6">
<label className="block space-y-2 text-sm"><span className="font-medium"></span><Textarea aria-label="用途" className="min-h-20 resize-y" maxLength={2000} value={draft.purpose}
onChange={event => setDraft({ ...draft, purpose: event.target.value })} placeholder="一句话介绍,它可以帮你做什么。" /></label>
@@ -144,7 +154,8 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt }
<ConfigurationFields slug={saved.slug} section={category} chooseInitialModel={startingPrompt !== undefined} value={draft.configuration ?? EMPTY_CLOUD_CONFIGURATION} disabled={busy}
onChange={configuration => setDraft({ ...draft, configuration })} />
</fieldset></div>
</>} preview={expandAction => <>
</TabsContent>
</Tabs>} preview={expandAction => <>
{!saved.configuration?.model && !previewCreated ? <div className="my-auto p-6 text-sm leading-7">{expandAction}<h2 className="font-medium"></h2>
<p className="mt-2 text-muted-foreground"></p>
<Button className="mt-4 min-h-10" disabled={!canSave || !draft.configuration?.model} onClick={async () => { if (await save() !== undefined) setPreviewActivation(v => v + 1); }}></Button>

View File

@@ -117,7 +117,29 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
await page.getByRole('button', { name: '保存并开始试用', exact: true }).click();
await expect(page.getByRole('button', { name: '已保存', exact: true })).toBeVisible();
await expect(page.getByTestId('titlebar-page-title')).toHaveText('智能体工作区');
await page.getByRole('button', { name: '高级设置', exact: true }).click();
const requirementsTab = page.getByRole('tab', { name: '我的要求', exact: true });
const advancedTab = page.getByRole('tab', { name: '高级设置', exact: true });
await expect(requirementsTab).toHaveAttribute('aria-selected', 'true');
await expect(page.getByRole('navigation', { name: '配置分类' })).toHaveCount(0);
await requirementsTab.focus();
await requirementsTab.press('ArrowRight');
await expect(advancedTab).toBeFocused();
await expect(advancedTab).toHaveAttribute('aria-selected', 'true');
await expect(page.getByRole('tabpanel', { name: '高级设置' })).toBeVisible();
await expect(page.getByLabel('特别要求')).toBeHidden();
await expect(page.getByRole('button', { name: '能力', exact: true })).toHaveAttribute('aria-current', 'page');
await advancedTab.click();
await expect(advancedTab).toHaveAttribute('aria-selected', 'true');
await page.screenshot({ path: testInfo.outputPath('cloud-agent-advanced-tab.png') });
const configurationScroll = page.getByRole('tabpanel').locator('.agent-config-scroll');
await configurationScroll.evaluate(el => { el.scrollTop = el.scrollHeight; });
await expect.poll(() => configurationScroll.evaluate(el => el.scrollTop)).toBeGreaterThan(0);
await requirementsTab.click();
await expect.poll(() => configurationScroll.evaluate(el => el.scrollTop)).toBe(0);
await expect(page.getByLabel('用途', { exact: true })).toBeInViewport();
await page.screenshot({ path: testInfo.outputPath('cloud-agent-requirements-tab.png') });
await advancedTab.click();
await expect.poll(() => configurationScroll.evaluate(el => el.scrollTop)).toBe(0);
await page.getByRole('button', { name: '知识', exact: true }).click();
await page.getByText('管理我的知识文档', { exact: true }).click();
const knowledgeDescription = page.getByText(/上传到知识库的文档可供/);
@@ -131,6 +153,12 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
await expect(page.getByText(/Yuxi 后台/)).toContainText('服务地址和 API Key');
await expect(page.getByText(/Works Square 运营后台|one-api/)).toHaveCount(0);
await page.getByLabel('知识库名称', { exact: true }).fill('我的创作资料');
await requirementsTab.click();
await expect(page.getByRole('navigation', { name: '配置分类' })).toHaveCount(0);
await expect(page.getByLabel('特别要求')).toHaveValue('使用简洁自然的中文。先理解我的目的,再协助梳理结构。');
await advancedTab.click();
await expect(page.getByRole('button', { name: '知识', exact: true })).toHaveAttribute('aria-current', 'page');
await expect(page.getByLabel('知识库名称', { exact: true })).toHaveValue('我的创作资料');
await page.screenshot({ path: testInfo.outputPath('knowledge-empty.png') });
await page.getByRole('button', { name: '刷新知识库与模型', exact: true }).click();
await page.getByLabel('向量模型', { exact: true }).selectOption('fixture:test-embedding');
@@ -152,7 +180,7 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
await document.scrollIntoViewIfNeeded();
expect(await document.evaluate(el => el.scrollWidth - el.clientWidth)).toBeLessThanOrEqual(1);
await page.screenshot({ path: testInfo.outputPath('knowledge-ready.png') });
await page.getByRole('button', { name: '我的要求', exact: true }).click();
await page.getByRole('tab', { name: '我的要求', exact: true }).click();
const preview = page.getByRole('region', { name: '草稿预览', exact: true });
await preview.getByRole('button', { name: '保存并重新试用', exact: true }).click();
await expect(preview.getByText('当前试用 · 修订 3')).toBeVisible();
@@ -225,10 +253,17 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
await page.getByTestId('ai-module-option-cloud_agents').click();
await expect(page.getByTestId('cloud-agent-editor')).toBeVisible();
await expect(page.getByLabel('特别要求')).toHaveValue('使用简洁自然的中文。先理解我的目的,再协助梳理结构。补充具体例子。');
await page.getByRole('button', { name: '高级设置', exact: true }).click();
await page.getByRole('tab', { name: '高级设置', exact: true }).click();
await page.getByRole('button', { name: '限制', exact: true }).click();
await page.getByLabel('每次任务上限(词元点数)').fill('2.50');
await page.getByLabel('每日上限(词元点数)').fill('10.00');
await advancedTab.press('ArrowLeft');
await expect(requirementsTab).toBeFocused();
await expect(requirementsTab).toHaveAttribute('aria-selected', 'true');
await expect(page.getByLabel('每日上限(词元点数)')).toBeHidden();
await requirementsTab.press('ArrowRight');
await expect(page.getByRole('button', { name: '限制', exact: true })).toHaveAttribute('aria-current', 'page');
await expect(page.getByLabel('每日上限(词元点数)')).toHaveValue('10.00');
await page.getByRole('button', { name: '保存费用上限' }).click();
await expect(page.getByRole('button', { name: '保存费用上限' })).toBeDisabled();
await page.getByRole('button', { name: '使用与分享', exact: true }).click();

View File

@@ -308,6 +308,51 @@ it('keeps unsent preview text when configuration changes and waits before switch
});
it('switches peer editor tabs while retaining the advanced category and unfinished form inputs', async () => {
api.list.mockResolvedValue({ agents: [{ ...draft, configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'test-model' } }], next_cursor: null });
open();
fireEvent.click(await screen.findByRole('button', { name: /写作搭档/ }));
await waitFor(() => expect(screen.getByLabelText('消息')).toBeEnabled());
const requirements = screen.getByRole('tab', { name: '我的要求', exact: true });
const advanced = screen.getByRole('tab', { name: '高级设置', exact: true });
expect(requirements).toHaveAttribute('aria-selected', 'true');
expect(advanced).toHaveAttribute('aria-selected', 'false');
expect(screen.getByRole('tabpanel', { name: '我的要求' })).toBeVisible();
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
const instructions = screen.getByLabelText('特别要求');
const preview = screen.getByLabelText('消息');
fireEvent.change(instructions, { target: { value: '没有保存的要求' } });
fireEvent.change(preview, { target: { value: '没有发送的问题' } });
fireEvent.mouseDown(advanced, { button: 0, ctrlKey: false });
expect(advanced).toHaveAttribute('aria-selected', 'true');
expect(requirements).toHaveAttribute('aria-selected', 'false');
expect(screen.getByRole('tabpanel', { name: '高级设置' })).toBeVisible();
expect(instructions).not.toBeVisible();
expect(screen.getByRole('button', { name: '能力', exact: true })).toHaveAttribute('aria-current', 'page');
fireEvent.click(screen.getByRole('button', { name: '限制', exact: true }));
const budgetInput = screen.getByLabelText('每日上限(词元点数)');
await waitFor(() => expect(budgetInput).toBeEnabled());
fireEvent.change(budgetInput, { target: { value: '12.50' } });
fireEvent.mouseDown(advanced, { button: 0, ctrlKey: false });
expect(screen.getByRole('button', { name: '限制', exact: true })).toHaveAttribute('aria-current', 'page');
expect(budgetInput).toBeVisible();
fireEvent.mouseDown(requirements, { button: 0, ctrlKey: false });
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
expect(budgetInput).not.toBeVisible();
expect(screen.getByLabelText('特别要求')).toBe(instructions);
expect(instructions).toHaveValue('没有保存的要求');
fireEvent.mouseDown(advanced, { button: 0, ctrlKey: false });
expect(screen.getByRole('button', { name: '限制', exact: true })).toHaveAttribute('aria-current', 'page');
expect(screen.getByLabelText('每日上限(词元点数)')).toBe(budgetInput);
expect(budgetInput).toHaveValue(12.5);
expect(screen.getByLabelText('消息')).toBe(preview);
expect(preview).toHaveValue('没有发送的问题');
expect(api.save).not.toHaveBeenCalled();
expect(api.call.mock.calls.some(([operation]) => ['saveBudget', 'preview', 'submit'].includes(operation))).toBe(false);
});
it('keeps preview text across mode switches and saves a budget independently of the draft', async () => {
const configured = { ...draft, configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'test-model' } };
api.list.mockResolvedValue({ agents: [configured], next_cursor: null });
@@ -322,13 +367,13 @@ it('keeps preview text across mode switches and saves a budget independently of
fireEvent.click(screen.getByRole('button', { name: '编辑', exact: true }));
expect(screen.getByLabelText('消息')).toHaveValue('待发送的问题');
expect(screen.getByLabelText('特别要求')).toHaveValue('未保存的指令');
fireEvent.click(screen.getByRole('button', { name: '高级设置' }));
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
fireEvent.click(screen.getByRole('button', { name: '限制', exact: true }));
fireEvent.change(screen.getByLabelText('每日上限(词元点数)'), { target: { value: '12' } });
fireEvent.click(screen.getByRole('button', { name: '保存费用上限' }));
await waitFor(() => expect(api.call).toHaveBeenCalledWith('saveBudget', expect.objectContaining({ daily_limit_points: '12' })));
expect(api.save).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '我的要求', exact: true }));
fireEvent.mouseDown(screen.getByRole('tab', { name: '我的要求', exact: true }), { button: 0, ctrlKey: false });
expect(screen.getByLabelText('特别要求')).toHaveValue('未保存的指令');
expect(screen.getByLabelText('消息')).toHaveValue('待发送的问题');
});
@@ -343,7 +388,7 @@ it('keeps an existing preview when the saved draft clears its model selection',
await waitFor(() => expect(screen.getByLabelText('消息')).toBeEnabled());
const prompt = screen.getByLabelText('消息');
fireEvent.change(prompt, { target: { value: '还没有发送' } });
fireEvent.click(screen.getByRole('button', { name: '高级设置' }));
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
fireEvent.click(screen.getByRole('button', { name: '能力', exact: true }));
fireEvent.change(screen.getByLabelText('模型'), { target: { value: '' } });
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
@@ -364,11 +409,14 @@ it('searches capabilities while retaining existing selections outside the result
api.save.mockImplementation(async (_slug, input) => ({ ...configured, ...input, draft_revision: 2 }));
open();
fireEvent.click(await screen.findByRole('button', { name: /写作搭档/ }));
fireEvent.click(screen.getByRole('button', { name: '高级设置' }));
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
fireEvent.click(screen.getByRole('button', { name: '能力', exact: true }));
expect(await screen.findByRole('checkbox', { name: /missing-tool/ })).toBeChecked();
fireEvent.click(screen.getByRole('button', { name: '选择工具', exact: true }));
fireEvent.change(screen.getByLabelText('搜索工具'), { target: { value: '资料' } });
fireEvent.mouseDown(screen.getByRole('tab', { name: '我的要求' }), { button: 0, ctrlKey: false });
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
expect(screen.getByLabelText('搜索工具')).toHaveValue('资料');
expect(screen.queryByRole('checkbox', { name: '当前时间' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('checkbox', { name: /资料搜索/ }));
fireEvent.click(screen.getByRole('button', { name: '完成选择' }));