diff --git a/src/components/message/content-parts-renderer.tsx b/src/components/message/content-parts-renderer.tsx
index ce8f02d75c..949bc12bd9 100644
--- a/src/components/message/content-parts-renderer.tsx
+++ b/src/components/message/content-parts-renderer.tsx
@@ -3047,6 +3047,15 @@ const ToolGroupPart = memo(function ToolGroupPart({
if (part.items.length === 0) return null
+ // A lone call has nothing to summarize — the pill would only read
+ // "Ran 1 command" and hide the card one click away. Unwrap it back to the
+ // direct tool card (the same one the expanded list below renders), so a
+ // single call sitting between two reasoning blocks reads as a plain card.
+ const [solo] = part.items
+ if (part.items.length === 1 && solo) {
+ return
+ }
+
const joiner = t("joiner")
const titleText = phrases.join(joiner)
diff --git a/src/components/message/content-parts-tool-group.test.tsx b/src/components/message/content-parts-tool-group.test.tsx
new file mode 100644
index 0000000000..e2733c011e
--- /dev/null
+++ b/src/components/message/content-parts-tool-group.test.tsx
@@ -0,0 +1,82 @@
+import { type ReactNode } from "react"
+import { render } from "@testing-library/react"
+import { NextIntlClientProvider } from "next-intl"
+import { describe, expect, it, vi } from "vitest"
+
+/**
+ * A collapsed tool-group pill exists to summarize a RUN of calls. When a
+ * reasoning block splits a turn into runs of one, the pill adds nothing but a
+ * click — "Ran 1 command" hiding the very card the user wants to read. The
+ * adapter still wraps every run uniformly; the renderer unwraps a one-item
+ * group back into the direct tool card.
+ */
+
+vi.mock("@/components/ai-elements/link-safety", () => ({
+ FilePathLink: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+ useStreamdownLinkSafety: () => ({ enabled: false }),
+}))
+
+vi.mock("@/components/ai-elements/code-block", () => ({
+ CodeBlock: ({ code }: { code: string }) =>
{code},
+}))
+
+vi.mock("@/components/ai-elements/message", () => ({
+ MessageResponse: ({ children }: { children: string }) => (
+ {children}
+ ),
+}))
+
+import { ContentPartsRenderer } from "./content-parts-renderer"
+import enMessages from "@/i18n/messages/en.json"
+import type { AdaptedContentPart } from "@/lib/adapters/ai-elements-adapter"
+
+type ToolCallPartType = Extract
+
+function settledBash(toolCallId: string, command: string): ToolCallPartType {
+ return {
+ type: "tool-call",
+ toolCallId,
+ toolName: "bash",
+ displayTitle: command,
+ input: JSON.stringify({ command, cwd: "/tmp/work" }),
+ state: "output-available",
+ output: "done",
+ toolStatus: "completed",
+ meta: null,
+ }
+}
+
+function groupOf(...items: ToolCallPartType[]): AdaptedContentPart {
+ return { type: "tool-group", items, isStreaming: false }
+}
+
+function renderParts(parts: AdaptedContentPart[]) {
+ return render(
+
+
+
+ )
+}
+
+describe("tool-group renderer — lone call unwrapping", () => {
+ it("renders a single call directly instead of a collapsed pill", () => {
+ const { container } = renderParts([groupOf(settledBash("tc-1", "ls -la"))])
+ expect(container.textContent).not.toContain("Ran 1 command")
+ // The card itself is on screen without the click the pill would require.
+ expect(container.textContent).toContain("ls -la")
+ })
+
+ it("keeps the collapsed pill once a run has two or more calls", () => {
+ const { container } = renderParts([
+ groupOf(settledBash("tc-1", "ls -la"), settledBash("tc-2", "pwd")),
+ ])
+ expect(container.textContent).toContain("Ran 2 commands")
+ })
+
+ it("still renders nothing for an empty group", () => {
+ const { container } = renderParts([groupOf()])
+ expect(container.textContent).toBe("")
+ })
+})
diff --git a/src/lib/adapters/ai-elements-adapter.ts b/src/lib/adapters/ai-elements-adapter.ts
index 84015fc6a7..a2a6cfe754 100644
--- a/src/lib/adapters/ai-elements-adapter.ts
+++ b/src/lib/adapters/ai-elements-adapter.ts
@@ -1577,8 +1577,9 @@ export function mergeAdjacentToolGroups(
/**
* Wrap any consecutive run of tool-call parts into a single tool-group.
* Text, reasoning, tool-result and any other part types break the run.
- * Even a single tool call is wrapped, so the renderer can present a uniform
- * collapsed summary across history.
+ * Even a single tool call is wrapped — grouping stays uniform at this layer —
+ * but the renderer unwraps a one-item group back into a direct tool card, so
+ * lone calls never hide behind a "Ran 1 command" pill.
*/
export function groupConsecutiveToolCalls(
parts: AdaptedContentPart[]