|
@@ -1,4 +1,4 @@
|
|
|
-import { useRef, useEffect, useCallback, useState, useMemo, type CSSProperties } from "react"
|
|
|
|
|
|
|
+import { useRef, useEffect, useCallback, useState, useMemo, useDeferredValue, type CSSProperties } from "react"
|
|
|
import { createPortal } from "react-dom"
|
|
import { createPortal } from "react-dom"
|
|
|
import { useTranslation } from "react-i18next"
|
|
import { useTranslation } from "react-i18next"
|
|
|
import { BookOpen, Plus, Trash2, MessageSquare, FileEdit, Drama, ListChecks, ChevronDown, Check, History, ArrowDown } from "lucide-react"
|
|
import { BookOpen, Plus, Trash2, MessageSquare, FileEdit, Drama, ListChecks, ChevronDown, Check, History, ArrowDown } from "lucide-react"
|
|
@@ -140,6 +140,15 @@ import {
|
|
|
type ContextHubResult,
|
|
type ContextHubResult,
|
|
|
type ContextIntent,
|
|
type ContextIntent,
|
|
|
} from "@/lib/context-hub"
|
|
} from "@/lib/context-hub"
|
|
|
|
|
+import { buildAgentHistoryMessages } from "@/lib/context-hub/chapter-body-injection"
|
|
|
|
|
+import {
|
|
|
|
|
+ buildContextUsageSnapshot,
|
|
|
|
|
+ calibrateContextUsageSnapshot,
|
|
|
|
|
+ composeLiveContextUsage,
|
|
|
|
|
+} from "@/lib/context-usage"
|
|
|
|
|
+import { toOpenAITools } from "@/lib/agent/tools-schema"
|
|
|
|
|
+import { getEffectiveMaxContextSize } from "@/lib/llm-providers"
|
|
|
|
|
+import { ContextUsageRing } from "@/components/chat/context-usage-ring"
|
|
|
import { enqueueUserMemoryLearning } from "@/lib/user-memory/learning-service"
|
|
import { enqueueUserMemoryLearning } from "@/lib/user-memory/learning-service"
|
|
|
import { recordLatestUserMemoryFeedback } from "@/lib/user-memory/feedback-service"
|
|
import { recordLatestUserMemoryFeedback } from "@/lib/user-memory/feedback-service"
|
|
|
|
|
|
|
@@ -995,6 +1004,43 @@ export function ChatPanel() {
|
|
|
writingSkills: agentUserWritingSkills,
|
|
writingSkills: agentUserWritingSkills,
|
|
|
mcpCapabilities: agentMcpCapabilities,
|
|
mcpCapabilities: agentMcpCapabilities,
|
|
|
} = useAgentConfig(agentSystemPrompt)
|
|
} = useAgentConfig(agentSystemPrompt)
|
|
|
|
|
+ const deferredReferenceText = useDeferredValue(referenceText)
|
|
|
|
|
+ const liveContextUsage = useMemo(() => {
|
|
|
|
|
+ const historyMessages = selectContextHistoryMessages(
|
|
|
|
|
+ activeMessages.filter((message) => (
|
|
|
|
|
+ (message.role === "user" || message.role === "assistant")
|
|
|
|
|
+ && !message.discarded
|
|
|
|
|
+ && !message.isAgentRunning
|
|
|
|
|
+ )),
|
|
|
|
|
+ activeConversation?.contextSummary?.text,
|
|
|
|
|
+ )
|
|
|
|
|
+ const measuredAt = activeConversation?.lastContextUsage?.measuredAt ?? 0
|
|
|
|
|
+ const pendingToolResultTexts = activeMessages
|
|
|
|
|
+ .filter((message) => message.isAgentRunning)
|
|
|
|
|
+ .flatMap((message) => message.agentToolCalls ?? [])
|
|
|
|
|
+ .filter((call) => (
|
|
|
|
|
+ call.status === "done"
|
|
|
|
|
+ && typeof call.result === "string"
|
|
|
|
|
+ && call.result.trim().length > 0
|
|
|
|
|
+ && (call.finishedAt ?? 0) > measuredAt
|
|
|
|
|
+ ))
|
|
|
|
|
+ .map((call) => call.result)
|
|
|
|
|
+ return composeLiveContextUsage(activeConversation?.lastContextUsage, {
|
|
|
|
|
+ windowTokens: agentConfig?.llmConfig
|
|
|
|
|
+ ? getEffectiveMaxContextSize(agentConfig.llmConfig)
|
|
|
|
|
+ : undefined,
|
|
|
|
|
+ sessionSummaryText: activeConversation?.contextSummary?.text ?? "",
|
|
|
|
|
+ historyTexts: historyMessages.map((message) => message.content),
|
|
|
|
|
+ currentInput: deferredReferenceText,
|
|
|
|
|
+ pendingToolResultTexts,
|
|
|
|
|
+ })
|
|
|
|
|
+ }, [
|
|
|
|
|
+ activeConversation?.contextSummary?.text,
|
|
|
|
|
+ activeConversation?.lastContextUsage,
|
|
|
|
|
+ activeMessages,
|
|
|
|
|
+ agentConfig?.llmConfig,
|
|
|
|
|
+ deferredReferenceText,
|
|
|
|
|
+ ])
|
|
|
const runChapterPlanSelfCheck = useCallback(async (planContent: string, contextPack?: ContextPack | null) => {
|
|
const runChapterPlanSelfCheck = useCallback(async (planContent: string, contextPack?: ContextPack | null) => {
|
|
|
const trimmedPlan = planContent.trim()
|
|
const trimmedPlan = planContent.trim()
|
|
|
if (!trimmedPlan) {
|
|
if (!trimmedPlan) {
|
|
@@ -1113,6 +1159,7 @@ export function ChatPanel() {
|
|
|
}, [closeChapterPlanDialog])
|
|
}, [closeChapterPlanDialog])
|
|
|
|
|
|
|
|
const handleSaveAsChapter = useCallback(async (
|
|
const handleSaveAsChapter = useCallback(async (
|
|
|
|
|
+ messageId: string,
|
|
|
content: string,
|
|
content: string,
|
|
|
toolCalls?: CopyableToolCall[],
|
|
toolCalls?: CopyableToolCall[],
|
|
|
) => {
|
|
) => {
|
|
@@ -1176,6 +1223,11 @@ export function ChatPanel() {
|
|
|
const chapterPath = `${chapterDir}/chapter-${String(targetChapterNumber).padStart(3, "0")}.md`
|
|
const chapterPath = `${chapterDir}/chapter-${String(targetChapterNumber).padStart(3, "0")}.md`
|
|
|
const chapterMarkdown = buildDraftContent(targetChapterNumber, chapterTitle, cleanedContent)
|
|
const chapterMarkdown = buildDraftContent(targetChapterNumber, chapterTitle, cleanedContent)
|
|
|
await writeFile(chapterPath, chapterMarkdown)
|
|
await writeFile(chapterPath, chapterMarkdown)
|
|
|
|
|
+ useChatStore.getState().setMessageChapterRef(messageId, {
|
|
|
|
|
+ chapterNumber: targetChapterNumber,
|
|
|
|
|
+ path: chapterPath,
|
|
|
|
|
+ savedAt: Date.now(),
|
|
|
|
|
+ })
|
|
|
setChapterSaveStatus(`已保存为${chapterTitle}`)
|
|
setChapterSaveStatus(`已保存为${chapterTitle}`)
|
|
|
useWikiStore.getState().setSelectedFile(chapterPath)
|
|
useWikiStore.getState().setSelectedFile(chapterPath)
|
|
|
useWikiStore.getState().setFileContent(chapterMarkdown)
|
|
useWikiStore.getState().setFileContent(chapterMarkdown)
|
|
@@ -1740,18 +1792,20 @@ export function ChatPanel() {
|
|
|
const userContent = !effectiveDeAiSkill && deAiMode
|
|
const userContent = !effectiveDeAiSkill && deAiMode
|
|
|
? injectDeAiDirective(rawUserContent, deAiMode)
|
|
? injectDeAiDirective(rawUserContent, deAiMode)
|
|
|
: rawUserContent
|
|
: rawUserContent
|
|
|
|
|
+ const readChapterToolAvailable = !prePluginResult?.enabledToolNames
|
|
|
|
|
+ || prePluginResult.enabledToolNames.includes("read_chapter")
|
|
|
|
|
+ const historyForModel = selectContextHistoryMessages(
|
|
|
|
|
+ activeConvMessages,
|
|
|
|
|
+ contextHubResult?.sessionSummary,
|
|
|
|
|
+ )
|
|
|
|
|
+ const historyMessages = await buildAgentHistoryMessages(historyForModel, {
|
|
|
|
|
+ projectPath: pp,
|
|
|
|
|
+ novelMode,
|
|
|
|
|
+ readChapterToolAvailable,
|
|
|
|
|
+ })
|
|
|
const agentMessages: AgentMessage[] = [
|
|
const agentMessages: AgentMessage[] = [
|
|
|
{ role: "system", content: contextHubSystemContent ?? effectiveSystemPrompt },
|
|
{ role: "system", content: contextHubSystemContent ?? effectiveSystemPrompt },
|
|
|
- ...selectContextHistoryMessages(
|
|
|
|
|
- activeConvMessages,
|
|
|
|
|
- contextHubResult?.sessionSummary,
|
|
|
|
|
- ).map((message) => ({
|
|
|
|
|
- role: message.role,
|
|
|
|
|
- content: message.content,
|
|
|
|
|
- ...(message.reasoning_content !== undefined
|
|
|
|
|
- ? { reasoning_content: message.reasoning_content }
|
|
|
|
|
- : {}),
|
|
|
|
|
- } satisfies AgentMessage)),
|
|
|
|
|
|
|
+ ...historyMessages,
|
|
|
{ role: "user", content: userContent },
|
|
{ role: "user", content: userContent },
|
|
|
]
|
|
]
|
|
|
const sessionRegistry = new ToolRegistry()
|
|
const sessionRegistry = new ToolRegistry()
|
|
@@ -1812,6 +1866,27 @@ export function ChatPanel() {
|
|
|
(tool) => tool.name !== "run_chapter_workflow" && tool.category !== "write",
|
|
(tool) => tool.name !== "run_chapter_workflow" && tool.category !== "write",
|
|
|
)
|
|
)
|
|
|
: agentConfig.tools
|
|
: agentConfig.tools
|
|
|
|
|
+ const advertisedTools = prePluginResult?.enabledToolNames
|
|
|
|
|
+ ? sessionTools.filter((tool) => prePluginResult.enabledToolNames!.includes(tool.name))
|
|
|
|
|
+ : sessionTools
|
|
|
|
|
+ const usageSnapshotBase = buildContextUsageSnapshot({
|
|
|
|
|
+ windowTokens: getEffectiveMaxContextSize(agentConfig.llmConfig),
|
|
|
|
|
+ softwareRules: contextHubResult ? contextHubSoftwareRules : systemPromptForConfig,
|
|
|
|
|
+ toolDefinitionsJson: JSON.stringify(toOpenAITools(advertisedTools)),
|
|
|
|
|
+ stableTokens: contextHubResult?.stats.stableTokens,
|
|
|
|
|
+ summaryTokens: contextHubResult?.stats.summaryTokens,
|
|
|
|
|
+ dynamicTokens: contextHubResult?.stats.dynamicTokens,
|
|
|
|
|
+ historyTexts: historyMessages.map((message) => (
|
|
|
|
|
+ typeof message.content === "string"
|
|
|
|
|
+ ? message.content
|
|
|
|
|
+ : message.content.map((block) => block.type === "text" ? block.text : "").join("")
|
|
|
|
|
+ )),
|
|
|
|
|
+ currentInput: typeof userContent === "string"
|
|
|
|
|
+ ? userContent
|
|
|
|
|
+ : userContent.map((block) => block.type === "text" ? block.text : "").join(""),
|
|
|
|
|
+ })
|
|
|
|
|
+ // Seed this turn's baseline so the ring can grow with tool reads before the first usage report.
|
|
|
|
|
+ useChatStore.getState().setConversationContextUsage(capturedConvId, usageSnapshotBase)
|
|
|
const record = await withWritingWakeLock(keepAwake, () => runAiChatSession({
|
|
const record = await withWritingWakeLock(keepAwake, () => runAiChatSession({
|
|
|
userMessage: plainText,
|
|
userMessage: plainText,
|
|
|
projectPath,
|
|
projectPath,
|
|
@@ -1864,6 +1939,13 @@ export function ChatPanel() {
|
|
|
agentStages: applyAgentActivityEvent(message.agentStages, event),
|
|
agentStages: applyAgentActivityEvent(message.agentStages, event),
|
|
|
}))
|
|
}))
|
|
|
},
|
|
},
|
|
|
|
|
+ onUsage: (usage) => {
|
|
|
|
|
+ if (!streamSessionGuardRef.current.isActive(capturedConvId, sessionId)) return
|
|
|
|
|
+ useChatStore.getState().setConversationContextUsage(
|
|
|
|
|
+ capturedConvId,
|
|
|
|
|
+ calibrateContextUsageSnapshot(usageSnapshotBase, usage),
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
onDone: () => {
|
|
onDone: () => {
|
|
|
if (!streamSessionGuardRef.current.isActive(capturedConvId, sessionId)) return
|
|
if (!streamSessionGuardRef.current.isActive(capturedConvId, sessionId)) return
|
|
|
const finalContent = useChatStore.getState().streamingContents[capturedConvId] ?? ""
|
|
const finalContent = useChatStore.getState().streamingContents[capturedConvId] ?? ""
|
|
@@ -1884,6 +1966,10 @@ export function ChatPanel() {
|
|
|
|
|
|
|
|
if (controller.signal.aborted) return
|
|
if (controller.signal.aborted) return
|
|
|
if (!streamSessionGuardRef.current.isActive(capturedConvId, sessionId)) return
|
|
if (!streamSessionGuardRef.current.isActive(capturedConvId, sessionId)) return
|
|
|
|
|
+ useChatStore.getState().setConversationContextUsage(
|
|
|
|
|
+ capturedConvId,
|
|
|
|
|
+ calibrateContextUsageSnapshot(usageSnapshotBase, record.usage),
|
|
|
|
|
+ )
|
|
|
if (contextHubResult && record.usage) {
|
|
if (contextHubResult && record.usage) {
|
|
|
try {
|
|
try {
|
|
|
const contextHubSnapshot = await persistContextHubProviderUsage(
|
|
const contextHubSnapshot = await persistContextHubProviderUsage(
|
|
@@ -2554,6 +2640,12 @@ export function ChatPanel() {
|
|
|
submitDisabled={concurrencyFull}
|
|
submitDisabled={concurrencyFull}
|
|
|
submitDisabledReason={concurrencyFull ? concurrencyLimitReason : undefined}
|
|
submitDisabledReason={concurrencyFull ? concurrencyLimitReason : undefined}
|
|
|
onStop={handleStop}
|
|
onStop={handleStop}
|
|
|
|
|
+ leftFooterControls={
|
|
|
|
|
+ <ContextUsageRing
|
|
|
|
|
+ usage={liveContextUsage}
|
|
|
|
|
+ onCreateConversation={() => createConversation()}
|
|
|
|
|
+ />
|
|
|
|
|
+ }
|
|
|
rightControls={
|
|
rightControls={
|
|
|
<ChatModelSelector
|
|
<ChatModelSelector
|
|
|
value={aiChatModel}
|
|
value={aiChatModel}
|