+ - {t("analytics.cacheReadShare", "Cache read share")}
- {share(value.readShare)}
+ - {t("analytics.cacheWriteShare", "Cache write share")}
- {share(value.writeShare)}
+ - {t("analytics.uncachedShare", "Uncached input share")}
- {share(value.uncachedShare)}
+ - {t("analytics.cacheCohort", "Eligible cache samples")}
- {formatNumber(value.eligibleSamples)}
+
+ {t("analytics.cacheShareMeaning", "Shares use samples with all three valid counters. Cache is already included in input. {missing} samples are excluded; insufficient data, zero input or overflow means Unknown.", { missing: formatNumber(value.missingSamples) })}
+ {t("analytics.cacheCohortTokens", "Eligible input: {input} · cache read: {read} · cache write: {write} · uncached: {uncached}", {
+ input: value.inputTokens === null ? t("analytics.unknown") : formatNumber(value.inputTokens),
+ read: value.readTokens === null ? t("analytics.unknown") : formatNumber(value.readTokens),
+ write: value.writeTokens === null ? t("analytics.unknown") : formatNumber(value.writeTokens),
+ uncached: value.uncachedTokens === null ? t("analytics.unknown") : formatNumber(value.uncachedTokens) })}
+ >
+}
+
+export function SessionTokenActivityView({ report, threads, thread, model, tokenRange, refreshing, onScope, onRange }: {
+ readonly report: TokenActivity | undefined
+ readonly threads: ReadonlyArray<{ readonly id: string; readonly label: string }>
+ readonly thread?: string
+ readonly model?: string
+ readonly tokenRange?: string
+ readonly refreshing: boolean
+ readonly onScope: (scope: { readonly thread?: string; readonly model: string | null }) => void
+ readonly onRange: (range: string) => void
+}) {
+ const id = useId()
+ if (report === undefined) return
+
+
{t("analytics.tokenActivity", "Token consumption")}
+
+
+
+
+
+ {t("analytics.tokenActivityMeaning", "Follows the selected Thread and model. Subtotals include known counters only; missing usage stays unknown. No cost is estimated.")}
+ {report.samples === 0 && {t("analytics.noRecordedUsage", "No recorded usage is available to analyze. This does not mean zero consumption.")}
}
+
+ - {t("analytics.usageSamples", "Usage samples")}
- {formatNumber(report.samples)}
+ - {t("analytics.timedUsageSamples", "Dated usage samples")}
- {formatNumber(report.timedSamples)}
+ - {t("analytics.untimedUsageSamples", "Usage samples without time")}
- {formatNumber(report.untimedSamples)}
+
+ {t("analytics.knownTokenSubtotals", "Known Token subtotals")}
+
+ {t("analytics.cacheShares", "Recorded input cache shares")}
+
+ {t("analytics.tokenDistribution", "Token distribution over recorded usage time")}
+ {t("analytics.usageClockMeaning", "Times are recorded usage values and have not been independently verified.")}
+ {t("analytics.tokenRangeMeaning", "Read records from the same interval and selected Thread. They provide context, not exact Token attribution to a response or model. Records with unknown or legacy times are excluded.")}
+ {t("analytics.tokenBarMeaning", "Bars and hotspots use known total subtotals. Unknown values have no bar. Intervals without records cannot be opened.")}
+ {report.firstRecordedAt !== null && report.lastRecordedAt !== null &&
+ →
+
+
}
+ {report.buckets.length === 0 ? {t("analytics.noDatedUsage", "No dated usage samples are available for intervals.")}
:
+ {report.buckets.map(bucket => )}
+
}
+ {hotspots.length > 0 && <>
+ {t("analytics.tokenHotspots", "Highest recorded consumption intervals")}
+ {hotspots.map(bucket => -
+
+ {bucket.records === 0 && {t("analytics.noTokenIntervalRecords", "No contemporaneous records are available in this interval.")}}
+ {t("analytics.tokenIntervalCoverage", "{samples} usage samples · {reported} total counters reported · {missing} total counters missing · {records} interval records", {
+ samples: formatNumber(bucket.samples), reported: formatNumber(bucket.tokens.total.reportedSamples), missing: formatNumber(bucket.tokens.total.missingSamples), records: formatNumber(bucket.records) })}
+
)}
+ >}
+ {report.untimedSamples > 0 && {t("analytics.untimedTokenSubtotals", "Known subtotals without a usage time ({count} samples)", { count: formatNumber(report.untimedSamples) })}
+ {t("analytics.untimedTokenMeaning", "Included in subtotals; no interval can be selected without a time.")}
+
+ }
+
+}
diff --git a/apps/web/src/workspace.css b/apps/web/src/workspace.css
index f3db197..72d4693 100644
--- a/apps/web/src/workspace.css
+++ b/apps/web/src/workspace.css
@@ -1197,6 +1197,17 @@ button.workspace-team-trigger:hover,
.analytics-link:disabled { color: inherit; cursor: default; }
.analytics-models { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); gap: 20px; }
.analytics-models h3 { font-size: 1rem; overflow-wrap: anywhere; }
+.analytics-token-scope { display: flex; flex-wrap: wrap; gap: 12px; max-width: 100%; }
+.analytics-token-coverage p { margin: 8px 0 0; color: var(--atape-color-text-muted); font-size: 0.8rem; line-height: 1.5; }
+.analytics-token-bars .analytics-activity-bar { min-width: 34px; }
+.analytics-token-unknown { border-style: dashed; }
+.analytics-token-hotspots { padding-inline-start: 24px; }
+.analytics-token-hotspots li { margin: 12px 0; }
+.analytics-token-hotspots li > span { display: block; margin-top: 4px; color: var(--atape-color-text-muted); font-size: 0.85rem; }
+.analytics-token-hotspots .analytics-link[aria-pressed="true"] { text-decoration: underline; }
+.analytics-token-hotspots .analytics-link:focus-visible { outline: 2px solid var(--atape-color-accent); outline-offset: 3px; }
+.analytics-untimed-tokens { margin-top: 24px; }
+.analytics-untimed-tokens summary { cursor: pointer; }
.analytics-filters { display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 20px; }
.analytics-filters label { display: grid; gap: 6px; font-size: 0.85rem; min-width: 150px; max-width: 100%; }
.analytics-filters select { max-width: 280px; border: 1px solid var(--atape-color-border-soft); border-radius: 6px; padding: 8px; background: var(--atape-color-paper); color: inherit; }
diff --git a/apps/web/test/sessionTokenActivityFixture.ts b/apps/web/test/sessionTokenActivityFixture.ts
new file mode 100644
index 0000000..c0ed5a8
--- /dev/null
+++ b/apps/web/test/sessionTokenActivityFixture.ts
@@ -0,0 +1,69 @@
+import type { SessionAnalytics } from "@atape/domain"
+import { stableToolsFixture } from "./sessionAnalyticsFixture"
+
+export type TokenAnalytics = Extract