Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions apps/agentstack-sdk-py/examples/trajectory.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,74 @@ async def example_agent(
yield metadata
await context.store(AgentMessage(metadata=metadata))

metadata = trajectory.trajectory_metadata(
title="Test Markdown rendering",
content="""
# 🧭 Trajectory Markdown Rendering Test

This document tests **Markdown rendering** capabilities within the trajectory feature.

---

## 🧩 Section 1: Headers and Text Formatting

### Header Level 3

You should see **bold**, *italic*, and ***bold italic*** text properly rendered.
> This is a blockquote — it should appear indented and stylized.

Need Markdown basics? Check out [Markdown Guide](https://www.markdownguide.org/basic-syntax/).

---

## 🧾 Section 2: Lists

### Unordered List
- Apple 🍎 — [Learn more about apples](https://en.wikipedia.org/wiki/Apple)
- Banana 🍌 — [Banana facts](https://en.wikipedia.org/wiki/Banana)
- Cherry 🍒

### Ordered List
1. First item
2. Second item
3. Third item

### Nested List
- Outer item
- Inner item
- Deep inner item

---

## 📊 Section 3: Tables

| Entity Type | Example Value | Confidence | Reference |
|--------------|------------------|-------------|------------|
| **Name** | Alice Johnson | 0.97 | [Details](https://example.com) |
| **Date** | 2025-11-12 | 0.88 | [Details](https://example.com) |
| **Location** | San Francisco, CA | 0.91 | [Details](https://example.com) |

---

## 💻 Section 4: Code Blocks

### Inline Code
You can include inline code like `const result = extractEntities(text);`.

### Fenced Code Block
```python
def extract_entities(text):
entities = {
"name": "Alice Johnson",
"date": "2025-11-12",
"location": "San Francisco"
}
return entities
""",
)
yield metadata
await context.store(AgentMessage(metadata=metadata))

await asyncio.sleep(1)

metadata = trajectory.trajectory_metadata(title="Searching the web", content="Searching...", group_id="websearch")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
@include line-clamp(0);
}

.sentinel {
display: block;
inline-size: 1px;
block-size: 1px;
}

.button {
display: flex;
align-items: flex-start;
Expand Down
65 changes: 23 additions & 42 deletions apps/agentstack-ui/src/components/LineClampText/LineClampText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { OverflowMenuHorizontal } from '@carbon/icons-react';
import { IconButton } from '@carbon/react';
import clsx from 'clsx';
import type { CSSProperties, PropsWithChildren } from 'react';
import { useCallback, useEffect, useId, useRef, useState } from 'react';
import { useDebounceCallback } from 'usehooks-ts';
import { useEffect, useId, useRef, useState } from 'react';

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

Expand All @@ -32,8 +31,12 @@ export function LineClampText({
}: PropsWithChildren<Props>) {
const id = useId();
const textRef = useRef<HTMLDivElement>(null);
const sentinelRef = useRef<HTMLSpanElement>(null);

const [isExpanded, setIsExpanded] = useState(false);
const [showButton, setShowButton] = useState(false);
const [overflowDetected, setOverflowDetected] = useState(false);

const showButton = isExpanded || overflowDetected;

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

const checkOverflow = useCallback(() => {
const element = textRef.current;

if (!element) {
return;
}

const { scrollHeight } = element;

if (scrollHeight === 0) {
return;
}

const lineHeight = parseFloat(getComputedStyle(element).lineHeight);
const height = lineHeight * lines;

if (scrollHeight > height) {
setShowButton(true);
} else {
setShowButton(false);
}
}, [lines]);

const debouncedCheckOverflow = useDebounceCallback(checkOverflow, 200);

useEffect(() => {
const element = textRef.current;
const textElement = textRef.current;
const sentinelElement = sentinelRef.current;

if (!element) {
if (isExpanded || !textElement || !sentinelElement) {
return;
}

const resizeObserver = new ResizeObserver(() => {
debouncedCheckOverflow();
});
const observer = new IntersectionObserver(
([entry]) => {
setOverflowDetected(!entry.isIntersecting);
},
{
root: textElement,
threshold: 1,
},
);

resizeObserver.observe(element);
observer.observe(sentinelElement);

return () => {
if (element) {
resizeObserver.unobserve(element);
}
observer.disconnect();
};
}, [debouncedCheckOverflow]);

useEffect(() => {
checkOverflow();
}, [checkOverflow]);
}, [isExpanded]);

return (
<Component className={clsx(classes.root, className)}>
Expand All @@ -101,6 +80,8 @@ export function LineClampText({
style={{ '--line-clamp-lines': lines } as CSSProperties}
>
{children}

<span ref={sentinelRef} className={classes.sentinel} />
</Component>

{showButton && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
background: $border-subtle-00;
block-size: 1px;
border: none;
margin-block-end: 0;
}

pre {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@
row-gap: $spacing-03;
}

.author {
font-size: rem(14px);
}

.docsLink {
display: flex;
align-items: center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function AgentDetailPanel() {
{!isPending ? (
<>
<div className={classes.mainInfo}>
{description && <MarkdownContent className={classes.description}>{description}</MarkdownContent>}
{description && <MarkdownContent>{description}</MarkdownContent>}

{(author || contributors) && <AgentCredits author={author} contributors={contributors} />}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@
color: $text-secondary;
letter-spacing: $letter-spacing;
}

.content {
font-size: inherit;
line-height: inherit;
letter-spacing: inherit;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { match } from 'ts-pattern';

import { CodeSnippet } from '#components/CodeSnippet/CodeSnippet.tsx';
import { LineClampText } from '#components/LineClampText/LineClampText.tsx';
import { MarkdownContent } from '#components/MarkdownContent/MarkdownContent.tsx';
import type { UITrajectoryPart } from '#modules/messages/types.ts';
import { maybeParseJson } from '#modules/runs/utils.ts';
import { fadeProps } from '#utils/fadeProps.ts';
Expand Down Expand Up @@ -38,8 +39,8 @@ export function TrajectoryItem({ trajectory }: Props) {
{parsed.map((item, idx) =>
match(item)
.with({ type: 'string' }, ({ value }) => (
<LineClampText lines={5} key={idx}>
{value}
<LineClampText lines={5} key={idx} useBlockElement>
<MarkdownContent className={classes.content}>{value}</MarkdownContent>
</LineClampText>
))
.otherwise(({ value }) => {
Expand Down
23 changes: 23 additions & 0 deletions docs/extensions/trajectory.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@ async def my_agent(
yield "Final result"
```

## Markdown Support
The `content` field of `trajectory_metadata` supports Markdown, which is rendered directly in the UI.

Supported elements include:
- Headers
- Bold and italic text
- Ordered and unordered lists
- Tables
- Code blocks
- Links

```python
yield trajectory.trajectory_metadata(
title="Checklist",
content="""
- Load data
- Validate schema
- Run inference
- Generate report
"""
)
```

## Common Patterns

**Progress Steps:**
Expand Down