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 && (