|
@@ -5,7 +5,7 @@ import path from "path"
|
|
|
import { Filesystem } from "@/util/filesystem"
|
|
import { Filesystem } from "@/util/filesystem"
|
|
|
import { useLocal } from "@tui/context/local"
|
|
import { useLocal } from "@tui/context/local"
|
|
|
import { useTheme } from "@tui/context/theme"
|
|
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 { useSDK } from "@tui/context/sdk"
|
|
|
import { useRoute } from "@tui/context/route"
|
|
import { useRoute } from "@tui/context/route"
|
|
|
import { useSync } from "@tui/context/sync"
|
|
import { useSync } from "@tui/context/sync"
|
|
@@ -22,7 +22,7 @@ import { useKeyboard, useRenderer } from "@opentui/solid"
|
|
|
import { Editor } from "@tui/util/editor"
|
|
import { Editor } from "@tui/util/editor"
|
|
|
import { useExit } from "../../context/exit"
|
|
import { useExit } from "../../context/exit"
|
|
|
import { Clipboard } from "../../util/clipboard"
|
|
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 { TuiEvent } from "../../event"
|
|
|
import { iife } from "@/util/iife"
|
|
import { iife } from "@/util/iife"
|
|
|
import { Locale } from "@/util/locale"
|
|
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 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 SHELL_PLACEHOLDERS = ["ls -la", "git status", "pwd"]
|
|
|
|
|
+const money = new Intl.NumberFormat("en-US", {
|
|
|
|
|
+ style: "currency",
|
|
|
|
|
+ currency: "USD",
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
export function Prompt(props: PromptProps) {
|
|
export function Prompt(props: PromptProps) {
|
|
|
let input: TextareaRenderable
|
|
let input: TextareaRenderable
|
|
@@ -122,6 +126,25 @@ export function Prompt(props: PromptProps) {
|
|
|
return messages.findLast((m) => m.role === "user")
|
|
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<{
|
|
const [store, setStore] = createStore<{
|
|
|
prompt: PromptInfo
|
|
prompt: PromptInfo
|
|
|
mode: "normal" | "shell"
|
|
mode: "normal" | "shell"
|
|
@@ -833,8 +856,7 @@ export function Prompt(props: PromptProps) {
|
|
|
border={["left"]}
|
|
border={["left"]}
|
|
|
borderColor={highlight()}
|
|
borderColor={highlight()}
|
|
|
customBorderChars={{
|
|
customBorderChars={{
|
|
|
- ...EmptyBorder,
|
|
|
|
|
- vertical: "┃",
|
|
|
|
|
|
|
+ ...SplitBorder.customBorderChars,
|
|
|
bottomLeft: "╹",
|
|
bottomLeft: "╹",
|
|
|
}}
|
|
}}
|
|
|
>
|
|
>
|
|
@@ -1158,14 +1180,25 @@ export function Prompt(props: PromptProps) {
|
|
|
<box gap={2} flexDirection="row">
|
|
<box gap={2} flexDirection="row">
|
|
|
<Switch>
|
|
<Switch>
|
|
|
<Match when={store.mode === "normal"}>
|
|
<Match when={store.mode === "normal"}>
|
|
|
- <Show when={local.model.variant.list().length > 0}>
|
|
|
|
|
- <text fg={theme.text}>
|
|
|
|
|
- {keybind.print("variant_cycle")} <span style={{ fg: theme.textMuted }}>variants</span>
|
|
|
|
|
- </text>
|
|
|
|
|
- </Show>
|
|
|
|
|
- <text fg={theme.text}>
|
|
|
|
|
- {keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>agents</span>
|
|
|
|
|
- </text>
|
|
|
|
|
|
|
+ <Switch>
|
|
|
|
|
+ <Match when={usage()}>
|
|
|
|
|
+ {(item) => (
|
|
|
|
|
+ <text fg={theme.textMuted} wrapMode="none">
|
|
|
|
|
+ {[item().context, item().cost].filter(Boolean).join(" · ")}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ <Match when={true}>
|
|
|
|
|
+ <Show when={local.model.variant.list().length > 0}>
|
|
|
|
|
+ <text fg={theme.text}>
|
|
|
|
|
+ {keybind.print("variant_cycle")} <span style={{ fg: theme.textMuted }}>variants</span>
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </Show>
|
|
|
|
|
+ <text fg={theme.text}>
|
|
|
|
|
+ {keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>agents</span>
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ </Switch>
|
|
|
<text fg={theme.text}>
|
|
<text fg={theme.text}>
|
|
|
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
|
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
|
|
</text>
|
|
</text>
|