diff --git a/packages/junior-dashboard/e2e/conversation-details.spec.ts b/packages/junior-dashboard/e2e/conversation-details.spec.ts
index 8259cb8122..a3d294c999 100644
--- a/packages/junior-dashboard/e2e/conversation-details.spec.ts
+++ b/packages/junior-dashboard/e2e/conversation-details.spec.ts
@@ -48,11 +48,11 @@ test("opens conversation details beside the chat", async ({
});
// Wide viewports open details beside the chat by default.
await expect(details).toBeVisible();
- // Run facts live in details only, not in the title bar.
+ // Cost stays in the title bar. Desktop details hide their copy of it.
const costMetric = page
.getByRole("main")
.locator('span[tabindex="0"]')
- .filter({ hasText: /^\$0\.03$/ });
+ .filter({ hasText: /^\$0\.03$/, visible: true });
await expect(costMetric).toHaveCount(1);
// Tooltips open on keyboard focus, not on focus after a mouse click. A key
// press first makes this a keyboard focus.
@@ -72,7 +72,7 @@ test("opens conversation details beside the chat", async ({
).toHaveAttribute("href", "https://github.com/getsentry/payments/pull/77");
await detailsButton.click();
await expect(details).toBeHidden();
- await expect(costMetric).toHaveCount(0);
+ await expect(costMetric).toHaveCount(1);
await context.grantPermissions(["clipboard-read", "clipboard-write"], {
origin: dashboard.baseURL,
});
diff --git a/packages/junior-dashboard/src/client/conversations/ConversationHeader.tsx b/packages/junior-dashboard/src/client/conversations/ConversationHeader.tsx
index 22acf3c50a..a15b835c84 100644
--- a/packages/junior-dashboard/src/client/conversations/ConversationHeader.tsx
+++ b/packages/junior-dashboard/src/client/conversations/ConversationHeader.tsx
@@ -20,14 +20,17 @@ import { ShareConversationButton } from "./ShareConversationButton";
import type { TranscriptViewMode } from "./transcriptRenderModel";
/**
- * Show the conversation title bar: where it runs, the title, and actions.
- * Run facts, linked work, and the Brief live in Conversation details.
+ * Show the conversation title bar: where it runs, the title, its cost, and
+ * actions. Other run facts, linked work, and the Brief live in Conversation
+ * details.
*/
export function ConversationHeader(props: {
/** Parent chain of a child Conversation. Root Conversations omit it. */
agentTree?: ReactNode;
/** A `menu` layout copy action for the `⋯` menu. */
copyAction?: ReactNode;
+ /** Estimated cost. Desktop only; mobile shows it in Conversation details. */
+ cost?: ReactNode;
archive: ConversationArchiveAction;
conversationId: string;
detailsOpen: boolean;
@@ -179,6 +182,11 @@ export function ConversationHeader(props: {
{props.privacy}
+ {props.cost ? (
+
+ {props.cost}
+
+ ) : null}
{props.view === "raw" ? (
// Event log is a debug view reached from the menu. Say which
// view this is, and give one click back.
diff --git a/packages/junior-dashboard/src/client/conversations/ConversationMeta.tsx b/packages/junior-dashboard/src/client/conversations/ConversationMeta.tsx
index 5c10dc37ea..14f53a54f6 100644
--- a/packages/junior-dashboard/src/client/conversations/ConversationMeta.tsx
+++ b/packages/junior-dashboard/src/client/conversations/ConversationMeta.tsx
@@ -568,6 +568,37 @@ export function conversationLocation(
);
}
+/**
+ * Show the estimated cost of a conversation with its breakdown tooltip.
+ * Returns null until there is a cost or a live run to show.
+ */
+export function conversationCost(
+ conversation: Conversation | undefined,
+ detail: ConversationDetailReport | undefined,
+ tooltipPlacement?: "above" | "below",
+): ReactNode {
+ if (!conversation) return null;
+ const costSummary = summarizeCost(
+ detail?.cumulativeUsage ?? conversation.cumulativeUsage,
+ );
+ const auxiliaryCosts = detail?.auxiliaryCosts ?? conversation.auxiliaryCosts;
+ const live = (detail?.status ?? conversation.status) === "active";
+ if (!costSummary && !auxiliaryCosts && !live) return null;
+ return (
+
+ );
+}
+
function conversationStatItems(props: {
conversation: Conversation | undefined;
detail?: ConversationDetailReport;
@@ -580,7 +611,6 @@ function conversationStatItems(props: {
props.detail?.cumulativeUsage ?? props.conversation.cumulativeUsage;
const tokenSummary =
summarizeModelUsage(props.detail?.modelUsage) ?? summarizeUsage(usage);
- const costSummary = summarizeCost(usage);
const durationLabel = formatConversationDuration(props.conversation);
const live = (props.detail?.status ?? props.conversation.status) === "active";
const activeModelId = liveModelId(props.detail);
@@ -589,8 +619,9 @@ function conversationStatItems(props: {
props.detail?.childAgentTotals ?? props.conversation.childAgentTotals;
const lastActivityAt = props.conversation.lastSeenAt;
const location = conversationLocation(props.conversation, props.detail);
+ const cost = conversationCost(props.conversation, props.detail);
const rawStats: Array = [
- // The desktop title bar shows the location. Mobile hides that bar.
+ // The desktop title bar shows the location and cost. Mobile hides that bar.
location
? {
content: location,
@@ -660,26 +691,12 @@ function conversationStatItems(props: {
label: "Tokens",
}
: undefined,
- costSummary ||
- props.detail?.auxiliaryCosts ||
- props.conversation.auxiliaryCosts ||
- live
+ cost
? {
- content: (
-
- ),
+ content: cost,
key: "cost",
label: "Cost",
+ mobileOnly: true,
}
: undefined,
childAgentTotals
diff --git a/packages/junior-dashboard/src/client/conversations/ConversationPage.tsx b/packages/junior-dashboard/src/client/conversations/ConversationPage.tsx
index 7b20e38bad..4ae43473d8 100644
--- a/packages/junior-dashboard/src/client/conversations/ConversationPage.tsx
+++ b/packages/junior-dashboard/src/client/conversations/ConversationPage.tsx
@@ -50,6 +50,7 @@ import { ConversationAgentTree } from "./ConversationAgentTree";
import { conversationLinks } from "./ConversationLinkedWork";
import {
ConversationAnnotations,
+ conversationCost,
conversationLocation,
ConversationPrivacyChip,
ConversationStats,
@@ -373,6 +374,7 @@ export function ConversationPage(props: {
agentTree ? : null
}
conversationId={conversationId}
+ cost={conversationCost(conversation, detail.data, "below")}
detailsOpen={showDetails}
onDetailsClick={() => {
if (eventOpen) setSelectedEventSeq(undefined);
diff --git a/packages/junior-dashboard/src/client/conversations/TelemetryMetrics.tsx b/packages/junior-dashboard/src/client/conversations/TelemetryMetrics.tsx
index 35935b1e9b..3d6b48bd3e 100644
--- a/packages/junior-dashboard/src/client/conversations/TelemetryMetrics.tsx
+++ b/packages/junior-dashboard/src/client/conversations/TelemetryMetrics.tsx
@@ -350,6 +350,7 @@ export function CostMetric(props: {
liveModelId?: string;
modelUsage?: ConversationModelUsage[];
summary: CostUsageSummary | undefined;
+ tooltipPlacement?: "above" | "below";
}) {
// Model history can include calls not yet counted in saved Run totals.
const summary = summarizeModelCost(props.modelUsage) ?? props.summary;
@@ -369,7 +370,7 @@ export function CostMetric(props: {
return (
-
+
{props.title}
{props.note ? (
· {props.note}
) : null}
-
+
{props.total}
@@ -124,7 +124,7 @@ export function UsageCard(props: UsageCardProps) {