mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 21:43:31 +00:00
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
|
|
@ -52,7 +52,7 @@ import { readLocalAttachment } from "./local-attachment"
|
|||
import { useData } from "../../context/data"
|
||||
import { useLocation } from "../../context/location"
|
||||
import { Keymap, type KeymapCommand } from "../../context/keymap"
|
||||
import { contextUsage } from "../../util/session"
|
||||
import { contextUsage, formatContextUsage } from "../../util/session"
|
||||
import { abbreviateHome } from "../../runtime"
|
||||
|
||||
registerOpencodeSpinner()
|
||||
|
|
@ -312,11 +312,7 @@ export function Prompt(props: PromptProps) {
|
|||
session.revert?.messageID,
|
||||
)
|
||||
return {
|
||||
context: context
|
||||
? context.percent === undefined
|
||||
? Locale.number(context.tokens)
|
||||
: `${Locale.number(context.tokens)} (${context.percent}%)`
|
||||
: undefined,
|
||||
context: context ? formatContextUsage(context.tokens, context.percent) : undefined,
|
||||
cost: formattedCost,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { SplitBorder } from "../../ui/border"
|
|||
import { Locale } from "../../util/locale"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { Keymap } from "../../context/keymap"
|
||||
import { contextUsage } from "../../util/session"
|
||||
import { contextUsage, formatContextUsage } from "../../util/session"
|
||||
|
||||
const money = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
|
|
@ -37,11 +37,7 @@ export function SubagentFooter() {
|
|||
)
|
||||
|
||||
return {
|
||||
context: context
|
||||
? context.percent === undefined
|
||||
? Locale.number(context.tokens)
|
||||
: `${Locale.number(context.tokens)} (${context.percent}%)`
|
||||
: undefined,
|
||||
context: context ? formatContextUsage(context.tokens, context.percent) : undefined,
|
||||
cost: formattedCost,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ModelInfo, SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client"
|
||||
import { Locale } from "./locale"
|
||||
|
||||
export function isDefaultTitle(title: string) {
|
||||
return /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(title)
|
||||
|
|
@ -33,3 +34,8 @@ export function contextUsage(
|
|||
percent: model?.limit.context ? Math.round((tokens / model.limit.context) * 100) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatContextUsage(tokens: number, percent?: number) {
|
||||
const value = Locale.number(tokens)
|
||||
return percent === undefined ? value : `${value} (${percent}%)`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,9 @@ describe("run catalog shared", () => {
|
|||
cost: {
|
||||
input: 0,
|
||||
},
|
||||
limit: {
|
||||
context: 128_000,
|
||||
},
|
||||
status: "active",
|
||||
variants: {
|
||||
high: {},
|
||||
|
|
|
|||
|
|
@ -259,15 +259,9 @@ function footerComposerFrame(root: BoxRenderable | RootRenderable) {
|
|||
|
||||
function footerStatusline(root: BoxRenderable | RootRenderable) {
|
||||
const status = (RUN_THEME_FALLBACK.footer.status as RGBA).toInts()
|
||||
const accent = (RUN_THEME_FALLBACK.footer.statusAccent as RGBA).toInts()
|
||||
const boxes = root.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable)
|
||||
for (const box of boxes) {
|
||||
const first = box.getChildren().find((item): item is BoxRenderable => item instanceof BoxRenderable)
|
||||
if (
|
||||
box.backgroundColor?.toInts().every((value, index) => value === status[index]) &&
|
||||
first?.backgroundColor?.toInts().every((value, index) => value === accent[index])
|
||||
)
|
||||
return box
|
||||
if (box.backgroundColor?.toInts().every((value, index) => value === status[index])) return box
|
||||
boxes.push(...box.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable))
|
||||
}
|
||||
throw new Error("Footer statusline not found")
|
||||
|
|
@ -1076,30 +1070,25 @@ test("direct footer shows authoritative pending work while running", async () =>
|
|||
const frame = app.captureCharFrame()
|
||||
const transparent = RGBA.fromValues(0, 0, 0, 0).toInts()
|
||||
const tinted = (RUN_THEME_FALLBACK.footer.status as RGBA).toInts()
|
||||
const accent = (RUN_THEME_FALLBACK.footer.statusAccent as RGBA).toInts()
|
||||
const statusline = footerStatusline(app.renderer.root)
|
||||
const statusItems = statusline.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable)
|
||||
const mode = statusItems[0]
|
||||
const main = statusItems[1]
|
||||
const main = statusItems[0]
|
||||
const spinner = main.getChildren()[0]
|
||||
const model = statusItems[2]
|
||||
const queued = statusItems[3]
|
||||
const background = statusItems[1]
|
||||
const queued = statusItems[2]
|
||||
const hint = statusItems.at(-1)!
|
||||
|
||||
expect(spinner).toBeDefined()
|
||||
expect(frame).toContain("a-model-name-long-enough-to-force-responsive-truncation")
|
||||
expect(frame).toContain("1 pending")
|
||||
expect(frame).toContain("ctrl+b background")
|
||||
expect(frame).toContain("ctrl+x q 1 pending")
|
||||
expect(frame).toContain("↓ subagents")
|
||||
expect(frame).toContain("ctrl+p cmd")
|
||||
expect(frame).toContain("a-model-name-long-enough-to-force-responsive-truncation")
|
||||
expect(frame).toContain("subagents · ctrl+p cmd")
|
||||
expect(frame).not.toContain("1 agent")
|
||||
expect(statusline.backgroundColor.toInts()).toEqual(tinted)
|
||||
expect(mode.backgroundColor.toInts()).toEqual(accent)
|
||||
expect(main.backgroundColor.toInts()).toEqual(transparent)
|
||||
expect(model.backgroundColor.toInts()).toEqual(transparent)
|
||||
expect(background.backgroundColor.toInts()).toEqual(transparent)
|
||||
expect(queued.backgroundColor.toInts()).toEqual(transparent)
|
||||
expect(hint.backgroundColor.toInts()).toEqual(transparent)
|
||||
} finally {
|
||||
|
|
@ -1127,9 +1116,7 @@ test("direct footer always offers backgrounding for a foreground subagent", asyn
|
|||
await app.renderOnce()
|
||||
const frame = app.captureCharFrame()
|
||||
|
||||
expect(frame).toContain("GPT-5")
|
||||
expect(frame).toContain("xhigh · ctrl+b background · ↓ subagents · ctrl+p cmd")
|
||||
expect(frame).toContain("ctrl+b background")
|
||||
expect(frame).toContain("ctrl+b background · ↓ subagents · ctrl+p cmd")
|
||||
expect(frame).not.toContain("queued")
|
||||
} finally {
|
||||
app.cleanup()
|
||||
|
|
@ -1154,8 +1141,7 @@ test("direct footer hides the subagent hint when only completed subagents remain
|
|||
await app.renderOnce()
|
||||
const frame = app.captureCharFrame()
|
||||
|
||||
expect(frame).toContain("GPT-5")
|
||||
expect(frame).toContain("xhigh · ctrl+p cmd")
|
||||
expect(frame).toContain("ctrl+p cmd")
|
||||
expect(frame).not.toContain("↓ subagents")
|
||||
} finally {
|
||||
app.cleanup()
|
||||
|
|
@ -1194,7 +1180,7 @@ test("direct footer shows full usage metadata when room is available", async ()
|
|||
}
|
||||
})
|
||||
|
||||
test("direct footer mode label keeps left padding without a status pill", async () => {
|
||||
test("direct footer does not label normal mode as build", async () => {
|
||||
const app = await renderFooter()
|
||||
|
||||
try {
|
||||
|
|
@ -1202,10 +1188,10 @@ test("direct footer mode label keeps left padding without a status pill", async
|
|||
const statusline = app
|
||||
.captureCharFrame()
|
||||
.split("\n")
|
||||
.find((line) => line.includes("BUILD") && line.includes("cmd"))
|
||||
.find((line) => line.includes("cmd"))
|
||||
|
||||
expect(statusline).toBeDefined()
|
||||
expect(statusline?.startsWith(" BUILD ")).toBe(true)
|
||||
expect(statusline).not.toContain("BUILD")
|
||||
} finally {
|
||||
app.cleanup()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ describe("run footer width", () => {
|
|||
expect(narrow.statusline.showCommandHint).toBe(true)
|
||||
expect(narrow.statusline.showContextHints).toBe(false)
|
||||
expect(narrow.statusline.contextHintLimit).toBe(0)
|
||||
expect(narrow.statusline.showModel).toBe(false)
|
||||
|
||||
const command = footerWidthPolicy(65)
|
||||
expect(command.statusline.showCommandHint).toBe(false)
|
||||
|
|
@ -22,14 +21,11 @@ describe("run footer width", () => {
|
|||
expect(compact.statusline.showActivityMeta).toBe(true)
|
||||
expect(compact.statusline.showContextHints).toBe(true)
|
||||
expect(compact.statusline.contextHintLimit).toBe(1)
|
||||
expect(compact.statusline.showModel).toBe(false)
|
||||
|
||||
const model = footerWidthPolicy(120)
|
||||
expect(model.statusline.contextHintLimit).toBe(2)
|
||||
expect(model.statusline.showModel).toBe(true)
|
||||
const context = footerWidthPolicy(120)
|
||||
expect(context.statusline.contextHintLimit).toBe(2)
|
||||
|
||||
const spacious = footerWidthPolicy(150)
|
||||
expect(spacious.statusline.contextHintLimit).toBeUndefined()
|
||||
expect(spacious.statusline.showModel).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -128,6 +128,9 @@ describe("run runtime boot", () => {
|
|||
cost: {
|
||||
input: 0,
|
||||
},
|
||||
limit: {
|
||||
context: 128_000,
|
||||
},
|
||||
status: "active",
|
||||
variants: {
|
||||
high: {},
|
||||
|
|
|
|||
|
|
@ -200,6 +200,50 @@ afterEach(() => {
|
|||
})
|
||||
|
||||
describe("V2 mini transport", () => {
|
||||
test("formats footer usage with compact tokens and context percentage", async () => {
|
||||
const events = feed()
|
||||
events.push(connected())
|
||||
const ui = footer()
|
||||
const transport = await createSessionTransport({
|
||||
sdk: sdk({ streams: [events] }),
|
||||
sessionID: "ses_1",
|
||||
thinking: false,
|
||||
footer: ui.api,
|
||||
contextLimit: (model) =>
|
||||
model.providerID === "test" && model.modelID === "model" ? 160_000 : undefined,
|
||||
})
|
||||
|
||||
events.push({
|
||||
id: "evt_step_started",
|
||||
created: 1,
|
||||
type: "session.step.started",
|
||||
durable: durable("ses_1", 1),
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_assistant",
|
||||
agent: "build",
|
||||
model: { providerID: "test", id: "model" },
|
||||
},
|
||||
})
|
||||
events.push({
|
||||
id: "evt_step_ended",
|
||||
created: 2,
|
||||
type: "session.step.ended",
|
||||
durable: durable("ses_1", 2),
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_assistant",
|
||||
finish: "stop",
|
||||
cost: 0,
|
||||
tokens: { input: 7_000, output: 500, reasoning: 8, cache: { read: 0, write: 0 } },
|
||||
},
|
||||
})
|
||||
|
||||
while (!ui.events.some((event) => event.type === "stream.patch" && event.patch.usage)) await Bun.sleep(0)
|
||||
expect(ui.events).toContainEqual({ type: "stream.patch", patch: { usage: "7.5K (5%)" } })
|
||||
await transport.close()
|
||||
})
|
||||
|
||||
test("recursively hydrates blockers for direct and transitive descendants", async () => {
|
||||
const events = feed()
|
||||
events.push(connected())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue