fix(types): resolve type errors in usage, session, and TUI components

- Fix SQLite binding types in usage storage with proper any cast
- Fix assistantMessage.time.start → .created in processor
- Handle legacy string model format in prompt.ts
- Extend SDK Agent type with fallback property for TUI
- Extend SDK KeybindsConfig with model_fallback_toggle
- Fix text/span styling for bold in prompt component

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Artur Do Lago
2026-01-16 17:47:39 +01:00
parent f137dbf536
commit bfeca4a991
8 changed files with 32 additions and 17 deletions
@@ -10,8 +10,7 @@ import {
type ParentProps,
} from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { useKeybind } from "@tui/context/keybind"
import type { KeybindsConfig } from "@opencode-ai/sdk/v2"
import { useKeybind, type KeybindsConfig } from "@tui/context/keybind"
type Context = ReturnType<typeof init>
const ctx = createContext<Context>()
@@ -1066,8 +1066,8 @@ export function Prompt(props: PromptProps) {
</text>
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
<Show when={local.model.isFallbackActive()}>
<text fg={theme.warning} style={{ bold: true }}>
[FB]
<text>
<span style={{ fg: theme.warning, bold: true }}>[FB]</span>
</text>
</Show>
<Show when={showVariant()}>
@@ -2,21 +2,26 @@ import { createMemo } from "solid-js"
import { useSync } from "@tui/context/sync"
import { Keybind } from "@/util/keybind"
import { pipe, mapValues } from "remeda"
import type { KeybindsConfig } from "@opencode-ai/sdk/v2"
import type { KeybindsConfig as SDKKeybindsConfig } from "@opencode-ai/sdk/v2"
import type { ParsedKey, Renderable } from "@opentui/core"
import { createStore } from "solid-js/store"
import { useKeyboard, useRenderer } from "@opentui/solid"
import { createSimpleContext } from "./helper"
// Extended keybinds type with new keybinds not yet in SDK
export type KeybindsConfig = SDKKeybindsConfig & {
model_fallback_toggle?: string
}
export const { use: useKeybind, provider: KeybindProvider } = createSimpleContext({
name: "Keybind",
init: () => {
const sync = useSync()
const keybinds = createMemo(() => {
return pipe(
sync.data.config.keybinds ?? {},
(sync.data.config.keybinds ?? {}) as KeybindsConfig,
mapValues((value) => Keybind.parse(value)),
)
) as { [K in keyof KeybindsConfig]?: Keybind.Info[] }
})
const [store, setStore] = createStore({
leader: false,
@@ -12,6 +12,12 @@ import { Provider } from "@/provider/provider"
import { useArgs } from "./args"
import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core"
import type { Agent as SDKAgent } from "@opencode-ai/sdk/v2"
// Extended agent type with fallback model support (internal feature not yet in SDK)
type AgentWithFallback = SDKAgent & {
fallback?: { providerID: string; modelID: string }
}
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
@@ -36,8 +42,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
const agent = iife(() => {
const agents = createMemo(() => {
const list = sync.data?.agent
const agents = createMemo((): AgentWithFallback[] => {
const list = sync.data?.agent as AgentWithFallback[] | undefined
if (!list || !Array.isArray(list)) return []
return list
.filter((x) => x.mode !== "subagent" && !x.hidden)
@@ -83,6 +89,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
permission: [],
options: {},
model: undefined as { providerID: string; modelID: string } | undefined,
fallback: undefined as { providerID: string; modelID: string } | undefined,
}
return {
@@ -138,7 +138,7 @@ export namespace ProviderTransform {
? model.capabilities.interleaved.field
: null
if (interleavedField === "reasoning_content" || interleavedField === "reasoning") {
if (interleavedField === "reasoning_content" || interleavedField === "reasoning_details") {
return msgs.map((msg) => {
if (msg.role === "assistant" && Array.isArray(msg.content)) {
const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
@@ -157,7 +157,7 @@ export namespace ProviderTransform {
providerOptions: {
...msg.providerOptions,
openaiCompatible: {
...(typeof existingOptions === "object" ? existingOptions : {}),
...(existingOptions && typeof existingOptions === "object" ? existingOptions : {}),
[interleavedField]: reasoningText,
},
},
+1 -1
View File
@@ -297,7 +297,7 @@ export namespace SessionProcessor {
inputCost: usage.cost * (usage.tokens.input / (usage.tokens.input + usage.tokens.output + usage.tokens.reasoning || 1)),
outputCost: usage.cost * ((usage.tokens.output + usage.tokens.reasoning) / (usage.tokens.input + usage.tokens.output + usage.tokens.reasoning || 1)),
cacheCost: 0, // Cache cost already included in inputCost for Anthropic
durationMs: Date.now() - (input.assistantMessage.time.start || Date.now()),
durationMs: Date.now() - (input.assistantMessage.time.created || Date.now()),
streaming: true,
toolCalls: Object.keys(toolcalls).length || undefined,
}).catch((e) => log.warn("failed to record usage", { error: String(e) }))
+7 -4
View File
@@ -1573,8 +1573,11 @@ export namespace SessionPrompt {
return await lastModel(input.sessionID)
})()
// Handle both legacy string format ("provider/model") and object format
const resolvedModel = typeof taskModel === "string" ? Provider.parseModel(taskModel) : taskModel
try {
await Provider.getModel(taskModel.providerID, taskModel.modelID)
await Provider.getModel(resolvedModel.providerID, resolvedModel.modelID)
} catch (e) {
if (Provider.ModelNotFoundError.isInstance(e)) {
const { providerID, modelID, suggestions } = e.data
@@ -1608,8 +1611,8 @@ export namespace SessionPrompt {
description: command.description ?? "",
command: input.command,
model: {
providerID: taskModel.providerID,
modelID: taskModel.modelID,
providerID: resolvedModel.providerID,
modelID: resolvedModel.modelID,
},
// LIMITATION: Task tool currently only takes text prompt, not complex parts
// This is intentional for simplicity; subagents get minimal context
@@ -1623,7 +1626,7 @@ export namespace SessionPrompt {
? input.model
? Provider.parseModel(input.model)
: await lastModel(input.sessionID)
: taskModel
: resolvedModel
await Plugin.trigger(
"command.execute.before",
+2 -1
View File
@@ -200,7 +200,8 @@ export function getSummary(query: UsageSummaryQuery = {}): UsageSummary {
const { startTime, endTime } = getPeriodRange(query.period ?? "day", query.from, query.to)
const conditions: string[] = ["timestamp >= $startTime AND timestamp <= $endTime"]
const params: Record<string, unknown> = { $startTime: startTime, $endTime: endTime }
// biome-ignore lint/suspicious/noExplicitAny: SQLite bindings accept dynamic params
const params: Record<string, any> = { $startTime: startTime, $endTime: endTime }
if (query.providerId) {
conditions.push("provider_id = $providerId")