fix: 协助卡停轮与提交防 409,隐藏配置向导会话,并限制岗位会商产物重复写入

澄清中间件改为 ToolMessage + after_model 停轮,前端提交前取消残留 run;系统/向导 thread_type 从对话列表排除;岗位总结席位加 write_file 上限。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-08-23 20:29:59 +08:00
1 parent 51afebb394
commit b81d200eca
31 files changed
+1459 -592

No files matched your search

@@ -72,6 +72,7 @@ import type { AgentThread, AgentThreadState } from "@/core/threads/types";
import {
bootstrapResumePath,
isBootstrapThread,
isHiddenFromRecentChats,
isThreadFavorite,
isThreadPinned,
sortThreadsPinnedFirst,
@@ -372,17 +373,10 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
const [renameValue, setRenameValue] = useState("");
const [shareThreadId, setShareThreadId] = useState<string | null>(null);
// 排除调度/会商类系统会话,与首页一致;智能体创建(bootstrap)会话保留展示
// (带机器人图标、点击回创建页);置顶优先。
// 排除调度/会商/报告结构&写作配置向导等系统会话,与首页一致;
// 智能体创建(bootstrap)会话保留展示(带机器人图标、点击回创建页);置顶优先。
const visibleThreads = useMemo(
() =>
sortThreadsPinnedFirst(
threads.filter(
(thread) =>
thread.metadata?.thread_type !== "scheduler" &&
thread.metadata?.thread_type !== "roundtable",
),
),
() => sortThreadsPinnedFirst(threads.filter((thread) => !isHiddenFromRecentChats(thread))),
[threads],
);
@@ -5,7 +5,7 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { useThreadsPage } from "@/core/threads/hooks";
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
import { pathOfThread, titleOfThread, isHiddenFromRecentChats, isBootstrapThread } from "@/core/threads/utils";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
@@ -22,12 +22,9 @@ export function AgentRecentChats({ agentId }: { agentId: string }) {
pageSize: RECENT_LIMIT,
metadata: { agent_id: agentId },
});
// 该智能体的会话里同样排除调度 / 引导 / 会商类系统会话。
// 该智能体的会话里同样排除引导 / 系统 / 配置向导会话。
const threads = (data?.threads ?? []).filter(
(th) =>
th.metadata?.thread_type !== "bootstrap" &&
th.metadata?.thread_type !== "scheduler" &&
th.metadata?.thread_type !== "roundtable",
(th) => !isBootstrapThread(th) && !isHiddenFromRecentChats(th),
);
return (
@@ -76,6 +76,7 @@ import {
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
import {
isBootstrapThread,
isHiddenFromRecentChats,
isThreadFavorite,
isThreadPinned,
listPathOfThread,
@@ -226,14 +227,8 @@ export function GeneralQaSidebar() {
const visibleThreads = useMemo(
() =>
// 智能体创建(bootstrap)会话也展示(带机器人图标、点击回创建页);
// 仅排除调度 / 会商类系统会话。
sortThreadsPinnedFirst(
threads.filter(
(thread) =>
thread.metadata?.thread_type !== "scheduler" &&
thread.metadata?.thread_type !== "roundtable",
),
),
// 排除调度 / 会商 / 报告结构&写作配置向导等系统会话。
sortThreadsPinnedFirst(threads.filter((thread) => !isHiddenFromRecentChats(thread))),
[threads],
);
@@ -66,6 +66,7 @@ import {
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
import {
isBootstrapThread,
isHiddenFromRecentChats,
isThreadFavorite,
isThreadPinned,
listPathOfThread,
@@ -193,13 +194,9 @@ export function RecentChatList({
);
// 智能体创建(bootstrap)会话也展示在常用对话里(带机器人图标、点击回创建页);
// 仅排除调度 / 会商类系统会话。
// 排除调度 / 会商 / 报告结构&写作配置向导等系统会话。
const visibleThreads = sortThreadsPinnedFirst(
threads.filter(
(thread) =>
thread.metadata?.thread_type !== "scheduler" &&
thread.metadata?.thread_type !== "roundtable",
),
threads.filter((thread) => !isHiddenFromRecentChats(thread)),
);
if (visibleThreads.length === 0) {
+8 -2
View File
@@ -99,8 +99,14 @@ export function useNotebookChat(
},
});
const send = async (text: string, selectedSourceIds: number[]) => {
if (thread.isLoading || !token) return;
const send = async (
text: string,
selectedSourceIds: number[],
options?: { fromClarification?: boolean },
) => {
if (!token) return;
// 协助卡提交前会先停轮;此时 isLoading 可能仍为 true,不能直接丢弃。
if (thread.isLoading && !options?.fromClarification) return;
// API 需要 document.id,而不是 source.id
const toDocIds = (ids: number[]) =>
ids
+44 -13
View File
@@ -28,6 +28,7 @@ import {
isAbortLikeError,
isTransientStreamTransportError,
} from "./thread-access-error";
import { isHiddenFromRecentChats, shouldHideSystemThreadsFromList } from "./utils";
export type ToolEndEvent = {
name: string;
@@ -210,6 +211,39 @@ export async function stopThreadRun(
}
}
/**
* 协助卡提交前:若上一轮还在流式(弱模型出卡后偶发未停干净),先取消服务端 run
* 并断开本地 SSE,避免 409「已有活跃 run」或用户点了选项却发不出去。
*/
export async function stopActiveThreadBeforeClarificationReply(
threadId: string | null | undefined,
thread: { isLoading?: boolean; stop: () => void | Promise<void> },
): Promise<void> {
let storedRunId: string | null = null;
if (threadId && typeof window !== "undefined") {
try {
storedRunId = normalizeStoredRunId(
window.sessionStorage.getItem(`lg:stream:${threadId}`),
);
} catch {
storedRunId = null;
}
}
if (!thread.isLoading && !storedRunId) return;
try {
await stopThreadRun(threadId);
} catch {
/* best-effort */
}
try {
await Promise.resolve(thread.stop());
} catch {
/* best-effort */
}
// cancel 落地需要一拍,立刻 submit 仍可能 409。
await new Promise((resolve) => setTimeout(resolve, 120));
}
function getStreamErrorMessage(error: unknown): string {
if (typeof error === "string" && error.trim()) {
return error;
@@ -1451,18 +1485,10 @@ export function useThreads(
}
// Safety net on top of the backend's system-thread exclusion: drop
// machine-created threads (scheduled tasks, roundtable workers,
// notebooks) in case an older backend still returns them.
return threads.filter((t) => {
const metadata = t.metadata as Record<string, unknown> | null;
if (metadata?.system) return false;
const threadType = metadata?.thread_type;
return (
threadType !== "notebook" &&
threadType !== "scheduler" &&
threadType !== "roundtable"
);
});
// machine-created / wizard threads (scheduled tasks, report-structure
// setup, writing setup, notebooks, …) in case an older backend still
// returns them.
return threads.filter((t) => !isHiddenFromRecentChats(t));
},
refetchOnWindowFocus: false,
});
@@ -1545,8 +1571,13 @@ export function useThreadsPage(options: {
throw new Error("Failed to load conversations");
}
const rows = (await response.json()) as AgentThread[];
// 与后端 exclude_system 对齐:普通列表再兜一层。调用方显式按
// thread_type / system 取数(笔记本等)时不滤,避免把目标会话藏掉。
const visible = shouldHideSystemThreadsFromList(metadata)
? rows.filter((thread) => !isHiddenFromRecentChats(thread))
: rows;
return {
threads: rows.slice(0, pageSize),
threads: visible.slice(0, pageSize),
hasMore: rows.length > pageSize,
};
},
+38
View File
@@ -91,6 +91,44 @@ export function isBootstrapThread(thread: {
return thread.metadata?.thread_type === "bootstrap";
}
/**
* 不应出现在普通对话列表里的 thread_type。
* 与后端 ``EXCLUDED_SYSTEM_THREAD_TYPES`` 保持同步;bootstrap 不在此列(侧栏仍展示)。
*/
export const HIDDEN_FROM_RECENT_CHAT_TYPES = new Set([
"scheduler",
"roundtable",
"notebook",
"deep_research",
"writing_setup",
"report_structure_setup",
]);
export function isSetupWizardThread(thread: {
metadata?: Record<string, unknown> | null;
}): boolean {
const threadType = thread.metadata?.thread_type;
return threadType === "writing_setup" || threadType === "report_structure_setup";
}
/** 侧栏 / 全部对话应隐藏的系统 / 向导会话(bootstrap 除外,仍会展示)。 */
export function isHiddenFromRecentChats(thread: {
metadata?: Record<string, unknown> | null;
}): boolean {
const metadata = thread.metadata;
if (metadata?.system === true) return true;
const threadType = metadata?.thread_type;
return typeof threadType === "string" && HIDDEN_FROM_RECENT_CHAT_TYPES.has(threadType);
}
/** 调用方显式按 thread_type / system 过滤时,不要再套一层「最近对话」隐藏。 */
export function shouldHideSystemThreadsFromList(
metadata?: Record<string, unknown> | null,
): boolean {
if (!metadata) return true;
return !("thread_type" in metadata || "system" in metadata);
}
/**
* bootstrap 会话点击后回到「创建智能体」页并恢复该会话。该页只挂在
* /page/workspace 下(见 WorkspaceRoutes 的 agents/new/:thread_id),故路径固定,
@@ -26,7 +26,7 @@ import { ThreadContext } from '@/components/workspace/messages/context';
import { extractContentFromMessage, extractReasoningContentFromMessage, hasToolCalls } from '@/core/messages/utils';
import { getAPIClient } from '@/core/api';
import { useModels } from '@/core/models/hooks';
import { useThreadStream } from '@/core/threads/hooks';
import { stopActiveThreadBeforeClarificationReply, useThreadStream } from '@/core/threads/hooks';
import { uuid } from '@/core/utils/uuid';
import { cn } from '@/lib/utils';
@@ -146,7 +146,10 @@ export function WritingSetupChat({
const ensureThreadExists = useCallback(async () => {
if (threadPrepared) return;
await getAPIClient().threads.create({ threadId, metadata: { thread_type: 'writing_setup' } });
await getAPIClient().threads.create({
threadId,
metadata: { thread_type: 'writing_setup', system: true },
});
setThreadPrepared(true);
}, [threadId, threadPrepared]);
@@ -167,15 +170,9 @@ export function WritingSetupChat({
const handleClarificationSubmit = useCallback(
async (text: string) => {
if (thread.isLoading) {
try {
await Promise.resolve(thread.stop());
} catch {
/* still send the answer */
}
}
const trimmed = text.trim();
if (!trimmed) return;
await stopActiveThreadBeforeClarificationReply(threadId, thread);
await ensureThreadExists();
await sendMessage(threadId, { text: trimmed, files: [] }, buildWritingSampleContext(formCtx));
},
@@ -36,6 +36,7 @@ import { Textarea } from "@/components/ui/textarea";
import { MessageList } from "@/components/workspace/messages";
import { useModels } from "@/core/models/hooks";
import type { AgentThreadState } from "@/core/threads";
import { stopActiveThreadBeforeClarificationReply } from "@/core/threads/hooks";
import { cn } from "@/lib/utils";
import { useCanvasGraphContext } from "../../contexts/CanvasGraphContext";
@@ -177,9 +178,10 @@ export function CanvasChatPanel() {
const handleClarificationSubmit = useCallback(
async (answer: string) => {
await stopActiveThreadBeforeClarificationReply(threadId, thread);
await sendMessage(answer, { mode });
},
[sendMessage, mode],
[sendMessage, mode, thread, threadId],
);
const handleModelSelect = useCallback(
+5 -4
View File
@@ -31,7 +31,7 @@ import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-dep
import { useModels } from "@/core/models/hooks";
import { useNotification } from "@/core/notification/hooks";
import { saveLastAgentId, useThreadSettings } from "@/core/settings";
import { stopThreadRun, useThreadStream } from "@/core/threads/hooks";
import { stopActiveThreadBeforeClarificationReply, stopThreadRun, useThreadStream } from "@/core/threads/hooks";
import { textOfMessage } from "@/core/threads/utils";
import { OptType, writeYLog } from "@/core/log/y-log";
import {
@@ -542,11 +542,12 @@ export default function AgentChatPage({
}, [taskAutoStart, taskStartText, isNewThread, handleSubmit]);
const handleClarificationSubmit = useCallback(
(answer: string) => {
async (answer: string) => {
setHasStartedConversation(true);
void sendMessage(threadId, { text: answer, files: [] }, { agent_id });
await stopActiveThreadBeforeClarificationReply(threadId, thread);
await sendMessage(threadId, { text: answer, files: [] }, { agent_id });
},
[sendMessage, threadId, agent_id],
[sendMessage, thread, threadId, agent_id],
);
const handleStop = useCallback(async () => {
+5 -4
View File
@@ -58,7 +58,7 @@ import {
useKnowledgeRagEnabled,
useThreadSettings,
} from "@/core/settings";
import { useThreadStream } from "@/core/threads/hooks";
import { useThreadStream, stopActiveThreadBeforeClarificationReply } from "@/core/threads/hooks";
import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-deposit";
import { useSingleRoundRagMessages } from "@/core/threads/use-single-round-rag";
import { useMultiRoundRagMessages } from "@/core/threads/use-multi-round-rag";
@@ -525,11 +525,12 @@ export default function ChatPage() {
);
const handleClarificationSubmit = useCallback(
(answer: string) => {
async (answer: string) => {
setHasStartedConversation(true);
void sendMessage(threadId, { text: answer, files: [] });
await stopActiveThreadBeforeClarificationReply(threadId, thread);
await sendMessage(threadId, { text: answer, files: [] });
},
[sendMessage, threadId],
[sendMessage, thread, threadId],
);
useEffect(() => {
+5 -4
View File
@@ -9,7 +9,7 @@ import { setStoredAuth } from "@/core/auth";
import { resolveEmbedChatParams } from "@/core/embed/params";
import { registerEmbedSession } from "@/core/embed/status-api";
import { useThreadSettings } from "@/core/settings";
import { useThreadStream } from "@/core/threads/hooks";
import { stopActiveThreadBeforeClarificationReply, useThreadStream } from "@/core/threads/hooks";
import { uuid } from "@/core/utils/uuid";
import { ChatBox } from "@/components/workspace/chats";
import {
@@ -110,10 +110,11 @@ export default function EmbedChatPage() {
}, [authReady, threadId, registerSessionForThread]);
const handleClarificationSubmit = useCallback(
(answer: string) => {
void sendMessage(threadId, { text: answer, files: [] });
async (answer: string) => {
await stopActiveThreadBeforeClarificationReply(threadId, thread);
await sendMessage(threadId, { text: answer, files: [] });
},
[sendMessage, threadId],
[sendMessage, thread, threadId],
);
useEffect(() => {
+5 -4
View File
@@ -40,7 +40,7 @@ import {
useThreadSettings,
} from "@/core/settings";
import { usePluginManagementSettings } from "@/core/system-settings/hooks";
import { useThreadStream } from "@/core/threads/hooks";
import { stopActiveThreadBeforeClarificationReply, useThreadStream } from "@/core/threads/hooks";
import { textOfMessage } from "@/core/threads/utils";
import { env } from "@/env";
import { cn } from "@/lib/utils";
@@ -336,11 +336,12 @@ export default function IframeChatPage() {
);
const handleClarificationSubmit = useCallback(
(answer: string) => {
async (answer: string) => {
setHasStartedConversation(true);
void sendMessage(threadId, { text: answer, files: [] });
await stopActiveThreadBeforeClarificationReply(threadId, thread);
await sendMessage(threadId, { text: answer, files: [] });
},
[sendMessage, threadId],
[sendMessage, thread, threadId],
);
const handleAgentChange = useCallback((agentId: string | null) => {
+15 -3
View File
@@ -53,7 +53,7 @@ import { useI18n } from "@/core/i18n/hooks";
import { useModels } from "@/core/models/hooks";
import { isAgentSkillCandidate } from "@/core/skills";
import { useSkills } from "@/core/skills/hooks";
import { useThreadStream } from "@/core/threads/hooks";
import { stopActiveThreadBeforeClarificationReply, useThreadStream } from "@/core/threads/hooks";
import { uuid } from "@/core/utils/uuid";
import { cn } from "@/lib/utils";
@@ -335,13 +335,25 @@ export default function NewAgentPage() {
// Chat is purely a discussion channel. The agent record is only
// created when the user explicitly clicks Save (top-right). If the
// model proposes a config via setup_agent, our approval handler
// prefills the form locally 鈥?it does NOT persist anything.
// prefills the form locally — it does NOT persist anything.
await ensureThreadExists();
await sendMessage(threadId, { text: trimmed, files: [] });
},
[ensureThreadExists, sendMessage, thread.isLoading, threadId],
);
const handleClarificationSubmit = useCallback(
async (text: string) => {
const trimmed = text.trim();
if (!trimmed) return;
// 协助卡提交不能因 isLoading 直接丢弃——弱模型出卡后可能仍在流式。
await stopActiveThreadBeforeClarificationReply(threadId, thread);
await ensureThreadExists();
await sendMessage(threadId, { text: trimmed, files: [] });
},
[ensureThreadExists, sendMessage, thread, threadId],
);
const handleApproveSetupAgent = useCallback(
async (payload: SetupAgentApprovalPayload) => {
// Approving the model's setup_agent proposal **does not** persist
@@ -425,7 +437,7 @@ export default function NewAgentPage() {
className="size-full"
threadId={threadId}
thread={thread}
onClarificationSubmit={handleChatSubmit}
onClarificationSubmit={handleClarificationSubmit}
onSetupAgentApprove={handleApproveSetupAgent}
onSetupAgentReject={handleRejectSetupAgent}
/>
@@ -25,6 +25,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { useNotebookChat, useNotebookSources, useNotebookThreads, useStudioNotebooks } from "@/core/studio/hooks";
import { stopActiveThreadBeforeClarificationReply } from "@/core/threads/hooks";
import { fetchDocumentByChunk } from "@/core/studio/api";
import type { DocumentWithChunks } from "@/core/studio/api";
import type { SourceItem } from "@/core/studio/types";
@@ -381,12 +382,13 @@ export default function StudioNotebookDetailPage({
// Clarification 卡片的「发送」按钮回调。MessageList 内部把用户选中的选项 +
// 自由文本合并成一段 answer 传过来,这里走和普通发送同一条 send() 链路,
// 把当前选中的来源也一并带上,保持上下文一致。
// 弱模型出卡后可能仍在流式:先 stop 再发,不能因 isStreaming 直接丢弃。
const handleClarificationSubmit = useCallback(
async (answer: string) => {
if (isStreaming) return;
await send(answer, selectedSourceIds);
await stopActiveThreadBeforeClarificationReply(chatThreadId, thread);
await send(answer, selectedSourceIds, { fromClarification: true });
},
[send, selectedSourceIds, isStreaming],
[send, selectedSourceIds, chatThreadId, thread],
);
const handleBackToList = () => {
@@ -87,7 +87,11 @@ import { useLocalSettings } from "@/core/settings/hooks";
import { useModels } from "@/core/models/hooks";
import type { DocumentRewriteStreamEvent, DocumentRewriteSummary } from "@/open-canvas/api/rewrite";
import { useDocumentRewrite } from "@/open-canvas/hooks/useDocumentRewrite";
import { stopThreadRun, useThreadStream } from "@/core/threads/hooks";
import {
stopActiveThreadBeforeClarificationReply,
stopThreadRun,
useThreadStream,
} from "@/core/threads/hooks";
import {
extractContentFromMessage,
extractReasoningContentFromMessage,
@@ -947,9 +951,11 @@ function DeepResearchWorkbenchInner() {
const handleClarificationSubmit = useCallback(
async (text: string) => {
const threadId = session?.runtimeThreadId;
await stopActiveThreadBeforeClarificationReply(threadId, collectorThread);
await sendCollectorMessage(text);
},
[sendCollectorMessage],
[collectorThread, sendCollectorMessage, session?.runtimeThreadId],
);
// The very first research topic: auto-sent into the freshly pre-built
@@ -28,7 +28,7 @@ import { extractContentFromMessage, extractReasoningContentFromMessage, hasToolC
import { getAPIClient } from "@/core/api";
import { useModels } from "@/core/models/hooks";
import { useSkills } from "@/core/skills/hooks";
import { useThreadStream } from "@/core/threads/hooks";
import { stopActiveThreadBeforeClarificationReply, useThreadStream } from "@/core/threads/hooks";
import { uuid } from "@/core/utils/uuid";
import { cn } from "@/lib/utils";
@@ -79,7 +79,10 @@ export function ReportStructureSetupChat({
const ensureThreadExists = useCallback(async () => {
if (threadPrepared) return;
await getAPIClient().threads.create({ threadId, metadata: { thread_type: "report_structure_setup" } });
await getAPIClient().threads.create({
threadId,
metadata: { thread_type: "report_structure_setup", system: true },
});
setThreadPrepared(true);
}, [threadId, threadPrepared]);
@@ -99,15 +102,9 @@ export function ReportStructureSetupChat({
const handleClarificationSubmit = useCallback(
async (text: string) => {
if (thread.isLoading) {
try {
await Promise.resolve(thread.stop());
} catch {
/* best-effort: still send the answer */
}
}
const trimmed = text.trim();
if (!trimmed) return;
await stopActiveThreadBeforeClarificationReply(threadId, thread);
await ensureThreadExists();
await sendMessage(threadId, { text: trimmed, files: [] });
},
File diff suppressed because it is too large. Load diff
@@ -9,7 +9,7 @@ import { MessageList } from "@/components/workspace/messages";
import { ThreadContext } from "@/components/workspace/messages/context";
import { useAgent } from "@/core/agents";
import { useLocalSettings } from "@/core/settings";
import { useThreadStream } from "@/core/threads/hooks";
import { stopActiveThreadBeforeClarificationReply, useThreadStream } from "@/core/threads/hooks";
import type { AgentThreadState } from "@/core/threads/types";
import { textOfMessage } from "@/core/threads/utils";
import { uuid } from "@/core/utils/uuid";
@@ -460,7 +460,10 @@ export function PositionBuiltinAgentChat({
);
const handleSubmit = useCallback(
async (message: PromptInputMessage) => {
async (
message: PromptInputMessage,
options?: { fromClarification?: boolean },
) => {
if (!session) {
toast.warning("请先启动业务链");
return;
@@ -469,7 +472,9 @@ export function PositionBuiltinAgentChat({
toast.info(readOnlyHint || "当前阶段暂不可对话");
return;
}
if (pendingSubmissionRef.current || thread.isLoading) {
if (options?.fromClarification) {
pendingSubmissionRef.current = false;
} else if (pendingSubmissionRef.current || thread.isLoading) {
return;
}
pendingSubmissionRef.current = true;
@@ -540,6 +545,8 @@ export function PositionBuiltinAgentChat({
{
agent_id: submission.agentId,
...(selectedModel ? { model: selectedModel } : {}),
// Summary: one Markdown only. Action-plan needs report + JSON — no cap.
...(kind === "summary" ? { position_artifact_cap: 1 } : {}),
},
{
hiddenContext: {
@@ -683,9 +690,18 @@ export function PositionBuiltinAgentChat({
// Use the built-in agent's regular submit path. This keeps a
// clarification answer on the same persisted thread and retains
// the summary/action-plan preparation and completion lifecycle.
onClarificationSubmit={(answer) =>
handleSubmit({ text: answer, files: [] })
}
onClarificationSubmit={(answer) => {
void (async () => {
await stopActiveThreadBeforeClarificationReply(
boundThreadId ?? displayThreadId,
thread,
);
await handleSubmit(
{ text: answer, files: [] },
{ fromClarification: true },
);
})();
}}
onEditResubmit={(text) => void handleSubmit({ text, files: [] })}
onOpenArtifact={handleOpenMessageArtifact}
/>
@@ -16,6 +16,7 @@ import { MessageList } from "@/components/workspace/messages";
import { useAgent } from "@/core/agents";
import { useLocalSettings } from "@/core/settings";
import {
stopActiveThreadBeforeClarificationReply,
useEditMessage,
useThreadStream,
type ToolStartEvent,
@@ -852,6 +853,9 @@ export function PositionNodeChat({
await sendMessage(boundThreadIdRef.current ?? draftThreadIdRef.current, message, {
agent_id: node.agentId,
task_external_context: taskExternalContext,
// Hard-cap outputs write_file to one deliverable per turn (weak models
// otherwise rewrite the same Markdown and loop). Action-plan is exempt.
position_artifact_cap: 1,
});
} catch (error) {
autoPreviewRunArmedRef.current = false;
@@ -1006,9 +1010,15 @@ export function PositionNodeChat({
// nodes own the actual stream. Route their answer through the
// same submit path as the normal input so handoff context,
// permissions and node lifecycle updates are all preserved.
onClarificationSubmit={(answer) =>
handleSubmit({ text: answer, files: [] })
}
onClarificationSubmit={(answer) => {
void (async () => {
await stopActiveThreadBeforeClarificationReply(
boundThreadId ?? displayThreadId,
thread,
);
await handleSubmit({ text: answer, files: [] });
})();
}}
onEditResubmit={(text) => void handleSubmit({ text, files: [] })}
onEditSave={handleSaveMessageEdit}
messageContentOverrides={messageContentOverrides}
+20
View File
@@ -76,6 +76,26 @@
@custom-variant dark (&:is(.dark *));
@layer utilities {
.report-structure-detail-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.report-structure-detail-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.report-structure-detail-scrollbar::-webkit-scrollbar-thumb {
background: #e2e8f0;
border-radius: 9999px;
}
.report-structure-detail-scrollbar::-webkit-scrollbar-thumb:hover {
background: #cbd5e1;
}
}
@theme {
--font-sans:
"Noto Sans S Chinese", "Noto Sans SC", "Source Han Sans SC", "思源黑体",