diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 1e95f82a6..62ac4f1c7 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -73,6 +73,8 @@ The frontend is a stateful chat application. Users create **threads** (conversat Human input requests are a structured message protocol layered on normal chat history. The backend writes request payloads to `ToolMessage.artifact.human_input`, `src/core/messages/human-input.ts` owns the runtime validators/types, and `src/components/workspace/messages/human-input-card.tsx` renders the reusable card. `MessageList` owns answered/latest/pending state for visible cards, but derives answered responses from raw `thread.messages` because replies are hidden; pending cards clear when the hidden reply appears, when dispatch is dropped, or when a new `thread.error` reports an async stream failure. Page-level submit callbacks must send a normal human message and put `hide_from_ui: true` plus the response payload in the fourth `sendMessage(..., options)` argument as `options.additionalKwargs`; the third argument remains run context such as `{ agent_name }`. Composer entry points should disable normal bottom input while `hasOpenHumanInputRequest(...)` is true so users answer through the card and preserve response metadata. +Tool-calling AI messages can contain user-visible text as well as `tool_calls`. `core/messages/utils.ts` keeps these turns in an `assistant:processing` group, and `components/workspace/messages/message-group.tsx` must render the visible text as a processing step instead of treating the message as only tool metadata. This preserves provider text such as error explanations or "trying another approach" notes during tool-heavy runs. + ### Key Patterns - **Server Components by default**, `"use client"` only for interactive components diff --git a/frontend/src/components/workspace/messages/message-group.tsx b/frontend/src/components/workspace/messages/message-group.tsx index 636930bed..4ea926df6 100644 --- a/frontend/src/components/workspace/messages/message-group.tsx +++ b/frontend/src/components/workspace/messages/message-group.tsx @@ -8,6 +8,7 @@ import { LightbulbIcon, ListTodoIcon, MessageCircleQuestionMarkIcon, + MessageSquareTextIcon, NotebookPenIcon, SearchIcon, SquareTerminalIcon, @@ -28,6 +29,7 @@ import { useI18n } from "@/core/i18n/hooks"; import { formatTokenCount } from "@/core/messages/usage"; import type { TokenDebugStep } from "@/core/messages/usage-model"; import { + extractContentFromMessage, extractReasoningContentFromMessage, findToolCallResult, } from "@/core/messages/utils"; @@ -96,6 +98,11 @@ export function MessageGroup({ } return []; }, [lastToolCallStep, steps]); + const collapsibleAboveLastToolCallSteps = useMemo( + () => + aboveLastToolCallSteps.filter((step) => step.type !== "assistantText"), + [aboveLastToolCallSteps], + ); const lastReasoningStep = useMemo(() => { if (lastToolCallStep) { const index = steps.indexOf(lastToolCallStep); @@ -220,6 +227,50 @@ export function MessageGroup({ ); }; + const renderAssistantText = (step: CoTAssistantTextStep) => ( + + } + > + ); + + const renderStep = (step: CoTStep) => { + const stepIndex = steps.indexOf(step); + if (step.type === "assistantText") { + return [ + renderDebugSummary(step.messageId, stepIndex), + renderAssistantText(step), + ]; + } + if (step.type === "reasoning") { + return [ + renderDebugSummary(step.messageId, stepIndex), + + } + >, + ]; + } + + return [ + renderDebugSummary(step.messageId, stepIndex), + renderToolCall(step), + ]; + }; + const lastReasoningDebugStep = showTokenDebugSummaries && lastReasoningStep?.messageId ? debugStepByMessageId.get(lastReasoningStep.messageId) @@ -230,7 +281,7 @@ export function MessageGroup({ className={cn("w-full gap-2 rounded-lg border p-0.5", className)} open={true} > - {aboveLastToolCallSteps.length > 0 && ( + {collapsibleAboveLastToolCallSteps.length > 0 && (