Skip to content

Commit f3922e6

Browse files
kathiekiwiTrigger.dev RepoOps
authored andcommitted
feat(dashboard-agent): investigation cards for run questions, timeline, fullscreen blocks and queue charts
Mono-RevId: 7a958bdcafd56dd32ed248d8928893400dbaac6b
1 parent 6172bcd commit f3922e6

87 files changed

Lines changed: 7896 additions & 516 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
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+
The assistant in the dashboard now answers with cards: investigation cards for run questions, carrying the run's timeline and links to the run, queue or deployment, alongside charts and tables you can open fullscreen or copy — including queue metrics — and it answers about another project or environment without you switching to it first. The assistant reads run timelines through a dedicated endpoint, so span durations and launch events are consistent on every event store; the public run trace API is unchanged.

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import { ChartBlankState } from "../primitives/charts/ChartBlankState";
1212
import { Callout } from "../primitives/Callout";
1313
import type { AggregationType, ChartConfiguration } from "../metrics/QueryWidget";
1414
import { aggregateValues } from "../primitives/charts/aggregation";
15+
import { MAX_SERIES } from "~/components/primitives/charts/seriesFromRows";
1516
import { getRunStatusChartColor } from "~/components/runs/v3/TaskRunStatus";
1617
import { getSeriesColor } from "./chartColors";
1718

18-
const MAX_SERIES = 50;
1919
const MAX_SVG_ELEMENT_BUDGET = 6_000;
2020
const MIN_DATA_POINTS = 100;
2121
const MAX_DATA_POINTS = 500;
@@ -65,6 +65,8 @@ interface QueryResultsChartProps {
6565
/** When true, constrains legend to max 50% height with scrolling */
6666
legendScrollable?: boolean;
6767
isLoading?: boolean;
68+
/** Overrides the y-value format inferred from the first y column's metadata. */
69+
valueFormat?: ColumnFormatType;
6870
}
6971

7072
interface TransformedData {
@@ -805,6 +807,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
805807
onViewAllLegendItems,
806808
isLoading = false,
807809
legendScrollable = false,
810+
valueFormat,
808811
}: QueryResultsChartProps) {
809812
const {
810813
xAxisColumn,
@@ -911,10 +914,11 @@ export const QueryResultsChart = memo(function QueryResultsChart({
911914

912915
// Resolve the Y-axis column format for formatting
913916
const yAxisFormat = useMemo(() => {
917+
if (valueFormat) return valueFormat;
914918
if (yAxisColumns.length === 0) return undefined;
915919
const col = columns.find((c) => c.name === yAxisColumns[0]);
916920
return (col?.format ?? col?.customRenderType) as ColumnFormatType | undefined;
917-
}, [yAxisColumns, columns]);
921+
}, [valueFormat, yAxisColumns, columns]);
918922

919923
// Create dynamic Y-axis formatter based on data range and format
920924
const yAxisFormatter = useMemo(
@@ -1243,7 +1247,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
12431247
/**
12441248
* Creates a Y-axis value formatter based on the data range and optional format hint
12451249
*/
1246-
function createYAxisFormatter(
1250+
export function createYAxisFormatter(
12471251
data: Record<string, unknown>[],
12481252
series: string[],
12491253
format?: ColumnFormatType

apps/webapp/app/components/code/StreamdownRenderer.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,13 @@ describe("StreamdownRenderer (rendered markdown)", () => {
170170
expect(html).toContain('target="_blank"');
171171
expect(html).toContain('rel="noopener noreferrer"');
172172
});
173+
174+
it("wraps a rendered table in a scrollable div and drops the hast node prop", async () => {
175+
const html = await render(["| a | b |", "| --- | --- |", "| 1 | 2 |"].join("\n"));
176+
177+
expect(html).toMatch(/<div class="overflow-x-auto"><table/);
178+
expect(html).not.toMatch(/<table[^>]*node=/);
179+
});
173180
});
174181

175182
// No jsdom in this repo — stand in for the browser globals `createStaleAssetRecovery` touches.

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

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,22 @@ type TriggerUriResolver = (uri: string) => TriggerLinkResolution | null;
4747

4848
const TriggerUriResolverContext = createContext<TriggerUriResolver | undefined>(undefined);
4949

50-
function TriggerAwareAnchor({ href, children }: { href?: string; children?: React.ReactNode }) {
50+
// Exported so a consumer (e.g. the agent chat) can wrap it with its own link styling
51+
// without forking the trigger:// resolution logic.
52+
export function TriggerAwareAnchor({
53+
href,
54+
children,
55+
className,
56+
}: {
57+
href?: string;
58+
children?: React.ReactNode;
59+
className?: string;
60+
}) {
5161
const resolveTriggerUri = useContext(TriggerUriResolverContext);
5262

5363
if (!href || !isTriggerUri(href)) {
5464
return (
55-
<a href={href} target="_blank" rel="noopener noreferrer">
65+
<a href={href} target="_blank" rel="noopener noreferrer" className={className}>
5666
{children}
5767
</a>
5868
);
@@ -62,15 +72,30 @@ function TriggerAwareAnchor({ href, children }: { href?: string; children?: Reac
6272
if (!resolved) return children;
6373
if (resolved.external) {
6474
return (
65-
<a href={resolved.url} target="_blank" rel="noopener noreferrer">
75+
<a href={resolved.url} target="_blank" rel="noopener noreferrer" className={className}>
6676
{children}
6777
</a>
6878
);
6979
}
70-
return <a href={resolved.url}>{children}</a>;
80+
return (
81+
<a href={resolved.url} className={className}>
82+
{children}
83+
</a>
84+
);
7185
}
7286

73-
const STREAMDOWN_COMPONENTS = { a: TriggerAwareAnchor };
87+
function ScrollableTable({
88+
node,
89+
...props
90+
}: React.TableHTMLAttributes<HTMLTableElement> & { node?: unknown }) {
91+
return (
92+
<div className="overflow-x-auto">
93+
<table {...props} />
94+
</div>
95+
);
96+
}
97+
98+
const STREAMDOWN_COMPONENTS = { a: TriggerAwareAnchor, table: ScrollableTable };
7499

75100
// Same shape the browser actually throws for a chunk fetch that 404s under asset skew
76101
// (Vite/Rollup's dynamic-import wrapper, or the module-script equivalent).
@@ -81,11 +106,14 @@ const PlainTextFallback = ({ children }: { children: string }) => (
81106
<pre className="whitespace-pre-wrap break-words font-sans text-sm">{children}</pre>
82107
);
83108

109+
type StreamdownComponents = StreamdownModule.StreamdownProps["components"];
110+
84111
type StreamdownRendererModule = {
85112
default: (props: {
86113
children: string;
87114
isAnimating?: boolean;
88115
resolveTriggerUri?: TriggerUriResolver;
116+
components?: StreamdownComponents;
89117
}) => JSX.Element;
90118
};
91119

@@ -134,11 +162,17 @@ export function loadStreamdownRenderer(
134162
children,
135163
isAnimating = false,
136164
resolveTriggerUri,
165+
components,
137166
}: {
138167
children: string;
139168
isAnimating?: boolean;
140169
resolveTriggerUri?: TriggerUriResolver;
170+
components?: StreamdownComponents;
141171
}) {
172+
const mergedComponents = {
173+
...STREAMDOWN_COMPONENTS,
174+
...components,
175+
} as StreamdownComponents;
142176
return (
143177
<TriggerUriResolverContext.Provider value={resolveTriggerUri}>
144178
<Streamdown
@@ -148,7 +182,7 @@ export function loadStreamdownRenderer(
148182
urlTransform={restrictModelUrls}
149183
linkSafety={{ enabled: false }}
150184
rehypePlugins={rehypePlugins}
151-
components={STREAMDOWN_COMPONENTS}
185+
components={mergedComponents}
152186
>
153187
{children}
154188
</Streamdown>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useState } from "react";
2+
import {
3+
Popover,
4+
PopoverContent,
5+
PopoverMenuItem,
6+
PopoverVerticalEllipseTrigger,
7+
} from "~/components/primitives/Popover";
8+
import { cn } from "~/utils/cn";
9+
import type { AgentBlockTool } from "./agent-block-tools";
10+
11+
// Shared by the inline hover-reveal trigger and the always-visible fullscreen header trigger.
12+
export function AgentBlockToolsMenu({
13+
tools,
14+
revealOnHover = true,
15+
}: {
16+
tools: AgentBlockTool[];
17+
revealOnHover?: boolean;
18+
}) {
19+
const [isMenuOpen, setIsMenuOpen] = useState(false);
20+
return (
21+
<Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
22+
<PopoverVerticalEllipseTrigger
23+
isOpen={isMenuOpen}
24+
aria-label="More actions"
25+
className={cn(
26+
"transition-opacity",
27+
isMenuOpen || !revealOnHover
28+
? "opacity-100"
29+
: "opacity-0 group-focus-within:opacity-100 group-hover:opacity-100"
30+
)}
31+
/>
32+
<PopoverContent align="end" className="p-0">
33+
<div className="flex flex-col gap-1 p-1">
34+
{tools.map((tool, i) => (
35+
<PopoverMenuItem
36+
key={i}
37+
icon={tool.icon}
38+
title={tool.title}
39+
disabled={tool.disabled}
40+
onClick={() => {
41+
tool.onClick();
42+
setIsMenuOpen(false);
43+
}}
44+
leadingIconClassName="-ml-0.5 -mr-1"
45+
/>
46+
))}
47+
</div>
48+
</PopoverContent>
49+
</Popover>
50+
);
51+
}

0 commit comments

Comments
 (0)