Skip to content

Commit 9f3c63a

Browse files
authored
feat(ui): render Markdown syntax inside trajectories (#1555)
1 parent 8d060da commit 9f3c63a

File tree

9 files changed

+131
-49
lines changed

9 files changed

+131
-49
lines changed

apps/agentstack-sdk-py/examples/trajectory.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,74 @@ async def example_agent(
3939
yield metadata
4040
await context.store(AgentMessage(metadata=metadata))
4141

42+
metadata = trajectory.trajectory_metadata(
43+
title="Test Markdown rendering",
44+
content="""
45+
# 🧭 Trajectory Markdown Rendering Test
46+
47+
This document tests **Markdown rendering** capabilities within the trajectory feature.
48+
49+
---
50+
51+
## 🧩 Section 1: Headers and Text Formatting
52+
53+
### Header Level 3
54+
55+
You should see **bold**, *italic*, and ***bold italic*** text properly rendered.
56+
> This is a blockquote — it should appear indented and stylized.
57+
58+
Need Markdown basics? Check out [Markdown Guide](https://www.markdownguide.org/basic-syntax/).
59+
60+
---
61+
62+
## 🧾 Section 2: Lists
63+
64+
### Unordered List
65+
- Apple 🍎 — [Learn more about apples](https://en.wikipedia.org/wiki/Apple)
66+
- Banana 🍌 — [Banana facts](https://en.wikipedia.org/wiki/Banana)
67+
- Cherry 🍒
68+
69+
### Ordered List
70+
1. First item
71+
2. Second item
72+
3. Third item
73+
74+
### Nested List
75+
- Outer item
76+
- Inner item
77+
- Deep inner item
78+
79+
---
80+
81+
## 📊 Section 3: Tables
82+
83+
| Entity Type | Example Value | Confidence | Reference |
84+
|--------------|------------------|-------------|------------|
85+
| **Name** | Alice Johnson | 0.97 | [Details](https://example.com) |
86+
| **Date** | 2025-11-12 | 0.88 | [Details](https://example.com) |
87+
| **Location** | San Francisco, CA | 0.91 | [Details](https://example.com) |
88+
89+
---
90+
91+
## 💻 Section 4: Code Blocks
92+
93+
### Inline Code
94+
You can include inline code like `const result = extractEntities(text);`.
95+
96+
### Fenced Code Block
97+
```python
98+
def extract_entities(text):
99+
entities = {
100+
"name": "Alice Johnson",
101+
"date": "2025-11-12",
102+
"location": "San Francisco"
103+
}
104+
return entities
105+
""",
106+
)
107+
yield metadata
108+
await context.store(AgentMessage(metadata=metadata))
109+
42110
await asyncio.sleep(1)
43111

44112
metadata = trajectory.trajectory_metadata(title="Searching the web", content="Searching...", group_id="websearch")

apps/agentstack-ui/src/components/LineClampText/LineClampText.module.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
@include line-clamp(0);
1717
}
1818

19+
.sentinel {
20+
display: block;
21+
inline-size: 1px;
22+
block-size: 1px;
23+
}
24+
1925
.button {
2026
display: flex;
2127
align-items: flex-start;

apps/agentstack-ui/src/components/LineClampText/LineClampText.tsx

Lines changed: 23 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import { OverflowMenuHorizontal } from '@carbon/icons-react';
77
import { IconButton } from '@carbon/react';
88
import clsx from 'clsx';
99
import type { CSSProperties, PropsWithChildren } from 'react';
10-
import { useCallback, useEffect, useId, useRef, useState } from 'react';
11-
import { useDebounceCallback } from 'usehooks-ts';
10+
import { useEffect, useId, useRef, useState } from 'react';
1211

1312
import { ExpandButton } from '#components/ExpandButton/ExpandButton.tsx';
1413

@@ -32,8 +31,12 @@ export function LineClampText({
3231
}: PropsWithChildren<Props>) {
3332
const id = useId();
3433
const textRef = useRef<HTMLDivElement>(null);
34+
const sentinelRef = useRef<HTMLSpanElement>(null);
35+
3536
const [isExpanded, setIsExpanded] = useState(false);
36-
const [showButton, setShowButton] = useState(false);
37+
const [overflowDetected, setOverflowDetected] = useState(false);
38+
39+
const showButton = isExpanded || overflowDetected;
3740

3841
const Component = useBlockElement ? 'div' : 'span';
3942
const buttonProps = {
@@ -43,54 +46,30 @@ export function LineClampText({
4346
};
4447
const buttonLabel = isExpanded ? 'View less' : 'View more';
4548

46-
const checkOverflow = useCallback(() => {
47-
const element = textRef.current;
48-
49-
if (!element) {
50-
return;
51-
}
52-
53-
const { scrollHeight } = element;
54-
55-
if (scrollHeight === 0) {
56-
return;
57-
}
58-
59-
const lineHeight = parseFloat(getComputedStyle(element).lineHeight);
60-
const height = lineHeight * lines;
61-
62-
if (scrollHeight > height) {
63-
setShowButton(true);
64-
} else {
65-
setShowButton(false);
66-
}
67-
}, [lines]);
68-
69-
const debouncedCheckOverflow = useDebounceCallback(checkOverflow, 200);
70-
7149
useEffect(() => {
72-
const element = textRef.current;
50+
const textElement = textRef.current;
51+
const sentinelElement = sentinelRef.current;
7352

74-
if (!element) {
53+
if (isExpanded || !textElement || !sentinelElement) {
7554
return;
7655
}
7756

78-
const resizeObserver = new ResizeObserver(() => {
79-
debouncedCheckOverflow();
80-
});
57+
const observer = new IntersectionObserver(
58+
([entry]) => {
59+
setOverflowDetected(!entry.isIntersecting);
60+
},
61+
{
62+
root: textElement,
63+
threshold: 1,
64+
},
65+
);
8166

82-
resizeObserver.observe(element);
67+
observer.observe(sentinelElement);
8368

8469
return () => {
85-
if (element) {
86-
resizeObserver.unobserve(element);
87-
}
70+
observer.disconnect();
8871
};
89-
}, [debouncedCheckOverflow]);
90-
91-
useEffect(() => {
92-
checkOverflow();
93-
}, [checkOverflow]);
72+
}, [isExpanded]);
9473

9574
return (
9675
<Component className={clsx(classes.root, className)}>
@@ -101,6 +80,8 @@ export function LineClampText({
10180
style={{ '--line-clamp-lines': lines } as CSSProperties}
10281
>
10382
{children}
83+
84+
<span ref={sentinelRef} className={classes.sentinel} />
10485
</Component>
10586

10687
{showButton && (

apps/agentstack-ui/src/components/MarkdownContent/MarkdownContent.module.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
background: $border-subtle-00;
8484
block-size: 1px;
8585
border: none;
86+
margin-block-end: 0;
8687
}
8788

8889
pre {

apps/agentstack-ui/src/modules/agents/components/detail/AgentDetailPanel.module.scss

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@
2424
row-gap: $spacing-03;
2525
}
2626

27-
.author {
28-
font-size: rem(14px);
29-
}
30-
3127
.docsLink {
3228
display: flex;
3329
align-items: center;

apps/agentstack-ui/src/modules/agents/components/detail/AgentDetailPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function AgentDetailPanel() {
5151
{!isPending ? (
5252
<>
5353
<div className={classes.mainInfo}>
54-
{description && <MarkdownContent className={classes.description}>{description}</MarkdownContent>}
54+
{description && <MarkdownContent>{description}</MarkdownContent>}
5555

5656
{(author || contributors) && <AgentCredits author={author} contributors={contributors} />}
5757
</div>

apps/agentstack-ui/src/modules/trajectories/components/TrajectoryItem.module.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,9 @@
2222
color: $text-secondary;
2323
letter-spacing: $letter-spacing;
2424
}
25+
26+
.content {
27+
font-size: inherit;
28+
line-height: inherit;
29+
letter-spacing: inherit;
30+
}

apps/agentstack-ui/src/modules/trajectories/components/TrajectoryItem.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { match } from 'ts-pattern';
1010

1111
import { CodeSnippet } from '#components/CodeSnippet/CodeSnippet.tsx';
1212
import { LineClampText } from '#components/LineClampText/LineClampText.tsx';
13+
import { MarkdownContent } from '#components/MarkdownContent/MarkdownContent.tsx';
1314
import type { UITrajectoryPart } from '#modules/messages/types.ts';
1415
import { maybeParseJson } from '#modules/runs/utils.ts';
1516
import { fadeProps } from '#utils/fadeProps.ts';
@@ -38,8 +39,8 @@ export function TrajectoryItem({ trajectory }: Props) {
3839
{parsed.map((item, idx) =>
3940
match(item)
4041
.with({ type: 'string' }, ({ value }) => (
41-
<LineClampText lines={5} key={idx}>
42-
{value}
42+
<LineClampText lines={5} key={idx} useBlockElement>
43+
<MarkdownContent className={classes.content}>{value}</MarkdownContent>
4344
</LineClampText>
4445
))
4546
.otherwise(({ value }) => {

docs/extensions/trajectory.mdx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,29 @@ async def my_agent(
3030
yield "Final result"
3131
```
3232

33+
## Markdown Support
34+
The `content` field of `trajectory_metadata` supports Markdown, which is rendered directly in the UI.
35+
36+
Supported elements include:
37+
- Headers
38+
- Bold and italic text
39+
- Ordered and unordered lists
40+
- Tables
41+
- Code blocks
42+
- Links
43+
44+
```python
45+
yield trajectory.trajectory_metadata(
46+
title="Checklist",
47+
content="""
48+
- Load data
49+
- Validate schema
50+
- Run inference
51+
- Generate report
52+
"""
53+
)
54+
```
55+
3356
## Common Patterns
3457

3558
**Progress Steps:**

0 commit comments

Comments
 (0)