session-context-usage.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import { Match, Show, Switch, createMemo } from "solid-js"
  2. import { Tooltip } from "@opencode-ai/ui/tooltip"
  3. import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
  4. import { Button } from "@opencode-ai/ui/button"
  5. import { useParams } from "@solidjs/router"
  6. import { AssistantMessage } from "@opencode-ai/sdk/v2/client"
  7. import { findLast } from "@opencode-ai/util/array"
  8. import { useLayout } from "@/context/layout"
  9. import { useSync } from "@/context/sync"
  10. import { useLanguage } from "@/context/language"
  11. interface SessionContextUsageProps {
  12. variant?: "button" | "indicator"
  13. }
  14. export function SessionContextUsage(props: SessionContextUsageProps) {
  15. const sync = useSync()
  16. const params = useParams()
  17. const layout = useLayout()
  18. const language = useLanguage()
  19. const variant = createMemo(() => props.variant ?? "button")
  20. const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
  21. const tabs = createMemo(() => layout.tabs(sessionKey))
  22. const view = createMemo(() => layout.view(sessionKey))
  23. const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
  24. const usd = createMemo(
  25. () =>
  26. new Intl.NumberFormat(language.locale(), {
  27. style: "currency",
  28. currency: "USD",
  29. }),
  30. )
  31. const cost = createMemo(() => {
  32. const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
  33. return usd().format(total)
  34. })
  35. const context = createMemo(() => {
  36. const locale = language.locale()
  37. const last = findLast(messages(), (x) => {
  38. if (x.role !== "assistant") return false
  39. const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write
  40. return total > 0
  41. }) as AssistantMessage
  42. if (!last) return
  43. const total =
  44. last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
  45. const model = sync.data.provider.all.find((x) => x.id === last.providerID)?.models[last.modelID]
  46. return {
  47. tokens: total.toLocaleString(locale),
  48. percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null,
  49. }
  50. })
  51. const openContext = () => {
  52. if (!params.id) return
  53. if (!view().reviewPanel.opened()) view().reviewPanel.open()
  54. layout.fileTree.open()
  55. layout.fileTree.setTab("all")
  56. tabs().open("context")
  57. tabs().setActive("context")
  58. }
  59. const circle = () => (
  60. <div class="flex items-center justify-center">
  61. <ProgressCircle size={16} strokeWidth={2} percentage={context()?.percentage ?? 0} />
  62. </div>
  63. )
  64. const tooltipValue = () => (
  65. <div>
  66. <Show when={context()}>
  67. {(ctx) => (
  68. <>
  69. <div class="flex items-center gap-2">
  70. <span class="text-text-invert-strong">{ctx().tokens}</span>
  71. <span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
  72. </div>
  73. <div class="flex items-center gap-2">
  74. <span class="text-text-invert-strong">{ctx().percentage ?? 0}%</span>
  75. <span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
  76. </div>
  77. </>
  78. )}
  79. </Show>
  80. <div class="flex items-center gap-2">
  81. <span class="text-text-invert-strong">{cost()}</span>
  82. <span class="text-text-invert-base">{language.t("context.usage.cost")}</span>
  83. </div>
  84. </div>
  85. )
  86. return (
  87. <Show when={params.id}>
  88. <Tooltip value={tooltipValue()} placement="top">
  89. <Switch>
  90. <Match when={variant() === "indicator"}>{circle()}</Match>
  91. <Match when={true}>
  92. <Button
  93. type="button"
  94. variant="ghost"
  95. class="size-6"
  96. onClick={openContext}
  97. aria-label={language.t("context.usage.view")}
  98. >
  99. {circle()}
  100. </Button>
  101. </Match>
  102. </Switch>
  103. </Tooltip>
  104. </Show>
  105. )
  106. }