Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
158 commits
Select commit Hold shift + click to select a range
3f1731f
feat(metrics-pipeline): generic Redis-stream to ClickHouse metrics pi…
ericallam Jul 2, 2026
33562f5
feat(clickhouse): queue metrics tables and read queries
ericallam Jul 2, 2026
a3a2434
feat(run-engine): emit queue depth, throughput, and scheduling-delay …
ericallam Jul 2, 2026
c206e12
feat(webapp): queue metrics ingestion, admin controls, and emission s…
ericallam Jul 2, 2026
3581326
feat(tsql): opt-in gap-fill for time-bucketed series
ericallam Jul 2, 2026
2f294d3
feat(webapp): Queues dashboard and per-org metrics UI flag
ericallam Jul 2, 2026
412a385
chore(webapp): add server-changes note for queue metrics
ericallam Jul 2, 2026
7a26ca5
chore: apply oxfmt formatting
ericallam Jul 3, 2026
26e6972
chore: use import type for type-only imports
ericallam Jul 3, 2026
05a01df
fix(tsql): avoid polynomial backtracking in ORDER BY direction strip
ericallam Jul 3, 2026
6846079
fix(tsql): strip ORDER BY direction without a backtracking regex
ericallam Jul 3, 2026
635c9c7
fix(clickhouse): remove semicolons from queue metrics migration comments
ericallam Jul 3, 2026
6d2a357
test(clickhouse): rewrite queue metrics test for cumulative counters
ericallam Jul 3, 2026
5f6b7db
fix(metrics-pipeline): use BigInt order keys and namespaced odometer …
ericallam Jul 3, 2026
b3893a7
fix(clickhouse): filter zero waits from quantile view and accept stri…
ericallam Jul 3, 2026
5139255
fix(webapp): fail open on queue metrics and honor sparkline total ove…
ericallam Jul 3, 2026
9e8a8d7
test(run-engine): import describe from vitest in run-queue metrics test
ericallam Jul 3, 2026
ad3668b
fix(tsql): skip gap-fill on descending bucket order
ericallam Jul 3, 2026
e925979
fix(metrics-pipeline): widen order_key packing factor to 1e6
ericallam Jul 3, 2026
69600b3
feat(webapp,clickhouse): standard time filter for queue metrics pages
ericallam Jul 4, 2026
d4edb4c
fix(tsql): register the deltaSumTimestampMerge aggregate
ericallam Jul 4, 2026
b1dded7
chore(webapp): use shared primitives on the admin queue metrics page
ericallam Jul 4, 2026
5c372a1
feat(webapp): house style hero charts on the queues list
ericallam Jul 4, 2026
9f4f5e9
feat(clickhouse): queue activity ranking queries
ericallam Jul 4, 2026
ea0a4b2
feat(webapp): queue allocation view and relevance-ordered queue list
ericallam Jul 4, 2026
5e04145
fix(tsql): inject time fallbacks into FROM subqueries
ericallam Jul 4, 2026
d527796
feat(clickhouse,tsql): queue metrics rollups and single-scan ranking
ericallam Jul 4, 2026
7c77040
feat(webapp): serve queue metrics reads from rollups and fix env totals
ericallam Jul 4, 2026
5c7f7af
feat(clickhouse,webapp): keep 10-second resolution on the env metrics…
ericallam Jul 4, 2026
9f603ae
fix(webapp): include rollup tables in the queue metrics simulator reset
ericallam Jul 4, 2026
15e0e5f
fix(webapp): update the queue metrics simulator for cumulative counters
ericallam Jul 4, 2026
4be8159
feat(webapp): stage fake Redis usage from the queue metrics simulator
ericallam Jul 4, 2026
9bd77e8
fix(webapp): keep the search filter on the ranked queue list's tail
ericallam Jul 5, 2026
5d0cce5
fix(metrics-pipeline): drop metric emits while the metrics Redis is n…
ericallam Jul 5, 2026
1ed93e7
feat(webapp): remove auto-balance from the allocation view
ericallam Jul 5, 2026
2b866b8
feat(tsql,webapp): reject cross-queue merges of per-queue counter states
ericallam Jul 5, 2026
9c3b077
refactor(clickhouse,webapp): use the stored quantile list in all merg…
ericallam Jul 5, 2026
74f1979
fix(tsql): satisfy the tenant column type in the merge guard test schema
ericallam Jul 5, 2026
b8b4f2e
fix(clickhouse): dedup retried metric batches in the MV target tiers
ericallam Jul 5, 2026
465b361
fix(webapp,metrics-pipeline): review hardening for overflow counters,…
ericallam Jul 5, 2026
fcc27bc
feat(run-engine,clickhouse,webapp): concurrency-key health metrics an…
ericallam Jul 5, 2026
9233032
feat(webapp,run-engine,clickhouse,tsql): per-concurrency-key queue me…
ericallam Jul 5, 2026
046fcda
feat(webapp): move concurrency-key metrics to a tab on the queue deta…
ericallam Jul 5, 2026
c17b1cd
feat(webapp,tsql): hide the per-key metrics table from user-facing qu…
ericallam Jul 5, 2026
b53fbdb
fix(run-engine): anchor scheduling delay to the current queue stint o…
ericallam Jul 5, 2026
9781651
feat(webapp): surface queue metrics on task detail and run inspector
ericallam Jul 5, 2026
61931ab
chore(webapp): remove trailing blank line flagged by oxfmt
ericallam Jul 5, 2026
ddae31f
refactor(webapp): flatten the run inspector queue block and tab the t…
ericallam Jul 6, 2026
b666663
fix(clickhouse): renumber queue-metrics migration to 036 to avoid a v…
ericallam Jul 13, 2026
81274f4
Queue metrics and health UI (#4289)
kathiekiwi Jul 21, 2026
6716463
Merge remote-tracking branch 'origin/main' into feat/queue-metrics-an…
kathiekiwi Jul 21, 2026
c6de9d4
feat(webapp): compact queue status block on waiting runs
kathiekiwi Jul 22, 2026
0dfaec2
fix(webapp): hide queue header charts when there are no queues to chart
kathiekiwi Jul 22, 2026
a585dea
Queue metrics & health: UI polish (TRI-12068) (#4354)
samejr Jul 24, 2026
76e3062
Merge remote-tracking branch 'origin/main' into feat/queue-metrics-an…
kathiekiwi Jul 27, 2026
0418961
feat: add the /report health report (get_report MCP tool + CLI + API)…
kathiekiwi Jul 27, 2026
416cfa7
feat(agent): eval sample-rate gate, pageContext in clientData, doc hy…
kathiekiwi Jul 27, 2026
4946042
feat(agent): investigations and watches tables in dashboard-agent-db
kathiekiwi Jul 27, 2026
add16bc
feat(agent): @internal/dashboard-agent-contracts — frozen M0 contracts
kathiekiwi Jul 27, 2026
18f8b4a
fix(webapp): surface head-start failures as an error turn; inject can…
kathiekiwi Jul 27, 2026
1ccf334
feat(webapp): agent page context plumbing, openWith launcher, envelop…
kathiekiwi Jul 27, 2026
9eb0d06
fix(webapp): dashboard agent UI audit fixes
kathiekiwi Jul 27, 2026
66a1537
feat(webapp): dashboard agent demo mode — fixture layer + playbook
kathiekiwi Jul 27, 2026
f82baa1
feat(webapp): wire demo mode into the agent panel
kathiekiwi Jul 27, 2026
514ab01
feat(webapp): agent UI state gallery + screenshot pack
kathiekiwi Jul 27, 2026
8035dfb
feat(webapp): deterministic waiting-run diagnosis module + API route
kathiekiwi Jul 27, 2026
9d2753a
feat(webapp): reports spine — ReportBlock, ReportView, trigger:// URI…
kathiekiwi Jul 27, 2026
d119412
feat(agent): navigation, docs and the read-tool batch (M3)
kathiekiwi Jul 27, 2026
f13606b
feat(webapp): page- and state-aware suggested prompts with a promoted…
kathiekiwi Jul 27, 2026
a51bf55
feat(agent): runs-collection kind in the trigger:// grammar
kathiekiwi Jul 27, 2026
e92141e
feat(webapp): remove the Docs button from page headers
kathiekiwi Jul 27, 2026
f32b160
feat(webapp): demo chats present as real conversations
kathiekiwi Jul 27, 2026
652e68f
feat(webapp): five-slot suggested prompts + unified list rows
kathiekiwi Jul 28, 2026
a1fdb7e
feat(webapp): design-review pass on agent cards and chat chrome
kathiekiwi Jul 28, 2026
3e5748e
feat(webapp): terminal report skin for chat (MCP-UI-ready)
kathiekiwi Jul 28, 2026
8d10808
feat(webapp): diagnosis category as a titled subtitle, not a bare chip
kathiekiwi Jul 28, 2026
c73ab77
feat(webapp): humanize the diagnosis category subtitle
kathiekiwi Jul 28, 2026
24b23dd
fix(webapp): investigation progress line moves below the card
kathiekiwi Jul 28, 2026
1db45c8
Revert "feat(webapp): terminal report skin for chat (MCP-UI-ready)"
kathiekiwi Jul 28, 2026
680dc74
fix(webapp): badge casing, View run fallback, run id placement
kathiekiwi Jul 28, 2026
bda411f
feat(webapp): demo conversations at full card fidelity
kathiekiwi Jul 28, 2026
12b4614
feat(webapp): chat-layout library — transcript rules as components
kathiekiwi Jul 28, 2026
16225d7
feat(webapp): demo cases as single live scenarios
kathiekiwi Jul 28, 2026
20e3b2c
fix(webapp): intercept log on empty demo chats; banner variants move …
kathiekiwi Jul 28, 2026
5b846e3
feat(webapp): report card polish — sparklines, tooltips, 14px
kathiekiwi Jul 28, 2026
687c33f
feat(webapp): report card layout redesign
kathiekiwi Jul 28, 2026
e39873a
feat(webapp): report sparklines switch to the tasks-page mini bar chart
kathiekiwi Jul 28, 2026
98853a0
reports: shorten the pinned-concurrency annotation
kathiekiwi Jul 28, 2026
92094e0
fix(webapp): hero annotation aligns with the delta column
kathiekiwi Jul 28, 2026
7112071
fix(webapp): more breathing room between report metric rows
kathiekiwi Jul 28, 2026
42001cd
fix(webapp): hero annotation joins the info tooltip
kathiekiwi Jul 28, 2026
45bce86
feat(webapp): highlight rules for report prose; calmer watch chips
kathiekiwi Jul 28, 2026
fb7edd6
fix(webapp): investigation card header run id and hypothesis evidence…
kathiekiwi Jul 28, 2026
93c2d77
fix(webapp): report metric grid — wider labels, sub-values at parent …
kathiekiwi Jul 28, 2026
57fc004
fix(webapp): footer links follow the app's external-link convention; …
kathiekiwi Jul 28, 2026
7383b64
feat(webapp): report footer — contact-us action, docs as buttons; ali…
kathiekiwi Jul 28, 2026
37f7fd8
reports: env-limit recommendation becomes a contact-us footer
kathiekiwi Jul 28, 2026
66d47e0
fix(webapp): report footer reads as one sentence; single chip tooltip
kathiekiwi Jul 28, 2026
8109983
feat(agent): investigation block with system-owned identity (M5 core)
kathiekiwi Jul 28, 2026
d5b00f8
feat(webapp): Investigate entry points across the dashboard
kathiekiwi Jul 28, 2026
2f4fd4a
feat(agent): investigation protocol prompt, cross-turn continuation, …
kathiekiwi Jul 28, 2026
f45c31e
feat(webapp): agent-examples seeder — live example conversations over…
kathiekiwi Jul 28, 2026
022dd6f
feat(webapp): agent-examples seeder duplicates the world into dev
kathiekiwi Jul 28, 2026
6fe38a6
feat(agent): no UI narration in answers
kathiekiwi Jul 28, 2026
b7b22de
feat(webapp): reopening the panel on a new page starts a fresh chat
kathiekiwi Jul 28, 2026
0785989
feat(webapp): retire panel demo mode — live seeded chats replace the …
kathiekiwi Jul 28, 2026
082ab26
feat(webapp): seeder --heartbeat keeps the stand from aging
kathiekiwi Jul 28, 2026
559dc06
feat(agent): golden evals green — protocol prompt tightened, harness …
kathiekiwi Jul 28, 2026
e35e1fc
fix(agent): sanitize replayed tool-call inputs
kathiekiwi Jul 28, 2026
814b71d
chore: server-changes note for the Investigate feature
kathiekiwi Jul 28, 2026
6c78a06
feat(agent): answers and actions first — no thinking out loud
kathiekiwi Jul 28, 2026
940d4ad
fix(webapp): seeded/heartbeat rows always hydrate on the runs list
kathiekiwi Jul 28, 2026
95d620b
fix(webapp): single New-chat affordance; one-line composer at rest
kathiekiwi Jul 28, 2026
4a5aea3
fix(agent): investigation storage failures return a clean tool error
kathiekiwi Jul 28, 2026
7970af9
fix(webapp): truncate tool-error summaries in agent transcripts
kathiekiwi Jul 28, 2026
e14115a
feat(webapp): visual round 2 — footer sentences, aligned sub-rows, ca…
kathiekiwi Jul 28, 2026
c37d0e6
fix(webapp): composer Send/Stop aligns with the text line inset
kathiekiwi Jul 28, 2026
68cbe33
feat(webapp): degraded report footer as an actions list
kathiekiwi Jul 28, 2026
b58c55f
reports: 'Read concurrency docs' action label
kathiekiwi Jul 28, 2026
3713d18
docs(agent): team guidebook — what to ask, scenarios, local setup
kathiekiwi Jul 28, 2026
9a37494
feat(agent): schedule_watch tool, watcher task, wake narration (M6 ag…
kathiekiwi Jul 28, 2026
3b55c36
feat(webapp): watch tokens, check endpoint, createWatch adapters (M6 …
kathiekiwi Jul 28, 2026
8b15a37
feat(webapp): live watch chips, cancel, and host-side watch intent ro…
kathiekiwi Jul 28, 2026
c3cfbc0
fix(webapp): a prefill applies only to the chat it targeted
kathiekiwi Jul 28, 2026
5172916
perf(agent): scope-safe JWT cache with 401 re-exchange, leaner tool o…
kathiekiwi Jul 28, 2026
9eb8048
fix(agent): wake narration streams under its persisted id; null tool …
kathiekiwi Jul 28, 2026
6fdf6dd
fix(agent): investigation evidence takes bare ids; executor builds th…
kathiekiwi Jul 29, 2026
34b5ca9
fix(agent): salvage the bare id from improvised almost-URIs in invest…
kathiekiwi Jul 29, 2026
bbabe7a
fix(webapp): error_recurrence watch normalizes the API error id to th…
kathiekiwi Jul 29, 2026
82c3102
feat(agent): list_alerts / create_alert / delete_alert tools + wake f…
kathiekiwi Jul 29, 2026
f980536
feat(webapp): unread badge on the chat launcher for watch wakes
kathiekiwi Jul 29, 2026
499824b
feat(seeder): calm/degraded heartbeat modes + --recover / --degrade o…
kathiekiwi Jul 29, 2026
51bc591
feat(webapp): standing alerts for dashboard-agent watch fires
kathiekiwi Jul 29, 2026
3057728
feat(webapp): unread chats sort first and are highlighted in the chat…
kathiekiwi Jul 29, 2026
ab42f76
feat(agent): offer an email alert when a watch is scheduled
kathiekiwi Jul 29, 2026
e8fd858
feat(webapp): per-chat process icons in the chat history list
kathiekiwi Jul 29, 2026
49a7e70
feat(webapp): wake banner, persistent wake toast, and Next steps foot…
kathiekiwi Jul 29, 2026
34eb1bd
fix(webapp): footer docs button gets the book icon; row links match b…
kathiekiwi Jul 29, 2026
eab2df2
fix(webapp): watch buttons speak in the chat; docs entries are always…
kathiekiwi Jul 29, 2026
2346338
chore(webapp): drop dead toast wiring left by the silent watch path
kathiekiwi Jul 29, 2026
004ee4c
fix(webapp): wake banner knows the fired watch's kind; watch confirma…
kathiekiwi Jul 29, 2026
19c29d7
fix(webapp): email-alert gate accepts any configured alert transport,…
kathiekiwi Jul 29, 2026
5301596
fix(webapp): history status icons move left of the title with a reser…
kathiekiwi Jul 29, 2026
28f9b10
fix(agent): alert tools carry the chat id the API scopes authorizatio…
kathiekiwi Jul 29, 2026
cdf91bb
fix(webapp): composite metric rows center the label and align every n…
kathiekiwi Jul 29, 2026
5ffc2e8
docs(agent): guidebook second edition — watches, alerts, notification…
kathiekiwi Jul 29, 2026
d1c42b0
fix(emails): watch-fired email matches the billing alert look and spe…
kathiekiwi Jul 29, 2026
cebd6e2
fix(webapp): breakdown sub-rows keep 'done 842' order — label indente…
kathiekiwi Jul 29, 2026
86689d3
feat(emails): watch email opens with the wake banner line, toned by o…
kathiekiwi Jul 29, 2026
bbd3ae7
feat(emails): the wake banner is the watch email's headline
kathiekiwi Jul 29, 2026
2f96c17
docs(agent): guidebook gains the full say-it-like-this toolbox table …
kathiekiwi Jul 29, 2026
113bb09
fix(webapp): history status slot renders only when there is a status …
kathiekiwi Jul 29, 2026
16e835a
fix(agent): page context is per-turn truth; contact action is a direc…
kathiekiwi Jul 29, 2026
725099f
fix(webapp): chat navigation works, stable transcript order, one prog…
kathiekiwi Jul 29, 2026
1f03c5b
fix(webapp): breakdown labels indent shallower so 'triggered' stays i…
kathiekiwi Jul 29, 2026
c0b1072
docs(agent): guidebook gains the four-act demo script
kathiekiwi Jul 29, 2026
fe2688a
docs(agent): degrade right before the demo — an aged degradation read…
kathiekiwi Jul 29, 2026
1d15fe3
feat(webapp): pending tool pill replaces streamed input JSON in the chat
kathiekiwi Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions .changeset/report-health.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@trigger.dev/core": patch
"trigger.dev": patch
---

Add the `get_report` MCP tool, a `trigger report` CLI command, and `GET /api/v1/reports/:key`, starting with the `health` report: a deterministic verdict on whether work is flowing, whether the runs that start are healthy, and whether telemetry is fresh — rendered as text with unicode sparklines (coloured in a terminal). Also adds a `report` MCP prompt, surfaced as a slash command in hosts that support prompts.

Also: `trigger mcp` now always starts the server — the install wizard is gated behind `trigger mcp --install`. A TTY previously launched the wizard, so MCP hosts spawning the server over a PTY timed out.
6 changes: 6 additions & 0 deletions .server-changes/agent-detail-metrics-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

The agent detail page now matches the shared metrics-page layout: the time filter and pagination sit in a filter row at the top, the activity charts form a tile row beneath it, and the tabs and table flow below in a single page scroll, with the agent config panel as a resizable sidebar on the right.
6 changes: 6 additions & 0 deletions .server-changes/dashboard-agent-first-turn-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: fix
---

When the assistant fails to start its very first reply, the chat now shows an error you can retry instead of sitting empty.
6 changes: 6 additions & 0 deletions .server-changes/dashboard-agent-investigate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

The dashboard agent can now investigate failures for you: click Investigate on a failed run, an error, or a backed-up queue and it gathers the evidence, tests a few hypotheses, and answers with what happened and how to fix it — every claim linked to the runs, errors, and deploys behind it. Available to organizations with the dashboard agent enabled.
6 changes: 6 additions & 0 deletions .server-changes/dashboard-agent-watch-alerts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

Watches you set up with the dashboard agent can now alert you by email, Slack, or webhook when they fire. Pick the new "Dashboard agent watches" type on the Alerts page, and turn it off again from any alert email.
6 changes: 6 additions & 0 deletions .server-changes/dashboard-agent-watches.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

Ask the dashboard agent to tell you when something happens — a run starting or finishing, a backlog clearing, an error coming back, an environment recovering — and it messages you in the chat once it does. Each chat can wait on up to three things at a time, for up to 24 hours.
6 changes: 6 additions & 0 deletions .server-changes/paginate-concurrency-keys-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

The concurrency keys table on a queue's page is now paginated, so queues with thousands of keys can page through all of them instead of only showing the top 50.
6 changes: 6 additions & 0 deletions .server-changes/queue-metrics-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

You can now read a single queue's wait times, peak depth, throughput, and how often it was throttled over a time window from the API.
6 changes: 6 additions & 0 deletions .server-changes/queue-metrics-dashboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

New Queue metrics & health dashboard (per-org opt-in): per-queue depth, throughput, concurrency, throttling and scheduling-delay charts, a per-queue detail view, and live queue stats. Queue concurrency limits set above the environment limit are now rejected instead of being silently capped.
6 changes: 6 additions & 0 deletions .server-changes/remove-header-docs-buttons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

The Docs button has been removed from page headers to reduce clutter. Documentation links remain available in context where they're most useful.
7 changes: 6 additions & 1 deletion apps/webapp/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ node_modules
/cypress/screenshots
/cypress/videos

# Output of `pnpm run agent-ui:screenshots`
/screenshots

/app/styles/tailwind.css

# Ensure the .env symlink is not removed by accident
Expand All @@ -20,4 +23,6 @@ storybook-static

/prisma/seed.js
/prisma/populate.js
.memory-snapshots
.memory-snapshots
# Heartbeat story mode for seed-agent-examples (degraded | calm)
.agent-examples-heartbeat-mode
30 changes: 24 additions & 6 deletions apps/webapp/app/components/dashboard-agent/AgentChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Spinner } from "~/components/primitives/Spinner";
import { useOptionalEnvironment } from "~/hooks/useEnvironment";
import { useOptionalOrganization } from "~/hooks/useOrganizations";
import { useOptionalProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";

// Render an agent "chart" block by running its TRQL query through the dashboard's
// own /resources/metric endpoint (session-authed, returns rows + real column
Expand All @@ -25,6 +26,21 @@ type MetricResponse =
};
};

// The chart block's schema (`chartBlockBodySchema` in
// @internal/dashboard-agent-contracts) carries only `period` for the time window
// — no scope, no explicit from/to, no height. So these are fixed here rather
// than plumbed from the block. If the schema grows those fields, read them off
// `block` instead of using these.
const CHART_SCOPE = "environment"; // the panel is always open in one environment
const CHART_FROM = null; // `period` is the only window the agent can ask for
const CHART_TO = null;
const CHART_HEIGHT_CLASS = "h-64"; // fits the panel at its default width

// Query errors come from ClickHouse via the metric endpoint and can carry SQL
// and schema detail, so the panel shows a fixed message and the real one goes to
// the console for whoever is debugging.
const CHART_ERROR_MESSAGE = "This chart's query couldn't run.";

type ChartState =
| { status: "loading" }
| { status: "error"; error: string }
Expand Down Expand Up @@ -63,18 +79,19 @@ export function AgentChart({ block }: { block: ChartBlock }) {
organizationId,
projectId,
environmentId,
scope: "environment",
scope: CHART_SCOPE,
period: block.period ?? null,
from: null,
to: null,
from: CHART_FROM,
to: CHART_TO,
}),
signal: controller.signal,
})
.then(async (res) => (await res.json()) as MetricResponse)
.then((data) => {
if (controller.signal.aborted) return;
if (!data.success) {
setState({ status: "error", error: data.error });
console.error("Dashboard agent chart query failed:", data.error);
setState({ status: "error", error: CHART_ERROR_MESSAGE });
} else {
setState({
status: "ready",
Expand All @@ -86,7 +103,8 @@ export function AgentChart({ block }: { block: ChartBlock }) {
})
.catch((err) => {
if (controller.signal.aborted) return;
setState({ status: "error", error: err?.message ?? "The query failed to run." });
console.error("Dashboard agent chart request failed:", err);
setState({ status: "error", error: CHART_ERROR_MESSAGE });
});
return () => controller.abort();
}, [block.query, block.period, organizationId, projectId, environmentId]);
Expand All @@ -109,7 +127,7 @@ export function AgentChart({ block }: { block: ChartBlock }) {
{block.title}
</div>
) : null}
<div className="h-64 w-full p-2">
<div className={cn("w-full p-2", CHART_HEIGHT_CLASS)}>
{state.status === "loading" ? (
<div className="flex h-full items-center justify-center gap-2 text-xs text-text-dimmed">
<Spinner className="size-3" />
Expand Down
143 changes: 140 additions & 3 deletions apps/webapp/app/components/dashboard-agent/DashboardAgent.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { useState } from "react";
import type { SuggestedPrompt } from "@internal/dashboard-agent-contracts";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "~/components/primitives/Resizable";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { DashboardAgentPanel } from "./DashboardAgentPanel";
import { DashboardAgentProvider } from "./dashboardAgentLauncher";
import {
showWatchWakesSummaryToast,
showWatchWakeToast,
WAKE_TOAST_MAX_INDIVIDUAL,
type WatchWake,
} from "./WatchWakeToast";

// How often the closed panel asks whether a watch woke a chat. A wake is worth
// noticing within a minute, and the count is one indexed query.
const UNREAD_POLL_INTERVAL_MS = 60_000;

/**
* Mounts the dashboard agent in the env layout. Renders the page content
Expand All @@ -22,18 +36,135 @@ import { DashboardAgentProvider } from "./dashboardAgentLauncher";
export function DashboardAgent({
children,
hasAccess = false,
promotedPrompt,
}: {
children: React.ReactNode;
hasAccess?: boolean;
// The product-controlled promoted prompt chip, from the feature flag.
promotedPrompt?: SuggestedPrompt;
}) {
const organization = useOrganization();
const project = useProject();
const environment = useEnvironment();
const actionPath = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/dashboard-agent`;

const [open, setOpen] = useState(false);
const [unreadWakes, setUnreadWakes] = useState(0);
// Wakes already toasted this session. Session-scoped on purpose: a wake that
// arrived overnight deserves the toast on the first poll after a reload, but a
// wake the user has already been shown (and maybe dismissed) must not come
// back every 60s while the chat stays unread.
const toastedWakes = useRef(new Set<string>());
// A request from `openWith`, handed to the panel. `seq` makes repeat requests
// with the same text distinct, so the panel can tell them apart.
const [requestedMessage, setRequestedMessage] = useState<
{ text: string; seq: number } | undefined
>(undefined);
// A specific chat to open, from a wake toast. `seq` so the same chat can be
// asked for twice (a second wake in a chat the user has already left).
const [openChatRequest, setOpenChatRequest] = useState<
{ chatId: string; seq: number } | undefined
>(undefined);

// Closing drops any pending request, so reopening the panel later doesn't
// replay text the user has moved on from.
const setPanelOpen = useCallback((next: boolean) => {
setOpen(next);
// Closing drops both pending requests: the panel unmounts, so a stale one
// would re-apply on the next open instead of restoring the last chat.
if (!next) {
setRequestedMessage(undefined);
setOpenChatRequest(undefined);
}
}, []);

// Open the panel on the chat a wake happened in. Without the chat id the panel
// would just restore whatever it had open last, which is rarely the one the
// toast is about.
const openChat = useCallback((chatId: string) => {
setOpen(true);
setOpenChatRequest((current) => ({ chatId, seq: (current?.seq ?? 0) + 1 }));
}, []);

const openWith = useCallback((text: string) => {
const trimmed = text.trim();
if (!trimmed) return;
setOpen(true);
setRequestedMessage((current) => ({ text: trimmed, seq: (current?.seq ?? 0) + 1 }));
}, []);

// The dot's poll, and the toast's. Runs only while the panel is CLOSED — an
// open panel shows the wake in the transcript, so polling then would only race
// the read marker. Both the interval and the on-close refresh come from this
// effect re-running on `open`.
useEffect(() => {
if (!hasAccess || open) return;

let cancelled = false;
const load = async () => {
try {
const res = await fetch(`${actionPath}?unread=1`);
if (!res.ok) return;
const data = (await res.json()) as { unreadWakes?: number; wakes?: WatchWake[] };
if (cancelled) return;
setUnreadWakes(data.unreadWakes ?? 0);

const fresh = (data.wakes ?? []).filter((wake) => !toastedWakes.current.has(wake.watchId));
for (const wake of fresh) toastedWakes.current.add(wake.watchId);

// A burst gets one summary toast: a stack of persistent toasts is a wall,
// not a notification.
if (fresh.length > WAKE_TOAST_MAX_INDIVIDUAL) {
showWatchWakesSummaryToast(fresh.length, () => setPanelOpen(true));
} else {
// Oldest first, so the newest wake ends up nearest the user.
for (const wake of [...fresh].reverse()) {
showWatchWakeToast(wake, openChat);
}
}
} catch {
// Offline or a hiccup — leave the dot as it is and try again next tick.
}
};

void load();
const interval = window.setInterval(load, UNREAD_POLL_INTERVAL_MS);
return () => {
cancelled = true;
window.clearInterval(interval);
};
}, [hasAccess, open, actionPath, setPanelOpen, openChat]);

// A chat the user is now looking at has no unread wakes. Zeroes the dot right
// away (the poll restores the truth on close if another chat still has one) and
// persists the read marker for the chat that's actually visible.
const markChatRead = useCallback(
async (chatId: string) => {
setUnreadWakes(0);
const body = new FormData();
body.set("intent", "read");
body.set("chatId", chatId);
try {
await fetch(actionPath, { method: "POST", body });
} catch {
// Not worth surfacing: the marker is caught up the next time the chat is
// opened.
}
},
[actionPath]
);

const context = useMemo(
() => ({ open, setOpen: setPanelOpen, openWith, unreadWakes }),
[open, setPanelOpen, openWith, unreadWakes]
);

if (!hasAccess) {
return <div className="h-full min-h-0">{children}</div>;
}

return (
<DashboardAgentProvider value={{ open, setOpen }}>
<DashboardAgentProvider value={context}>
{open ? (
<ResizablePanelGroup
orientation="horizontal"
Expand All @@ -45,7 +176,13 @@ export function DashboardAgent({
</ResizablePanel>
<ResizableHandle id="dashboard-agent-handle" />
<ResizablePanel id="dashboard-agent-panel" default="380px" min="320px" max="720px">
<DashboardAgentPanel onClose={() => setOpen(false)} />
<DashboardAgentPanel
onClose={() => setPanelOpen(false)}
requestedMessage={requestedMessage}
openChatRequest={openChatRequest}
promotedPrompt={promotedPrompt}
onChatRead={markChatRead}
/>
</ResizablePanel>
</ResizablePanelGroup>
) : (
Expand Down
Loading
Loading