diff --git a/.changeset/chat-stream-mid-turn-reconnect.md b/.changeset/chat-stream-mid-turn-reconnect.md new file mode 100644 index 00000000000..5a929f3cdeb --- /dev/null +++ b/.changeset/chat-stream-mid-turn-reconnect.md @@ -0,0 +1,7 @@ +--- +"@trigger.dev/core": patch +"@trigger.dev/sdk": patch +"trigger.dev": patch +--- + +Chat in the browser now reconnects when the connection drops mid-turn, instead of leaving the reply stuck as if it were still generating. Reports can be fetched as structured data with the `json` format, and the shortest report period is now one minute (`30m`, `1h`, `7d`). The `mint-token` command's help is clearer too: a token minted without `--cap` is read-only, and `--ttl` shows the correct maximum lifetime of 7 days. diff --git a/.claude/skills/errors-api-e2e/SKILL.md b/.claude/skills/errors-api-e2e/SKILL.md index e74c50bf5ea..162526ee445 100644 --- a/.claude/skills/errors-api-e2e/SKILL.md +++ b/.claude/skills/errors-api-e2e/SKILL.md @@ -154,7 +154,7 @@ PASS: one run, `run_` (status maps to `FAILED`). Proves `filter[error]` -> ### 6. Attribution — `mint-token` -> JWT exchange records the acting user ```bash -TOKEN=$(cli mint-token --profile $PROFILE --client errors-api-e2e 2>/dev/null) # UAT +TOKEN=$(cli mint-token --profile $PROFILE --client errors-api-e2e --cap read:errors,write:errors 2>/dev/null) # UAT ENVJWT=$(curl -sS -X POST "$B/api/v1/projects/$REF/dev/jwt" -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' -d '{"claims":{"scopes":["read:errors","write:errors"]}}' \ | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..9af4260211f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Generated, not hand-written: collapsed in diffs and excluded from language stats. +internal-packages/dashboard-agent-db/drizzle/meta/*.json linguist-generated=true +internal-packages/dashboard-agent-db/drizzle/meta/** linguist-generated=true +**/__snapshots__/*.snap linguist-generated=true +pnpm-lock.yaml linguist-generated=true diff --git a/.gitignore b/.gitignore index f540927e32b..b11dded2b02 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,5 @@ ailogger-output.log # observability-map CLI output artifact, not committed observability-map.json + +.claude/worktrees/ diff --git a/.server-changes/dashboard-agent.md b/.server-changes/dashboard-agent.md new file mode 100644 index 00000000000..617e60089e5 --- /dev/null +++ b/.server-changes/dashboard-agent.md @@ -0,0 +1,8 @@ +--- +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, replacing Ask AI everywhere it used to appear. Investigate a failed run, an error, a backed-up queue or a run that hasn't started to get a worked-through answer — what happened, why, and how to fix it, with every claim linked to the runs, errors and deploys behind it. It reads your data read-only, works on preview and dev branches with that branch's own data, and reads the same everywhere — dashboard, terminal, editor. A very long chat keeps working: the agent summarises the earlier part and carries on. + +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. The Docs button is gone from page headers — ask the agent instead, or open Documentation from Help & Feedback. Separately, a queue's wait times, peak depth, throughput and throttling can now be read from the API. diff --git a/apps/webapp/.gitignore b/apps/webapp/.gitignore index 595ab180e15..f825411d640 100644 --- a/apps/webapp/.gitignore +++ b/apps/webapp/.gitignore @@ -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 @@ -20,4 +23,4 @@ storybook-static /prisma/seed.js /prisma/populate.js -.memory-snapshots \ No newline at end of file +.memory-snapshots diff --git a/apps/webapp/app/components/AskAI.tsx b/apps/webapp/app/components/AskAI.tsx index d61ea0055fa..389d5e9e569 100644 --- a/apps/webapp/app/components/AskAI.tsx +++ b/apps/webapp/app/components/AskAI.tsx @@ -1,3 +1,9 @@ +/** + * @deprecated Superseded by the dashboard agent (`components/dashboard-agent`). Nothing mounts + * this any more — every Ask AI entry point now opens Ask Trigger. Kept until the agent has + * shipped, then removed along with `@kapaai/react-sdk` and `KAPA_AI_WEBSITE_ID`. + */ + import { ArrowPathIcon, ArrowUpIcon, @@ -81,6 +87,8 @@ function useAskAIState() { * it around the popover, not inside, so the dialog and shortcut survive the popover closing. * `children` receives the open function, or undefined when Ask AI is unavailable (self-hosted, no * Kapa website id, or SSR). + * + * @deprecated See the note at the top of this file. */ export function AskAIRoot({ children, @@ -137,6 +145,7 @@ function AskAIRootProvider({ ); } +/** @deprecated See the note at the top of this file. */ export function AskAI({ isCollapsed = false }: { isCollapsed?: boolean }) { const { isManagedCloud } = useFeatures(); const websiteId = useKapaWebsiteId(); diff --git a/apps/webapp/app/components/code/StreamdownRenderer.test.ts b/apps/webapp/app/components/code/StreamdownRenderer.test.ts new file mode 100644 index 00000000000..994f82ff26e --- /dev/null +++ b/apps/webapp/app/components/code/StreamdownRenderer.test.ts @@ -0,0 +1,80 @@ +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import { restrictModelUrls, StreamdownRenderer } from "./StreamdownRenderer"; + +// streamdown calls urlTransform(url, key, node) to compute each url attribute; a +// returned undefined removes the attribute, so no request is ever issued. +const img = { tagName: "img" } as any; +const link = { tagName: "a" } as any; + +describe("restrictModelUrls (image src)", () => { + it("drops a remote model-authored image (the favicon beacon)", () => { + expect( + restrictModelUrls("https://www.google.com/s2/favicons?domain=evil", "src", img) + ).toBeUndefined(); + }); + + it("drops any absolute or protocol-relative remote image", () => { + expect(restrictModelUrls("http://evil.tld/pixel.gif", "src", img)).toBeUndefined(); + expect(restrictModelUrls("//evil.tld/pixel.gif", "src", img)).toBeUndefined(); + }); + + it("keeps inline and same-origin images", () => { + expect(restrictModelUrls("data:image/png;base64,AAAA", "src", img)).toBe( + "data:image/png;base64,AAAA" + ); + expect(restrictModelUrls("blob:abc", "src", img)).toBe("blob:abc"); + expect(restrictModelUrls("/local/pic.png", "src", img)).toBe("/local/pic.png"); + }); +}); + +describe("restrictModelUrls (link href)", () => { + it("keeps http(s), mailto and relative links", () => { + expect(restrictModelUrls("https://trigger.dev/docs", "href", link)).toBe( + "https://trigger.dev/docs" + ); + expect(restrictModelUrls("http://example.com", "href", link)).toBe("http://example.com"); + expect(restrictModelUrls("mailto:hi@trigger.dev", "href", link)).toBe("mailto:hi@trigger.dev"); + expect(restrictModelUrls("/runs/123", "href", link)).toBe("/runs/123"); + }); + + it("drops unsafe link schemes", () => { + expect(restrictModelUrls("javascript:alert(1)", "href", link)).toBeUndefined(); + expect(restrictModelUrls("data:text/html,