Skip to content

Commit 14a2921

Browse files
committed
feat: upgrade streamdown to v2.5.0 with custom Trigger.dev Shiki theme
- Upgrade streamdown from v1.4.0 to v2.5.0 with @streamdown/code plugin - Custom Shiki theme matching the Trigger.dev VS Code dark theme colors - Consolidate duplicated lazy StreamdownRenderer into shared component - Patch streamdown to inline highlighted body (fixes Arc browser) - Add streamdown storybook page for visual testing - Handle AGENT triggerSource in TestTaskPresenter exhaustive switch - Update Tailwind config to scan streamdown dist for utility classes
1 parent a59dd9e commit 14a2921

File tree

23 files changed

+1235
-86
lines changed

23 files changed

+1235
-86
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Upgrade streamdown from v1.4.0 to v2.5.0. Custom Shiki syntax highlighting theme matching our CodeMirror dark theme colors. Consolidate duplicated lazy StreamdownRenderer into a shared component.

apps/webapp/app/components/code/AIQueryInput.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
11
import { CheckIcon, PencilSquareIcon, PlusIcon, XMarkIcon } from "@heroicons/react/20/solid";
22
import { AnimatePresence, motion } from "framer-motion";
3-
import { Suspense, lazy, useCallback, useEffect, useRef, useState } from "react";
3+
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
44
import { Button } from "~/components/primitives/Buttons";
55
import { Spinner } from "~/components/primitives/Spinner";
6+
import { StreamdownRenderer } from "~/components/code/StreamdownRenderer";
67
import { useEnvironment } from "~/hooks/useEnvironment";
78
import { useOrganization } from "~/hooks/useOrganizations";
89
import { useProject } from "~/hooks/useProject";
910
import type { AITimeFilter } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/types";
1011
import { cn } from "~/utils/cn";
1112

