refactor(tui): simplify mini footer status line (#38112)
This commit is contained in:
parent
592ef7433a
commit
dd50d457b0
15 changed files with 111 additions and 102 deletions
|
|
@ -75,6 +75,7 @@ export function runProviders(providers: CurrentProvider[], models: CurrentModel[
|
|||
provider.models[model.id] = {
|
||||
name: model.name,
|
||||
cost: cost === undefined ? undefined : { input: cost },
|
||||
limit: { context: model.limit.context },
|
||||
status: model.status,
|
||||
variants: Object.fromEntries((model.variants ?? []).map((variant) => [variant.id, {}])),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ import type {
|
|||
RunTuiConfig,
|
||||
} from "./types"
|
||||
import type { RunTheme } from "./theme"
|
||||
import { modelInfo } from "./variant.shared"
|
||||
|
||||
registerOpencodeSpinner()
|
||||
|
||||
|
|
@ -158,10 +157,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
return tabs().findIndex((item) => item.sessionID === sessionID) + 1
|
||||
})
|
||||
const foregroundSubagents = createMemo(() => activeTabs().some((item) => !item.background))
|
||||
const model = createMemo(() => {
|
||||
const current = props.currentModel()
|
||||
return current ? modelInfo(props.providers(), current).model : props.state().model
|
||||
})
|
||||
const detail = createMemo(() => {
|
||||
const current = route()
|
||||
return current.type === "subagent" ? subagent().details[current.sessionID] : undefined
|
||||
|
|
@ -190,19 +185,15 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
const theme = createMemo(() => runTheme().footer)
|
||||
const block = createMemo(() => runTheme().block)
|
||||
const spin = createMemo(() => {
|
||||
const options = {
|
||||
color: theme().highlight,
|
||||
style: "blocks" as const,
|
||||
inactiveFactor: 0.6,
|
||||
minAlpha: 0.3,
|
||||
}
|
||||
return {
|
||||
frames: createFrames({
|
||||
color: theme().highlight,
|
||||
style: "blocks",
|
||||
inactiveFactor: 0.6,
|
||||
minAlpha: 0.3,
|
||||
}),
|
||||
color: createColors({
|
||||
color: theme().highlight,
|
||||
style: "blocks",
|
||||
inactiveFactor: 0.6,
|
||||
minAlpha: 0.3,
|
||||
}),
|
||||
frames: createFrames(options),
|
||||
color: createColors(options),
|
||||
}
|
||||
})
|
||||
const permission = createMemo<Extract<FooterView, { type: "permission" }> | undefined>(() => {
|
||||
|
|
@ -338,7 +329,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
return "EXIT"
|
||||
}
|
||||
|
||||
return shell() ? "SHELL" : "BUILD"
|
||||
return shell() ? "SHELL" : undefined
|
||||
})
|
||||
const modeColor = createMemo(() => {
|
||||
if (exiting()) {
|
||||
|
|
@ -373,17 +364,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
|
||||
return usage()
|
||||
})
|
||||
const modelStatus = createMemo(() => {
|
||||
const current = props.currentModel()
|
||||
if (!prompt() || shell() || !current) {
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
model: model(),
|
||||
variant: props.currentVariant(),
|
||||
}
|
||||
})
|
||||
const statusColor = createMemo(() => {
|
||||
if (exiting()) {
|
||||
return theme().error
|
||||
|
|
@ -401,7 +381,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
})
|
||||
const statuslineBackground = createMemo(() => theme().status)
|
||||
const hasActivityMeta = createMemo(() => activityMeta().length > 0)
|
||||
const hasModelStatus = createMemo(() => responsive().statusline.showModel && Boolean(modelStatus()))
|
||||
const contextHints = createMemo(() => {
|
||||
if (!prompt() || shell() || !responsive().statusline.showContextHints) {
|
||||
return []
|
||||
|
|
@ -796,11 +775,15 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
flexShrink={0}
|
||||
backgroundColor={statuslineBackground()}
|
||||
>
|
||||
<box paddingLeft={1} paddingRight={1} backgroundColor={theme().statusAccent} flexShrink={0}>
|
||||
<text wrapMode="none" truncate>
|
||||
<span style={{ fg: modeColor(), bold: true }}>{modeLabel()}</span>
|
||||
</text>
|
||||
</box>
|
||||
<Show when={modeLabel()}>
|
||||
{(label) => (
|
||||
<box paddingLeft={1} paddingRight={1} backgroundColor={theme().statusAccent} flexShrink={0}>
|
||||
<text wrapMode="none" truncate>
|
||||
<span style={{ fg: modeColor(), bold: true }}>{label()}</span>
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<box
|
||||
flexDirection="row"
|
||||
|
|
@ -836,28 +819,11 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
</box>
|
||||
</Show>
|
||||
|
||||
<Show when={responsive().statusline.showModel && modelStatus()}>
|
||||
{(info) => (
|
||||
<box paddingRight={1} backgroundColor="transparent" flexShrink={0}>
|
||||
<text fg={theme().text} wrapMode="none">
|
||||
{info().model}
|
||||
<Show when={info().variant}>
|
||||
{(variant) => (
|
||||
<>
|
||||
<span style={{ fg: theme().warning, bold: true }}> {variant()}</span>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<For each={contextHints()}>
|
||||
{(hint, index) => (
|
||||
<box paddingRight={1} backgroundColor="transparent" flexShrink={0} maxWidth={24}>
|
||||
<text fg={theme().text} wrapMode="none" truncate>
|
||||
<Show when={index() > 0 || ((hasActivityMeta() || hasModelStatus()) && index() === 0)}>
|
||||
<Show when={index() > 0 || (hasActivityMeta() && index() === 0)}>
|
||||
{sectionSeparator()}
|
||||
</Show>
|
||||
<span style={{ fg: theme().text }}>{hint.key}</span>{" "}
|
||||
|
|
@ -871,7 +837,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
{(hint) => (
|
||||
<box paddingRight={1} backgroundColor="transparent" flexShrink={0} maxWidth={18}>
|
||||
<text fg={theme().text} wrapMode="none" truncate>
|
||||
<Show when={hasActivityMeta() || hasModelStatus() || hasContextHints()}>
|
||||
<Show when={hasActivityMeta() || hasContextHints()}>
|
||||
{sectionSeparator()}
|
||||
</Show>
|
||||
<span style={{ fg: theme().text }}>{hint().key}</span>{" "}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@
|
|||
const FOOTER_WIDTH_BREAKPOINTS = {
|
||||
compact: 80,
|
||||
commandHint: 66,
|
||||
model: 120,
|
||||
context: 120,
|
||||
spacious: 150,
|
||||
} as const
|
||||
|
||||
export function footerWidthPolicy(width: number) {
|
||||
const compact = width >= FOOTER_WIDTH_BREAKPOINTS.compact
|
||||
const model = width >= FOOTER_WIDTH_BREAKPOINTS.model
|
||||
const context = width >= FOOTER_WIDTH_BREAKPOINTS.context
|
||||
const spacious = width >= FOOTER_WIDTH_BREAKPOINTS.spacious
|
||||
|
||||
return {
|
||||
|
|
@ -20,8 +20,7 @@ export function footerWidthPolicy(width: number) {
|
|||
showActivityMeta: compact,
|
||||
showCommandHint: width >= FOOTER_WIDTH_BREAKPOINTS.commandHint,
|
||||
showContextHints: compact,
|
||||
contextHintLimit: !compact ? 0 : spacious ? undefined : model ? 2 : 1,
|
||||
showModel: model,
|
||||
contextHintLimit: !compact ? 0 : spacious ? undefined : context ? 2 : 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -729,6 +729,8 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
|||
onCommit: rememberLocal,
|
||||
trace: log,
|
||||
onCatalogRefresh: requestCatalogRefresh,
|
||||
contextLimit: (model) =>
|
||||
state.providers.find((provider) => provider.id === model.providerID)?.models[model.modelID]?.limit?.context,
|
||||
})
|
||||
if (footer.isClosed) {
|
||||
await handle.close()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type {
|
|||
} from "@opencode-ai/client/promise"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import { formatContextUsage } from "../util/session"
|
||||
import { blockerStatus, pickBlockerView } from "./session-data"
|
||||
import { writeSessionOutput } from "./stream"
|
||||
import { createFragmentReconciler, fragmentRef, type FragmentReconciler } from "./stream-v2.fragment"
|
||||
|
|
@ -48,6 +49,7 @@ type StreamInput = {
|
|||
trace?: Trace
|
||||
signal?: AbortSignal
|
||||
onCatalogRefresh?: (signal?: AbortSignal) => unknown | Promise<unknown>
|
||||
contextLimit?: (model: NonNullable<RunInput["model"]>) => number | undefined
|
||||
}
|
||||
|
||||
export type SessionTurnInput = {
|
||||
|
|
@ -141,6 +143,7 @@ type State = {
|
|||
errors: Set<string>
|
||||
pending: Map<string, FooterQueuedPrompt>
|
||||
admitted: Set<string>
|
||||
stepModel: RunInput["model"]
|
||||
}
|
||||
|
||||
const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })
|
||||
|
|
@ -409,6 +412,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
errors: new Set(),
|
||||
pending: new Map(),
|
||||
admitted: new Set(),
|
||||
stepModel: undefined,
|
||||
}
|
||||
let readyResolve!: () => void
|
||||
let readyReject!: (error: unknown) => void
|
||||
|
|
@ -857,6 +861,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
return
|
||||
}
|
||||
if (event.type === "session.step.started") {
|
||||
state.stepModel = { providerID: event.data.model.providerID, modelID: event.data.model.id }
|
||||
write([], { phase: "running", status: "assistant responding" })
|
||||
return
|
||||
}
|
||||
|
|
@ -1106,13 +1111,16 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
event.data.tokens.reasoning +
|
||||
event.data.tokens.cache.read +
|
||||
event.data.tokens.cache.write
|
||||
const usage = total > 0 ? total.toLocaleString() : ""
|
||||
const limit = state.stepModel ? input.contextLimit?.(state.stepModel) : undefined
|
||||
state.stepModel = undefined
|
||||
const usage = total > 0 ? formatContextUsage(total, limit ? Math.round((total / limit) * 100) : undefined) : ""
|
||||
write([], {
|
||||
usage: event.data.cost ? `${usage} · ${money.format(event.data.cost)}` : usage,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (event.type === "session.step.failed") {
|
||||
state.stepModel = undefined
|
||||
const rendered = state.errors.has(event.data.assistantMessageID)
|
||||
state.errors.add(event.data.assistantMessageID)
|
||||
if (state.wait) state.wait.failureRendered = true
|
||||
|
|
|
|||
|
|
@ -374,7 +374,7 @@ function map(
|
|||
const shade = fade(footerTheme.backgroundMenu, footerTheme.background, 0.12, 0.56, 0.72)
|
||||
const surface = fade(footerTheme.backgroundMenu, footerTheme.background, 0.18, 0.76, 0.9)
|
||||
const line = fade(footerTheme.backgroundMenu, footerTheme.background, 0.24, 0.9, 0.98)
|
||||
const statusBase = tint(footerBackground, rgba("#000000"), footerMode === "dark" ? 0.12 : 0.06)
|
||||
const statusBase = tint(footerBackground, rgba("#000000"), footerMode === "dark" ? 0.13 : 0.06)
|
||||
const statusAccentBase =
|
||||
footerMode === "dark" ? tint(footerBackground, rgba("#ffffff"), 0.06) : tint(statusBase, rgba("#000000"), 0.04)
|
||||
const collapsedStatus = footerMode === "dark" && luminance(statusBase) <= 0.04
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ export type RunProviderModel = {
|
|||
cost?: {
|
||||
input: number
|
||||
}
|
||||
limit?: {
|
||||
context: number
|
||||
}
|
||||
status?: string
|
||||
variants?: Record<string, unknown>
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue