session-context-usage.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import { Match, Show, Switch, createMemo } from "solid-js"
  2. import { Tooltip, type TooltipProps } from "@opencode-ai/ui/tooltip"
  3. import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
  4. import { Button } from "@opencode-ai/ui/button"
  5. import { useFile } from "@/context/file"
  6. import { useLayout } from "@/context/layout"
  7. import { useSync } from "@/context/sync"
  8. import { useLanguage } from "@/context/language"
  9. import { useProviders } from "@/hooks/use-providers"
  10. import { getSessionContextMetrics } from "@/components/session/session-context-metrics"
  11. import { useSessionLayout } from "@/pages/session/session-layout"
  12. import { createSessionTabs } from "@/pages/session/helpers"
  13. interface SessionContextUsageProps {
  14. variant?: "button" | "indicator"
  15. placement?: TooltipProps["placement"]
  16. }
  17. function openSessionContext(args: {
  18. view: ReturnType<ReturnType<typeof useLayout>["view"]>
  19. layout: ReturnType<typeof useLayout>
  20. tabs: ReturnType<ReturnType<typeof useLayout>["tabs"]>
  21. }) {
  22. if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open()
  23. if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
  24. void args.tabs.open("context")
  25. args.tabs.setActive("context")
  26. }
  27. export function SessionContextUsage(props: SessionContextUsageProps) {
  28. const sync = useSync()
  29. const file = useFile()
  30. const layout = useLayout()
  31. const language = useLanguage()
  32. const providers = useProviders()
  33. const { params, tabs, view } = useSessionLayout()
  34. const variant = createMemo(() => props.variant ?? "button")
  35. const tabState = createSessionTabs({
  36. tabs,
  37. pathFromTab: file.pathFromTab,
  38. normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
  39. })
  40. const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
  41. const usd = createMemo(
  42. () =>
  43. new Intl.NumberFormat(language.intl(), {
  44. style: "currency",
  45. currency: "USD",
  46. }),
  47. )
  48. const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all()))
  49. const context = createMemo(() => metrics().context)
  50. const cost = createMemo(() => {
  51. return usd().format(metrics().totalCost)
  52. })
  53. const openContext = () => {
  54. if (!params.id) return
  55. if (tabState.activeTab() === "context") {
  56. tabs().close("context")
  57. return
  58. }
  59. openSessionContext({
  60. view: view(),
  61. layout,
  62. tabs: tabs(),
  63. })
  64. }
  65. const circle = () => (
  66. <div class="flex items-center justify-center">
  67. <ProgressCircle size={16} strokeWidth={2} percentage={context()?.usage ?? 0} />
  68. </div>
  69. )
  70. const tooltipValue = () => (
  71. <div>
  72. <Show when={context()}>
  73. {(ctx) => (
  74. <>
  75. <div class="flex items-center gap-2">
  76. <span class="text-text-invert-strong">{ctx().total.toLocaleString(language.intl())}</span>
  77. <span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
  78. </div>
  79. <div class="flex items-center gap-2">
  80. <span class="text-text-invert-strong">{ctx().usage ?? 0}%</span>
  81. <span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
  82. </div>
  83. </>
  84. )}
  85. </Show>
  86. <div class="flex items-center gap-2">
  87. <span class="text-text-invert-strong">{cost()}</span>
  88. <span class="text-text-invert-base">{language.t("context.usage.cost")}</span>
  89. </div>
  90. </div>
  91. )
  92. return (
  93. <Show when={params.id}>
  94. <Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
  95. <Switch>
  96. <Match when={variant() === "indicator"}>{circle()}</Match>
  97. <Match when={true}>
  98. <Button
  99. type="button"
  100. variant="ghost"
  101. class="size-6"
  102. onClick={openContext}
  103. aria-label={language.t("context.usage.view")}
  104. >
  105. {circle()}
  106. </Button>
  107. </Match>
  108. </Switch>
  109. </Tooltip>
  110. </Show>
  111. )
  112. }