Quellcode durchsuchen

Merge pull request #62 from Mochocyang/cursor/outline-workflow-conversation-display-b1e6

fix(outline): 标准工作流过程重新显示在对话里
darknessomi vor 3 Wochen
Ursprung
Commit
14afe5a39a

+ 29 - 0
src/components/sources/outline-chat-panel.spec.tsx

@@ -459,6 +459,35 @@ describe("OutlineChatPanel controls", () => {
     expect(source).not.toContain("conversations.map((conv) => (")
   })
 
+  it("标准菜单生成补 forceRefresh,收尾把工具过程留在对话里", () => {
+    expect(source).toContain("intentPhase: \"intent_analysis\"")
+    expect(source).toContain("forceRefresh: true")
+    expect(source).toContain("workflowMode: outlineMode")
+    expect(source).toContain("intentPhase: options.intentPhase")
+    expect(source).toContain("标准工作流必须把工具过程留在对话里")
+    expect(source).toContain("message.agentToolCalls?.length ? message.agentToolCalls : hiddenToolCalls")
+    expect(source).toContain("shouldShowToolProcess")
+    expect(source).toContain("historyPlan.showToolProcess")
+  })
+
+  it("顶栏会话 chips 保底可见,阶段徽章不和会话列表抢宽度", () => {
+    expect(source).toContain("min-w-[72px]")
+    expect(source).toContain("topConversations.length > 0")
+    expect(source).toContain("暂无大纲对话")
+    const chipsBlockStart = source.indexOf("{topConversations.length > 0 ? (")
+    const chipsBlock = source.slice(chipsBlockStart, source.indexOf("qmai-outline-history-button"))
+    expect(chipsBlock).toContain("意图分析中")
+    expect(chipsBlock).toContain("outlineWorkflowStage !== \"idle\"")
+    expect(chipsBlockStart).toBeGreaterThan(-1)
+  })
+
+  it("有活跃大纲会话时顶栏显示会话而不是空状态", async () => {
+    setOutlineConversations([conversation()], "outline-active")
+    const container = await renderOutlineChatPanel()
+    expect(container.textContent).toContain("测试大纲会话")
+    expect(container.textContent).not.toContain("暂无大纲对话")
+  })
+
   it("provides one-click clearing for outline conversation history", () => {
     expect(source).toContain('aria-label="一键清理会话历史"')
     expect(source).toContain("requestClearHistory")

+ 25 - 16
src/components/sources/outline-chat-panel.tsx

@@ -2038,14 +2038,18 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
       const hasPriorAssistantAnswer = historyBeforeSend.some(
         (message) => message.role === "assistant" && message.content.trim(),
       );
+      const outlineMode = resolveOutlineWorkflowMode(useWikiStore.getState().outlineWorkflowMode);
+      const enableMultiAgent = Boolean(options.enableMultiAgent) && outlineMode !== "fast";
       const forceRefresh = options.forceRefresh === true || forceRefreshNext;
       const contextDecision = planOutlineContextReuse({
         hasPriorAssistantAnswer,
         attachedReferenceCount: tokens.length,
         inputText: prompt,
-        enableMultiAgent: options.enableMultiAgent,
+        enableMultiAgent,
         forceRefresh,
         systemGenerated: options.systemGenerated,
+        workflowMode: outlineMode,
+        intentPhase: options.intentPhase,
       });
       const cachedSummary =
         contextDecision.mode === "reuse"
@@ -2058,6 +2062,9 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
         history: historyBeforeSend,
         contextDecision,
         cachedSummary,
+        workflowMode: outlineMode,
+        intentPhase: options.intentPhase,
+        enableMultiAgent,
       });
       if (forceRefreshNext) {
         setForceRefreshNext(false);
@@ -2113,8 +2120,6 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
       // 避免整段结果被静默丢弃。
       let bestGeneratedText = "";
       let deliverableTruncated = false;
-      const outlineMode = resolveOutlineWorkflowMode(useWikiStore.getState().outlineWorkflowMode);
-      const enableMultiAgent = Boolean(options.enableMultiAgent) && outlineMode !== "fast";
       const outlineBudgetStage: OutlineBudgetStage = options.intentPhase === "intent_analysis"
         ? "analysis"
         : "generation";
@@ -2933,6 +2938,7 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
           visibleToolCalls.some((call) => call.status === "approval_required" || call.status === "error");
         // 最终内容提交不受 run 状态闸门限制:即使运行状态已被切换/停止,
         // 已生成的结果也必须写入消息,只有后续 UI 副作用才需要闸门。
+        // 标准工作流必须把工具过程留在对话里,不能在收尾时清成空数组。
         updateOutlineAssistantMessage(convId, assistantId, (message) => ({
           ...message,
           content: finalContent,
@@ -2941,7 +2947,9 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
           showThinkingProcess: historyPlan.showThinkingProcess,
           agentToolCalls: shouldShowToolProcess
             ? settleRunningAgentToolCalls(
-                allToolCalls.length ? allToolCalls : message.agentToolCalls,
+                allToolCalls.length
+                  ? allToolCalls
+                  : (message.agentToolCalls?.length ? message.agentToolCalls : hiddenToolCalls),
               )
             : [],
           isAgentRunning: false,
@@ -3171,6 +3179,7 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
         conversationId: capturedConvId,
         intentPhase: "intent_analysis",
         systemGenerated: true,
+        forceRefresh: true,
         userDisplayText: `生成${title}`,
       });
     },
@@ -4437,16 +4446,16 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
                     key={conv.id}
                     onMouseEnter={() => setHoveredConversationId(conv.id)}
                     onMouseLeave={() => setHoveredConversationId(null)}
-                    className={`group flex shrink-0 items-center rounded-full border text-xs transition-colors ${
+                    className={`group flex min-w-[72px] items-center rounded-full border text-xs transition-colors ${
                       isActive
-                        ? "border-primary/40 bg-background text-foreground shadow-sm"
-                        : "border-border bg-background/70 text-muted-foreground hover:bg-accent hover:text-foreground"
+                        ? "shrink border-primary/40 bg-background text-foreground shadow-sm"
+                        : "shrink-0 border-border bg-background/70 text-muted-foreground hover:bg-accent hover:text-foreground"
                     }`}
                   >
                     <button
                       type="button"
                       onClick={() => setActiveConversation(conv.id)}
-                      className="flex items-center gap-2 rounded-full px-3 py-1.5"
+                      className="flex min-w-0 items-center gap-2 rounded-full px-3 py-1.5"
                       title={conv.title}
                     >
                       <ConversationRunStatusIcon state={runStates[conv.id]} />
@@ -4467,6 +4476,14 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
                   </div>
                 );
               })}
+              {outlineWorkflowStage !== "idle" && outlineWorkflowStage !== "saved" ? (
+                <span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
+                  {outlineWorkflowStage === "intent_analysis" ? "意图分析中" :
+                   outlineWorkflowStage === "waiting_user_input" ? "等待选择" :
+                   outlineWorkflowStage === "sufficiency_check" ? "生成中" :
+                   "处理中"}
+                </span>
+              ) : null}
             </div>
           ) : (
             <span className="shrink-0 truncate text-xs text-muted-foreground">
@@ -4474,14 +4491,6 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
             </span>
           )}
         </div>
-        {outlineWorkflowStage !== "idle" && outlineWorkflowStage !== "saved" ? (
-          <span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
-            {outlineWorkflowStage === "intent_analysis" ? "意图分析中" :
-             outlineWorkflowStage === "waiting_user_input" ? "等待选择" :
-             outlineWorkflowStage === "sufficiency_check" ? "生成中" :
-             "处理中"}
-          </span>
-        ) : null}
         <div className="relative shrink-0" ref={historyRef}>
           <button
             ref={historyButtonRef}

+ 69 - 0
src/lib/novel/outline-context-reuse.spec.ts

@@ -5,6 +5,7 @@ import {
   OUTLINE_CONTEXT_REUSE_DISABLED_TOOLS,
   planOutlineAgentHistory,
   planOutlineContextReuse,
+  shouldShowOutlineWorkflowProcess,
 } from "./outline-context-reuse"
 
 describe("AI 大纲上下文复用策略", () => {
@@ -251,4 +252,72 @@ describe("AI 大纲上下文复用策略", () => {
 
     expect(decision.mode).toBe("refresh")
   })
+
+  it("标准模式意图分析/生成即使已有助手回复也刷新并展示工具过程", () => {
+    expect(shouldShowOutlineWorkflowProcess({
+      workflowMode: "standard",
+      intentPhase: "intent_analysis",
+    })).toBe(true)
+    expect(shouldShowOutlineWorkflowProcess({
+      workflowMode: "standard",
+      intentPhase: "generation",
+      enableMultiAgent: false,
+    })).toBe(true)
+    expect(shouldShowOutlineWorkflowProcess({
+      workflowMode: "fast",
+      intentPhase: "generation",
+    })).toBe(false)
+    expect(shouldShowOutlineWorkflowProcess({
+      workflowMode: "standard",
+      intentPhase: undefined,
+    })).toBe(false)
+
+    const decision = planOutlineContextReuse({
+      hasPriorAssistantAnswer: true,
+      attachedReferenceCount: 0,
+      inputText: "3. 读取资料。4. 提取关键内容。5. 生成大纲。",
+      enableMultiAgent: false,
+      systemGenerated: true,
+      workflowMode: "standard",
+      intentPhase: "generation",
+    })
+    expect(decision.mode).toBe("refresh")
+    expect(decision.reason).toContain("标准大纲工作流")
+
+    const plan = planOutlineAgentHistory({
+      history: [
+        { role: "user", content: "先闲聊一句" },
+        { role: "assistant", content: "好的" },
+      ],
+      contextDecision: decision,
+      workflowMode: "standard",
+      intentPhase: "generation",
+    })
+    expect(plan.showToolProcess).toBe(true)
+    expect(plan.showThinkingProcess).toBe(true)
+  })
+
+  it("快速闲聊追问仍隐藏重复工具过程", () => {
+    const decision = planOutlineContextReuse({
+      hasPriorAssistantAnswer: true,
+      attachedReferenceCount: 0,
+      inputText: "继续优化",
+      enableMultiAgent: false,
+      workflowMode: "fast",
+    })
+    expect(decision.mode).toBe("reuse")
+
+    const plan = planOutlineAgentHistory({
+      history: [
+        { role: "user", content: "生成大纲" },
+        { role: "assistant", content: "大纲结果" },
+      ],
+      contextDecision: decision,
+      workflowMode: "fast",
+    })
+    expect(plan.showToolProcess).toBe(false)
+    expect(plan.showThinkingProcess).toBe(false)
+    expect(plan.showToolProcessOnError).toBe(true)
+    expect(plan.sources).toContain("过程: 已隐藏重复工具过程")
+  })
 })

+ 29 - 3
src/lib/novel/outline-context-reuse.ts

@@ -1,3 +1,8 @@
+import {
+  resolveOutlineWorkflowMode,
+  type OutlineWorkflowMode,
+} from "@/lib/agent/workflow-mode"
+
 export const OUTLINE_CONTEXT_REUSE_DISABLED_TOOLS = [
   "read_chapter",
   "read_outline",
@@ -15,6 +20,7 @@ export const OUTLINE_CONTEXT_REUSE_DISABLED_TOOLS = [
 
 export type OutlineContextReuseMode = "refresh" | "reuse"
 export type OutlineContextPressureLevel = "low" | "medium" | "high"
+export type OutlineIntentPhase = "intent_analysis" | "generation" | "waiting_user_input"
 
 export interface OutlineAgentHistoryMessage {
   role: "user" | "assistant" | "tool" | "system"
@@ -30,6 +36,8 @@ export interface OutlineContextReuseInput {
   forceRefresh?: boolean
   /** 标记 prompt 由系统生成(如 buildGenerationPrompt),跳过关键词检测 */
   systemGenerated?: boolean
+  workflowMode?: OutlineWorkflowMode | null
+  intentPhase?: OutlineIntentPhase
 }
 
 export interface OutlineContextReuseDecision {
@@ -45,6 +53,20 @@ export interface OutlineAgentHistoryInput {
   contextDecision: OutlineContextReuseDecision
   cachedSummary?: string
   summaryInSystem?: boolean
+  workflowMode?: OutlineWorkflowMode | null
+  intentPhase?: OutlineIntentPhase
+  enableMultiAgent?: boolean
+}
+
+export function shouldShowOutlineWorkflowProcess(input: {
+  workflowMode?: OutlineWorkflowMode | null
+  intentPhase?: OutlineIntentPhase
+  enableMultiAgent?: boolean
+}): boolean {
+  if (resolveOutlineWorkflowMode(input.workflowMode) !== "standard") return false
+  return input.intentPhase === "intent_analysis"
+    || input.intentPhase === "generation"
+    || input.enableMultiAgent === true
 }
 
 export interface OutlineAgentHistoryPlan {
@@ -75,10 +97,12 @@ const REFRESH_KEYWORD_PATTERN =
 
 export function planOutlineContextReuse(input: OutlineContextReuseInput): OutlineContextReuseDecision {
   const trimmed = input.inputText.trim()
+  const showWorkflowProcess = shouldShowOutlineWorkflowProcess(input)
   const shouldRefresh =
     !input.hasPriorAssistantAnswer ||
     input.forceRefresh === true ||
     input.enableMultiAgent === true ||
+    showWorkflowProcess ||
     input.attachedReferenceCount > 0 ||
     (!input.systemGenerated && REFRESH_KEYWORD_PATTERN.test(trimmed))
 
@@ -148,17 +172,18 @@ export function planOutlineAgentHistory(input: OutlineAgentHistoryInput): Outlin
       : "",
     "不要把工具调用过程、来源列表或内部思考当成新的创作事实;以最终大纲结论为准。",
   ].filter(Boolean).join("\n")
+  const showWorkflowProcess = shouldShowOutlineWorkflowProcess(input)
 
   return {
     level,
     messages,
     instruction,
     sources: [
-      "过程: 已隐藏重复工具过程",
+      showWorkflowProcess ? "过程: 将显示必要工具过程" : "过程: 已隐藏重复工具过程",
       ...(cachedSummary ? ["摘要: 已复用上下文摘要缓存"] : []),
     ],
-    showThinkingProcess: false,
-    showToolProcess: false,
+    showThinkingProcess: showWorkflowProcess,
+    showToolProcess: showWorkflowProcess,
     showToolProcessOnError: true,
   }
 }
@@ -197,6 +222,7 @@ function refreshReason(input: OutlineContextReuseInput): string {
   if (!input.hasPriorAssistantAnswer) return "首次生成需要建立上下文。"
   if (input.forceRefresh) return "用户手动要求强制刷新上下文。"
   if (input.enableMultiAgent) return "固定生成向导或多 Agent 任务需要完整上下文。"
+  if (shouldShowOutlineWorkflowProcess(input)) return "标准大纲工作流需要完整上下文。"
   if (input.attachedReferenceCount > 0) return "本轮带有新的引用资料。"
   if (REFRESH_KEYWORD_PATTERN.test(input.inputText.trim())) {
     return "用户明确要求读取或刷新资料。"