import { For, Show, createSignal, createEffect, createMemo } from "solid-js" import type { Message } from "../types/message" import MessageItem from "./message-item" import ToolCall from "./tool-call" import { sseManager } from "../lib/sse-manager" import Kbd from "./kbd" import { preferences } from "../stores/preferences" import { providers, getSessionInfo } from "../stores/sessions" // Calculate session tokens and cost from messagesInfo (matches TUI logic) function calculateSessionInfo(messagesInfo?: Map, instanceId?: string) { if (!messagesInfo || messagesInfo.size === 0) return { tokens: 0, cost: 0, contextWindow: 0, isSubscriptionModel: false } let tokens = 0 let cost = 0 let contextWindow = 0 let isSubscriptionModel = false let modelID = "" let providerID = "" // Go backwards through messages to find the last relevant assistant message (like TUI) const messageArray = Array.from(messagesInfo.values()).reverse() for (const info of messageArray) { if (info.role === "assistant" && info.tokens) { const usage = info.tokens if (usage.output > 0) { if (info.summary) { // If summary message, only count output tokens and stop (like TUI) tokens = usage.output || 0 cost = info.cost || 0 } else { // Regular message - count all token types (like TUI) tokens = (usage.input || 0) + (usage.cache?.read || 0) + (usage.cache?.write || 0) + (usage.output || 0) + (usage.reasoning || 0) cost = info.cost || 0 } // Get model info for context window and subscription check modelID = info.modelID || "" providerID = info.providerID || "" isSubscriptionModel = cost === 0 break } } } // Try to get context window from providers if (instanceId && modelID && providerID) { const instanceProviders = providers().get(instanceId) || [] console.log("[calculateSessionInfo] instanceProviders:", instanceProviders) console.log("[calculateSessionInfo] looking for providerID:", providerID, "modelID:", modelID) const provider = instanceProviders.find((p) => p.id === providerID) console.log("[calculateSessionInfo] found provider:", provider) if (provider) { const model = provider.models.find((m) => m.id === modelID) console.log("[calculateSessionInfo] found model:", model) if (model?.limit?.context) { contextWindow = model.limit.context } // Check if it's a subscription model (cost is 0 for both input and output) if (model?.cost?.input === 0 && model?.cost?.output === 0) { isSubscriptionModel = true } } } return { tokens, cost, contextWindow, isSubscriptionModel } } // Format tokens like TUI (e.g., "110K", "1.2M") function formatTokens(tokens: number): string { if (tokens >= 1000000) { return `${(tokens / 1000000).toFixed(1)}M` } else if (tokens >= 1000) { return `${(tokens / 1000).toFixed(0)}K` } return tokens.toString() } // Format session info like TUI (e.g., "110K/73% ($0.42)" or "110K/73%") function formatSessionInfo(tokens: number, cost: number, contextWindow: number, isSubscriptionModel: boolean): string { const tokensStr = formatTokens(tokens) // Calculate percentage if we have context window if (contextWindow > 0) { const percentage = Math.round((tokens / contextWindow) * 100) if (isSubscriptionModel) { return `${tokensStr}/${percentage}%` } return `${tokensStr}/${percentage}% ($${cost.toFixed(2)})` } // Fallback without context window if (isSubscriptionModel) { return tokensStr } return `${tokensStr} ($${cost.toFixed(2)})` } interface MessageStreamProps { instanceId: string sessionId: string messages: Message[] messagesInfo?: Map revert?: { messageID: string partID?: string snapshot?: string diff?: string } loading?: boolean onRevert?: (messageId: string) => void } interface DisplayItem { type: "message" | "tool" data: any messageInfo?: any } export default function MessageStream(props: MessageStreamProps) { let containerRef: HTMLDivElement | undefined const [autoScroll, setAutoScroll] = createSignal(true) const [showScrollButton, setShowScrollButton] = createSignal(false) const connectionStatus = () => sseManager.getStatus(props.instanceId) const sessionInfo = createMemo(() => { return ( getSessionInfo(props.instanceId, props.sessionId) || { tokens: 0, cost: 0, contextWindow: 0, isSubscriptionModel: false, } ) }) const formattedSessionInfo = createMemo(() => { const sessionInfo = getSessionInfo(props.instanceId, props.sessionId) || { tokens: 0, cost: 0, contextWindow: 0, isSubscriptionModel: false, } return formatSessionInfo( sessionInfo.tokens, sessionInfo.cost, sessionInfo.contextWindow, sessionInfo.isSubscriptionModel, ) }) function scrollToBottom() { if (containerRef) { containerRef.scrollTop = containerRef.scrollHeight setAutoScroll(true) setShowScrollButton(false) } } function handleScroll() { if (!containerRef) return const { scrollTop, scrollHeight, clientHeight } = containerRef const isAtBottom = scrollHeight - scrollTop - clientHeight < 50 setAutoScroll(isAtBottom) setShowScrollButton(!isAtBottom) } const displayItems = createMemo(() => { const items: DisplayItem[] = [] let lastAssistantMessageId = "" for (let i = props.messages.length - 1; i >= 0; i--) { if (props.messages[i].type === "assistant") { lastAssistantMessageId = props.messages[i].id break } } for (const message of props.messages) { const messageInfo = props.messagesInfo?.get(message.id) // If we hit the revert point, stop rendering messages if (props.revert?.messageID && message.id === props.revert.messageID) { break } const textParts = message.parts.filter((p) => p.type === "text" && !p.synthetic) const toolParts = message.parts.filter((p) => p.type === "tool") const reasoningParts = preferences().showThinkingBlocks ? message.parts.filter((p) => p.type === "reasoning") : [] const isQueued = message.type === "user" && message.id > lastAssistantMessageId if (textParts.length > 0 || reasoningParts.length > 0 || messageInfo?.error) { items.push({ type: "message", data: { ...message, parts: [...textParts, ...reasoningParts], isQueued, }, messageInfo, }) } for (const toolPart of toolParts) { items.push({ type: "tool", data: toolPart, messageInfo, }) } } return items }) const itemsLength = () => displayItems().length createEffect(() => { itemsLength() if (autoScroll()) { setTimeout(scrollToBottom, 0) } }) return (
{formattedSessionInfo()}
Command Palette
Connected Connecting... Disconnected

Start a conversation

Type a message below or try:

  • /init-project
  • Ask about your codebase
  • Attach files with @

Loading messages...

{(item, index) => { const key = item.type === "message" ? `msg-${item.data.id}` : `tool-${item.data.id}` return (
🔧 Tool Call {item.data?.tool || "unknown"}
} >
) }}
) }