From 36b5c74bc5b37b288eeaedb95ec0997ca86fa2fa Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 26 Jun 2026 10:17:41 +0100 Subject: [PATCH] feat(webapp): move the in-dashboard agent launcher into the page header The agent was launched from a button pinned to the bottom-right of every page, which floated over page controls (for example the run inspector's action bar). It now opens from a compact "Chat" button on the far right of the page header, and toggles to "Collapse" while the panel is open. The panel and launcher read "Chat" in the UI. The launcher only renders on env-scoped pages where the agent is enabled, gated by the same feature flag, so nothing changes for users who don't have it. --- .../dashboard-agent/DashboardAgent.tsx | 66 ++++++++----------- .../dashboard-agent/DashboardAgentChat.tsx | 4 +- .../dashboard-agent/DashboardAgentHeader.tsx | 2 +- .../dashboardAgentLauncher.tsx | 48 ++++++++++++++ .../app/components/primitives/PageHeader.tsx | 6 +- 5 files changed, 84 insertions(+), 42 deletions(-) create mode 100644 apps/webapp/app/components/dashboard-agent/dashboardAgentLauncher.tsx diff --git a/apps/webapp/app/components/dashboard-agent/DashboardAgent.tsx b/apps/webapp/app/components/dashboard-agent/DashboardAgent.tsx index 47e280da1f1..2796c8516df 100644 --- a/apps/webapp/app/components/dashboard-agent/DashboardAgent.tsx +++ b/apps/webapp/app/components/dashboard-agent/DashboardAgent.tsx @@ -1,4 +1,3 @@ -import { SparklesIcon } from "@heroicons/react/20/solid"; import { useState } from "react"; import { ResizableHandle, @@ -6,17 +5,19 @@ import { ResizablePanelGroup, } from "~/components/primitives/Resizable"; import { DashboardAgentPanel } from "./DashboardAgentPanel"; +import { DashboardAgentProvider } from "./dashboardAgentLauncher"; /** * Mounts the dashboard agent in the env layout. Renders the page content - * (`children` = the route Outlet); when the agent is open it splits the layout - * into a resizable content + agent panel using the shared Resizable primitive, - * with `autosaveId` persisting the width. When closed it's a floating launcher. + * (`children` = the route Outlet) and shares the open/close state via context so + * the page-header launcher (`DashboardAgentLauncher`) can toggle it. When open it + * splits the layout into a resizable content + agent panel, `autosaveId` persists + * the width. * - * `hasAccess` is resolved server-side in the env layout loader (via - * `canAccessDashboardAgent`: global env, admins/impersonators, then the - * global/per-org feature flag, default off), so the launcher is hidden unless - * the agent is enabled. The resource routes enforce the same check server-side. + * `hasAccess` is resolved server-side in the env layout loader + * (`canAccessDashboardAgent`); when false we render the content untouched and + * never expose the context, so the launcher stays hidden. The resource routes + * enforce the same check server-side. */ export function DashboardAgent({ children, @@ -31,36 +32,25 @@ export function DashboardAgent({ return
{children}
; } - if (!open) { - return ( -
-
{children}
- -
- ); - } - return ( - - -
{children}
-
- - - setOpen(false)} /> - -
+ + {open ? ( + + +
{children}
+
+ + + setOpen(false)} /> + +
+ ) : ( +
{children}
+ )} +
); } diff --git a/apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx b/apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx index 1c6b3e9552c..e6662ca8494 100644 --- a/apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx +++ b/apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx @@ -101,7 +101,7 @@ export function DashboardAgentChat({ const res = await fetch(actionPath, { method: "POST", body }); const data = (await res.json()) as { publicAccessToken?: string; error?: string }; if (!res.ok || !data.publicAccessToken) { - throw new Error(data.error ?? "The dashboard agent couldn't start."); + throw new Error(data.error ?? "The chat couldn't start."); } return { publicAccessToken: data.publicAccessToken }; }, @@ -112,7 +112,7 @@ export function DashboardAgentChat({ const res = await fetch(actionPath, { method: "POST", body }); const data = (await res.json()) as { token?: string; error?: string }; if (!res.ok || !data.token) { - throw new Error(data.error ?? "Couldn't refresh the dashboard agent token."); + throw new Error(data.error ?? "Couldn't refresh the chat token."); } return data.token; }, diff --git a/apps/webapp/app/components/dashboard-agent/DashboardAgentHeader.tsx b/apps/webapp/app/components/dashboard-agent/DashboardAgentHeader.tsx index c782e5f13c3..8b96dd06c01 100644 --- a/apps/webapp/app/components/dashboard-agent/DashboardAgentHeader.tsx +++ b/apps/webapp/app/components/dashboard-agent/DashboardAgentHeader.tsx @@ -14,7 +14,7 @@ export function DashboardAgentHeader({ }) { return (
- Dashboard agent + Chat
void; +}; + +const DashboardAgentContext = createContext(null); + +export const DashboardAgentProvider = DashboardAgentContext.Provider; + +// Null outside the env layout (no provider) or when the agent is gated off, so +// the launcher self-hides everywhere it can't open. +export function useDashboardAgent() { + return useContext(DashboardAgentContext); +} + +export function DashboardAgentLauncher() { + const agent = useDashboardAgent(); + if (!agent) { + return null; + } + + const { open, setOpen } = agent; + + return ( + + ); +} diff --git a/apps/webapp/app/components/primitives/PageHeader.tsx b/apps/webapp/app/components/primitives/PageHeader.tsx index 1b5e3be5579..31fa8104dd4 100644 --- a/apps/webapp/app/components/primitives/PageHeader.tsx +++ b/apps/webapp/app/components/primitives/PageHeader.tsx @@ -8,6 +8,7 @@ import { Header2 } from "./Headers"; import { LoadingBarDivider } from "./LoadingBarDivider"; import { SimpleTooltip } from "./Tooltip"; import { EnvironmentBanner } from "../navigation/EnvironmentBanner"; +import { DashboardAgentLauncher } from "../dashboard-agent/dashboardAgentLauncher"; type WithChildren = { children: React.ReactNode; @@ -24,7 +25,10 @@ export function NavBar({ children }: WithChildren) { return (
-
{children}
+
+
{children}
+ +
{showUpgradePrompt.shouldShow && organization ? : }