diff --git a/.project-docs/30-worklog/tasks/20261012-agent-chat-composer-42b8f6d1.md b/.project-docs/30-worklog/tasks/20261012-agent-chat-composer-42b8f6d1.md new file mode 100644 index 00000000..3efed82f --- /dev/null +++ b/.project-docs/30-worklog/tasks/20261012-agent-chat-composer-42b8f6d1.md @@ -0,0 +1,76 @@ +# Task: Simplify Agent chat and add attachment composer + +## Identity + +- Task ID: 20261012-agent-chat-composer-42b8f6d1 +- Mode: Feature +- Branch: codex/20261012-agent-chat-composer-42b8f6d1-agent-chat-composer +- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261012-agent-chat-composer-42b8f6d1 +- Base commit: 7af43acebb5d8b118fa16c838dfba2f18e535bb1 +- Owner: codex +- Status: Implementing + +## Scope + +- Simplify personal Agent chat into read-only Agent identity, transcript and attachment composer. Remove configuration/save/apply controls, source tabs, toolbar title/billing/version copy and history/new-chat icons from the published conversation. Add inline pending attachments and user bubbles using the existing Makelore visual system. Integrate committed thinking increment from ad1d2f2→4160119 without reverting the current chat/composer/layout. Shrink own Agent list cards to about two-thirds of their former desktop dimensions, retaining usable controls. Update the currently logged-in temporary application with this and inherited Agent/channel changes. + +## Intent And Constraints + +- User explicitly authorizes UI implementation and temporary-app refresh; product language throughout. Configuration remains editable from cards; chat identity is read-only because save/apply controls are removed there. Retain published/preview request identities, recovery, Run/approval/cancellation, existing history and creator billing semantics. Files use Main-owned upload/confirm operations, removal from the composer deselects rather than deleting cloud data. No live model calls, user-file uploads or WeChat operations for verification. +- Isolated feature writes only own source/tests/README/task record and new ignored runtime stage. Prior approved channel/sidebar/config/chat ancestry retained. Do not copy uncommitted peer source or change auth/profile/Main/runtime contract as incidental UI work. + +## Project Context Loaded + +Task context: +- Task: 20261012-agent-chat-composer-42b8f6d1, Feature, owner codex. +- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261012-agent-chat-composer-42b8f6d1. +- Branch: codex/20261012-agent-chat-composer-42b8f6d1-agent-chat-composer. +- Base/HEAD at gate: 7af43acebb5d8b118fa16c838dfba2f18e535bb1. Bundled check/start/status passed and exact registry identity independently checked. +- 40 peer records read only: 39 specific, one old undefined child-language record. No blocked semantic dependency. At the original gate thinking task 20261012-cloud-agent-thinking-76a89e3d remained planning without a deliverable; a later explicit delegation now supplies committed 416011943ea306fa0336d044c8a328e500034c01 and requests its thinking-only increment from ad1d2f2. Commit and record read; semantic three-way integration preserves this task’s layout, attachments and generation guards. No uncommitted peer source read/copied. + +Read: +- AGENTS, read-before-planning, memory index, own record, positioning/current integrated snapshot, decision index, system overview/community collaboration, domain/success criteria, personal-cloud-agent ADR, evidence/reflection/commitment indexes. Large integrated historical snapshots read with targeted current/relevant sections; historical UI descriptions defer to this explicit user direction. +- CloudChat, DraftEditor, CloudFiles, local chat CSS, typed attachment APIs and Main upload boundary; focused unit and Electron specs. Design skill search reviewed, adapting only accessible progressive disclosure to existing cream/pastel/orange palette, not its suggested new font/pink/motion scheme. + +Relevant understanding: +- Client supports students/creators and personal Agents; no courses. Main owns network/auth/upload, Yuxi owns cloud requests/history and WS owns billing. Removing visible billing/version copy does not alter payment or execution. +- User wants a calm conversation page without configuration/management toolbars; the list card remains the configuration entry. Preserve loaded conversation and approvals while simplifying presentation. Image/file picking uses the existing native upload boundary; do not fake image URLs or bypass Host API. +- Current temporary Main PID63739/Host62867, bootstrap Entry Trial, renderer from verified WeChat task d823ddb. New stage must explicitly understand its live dist symlink and preserve the prior renderer for rollback. Final source clean commit/build/Main byte checks plus actual visible CUA verification required. +- Likely files: DraftEditor, CloudChat/local CSS, new attachment-composer component, focused checks/README/own record. Canonical docs read-only, feature promotion proposals only. No unresolved product conflict. + +Gate result: +- Passed. + +## Plan + +1. Separate chat identity from configuration controls, remove source/management toolbar from published chat while retaining preview-only maintenance behavior used elsewhere. +2. Integrate file/image picking, pending file cards and safe per-message removal inside the composer; user messages use distinct rounded bubbles. Keep loading/errors/running/approval and safe send/retry behavior. +3. Run relevant unit, typecheck/scoped lint/build and production Electron checks at large/compact sizes. Commit the clean reviewed result, refresh the known trial renderer with rollback and verify the visible updated conversation; record actual evidence and limits. + +## Outcome + +- Published chat has read-only saved identity and return control. Configuration status/actions, source tabs, routine billing/version title and published history/new-chat controls are removed; card adjustment entry and explicit save/apply behavior remain. +- User messages use lilac bubbles. The composer directly adds native-picked files/images, shows pending cards and deselects without deleting. Remote confirmation failures preserve uploaded object/thread/method for explicit retry. Pending confirmation blocks sending; attachment-only input uses the explicit query “请看看这些附件。”. No fabricated image URL contract or new Main endpoint. +- Preview maintenance controls retain safe behavior; attachment createThread late responses cannot change a newer Agent/thread. Requests, version binding, history recovery, approval and stop boundaries remain. +- Home cards measured at 1440px from 386.7×323.5 to 256×221 (66.2% width, 68.3% height); 48px avatar and 44px operation targets, no transform scaling. Narrow windows adapt, longer descriptions clamp to two lines. +- Committed thinking-only increment from ad1d2f2→4160119 integrated without older layout changes. Separate reasoning block, exact Run/main-thread filtering, replay deduplication and bounded final-history hydration preserved. Added explicit execution epoch for same-Run approval continuation and guarded late approval callbacks; no cross-view overwrites. Durable history depends on the Yuxi API patch not yet deployed. +- Temporary application refresh pending clean source binding and background activation. Native activation/clicks stopped after latest no-focus direction; visible reload will be handed to the user. + +## Verification + +- Pinned pnpm typecheck passed; scoped source/test lint passed with two pre-existing CloudChat warnings (export/cleanup-ref), no errors. git diff --check passed. +- Delegated relevant page/start-use/workflow tests: 61 passed. Attachment component tests: 14 passed. Root ran entry-navigation/avatar/guidance/management: 28 passed. +- Browser layout fixtures: 10 passed at 375/768/1024/1440, long Markdown independently scrolls; inline image-type cards, deselection, approval and stop verified. Root viewed desktop inline attachment and compact bubble screenshots; adjusted compact send alignment to right. +- Card fixture measurements and screenshots inspected at 375/768/1024/1440, no overflow or operation wrapping, long/empty introduction handled. +- Final combined production Vite renderer/Main/preload/utility compile passed. Combined reasoning/stream tests: 14 passed including same-Run continuation and late approval replies; combined workflows 21 passed, headless layout 12 passed (includes two reasoning scenarios). Root viewed compact live reasoning screenshot and confirmed readable content/composer. +- Before the added no-focus constraint, the production Electron identity/configuration/chat spec passed; the old channel spec failed because it still required the intentionally removed WeChat conversation tab. That spec was adapted to current binding UI and linted, but the combined reasoning and adapted channel Electron specs were not rerun because the fixture launches a visible window. This limit is explicit; no production/backend/real-provider claims. No actual uploads, model calls or WeChat mutations performed. +- Read-only native observation after the no-focus direction showed the known logged-in trial on its Agent list with no pending editor/composer. No activation, desktop click or credential/profile read. + +## Follow-ups + +- None recorded. + +## Promotion Candidates + +- Update current personal Agent presentation documentation at integration: published conversation contains identity/transcript/composer only, pending attachments are per-message deselection, recent history continues automatically; home cards use compact 256px desktop width. +- Thinking-only increment of committed 4160119 is now integrated; history recovery depends on Yuxi committed 7b16aef deployment by its owner. No production deployment performed; current stage requires final combined verification. User requests no desktop focus capture during the remaining verification/update; native UI actions are paused and renderer update must stay background. diff --git a/README.md b/README.md index aae8ad06..90ed57eb 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智 - `Makelore Code|AI 编程`:管理本地项目、项目智能体、对话、文件上下文、代码变更和运行时。 会话工作台采用浅蓝项目栏、彩色通用手柄图标与卡片选中态;中间对话/作品使用圆角标签,作品预览的后退、前进、刷新与网址输入统一放在标签右侧;只在作品页显示,窄窗口自动换行。右侧智能体咨询使用奶油色标题区。沿用真实项目和会话入口、侧栏伸缩与账户菜单,样式只作用于 Code 会话工作台。可编辑的空对话在输入区上方的可用区域中央显示童趣欢迎语;「去作品广场逛逛」入口紧贴输入框上方,两者一起位于底部。老师介绍以固定宽度浮在右上角,不占用中间布局;欢迎语和底部输入区始终相对项目栏右侧的整个内容区域居中;小窗口的欢迎内容独立滚动;发送首条消息后收起欢迎区。侧栏保留「作品广场」入口,只有点击后才在系统浏览器打开公共广场,不请求、摘取或内嵌展示广场作品。打开广场保留当前项目、对话、草稿和附件,不自动发送消息;老师求助按钮保留临时应用的最新文案「帮我整体看🧠」。 -- `Makelore Agents|AI 智能体`:从模块首页进入时先打开“我的智能体”卡片列表,保留此前对话与操作恢复记录;返回首页箭头与侧栏折叠按钮同排,折叠后仍可返回;智能体模块的侧栏只通过按钮点击或键盘激活展开,鼠标经过左侧、收纳按钮或顶栏不会展开,切换模块不沿用旧悬停预览;其他模块仍保留原预览方式。“我的智能体”使用浅紫对话机器人图标,“渠道”使用浅蓝连接节点图标。macOS 原生窗口按钮留出安全空间。首页围绕自己的智能体和创建入口,显示童趣问候“Hi,今天想创造还是想聊聊?”,不再显示更多、收到的分享、活动或归档筛选。卡片显示名称、可为空的一句话介绍及状态,支持聊天、调整、继续制作和确认删除;删除沿用云端归档机制,移出列表并停用新调用,历史和费用保留。创建先填写名称并选择平台像素头像或上传头像,下一步进入独立配置。顶部名称有编辑图标,头像可点击修改;未设置头像时使用同一张默认像素头像,从创建到配置、改名、保存和重新打开保持一致。基础页为“介绍与提示词”,一句话介绍可留空,完整提示词保留原始文本;模型作为高级设置第一项必选,未选择时不能开始使用,草稿可分步保存,不自动选模型。介绍与提示词采用浅蓝介绍卡和浅紫提示词卡;高级设置按模型、工具箱(Tool)、做事手册(Skill)、MCP 服务、子智能体和知识库纵向呈现,不再设内部分类切换;不展示 Tool/Skill 解释、资源开放规则提示、云端能力管理、费用上限或运行限制卡片,保存其他配置时保留既有运行限制,费用规则不变。顶层页签切换保留搜索、资源选择、知识表单及未保存输入。配置页移除聊天、自动任务和使用与接入导航,独立聊天仍从卡片进入,微信账号仍在侧栏渠道管理。草稿明确保存(Ctrl/⌘+S);保存只更新草稿,“应用最新修改”使新配置生效,失败重试保持原操作身份;点击“开始使用”完成必要的私有启用后直接打开聊天,不再展示二次确认页;未保存修改、缺少模型、冲突或尚未确认的请求会留在配置页处理,打开聊天不自动发送消息。独立对话沿用奶油底色和浅蓝、浅紫、薄荷色:空对话展示当前智能体头像与名称,示例只填入草稿;用户消息和智能体回复区分呈现,长 Markdown、表格和代码在消息区内滚动,底部输入框、附件、停止和历史入口保留;短窗口中,配置完全生效且没有待处理问题时收起重复的已保存/已应用提示和禁用按钮,修改或待确认时恢复;“调整智能体”与客户端/微信来源同排,窄窗口会换行,未发送内容的离开保护不变。头像上传、Main-owned Host API、访问隔离、费用与不可变版本规则保持;介绍留空需要配套 Yuxi DraftCreate/DraftSave 的空字符串契约。 +- `Makelore Agents|AI 智能体`:从模块首页进入时先打开“我的智能体”卡片列表,保留此前对话与操作恢复记录;返回首页箭头与侧栏折叠按钮同排,折叠后仍可返回;智能体模块的侧栏只通过按钮点击或键盘激活展开,鼠标经过左侧、收纳按钮或顶栏不会展开,切换模块不沿用旧悬停预览;其他模块仍保留原预览方式。“我的智能体”使用浅紫对话机器人图标,“渠道”使用浅蓝连接节点图标。macOS 原生窗口按钮留出安全空间。首页围绕自己的智能体和创建入口,显示童趣问候“Hi,今天想创造还是想聊聊?”,不再显示更多、收到的分享、活动或归档筛选。卡片采用更紧凑的尺寸与留白,显示名称、可为空的一句话介绍及状态,支持聊天、调整、继续制作和确认删除;删除沿用云端归档机制,移出列表并停用新调用,历史和费用保留。创建先填写名称并选择平台像素头像或上传头像,下一步进入独立配置。顶部名称有编辑图标,头像可点击修改;未设置头像时使用同一张默认像素头像,从创建到配置、改名、保存和重新打开保持一致。基础页为“介绍与提示词”,一句话介绍可留空,完整提示词保留原始文本;模型作为高级设置第一项必选,未选择时不能开始使用,草稿可分步保存,不自动选模型。介绍与提示词采用浅蓝介绍卡和浅紫提示词卡;高级设置按模型、工具箱(Tool)、做事手册(Skill)、MCP 服务、子智能体和知识库纵向呈现,不再设内部分类切换;不展示 Tool/Skill 解释、资源开放规则提示、云端能力管理、费用上限或运行限制卡片,保存其他配置时保留既有运行限制,费用规则不变。顶层页签切换保留搜索、资源选择、知识表单及未保存输入。配置页移除聊天、自动任务和使用与接入导航,独立聊天仍从卡片进入,微信账号仍在侧栏渠道管理。草稿明确保存(Ctrl/⌘+S);保存只更新草稿,“应用最新修改”使新配置生效,失败重试保持原操作身份;点击“开始使用”完成必要的私有启用后直接打开聊天,不再展示二次确认页;未保存修改、缺少模型、冲突或尚未确认的请求会留在配置页处理,打开聊天不自动发送消息。独立对话沿用奶油底色和浅蓝、浅紫、薄荷色:顶部只展示返回、只读头像和名称,不展示调整、保存/应用状态、客户端/微信来源、费用/版本说明或历史/新对话图标;配置由列表卡片的“调整”进入。空对话展示当前智能体头像与名称,示例只填入草稿;用户消息使用浅紫圆角气泡,助手 Markdown、表格和代码在消息区内滚动。文件或图片从输入框内添加,上传成功后以待发送卡片呈现,可移除本次携带;移除不会删除云端文件,添加不会自动发送。确认失败沿用同一文件和对话重试,发送前需完成或取消添加;支持仅发送附件,使用明确的附件提问。对话文件按需展开查看和选择,生成文件仍可下载;最近对话自动恢复、审批、停止和未发送内容的离开保护保留。模型返回的思考独立于正文展示,运行时可展开、手动折叠,完成后折叠;无思考内容时不显示。重连事件去重并隔离当前运行与主对话,终态历史未到达时保留已收到的片段并有限补读;服务端返回的最终回答思考可在历史中回看,不进入后续提问。历史回看需要 Yuxi API 思考投影补丁上线,当前未部署;实时思考沿用既有事件流。头像上传、Main-owned Host API、访问隔离、费用与不可变版本规则保持;介绍留空需要配套 Yuxi DraftCreate/DraftSave 的空字符串契约。 云知识库的 embedding 模型只由平台管理员在 Yuxi 后台维护:在「智能体管理 → 模型供应商」启用供应商,按供应商要求填写有效的服务地址和 API Key,添加并启用 embedding 模型并填写实际向量维度;embedding 向量化费用由平台承担,不计入用户用量、词元点数或智能体费用上限;one-api 不参与 embedding 配置,Works Square 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。 diff --git a/shared/cloud-agents.ts b/shared/cloud-agents.ts index 52d50e7d..b9bd84ae 100644 --- a/shared/cloud-agents.ts +++ b/shared/cloud-agents.ts @@ -98,6 +98,7 @@ export interface CloudInterrupt { export interface CloudMessage { id: number; role: 'user' | 'assistant' | 'system'; content: string; request_id: string; run_id: string | null; created_at: string; + reasoning_content?: string; } export interface CloudHistory { thread_id: string; messages: CloudMessage[]; run: CloudRun | null; next_offset: number | null; diff --git a/src/pages/CloudAgents/CloudChat.tsx b/src/pages/CloudAgents/CloudChat.tsx index 2003ee3b..028dc000 100644 --- a/src/pages/CloudAgents/CloudChat.tsx +++ b/src/pages/CloudAgents/CloudChat.tsx @@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { cloudAgentsApi } from '@/lib/cloud-agents-api'; import type { CloudHistory, CloudInterrupt, CloudPrompt, CloudRequest, CloudRun, CloudThread, CloudScheduleProposal } from '../../../shared/cloud-agents'; -import { CloudFiles } from './CloudFiles'; +import { CloudComposerAttachments } from './CloudComposerAttachments'; import { CloudAvatar } from './CloudAvatar'; import './desktop-workspace.css'; import './agent-chat.css'; @@ -12,6 +12,7 @@ import ReactMarkdown, { defaultUrlTransform } from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { CloudPendingContext, useCloudPendingState, usePendingCloudInput } from './CloudPending'; import { agentExamples } from './agent-guidance'; +import { consumeCloudChatEvent, type CloudChatStream } from './cloud-chat-stream'; export const cloudStatus = (status: string) => ({ idle: '尚未开始', queued: '等待执行', dispatching: '准备执行', dispatched: '已提交', pending: '准备中', @@ -41,12 +42,19 @@ export function CloudChat({ slug, previewRevision: latestRevision, initialThread const [intent, setIntent] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); - const [liveText, setLiveText] = useState(''); + const [liveReply, setLiveReply] = useState(null); + const streamReply = useRef(null); + const updateLiveReply = useCallback((next: CloudChatStream | null) => { + streamReply.current = next; + setLiveReply(next); + }, []); const [progress, setProgress] = useState(''); const [interrupt, setInterrupt] = useState(null); const [loading, setLoading] = useState(true); const [leaving, setLeaving] = useState(false); const [attachmentIds, setAttachmentIds] = useState([]); + const [attachmentBusy, setAttachmentBusy] = useState(false); + const [executionEpoch, setExecutionEpoch] = useState(0); const childrenPending = useCloudPendingState(); usePendingCloudInput(busy || Boolean(intent) || Boolean(query.trim()) || attachmentIds.length > 0 || childrenPending.hasPending); const generation = useRef(0); @@ -63,11 +71,16 @@ export function CloudChat({ slug, previewRevision: latestRevision, initialThread } if (!alive.current || expectedGeneration !== generation.current) return; setHistory(page); setRun(page.run); setInterrupt(page.run?.interrupt ?? null); + // Replace the preview only after authoritative assistant history arrives. + // Failed/cancelled Runs without a saved answer retain received fragments. + const preview = streamReply.current; + if (preview?.threadId === id && page.run?.agent_run_id === preview.runId && !active(page.run.status) + && page.messages.some(item => item.role === 'assistant' && item.run_id === preview.runId)) updateLiveReply(null); setQueued(page.queued_requests ?? []); setRequest(active(page.run?.status) ? null : page.queued_requests?.[0] ?? null); if (page.run) setThreads(current => current.map(item => item.thread_id === id ? { ...item, run_id: page.run!.agent_run_id, status: !active(page.run!.status) && page.queued_requests?.length ? 'queued' : page.run!.status, unread: false } : item)); - }, []); + }, [updateLiveReply]); useEffect(() => { alive.current = true; const g = ++generation.current; @@ -149,7 +162,7 @@ export function CloudChat({ slug, previewRevision: latestRevision, initialThread }); }, 2000); return () => window.clearInterval(timer); - }, [queued.length, thread, readHistory]); + }, [queued.length, thread, readHistory, executionEpoch]); const runId = request && active(request.status) ? request.run_id ?? undefined : run?.agent_run_id; const running = active(run?.status) || active(request?.status); @@ -162,25 +175,26 @@ export function CloudChat({ slug, previewRevision: latestRevision, initialThread if (!live || g !== generation.current) return; try { const envelope = object(JSON.parse(event.data)); - const payload = object(envelope.payload); - const chunk = object(payload.chunk); const targetThread = thread?.thread_id ?? request?.thread_id; - if (envelope.thread_id && targetThread && envelope.thread_id !== targetThread) return; - for (const item of Array.isArray(payload.items) ? payload.items : [chunk]) { - const semantic = object(object(item).stream_event); - if (semantic.type === 'message_delta' && typeof semantic.content === 'string') setLiveText(v => v + semantic.content); - if (semantic.type === 'tool_call' && typeof semantic.name === 'string') setProgress('正在使用 ' + semantic.name); - } + if (!targetThread) return; + const targetRequest = request?.request_id ?? run?.request_id; + const next = consumeCloudChatEvent(streamReply.current, envelope, event.type, event.lastEventId, runId, targetThread, targetRequest); + if (next !== streamReply.current) updateLiveReply(next); + if (envelope.run_id !== runId || envelope.thread_id !== targetThread + || targetRequest && envelope.request_id && envelope.request_id !== targetRequest) return; + const chunk = object(object(envelope.payload).chunk); + if (next?.toolName) setProgress('正在使用 ' + next.toolName); if (event.type === 'interrupt') setInterrupt(chunk as CloudInterrupt); if (event.type === 'end') { source?.close(); - setLiveText(''); setProgress(''); + setProgress(''); const id = thread?.thread_id ?? request?.thread_id; if (id) void readHistory(id, g).catch(e => { if (live) setError(message(e)); }); } } catch { setError('运行事件读取失败,正在重新读取状态'); } }; - cloudAgentsApi.events(runId).then(stream => { + const cursor = streamReply.current?.runId === runId ? streamReply.current.cursor : '0-0'; + (cursor === '0-0' ? cloudAgentsApi.events(runId) : cloudAgentsApi.events(runId, cursor)).then(stream => { if (!live) { stream.close(); return; } source = stream; for (const name of ['metadata', 'messages', 'custom', 'interrupt', 'end']) stream.addEventListener(name, accept as EventListener); @@ -193,14 +207,39 @@ export function CloudChat({ slug, previewRevision: latestRevision, initialThread if (!live || g !== generation.current) return; setRun(next); if (!active(next.status)) { - source?.close(); setLiveText(''); setProgress(''); setInterrupt(next.interrupt ?? null); + source?.close(); setProgress(''); setInterrupt(next.interrupt ?? null); void readHistory(next.thread_id, g).catch(e => { if (live) setError(message(e)); }); } }).catch(e => { if (live) setError(message(e)); }); }, 5000); return () => { live = false; source?.close(); window.clearInterval(timer); }; - }, [runId, running, thread?.thread_id, request?.thread_id, readHistory]); - useEffect(() => { if (following.current && transcript.current) transcript.current.scrollTop = transcript.current.scrollHeight; }, [history?.messages.length, liveText, interrupt]); + }, [runId, running, thread?.thread_id, request?.thread_id, request?.request_id, run?.request_id, readHistory, updateLiveReply, executionEpoch]); + // Run settlement can arrive before its final message is persisted. Keep + // hydration independent of the running-state poll, and never replay a prompt. + const previewRunId = liveReply?.runId; + const previewThreadId = liveReply?.threadId; + const previewEnded = liveReply?.ended; + const currentRunId = run?.agent_run_id; + const currentRunStatus = run?.status; + useEffect(() => { + if (!previewRunId || !previewThreadId || currentRunId && (currentRunId !== previewRunId || currentRunStatus === 'interrupted') + || !previewEnded && running) return; + let live = true; + let attempts = 0; + let timer: number; + const g = generation.current; + const id = previewThreadId; + const refresh = async () => { + if (!live || g !== generation.current) return; + attempts++; + try { await readHistory(id, g); } + catch (error) { if (live && g === generation.current) setError(message(error)); } + if (live && g === generation.current && attempts < 10) timer = window.setTimeout(() => void refresh(), 2000); + }; + timer = window.setTimeout(() => void refresh(), 2000); + return () => { live = false; window.clearTimeout(timer); }; + }, [previewRunId, previewThreadId, previewEnded, currentRunId, currentRunStatus, running, readHistory, executionEpoch]); + useEffect(() => { if (following.current && transcript.current) transcript.current.scrollTop = transcript.current.scrollHeight; }, [history?.messages.length, liveReply, interrupt]); const stalePreview = previewRevision !== undefined && latestRevision !== undefined && previewRevision !== latestRevision; const rememberThread = (next: CloudThread) => { @@ -208,19 +247,21 @@ export function CloudChat({ slug, previewRevision: latestRevision, initialThread setThreads(current => [next, ...current.filter(item => item.thread_id !== next.thread_id)]); }; const send = async () => { - if (busy || running || thread?.archived || (!intent && (hasUnsavedChanges || stalePreview || !previewReady || !query.trim()))) return; + if (busy || attachmentBusy || childrenPending.hasPending || running || thread?.archived || (!intent && (hasUnsavedChanges || stalePreview || !previewReady || (!query.trim() && !attachmentIds.length)))) return; const input = intent ?? { - request_id: crypto.randomUUID(), thread_id: thread?.thread_id ?? clientId, query: query.trim(), - attachment_file_ids: attachmentIds, + request_id: crypto.randomUUID(), thread_id: thread?.thread_id ?? clientId, query: query.trim() || '请看看这些附件。', + attachment_file_ids: [...attachmentIds], ...(previewRevision === undefined ? {} : { expected_revision: previewRevision }), }; following.current = true; setBusy(true); setIntent(input); setError(''); - const g = generation.current; + let g = generation.current; try { const accepted = await cloudAgentsApi.call(previewRevision === undefined ? 'submit' : 'preview', { slug, ...input }); if (!alive.current || g !== generation.current) return; - setRequest(accepted); setIntent(null); setQuery(''); setLiveText(''); setAttachmentIds([]); + g = ++generation.current; + setExecutionEpoch(value => value + 1); + setRequest(accepted); setIntent(null); setQuery(''); updateLiveReply(null); setProgress(''); setAttachmentIds([]); rememberThread({ thread_id: accepted.thread_id, client_thread_id: clientId, agent_slug: slug, title: input.query.slice(0, 80), mode, draft_revision: previewRevision, updated_at: new Date().toISOString(), run_id: accepted.run_id, status: accepted.status, unread: false }); await readHistory(accepted.thread_id, g); @@ -233,7 +274,7 @@ export function CloudChat({ slug, previewRevision: latestRevision, initialThread if (mode === 'preview') setPreviewRevision(next?.draft_revision ?? revision); const g = ++generation.current; setThread(next); setClientId(next?.client_thread_id ?? crypto.randomUUID()); setHistory(null); - setRequest(null); setQueued([]); setRun(null); setLiveText(''); setInterrupt(null); setError(''); if (!keepQuery) setQuery(''); + setRequest(null); setQueued([]); setRun(null); updateLiveReply(null); setProgress(''); setInterrupt(null); setError(''); if (!keepQuery) setQuery(''); setAttachmentIds([]); if (next) { setLoading(true); try { await readHistory(next.thread_id, g); } catch(e) { setError(message(e)); } finally { setLoading(false); } } }; @@ -258,13 +299,14 @@ export function CloudChat({ slug, previewRevision: latestRevision, initialThread else if (runId) await cloudAgentsApi.call('cancelRun', { run_id: runId }); } catch (e) { setError(message(e)); } finally { setBusy(false); } }; + const approvalGeneration = generation.current; return
-
+ {mode === 'preview' &&

{previewRevision === undefined ? '和助手聊一聊' : '试一试'}

{previewRevision !== undefined &&

当前试用 · 修订 {previewRevision}

}

由创建者支付词元点数{run?.version && ` · 版本 ${run.version}`}

- {headerAction}
+ {headerAction}
{showHistory && threads.length === 0 &&

还没有历史对话

} {showHistory && threads.length > 0 &&