Skip to content

Commit ecfac76

Browse files
committed
feat(webapp): agent-view dashboard for chat.agent runs
Dashboard surfaces for inspecting and debugging chat.agent runs. Depends on the Sessions primitive (L1) and chat.agent runtime (L2+L3). Run inspector — chat-aware: - AgentView + AgentMessageView (run inspector tab for chat.agent runs) - AIChatMessages + AISpanDetails + types.ts (per-span chat message rendering, tool-call/tool-output handling) - PromptSpanDetails (gen_ai.* span detail panel) - StreamdownRenderer + shikiTheme (markdown renderer with shiki highlighting and v2 patch) - useAutoScrollToBottom hook Playground UI (interactive chat.agent debugger): - /playground index + /playground/$agentParam routes - /agents route + AgentListPresenter - PlaygroundPresenter (per-org basin variants, clientData wiring) - realtime session routes for playground + run inspector chat - AI-generate-payload + AIPayloadTabContent for the test panel Navigation + theming: - SideMenu links for Agents and Playground - BlankStatePanels copy updates - tailwind config + tailwind.css storybook hooks - streamdown@2 dep in apps/webapp/package.json Includes agent-view-sessions, playground-trigger-config-fields, run-agent-view, and streamdown-v2-upgrade .server-changes.
1 parent a60187c commit ecfac76

38 files changed

Lines changed: 5000 additions & 104 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Migrate the dashboard Agent tab (span inspector) to subscribe to the backing Session's `.out` and `.in` channels instead of the run-scoped chat output + chat-messages input streams. Pairs with the SDK + MCP migrations on the ai-chat branch.
7+
8+
- `SpanPresenter.server.ts` extracts `agentSession` from the run payload (prefers `sessionId`, falls back to `chatId` for pre-Sessions agent runs — matches `resolveSessionByIdOrExternalId`).
9+
- Span route threads `agentSession` through `AgentViewAuth` and gates `agentView` creation on having one.
10+
- New dashboard resource route `resources.orgs.../runs.$runParam/realtime/v1/sessions/$sessionId/$io` proxies `S2RealtimeStreams.streamResponseFromSessionStream` under dashboard session auth. The run param binds resource hierarchy; the session identity is verified against the environment.
11+
- `AgentView.tsx` subscribes to `/out` and `/in` URLs, drops local `CHAT_STREAM_KEY`/`CHAT_MESSAGES_STREAM_ID` constants, and parses the `.in` stream as `ChatInputChunk` (`{kind: "message", payload}` for user turns; `{kind: "stop"}` ignored). Output-stream parsing is unchanged — session v2 SSE already delivers UIMessageChunk objects from `record.body.data`.
12+
- Smoke: opened a prior `test-agent` run in the dashboard, Agent tab rendered user + assistant messages end-to-end with zero console errors. Both SSE endpoints (`/out`, `/in`) returned 200.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Playground action now forwards `maxDuration`, `version` (as `lockToVersion`), and `region` from the sidebar form into the Session's `triggerConfig`. Previously the form fields rendered as working controls but were silently dropped (`void`-suppressed) because `SessionTriggerConfig` didn't accept them — runs ignored the user's max duration, version pin, and region selection. With the schema extended in core, the playground now plumbs them through to `ensureRunForSession`.
7+
8+
Also fixes stale `clientData` in the playground transport: the JSON editor's value was captured at construction and never updated, so per-turn `metadata` merges used the original value across the whole conversation. Added a `useEffect` that calls `transport.setClientData(...)` whenever `clientDataJson` changes.

.server-changes/run-agent-view.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Add an Agent view to the run details page for runs whose `taskKind` annotation is `AGENT`. The view renders the agent's `UIMessage` conversation by subscribing to the run's `chat` realtime stream — the same data source as the Agent Playground content view. Switching is via a `Trace view` / `Agent view` segmented control above the run body, and the selected view is reflected in the URL via `?view=agent` so it's shareable.
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/BlankStatePanels.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ArrowsRightLeftIcon,
23
BeakerIcon,
34
BellAlertIcon,
45
BookOpenIcon,
@@ -189,6 +190,28 @@ export function BatchesNone() {
189190
);
190191
}
191192

193+
export function SessionsNone() {
194+
return (
195+
<InfoPanel
196+
title="Sessions"
197+
icon={ArrowsRightLeftIcon}
198+
iconClassName="text-teal-500"
199+
panelClassName="max-w-full"
200+
accessory={
201+
<LinkButton to={docsPath("/ai-chat/overview")} variant="docs/small" LeadingIcon={BookOpenIcon}>
202+
Sessions docs
203+
</LinkButton>
204+
}
205+
>
206+
<Paragraph spacing variant="small">
207+
You have no sessions in this environment. Sessions are durable, typed, bidirectional I/O
208+
primitives that outlive a single run — used by <InlineCode>chat.agent</InlineCode> and any
209+
long-running task that needs streaming input and output.
210+
</Paragraph>
211+
</InfoPanel>
212+
);
213+
}
214+
192215
export function TestHasNoTasks() {
193216
const organization = useOrganization();
194217
const project = useProject();

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+
};

0 commit comments

Comments
 (0)