fix: 协助卡停轮与提交防 409,隐藏配置向导会话,并限制岗位会商产物重复写入
澄清中间件改为 ToolMessage + after_model 停轮,前端提交前取消残留 run;系统/向导 thread_type 从对话列表排除;岗位总结席位加 write_file 上限。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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", "思源黑体",
|
||||
|
||||
Reference in new issue
Block a user