From 9109bcf136653dc53de0500f95af085e2e5f034b Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sat, 24 Oct 2026 03:43:38 -0500 Subject: [PATCH] feat(tui): show Go allowance when a session costs nothing (#58565) --- .../tui/src/feature-plugins/prompt/footer.tsx | 3 ++ .../src/feature-plugins/sidebar/context.tsx | 6 +++- .../feature-plugins/prompt-footer.test.tsx | 35 +++++++++++++++++++ .../feature-plugins/sidebar-context.test.tsx | 19 +++++++++- 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/tui/src/feature-plugins/prompt/footer.tsx b/packages/tui/src/feature-plugins/prompt/footer.tsx index f8fafdce21e..ea8e85a799f 100644 --- a/packages/tui/src/feature-plugins/prompt/footer.tsx +++ b/packages/tui/src/feature-plugins/prompt/footer.tsx @@ -43,9 +43,12 @@ export function PromptFooter(props: { session.revert?.messageID, ) const cost = props.context.data.session.cost(props.sessionID) + // A subscription such as OpenCode Go covers its requests from an allowance, so they cost nothing. + const allowance = props.context.data.session.allowance(props.sessionID) return [ usage ? formatContextUsage(usage.tokens, usage.percent) : undefined, cost > 0 ? money.format(cost) : undefined, + allowance > 0 ? `${money.format(allowance)} allowance` : undefined, ].filter((item): item is string => Boolean(item)) }) const live = createMemo(() => Boolean(subagents() || shells())) diff --git a/packages/tui/src/feature-plugins/sidebar/context.tsx b/packages/tui/src/feature-plugins/sidebar/context.tsx index 3ea5af7b355..0dc5c811e18 100644 --- a/packages/tui/src/feature-plugins/sidebar/context.tsx +++ b/packages/tui/src/feature-plugins/sidebar/context.tsx @@ -12,13 +12,14 @@ export function SidebarContext(props: { context: Plugin.Context; sessionID: stri const msg = createMemo(() => props.context.data.session.message.list(props.sessionID)) const session = createMemo(() => props.context.data.session.get(props.sessionID)) const cost = createMemo(() => props.context.data.session.cost(props.sessionID)) + const allowance = createMemo(() => props.context.data.session.allowance(props.sessionID)) const state = createMemo(() => contextUsage(msg(), props.context.data.location.model.list(session()?.location), session()?.revert?.messageID), ) return ( - 0}> + 0 || allowance() > 0}> Context @@ -36,6 +37,9 @@ export function SidebarContext(props: { context: Plugin.Context; sessionID: stri 0}> {money.format(cost())} spent + 0}> + {money.format(allowance())} allowance used + ) diff --git a/packages/tui/test/feature-plugins/prompt-footer.test.tsx b/packages/tui/test/feature-plugins/prompt-footer.test.tsx index eecef5b40a3..f9d811e6f0b 100644 --- a/packages/tui/test/feature-plugins/prompt-footer.test.tsx +++ b/packages/tui/test/feature-plugins/prompt-footer.test.tsx @@ -29,6 +29,7 @@ test("prompt footer separates simultaneous subagent, shell, and usage status", a status: (id: string) => (id === "child" ? "running" : "idle"), get: () => ({ id: "session", location: { directory: "/workspace" } }), cost: () => 1, + allowance: () => 0, message: { list: () => [] }, }, shell: { @@ -87,6 +88,7 @@ test("prompt footer can hide details", async () => { status: () => "running", get: () => ({ id: "session", location: { directory: "/workspace" } }), cost: () => 1, + allowance: () => 0, message: { list: () => [ { @@ -142,3 +144,36 @@ test("prompt footer can hide details", async () => { app.renderer.destroy() } }) + +test("prompt footer shows the allowance a subscription covered", async () => { + const color = RGBA.fromInts(200, 200, 200) + const context = { + location: { directory: "/workspace" }, + theme: { text: { base: color, muted: color } }, + keymap: { shortcuts: () => [] }, + data: { + session: { + family: () => ["session"], + status: () => "idle", + get: () => ({ id: "session", location: { directory: "/workspace" } }), + cost: () => 0, + allowance: () => 0.42, + message: { list: () => [] }, + }, + shell: { list: () => [] }, + location: { model: { list: () => [] } }, + }, + } as unknown as Context + const app = await testRender( + () => , + { width: 80, height: 1 }, + ) + + try { + await app.renderOnce() + expect(app.captureCharFrame()).toContain("$0.42 allowance") + expect(app.captureCharFrame()).not.toContain("$0.00") + } finally { + app.renderer.destroy() + } +}) diff --git a/packages/tui/test/feature-plugins/sidebar-context.test.tsx b/packages/tui/test/feature-plugins/sidebar-context.test.tsx index de5ecdffd99..c8cc49314b0 100644 --- a/packages/tui/test/feature-plugins/sidebar-context.test.tsx +++ b/packages/tui/test/feature-plugins/sidebar-context.test.tsx @@ -5,7 +5,7 @@ import { testRender } from "@opentui/solid" import type { Context } from "@opencode/plugin/tui/context" import { SidebarContext } from "../../src/feature-plugins/sidebar/context" -function context(options?: { cost?: number; tokens?: number }) { +function context(options?: { cost?: number; allowance?: number; tokens?: number }) { const color = RGBA.fromInts(200, 200, 200) return { theme: { text: { base: color, muted: color } }, @@ -13,6 +13,7 @@ function context(options?: { cost?: number; tokens?: number }) { session: { get: () => ({ location: { directory: "/workspace" } }), cost: () => options?.cost ?? 0, + allowance: () => options?.allowance ?? 0, message: { list: () => options?.tokens @@ -68,3 +69,19 @@ test("sidebar shows available context usage", async () => { app.renderer.destroy() } }) + +test("sidebar shows the allowance a subscription covered", async () => { + const app = await testRender(() => , { + width: 42, + height: 8, + }) + + try { + await app.renderOnce() + expect(app.captureCharFrame()).toContain("Context") + expect(app.captureCharFrame()).toContain("$0.42 allowance used") + expect(app.captureCharFrame()).not.toContain("spent") + } finally { + app.renderer.destroy() + } +})