Repository navigation
feat(web): opt-in tokens-per-second readout under finished responses #18304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -93,12 +93,14 @@ import { FileDiff } from "@pierre/diffs/react"; | |||||
| import { DiffWorkerPoolProvider } from "../DiffWorkerPoolProvider"; | ||||||
| import { | ||||||
| type TimelineEntry, | ||||||
| formatTokenRate, | ||||||
| providerErrorPresentation, | ||||||
| createMessageAttachmentPreviewProjector, | ||||||
| selectMessageImageResources, | ||||||
| workEntryDisplayIndicatesToolFailure, | ||||||
| workEntrySignalsSevereFailure, | ||||||
| workLogEntryIsToolLike, | ||||||
| type RunTokenRate, | ||||||
| } from "../../session-logic"; | ||||||
| import type { CodexArtifactTemplate } from "@t3tools/shared/codexArtifactTemplates"; | ||||||
| import { | ||||||
|
|
@@ -511,6 +513,8 @@ interface MessagesTimelineProps { | |||||
| skills?: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>; | ||||||
| providerStatuses: ReadonlyArray<ServerProvider>; | ||||||
| runs: ReadonlyArray<HandoffTimelineRun>; | ||||||
| /** Settled-run token rates for the assistant footer readout; absent when disabled. */ | ||||||
| tokenRateByRunId?: ReadonlyMap<RunId, RunTokenRate>; | ||||||
| anchorMessageId: MessageId | null; | ||||||
| onAnchorReady: (messageId: MessageId, anchorIndex: number) => void; | ||||||
| onAnchorSizeChanged: (messageId: MessageId, size: number) => void; | ||||||
|
|
@@ -616,6 +620,7 @@ const ConversationTimeline = memo(function ConversationTimeline({ | |||||
| skills = EMPTY_TIMELINE_SKILLS, | ||||||
| providerStatuses, | ||||||
| runs: runsProp, | ||||||
| tokenRateByRunId: tokenRateByRunIdProp, | ||||||
| anchorMessageId, | ||||||
| onAnchorReady, | ||||||
| onAnchorSizeChanged, | ||||||
|
|
@@ -878,6 +883,9 @@ const ConversationTimeline = memo(function ConversationTimeline({ | |||||
| readonly workspaceRoot: string | undefined; | ||||||
| readonly projection: MessagesTimelineRowsProjection; | ||||||
| } | null>(null); | ||||||
| // Stabilized beside the other row inputs so the map's fresh identity per | ||||||
| // projection event does not defeat the streaming fast path below. | ||||||
| const tokenRateByRunId = useStableTokenRates(tokenRateByRunIdProp); | ||||||
| const rawRows = useMemo(() => { | ||||||
| const previous = rowsProjectionRef.current; | ||||||
| const projection = deriveMessagesTimelineRowsWithState( | ||||||
|
|
@@ -894,6 +902,7 @@ const ConversationTimeline = memo(function ConversationTimeline({ | |||||
| turnDiffSummaries, | ||||||
| supportsConversationRollback, | ||||||
| worktreeSetup, | ||||||
| ...(tokenRateByRunId === undefined ? {} : { tokenRateByRunId }), | ||||||
| }, | ||||||
| previous?.threadKey === listIdentityKey && previous.workspaceRoot === workspaceRoot | ||||||
| ? previous.projection | ||||||
|
|
@@ -917,6 +926,7 @@ const ConversationTimeline = memo(function ConversationTimeline({ | |||||
| turnDiffSummaries, | ||||||
| supportsConversationRollback, | ||||||
| worktreeSetup, | ||||||
| tokenRateByRunId, | ||||||
| ]); | ||||||
| const rows = useStableRows(rawRows, listIdentityKey); | ||||||
| // Run status/timestamps churn on every stream event; the shared row context | ||||||
|
|
@@ -2792,6 +2802,7 @@ function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "mess | |||||
| message={row.message} | ||||||
| showCopyButton={row.showAssistantCopyButton} | ||||||
| copyStreaming={row.assistantCopyStreaming} | ||||||
| tokenRate={row.tokenRate} | ||||||
| /> | ||||||
| ) : null} | ||||||
| </div> | ||||||
|
|
@@ -2859,6 +2870,7 @@ function AssistantMetaTimelineRow({ | |||||
| showCopyButton={row.showAssistantCopyButton} | ||||||
| copyStreaming={row.assistantCopyStreaming} | ||||||
| alwaysVisible | ||||||
| tokenRate={row.tokenRate} | ||||||
| /> | ||||||
| </div> | ||||||
| ); | ||||||
|
|
@@ -2871,13 +2883,15 @@ function AssistantMessageMeta({ | |||||
| showCopyButton, | ||||||
| copyStreaming, | ||||||
| alwaysVisible = false, | ||||||
| tokenRate, | ||||||
| }: { | ||||||
| className?: string; | ||||||
| projectedItem?: Extract<TimelineRow, { kind: "message" }>["projectedItem"]; | ||||||
| message: ChatMessage; | ||||||
| showCopyButton: boolean; | ||||||
| copyStreaming: boolean; | ||||||
| alwaysVisible?: boolean; | ||||||
| tokenRate: RunTokenRate | undefined; | ||||||
| }) { | ||||||
| const ctx = use(TimelineRowCtx); | ||||||
|
|
||||||
|
|
@@ -2904,6 +2918,16 @@ function AssistantMessageMeta({ | |||||
| {projectedItem?.item.type === "assistant_message" ? ( | ||||||
| <AssistantForkButton projectedItem={projectedItem} /> | ||||||
| ) : null} | ||||||
| {tokenRate && !message.streaming ? ( | ||||||
| <Tooltip> | ||||||
| <TooltipTrigger render={<p className="text-muted-foreground text-xs tabular-nums" />}> | ||||||
| {formatTokenRate(tokenRate.outputTokens, tokenRate.elapsedMs)} | ||||||
| </TooltipTrigger> | ||||||
| <TooltipPopup> | ||||||
| {`${tokenRate.outputTokens} output tokens in ${(tokenRate.elapsedMs / 1000).toFixed(1)}s`} | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Keep the elapsed time exact in the tooltip. For a run lasting 1,234 ms, Proposed fix- {`${tokenRate.outputTokens} output tokens in ${(tokenRate.elapsedMs / 1000).toFixed(1)}s`}
+ {`${tokenRate.outputTokens} output tokens in ${tokenRate.elapsedMs}ms`}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| </TooltipPopup> | ||||||
| </Tooltip> | ||||||
| ) : null} | ||||||
| {!message.streaming && ( | ||||||
| <Tooltip> | ||||||
| <TooltipTrigger render={<p className="text-muted-foreground text-xs tabular-nums" />}> | ||||||
|
|
@@ -4910,6 +4934,31 @@ function useStableHandoffRuns( | |||||
| }, [runs]); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Content-stable token rates. The incoming map is rebuilt with every | ||||||
| * projection event, but settled-run rates never change afterwards — keep the | ||||||
| * reference until a rate is added so the row projection fast path survives. | ||||||
| */ | ||||||
| function useStableTokenRates( | ||||||
| rates: ReadonlyMap<RunId, RunTokenRate> | undefined, | ||||||
| ): ReadonlyMap<RunId, RunTokenRate> | undefined { | ||||||
| const prev = useRef<{ signature: string; value: ReadonlyMap<RunId, RunTokenRate> }>({ | ||||||
| signature: "", | ||||||
| value: new Map(), | ||||||
| }); | ||||||
| return useMemo(() => { | ||||||
| if (rates === undefined) return undefined; | ||||||
| const signature = [...rates] | ||||||
| .map(([runId, rate]) => `${runId} ${rate.outputTokens} ${rate.elapsedMs}`) | ||||||
| .sort() | ||||||
| .join("\n"); | ||||||
| if (signature !== prev.current.signature) { | ||||||
| prev.current = { signature, value: new Map(rates) }; | ||||||
| } | ||||||
| return prev.current.value; | ||||||
| }, [rates]); | ||||||
| } | ||||||
|
|
||||||
| /** Returns a structurally-shared copy of `rows`: for each row whose content | ||||||
| * hasn't changed since last call, the previous object reference is reused. */ | ||||||
| function useStableRows(rows: MessagesTimelineRow[], identity: string): MessagesTimelineRow[] { | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Compare
tokenRatebefore reusing a message row.If a user changes Token rate after a response settles,
deriveMessagesTimelineRowsupdates the rate field.isRowUnchangeddoes not compare that field for either message or assistant-meta rows, socomputeStableMessagesTimelineRowscan return the old row. Enabling the setting can leave the rate hidden; disabling it can leave the rate visible. IncludetokenRatein both row comparisons. (raw.githubusercontent.com)🤖 Prompt for AI Agents