12-
// Lazy load streamdown components to avoid SSR issues
13-
const StreamdownRenderer = lazy(() =>
14-
import("streamdown").then((mod) => ({
15-
default: ({ children, isAnimating }: { children: string; isAnimating: boolean }) => (
16-
<mod.ShikiThemeContext.Provider value={["one-dark-pro", "one-dark-pro"]}>
17-
<mod.Streamdown isAnimating={isAnimating}>{children}</mod.Streamdown>
18-
</mod.ShikiThemeContext.Provider>
19-
),
20-
}))
21-
);
22-
2313
type StreamEventType =
2414
| { type: "thinking"; content: string }
2515
| { type: "tool_call"; tool: string; args: unknown }
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { lazy } from "react";
2+
import type { CodeHighlighterPlugin } from "streamdown";
3+
4+
export const StreamdownRenderer = lazy(() =>
5+
Promise.all([import("streamdown"), import("@streamdown/code"), import("./shikiTheme")]).then(
6+
([{ Streamdown }, { createCodePlugin }, { triggerDarkTheme }]) => {
7+
// Type assertion needed: @streamdown/code and streamdown resolve different shiki
8+
// versions under pnpm, causing structurally-identical CodeHighlighterPlugin types
9+
// to be considered incompatible (different BundledLanguage string unions).
10+
const codePlugin = createCodePlugin({
11+
themes: [triggerDarkTheme, triggerDarkTheme],
12+
}) as unknown as CodeHighlighterPlugin;
13+
14+
return {
15+
default: ({
16+
children,
17+
isAnimating = false,
18+
}: {
19+
children: string;
20+
isAnimating?: boolean;
21+
}) => (
22+
<Streamdown isAnimating={isAnimating} plugins={{ code: codePlugin }}>
23+
{children}
24+
</Streamdown>
25+
),
26+
};
27+
}
28+
)
29+
);
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import type { ThemeRegistrationAny } from "streamdown";
2+
3+
// Custom Shiki theme matching the Trigger.dev VS Code dark theme.
4+
// Colors taken directly from the VS Code extension's tokenColors.
5+
export const triggerDarkTheme: ThemeRegistrationAny = {
6+
name: "trigger-dark",
7+
type: "dark",
8+
colors: {
9+
"editor.background": "#212327",
10+
"editor.foreground": "#878C99",
11+
"editorLineNumber.foreground": "#484c54",
12+
},
13+
tokenColors: [
14+
// Control flow keywords: pink-purple
15+
{
16+
scope: [
17+
"keyword.control",
18+
"keyword.operator.delete",
19+
"keyword.other.using",
20+
"keyword.other.operator",
21+
"entity.name.operator",
22+
],
23+
settings: { foreground: "#E888F8" },
24+
},
25+
// Storage type (const, let, var, function, class): purple
26+
{
27+
scope: "storage.type",
28+
settings: { foreground: "#8271ED" },
29+
},
30+
// Storage modifiers (async, export, etc.): purple
31+
{
32+
scope: ["storage.modifier", "keyword.operator.noexcept"],
33+
settings: { foreground: "#8271ED" },
34+
},
35+
// Keyword operator expressions (new, typeof, instanceof, etc.): purple
36+
{
37+
scope: [
38+
"keyword.operator.new",
39+
"keyword.operator.expression",
40+
"keyword.operator.cast",
41+
"keyword.operator.sizeof",
42+
"keyword.operator.instanceof",
43+
"keyword.operator.logical.python",
44+
"keyword.operator.wordlike",
45+
],
46+
settings: { foreground: "#8271ED" },
47+
},
48+
// Types and namespaces: hot pink
49+
{
50+
scope: [
51+
"support.class",
52+
"support.type",
53+
"entity.name.type",
54+
"entity.name.namespace",
55+
"entity.name.scope-resolution",
56+
"entity.name.class",
57+
"entity.other.inherited-class",
58+
],
59+
settings: { foreground: "#F770C6" },
60+
},
61+
// Functions: lime/yellow-green
62+
{
63+
scope: ["entity.name.function", "support.function"],
64+
settings: { foreground: "#D9F07C" },
65+
},
66+
// Variables and parameters: light lavender
67+
{
68+
scope: [
69+
"variable",
70+
"meta.definition.variable.name",
71+
"support.variable",
72+
"entity.name.variable",
73+
"constant.other.placeholder",
74+
],
75+
settings: { foreground: "#CCCBFF" },
76+
},
77+
// Constants and enums: medium purple
78+
{
79+
scope: ["variable.other.constant", "variable.other.enummember"],
80+
settings: { foreground: "#9C9AF2" },
81+
},
82+
// this/self: purple-blue
83+
{
84+
scope: "variable.language",
85+
settings: { foreground: "#9B99FF" },
86+
},
87+
// Object literal keys: medium purple-blue
88+
{
89+
scope: "meta.object-literal.key",
90+
settings: { foreground: "#8B89FF" },
91+
},
92+
// Strings: sage green
93+
{
94+
scope: ["string", "meta.embedded.assembly"],
95+
settings: { foreground: "#AFEC73" },
96+
},
97+
// String interpolation punctuation: blue-purple
98+
{
99+
scope: [
100+
"punctuation.definition.template-expression.begin",
101+
"punctuation.definition.template-expression.end",
102+
"punctuation.section.embedded",
103+
],
104+
settings: { foreground: "#7A78EA" },
105+
},
106+
// Template expression reset
107+
{
108+
scope: "meta.template.expression",
109+
settings: { foreground: "#d4d4d4" },
110+
},
111+
// Operators: gray (same as foreground)
112+
{
113+
scope: "keyword.operator",
114+
settings: { foreground: "#878C99" },
115+
},
116+
// Comments: olive gray
117+
{
118+
scope: "comment",
119+
settings: { foreground: "#6f736d" },
120+
},
121+
// Language constants (true, false, null, undefined): purple-blue
122+
{
123+
scope: "constant.language",
124+
settings: { foreground: "#9B99FF" },
125+
},
126+
// Numeric constants: light green
127+
{
128+
scope: [
129+
"constant.numeric",
130+
"keyword.operator.plus.exponent",
131+
"keyword.operator.minus.exponent",
132+
],
133+
settings: { foreground: "#b5cea8" },
134+
},
135+
// Regex: dark red
136+
{
137+
scope: "constant.regexp",
138+
settings: { foreground: "#646695" },
139+
},
140+
// HTML/JSX tags: purple-blue
141+
{
142+
scope: "entity.name.tag",
143+
settings: { foreground: "#9B99FF" },
144+
},
145+
// Tag brackets: dark gray
146+
{
147+
scope: "punctuation.definition.tag",
148+
settings: { foreground: "#5F6570" },
149+
},
150+
// HTML/JSX attributes: light purple
151+
{
152+
scope: "entity.other.attribute-name",
153+
settings: { foreground: "#C39EFF" },
154+
},
155+
// Escape characters: gold
156+
{
157+
scope: "constant.character.escape",
158+
settings: { foreground: "#d7ba7d" },
159+
},
160+
// Regex string: dark red
161+
{
162+
scope: "string.regexp",
163+
settings: { foreground: "#d16969" },
164+
},
165+
// Storage: purple-blue
166+
{
167+
scope: "storage",
168+
settings: { foreground: "#9B99FF" },
169+
},
170+
// TS-specific: type casts, math/dom/json constants
171+
{
172+
scope: [
173+
"meta.type.cast.expr",
174+
"meta.type.new.expr",
175+
"support.constant.math",
176+
"support.constant.dom",
177+
"support.constant.json",
178+
],
179+
settings: { foreground: "#9B99FF" },
180+
},
181+
// Markdown headings: purple-blue bold
182+
{
183+
scope: "markup.heading",
184+
settings: { foreground: "#9B99FF", fontStyle: "bold" },
185+
},
186+
// Markup bold: purple-blue
187+
{
188+
scope: "markup.bold",
189+
settings: { foreground: "#9B99FF", fontStyle: "bold" },
190+
},
191+
// Markup inline raw: sage green
192+
{
193+
scope: "markup.inline.raw",
194+
settings: { foreground: "#AFEC73" },
195+
},
196+
// Markup inserted: light green
197+
{
198+
scope: "markup.inserted",
199+
settings: { foreground: "#b5cea8" },
200+
},
201+
// Markup deleted: sage green
202+
{
203+
scope: "markup.deleted",
204+
settings: { foreground: "#AFEC73" },
205+
},
206+
// Markup changed: purple-blue
207+
{
208+
scope: "markup.changed",
209+
settings: { foreground: "#9B99FF" },
210+
},
211+
// Invalid: red
212+
{
213+
scope: "invalid",
214+
settings: { foreground: "#f44747" },
215+
},
216+
// JSX text content
217+
{
218+
scope: ["meta.jsx.children"],
219+
settings: { foreground: "#D7D9DD" },
220+
},
221+
],
222+
};

