refactor(web): project usage and snapshot request context
This commit is contained in:
@@ -221,7 +221,9 @@ function StreamingTail({ useSession, onGrow }: {
|
||||
* The chat view slot entry: pure component over the composed props (tool rows
|
||||
* render through the declared keyed hole's renderSlot share).
|
||||
*/
|
||||
export function ChatView({ useSession, useSessions, useStore, renderSlot, sessionId, openFile, loadOlder }: ChatViewSlotProps) {
|
||||
export function ChatView({
|
||||
useProjection, useSession, useSessions, useStore, renderSlot, sessionId, openFile, loadOlder,
|
||||
}: ChatViewSlotProps) {
|
||||
const nodes = useSession(s => s.nodes)
|
||||
// Workspace root off the session list row: path summaries display relative to it.
|
||||
const cwd = useSessions(s => s.byId[sessionId]?.cwd)
|
||||
@@ -381,7 +383,7 @@ export function ChatView({ useSession, useSessions, useStore, renderSlot, sessio
|
||||
{running && <TurnDots />}
|
||||
</div>
|
||||
</div>
|
||||
<StatsLine useSession={useSession} />
|
||||
<StatsLine useSession={useSession} useProjection={useProjection} />
|
||||
{!atBottom && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// Settled-node identity prevents stream-delta updates from rerendering this row.
|
||||
|
||||
import { memo, useMemo } from 'react'
|
||||
import type { ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {
|
||||
ConversationSnapshot, UseProjection,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ModelRequestTelemetry } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { TokenUsageProjection } from '@deepseek-ai/dsh-token-meter/client'
|
||||
import css from './StatsLine.module.css'
|
||||
|
||||
type SessionMetrics = NonNullable<ConversationSnapshot['metrics']>
|
||||
|
||||
interface VisibleCounts {
|
||||
turns: number
|
||||
steps: number
|
||||
@@ -44,59 +46,59 @@ export function formatMetricTokens(value: number): string {
|
||||
|
||||
/**
|
||||
* Existing Web cache-hit formula over disjoint uncached and cache-read input.
|
||||
* @param metrics - Host-owned durable usage.
|
||||
* @param usage - full-log token usage projection.
|
||||
* @returns rounded integer percent, or null when no input was billed.
|
||||
*/
|
||||
export function cacheHitPercent(metrics: SessionMetrics): number | null {
|
||||
const denominator = metrics.uncachedInputTokens + metrics.cacheReadTokens
|
||||
export function cacheHitPercent(usage: TokenUsageProjection): number | null {
|
||||
const denominator = usage.uncachedInputTokens + usage.cacheReadTokens
|
||||
return denominator === 0
|
||||
? null
|
||||
: Math.round(metrics.cacheReadTokens / denominator * 100)
|
||||
: Math.round(usage.cacheReadTokens / denominator * 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Current context occupancy using the TUI's integer rounding and upper clamp.
|
||||
* @param metrics - Host-owned durable pressure.
|
||||
* @param contextWindow - capacity from the latest request observed on this mux generation.
|
||||
* @param request - one atomic request snapshot observed on this mux generation.
|
||||
* @returns occupancy percent, or null when either input is unavailable.
|
||||
*/
|
||||
export function contextPercent(metrics: SessionMetrics, contextWindow: number | undefined): number | null {
|
||||
if (metrics.contextTokens === undefined || contextWindow === undefined) return null
|
||||
return Math.min(100, Math.round(metrics.contextTokens / contextWindow * 100))
|
||||
export function contextPercent(request: ModelRequestTelemetry | null): number | null {
|
||||
if (request?.contextTokens === undefined || request.contextWindow === undefined) return null
|
||||
return Math.min(100, Math.round(request.contextTokens / request.contextWindow * 100))
|
||||
}
|
||||
|
||||
/** Props: the conversation-snapshot selector hook (handed down by ChatView). */
|
||||
export interface StatsLineProps { useSession: SnapshotSelectorHook<ConversationSnapshot> }
|
||||
/** Props: standard session hooks handed down by ChatView. */
|
||||
export interface StatsLineProps {
|
||||
useSession: SnapshotSelectorHook<ConversationSnapshot>
|
||||
useProjection: UseProjection
|
||||
}
|
||||
|
||||
export const StatsLine = memo(function StatsLine({ useSession }: StatsLineProps) {
|
||||
export const StatsLine = memo(function StatsLine({ useSession, useProjection }: StatsLineProps) {
|
||||
const nodes = useSession(s => s.nodes)
|
||||
const metrics = useSession(s => s.metrics)
|
||||
const contextWindow = useSession(s => s.modelRequestContextWindow)
|
||||
const modelRequest = useSession(s => s.modelRequest)
|
||||
const usage = useProjection('tokenUsage')
|
||||
const counts = useMemo(() => deriveVisibleCounts(nodes), [nodes])
|
||||
if (counts.steps === 0 && (
|
||||
metrics === null
|
||||
|| (
|
||||
metrics.uncachedInputTokens === 0
|
||||
&& metrics.outputTokens === 0
|
||||
&& metrics.cacheReadTokens === 0
|
||||
&& (metrics.contextTokens ?? 0) === 0
|
||||
)
|
||||
)) return null
|
||||
const hasUsage = usage !== undefined && (
|
||||
usage.uncachedInputTokens !== 0
|
||||
|| usage.outputTokens !== 0
|
||||
|| usage.cacheReadTokens !== 0
|
||||
|| usage.cacheWriteTokens !== 0
|
||||
)
|
||||
const context = contextPercent(modelRequest)
|
||||
if (counts.steps === 0 && !hasUsage && context === null) return null
|
||||
|
||||
const parts: string[] = []
|
||||
if (metrics === null) {
|
||||
if (usage === undefined) {
|
||||
parts.push('usage unknown')
|
||||
parts.push('context unknown')
|
||||
} else {
|
||||
parts.push(`${formatMetricTokens(metrics.uncachedInputTokens)} uncached input`)
|
||||
parts.push(`${formatMetricTokens(metrics.outputTokens)} output`)
|
||||
parts.push(`${formatMetricTokens(metrics.cacheReadTokens)} cache read`)
|
||||
const cacheHit = cacheHitPercent(metrics)
|
||||
parts.push(`${formatMetricTokens(usage.uncachedInputTokens)} uncached input`)
|
||||
parts.push(`${formatMetricTokens(usage.outputTokens)} output`)
|
||||
parts.push(`${formatMetricTokens(usage.cacheReadTokens)} cache read`)
|
||||
const cacheHit = cacheHitPercent(usage)
|
||||
if (cacheHit !== null) parts.push(`cache hit ${cacheHit}%`)
|
||||
const context = contextPercent(metrics, contextWindow)
|
||||
parts.push(context === null
|
||||
? 'context unknown'
|
||||
: `context ${context}% of ${formatMetricTokens(contextWindow as number)}`)
|
||||
}
|
||||
parts.push(context === null || modelRequest?.contextWindow === undefined
|
||||
? 'context unknown'
|
||||
: `context ${context}% of ${formatMetricTokens(modelRequest.contextWindow)}`)
|
||||
parts.push(`${counts.turns} turns`)
|
||||
parts.push(`${counts.steps} steps`)
|
||||
return <div className={css.root}>{parts.join(' · ')}</div>
|
||||
|
||||
Reference in New Issue
Block a user