feat: add chat summary tab in the right sidebar and per-chat cost endpoint (#26649)

Stacked on #26657 (the persisted whole-chat summary backend). Base
branch is `chat-summary-62j9`; review/merge that first.

Adds a reusable `ChatSummary` component.

The summary text is the persisted whole-chat summary (`chat.summary`)
introduced by #26657. It is generated asynchronously and may be `null`
until the first summary is produced, in which case the popover renders a
muted empty state. Live updates arrive via that PR's
`chat_summary_change` watch event, which is already merged into the chat
caches.

Cost is served by a new per-chat endpoint, `GET
/api/experimental/chats/{chat}/cost`, which rolls up assistant-message
cost across a chat's root and child (subagent) chats and is authorized
like the other `{chat}` routes (read on the chat, 404 otherwise).

Visual and interaction coverage lives in `ChatSummary.stories.tsx` and
`ChatSummaryPopover.stories.tsx` (including populated-summary,
empty-state, and cost-loading cases).

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jaayden Halko
2026-07-27 10:05:05 +01:00
committed by GitHub
co-authored by Cursor
parent 88c7304e0b
commit 6f2011af88
38 changed files with 1744 additions and 87 deletions
+38 -13
View File
@@ -1923,19 +1923,19 @@ type ChatCostUsersOptions struct {
// ChatCostSummary is the response from the chat cost summary endpoint.
type ChatCostSummary struct {
StartDate time.Time `json:"start_date" format:"date-time"`
EndDate time.Time `json:"end_date" format:"date-time"`
TotalCostMicros int64 `json:"total_cost_micros"`
PricedMessageCount int64 `json:"priced_message_count"`
UnpricedMessageCount int64 `json:"unpriced_message_count"`
TotalInputTokens int64 `json:"total_input_tokens"`
TotalOutputTokens int64 `json:"total_output_tokens"`
TotalCacheReadTokens int64 `json:"total_cache_read_tokens"`
TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"`
TotalRuntimeMs int64 `json:"total_runtime_ms"`
ByModel []ChatCostModelBreakdown `json:"by_model"`
ByChat []ChatCostChatBreakdown `json:"by_chat"`
UsageLimit *ChatUsageLimitStatus `json:"usage_limit,omitempty"`
StartDate time.Time `json:"start_date" format:"date-time"`
EndDate time.Time `json:"end_date" format:"date-time"`
TotalCostMicros int64 `json:"total_cost_micros"`
PricedMessageCount int64 `json:"priced_message_count"`
UnpricedMessagesHavingUsageCount int64 `json:"unpriced_messages_having_usage_count"`
TotalInputTokens int64 `json:"total_input_tokens"`
TotalOutputTokens int64 `json:"total_output_tokens"`
TotalCacheReadTokens int64 `json:"total_cache_read_tokens"`
TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"`
TotalRuntimeMs int64 `json:"total_runtime_ms"`
ByModel []ChatCostModelBreakdown `json:"by_model"`
ByChat []ChatCostChatBreakdown `json:"by_chat"`
UsageLimit *ChatUsageLimitStatus `json:"usage_limit,omitempty"`
}
// ChatCostModelBreakdown contains per-model cost aggregation.
@@ -1966,6 +1966,17 @@ type ChatCostChatBreakdown struct {
TotalRuntimeMs int64 `json:"total_runtime_ms"`
}
// ChatCost is the cumulative cost for a selected chat's subtree: the
// chat itself plus every descendant (subagent) chat it spawned. A root
// chat therefore reports its whole tree, while a subagent reports only
// its own spend plus any nested subagents.
type ChatCost struct {
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
TotalCostMicros int64 `json:"total_cost_micros"`
PricedMessageCount int64 `json:"priced_message_count"`
UnpricedMessagesHavingUsageCount int64 `json:"unpriced_messages_having_usage_count"`
}
// ChatCostUserRollup contains per-user cost aggregation for admin views.
type ChatCostUserRollup struct {
UserID uuid.UUID `json:"user_id" format:"uuid"`
@@ -2541,6 +2552,20 @@ func (c *ExperimentalClient) GetChatCostSummary(ctx context.Context, user string
return summary, json.NewDecoder(res.Body).Decode(&summary)
}
// GetChatCost returns the cumulative cost for a single chat.
func (c *ExperimentalClient) GetChatCost(ctx context.Context, chatID uuid.UUID) (ChatCost, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s/cost", chatID), nil)
if err != nil {
return ChatCost{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatCost{}, ReadBodyAsError(res)
}
var cost ChatCost
return cost, json.NewDecoder(res.Body).Decode(&cost)
}
// GetChatCostUsers returns a per-user cost rollup for the deployment
// (admin only). Zero-valued StartDate or EndDate fields are omitted from
// the request, letting the server apply its own defaults (typically the