2026-01-01 11:23:00 +00:00
|
|
|
import { Match, Show, Switch, createMemo } from "solid-js"
|
2026-02-17 13:16:23 +00:00
|
|
|
import { Tooltip, type TooltipProps } from "@opencode-ai/ui/tooltip"
|
2025-12-22 19:56:36 +00:00
|
|
|
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
2026-06-29 10:11:32 +00:00
|
|
|
import { ProgressCircleV2 } from "@opencode-ai/ui/v2/progress-circle-v2"
|
2026-01-01 11:23:00 +00:00
|
|
|
import { Button } from "@opencode-ai/ui/button"
|
2026-06-29 10:11:32 +00:00
|
|
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
2025-12-22 19:56:36 +00:00
|
|
|
|
2026-03-12 16:32:05 +00:00
|
|
|
import { useFile } from "@/context/file"
|
2026-01-01 11:23:00 +00:00
|
|
|
import { useLayout } from "@/context/layout"
|
|
|
|
|
import { useSync } from "@/context/sync"
|
2026-01-20 16:20:43 +00:00
|
|
|
import { useLanguage } from "@/context/language"
|
2026-03-26 18:41:22 +00:00
|
|
|
import { useProviders } from "@/hooks/use-providers"
|
2026-06-25 04:42:06 +00:00
|
|
|
import { useSDK } from "@/context/sdk"
|
2026-06-28 23:17:08 +00:00
|
|
|
import { getSessionContext, getSessionTokenTotal } from "@/components/session/session-context-metrics"
|
2026-03-12 13:52:51 +00:00
|
|
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
2026-03-12 16:32:05 +00:00
|
|
|
import { createSessionTabs } from "@/pages/session/helpers"
|
2026-01-01 11:23:00 +00:00
|
|
|
|
|
|
|
|
interface SessionContextUsageProps {
|
|
|
|
|
variant?: "button" | "indicator"
|
2026-06-29 10:11:32 +00:00
|
|
|
buttonAppearance?: "default" | "v2"
|
2026-02-17 13:16:23 +00:00
|
|
|
placement?: TooltipProps["placement"]
|
2026-01-01 11:23:00 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-12 15:49:14 +00:00
|
|
|
function openSessionContext(args: {
|
|
|
|
|
view: ReturnType<ReturnType<typeof useLayout>["view"]>
|
2026-02-12 20:48:09 +00:00
|
|
|
layout: ReturnType<typeof useLayout>
|
2026-02-12 15:49:14 +00:00
|
|
|
tabs: ReturnType<ReturnType<typeof useLayout>["tabs"]>
|
|
|
|
|
}) {
|
|
|
|
|
if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open()
|
2026-02-12 20:48:09 +00:00
|
|
|
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
|
2026-04-16 03:27:32 +00:00
|
|
|
void args.tabs.open("context")
|
2026-02-12 15:49:14 +00:00
|
|
|
args.tabs.setActive("context")
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 11:23:00 +00:00
|
|
|
export function SessionContextUsage(props: SessionContextUsageProps) {
|
2025-12-22 19:56:36 +00:00
|
|
|
const sync = useSync()
|
2026-03-12 16:32:05 +00:00
|
|
|
const file = useFile()
|
2026-01-01 11:23:00 +00:00
|
|
|
const layout = useLayout()
|
2026-01-20 16:20:43 +00:00
|
|
|
const language = useLanguage()
|
2026-06-25 04:42:06 +00:00
|
|
|
const sdk = useSDK()
|
|
|
|
|
const providers = useProviders(() => sdk().directory)
|
2026-03-12 13:52:51 +00:00
|
|
|
const { params, tabs, view } = useSessionLayout()
|
2026-01-01 11:23:00 +00:00
|
|
|
|
|
|
|
|
const variant = createMemo(() => props.variant ?? "button")
|
2026-06-29 10:11:32 +00:00
|
|
|
const buttonAppearance = createMemo(() => props.buttonAppearance ?? "default")
|
2026-03-12 16:32:05 +00:00
|
|
|
const tabState = createSessionTabs({
|
|
|
|
|
tabs,
|
|
|
|
|
pathFromTab: file.pathFromTab,
|
|
|
|
|
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
|
|
|
|
})
|
2026-06-14 11:24:10 +00:00
|
|
|
const messages = createMemo(() => (params.id ? (sync().data.message[params.id] ?? []) : []))
|
2026-06-28 23:17:08 +00:00
|
|
|
const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined))
|
2025-12-22 19:56:36 +00:00
|
|
|
|
2026-01-25 12:42:33 +00:00
|
|
|
const usd = createMemo(
|
|
|
|
|
() =>
|
2026-03-05 14:10:32 +00:00
|
|
|
new Intl.NumberFormat(language.intl(), {
|
2026-01-25 12:42:33 +00:00
|
|
|
style: "currency",
|
|
|
|
|
currency: "USD",
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-28 23:17:08 +00:00
|
|
|
const context = createMemo(() => getSessionContext(messages(), [...providers.all().values()]))
|
|
|
|
|
const tokens = createMemo(() => info()?.tokens)
|
2025-12-22 19:56:36 +00:00
|
|
|
const cost = createMemo(() => {
|
2026-06-28 23:17:08 +00:00
|
|
|
return usd().format(info()?.cost ?? 0)
|
2025-12-22 19:56:36 +00:00
|
|
|
})
|
|
|
|
|
|
2026-01-01 11:23:00 +00:00
|
|
|
const openContext = () => {
|
|
|
|
|
if (!params.id) return
|
2026-02-17 13:16:23 +00:00
|
|
|
|
2026-03-12 16:32:05 +00:00
|
|
|
if (tabState.activeTab() === "context") {
|
2026-02-17 13:16:23 +00:00
|
|
|
tabs().close("context")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-02-12 15:49:14 +00:00
|
|
|
openSessionContext({
|
|
|
|
|
view: view(),
|
2026-02-12 20:48:09 +00:00
|
|
|
layout,
|
2026-02-12 15:49:14 +00:00
|
|
|
tabs: tabs(),
|
|
|
|
|
})
|
2026-01-01 11:23:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const circle = () => (
|
2026-02-02 22:02:56 +00:00
|
|
|
<div class="flex items-center justify-center">
|
2026-02-06 15:37:49 +00:00
|
|
|
<ProgressCircle size={16} strokeWidth={2} percentage={context()?.usage ?? 0} />
|
2026-01-01 11:23:00 +00:00
|
|
|
</div>
|
|
|
|
|
)
|
2026-06-29 10:11:32 +00:00
|
|
|
const circleV2 = () => (
|
|
|
|
|
<div class="flex items-center justify-center">
|
|
|
|
|
<ProgressCircleV2 percentage={context()?.usage ?? 0} />
|
|
|
|
|
</div>
|
|
|
|
|
)
|
2026-01-01 11:23:00 +00:00
|
|
|
|
|
|
|
|
const tooltipValue = () => (
|
|
|
|
|
<div>
|
2026-06-28 23:17:08 +00:00
|
|
|
<Show when={tokens()}>
|
|
|
|
|
{(value) => (
|
|
|
|
|
<div class="flex items-center gap-2">
|
2026-06-28 23:18:35 +00:00
|
|
|
<span class="text-text-invert-strong">
|
|
|
|
|
{getSessionTokenTotal(value())?.toLocaleString(language.intl())}
|
|
|
|
|
</span>
|
2026-06-28 23:17:08 +00:00
|
|
|
<span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Show>
|
2026-01-01 11:23:00 +00:00
|
|
|
<Show when={context()}>
|
|
|
|
|
{(ctx) => (
|
2026-06-28 23:17:08 +00:00
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<span class="text-text-invert-strong">{ctx().usage ?? 0}%</span>
|
|
|
|
|
<span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
|
|
|
|
|
</div>
|
2026-01-01 11:23:00 +00:00
|
|
|
)}
|
|
|
|
|
</Show>
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<span class="text-text-invert-strong">{cost()}</span>
|
2026-01-20 16:20:43 +00:00
|
|
|
<span class="text-text-invert-base">{language.t("context.usage.cost")}</span>
|
2026-01-01 11:23:00 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Show when={params.id}>
|
2026-02-17 13:16:23 +00:00
|
|
|
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
|
2026-01-01 11:23:00 +00:00
|
|
|
<Switch>
|
|
|
|
|
<Match when={variant() === "indicator"}>{circle()}</Match>
|
2026-06-29 10:11:32 +00:00
|
|
|
<Match when={buttonAppearance() === "v2"}>
|
|
|
|
|
<IconButtonV2
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost-muted"
|
|
|
|
|
size="large"
|
|
|
|
|
icon={circleV2()}
|
|
|
|
|
onClick={openContext}
|
|
|
|
|
aria-label={language.t("context.usage.view")}
|
|
|
|
|
/>
|
|
|
|
|
</Match>
|
2026-01-01 11:23:00 +00:00
|
|
|
<Match when={true}>
|
2026-01-22 11:36:28 +00:00
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
2026-01-31 13:18:51 +00:00
|
|
|
class="size-6"
|
2026-01-22 11:36:28 +00:00
|
|
|
onClick={openContext}
|
|
|
|
|
aria-label={language.t("context.usage.view")}
|
|
|
|
|
>
|
2026-01-01 11:23:00 +00:00
|
|
|
{circle()}
|
|
|
|
|
</Button>
|
|
|
|
|
</Match>
|
|
|
|
|
</Switch>
|
|
|
|
|
</Tooltip>
|
2025-12-22 19:56:36 +00:00
|
|
|
</Show>
|
|
|
|
|
)
|
|
|
|
|
}
|