-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(webapp): dashboard agent — chat, reports, investigate #4418
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3ed1c1f
cca0d53
7b16385
331f80e
2b5a705
e740c16
ef65f46
75aba7f
95fa1b6
814b527
1bc6a01
0045062
0b66440
4606b94
37e4c23
d3d33a6
ac1fdfd
4dedd60
cbeee67
e16320b
5d90eb1
3993381
93c5344
1989336
a4cd475
f428759
8349bf4
27a9263
16682d5
5247a00
398e8c5
0357d79
d1fff9c
6ac4214
a5dd3f1
c331843
d70bcfc
0468776
a303cb0
1af895b
5aef047
547ec71
f6865d5
880d94a
78aea57
b062490
f3237de
17e11c7
bd6835f
cb70b83
0d415c0
93bc6c1
0739337
a4f8ed1
e6ec1d2
9b7ca8c
5153cca
42749c9
9270caf
f9eb9c0
8d3676a
5800e89
3773f31
fd83288
9b32bfd
5a80876
3f85acc
0760f1b
a7a29f3
d4f0357
6a6fa1f
02debc6
dd1f309
7850289
e8d35fa
893b9e8
9b4e473
fc4381d
e109a24
d26a28b
1dd8536
8b042be
be1cb04
cafd920
a88228c
b6ebe55
5b5a258
d6debe8
30ec754
2ed5d52
46290d1
18dffa6
a0d06e4
ebad471
c3ee6a4
08dc7ba
7f13add
42ddcff
3c5b2d5
a4b4ef1
263b324
99b8933
29d9a8e
31698fa
fea1a29
4876e07
fc32fc6
f963614
99850ac
1d083f3
42a76fb
0c41a70
6c993f2
ddc6d25
4e39228
a3be7bd
e599f12
d719c9b
28b1f38
49a9538
cbc2edc
4ff8859
50fd10b
1cdf1f0
62b598d
85b98ab
f5a146b
880bf60
9d45284
d51cb9a
f58a9dc
d12e6a8
d2511bd
e485209
bd2559a
4846cf8
012ce0d
5f2f5fe
5176e3e
a63708c
a0640ba
dd90b6e
94ec67f
1aee9ca
488621e
32553b9
d9ff853
82f4a73
933aeb9
05b3472
d73f3f5
48d0d4a
cd112ba
5adba81
fb99526
9daa8f9
09d1b55
22bf6d9
210863e
240799d
478b30c
4e00b79
d00b126
a7fc406
f700174
3336a47
7a645e5
79482de
d646809
c4d3f58
49e8fb5
7b2f9c6
f7b8374
26f93da
07b83da
60a9584
e62d8c6
1759618
657f11d
29de34f
5bf3612
37c56aa
44b4e57
49a1504
2e23a09
f6f753c
afc0cfa
d52c224
abaeebb
c3f4ba8
7ab35df
020dad2
81e8349
398a298
aad8498
d8fda07
f159c69
ceaa38d
3d3c962
e3fcc20
f4337f7
bb036f9
9fad665
c37bdf8
5edfd78
c88a448
0d3d216
9c6f148
db9f9a2
dfcee8b
49051f5
798fdf9
c025bbf
2ff7ec0
711b79e
c236601
8adf2b2
df2227e
bd357fc
f891584
cb0bcc9
0f2814e
33a3f6b
983055a
5126bae
2d23dde
05505d5
1ee6704
0db1cf0
d0be659
4c42230
aa55227
f598f96
b38c518
de4cdad
e2704ea
f6afda3
14d70be
c3f0d62
314a1d7
a4ba027
d0f06d5
a660349
3355b68
566de27
ad2698b
643ea39
ed0a7b3
0cae301
8fc6f20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "trigger.dev": patch | ||
| --- | ||
|
|
||
| `trigger mint-token --help` now says that a token minted without `--cap` is read-only, instead of claiming it carries your full role. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@trigger.dev/core": patch | ||
| "trigger.dev": patch | ||
| --- | ||
|
|
||
| Reports can be fetched as structured data with the `json` format. The shortest report period is now one minute (`30m`, `1h`, `7d`). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| --- | ||
| area: webapp | ||
| type: feature | ||
| --- | ||
|
|
||
| Meet the dashboard agent: a chat in every environment that answers questions about your runs, queues, errors and health with real data and links. It takes over from Ask AI everywhere that used to appear; on the Free plan you get 20 messages. | ||
|
|
||
| **Investigate** on a failed run, an error, a backed-up queue or a run that hasn't started gets you a worked-through answer — what happened, why, and how to fix it, with every claim linked to the runs, errors and deploys behind it. | ||
|
|
||
| The agent works on preview and dev branches, with that branch's own data. | ||
|
|
||
| The health report reads the same everywhere — dashboard, terminal, editor. A very long chat keeps working: the agent summarises the earlier part and carries on. The agent's replies no longer show images. | ||
|
|
||
| A sample of conversations is scored automatically so the agent keeps getting better. Only the score and a one-line summary are kept, never your messages, data or code, and we can switch it off for your organization on request. | ||
|
|
||
| Separately: a queue's wait times, peak depth, throughput and throttling can now be read from the API, and the Docs button has been removed from page headers. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import type { | ||
| ActionsBlock as ActionsBlockPayload, | ||
| AgentIntent, | ||
| } from "@internal/dashboard-agent-contracts"; | ||
| import { Button } from "~/components/primitives/Buttons"; | ||
| import { ChatActionsRow } from "./chat-layout"; | ||
| import { renderableActions } from "./view-actions"; | ||
|
|
||
| export function ActionsBlock({ | ||
| block, | ||
| onIntent, | ||
| }: { | ||
| block: ActionsBlockPayload; | ||
| onIntent?: (intent: AgentIntent) => void; | ||
| }) { | ||
| const renderable = renderableActions(block.actions); | ||
| if (!onIntent || renderable.length === 0) return null; | ||
| return ( | ||
| <ChatActionsRow> | ||
| {renderable.map((action, i) => ( | ||
| <Button | ||
| key={i} | ||
| variant={i === 0 ? "primary/small" : "secondary/small"} | ||
| onClick={() => onIntent(action.intent as AgentIntent)} | ||
| > | ||
| {action.label} | ||
| </Button> | ||
| ))} | ||
| </ChatActionsRow> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,14 +23,65 @@ function viewSpecFor(part: UIMessage["parts"][number]): { blocks: unknown[] } | | |
| return Array.isArray(p.output?.blocks) ? { blocks: p.output!.blocks! } : null; | ||
| } | ||
|
|
||
| function hostViewBlocks(part: UIMessage["parts"][number]): unknown[] | null { | ||
| const p = part as { type: string; data?: { blocks?: unknown[] } }; | ||
| if (p.type !== "data-view") return null; | ||
| return Array.isArray(p.data?.blocks) ? p.data!.blocks! : null; | ||
| } | ||
|
|
||
| function investigationBlocksFor(part: UIMessage["parts"][number]): unknown[] | null { | ||
| return viewSpecFor(part)?.blocks ?? hostViewBlocks(part); | ||
| } | ||
|
|
||
| type InvestigationRef = { id: string; revision: number }; | ||
|
|
||
| function investigationRef(block: unknown): InvestigationRef | null { | ||
| const b = block as { type?: string; id?: string; revision?: number }; | ||
| if (b?.type !== "investigation" || typeof b.id !== "string") return null; | ||
| return { id: b.id, revision: typeof b.revision === "number" ? b.revision : 0 }; | ||
| } | ||
|
|
||
| /** Per investigation id, the one `messageId:partIndex` allowed to render: highest revision. */ | ||
| export function winningInvestigationOccurrences(messages: UIMessage[]): Map<string, string> { | ||
| const best = new Map<string, { revision: number; occurrence: string }>(); | ||
| for (const message of messages) { | ||
| (message.parts ?? []).forEach((part, partIndex) => { | ||
| for (const block of investigationBlocksFor(part) ?? []) { | ||
| const ref = investigationRef(block); | ||
| if (!ref) continue; | ||
| const current = best.get(ref.id); | ||
| if (!current || ref.revision >= current.revision) { | ||
| best.set(ref.id, { revision: ref.revision, occurrence: `${message.id}:${partIndex}` }); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| return new Map([...best.entries()].map(([id, w]) => [id, w.occurrence])); | ||
| } | ||
|
|
||
| function withoutSupersededInvestigations( | ||
| blocks: unknown[], | ||
| occurrence: string, | ||
| winners: Map<string, string> | undefined | ||
| ): unknown[] { | ||
| if (!winners) return blocks; | ||
| return blocks.filter((block) => { | ||
| const ref = investigationRef(block); | ||
| return !ref || winners.get(ref.id) === occurrence; | ||
| }); | ||
| } | ||
|
|
||
| // Renders one message. Assistant messages that include a completed render_view | ||
| // part get the catalog cards (plus the gather tool rows / lead-in text for | ||
| // transparency); everything else uses the shared MessageBubble unchanged, so | ||
| // its streaming memoization is preserved for the common case. | ||
| const DashboardAgentMessageBubble = memo(function DashboardAgentMessageBubble({ | ||
| message, | ||
| investigationWinners, | ||
| }: { | ||
| message: UIMessage; | ||
| /** See {@link winningInvestigationOccurrences}. */ | ||
| investigationWinners?: Map<string, string>; | ||
| }) { | ||
| if (message.role !== "assistant" || !message.parts?.some((p) => viewSpecFor(p))) { | ||
| return <MessageBubble message={message} />; | ||
|
|
@@ -39,8 +90,14 @@ const DashboardAgentMessageBubble = memo(function DashboardAgentMessageBubble({ | |
| <div className="space-y-2"> | ||
| {message.parts.map((part, i) => { | ||
| const spec = viewSpecFor(part); | ||
| if (spec) return <ViewBlocks key={i} blocks={spec.blocks as never} />; | ||
| return renderPart(part, i); | ||
| if (!spec) return renderPart(part, i); | ||
| const blocks = withoutSupersededInvestigations( | ||
| spec.blocks, | ||
| `${message.id}:${i}`, | ||
| investigationWinners | ||
| ); | ||
| if (blocks.length === 0) return null; | ||
| return <ViewBlocks key={i} blocks={blocks as never} />; | ||
|
Comment on lines
+93
to
+100
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Forward the dashboard-agent view handlers. Line 100 renders Add these host inputs to |
||
| })} | ||
| </div> | ||
| ); | ||
|
|
@@ -60,12 +117,17 @@ export function DashboardAgentMessages({ | |
| error?: Error; | ||
| }) { | ||
| const rootRef = useAutoScrollToBottom([messages, isThinking]); | ||
| const investigationWinners = winningInvestigationOccurrences(messages); | ||
|
|
||
| return ( | ||
| <div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"> | ||
| <div ref={rootRef} className="space-y-4 p-4"> | ||
| {messages.map((message) => ( | ||
| <DashboardAgentMessageBubble key={message.id} message={stripStepParts(message)} /> | ||
| <DashboardAgentMessageBubble | ||
| key={message.id} | ||
| message={stripStepParts(message)} | ||
| investigationWinners={investigationWinners} | ||
| /> | ||
| ))} | ||
| {isThinking && ( | ||
| <div className="flex items-center gap-2 text-sm text-text-dimmed"> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { readFileSync } from "node:fs"; | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| const source = readFileSync(new URL("./InvestigationCard.tsx", import.meta.url), "utf8"); | ||
|
|
||
| describe("InvestigationCard purity", () => { | ||
| it("imports nothing from Remix", () => { | ||
| expect(source).not.toMatch(/from\s+"@remix-run\//); | ||
| }); | ||
|
|
||
| it("imports no app hooks and no server module", () => { | ||
| expect(source).not.toMatch(/from\s+"~\/hooks\//); | ||
| expect(source).not.toMatch(/\.server"/); | ||
| }); | ||
|
|
||
| it("calls no hook other than useState", () => { | ||
| const hooks = [...source.matchAll(/\buse([A-Z]\w*)\(/g)].map((match) => `use${match[1]}`); | ||
| expect([...new Set(hooks)]).toEqual(["useState"]); | ||
| }); | ||
|
|
||
| it("resolves evidence URIs through the host, never a route of its own", () => { | ||
| expect(source).toMatch(/resolveUri/); | ||
| expect(source).not.toMatch(/\/orgs\//); | ||
| }); | ||
|
|
||
| it("hands its actions to the host as intents, and never composes its own", () => { | ||
| expect(source).toMatch(/capabilities\?\.actions/); | ||
| expect(source).toMatch(/onIntent\(action\.intent\)/); | ||
| expect(source).not.toMatch(/kind:\s*"(ask|navigate)"/); | ||
| expect(source).toMatch(/ChatActionsRow/); | ||
| }); | ||
|
|
||
| it("renders no spinner — the transcript owns the one live progress element", () => { | ||
| // A spinner in the card would restart on every revision. | ||
| expect(source).not.toMatch(/AgentSpinner|ChatProgress|ChatPendingTool/); | ||
| }); | ||
|
|
||
| it("renders nothing action-shaped without a host to hand intents to", () => { | ||
| expect(source).toMatch(/if \(!onIntent \|\| actions\.length === 0\) return null;/); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.