/** @jsxImportSource @opentui/solid */
/**
* Context usage widget for the OpenCode V2 TUI sidebar.
*
* Claimed as the first element inside `sidebar.content`, so it renders above
* every section the host appends to the sidebar, regardless of enable order.
*/
import type { Plugin } from "@opencode/plugin/tui";
import { TextAttributes } from "@opentui/core";
import { createMemo, Show } from "solid-js";
import { calculateCacheHitPercent, calculateUsage, safeNumber } from "./context";
import { loadSessionMessages } from "./database";
import { createSessionMessageCache } from "./history";
const BAR_WIDTH = 24;
const sessionMessageCache = createSessionMessageCache(loadSessionMessages, 5_000);
function formatInt(value: number): string {
return new Intl.NumberFormat("en-US").format(Math.max(0, Math.round(safeNumber(value))));
}
function formatMoney(value: number): string {
return `$${safeNumber(value).toFixed(2)}`;
}
function buildBar(percent: number): { filled: string; empty: string } {
const clamped = Math.max(0, Math.min(100, safeNumber(percent)));
const filled = clamped > 0 ? Math.max(1, Math.round((clamped / 100) * BAR_WIDTH)) : 0;
return { filled: "█".repeat(filled), empty: "░".repeat(BAR_WIDTH - filled) };
}
function View(props: { context: Plugin.Context; sessionID: string }) {
const sessionMessages = createMemo(() => props.context.data.session.message.list(props.sessionID));
const historicalMessages = createMemo(() => sessionMessageCache.get(props.sessionID, sessionMessages()));
const session = createMemo(() => props.context.data.session.get(props.sessionID));
const cost = createMemo(() => props.context.data.session.cost(props.sessionID));
const models = createMemo(() => props.context.data.location.model.list(session()?.location));
const usage = createMemo(() => {
const messages = sessionMessages();
return calculateUsage(messages, session()?.revert?.messageID, models(), session()?.model);
});
const cacheHitPercent = createMemo(() => calculateCacheHitPercent(historicalMessages(), session()?.revert?.messageID));
const percent = createMemo(() => {
const state = usage();
return state && state.contextWindow > 0 ? Math.round((state.tokens / state.contextWindow) * 100) : 0;
});
const detailLine = createMemo(() => {
const state = usage();
const limitText = state && state.contextWindow > 0 ? formatInt(state.contextWindow) : "--";
const cacheText = cacheHitPercent() === undefined ? "--" : `${cacheHitPercent()}%`;
return `${formatInt(state?.tokens ?? 0)} / ${limitText} / ${cacheText} / ${formatMoney(cost())}`;
});
const bar = createMemo(() => buildBar(percent()));
const color = createMemo(() => {
const value = percent();
const feedback = props.context.theme.text.feedback;
return value >= 90 ? feedback.error.base : value >= 70 ? feedback.warning.base : feedback.success.base;
});
return (
0}>
Context
{bar().filled}
{bar().empty}
{percent()}%
{detailLine()}
);
}
export default {
id: "npv12.context-sidebar",
setup(context) {
return context.ui.slot({
prepend: "sidebar.content",
render: (props) => ,
});
},
} satisfies Plugin.Definition;