From 8446719b13a1c9566bccd206cb522f7e524b1867 Mon Sep 17 00:00:00 2001 From: Dax Date: Sat, 28 Mar 2026 01:05:18 -0400 Subject: [PATCH] refactor(session): move context into prompt footer (#19486) --- packages/opencode/src/cli/cmd/tui/app.tsx | 1 - .../cli/cmd/tui/component/prompt/index.tsx | 57 ++++-- .../src/cli/cmd/tui/routes/session/header.tsx | 172 ------------------ .../src/cli/cmd/tui/routes/session/index.tsx | 17 +- 4 files changed, 47 insertions(+), 200 deletions(-) delete mode 100644 packages/opencode/src/cli/cmd/tui/routes/session/header.tsx diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 2557d965ad..5a2e1b1558 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -584,7 +584,6 @@ function App(props: { onSnapshot?: () => Promise }) { value: "variant.cycle", keybind: "variant_cycle", category: "Agent", - hidden: true, onSelect: () => { local.model.variant.cycle() }, diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 257248c3c1..f6ac9660d3 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -5,7 +5,7 @@ import path from "path" import { Filesystem } from "@/util/filesystem" import { useLocal } from "@tui/context/local" import { useTheme } from "@tui/context/theme" -import { EmptyBorder } from "@tui/component/border" +import { EmptyBorder, SplitBorder } from "@tui/component/border" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" import { useSync } from "@tui/context/sync" @@ -22,7 +22,7 @@ import { useKeyboard, useRenderer } from "@opentui/solid" import { Editor } from "@tui/util/editor" import { useExit } from "../../context/exit" import { Clipboard } from "../../util/clipboard" -import type { FilePart } from "@opencode-ai/sdk/v2" +import type { AssistantMessage, FilePart } from "@opencode-ai/sdk/v2" import { TuiEvent } from "../../event" import { iife } from "@/util/iife" import { Locale } from "@/util/locale" @@ -59,6 +59,10 @@ export type PromptRef = { const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"] const SHELL_PLACEHOLDERS = ["ls -la", "git status", "pwd"] +const money = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", +}) export function Prompt(props: PromptProps) { let input: TextareaRenderable @@ -122,6 +126,25 @@ export function Prompt(props: PromptProps) { return messages.findLast((m) => m.role === "user") }) + const usage = createMemo(() => { + if (!props.sessionID) return + const msg = sync.data.message[props.sessionID] ?? [] + const last = msg.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0) + if (!last) return + + const tokens = + last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write + if (tokens <= 0) return + + const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID] + const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined + const cost = msg.reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0) + return { + context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens), + cost: cost > 0 ? money.format(cost) : undefined, + } + }) + const [store, setStore] = createStore<{ prompt: PromptInfo mode: "normal" | "shell" @@ -833,8 +856,7 @@ export function Prompt(props: PromptProps) { border={["left"]} borderColor={highlight()} customBorderChars={{ - ...EmptyBorder, - vertical: "┃", + ...SplitBorder.customBorderChars, bottomLeft: "╹", }} > @@ -1158,14 +1180,25 @@ export function Prompt(props: PromptProps) { - 0}> - - {keybind.print("variant_cycle")} variants - - - - {keybind.print("agent_cycle")} agents - + + + {(item) => ( + + {[item().context, item().cost].filter(Boolean).join(" · ")} + + )} + + + 0}> + + {keybind.print("variant_cycle")} variants + + + + {keybind.print("agent_cycle")} agents + + + {keybind.print("command_list")} commands diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx deleted file mode 100644 index f64dbe533a..0000000000 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { type Accessor, createMemo, createSignal, Match, Show, Switch } from "solid-js" -import { useRouteData } from "@tui/context/route" -import { useSync } from "@tui/context/sync" -import { pipe, sumBy } from "remeda" -import { useTheme } from "@tui/context/theme" -import { SplitBorder } from "@tui/component/border" -import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2" -import { useCommandDialog } from "@tui/component/dialog-command" -import { useKeybind } from "../../context/keybind" -import { Flag } from "@/flag/flag" -import { useTerminalDimensions } from "@opentui/solid" - -const Title = (props: { session: Accessor }) => { - const { theme } = useTheme() - return ( - - # {props.session().title} - - ) -} - -const ContextInfo = (props: { context: Accessor; cost: Accessor }) => { - const { theme } = useTheme() - return ( - - - {props.context()} ({props.cost()}) - - - ) -} - -const WorkspaceInfo = (props: { workspace: Accessor }) => { - const { theme } = useTheme() - return ( - - - {props.workspace()} - - - ) -} - -export function Header() { - const route = useRouteData("session") - const sync = useSync() - const session = createMemo(() => sync.session.get(route.sessionID)!) - const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) - - const cost = createMemo(() => { - const total = pipe( - messages(), - sumBy((x) => (x.role === "assistant" ? x.cost : 0)), - ) - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(total) - }) - - const context = createMemo(() => { - const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage - if (!last) return - const total = - last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write - const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID] - let result = total.toLocaleString() - if (model?.limit.context) { - result += " " + Math.round((total / model.limit.context) * 100) + "%" - } - return result - }) - - const workspace = createMemo(() => { - const id = session()?.workspaceID - if (!id) return "Workspace local" - const info = sync.workspace.get(id) - if (!info) return `Workspace ${id}` - return `Workspace ${id} (${info.type})` - }) - - const { theme } = useTheme() - const keybind = useKeybind() - const command = useCommandDialog() - const [hover, setHover] = createSignal<"parent" | "prev" | "next" | null>(null) - const dimensions = useTerminalDimensions() - const narrow = createMemo(() => dimensions().width < 80) - - return ( - - - - - - - {Flag.OPENCODE_EXPERIMENTAL_WORKSPACES ? ( - - - Subagent session - - - - ) : ( - - Subagent session - - )} - - - - - setHover("parent")} - onMouseOut={() => setHover(null)} - onMouseUp={() => command.trigger("session.parent")} - backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel} - > - - Parent {keybind.print("session_parent")} - - - setHover("prev")} - onMouseOut={() => setHover(null)} - onMouseUp={() => command.trigger("session.child.previous")} - backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel} - > - - Prev {keybind.print("session_child_cycle_reverse")} - - - setHover("next")} - onMouseOut={() => setHover(null)} - onMouseUp={() => command.trigger("session.child.next")} - backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel} - > - - Next {keybind.print("session_child_cycle")} - - - - - - - - {Flag.OPENCODE_EXPERIMENTAL_WORKSPACES ? ( - - - <WorkspaceInfo workspace={workspace} /> - </box> - ) : ( - <Title session={session} /> - )} - <ContextInfo context={context} cost={cost} /> - </box> - </Match> - </Switch> - </box> - </box> - ) -} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 080065fd78..1baa2e9973 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -51,7 +51,6 @@ import { useSDK } from "@tui/context/sdk" import { useCommandDialog } from "@tui/component/dialog-command" import type { DialogContext } from "@tui/ui/dialog" import { useKeybind } from "@tui/context/keybind" -import { Header } from "./header" import { parsePatch } from "diff" import { useDialog } from "../../ui/dialog" import { TodoItem } from "../../component/todo-item" @@ -154,7 +153,6 @@ export function Session() { const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true) const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true) const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", true) - const [showHeader, setShowHeader] = kv.signal("header_visible", true) const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word") const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true) const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false) @@ -635,15 +633,6 @@ export function Session() { dialog.clear() }, }, - { - title: showHeader() ? "Hide header" : "Show header", - value: "session.toggle.header", - category: "Session", - onSelect: (dialog) => { - setShowHeader((prev) => !prev) - dialog.clear() - }, - }, { title: showGenericToolOutput() ? "Hide generic tool output" : "Show generic tool output", value: "session.toggle.generic_tool_output", @@ -1045,11 +1034,8 @@ export function Session() { }} > <box flexDirection="row"> - <box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}> + <box flexGrow={1} paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1}> <Show when={session()}> - <Show when={showHeader() && (!sidebarVisible() || !wide())}> - <Header /> - </Show> <scrollbox ref={(r) => (scroll = r)} viewportOptions={{ @@ -1068,6 +1054,7 @@ export function Session() { flexGrow={1} scrollAcceleration={scrollAcceleration()} > + <box height={1} /> <For each={messages()}> {(message, index) => ( <Switch>