diff --git a/src/components/message-stream.tsx b/src/components/message-stream.tsx index ba728e0b..0f5a4626 100644 --- a/src/components/message-stream.tsx +++ b/src/components/message-stream.tsx @@ -15,7 +15,6 @@ import { } from "../stores/sessions" import { setActiveInstanceId } from "../stores/instances" -const FALLBACK_MODEL_OUTPUT_LIMIT = 32_000 const SCROLL_OFFSET = 64 interface TaskSessionLocation { @@ -54,7 +53,7 @@ function navigateToTaskSession(location: TaskSessionLocation) { // 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, contextUsageTokens: 0 } + return { tokens: 0, cost: 0, contextWindow: 0, isSubscriptionModel: false } let tokens = 0 let cost = 0 @@ -62,9 +61,6 @@ function calculateSessionInfo(messagesInfo?: Map, instanceId?: stri let isSubscriptionModel = false let modelID = "" let providerID = "" - let inputTokensForUsage = 0 - let cacheReadTokensForUsage = 0 - let modelOutputLimit = FALLBACK_MODEL_OUTPUT_LIMIT // Go backwards through messages to find the last relevant assistant message (like TUI) const messageArray = Array.from(messagesInfo.values()).reverse() @@ -89,9 +85,6 @@ function calculateSessionInfo(messagesInfo?: Map, instanceId?: stri cost = info.cost || 0 } - inputTokensForUsage = usage.input || 0 - cacheReadTokensForUsage = usage.cache?.read || 0 - // Get model info for context window and subscription check modelID = info.modelID || "" providerID = info.providerID || "" @@ -115,9 +108,6 @@ function calculateSessionInfo(messagesInfo?: Map, instanceId?: stri if (model?.limit?.context) { contextWindow = model.limit.context } - if (model?.limit?.output && model.limit.output > 0) { - modelOutputLimit = model.limit.output - } // 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 @@ -125,12 +115,9 @@ function calculateSessionInfo(messagesInfo?: Map, instanceId?: stri } } - const contextUsageTokens = inputTokensForUsage + cacheReadTokensForUsage + modelOutputLimit - - return { tokens, cost, contextWindow, isSubscriptionModel, contextUsageTokens } + return { tokens, cost, contextWindow, isSubscriptionModel } } - // Format tokens like TUI (e.g., "110K", "1.2M") function formatTokens(tokens: number): string { if (tokens >= 1000000) { @@ -142,23 +129,16 @@ function formatTokens(tokens: number): string { } // Format session info for the session view header -function formatSessionInfo( - tokens: number, - _cost: number, - contextWindow: number, - _isSubscriptionModel: boolean, - contextUsageTokens?: number, -): string { - const usageTokens = typeof contextUsageTokens === "number" && contextUsageTokens > 0 ? contextUsageTokens : tokens - const usageLabel = formatTokens(usageTokens) +function formatSessionInfo(tokens: number, _cost: number, contextWindow: number, _isSubscriptionModel: boolean): string { + const tokensStr = formatTokens(tokens) if (contextWindow > 0) { const windowStr = formatTokens(contextWindow) - const percentage = Math.min(100, Math.max(0, Math.round((usageTokens / contextWindow) * 100))) - return `${usageLabel} of ${windowStr} (${percentage}%)` + const percentage = Math.min(100, Math.max(0, Math.round((tokens / contextWindow) * 100))) + return `${tokensStr} of ${windowStr} (${percentage}%)` } - return usageLabel + return tokensStr } interface MessageStreamProps { @@ -265,31 +245,25 @@ export default function MessageStream(props: MessageStreamProps) { cost: 0, contextWindow: 0, isSubscriptionModel: false, - contextUsageTokens: 0, } ) }) - const currentSession = createMemo(() => sessions().get(props.instanceId)?.get(props.sessionId)) - const formattedSessionInfo = createMemo(() => { const sessionInfo = getSessionInfo(props.instanceId, props.sessionId) || { tokens: 0, cost: 0, contextWindow: 0, isSubscriptionModel: false, - contextUsageTokens: 0, } return formatSessionInfo( sessionInfo.tokens, sessionInfo.cost, sessionInfo.contextWindow, sessionInfo.isSubscriptionModel, - sessionInfo.contextUsageTokens, ) }) - function isNearBottom(element: HTMLDivElement, offset = SCROLL_OFFSET) { const { scrollTop, scrollHeight, clientHeight } = element const distance = scrollHeight - (scrollTop + clientHeight) @@ -391,14 +365,7 @@ export default function MessageStream(props: MessageStreamProps) { tokenSegments.push(`${message.id}:${message.version ?? 0}:${message.status}:${message.parts.length}`) - for (const part of message.parts) { - const partId = typeof part.id === "string" ? part.id : part.type ?? "part" - const partVersion = typeof (part as any).version === "number" ? (part as any).version : 0 - tokenSegments.push(`${message.id}:${partId}:${partVersion}`) - } - const baseDisplayParts = message.displayParts - const displayParts: MessageDisplayParts = baseDisplayParts && baseDisplayParts.showThinking === showThinking ? baseDisplayParts diff --git a/src/components/tool-call.tsx b/src/components/tool-call.tsx index 54f1241f..e6cd63cc 100644 --- a/src/components/tool-call.tsx +++ b/src/components/tool-call.tsx @@ -568,20 +568,48 @@ export default function ToolCall(props: ToolCallProps) { return null } + const getStatusLabel = (status: string): string => { + switch (status) { + case "completed": + return "Completed" + case "in_progress": + return "In progress" + case "cancelled": + return "Cancelled" + default: + return "Pending" + } + } + + const shouldShowTag = (status: string) => status === "in_progress" || status === "cancelled" + return ( -
+
{(todo) => { - const content = todo.content + const content = typeof todo.content === "string" ? todo.content.trim() : "" if (!content) return null + const status = typeof todo.status === "string" ? todo.status : "pending" + const label = getStatusLabel(status) + return ( -
- {todo.status === "completed" && "- [x] "} - {todo.status !== "completed" && "- [ ] "} - {todo.status === "cancelled" && {content}} - {todo.status === "in_progress" && {content}} - {todo.status !== "cancelled" && todo.status !== "in_progress" && content} +
+ +
+ {content} + + {label} + +
) }} @@ -601,7 +629,7 @@ export default function ToolCall(props: ToolCallProps) { return (
initializeScrollContainer(element)} onScroll={(event) => updateScrollState(toolCallId(), event.currentTarget)} > diff --git a/src/styles/components.css b/src/styles/components.css index abd72bf3..f411507a 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -1042,25 +1042,102 @@ button.button-primary { } .tool-call-todos { - @apply my-2 flex flex-col gap-1; + @apply my-2 flex flex-col gap-2; + list-style: none; + padding: 4px 0; } .tool-call-todo-item { - font-family: var(--font-family-mono); - font-size: var(--font-size-xs); - line-height: var(--line-height-normal); + @apply flex items-start gap-3; + border: 1px solid var(--border-base); + border-radius: 8px; + padding: 10px 12px; + background-color: var(--surface-secondary); } -.tool-call-todo-item code { - background-color: rgba(0, 102, 255, 0.12); - padding: 2px 4px; - border-radius: 2px; - font-family: var(--font-family-mono); - color: inherit; +.tool-call-todo-item-completed { + background-color: var(--surface-code); } -[data-theme="dark"] .tool-call-todo-item code { - background-color: rgba(0, 128, 255, 0.22); +.tool-call-todo-item-active { + border-color: var(--accent-primary); + background-color: var(--surface-hover); +} + +.tool-call-todo-item-cancelled { + opacity: 0.75; +} + +.tool-call-todo-checkbox { + width: 1.1rem; + height: 1.1rem; + border-radius: 9999px; + border: 2px solid var(--border-base); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: var(--font-weight-semibold); + color: var(--text-muted); + background-color: transparent; +} + +.tool-call-todo-checkbox::after { + content: ""; + line-height: 1; +} + +.tool-call-todo-checkbox[data-status="completed"] { + background-color: var(--accent-primary); + border-color: var(--accent-primary); + color: var(--text-inverted); +} + +.tool-call-todo-checkbox[data-status="completed"]::after { + content: "✓"; +} + +.tool-call-todo-checkbox[data-status="in_progress"]::after { + content: "…"; + color: var(--accent-primary); +} + +.tool-call-todo-checkbox[data-status="cancelled"]::after { + content: "×"; + color: var(--status-error); +} + +.tool-call-todo-body { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.tool-call-todo-text { + font-size: var(--font-size-sm); + line-height: var(--line-height-tight); + color: var(--text-primary); +} + +.tool-call-todo-item-cancelled .tool-call-todo-text { + text-decoration: line-through; + color: var(--text-muted); +} + +.tool-call-todo-tag { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; + border-radius: 9999px; + padding: 2px 8px; + background-color: var(--surface-hover); + color: var(--text-muted); +} + +.tool-call-todo-item-active .tool-call-todo-tag { + background-color: var(--accent-primary); + color: var(--text-inverted); } .tool-call-task-container {