Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ function buildAgentBuilderContext(
}

/**
* The agent builder chat — an always-on dock talking to the deployed
* `agent-concierge`. Streams through the shared `useAgentChat`/`AgentChatSurface`
* stack, prepends the current `/code/agents` page context to the first message,
* answers `get_context`, and lets the agent drive the UI via `focus_*`.
* The Agent Builder chat — an always-on dock talking to the deployed meta-agent
* (backend slug `agent-concierge`). Streams through the shared
* `useAgentChat`/`AgentChatSurface` stack, prepends the current `/code/agents`
* page context to the first message, answers `get_context`, and lets the agent
* drive the UI via `focus_*`.
*/
export function AgentBuilderDock() {
const { data: application } = useAgentApplication(AGENT_BUILDER_SLUG);
Expand Down Expand Up @@ -254,7 +255,7 @@ export function AgentBuilderDock() {
<div className="p-4">
<AgentDetailEmptyState
title="Agent Builder unavailable"
description="The agent-concierge deployment has no reachable ingress in this environment."
description="The Agent Builder deployment has no reachable ingress in this environment."
/>
</div>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ import { useAgentBuilderStore } from "./agentBuilderStore";
/**
* The agents-header control cluster — identical across every agents view.
*
* Pinned absolutely to the top-right of the nearest `relative` ancestor so it
* sits on the same row as the Agent Builder dock header (matching `py-2`),
* keeping the two halves of the agents UI visually aligned across views.
*
* One split button is the single entry point into the Agent Builder dock:
* - the primary segment is the contextual "edit with AI" action for the view
* you're on (New agent / Edit configuration / Explain this session / …) — it
* opens the dock and seeds the matching prompt,
* - the trailing segment just opens/closes the dock without seeding, so you
* can peek at or dismiss the existing conversation.
* The two were previously near-identical gold buttons; fusing them keeps both
* affordances but with one sparkle (the AI identity) and one neutral toggle.
* Views with no obvious action (Scouts) collapse to the lone open/close toggle.
* - the trailing segment just opens the dock without seeding, so you can peek
* at the existing conversation; once the dock is open it disappears so we
* don't double up with the dock's own close button.
* Views with no obvious action (Scouts) collapse to the lone open toggle.
* Renders nothing unless the `agent-platform` flag is on.
*/
export function AgentBuilderHeaderControls() {
Expand All @@ -36,13 +39,15 @@ export function AgentBuilderHeaderControls() {
if (!enabled) return null;

const action = headerActionForPage(page);
const toggleTip = visible
? "Hide the agent builder (⌘⇧I)"
: "Open the agent builder (⌘⇧I)";
const openTip = "Open the agent builder (⌘⇧I)";

return (
<TooltipProvider delay={500}>
<Flex align="center" gap="2" className="shrink-0">
<Flex
align="center"
gap="2"
className="absolute top-0 right-0 z-10 shrink-0 px-6 py-2"
>
{action ? (
<div className="flex items-center">
<Tooltip>
Expand All @@ -51,7 +56,11 @@ export function AgentBuilderHeaderControls() {
<Button
variant="outline"
size="sm"
className="rounded-s-[3px] rounded-e-none"
className={
visible
? "rounded-[3px]"
: "rounded-s-[3px] rounded-e-none"
}
onClick={() =>
startAgentBuilder(action.prompt, action.agentSlug)
}
Expand All @@ -69,34 +78,33 @@ export function AgentBuilderHeaderControls() {
Open the agent builder and start here
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<Button
variant="outline"
size="icon-sm"
className="rounded-s-none rounded-e-[3px] border-s-0"
aria-label={toggleTip}
onClick={toggleVisible}
>
<SidebarSimpleIcon
size={14}
weight={visible ? "fill" : "regular"}
/>
</Button>
}
/>
<TooltipContent side="top">{toggleTip}</TooltipContent>
</Tooltip>
{visible ? null : (
<Tooltip>
<TooltipTrigger
render={
<Button
variant="outline"
size="icon-sm"
className="rounded-s-none rounded-e-[3px] border-s-0"
aria-label={openTip}
onClick={toggleVisible}
>
<SidebarSimpleIcon size={14} weight="regular" />
</Button>
}
/>
<TooltipContent side="top">{openTip}</TooltipContent>
</Tooltip>
)}
</div>
) : (
) : visible ? null : (
<Tooltip>
<TooltipTrigger
render={
<Button
variant="outline"
size="icon-sm"
aria-label={toggleTip}
aria-label={openTip}
onClick={toggleVisible}
>
<SparkleIcon
Expand All @@ -107,7 +115,7 @@ export function AgentBuilderHeaderControls() {
</Button>
}
/>
<TooltipContent side="top">{toggleTip}</TooltipContent>
<TooltipContent side="top">{openTip}</TooltipContent>
</Tooltip>
)}
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useAuthStateValue } from "../../auth/store";
import { useAgentAnalytics } from "../hooks/useAgentAnalytics";
import { useAgentApplications } from "../hooks/useAgentApplications";
import { useAgentFleetApprovals } from "../hooks/useAgentFleetApprovals";
import { formatSpendUsd } from "../utils/format";
import { displayAgentName, formatSpendUsd } from "../utils/format";
import { aiObservabilityTracesUrl } from "../utils/observabilityLinks";
import { AgentAnalyticsKpiStrip } from "./AgentAnalyticsView";
import { AgentDetailEmptyState } from "./AgentDetailLayout";
Expand Down Expand Up @@ -197,7 +197,7 @@ function ApplicationRow({
<Flex direction="column" gap="0.5" className="min-w-0">
<Flex align="center" gap="2" className="min-w-0">
<Text className="truncate font-medium text-[13px] text-gray-12">
{application.name}
{displayAgentName(application) ?? application.name}
Comment thread
benjackwhite marked this conversation as resolved.
Outdated
</Text>
<Badge color={isLive ? "green" : "gray"}>
{isLive ? "Live" : "Draft"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AgentBuilderHeaderControls } from "../agent-builder/AgentBuilderHeaderC
import type { AgentBuilderPageContext } from "../agent-builder/agentBuilderStore";
import { useSetAgentBuilderPage } from "../agent-builder/useSetAgentBuilderPage";
import { useAgentApplication } from "../hooks/useAgentApplication";
import { displayAgentName } from "../utils/format";

/** Map a detail sub-tab to the agent builder page context for this agent. */
function tabToAgentBuilderPage(
Expand Down Expand Up @@ -110,7 +111,7 @@ export function AgentDetailLayout({
isError,
} = useAgentApplication(idOrSlug);

const title = application?.name ?? idOrSlug;
const title = displayAgentName(application) ?? idOrSlug;
const headerContent = useMemo(
() => (
<Flex align="center" gap="2" className="w-full min-w-0">
Expand All @@ -134,16 +135,17 @@ export function AgentDetailLayout({
<Flex
direction="column"
gap="3"
className="cursor-default select-none border-(--gray-5) border-b px-6 pt-5"
className="relative cursor-default select-none border-(--gray-5) border-b px-6 pt-5"
>
<AgentBuilderHeaderControls />
<Link
to="/code/agents/applications"
className="flex w-fit items-center gap-1.5 text-[12px] text-gray-11 no-underline hover:text-gray-12"
>
<ArrowLeftIcon size={13} />
Applications
</Link>
<Flex align="center" gap="2" wrap="wrap">
<Flex align="center" gap="2" wrap="wrap" className="pr-44">
<Text className="font-bold text-[22px] text-gray-12 leading-tight tracking-tight">
{title}
</Text>
Expand All @@ -152,9 +154,6 @@ export function AgentDetailLayout({
{application.live_revision ? "Live" : "Draft"}
</Badge>
) : null}
<Flex align="center" className="ml-auto shrink-0">
<AgentBuilderHeaderControls />
</Flex>
</Flex>
{application?.description?.trim() ? (
<Text className="max-w-3xl text-[12.5px] text-gray-11 leading-snug">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { useMemo } from "react";
import { useSetAgentBuilderPage } from "../agent-builder/useSetAgentBuilderPage";
import { useAgentApplications } from "../hooks/useAgentApplications";
import { useAgentFleetApprovals } from "../hooks/useAgentFleetApprovals";
import { approvalStateColor, approvalStateLabel } from "../utils/format";
import {
approvalStateColor,
approvalStateLabel,
displayAgentName,
} from "../utils/format";
import { AgentApprovalDetail } from "./AgentApprovalDetail";
import { AgentDetailEmptyState } from "./AgentDetailLayout";
import { APPROVAL_FILTERS, type ApprovalFilter } from "./agentApprovalsFilters";
Expand Down Expand Up @@ -198,7 +202,9 @@ function FleetApprovalRow({
}) {
const isQueued = approval.state === "queued";
const agentLabel =
application?.name ?? application?.slug ?? approval.application_id;
displayAgentName(application) ??
application?.slug ??
approval.application_id;
return (
<button
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Link } from "@tanstack/react-router";
import { useMemo } from "react";
import { useAgentApplications } from "../hooks/useAgentApplications";
import { useAgentFleetLiveSessions } from "../hooks/useAgentFleetLiveSessions";
import { sessionStateColor } from "../utils/format";
import { displayAgentName, sessionStateColor } from "../utils/format";
import { RefreshIndicator } from "./RefreshIndicator";

/**
Expand Down Expand Up @@ -97,7 +97,9 @@ function LiveSessionRow({
application: AgentApplication | undefined;
}) {
const agentLabel =
application?.name ?? application?.slug ?? session.application_id;
displayAgentName(application) ??
application?.slug ??
session.application_id;
const idOrSlug =
application?.slug ?? application?.id ?? session.application_id;
const trigger = triggerLabel(session.trigger_metadata);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useAuthStateValue } from "../../auth/store";
import { useAgentAnalytics } from "../hooks/useAgentAnalytics";
import { useAgentApplication } from "../hooks/useAgentApplication";
import { displayAgentName } from "../utils/format";
import { aiObservabilityTracesUrl } from "../utils/observabilityLinks";
import { AgentAnalyticsView } from "./AgentAnalyticsView";
import { AgentDetailLayout } from "./AgentDetailLayout";
Expand Down Expand Up @@ -28,7 +29,7 @@ export function AgentObservabilityPane({ idOrSlug }: { idOrSlug: string }) {
<AgentAnalyticsView
data={data}
title="Observability"
subtitle={`${application?.name ?? "This agent"} · last 7 days (14-day trend)`}
subtitle={`${displayAgentName(application) ?? "This agent"} · last 7 days (14-day trend)`}
aiObservabilityUrl={aiObservabilityTracesUrl(region, projectId)}
isLoading={isLoading || !application}
isError={isError}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useNavigate } from "@tanstack/react-router";
import { useMemo, useState } from "react";
import { useAgentRevisionLifecycle } from "../hooks/useAgentRevisionLifecycle";
import { useCreateAgentDraftFromRevision } from "../hooks/useCreateAgentDraftFromRevision";
import { revisionStateColor } from "../utils/format";
import { displayAgentName, revisionStateColor } from "../utils/format";

type LifecycleAction = "freeze" | "promote" | "archive";

Expand Down Expand Up @@ -71,7 +71,7 @@ function dialogCopy(
title: `Promote ${id} to live`,
description: replacing
? `The current live revision will be archived and traffic switches to ${id} immediately.`
: `This becomes the live revision for ${agent.name}. Triggers start serving from it immediately.`,
: `This becomes the live revision for ${displayAgentName(agent) ?? agent.name}. Triggers start serving from it immediately.`,
confirmLabel: "Promote to live",
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ export function AgentSessionTranscriptView({
<Flex
direction="column"
gap="2"
className="shrink-0 cursor-default select-none px-6 pt-5 pb-3"
className="relative shrink-0 cursor-default select-none px-6 pt-5 pb-3"
>
<AgentBuilderHeaderControls />
<Link
to="/code/agents/applications/$idOrSlug/sessions"
params={{ idOrSlug }}
Expand All @@ -50,12 +51,9 @@ export function AgentSessionTranscriptView({
<ArrowLeftIcon size={13} />
Sessions
</Link>
<Flex align="center" justify="between" gap="4">
<Text className="font-bold text-[18px] text-gray-12 leading-tight tracking-tight">
Session transcript
</Text>
<AgentBuilderHeaderControls />
</Flex>
<Text className="pr-44 font-bold text-[18px] text-gray-12 leading-tight tracking-tight">
Session transcript
</Text>
</Flex>
<div className="min-h-0 flex-1">
<AgentSessionDetailBody idOrSlug={idOrSlug} sessionId={sessionId} />
Expand Down
39 changes: 39 additions & 0 deletions packages/ui/src/features/agent-applications/utils/format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { displayAgentName } from "./format";

describe("displayAgentName", () => {
it.each([
{
name: "concierge slug → Agent Builder (regardless of backend name)",
app: { slug: "agent-concierge", name: "Agent concierge" },
expected: "Agent Builder",
},
{
name: "concierge slug + no name → still Agent Builder",
app: { slug: "agent-concierge", name: null },
expected: "Agent Builder",
},
{
name: "non-concierge slug → returns the name",
app: { slug: "kudos-bot", name: "Kudos bot" },
expected: "Kudos bot",
},
{
name: "non-concierge slug + no name → undefined",
app: { slug: "kudos-bot", name: null },
expected: undefined,
},
{
name: "null app → undefined",
app: null,
expected: undefined,
},
{
name: "undefined app → undefined",
app: undefined,
expected: undefined,
},
])("$name", ({ app, expected }) => {
expect(displayAgentName(app)).toBe(expected);
});
});
18 changes: 18 additions & 0 deletions packages/ui/src/features/agent-applications/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@ import type {
AgentRevisionState,
AgentSessionState,
} from "@posthog/shared/agent-platform-types";
import { AGENT_BUILDER_SLUG } from "../agent-builder/agentBuilderStore";

/**
* The meta-agent that backs the in-app builder ships under the slug
* `agent-concierge` and is named "Agent concierge" in the backend. Everywhere
* the user sees it we want the product name "Agent Builder" instead —
* overriding here keeps the backend slug intact while normalising copy in one
* place, and matches the casing used elsewhere in the UI (dock header, page
* copy). Returns `undefined` when no name is known so callers can fall back to
* a slug or id of their choosing.
*/
export function displayAgentName(
app: { slug?: string | null; name?: string | null } | null | undefined,
): string | undefined {
if (!app) return undefined;
if (app.slug === AGENT_BUILDER_SLUG) return "Agent Builder";
return app.name ?? undefined;
}
Comment thread
benjackwhite marked this conversation as resolved.
Outdated

/** Formats a USD spend value for the fleet / agent stat strips. */
export function formatSpendUsd(value: number | null | undefined): string {
Expand Down
Loading
Loading