重构部分会话消息处理逻辑,优化会话消息渲染
This commit is contained in:
@@ -1,50 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { memo, useMemo } from "react"
|
|
||||||
import { useTranslations } from "next-intl"
|
|
||||||
import type { LiveMessage } from "@/contexts/acp-connections-context"
|
|
||||||
import { ContentPartsRenderer } from "@/components/message/content-parts-renderer"
|
|
||||||
import { adaptLiveMessageFromAcp } from "@/lib/adapters/ai-elements-adapter"
|
|
||||||
import { Message, MessageContent } from "@/components/ai-elements/message"
|
|
||||||
|
|
||||||
interface LiveMessageBlockProps {
|
|
||||||
message: LiveMessage
|
|
||||||
isStreaming?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LiveMessageBlock = memo(function LiveMessageBlock({
|
|
||||||
message,
|
|
||||||
isStreaming = true,
|
|
||||||
}: LiveMessageBlockProps) {
|
|
||||||
const t = useTranslations("Folder.chat.liveMessageBlock")
|
|
||||||
const sharedT = useTranslations("Folder.chat.shared")
|
|
||||||
const hasContent = message.content.length > 0
|
|
||||||
const adapted = useMemo(
|
|
||||||
() =>
|
|
||||||
adaptLiveMessageFromAcp(message, {
|
|
||||||
isLiveStreaming: isStreaming,
|
|
||||||
toolCallFailedText: sharedT("toolCallFailed"),
|
|
||||||
planUpdatedText: sharedT("planUpdated"),
|
|
||||||
}),
|
|
||||||
[message, isStreaming, sharedT]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Message from="assistant">
|
|
||||||
<MessageContent>
|
|
||||||
{hasContent ? (
|
|
||||||
<ContentPartsRenderer parts={adapted.content} role="assistant" />
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-1.5 text-muted-foreground py-1"
|
|
||||||
aria-label={t("assistantThinkingAria")}
|
|
||||||
>
|
|
||||||
<span className="h-2 w-2 rounded-full bg-primary animate-bounce [animation-delay:-0.3s]" />
|
|
||||||
<span className="h-2 w-2 rounded-full bg-primary animate-bounce [animation-delay:-0.15s]" />
|
|
||||||
<span className="h-2 w-2 rounded-full bg-primary animate-bounce" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</MessageContent>
|
|
||||||
</Message>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -540,6 +540,9 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
connStatus={connStatus}
|
connStatus={connStatus}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
sendSignal={sendSignal}
|
sendSignal={sendSignal}
|
||||||
|
sessionStats={detail?.session_stats ?? null}
|
||||||
|
detailLoading={detailLoading}
|
||||||
|
detailError={detailError}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef } from "react"
|
import { memo, useCallback, useEffect, useMemo, useRef } from "react"
|
||||||
import { useDbMessageDetail } from "@/hooks/use-db-message-detail"
|
|
||||||
import { useConversationRuntime } from "@/contexts/conversation-runtime-context"
|
import { useConversationRuntime } from "@/contexts/conversation-runtime-context"
|
||||||
import { ContentPartsRenderer } from "./content-parts-renderer"
|
import { ContentPartsRenderer } from "./content-parts-renderer"
|
||||||
import {
|
import {
|
||||||
adaptMessageTurns,
|
adaptMessageTurns,
|
||||||
type AdaptedContentPart,
|
|
||||||
type MessageGroup,
|
type MessageGroup,
|
||||||
type UserImageDisplay,
|
type UserImageDisplay,
|
||||||
type UserResourceDisplay,
|
type UserResourceDisplay,
|
||||||
groupAdaptedMessages,
|
groupAdaptedMessages,
|
||||||
extractUserResourcesFromText,
|
|
||||||
} from "@/lib/adapters/ai-elements-adapter"
|
} from "@/lib/adapters/ai-elements-adapter"
|
||||||
import { TurnStats } from "./turn-stats"
|
import { TurnStats } from "./turn-stats"
|
||||||
import { LiveTurnStats } from "./live-turn-stats"
|
import { LiveTurnStats } from "./live-turn-stats"
|
||||||
@@ -27,7 +24,7 @@ import {
|
|||||||
buildPlanKey,
|
buildPlanKey,
|
||||||
extractLatestPlanEntriesFromMessages,
|
extractLatestPlanEntriesFromMessages,
|
||||||
} from "@/lib/agent-plan"
|
} from "@/lib/agent-plan"
|
||||||
import type { ConnectionStatus } from "@/lib/types"
|
import type { ConnectionStatus, SessionStats } from "@/lib/types"
|
||||||
import { VirtualizedMessageThread } from "@/components/message/virtualized-message-thread"
|
import { VirtualizedMessageThread } from "@/components/message/virtualized-message-thread"
|
||||||
import { useStickToBottomContext } from "use-stick-to-bottom"
|
import { useStickToBottomContext } from "use-stick-to-bottom"
|
||||||
|
|
||||||
@@ -36,10 +33,12 @@ interface MessageListViewProps {
|
|||||||
connStatus?: ConnectionStatus | null
|
connStatus?: ConnectionStatus | null
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
sendSignal?: number
|
sendSignal?: number
|
||||||
|
sessionStats?: SessionStats | null
|
||||||
|
detailLoading?: boolean
|
||||||
|
detailError?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResolvedMessageGroup extends MessageGroup {
|
interface ResolvedMessageGroup extends MessageGroup {
|
||||||
parts: AdaptedContentPart[]
|
|
||||||
resources: UserResourceDisplay[]
|
resources: UserResourceDisplay[]
|
||||||
images: UserImageDisplay[]
|
images: UserImageDisplay[]
|
||||||
}
|
}
|
||||||
@@ -56,75 +55,6 @@ type ThreadRenderItem =
|
|||||||
kind: "typing"
|
kind: "typing"
|
||||||
}
|
}
|
||||||
|
|
||||||
function fallbackExtractUserResources(
|
|
||||||
group: MessageGroup,
|
|
||||||
attachedResourcesText: string
|
|
||||||
): {
|
|
||||||
parts: AdaptedContentPart[]
|
|
||||||
resources: UserResourceDisplay[]
|
|
||||||
images: UserImageDisplay[]
|
|
||||||
} {
|
|
||||||
if (group.role !== "user") {
|
|
||||||
return {
|
|
||||||
parts: group.parts,
|
|
||||||
resources: group.userResources ?? [],
|
|
||||||
images: group.userImages ?? [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedResources: UserResourceDisplay[] = []
|
|
||||||
const parsedParts: AdaptedContentPart[] = []
|
|
||||||
|
|
||||||
for (const part of group.parts) {
|
|
||||||
if (part.type !== "text") {
|
|
||||||
parsedParts.push(part)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const extracted = extractUserResourcesFromText(part.text)
|
|
||||||
if (extracted.resources.length > 0) {
|
|
||||||
parsedResources.push(...extracted.resources)
|
|
||||||
if (extracted.text.length > 0) {
|
|
||||||
parsedParts.push({ type: "text", text: extracted.text })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
parsedParts.push(part)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resources = [...(group.userResources ?? []), ...parsedResources]
|
|
||||||
const dedupedResources: UserResourceDisplay[] = []
|
|
||||||
const seen = new Set<string>()
|
|
||||||
for (const resource of resources) {
|
|
||||||
const key = `${resource.name}::${resource.uri}`
|
|
||||||
if (seen.has(key)) continue
|
|
||||||
seen.add(key)
|
|
||||||
dedupedResources.push(resource)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedParts.length === 0 && dedupedResources.length > 0) {
|
|
||||||
parsedParts.push({ type: "text", text: attachedResourcesText })
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
parts: parsedParts,
|
|
||||||
resources: dedupedResources,
|
|
||||||
images: group.userImages ?? [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveMessageGroup(
|
|
||||||
group: MessageGroup,
|
|
||||||
attachedResourcesText: string
|
|
||||||
): ResolvedMessageGroup {
|
|
||||||
const resolved = fallbackExtractUserResources(group, attachedResourcesText)
|
|
||||||
return {
|
|
||||||
...group,
|
|
||||||
parts: resolved.parts,
|
|
||||||
resources: resolved.resources,
|
|
||||||
images: resolved.images,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const HistoricalMessageGroup = memo(function HistoricalMessageGroup({
|
const HistoricalMessageGroup = memo(function HistoricalMessageGroup({
|
||||||
group,
|
group,
|
||||||
dimmed = false,
|
dimmed = false,
|
||||||
@@ -200,17 +130,18 @@ export function MessageListView({
|
|||||||
connStatus,
|
connStatus,
|
||||||
isActive = true,
|
isActive = true,
|
||||||
sendSignal = 0,
|
sendSignal = 0,
|
||||||
|
sessionStats = null,
|
||||||
|
detailLoading = false,
|
||||||
|
detailError = null,
|
||||||
}: MessageListViewProps) {
|
}: MessageListViewProps) {
|
||||||
const t = useTranslations("Folder.chat.messageList")
|
const t = useTranslations("Folder.chat.messageList")
|
||||||
const sharedT = useTranslations("Folder.chat.shared")
|
const sharedT = useTranslations("Folder.chat.shared")
|
||||||
const { detail, loading, error } = useDbMessageDetail(conversationId)
|
|
||||||
const { getSession, getTimelineTurns } = useConversationRuntime()
|
const { getSession, getTimelineTurns } = useConversationRuntime()
|
||||||
const session = getSession(conversationId)
|
const session = getSession(conversationId)
|
||||||
const liveMessage = session?.liveMessage ?? null
|
const liveMessage = session?.liveMessage ?? null
|
||||||
const timelineTurns = getTimelineTurns(conversationId)
|
const timelineTurns = getTimelineTurns(conversationId)
|
||||||
|
|
||||||
const { setSessionStats } = useSessionStats()
|
const { setSessionStats } = useSessionStats()
|
||||||
const sessionStats = detail?.session_stats ?? null
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
@@ -218,88 +149,71 @@ export function MessageListView({
|
|||||||
}
|
}
|
||||||
}, [isActive, sessionStats, setSessionStats])
|
}, [isActive, sessionStats, setSessionStats])
|
||||||
|
|
||||||
const shouldUseSmoothResize = !(isActive && !loading && timelineTurns.length)
|
const shouldUseSmoothResize = !(
|
||||||
const attachedResourcesText = sharedT("attachedResources")
|
isActive &&
|
||||||
|
!detailLoading &&
|
||||||
const groupedTimeline = useMemo(
|
timelineTurns.length
|
||||||
() =>
|
|
||||||
timelineTurns.reduce<
|
|
||||||
Array<{
|
|
||||||
phase: "persisted" | "optimistic" | "streaming"
|
|
||||||
turns: typeof timelineTurns
|
|
||||||
}>
|
|
||||||
>((acc, item) => {
|
|
||||||
const current = acc[acc.length - 1]
|
|
||||||
if (current && current.phase === item.phase) {
|
|
||||||
current.turns.push(item)
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
acc.push({
|
|
||||||
phase: item.phase,
|
|
||||||
turns: [item],
|
|
||||||
})
|
|
||||||
return acc
|
|
||||||
}, []),
|
|
||||||
[timelineTurns]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const threadItems = useMemo<ThreadRenderItem[]>(() => {
|
const adapterText = useMemo(
|
||||||
|
() => ({
|
||||||
|
attachedResources: sharedT("attachedResources"),
|
||||||
|
toolCallFailed: sharedT("toolCallFailed"),
|
||||||
|
}),
|
||||||
|
[sharedT]
|
||||||
|
)
|
||||||
|
|
||||||
|
const { threadItems, nonStreamingAdapted } = useMemo(() => {
|
||||||
|
const allTurns = timelineTurns.map((item) => item.turn)
|
||||||
|
const allAdapted = adaptMessageTurns(allTurns, adapterText)
|
||||||
|
|
||||||
|
// Collect non-streaming adapted messages for plan extraction
|
||||||
|
const nonStreaming = allAdapted.filter(
|
||||||
|
(_, index) => timelineTurns[index].phase !== "streaming"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Group adapted messages per phase-chunk to prevent merging
|
||||||
|
// assistant turns across phase boundaries (e.g. persisted + streaming).
|
||||||
const items: ThreadRenderItem[] = []
|
const items: ThreadRenderItem[] = []
|
||||||
for (
|
let chunkStart = 0
|
||||||
let chunkIndex = 0;
|
while (chunkStart < allAdapted.length) {
|
||||||
chunkIndex < groupedTimeline.length;
|
const chunkPhase = timelineTurns[chunkStart].phase
|
||||||
chunkIndex++
|
let chunkEnd = chunkStart + 1
|
||||||
) {
|
while (
|
||||||
const chunk = groupedTimeline[chunkIndex]
|
chunkEnd < allAdapted.length &&
|
||||||
const adapted = adaptMessageTurns(
|
timelineTurns[chunkEnd].phase === chunkPhase
|
||||||
chunk.turns.map((item) => item.turn),
|
) {
|
||||||
{
|
chunkEnd++
|
||||||
attachedResources: sharedT("attachedResources"),
|
}
|
||||||
toolCallFailed: sharedT("toolCallFailed"),
|
const chunkAdapted = allAdapted.slice(chunkStart, chunkEnd)
|
||||||
}
|
const groups = groupAdaptedMessages(chunkAdapted)
|
||||||
)
|
for (let i = 0; i < groups.length; i++) {
|
||||||
const groups = groupAdaptedMessages(adapted).map((group) =>
|
const group = groups[i]
|
||||||
resolveMessageGroup(group, attachedResourcesText)
|
|
||||||
)
|
|
||||||
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
|
||||||
const group = groups[groupIndex]
|
|
||||||
items.push({
|
items.push({
|
||||||
key: `${chunk.phase}-${chunkIndex}-${group.id}-${groupIndex}`,
|
key: `${chunkPhase}-${chunkStart}-${group.id}-${i}`,
|
||||||
kind: "turn",
|
kind: "turn",
|
||||||
group,
|
group: {
|
||||||
phase: chunk.phase,
|
...group,
|
||||||
|
resources: group.userResources ?? [],
|
||||||
|
images: group.userImages ?? [],
|
||||||
|
},
|
||||||
|
phase: chunkPhase,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
chunkStart = chunkEnd
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastPhase = timelineTurns[timelineTurns.length - 1]?.phase ?? null
|
const lastPhase = timelineTurns[timelineTurns.length - 1]?.phase ?? null
|
||||||
if (connStatus === "prompting" && lastPhase === "optimistic") {
|
if (connStatus === "prompting" && lastPhase === "optimistic") {
|
||||||
items.push({ key: "pending-typing", kind: "typing" })
|
items.push({ key: "pending-typing", kind: "typing" })
|
||||||
}
|
}
|
||||||
return items
|
|
||||||
}, [
|
|
||||||
attachedResourcesText,
|
|
||||||
connStatus,
|
|
||||||
groupedTimeline,
|
|
||||||
sharedT,
|
|
||||||
timelineTurns,
|
|
||||||
])
|
|
||||||
|
|
||||||
const historicalMessages = useMemo(
|
return { threadItems: items, nonStreamingAdapted: nonStreaming }
|
||||||
() =>
|
}, [adapterText, connStatus, timelineTurns])
|
||||||
adaptMessageTurns(
|
|
||||||
timelineTurns
|
|
||||||
.filter((item) => item.phase !== "streaming")
|
|
||||||
.map((item) => item.turn),
|
|
||||||
{
|
|
||||||
attachedResources: sharedT("attachedResources"),
|
|
||||||
toolCallFailed: sharedT("toolCallFailed"),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
[sharedT, timelineTurns]
|
|
||||||
)
|
|
||||||
const historicalPlanEntries = useMemo(
|
const historicalPlanEntries = useMemo(
|
||||||
() => extractLatestPlanEntriesFromMessages(historicalMessages),
|
() => extractLatestPlanEntriesFromMessages(nonStreamingAdapted),
|
||||||
[historicalMessages]
|
[nonStreamingAdapted]
|
||||||
)
|
)
|
||||||
const historicalPlanKey = useMemo(
|
const historicalPlanKey = useMemo(
|
||||||
() => buildPlanKey(historicalPlanEntries),
|
() => buildPlanKey(historicalPlanEntries),
|
||||||
@@ -337,7 +251,7 @@ export function MessageListView({
|
|||||||
|
|
||||||
const hasRenderableContent = threadItems.length > 0 || Boolean(liveMessage)
|
const hasRenderableContent = threadItems.length > 0 || Boolean(liveMessage)
|
||||||
|
|
||||||
if (loading && !hasRenderableContent) {
|
if (detailLoading && !hasRenderableContent) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
@@ -348,12 +262,12 @@ export function MessageListView({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error && !hasRenderableContent) {
|
if (detailError && !hasRenderableContent) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-destructive text-sm">
|
<p className="text-destructive text-sm">
|
||||||
{t("error", { message: error })}
|
{t("error", { message: detailError })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -306,6 +306,22 @@ function reducer(
|
|||||||
const current =
|
const current =
|
||||||
state.byConversationId.get(action.conversationId) ??
|
state.byConversationId.get(action.conversationId) ??
|
||||||
createEmptySession(action.conversationId)
|
createEmptySession(action.conversationId)
|
||||||
|
|
||||||
|
// Guard: prevent stale liveMessage from ACP reconnects overriding
|
||||||
|
// persisted data. When a session has no active liveMessage and no
|
||||||
|
// pending interaction (idle or reconciling without a live turn),
|
||||||
|
// a SET_LIVE_MESSAGE from a reconnected ACP connection carries
|
||||||
|
// the completed response that is already in persistedTurns.
|
||||||
|
// Accepting it would cause duplicate assistant text in the timeline.
|
||||||
|
if (
|
||||||
|
action.liveMessage !== null &&
|
||||||
|
current.liveMessage === null &&
|
||||||
|
current.syncState !== "awaiting_persist" &&
|
||||||
|
current.persistedTurns.length > 0
|
||||||
|
) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
const nextSession: ConversationRuntimeSession = {
|
const nextSession: ConversationRuntimeSession = {
|
||||||
...current,
|
...current,
|
||||||
liveMessage: action.liveMessage,
|
liveMessage: action.liveMessage,
|
||||||
|
|||||||
@@ -1114,8 +1114,7 @@
|
|||||||
},
|
},
|
||||||
"shared": {
|
"shared": {
|
||||||
"attachedResources": "الموارد المرفقة",
|
"attachedResources": "الموارد المرفقة",
|
||||||
"toolCallFailed": "فشل استدعاء الأداة",
|
"toolCallFailed": "فشل استدعاء الأداة"
|
||||||
"planUpdated": "تم تحديث الخطة"
|
|
||||||
},
|
},
|
||||||
"messageThread": {
|
"messageThread": {
|
||||||
"emptyTitle": "لا توجد رسائل بعد",
|
"emptyTitle": "لا توجد رسائل بعد",
|
||||||
@@ -1147,9 +1146,6 @@
|
|||||||
"noEnabledAgents": "لا يوجد وكلاء مفعّلون",
|
"noEnabledAgents": "لا يوجد وكلاء مفعّلون",
|
||||||
"openAgentsSettings": "فتح إعدادات الوكلاء"
|
"openAgentsSettings": "فتح إعدادات الوكلاء"
|
||||||
},
|
},
|
||||||
"liveMessageBlock": {
|
|
||||||
"assistantThinkingAria": "المساعد يفكر"
|
|
||||||
},
|
|
||||||
"agentPlanOverlay": {
|
"agentPlanOverlay": {
|
||||||
"title": "خطة الوكيل",
|
"title": "خطة الوكيل",
|
||||||
"collapsePlanAria": "طي الخطة",
|
"collapsePlanAria": "طي الخطة",
|
||||||
|
|||||||
@@ -1114,8 +1114,7 @@
|
|||||||
},
|
},
|
||||||
"shared": {
|
"shared": {
|
||||||
"attachedResources": "Angehängte Ressourcen",
|
"attachedResources": "Angehängte Ressourcen",
|
||||||
"toolCallFailed": "Tool-Aufruf fehlgeschlagen",
|
"toolCallFailed": "Tool-Aufruf fehlgeschlagen"
|
||||||
"planUpdated": "Plan aktualisiert"
|
|
||||||
},
|
},
|
||||||
"messageThread": {
|
"messageThread": {
|
||||||
"emptyTitle": "Noch keine Nachrichten",
|
"emptyTitle": "Noch keine Nachrichten",
|
||||||
@@ -1147,9 +1146,6 @@
|
|||||||
"noEnabledAgents": "Keine aktivierten Agenten",
|
"noEnabledAgents": "Keine aktivierten Agenten",
|
||||||
"openAgentsSettings": "Agenten-Einstellungen öffnen"
|
"openAgentsSettings": "Agenten-Einstellungen öffnen"
|
||||||
},
|
},
|
||||||
"liveMessageBlock": {
|
|
||||||
"assistantThinkingAria": "Assistent denkt nach"
|
|
||||||
},
|
|
||||||
"agentPlanOverlay": {
|
"agentPlanOverlay": {
|
||||||
"title": "Agentenplan",
|
"title": "Agentenplan",
|
||||||
"collapsePlanAria": "Plan einklappen",
|
"collapsePlanAria": "Plan einklappen",
|
||||||
|
|||||||
@@ -1114,8 +1114,7 @@
|
|||||||
},
|
},
|
||||||
"shared": {
|
"shared": {
|
||||||
"attachedResources": "Attached resources",
|
"attachedResources": "Attached resources",
|
||||||
"toolCallFailed": "Tool call failed",
|
"toolCallFailed": "Tool call failed"
|
||||||
"planUpdated": "Plan updated"
|
|
||||||
},
|
},
|
||||||
"messageThread": {
|
"messageThread": {
|
||||||
"emptyTitle": "No messages yet",
|
"emptyTitle": "No messages yet",
|
||||||
@@ -1147,9 +1146,6 @@
|
|||||||
"noEnabledAgents": "No enabled agents",
|
"noEnabledAgents": "No enabled agents",
|
||||||
"openAgentsSettings": "Open Agents settings"
|
"openAgentsSettings": "Open Agents settings"
|
||||||
},
|
},
|
||||||
"liveMessageBlock": {
|
|
||||||
"assistantThinkingAria": "Assistant is thinking"
|
|
||||||
},
|
|
||||||
"agentPlanOverlay": {
|
"agentPlanOverlay": {
|
||||||
"title": "Agent Plan",
|
"title": "Agent Plan",
|
||||||
"collapsePlanAria": "Collapse plan",
|
"collapsePlanAria": "Collapse plan",
|
||||||
|
|||||||
@@ -1114,8 +1114,7 @@
|
|||||||
},
|
},
|
||||||
"shared": {
|
"shared": {
|
||||||
"attachedResources": "Recursos adjuntos",
|
"attachedResources": "Recursos adjuntos",
|
||||||
"toolCallFailed": "Falló la llamada de herramienta",
|
"toolCallFailed": "Falló la llamada de herramienta"
|
||||||
"planUpdated": "Plan actualizado"
|
|
||||||
},
|
},
|
||||||
"messageThread": {
|
"messageThread": {
|
||||||
"emptyTitle": "Aún no hay mensajes",
|
"emptyTitle": "Aún no hay mensajes",
|
||||||
@@ -1147,9 +1146,6 @@
|
|||||||
"noEnabledAgents": "No hay agentes habilitados",
|
"noEnabledAgents": "No hay agentes habilitados",
|
||||||
"openAgentsSettings": "Abrir ajustes de agentes"
|
"openAgentsSettings": "Abrir ajustes de agentes"
|
||||||
},
|
},
|
||||||
"liveMessageBlock": {
|
|
||||||
"assistantThinkingAria": "El asistente está pensando"
|
|
||||||
},
|
|
||||||
"agentPlanOverlay": {
|
"agentPlanOverlay": {
|
||||||
"title": "Plan del agente",
|
"title": "Plan del agente",
|
||||||
"collapsePlanAria": "Contraer plan",
|
"collapsePlanAria": "Contraer plan",
|
||||||
|
|||||||
@@ -1114,8 +1114,7 @@
|
|||||||
},
|
},
|
||||||
"shared": {
|
"shared": {
|
||||||
"attachedResources": "Ressources jointes",
|
"attachedResources": "Ressources jointes",
|
||||||
"toolCallFailed": "Échec de l'appel d'outil",
|
"toolCallFailed": "Échec de l'appel d'outil"
|
||||||
"planUpdated": "Plan mis à jour"
|
|
||||||
},
|
},
|
||||||
"messageThread": {
|
"messageThread": {
|
||||||
"emptyTitle": "Aucun message pour le moment",
|
"emptyTitle": "Aucun message pour le moment",
|
||||||
@@ -1147,9 +1146,6 @@
|
|||||||
"noEnabledAgents": "Aucun agent activé",
|
"noEnabledAgents": "Aucun agent activé",
|
||||||
"openAgentsSettings": "Ouvrir les paramètres des agents"
|
"openAgentsSettings": "Ouvrir les paramètres des agents"
|
||||||
},
|
},
|
||||||
"liveMessageBlock": {
|
|
||||||
"assistantThinkingAria": "L'assistant réfléchit"
|
|
||||||
},
|
|
||||||
"agentPlanOverlay": {
|
"agentPlanOverlay": {
|
||||||
"title": "Plan de l'agent",
|
"title": "Plan de l'agent",
|
||||||
"collapsePlanAria": "Réduire le plan",
|
"collapsePlanAria": "Réduire le plan",
|
||||||
|
|||||||
@@ -1114,8 +1114,7 @@
|
|||||||
},
|
},
|
||||||
"shared": {
|
"shared": {
|
||||||
"attachedResources": "添付リソース",
|
"attachedResources": "添付リソース",
|
||||||
"toolCallFailed": "ツール呼び出しに失敗しました",
|
"toolCallFailed": "ツール呼び出しに失敗しました"
|
||||||
"planUpdated": "プランを更新しました"
|
|
||||||
},
|
},
|
||||||
"messageThread": {
|
"messageThread": {
|
||||||
"emptyTitle": "まだメッセージはありません",
|
"emptyTitle": "まだメッセージはありません",
|
||||||
@@ -1147,9 +1146,6 @@
|
|||||||
"noEnabledAgents": "有効なエージェントがありません",
|
"noEnabledAgents": "有効なエージェントがありません",
|
||||||
"openAgentsSettings": "エージェント設定を開く"
|
"openAgentsSettings": "エージェント設定を開く"
|
||||||
},
|
},
|
||||||
"liveMessageBlock": {
|
|
||||||
"assistantThinkingAria": "アシスタントが考え中です"
|
|
||||||
},
|
|
||||||
"agentPlanOverlay": {
|
"agentPlanOverlay": {
|
||||||
"title": "エージェントプラン",
|
"title": "エージェントプラン",
|
||||||
"collapsePlanAria": "プランを折りたたむ",
|
"collapsePlanAria": "プランを折りたたむ",
|
||||||
|
|||||||
@@ -1114,8 +1114,7 @@
|
|||||||
},
|
},
|
||||||
"shared": {
|
"shared": {
|
||||||
"attachedResources": "첨부된 리소스",
|
"attachedResources": "첨부된 리소스",
|
||||||
"toolCallFailed": "도구 호출 실패",
|
"toolCallFailed": "도구 호출 실패"
|
||||||
"planUpdated": "계획이 업데이트되었습니다"
|
|
||||||
},
|
},
|
||||||
"messageThread": {
|
"messageThread": {
|
||||||
"emptyTitle": "아직 메시지가 없습니다",
|
"emptyTitle": "아직 메시지가 없습니다",
|
||||||
@@ -1147,9 +1146,6 @@
|
|||||||
"noEnabledAgents": "활성화된 에이전트가 없습니다",
|
"noEnabledAgents": "활성화된 에이전트가 없습니다",
|
||||||
"openAgentsSettings": "에이전트 설정 열기"
|
"openAgentsSettings": "에이전트 설정 열기"
|
||||||
},
|
},
|
||||||
"liveMessageBlock": {
|
|
||||||
"assistantThinkingAria": "어시스턴트가 생각 중"
|
|
||||||
},
|
|
||||||
"agentPlanOverlay": {
|
"agentPlanOverlay": {
|
||||||
"title": "에이전트 계획",
|
"title": "에이전트 계획",
|
||||||
"collapsePlanAria": "계획 접기",
|
"collapsePlanAria": "계획 접기",
|
||||||
|
|||||||
@@ -1114,8 +1114,7 @@
|
|||||||
},
|
},
|
||||||
"shared": {
|
"shared": {
|
||||||
"attachedResources": "Recursos anexados",
|
"attachedResources": "Recursos anexados",
|
||||||
"toolCallFailed": "Falha na chamada da ferramenta",
|
"toolCallFailed": "Falha na chamada da ferramenta"
|
||||||
"planUpdated": "Plano atualizado"
|
|
||||||
},
|
},
|
||||||
"messageThread": {
|
"messageThread": {
|
||||||
"emptyTitle": "Ainda não há mensagens",
|
"emptyTitle": "Ainda não há mensagens",
|
||||||
@@ -1147,9 +1146,6 @@
|
|||||||
"noEnabledAgents": "Nenhum agente habilitado",
|
"noEnabledAgents": "Nenhum agente habilitado",
|
||||||
"openAgentsSettings": "Abrir configurações de agentes"
|
"openAgentsSettings": "Abrir configurações de agentes"
|
||||||
},
|
},
|
||||||
"liveMessageBlock": {
|
|
||||||
"assistantThinkingAria": "O assistente está pensando"
|
|
||||||
},
|
|
||||||
"agentPlanOverlay": {
|
"agentPlanOverlay": {
|
||||||
"title": "Plano do agente",
|
"title": "Plano do agente",
|
||||||
"collapsePlanAria": "Recolher plano",
|
"collapsePlanAria": "Recolher plano",
|
||||||
|
|||||||
@@ -1114,8 +1114,7 @@
|
|||||||
},
|
},
|
||||||
"shared": {
|
"shared": {
|
||||||
"attachedResources": "附加资源",
|
"attachedResources": "附加资源",
|
||||||
"toolCallFailed": "工具调用失败",
|
"toolCallFailed": "工具调用失败"
|
||||||
"planUpdated": "计划已更新"
|
|
||||||
},
|
},
|
||||||
"messageThread": {
|
"messageThread": {
|
||||||
"emptyTitle": "暂无消息",
|
"emptyTitle": "暂无消息",
|
||||||
@@ -1147,9 +1146,6 @@
|
|||||||
"noEnabledAgents": "暂无已启用的 Agent",
|
"noEnabledAgents": "暂无已启用的 Agent",
|
||||||
"openAgentsSettings": "打开 Agents 设置"
|
"openAgentsSettings": "打开 Agents 设置"
|
||||||
},
|
},
|
||||||
"liveMessageBlock": {
|
|
||||||
"assistantThinkingAria": "助手正在思考"
|
|
||||||
},
|
|
||||||
"agentPlanOverlay": {
|
"agentPlanOverlay": {
|
||||||
"title": "Agent 计划",
|
"title": "Agent 计划",
|
||||||
"collapsePlanAria": "折叠计划",
|
"collapsePlanAria": "折叠计划",
|
||||||
|
|||||||
@@ -1114,8 +1114,7 @@
|
|||||||
},
|
},
|
||||||
"shared": {
|
"shared": {
|
||||||
"attachedResources": "附加資源",
|
"attachedResources": "附加資源",
|
||||||
"toolCallFailed": "工具呼叫失敗",
|
"toolCallFailed": "工具呼叫失敗"
|
||||||
"planUpdated": "計畫已更新"
|
|
||||||
},
|
},
|
||||||
"messageThread": {
|
"messageThread": {
|
||||||
"emptyTitle": "暫無訊息",
|
"emptyTitle": "暫無訊息",
|
||||||
@@ -1147,9 +1146,6 @@
|
|||||||
"noEnabledAgents": "暫無已啟用的 Agent",
|
"noEnabledAgents": "暫無已啟用的 Agent",
|
||||||
"openAgentsSettings": "開啟 Agents 設定"
|
"openAgentsSettings": "開啟 Agents 設定"
|
||||||
},
|
},
|
||||||
"liveMessageBlock": {
|
|
||||||
"assistantThinkingAria": "助手正在思考"
|
|
||||||
},
|
|
||||||
"agentPlanOverlay": {
|
"agentPlanOverlay": {
|
||||||
"title": "Agent 計畫",
|
"title": "Agent 計畫",
|
||||||
"collapsePlanAria": "摺疊計畫",
|
"collapsePlanAria": "摺疊計畫",
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import type {
|
|||||||
MessageRole,
|
MessageRole,
|
||||||
TurnUsage,
|
TurnUsage,
|
||||||
} from "@/lib/types"
|
} from "@/lib/types"
|
||||||
import type { LiveMessage } from "@/contexts/acp-connections-context"
|
|
||||||
import { inferLiveToolName } from "@/lib/tool-call-normalization"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapted content part types for AI SDK Elements components
|
* Adapted content part types for AI SDK Elements components
|
||||||
@@ -71,7 +69,6 @@ export interface AdaptedMessage {
|
|||||||
export interface AdapterMessageText {
|
export interface AdapterMessageText {
|
||||||
attachedResources: string
|
attachedResources: string
|
||||||
toolCallFailed: string
|
toolCallFailed: string
|
||||||
planUpdated: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type InlineToolSegment =
|
type InlineToolSegment =
|
||||||
@@ -606,7 +603,7 @@ function buildToolResultMap(
|
|||||||
*/
|
*/
|
||||||
export function adaptMessageTurn(
|
export function adaptMessageTurn(
|
||||||
turn: MessageTurn,
|
turn: MessageTurn,
|
||||||
text: Pick<AdapterMessageText, "attachedResources" | "toolCallFailed">
|
text: AdapterMessageText
|
||||||
): AdaptedMessage {
|
): AdaptedMessage {
|
||||||
const adaptedContent: AdaptedContentPart[] = []
|
const adaptedContent: AdaptedContentPart[] = []
|
||||||
const resultMap = buildToolResultMap(turn.blocks)
|
const resultMap = buildToolResultMap(turn.blocks)
|
||||||
@@ -733,7 +730,7 @@ export function adaptMessageTurn(
|
|||||||
*/
|
*/
|
||||||
export function adaptMessageTurns(
|
export function adaptMessageTurns(
|
||||||
turns: MessageTurn[],
|
turns: MessageTurn[],
|
||||||
text: Pick<AdapterMessageText, "attachedResources" | "toolCallFailed">
|
text: AdapterMessageText
|
||||||
): AdaptedMessage[] {
|
): AdaptedMessage[] {
|
||||||
return turns.map((turn) => adaptMessageTurn(turn, text))
|
return turns.map((turn) => adaptMessageTurn(turn, text))
|
||||||
}
|
}
|
||||||
@@ -819,319 +816,3 @@ export function groupAdaptedMessages(
|
|||||||
|
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Map ACP tool call status to ToolCallState for display.
|
|
||||||
*/
|
|
||||||
function mapAcpStatusToToolCallState(status: string): ToolCallState {
|
|
||||||
switch (status) {
|
|
||||||
case "pending":
|
|
||||||
return "input-streaming"
|
|
||||||
case "in_progress":
|
|
||||||
return "input-available"
|
|
||||||
case "completed":
|
|
||||||
return "output-available"
|
|
||||||
case "failed":
|
|
||||||
return "output-error"
|
|
||||||
default:
|
|
||||||
return "input-available"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isReadToolName(toolName: string): boolean {
|
|
||||||
const normalized = toolName.trim().toLowerCase()
|
|
||||||
return normalized === "read" || normalized === "read file"
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTaskMarkdownToolName(toolName: string): boolean {
|
|
||||||
const normalized = toolName.trim().toLowerCase()
|
|
||||||
return (
|
|
||||||
normalized === "task" ||
|
|
||||||
normalized === "taskcreate" ||
|
|
||||||
normalized === "taskupdate" ||
|
|
||||||
normalized === "tasklist" ||
|
|
||||||
normalized.includes("explore")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function looksLikeJsonPayload(text: string): boolean {
|
|
||||||
const trimmed = text.trimStart()
|
|
||||||
return trimmed.startsWith("{") || trimmed.startsWith("[")
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectReadOutputText(value: unknown, depth: number = 0): string[] {
|
|
||||||
if (depth > 6 || value === null || value === undefined) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value.length > 0 ? [value] : []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.flatMap((item) => collectReadOutputText(item, depth + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value !== "object") {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const obj = value as Record<string, unknown>
|
|
||||||
const parts: string[] = []
|
|
||||||
const type = typeof obj.type === "string" ? obj.type.toLowerCase() : null
|
|
||||||
const text = obj.text
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof text === "string" &&
|
|
||||||
text.length > 0 &&
|
|
||||||
(type === null || type === "text")
|
|
||||||
) {
|
|
||||||
parts.push(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const nestedKey of ["content", "output", "result", "data"]) {
|
|
||||||
parts.push(...collectReadOutputText(obj[nestedKey], depth + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractReadTextFromJsonOutput(output: string): string | null {
|
|
||||||
if (!looksLikeJsonPayload(output)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed: unknown = JSON.parse(output)
|
|
||||||
const parts = collectReadOutputText(parsed)
|
|
||||||
if (parts.length === 0) return null
|
|
||||||
const text = parts.join("\n")
|
|
||||||
return text.length > 0 ? text : null
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeJsonTextValue(value: string): string {
|
|
||||||
try {
|
|
||||||
return JSON.parse(`"${value}"`) as string
|
|
||||||
} catch {
|
|
||||||
return value.replace(/\\"/g, '"').replace(/\\\\/g, "\\")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractTextFromMalformedJsonOutput(output: string): string | null {
|
|
||||||
const textValues = Array.from(
|
|
||||||
output.matchAll(/"text"\s*:\s*"((?:[^"\\]|\\.)*)"/g)
|
|
||||||
)
|
|
||||||
.map((match) => decodeJsonTextValue(match[1] ?? ""))
|
|
||||||
.map((value) => value.trim())
|
|
||||||
.filter((value) => value.length > 0)
|
|
||||||
|
|
||||||
if (textValues.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return textValues.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripWrappedMarkdownFence(text: string): string {
|
|
||||||
const normalized = text.replace(/\r\n/g, "\n")
|
|
||||||
const match = normalized.match(
|
|
||||||
/^\s*```[a-zA-Z0-9_-]*\s*\n([\s\S]*?)\n```\s*$/
|
|
||||||
)
|
|
||||||
if (!match) return text
|
|
||||||
return match[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeReadDisplayText(text: string): string {
|
|
||||||
return stripWrappedMarkdownFence(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectTaskMarkdownOutput(params: {
|
|
||||||
rawOutput: string | null
|
|
||||||
content: string | null
|
|
||||||
isFinalState: boolean
|
|
||||||
}): string | null {
|
|
||||||
for (const candidate of [params.content, params.rawOutput]) {
|
|
||||||
if (typeof candidate !== "string" || candidate.length === 0) continue
|
|
||||||
|
|
||||||
const extractedFromJson =
|
|
||||||
extractReadTextFromJsonOutput(candidate) ??
|
|
||||||
extractTextFromMalformedJsonOutput(candidate)
|
|
||||||
if (extractedFromJson) {
|
|
||||||
return normalizeReadDisplayText(extractedFromJson)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!looksLikeJsonPayload(candidate)) {
|
|
||||||
return normalizeReadDisplayText(candidate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!params.isFinalState) return null
|
|
||||||
|
|
||||||
const fallback = params.content ?? params.rawOutput
|
|
||||||
if (typeof fallback !== "string") return null
|
|
||||||
|
|
||||||
const extracted =
|
|
||||||
extractReadTextFromJsonOutput(fallback) ??
|
|
||||||
extractTextFromMalformedJsonOutput(fallback)
|
|
||||||
if (extracted) {
|
|
||||||
return normalizeReadDisplayText(extracted)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!looksLikeJsonPayload(fallback)) {
|
|
||||||
return normalizeReadDisplayText(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectLiveToolOutput(params: {
|
|
||||||
toolName: string
|
|
||||||
rawOutput: string | null
|
|
||||||
content: string | null
|
|
||||||
isFinalState: boolean
|
|
||||||
}): string | null {
|
|
||||||
if (isTaskMarkdownToolName(params.toolName)) {
|
|
||||||
return selectTaskMarkdownOutput(params)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isReadToolName(params.toolName)) {
|
|
||||||
return params.rawOutput ?? params.content
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const candidate of [params.content, params.rawOutput]) {
|
|
||||||
if (typeof candidate !== "string" || candidate.length === 0) continue
|
|
||||||
const extracted = extractReadTextFromJsonOutput(candidate)
|
|
||||||
if (extracted) return normalizeReadDisplayText(extracted)
|
|
||||||
if (!looksLikeJsonPayload(candidate))
|
|
||||||
return normalizeReadDisplayText(candidate)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!params.isFinalState) return null
|
|
||||||
const fallback = params.rawOutput ?? params.content
|
|
||||||
return typeof fallback === "string"
|
|
||||||
? normalizeReadDisplayText(fallback)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatPlanEntries(
|
|
||||||
entries: Array<{ content: string; priority: string; status: string }>,
|
|
||||||
planUpdatedText: string
|
|
||||||
): string {
|
|
||||||
if (entries.length === 0) {
|
|
||||||
return planUpdatedText
|
|
||||||
}
|
|
||||||
const lines = entries.map(
|
|
||||||
(entry) => `- [${entry.status}] ${entry.content} (${entry.priority})`
|
|
||||||
)
|
|
||||||
return `${planUpdatedText}:\n${lines.join("\n")}`
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AdaptLiveMessageOptions {
|
|
||||||
isLiveStreaming?: boolean
|
|
||||||
toolCallFailedText: string
|
|
||||||
planUpdatedText: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function isReasoningBlock(block: LiveMessage["content"][number]): boolean {
|
|
||||||
return block.type === "thinking" || block.type === "plan"
|
|
||||||
}
|
|
||||||
|
|
||||||
function findLastReasoningIndex(message: LiveMessage): number {
|
|
||||||
for (let index = message.content.length - 1; index >= 0; index -= 1) {
|
|
||||||
if (isReasoningBlock(message.content[index])) {
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform a LiveMessage (from ACP) to AdaptedMessage format
|
|
||||||
* This is used for live streaming messages from the ACP protocol
|
|
||||||
*/
|
|
||||||
export function adaptLiveMessageFromAcp(
|
|
||||||
message: LiveMessage,
|
|
||||||
options: AdaptLiveMessageOptions
|
|
||||||
): AdaptedMessage {
|
|
||||||
const isLiveStreaming = options.isLiveStreaming ?? true
|
|
||||||
const adaptedContent: AdaptedContentPart[] = []
|
|
||||||
const lastStreamingReasoningIndex = isLiveStreaming
|
|
||||||
? findLastReasoningIndex(message)
|
|
||||||
: -1
|
|
||||||
|
|
||||||
message.content.forEach((block, index) => {
|
|
||||||
switch (block.type) {
|
|
||||||
case "text":
|
|
||||||
adaptedContent.push({
|
|
||||||
type: "text",
|
|
||||||
text: block.text,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
|
|
||||||
case "thinking":
|
|
||||||
adaptedContent.push({
|
|
||||||
type: "reasoning",
|
|
||||||
content: block.text,
|
|
||||||
isStreaming: index === lastStreamingReasoningIndex,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
|
|
||||||
case "tool_call": {
|
|
||||||
const { info } = block
|
|
||||||
const toolName = inferLiveToolName({
|
|
||||||
title: info.title,
|
|
||||||
kind: info.kind,
|
|
||||||
rawInput: info.raw_input,
|
|
||||||
})
|
|
||||||
const state = mapAcpStatusToToolCallState(info.status)
|
|
||||||
const isFinalState =
|
|
||||||
state === "output-available" || state === "output-error"
|
|
||||||
const hasExplicitOutput =
|
|
||||||
info.raw_output !== null || info.content !== null
|
|
||||||
const selectedOutput = selectLiveToolOutput({
|
|
||||||
toolName,
|
|
||||||
rawOutput: info.raw_output,
|
|
||||||
content: info.content,
|
|
||||||
isFinalState,
|
|
||||||
})
|
|
||||||
const output = isFinalState
|
|
||||||
? selectedOutput
|
|
||||||
: hasExplicitOutput
|
|
||||||
? selectedOutput
|
|
||||||
: null
|
|
||||||
adaptedContent.push({
|
|
||||||
type: "tool-call",
|
|
||||||
toolCallId: info.tool_call_id,
|
|
||||||
toolName,
|
|
||||||
displayTitle: info.title,
|
|
||||||
input: info.raw_input,
|
|
||||||
state,
|
|
||||||
output,
|
|
||||||
errorText:
|
|
||||||
state === "output-error"
|
|
||||||
? selectedOutput || options.toolCallFailedText
|
|
||||||
: undefined,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "plan":
|
|
||||||
adaptedContent.push({
|
|
||||||
type: "reasoning",
|
|
||||||
content: formatPlanEntries(block.entries, options.planUpdatedText),
|
|
||||||
isStreaming: index === lastStreamingReasoningIndex,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: message.id,
|
|
||||||
role: message.role,
|
|
||||||
content: adaptedContent,
|
|
||||||
timestamp: new Date().toISOString(), // Live messages don't have timestamps
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user