apps/webapp/app/components/runs/v3/PromptSpanDetails.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { lazy, Suspense, useState } from "react";
1+
import { Suspense, useState } from "react";
22
import { CodeBlock } from "~/components/code/CodeBlock";
3+
import { StreamdownRenderer } from "~/components/code/StreamdownRenderer";
34
import { Header3 } from "~/components/primitives/Headers";
45
import { TextLink } from "~/components/primitives/TextLink";
56
import { tryPrettyJson } from "./ai/aiHelpers";
@@ -12,16 +13,6 @@ import { TabButton, TabContainer } from "~/components/primitives/Tabs";
1213
import type { PromptSpanData } from "~/presenters/v3/SpanPresenter.server";
1314
import { SpanHorizontalTimeline } from "~/components/runs/v3/SpanHorizontalTimeline";
1415

15-
const StreamdownRenderer = lazy(() =>
16-
import("streamdown").then((mod) => ({
17-
default: ({ children }: { children: string }) => (
18-
<mod.ShikiThemeContext.Provider value={["one-dark-pro", "one-dark-pro"]}>
19-
<mod.Streamdown isAnimating={false}>{children}</mod.Streamdown>
20-
</mod.ShikiThemeContext.Provider>
21-
),
22-
}))
23-
);
24-
2516
type PromptTab = "overview" | "input" | "template";
2617

2718
export function PromptSpanDetails({

apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,14 @@ import {
55
ClipboardDocumentIcon,
66
CodeBracketSquareIcon,
77
} from "@heroicons/react/20/solid";
8-
import { lazy, Suspense, useState } from "react";
8+
import { Suspense, useState } from "react";
99
import { CodeBlock } from "~/components/code/CodeBlock";
10+
import { StreamdownRenderer } from "~/components/code/StreamdownRenderer";
1011
import { Button, LinkButton } from "~/components/primitives/Buttons";
1112
import { Header3 } from "~/components/primitives/Headers";
1213
import tablerSpritePath from "~/components/primitives/tabler-sprite.svg";
1314
import type { DisplayItem, ToolUse } from "./types";
1415

15-
// Lazy load streamdown to avoid SSR issues
16-
const StreamdownRenderer = lazy(() =>
17-
import("streamdown").then((mod) => ({
18-
default: ({ children }: { children: string }) => (
19-
<mod.ShikiThemeContext.Provider value={["one-dark-pro", "one-dark-pro"]}>
20-
<mod.Streamdown isAnimating={false}>{children}</mod.Streamdown>
21-
</mod.ShikiThemeContext.Provider>
22-
),
23-
}))
24-
);
25-
2616
export type PromptLink = {
2717
slug: string;
2818
version?: string;

apps/webapp/app/components/runs/v3/ai/AISpanDetails.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CheckIcon, ClipboardDocumentIcon } from "@heroicons/react/20/solid";
2-
import { lazy, Suspense, useState } from "react";
2+
import { Suspense, useState } from "react";
33
import { Button } from "~/components/primitives/Buttons";
4+
import { StreamdownRenderer } from "~/components/code/StreamdownRenderer";
45
import { Header3 } from "~/components/primitives/Headers";
56
import { Paragraph } from "~/components/primitives/Paragraph";
67
import { TabButton, TabContainer } from "~/components/primitives/Tabs";
@@ -20,16 +21,6 @@ import type { AISpanData, DisplayItem } from "./types";
2021
import type { PromptSpanData } from "~/presenters/v3/SpanPresenter.server";
2122
import { SpanHorizontalTimeline } from "~/components/runs/v3/SpanHorizontalTimeline";
2223

23-
const StreamdownRenderer = lazy(() =>
24-
import("streamdown").then((mod) => ({
25-
default: ({ children }: { children: string }) => (
26-
<mod.ShikiThemeContext.Provider value={["one-dark-pro", "one-dark-pro"]}>
27-
<mod.Streamdown isAnimating={false}>{children}</mod.Streamdown>
28-
</mod.ShikiThemeContext.Provider>
29-
),
30-
}))
31-
);
32-
3324
type AITab = "overview" | "messages" | "tools" | "prompt";
3425

3526
export function AISpanDetails({

apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,10 @@ export class TestTaskPresenter {
373373
),
374374
};
375375
}
376+
case "AGENT": {
377+
// AGENT tasks are filtered out by TestPresenter and shouldn't reach here
378+
return { foundTask: false };
379+
}
376380
default: {
377381
return task.triggerSource satisfies never;
378382
}

0 commit comments

Comments
 (0)