feat(chat): use Streamdown for markdown rendering in chat messages
- Add streamdown package for AI-optimized markdown streaming - Update dashboard and public agent chat pages to render messages with Streamdown - Enables proper markdown formatting (code blocks, lists, etc.) in chat responses
This commit is contained in:
parent
27e7d45646
commit
1c903cd26e
5 changed files with 252 additions and 5 deletions
|
|
@ -57,6 +57,7 @@
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"resend": "^6.7.0",
|
"resend": "^6.7.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
|
"streamdown": "^1.6.11",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import Link from 'next/link';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
|
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
|
||||||
|
import { Streamdown } from 'streamdown';
|
||||||
import { AppHeader } from '~/components/AppHeader';
|
import { AppHeader } from '~/components/AppHeader';
|
||||||
import { LikeButton } from '~/components/LikeButton';
|
import { LikeButton } from '~/components/LikeButton';
|
||||||
|
|
||||||
|
|
@ -807,7 +808,9 @@ export default function PublicAgentChatPage(): React.ReactElement {
|
||||||
{streamingContent && (
|
{streamingContent && (
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<div className="max-w-[80%] rounded-lg p-4 bg-surface-secondary">
|
<div className="max-w-[80%] rounded-lg p-4 bg-surface-secondary">
|
||||||
<p className="whitespace-pre-wrap text-sm">{streamingContent}</p>
|
<div className="text-sm prose prose-sm dark:prose-invert max-w-none">
|
||||||
|
<Streamdown>{streamingContent}</Streamdown>
|
||||||
|
</div>
|
||||||
<span className="inline-block w-2 h-4 bg-primary animate-pulse ml-1" />
|
<span className="inline-block w-2 h-4 bg-primary animate-pulse ml-1" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -884,7 +887,9 @@ export default function PublicAgentChatPage(): React.ReactElement {
|
||||||
{message.role === 'USER' && (
|
{message.role === 'USER' && (
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<div className="max-w-[80%] rounded-lg p-4 bg-primary text-primary-foreground">
|
<div className="max-w-[80%] rounded-lg p-4 bg-primary text-primary-foreground">
|
||||||
<p className="whitespace-pre-wrap text-sm">{message.content}</p>
|
<div className="text-sm prose prose-sm prose-invert max-w-none">
|
||||||
|
<Streamdown>{message.content}</Streamdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -896,7 +901,9 @@ export default function PublicAgentChatPage(): React.ReactElement {
|
||||||
{message.content && (
|
{message.content && (
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<div className="max-w-[80%] rounded-lg p-4 bg-surface-secondary">
|
<div className="max-w-[80%] rounded-lg p-4 bg-surface-secondary">
|
||||||
<p className="whitespace-pre-wrap text-sm">{message.content}</p>
|
<div className="text-sm prose prose-sm dark:prose-invert max-w-none">
|
||||||
|
<Streamdown>{message.content}</Streamdown>
|
||||||
|
</div>
|
||||||
{/* Token usage for debugging */}
|
{/* Token usage for debugging */}
|
||||||
{(message.inputTokens || message.outputTokens) && (
|
{(message.inputTokens || message.outputTokens) && (
|
||||||
<div className="mt-2 pt-2 border-t border-border/50 text-[10px] text-foreground-tertiary font-mono">
|
<div className="mt-2 pt-2 border-t border-border/50 text-[10px] text-foreground-tertiary font-mono">
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { Streamdown } from 'streamdown';
|
||||||
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
|
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
|
||||||
|
|
||||||
interface Agent {
|
interface Agent {
|
||||||
|
|
@ -678,7 +679,9 @@ export default function AgentChatPage(): React.ReactElement {
|
||||||
: 'bg-surface-secondary'
|
: 'bg-surface-secondary'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<p className="whitespace-pre-wrap text-sm">{message.content}</p>
|
<div className="text-sm prose prose-sm dark:prose-invert max-w-none">
|
||||||
|
<Streamdown>{message.content}</Streamdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -707,7 +710,9 @@ export default function AgentChatPage(): React.ReactElement {
|
||||||
{streamingContent && (
|
{streamingContent && (
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<div className="max-w-[80%] rounded-lg p-4 bg-surface-secondary">
|
<div className="max-w-[80%] rounded-lg p-4 bg-surface-secondary">
|
||||||
<p className="whitespace-pre-wrap text-sm">{streamingContent}</p>
|
<div className="text-sm prose prose-sm dark:prose-invert max-w-none">
|
||||||
|
<Streamdown>{streamingContent}</Streamdown>
|
||||||
|
</div>
|
||||||
<span className="inline-block w-2 h-4 bg-primary animate-pulse ml-1" />
|
<span className="inline-block w-2 h-4 bg-primary animate-pulse ml-1" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
|
@ -338,6 +338,9 @@ importers:
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^2.0.7
|
specifier: ^2.0.7
|
||||||
version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
|
streamdown:
|
||||||
|
specifier: ^1.6.11
|
||||||
|
version: 1.6.11(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.3)
|
||||||
zod:
|
zod:
|
||||||
specifier: ^4.3.5
|
specifier: ^4.3.5
|
||||||
version: 4.3.5
|
version: 4.3.5
|
||||||
|
|
|
||||||
231
sprites_skill.md
Normal file
231
sprites_skill.md
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
# Sprites Tools Development & Testing Guide
|
||||||
|
|
||||||
|
This document covers how to build, publish, flush caches, and test the TPMJS sprites tools.
|
||||||
|
|
||||||
|
## Credentials
|
||||||
|
|
||||||
|
### TPMJS API Key
|
||||||
|
```
|
||||||
|
<your-tpmjs-api-key>
|
||||||
|
```
|
||||||
|
|
||||||
|
### SPRITES_TOKEN (set in collection env vars)
|
||||||
|
```
|
||||||
|
<your-sprites-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### CRON_SECRET (for triggering syncs)
|
||||||
|
```
|
||||||
|
<your-cron-secret>
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Endpoint
|
||||||
|
|
||||||
|
The sprites collection MCP endpoint:
|
||||||
|
```
|
||||||
|
https://tpmjs.com/api/mcp/ajax/sprites/http
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Sprites Tools
|
||||||
|
|
||||||
|
| Tool | Package | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `sprites-list` | `@tpmjs/tools-sprites-list` | List all sprites with status |
|
||||||
|
| `sprites-get` | `@tpmjs/tools-sprites-get` | Get a specific sprite's details |
|
||||||
|
| `sprites-create` | `@tpmjs/tools-sprites-create` | Create a new sprite |
|
||||||
|
| `sprites-delete` | `@tpmjs/tools-sprites-delete` | Delete a sprite |
|
||||||
|
| `sprites-exec` | `@tpmjs/tools-sprites-exec` | Execute commands in a sprite |
|
||||||
|
| `sprites-sessions` | `@tpmjs/tools-sprites-sessions` | List active sessions |
|
||||||
|
| `sprites-policy-get` | `@tpmjs/tools-sprites-policy-get` | Get sprite policy |
|
||||||
|
| `sprites-checkpoint-list` | `@tpmjs/tools-sprites-checkpoint-list` | List checkpoints |
|
||||||
|
| `sprites-checkpoint-create` | `@tpmjs/tools-sprites-checkpoint-create` | Create a checkpoint |
|
||||||
|
| `sprites-checkpoint-restore` | `@tpmjs/tools-sprites-checkpoint-restore` | Restore from checkpoint |
|
||||||
|
|
||||||
|
## Building a Sprites Tool
|
||||||
|
|
||||||
|
1. Navigate to the tool directory:
|
||||||
|
```bash
|
||||||
|
cd packages/tools/official/sprites-exec
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Build:
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Type-check:
|
||||||
|
```bash
|
||||||
|
pnpm type-check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publishing a Sprites Tool
|
||||||
|
|
||||||
|
1. Bump version in `package.json`:
|
||||||
|
```json
|
||||||
|
"version": "0.1.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Build and publish:
|
||||||
|
```bash
|
||||||
|
cd packages/tools/official/sprites-exec
|
||||||
|
pnpm build
|
||||||
|
npm publish
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Commit the version bump:
|
||||||
|
```bash
|
||||||
|
git add package.json
|
||||||
|
git commit -m "chore: bump sprites-exec to 0.1.5"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syncing to Database
|
||||||
|
|
||||||
|
After publishing, sync the package to the TPMJS database:
|
||||||
|
|
||||||
|
### Changes Feed Sync (fast, picks up recent npm changes)
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://tpmjs.com/api/sync/changes" \
|
||||||
|
-H "Authorization: Bearer <your-cron-secret>" \
|
||||||
|
-H "Content-Type: application/json"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keyword Sync (comprehensive, searches for all tpmjs packages)
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://tpmjs.com/api/sync/keyword" \
|
||||||
|
-H "Authorization: Bearer <your-cron-secret>" \
|
||||||
|
-H "Content-Type: application/json"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check version in database
|
||||||
|
```bash
|
||||||
|
curl -s "https://tpmjs.com/api/tools?q=sprites-exec" | jq '.data[] | select(.package.npmPackageName == "@tpmjs/tools-sprites-exec") | {name: .name, version: .package.npmVersion}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flushing Executor Caches
|
||||||
|
|
||||||
|
**CRITICAL**: The executor caches npm packages. After publishing a new version, you MUST flush the cache.
|
||||||
|
|
||||||
|
### Flush Primary Executor (executor.tpmjs.com)
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://executor.tpmjs.com/cache/clear" | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flush Railway Executor (backup)
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://endearing-commitment-production.up.railway.app/cache/clear" | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Cache Stats
|
||||||
|
```bash
|
||||||
|
curl -s "https://executor.tpmjs.com/cache/stats" | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing via MCP Endpoint
|
||||||
|
|
||||||
|
### List All Tools
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://tpmjs.com/api/mcp/ajax/sprites/http" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer <your-tpmjs-api-key>" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | jq '.result.tools[] | {name: .name}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test sprites-list
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://tpmjs.com/api/mcp/ajax/sprites/http" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer <your-tpmjs-api-key>" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"tpmjs-tools-sprites-list--spritesListTool","arguments":{}}}' | jq '.result'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test sprites-exec (simple command)
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://tpmjs.com/api/mcp/ajax/sprites/http" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer <your-tpmjs-api-key>" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"tpmjs-tools-sprites-exec--spritesExecTool","arguments":{"name":"hello-script-sprite","cmd":"echo hello"}}}' | jq '.result'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test sprites-exec (shell operators)
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://tpmjs.com/api/mcp/ajax/sprites/http" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer <your-tpmjs-api-key>" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"tpmjs-tools-sprites-exec--spritesExecTool","arguments":{"name":"hello-script-sprite","cmd":"echo foo && echo bar"}}}' | jq '.result'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test sprites-get
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://tpmjs.com/api/mcp/ajax/sprites/http" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer <your-tpmjs-api-key>" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"tpmjs-tools-sprites-get--spritesGetTool","arguments":{"name":"hello-script-sprite"}}}' | jq '.result'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test sprites-create
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://tpmjs.com/api/mcp/ajax/sprites/http" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer <your-tpmjs-api-key>" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"tpmjs-tools-sprites-create--spritesCreateTool","arguments":{"name":"test-sprite-123"}}}' | jq '.result'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test sprites-delete
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://tpmjs.com/api/mcp/ajax/sprites/http" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer <your-tpmjs-api-key>" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"tpmjs-tools-sprites-delete--spritesDeleteTool","arguments":{"name":"test-sprite-123"}}}' | jq '.result'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing via Agent Conversation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://tpmjs.com/api/ajax/agents/sprites/conversation/test-conv?format=json" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer <your-tpmjs-api-key>" \
|
||||||
|
-d '{"message":"List all sprites"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Workflow: Publish + Sync + Flush + Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Build and publish
|
||||||
|
cd packages/tools/official/sprites-exec
|
||||||
|
pnpm build && npm publish
|
||||||
|
|
||||||
|
# 2. Trigger sync
|
||||||
|
curl -s -X POST "https://tpmjs.com/api/sync/changes" \
|
||||||
|
-H "Authorization: Bearer <your-cron-secret>"
|
||||||
|
|
||||||
|
# 3. Flush caches
|
||||||
|
curl -s -X POST "https://executor.tpmjs.com/cache/clear"
|
||||||
|
curl -s -X POST "https://endearing-commitment-production.up.railway.app/cache/clear"
|
||||||
|
|
||||||
|
# 4. Test
|
||||||
|
curl -s -X POST "https://tpmjs.com/api/mcp/ajax/sprites/http" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer <your-tpmjs-api-key>" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"tpmjs-tools-sprites-exec--spritesExecTool","arguments":{"name":"hello-script-sprite","cmd":"echo test"}}}' | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known Issues & Fixes
|
||||||
|
|
||||||
|
### Issue: "Failed to parse response from Sprites API"
|
||||||
|
**Cause**: Sprites API returns JSON error `{"error":"..."}` with HTTP 200 instead of proper error codes.
|
||||||
|
**Fix**: Added JSON error detection in sprites-exec v0.1.4.
|
||||||
|
|
||||||
|
### Issue: "Invalid sprite status: cold"
|
||||||
|
**Cause**: Sprites API returns "cold" status for idle sprites, which wasn't recognized.
|
||||||
|
**Fix**: Added "cold" to valid statuses in sprites-list v0.1.3.
|
||||||
|
|
||||||
|
### Issue: "The update command takes no arguments"
|
||||||
|
**Cause**: Commands with shell operators (&&, ||, |, etc.) were being parsed incorrectly.
|
||||||
|
**Fix**: Added shell operator detection and `sh -c` wrapping in sprites-exec v0.1.5.
|
||||||
|
|
||||||
|
### Issue: Old version still being used after publish
|
||||||
|
**Cause**: Deno HTTP import cache + executor module cache.
|
||||||
|
**Fix**:
|
||||||
|
1. Pass explicit version from database (deployed to web app)
|
||||||
|
2. Flush executor caches after publishing
|
||||||
Loading…
Add table
Add a link
Reference in a new issue