session-context-usage.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  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 { useLayout } from "@/context/layout"
  8. import { useSync } from "@/context/sync"
  9. interface SessionContextUsageProps {
  10. variant?: "button" | "indicator"
  11. }
  12. export function SessionContextUsage(props: SessionContextUsageProps) {
  13. const sync = useSync()
  14. const params = useParams()
  15. const layout = useLayout()
  16. const variant = createMemo(() => props.variant ?? "button")
  17. const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
  18. const tabs = createMemo(() => layout.tabs(sessionKey()))
  19. const view = createMemo(() => layout.view(sessionKey()))
  20. const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
  21. const cost = createMemo(() => {
  22. const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
  23. return new Intl.NumberFormat("en-US", {
  24. style: "currency",
  25. currency: "USD",
  26. }).format(total)
  27. })
  28. const context = createMemo(() => {
  29. const last = messages().findLast((x) => {
  30. if (x.role !== "assistant") return false
  31. const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write
  32. return total > 0
  33. }) as AssistantMessage
  34. if (!last) return
  35. const total =
  36. last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
  37. const model = sync.data.provider.all.find((x) => x.id === last.providerID)?.models[last.modelID]
  38. return {
  39. tokens: total.toLocaleString(),
  40. percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null,
  41. }
  42. })
  43. const openContext = () => {
  44. if (!params.id) return
  45. view().reviewPanel.open()
  46. tabs().open("context")
  47. tabs().setActive("context")
  48. }
  49. const circle = () => (
  50. <div class="p-1">
  51. <ProgressCircle size={16} strokeWidth={2} percentage={context()?.percentage ?? 0} />
  52. </div>
  53. )
  54. const tooltipValue = () => (
  55. <div>
  56. <Show when={context()}>
  57. {(ctx) => (
  58. <>
  59. <div class="flex items-center gap-2">
  60. <span class="text-text-invert-strong">{ctx().tokens}</span>
  61. <span class="text-text-invert-base">Tokens</span>
  62. </div>
  63. <div class="flex items-center gap-2">
  64. <span class="text-text-invert-strong">{ctx().percentage ?? 0}%</span>
  65. <span class="text-text-invert-base">Usage</span>
  66. </div>
  67. </>
  68. )}
  69. </Show>
  70. <div class="flex items-center gap-2">
  71. <span class="text-text-invert-strong">{cost()}</span>
  72. <span class="text-text-invert-base">Cost</span>
  73. </div>
  74. <Show when={variant() === "button"}>
  75. <div class="text-11-regular text-text-invert-base mt-1">Click to view context</div>
  76. </Show>
  77. </div>
  78. )
  79. return (
  80. <Show when={params.id}>
  81. <Tooltip value={tooltipValue()} placement="top">
  82. <Switch>
  83. <Match when={variant() === "indicator"}>{circle()}</Match>
  84. <Match when={true}>
  85. <Button type="button" variant="ghost" class="size-6" onClick={openContext}>
  86. {circle()}
  87. </Button>
  88. </Match>
  89. </Switch>
  90. </Tooltip>
  91. </Show>
  92. )
  93. }