Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 3 additions & 3 deletions packages/junior-dashboard/e2e/conversation-details.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -179,6 +182,11 @@ export function ConversationHeader(props: {
<span className="shrink-0">{props.privacy}</span>
</div>
<div className="flex shrink-0 items-center gap-0.5">
{props.cost ? (
<span className="mr-2 text-xs tabular-nums text-dashboard-text-muted">
{props.cost}
</span>
) : null}
{props.view === "raw" ? (
// Event log is a debug view reached from the menu. Say which
// view this is, and give one click back.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<CostMetric
auxiliaryCosts={auxiliaryCosts}
childAgentTotals={
detail?.childAgentTotals ?? conversation.childAgentTotals
}
live={live}
liveModelId={liveModelId(detail)}
modelUsage={detail?.modelUsage}
summary={costSummary}
tooltipPlacement={tooltipPlacement}
/>
);
}

function conversationStatItems(props: {
conversation: Conversation | undefined;
detail?: ConversationDetailReport;
Expand All @@ -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);
Expand All @@ -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<ConversationStatItem | undefined> = [
// 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,
Expand Down Expand Up @@ -660,26 +691,12 @@ function conversationStatItems(props: {
label: "Tokens",
}
: undefined,
costSummary ||
props.detail?.auxiliaryCosts ||
props.conversation.auxiliaryCosts ||
live
cost
? {
content: (
<CostMetric
auxiliaryCosts={
props.detail?.auxiliaryCosts ??
props.conversation.auxiliaryCosts
}
childAgentTotals={childAgentTotals}
live={live}
liveModelId={activeModelId}
modelUsage={props.detail?.modelUsage}
summary={costSummary}
/>
),
content: cost,
key: "cost",
label: "Cost",
mobileOnly: true,
}
: undefined,
childAgentTotals
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { ConversationAgentTree } from "./ConversationAgentTree";
import { conversationLinks } from "./ConversationLinkedWork";
import {
ConversationAnnotations,
conversationCost,
conversationLocation,
ConversationPrivacyChip,
ConversationStats,
Expand Down Expand Up @@ -373,6 +374,7 @@ export function ConversationPage(props: {
agentTree ? <ConversationAgentTree nodes={agentTree} /> : null
}
conversationId={conversationId}
cost={conversationCost(conversation, detail.data, "below")}
detailsOpen={showDetails}
onDetailsClick={() => {
if (eventOpen) setSelectedEventSeq(undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -369,7 +370,7 @@ export function CostMetric(props: {
return (
<MetricValue
align={props.align}
tooltipPlacement="above"
tooltipPlacement={props.tooltipPlacement ?? "above"}
tooltip={
!card && props.liveModelId
? [modelHeading(props.liveModelId, props.liveModelId)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ export function UsageCard(props: UsageCardProps) {
)}
>
<span className="flex items-baseline justify-between gap-3">
<span className="min-w-0 truncate">
<span className="min-w-0 truncate font-medium text-dashboard-text">
{props.title}
{props.note ? (
<span className="text-dashboard-text-faint"> · {props.note}</span>
) : null}
</span>
<span className="whitespace-nowrap text-lg font-semibold tabular-nums text-dashboard-text">
<span className="whitespace-nowrap text-base font-semibold tabular-nums text-dashboard-text">
{props.total}
</span>
</span>
Expand Down Expand Up @@ -124,7 +124,7 @@ export function UsageCard(props: UsageCardProps) {
<span
className={cn(
"flex min-w-0 items-baseline gap-2",
row.code && "font-mono text-dashboard-text",
row.code && "font-mono",
)}
>
<span className="min-w-0 truncate" title={row.label}>
Expand Down
Loading