@tangle-network/ui 11.0.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -126,6 +126,40 @@ describe("AgentTimeline tool actions", () => {
126
126
  ).toBeInTheDocument()
127
127
  })
128
128
 
129
+ it("renders every row and no toggle when collapseAfter is unset", () => {
130
+ const items: AgentTimelineItem[] = Array.from({ length: 5 }, (_, i) => ({
131
+ id: `s-${i}`,
132
+ kind: "status",
133
+ label: `Step ${i}`,
134
+ }))
135
+ render(<AgentTimeline items={items} />)
136
+ expect(screen.getByText("Step 4")).toBeInTheDocument()
137
+ expect(
138
+ screen.queryByRole("button", { name: /more step/i }),
139
+ ).not.toBeInTheDocument()
140
+ })
141
+
142
+ it("collapses to the first N spine rows behind a toggle and reveals the rest", async () => {
143
+ const user = userEvent.setup()
144
+ const items: AgentTimelineItem[] = Array.from({ length: 5 }, (_, i) => ({
145
+ id: `s-${i}`,
146
+ kind: "status",
147
+ label: `Step ${i}`,
148
+ }))
149
+ render(<AgentTimeline items={items} collapseAfter={2} />)
150
+
151
+ // First 2 visible, the other 3 hidden behind the toggle
152
+ expect(screen.getByText("Step 0")).toBeInTheDocument()
153
+ expect(screen.getByText("Step 1")).toBeInTheDocument()
154
+ expect(screen.queryByText("Step 4")).not.toBeInTheDocument()
155
+
156
+ const toggle = screen.getByRole("button", { name: /show 3 more steps/i })
157
+ await user.click(toggle)
158
+
159
+ expect(screen.getByText("Step 4")).toBeInTheDocument()
160
+ expect(screen.getByRole("button", { name: /show less/i })).toBeInTheDocument()
161
+ })
162
+
129
163
  it("renders the source part's real input in the expanded detail", async () => {
130
164
  const user = userEvent.setup()
131
165
  const probePart: ToolPart = {
@@ -1,4 +1,4 @@
1
- import { type KeyboardEvent, type ReactNode } from "react";
1
+ import { useState, type KeyboardEvent, type ReactNode } from "react";
2
2
  import {
3
3
  AlertTriangle,
4
4
  CheckCircle2,
@@ -88,6 +88,9 @@ export interface AgentTimelineProps {
88
88
  /** Optional actions rendered beside each tool item (e.g. "open in artifacts").
89
89
  * Receives the source tool part carried on the item. */
90
90
  renderToolActions?: (part: ToolPart) => ReactNode;
91
+ /** When set, collapse the timeline to the first N spine rows behind a
92
+ * "Show N more steps" toggle. Omit to always show every row. */
93
+ collapseAfter?: number;
91
94
  }
92
95
 
93
96
  const TONE_STYLES: Record<AgentTimelineTone, { dot: string; card: string; text: string; icon: typeof Info }> = {
@@ -130,17 +133,24 @@ function formatTime(date: Date): string {
130
133
  interface AgentTimelineRowProps {
131
134
  isLast: boolean;
132
135
  accentClassName: string;
136
+ /** Dot top-offset. Defaults to aligning with the first text line; card rows
137
+ * pass CARD_ROW_DOT to center the dot on the card header instead. */
138
+ dotClassName?: string;
133
139
  children: ReactNode;
134
140
  }
135
141
 
136
- function AgentTimelineRow({ isLast, accentClassName, children }: AgentTimelineRowProps) {
142
+ // Centers the dot on a RunRowShell card header (2.5rem: h-6 badge + py-2),
143
+ // so tool and reasoning rows read as dot-aligned rather than top-anchored.
144
+ const CARD_ROW_DOT = "mt-[calc((2.5rem-var(--timeline-dot-size))/2)]";
145
+
146
+ function AgentTimelineRow({ isLast, accentClassName, dotClassName = "mt-2", children }: AgentTimelineRowProps) {
137
147
  return (
138
148
  <div className="grid grid-cols-[1.25rem_minmax(0,1fr)] gap-x-4">
139
149
  <div className="relative flex justify-center">
140
150
  {!isLast && (
141
151
  <span className="absolute top-4 bottom-[-0.75rem] left-1/2 w-px -translate-x-1/2 bg-border" />
142
152
  )}
143
- <span className={cn("relative mt-2 h-[var(--timeline-dot-size)] w-[var(--timeline-dot-size)] rounded-full ring-4 ring-[var(--bg-root)]", accentClassName)} />
153
+ <span className={cn("relative h-[var(--timeline-dot-size)] w-[var(--timeline-dot-size)] rounded-full ring-4 ring-[var(--bg-root)]", dotClassName, accentClassName)} />
144
154
  </div>
145
155
  <div className="min-w-0 pb-3">{children}</div>
146
156
  </div>
@@ -245,7 +255,10 @@ export function AgentTimeline({
245
255
  emptyState,
246
256
  className,
247
257
  renderToolActions,
258
+ collapseAfter,
248
259
  }: AgentTimelineProps) {
260
+ const [expanded, setExpanded] = useState(false);
261
+
249
262
  if (items.length === 0 && !isThinking) {
250
263
  return emptyState ? (
251
264
  <div className={cn("flex h-full items-center justify-center p-4", className)}>
@@ -258,13 +271,41 @@ export function AgentTimeline({
258
271
  ? [...items, { id: "__thinking__", kind: "custom", content: <ThinkingIndicator /> }]
259
272
  : items;
260
273
 
261
- // Determine which items participate in the timeline connector (non-user-message items)
262
- // User messages are rendered outside the timeline grid
263
- const timelineItems = renderedItems.filter((item) => !(item.kind === "message" && item.role === "user"));
274
+ const isUserMessage = (item: AgentTimelineItem) =>
275
+ item.kind === "message" && item.role === "user";
276
+
277
+ // Items on the vertical connector (user messages render off-spine).
278
+ const timelineItems = renderedItems.filter((item) => !isUserMessage(item));
279
+
280
+ const limit = collapseAfter ?? Infinity;
281
+ const collapsible = timelineItems.length > limit;
282
+ const collapsed = collapsible && !expanded;
283
+
284
+ // While collapsed, keep only the first `limit` spine rows (and any user
285
+ // messages that precede them); the rest hides behind the toggle.
286
+ let renderList = renderedItems;
287
+ let hiddenCount = 0;
288
+ if (collapsed) {
289
+ const visible: AgentTimelineItem[] = [];
290
+ let spineCount = 0;
291
+ for (const item of renderedItems) {
292
+ if (spineCount >= limit) break;
293
+ visible.push(item);
294
+ if (!isUserMessage(item)) spineCount += 1;
295
+ }
296
+ renderList = visible;
297
+ hiddenCount = timelineItems.length - limit;
298
+ }
299
+
300
+ const visibleSpine = renderList.filter((item) => !isUserMessage(item));
301
+ // When a toggle row follows, no real row is last — the toggle owns the tail.
302
+ const lastSpineItem = collapsible
303
+ ? undefined
304
+ : visibleSpine[visibleSpine.length - 1];
264
305
 
265
306
  return (
266
307
  <div className={cn("mx-auto w-full max-w-5xl px-4 py-4", className)}>
267
- {renderedItems.map((item, index) => {
308
+ {renderList.map((item) => {
268
309
  // User messages: right-aligned bubble, off-spine — needs its own vertical
269
310
  // rhythm so it doesn't sit flush against the status/tool/agent row below.
270
311
  if (item.kind === "message" && item.role === "user") {
@@ -275,8 +316,7 @@ export function AgentTimeline({
275
316
  );
276
317
  }
277
318
 
278
- const timelineIndex = timelineItems.indexOf(item);
279
- const isLast = timelineIndex === timelineItems.length - 1;
319
+ const isLast = item === lastSpineItem;
280
320
 
281
321
  if (item.kind === "message") {
282
322
  return (
@@ -288,7 +328,7 @@ export function AgentTimeline({
288
328
 
289
329
  if (item.kind === "tool") {
290
330
  return (
291
- <AgentTimelineRow key={item.id} isLast={isLast} accentClassName="bg-[var(--border-hover)]">
331
+ <AgentTimelineRow key={item.id} isLast={isLast} accentClassName="bg-[var(--border-hover)]" dotClassName={CARD_ROW_DOT}>
292
332
  <ToolCallStep
293
333
  type={item.call.type}
294
334
  label={item.call.label}
@@ -305,7 +345,7 @@ export function AgentTimeline({
305
345
 
306
346
  if (item.kind === "tool_group") {
307
347
  return (
308
- <AgentTimelineRow key={item.id} isLast={isLast} accentClassName="bg-[var(--border-hover)]">
348
+ <AgentTimelineRow key={item.id} isLast={isLast} accentClassName="bg-[var(--border-hover)]" dotClassName={CARD_ROW_DOT}>
309
349
  <ToolCallGroup title={item.title}>
310
350
  {item.calls.map((call, callIndex) => {
311
351
  const part = item.parts?.[callIndex];
@@ -354,11 +394,24 @@ export function AgentTimeline({
354
394
 
355
395
  // custom
356
396
  return (
357
- <AgentTimelineRow key={item.id} isLast={isLast} accentClassName="bg-[var(--border-hover)]">
397
+ <AgentTimelineRow key={item.id} isLast={isLast} accentClassName="bg-[var(--border-hover)]" dotClassName={CARD_ROW_DOT}>
358
398
  {(item as AgentTimelineCustomItem).content}
359
399
  </AgentTimelineRow>
360
400
  );
361
401
  })}
402
+ {collapsible ? (
403
+ <AgentTimelineRow isLast accentClassName="bg-[var(--border-hover)]">
404
+ <button
405
+ type="button"
406
+ onClick={() => setExpanded((value) => !value)}
407
+ className="text-xs font-medium text-muted-foreground transition-colors hover:text-foreground"
408
+ >
409
+ {collapsed
410
+ ? `Show ${hiddenCount} more step${hiddenCount === 1 ? "" : "s"}`
411
+ : "Show less"}
412
+ </button>
413
+ </AgentTimelineRow>
414
+ ) : null}
362
415
  </div>
363
416
  );
364
417
  }
@@ -63,6 +63,9 @@ export interface ChatContainerProps {
63
63
  /** Optional actions rendered beside individual tool items in the `timeline`
64
64
  * presentation, which has no run/message grouping (hence part-only). */
65
65
  renderTimelineToolActions?: (part: ToolPart) => ReactNode;
66
+ /** In the `timeline` presentation, collapse to the first N spine rows behind
67
+ * a "Show N more steps" toggle. Omit to always show every row. */
68
+ collapseTimelineAfter?: number;
66
69
  }
67
70
 
68
71
  const OPENUI_NODE_TYPES = new Set([
@@ -373,6 +376,7 @@ export const ChatContainer = memo(
373
376
  renderUserMessageActions,
374
377
  renderToolActions,
375
378
  renderTimelineToolActions,
379
+ collapseTimelineAfter,
376
380
  }: ChatContainerProps) => {
377
381
  const scrollRef = useRef<HTMLDivElement>(null);
378
382
 
@@ -414,6 +418,7 @@ export const ChatContainer = memo(
414
418
  items={timeline.items}
415
419
  isThinking={timeline.showThinking}
416
420
  renderToolActions={renderTimelineToolActions}
421
+ collapseAfter={collapseTimelineAfter}
417
422
  />
418
423
  </div>
419
424
  ) : (
@@ -0,0 +1,52 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { ExpandableContent } from "./expandable-content";
5
+
6
+ describe("ExpandableContent", () => {
7
+ let scrollHeightSpy: ReturnType<typeof vi.spyOn> | undefined;
8
+
9
+ afterEach(() => {
10
+ scrollHeightSpy?.mockRestore();
11
+ scrollHeightSpy = undefined;
12
+ });
13
+
14
+ it("renders no toggle when content fits within the cap", () => {
15
+ render(
16
+ <ExpandableContent collapsedMaxPx={180}>
17
+ <div>short</div>
18
+ </ExpandableContent>,
19
+ );
20
+ expect(
21
+ screen.queryByRole("button", { name: /show more/i }),
22
+ ).not.toBeInTheDocument();
23
+ });
24
+
25
+ describe("when content overflows", () => {
26
+ beforeEach(() => {
27
+ scrollHeightSpy = vi
28
+ .spyOn(HTMLElement.prototype, "scrollHeight", "get")
29
+ .mockReturnValue(999);
30
+ });
31
+
32
+ it("clamps with a Show more toggle that expands and collapses", async () => {
33
+ const user = userEvent.setup();
34
+ render(
35
+ <ExpandableContent collapsedMaxPx={180}>
36
+ <div>tall content</div>
37
+ </ExpandableContent>,
38
+ );
39
+
40
+ const showMore = screen.getByRole("button", { name: /show more/i });
41
+ await user.click(showMore);
42
+ expect(
43
+ screen.getByRole("button", { name: /show less/i }),
44
+ ).toBeInTheDocument();
45
+
46
+ await user.click(screen.getByRole("button", { name: /show less/i }));
47
+ expect(
48
+ screen.getByRole("button", { name: /show more/i }),
49
+ ).toBeInTheDocument();
50
+ });
51
+ });
52
+ });
@@ -0,0 +1,73 @@
1
+ import { useEffect, useRef, useState, type ReactNode } from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ export interface ExpandableContentProps {
5
+ children: ReactNode;
6
+ /** Height (px) the content is clamped to while collapsed. */
7
+ collapsedMaxPx?: number;
8
+ /**
9
+ * Gradient from-color for the bottom fade, so it blends with the surface
10
+ * behind the content (e.g. `from-muted`, `from-card`, `from-[var(--surface-danger-bg)]`).
11
+ */
12
+ fadeClassName?: string;
13
+ className?: string;
14
+ }
15
+
16
+ /**
17
+ * Clamps tall content to a fixed height with a bottom fade and a
18
+ * "Show more / Show less" toggle. Content shorter than the cap renders
19
+ * as-is with no toggle.
20
+ */
21
+ export function ExpandableContent({
22
+ children,
23
+ collapsedMaxPx = 180,
24
+ fadeClassName = "from-muted",
25
+ className,
26
+ }: ExpandableContentProps) {
27
+ const [expanded, setExpanded] = useState(false);
28
+ const [overflowing, setOverflowing] = useState(false);
29
+ const contentRef = useRef<HTMLDivElement>(null);
30
+
31
+ useEffect(() => {
32
+ const el = contentRef.current;
33
+ if (!el) return;
34
+ const measure = () =>
35
+ setOverflowing(el.scrollHeight > collapsedMaxPx + 4);
36
+ measure();
37
+ if (typeof ResizeObserver === "undefined") return;
38
+ const observer = new ResizeObserver(measure);
39
+ observer.observe(el);
40
+ return () => observer.disconnect();
41
+ }, [collapsedMaxPx]);
42
+
43
+ const clamped = overflowing && !expanded;
44
+
45
+ return (
46
+ <div className={className}>
47
+ <div
48
+ ref={contentRef}
49
+ className="relative overflow-hidden"
50
+ style={clamped ? { maxHeight: collapsedMaxPx } : undefined}
51
+ >
52
+ {children}
53
+ {clamped ? (
54
+ <div
55
+ className={cn(
56
+ "pointer-events-none absolute inset-x-0 bottom-0 h-10 bg-gradient-to-t to-transparent",
57
+ fadeClassName,
58
+ )}
59
+ />
60
+ ) : null}
61
+ </div>
62
+ {overflowing ? (
63
+ <button
64
+ type="button"
65
+ onClick={() => setExpanded((value) => !value)}
66
+ className="mt-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground"
67
+ >
68
+ {expanded ? "Show less" : "Show more"}
69
+ </button>
70
+ ) : null}
71
+ </div>
72
+ );
73
+ }
@@ -17,6 +17,7 @@ import { WebSearchPreview } from "../tool-previews/web-search-preview";
17
17
  import { QuestionPreview } from "../tool-previews/question-preview";
18
18
  import { DiffPreview } from "../tool-previews/diff-preview";
19
19
  import { cn } from "../lib/utils";
20
+ import { ExpandableContent } from "./expandable-content";
20
21
 
21
22
  export interface ExpandedToolDetailProps {
22
23
  part: ToolPart;
@@ -146,15 +147,17 @@ export const ExpandedToolDetail = memo(({ part }: ExpandedToolDetailProps) => {
146
147
  Output
147
148
  </span>
148
149
  </div>
149
- <CodeBlock
150
- code={
151
- outputStr.length > 2000
152
- ? outputStr.slice(0, 2000) + "\n...(truncated)"
153
- : outputStr
154
- }
155
- language="json"
156
- className="rounded-none border-0"
157
- />
150
+ <ExpandableContent fadeClassName="from-card">
151
+ <CodeBlock
152
+ code={
153
+ outputStr.length > 2000
154
+ ? outputStr.slice(0, 2000) + "\n...(truncated)"
155
+ : outputStr
156
+ }
157
+ language="json"
158
+ className="rounded-none border-0"
159
+ />
160
+ </ExpandableContent>
158
161
  </div>
159
162
  )}
160
163
 
@@ -167,9 +170,11 @@ export const ExpandedToolDetail = memo(({ part }: ExpandedToolDetailProps) => {
167
170
  Error
168
171
  </span>
169
172
  </div>
170
- <pre className="bg-[var(--surface-danger-bg)] p-3 text-xs font-mono whitespace-pre-wrap break-all text-[var(--surface-danger-text)]">
171
- {error}
172
- </pre>
173
+ <ExpandableContent fadeClassName="from-[var(--surface-danger-bg)]">
174
+ <pre className="bg-[var(--surface-danger-bg)] p-3 text-xs font-mono whitespace-pre-wrap break-all text-[var(--surface-danger-text)]">
175
+ {error}
176
+ </pre>
177
+ </ExpandableContent>
173
178
  </div>
174
179
  )}
175
180
 
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react'
2
2
  import { InlineThinkingItem } from './inline-thinking-item'
3
- import type { ReasoningPart } from '../types/parts'
3
+ import { InlineToolItem } from './inline-tool-item'
4
+ import type { ReasoningPart, ToolPart } from '../types/parts'
4
5
 
5
6
  const NOW = Date.now()
6
7
 
@@ -122,6 +123,34 @@ export const EmptyText: Story = {
122
123
  ),
123
124
  }
124
125
 
126
+ const bashTool: ToolPart = {
127
+ type: 'tool',
128
+ id: 'tool-shell-demo',
129
+ tool: 'bash',
130
+ state: {
131
+ status: 'completed',
132
+ input: { command: 'npx tsc --noEmit' },
133
+ output: '✓ No type errors',
134
+ time: { start: NOW - 2040, end: NOW - 200 },
135
+ },
136
+ }
137
+
138
+ // Reasoning and tool rows adjacent — they must read as one family (shared
139
+ // badge geometry, trailing status/duration, chevron), not two designs.
140
+ export const UnifiedWithToolRow: Story = {
141
+ name: 'Unified shell (reasoning + tool)',
142
+ render: () => (
143
+ <div className="p-6 max-w-2xl space-y-2">
144
+ <InlineThinkingItem
145
+ part={thinkingComplete}
146
+ defaultOpen={false}
147
+ autoCollapse={false}
148
+ />
149
+ <InlineToolItem part={bashTool} />
150
+ </div>
151
+ ),
152
+ }
153
+
125
154
  export const AllStates: Story = {
126
155
  render: () => (
127
156
  <div className="p-6 max-w-2xl space-y-3">
@@ -1,11 +1,10 @@
1
1
  import { memo, useEffect, useRef, useState } from "react";
2
- import * as Collapsible from "@radix-ui/react-collapsible";
3
- import { Brain, ChevronRight } from "lucide-react";
4
- import { cn } from "../lib/utils";
5
- import { formatDuration, truncateText } from "../utils/format";
2
+ import { Brain } from "lucide-react";
3
+ import { truncateText } from "../utils/format";
6
4
  import type { ReasoningPart } from "../types/parts";
7
5
  import { Markdown } from "../markdown/markdown";
8
- import { LiveDuration } from "./run-item-primitives";
6
+ import { RunRowShell } from "./run-row-shell";
7
+ import { ExpandableContent } from "./expandable-content";
9
8
 
10
9
  export interface InlineThinkingItemProps {
11
10
  part: ReasoningPart;
@@ -28,7 +27,8 @@ export const InlineThinkingItem = memo(
28
27
 
29
28
  const startTime = part.time?.start;
30
29
  const endTime = part.time?.end;
31
- const durationMs = startTime && endTime ? endTime - startTime : undefined;
30
+ const durationMs =
31
+ startTime != null && endTime != null ? endTime - startTime : undefined;
32
32
  const isActive = startTime != null && endTime == null;
33
33
  const preview = part.text ? truncateText(part.text, 120) : undefined;
34
34
 
@@ -50,70 +50,34 @@ export const InlineThinkingItem = memo(
50
50
  }, [autoCollapse, durationMs, isActive]);
51
51
 
52
52
  return (
53
- <Collapsible.Root open={open} onOpenChange={setOpen}>
54
- <div
55
- className={cn(
56
- "overflow-hidden rounded-[var(--radius-lg)] border bg-card transition-colors",
57
- isActive
58
- ? "border-[var(--border-accent)]"
59
- : "border-border hover:border-[var(--border-accent)]",
60
- className,
61
- )}
62
- >
63
- <Collapsible.Trigger asChild>
64
- <button
65
- className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm cursor-pointer"
66
- >
67
- <div
68
- className={cn(
69
- "flex h-6 w-6 shrink-0 items-center justify-center rounded-[var(--radius-sm)] border",
70
- isActive
71
- ? "border-[var(--border-accent)] bg-[var(--accent-surface-soft)] text-primary"
72
- : "border-border bg-muted text-muted-foreground",
73
- )}
74
- >
75
- <Brain className={cn("h-3 w-3 shrink-0", isActive && "animate-pulse")} />
76
- </div>
77
-
78
- <p className="min-w-0 flex-1 truncate font-[var(--font-sans)] text-muted-foreground">
79
- {preview ?? (isActive ? "Thinking…" : "Reasoning")}
80
- </p>
81
-
82
- <div className="flex shrink-0 items-center gap-2">
83
- {isActive && startTime ? <LiveDuration startTime={startTime} /> : null}
84
- {!isActive && durationMs != null ? (
85
- <span className="shrink-0 text-xs tabular-nums text-muted-foreground">
86
- {formatDuration(durationMs)}
87
- </span>
88
- ) : null}
89
- <ChevronRight
90
- className={cn(
91
- "h-3 w-3 text-muted-foreground transition-transform shrink-0",
92
- open && "rotate-90",
93
- )}
94
- />
95
- </div>
96
- </button>
97
- </Collapsible.Trigger>
98
-
99
- <Collapsible.Content className="overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp">
100
- {part.text ? (
101
- <div
102
- className={cn(
103
- "max-h-60 overflow-y-auto border-t border-border bg-muted px-3 py-3 text-sm leading-relaxed text-muted-foreground",
104
- contentClassName,
105
- )}
106
- >
107
- <Markdown>{part.text}</Markdown>
108
- </div>
109
- ) : (
110
- <div className="border-t border-border bg-muted px-3 py-2.5 text-xs text-muted-foreground">
111
- No reasoning text provided.
112
- </div>
113
- )}
114
- </Collapsible.Content>
115
- </div>
116
- </Collapsible.Root>
53
+ <RunRowShell
54
+ icon={<Brain className="h-3.5 w-3.5" />}
55
+ title={isActive ? "Thinking…" : "Reasoning"}
56
+ description={preview}
57
+ status={isActive ? "running" : "idle"}
58
+ startTime={startTime}
59
+ durationMs={durationMs}
60
+ open={open}
61
+ onOpenChange={setOpen}
62
+ className={className}
63
+ >
64
+ {part.text ? (
65
+ <ExpandableContent
66
+ fadeClassName="from-[var(--md3-surface-container)]"
67
+ className={
68
+ contentClassName ? `px-3 py-3 ${contentClassName}` : "px-3 py-3"
69
+ }
70
+ >
71
+ <div className="text-sm leading-relaxed text-foreground">
72
+ <Markdown>{part.text}</Markdown>
73
+ </div>
74
+ </ExpandableContent>
75
+ ) : (
76
+ <div className="px-3 py-2.5 text-xs text-muted-foreground">
77
+ No reasoning text provided.
78
+ </div>
79
+ )}
80
+ </RunRowShell>
117
81
  );
118
82
  },
119
83
  );