session-context-usage.tsx 4.2 KB

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