Session info + Todo formatting

This commit is contained in:
Shantur Rathore
2025-11-10 20:57:29 +00:00
parent 47f3948aec
commit 910249ff25
3 changed files with 133 additions and 61 deletions

View File

@@ -15,7 +15,6 @@ import {
} from "../stores/sessions" } from "../stores/sessions"
import { setActiveInstanceId } from "../stores/instances" import { setActiveInstanceId } from "../stores/instances"
const FALLBACK_MODEL_OUTPUT_LIMIT = 32_000
const SCROLL_OFFSET = 64 const SCROLL_OFFSET = 64
interface TaskSessionLocation { interface TaskSessionLocation {
@@ -54,7 +53,7 @@ function navigateToTaskSession(location: TaskSessionLocation) {
// Calculate session tokens and cost from messagesInfo (matches TUI logic) // Calculate session tokens and cost from messagesInfo (matches TUI logic)
function calculateSessionInfo(messagesInfo?: Map<string, any>, instanceId?: string) { function calculateSessionInfo(messagesInfo?: Map<string, any>, instanceId?: string) {
if (!messagesInfo || messagesInfo.size === 0) 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 tokens = 0
let cost = 0 let cost = 0
@@ -62,9 +61,6 @@ function calculateSessionInfo(messagesInfo?: Map<string, any>, instanceId?: stri
let isSubscriptionModel = false let isSubscriptionModel = false
let modelID = "" let modelID = ""
let providerID = "" 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) // Go backwards through messages to find the last relevant assistant message (like TUI)
const messageArray = Array.from(messagesInfo.values()).reverse() const messageArray = Array.from(messagesInfo.values()).reverse()
@@ -89,9 +85,6 @@ function calculateSessionInfo(messagesInfo?: Map<string, any>, instanceId?: stri
cost = info.cost || 0 cost = info.cost || 0
} }
inputTokensForUsage = usage.input || 0
cacheReadTokensForUsage = usage.cache?.read || 0
// Get model info for context window and subscription check // Get model info for context window and subscription check
modelID = info.modelID || "" modelID = info.modelID || ""
providerID = info.providerID || "" providerID = info.providerID || ""
@@ -115,9 +108,6 @@ function calculateSessionInfo(messagesInfo?: Map<string, any>, instanceId?: stri
if (model?.limit?.context) { if (model?.limit?.context) {
contextWindow = 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) // Check if it's a subscription model (cost is 0 for both input and output)
if (model?.cost?.input === 0 && model?.cost?.output === 0) { if (model?.cost?.input === 0 && model?.cost?.output === 0) {
isSubscriptionModel = true isSubscriptionModel = true
@@ -125,12 +115,9 @@ function calculateSessionInfo(messagesInfo?: Map<string, any>, instanceId?: stri
} }
} }
const contextUsageTokens = inputTokensForUsage + cacheReadTokensForUsage + modelOutputLimit return { tokens, cost, contextWindow, isSubscriptionModel }
return { tokens, cost, contextWindow, isSubscriptionModel, contextUsageTokens }
} }
// Format tokens like TUI (e.g., "110K", "1.2M") // Format tokens like TUI (e.g., "110K", "1.2M")
function formatTokens(tokens: number): string { function formatTokens(tokens: number): string {
if (tokens >= 1000000) { if (tokens >= 1000000) {
@@ -142,23 +129,16 @@ function formatTokens(tokens: number): string {
} }
// Format session info for the session view header // Format session info for the session view header
function formatSessionInfo( function formatSessionInfo(tokens: number, _cost: number, contextWindow: number, _isSubscriptionModel: boolean): string {
tokens: number, const tokensStr = formatTokens(tokens)
_cost: number,
contextWindow: number,
_isSubscriptionModel: boolean,
contextUsageTokens?: number,
): string {
const usageTokens = typeof contextUsageTokens === "number" && contextUsageTokens > 0 ? contextUsageTokens : tokens
const usageLabel = formatTokens(usageTokens)
if (contextWindow > 0) { if (contextWindow > 0) {
const windowStr = formatTokens(contextWindow) const windowStr = formatTokens(contextWindow)
const percentage = Math.min(100, Math.max(0, Math.round((usageTokens / contextWindow) * 100))) const percentage = Math.min(100, Math.max(0, Math.round((tokens / contextWindow) * 100)))
return `${usageLabel} of ${windowStr} (${percentage}%)` return `${tokensStr} of ${windowStr} (${percentage}%)`
} }
return usageLabel return tokensStr
} }
interface MessageStreamProps { interface MessageStreamProps {
@@ -265,31 +245,25 @@ export default function MessageStream(props: MessageStreamProps) {
cost: 0, cost: 0,
contextWindow: 0, contextWindow: 0,
isSubscriptionModel: false, isSubscriptionModel: false,
contextUsageTokens: 0,
} }
) )
}) })
const currentSession = createMemo(() => sessions().get(props.instanceId)?.get(props.sessionId))
const formattedSessionInfo = createMemo(() => { const formattedSessionInfo = createMemo(() => {
const sessionInfo = getSessionInfo(props.instanceId, props.sessionId) || { const sessionInfo = getSessionInfo(props.instanceId, props.sessionId) || {
tokens: 0, tokens: 0,
cost: 0, cost: 0,
contextWindow: 0, contextWindow: 0,
isSubscriptionModel: false, isSubscriptionModel: false,
contextUsageTokens: 0,
} }
return formatSessionInfo( return formatSessionInfo(
sessionInfo.tokens, sessionInfo.tokens,
sessionInfo.cost, sessionInfo.cost,
sessionInfo.contextWindow, sessionInfo.contextWindow,
sessionInfo.isSubscriptionModel, sessionInfo.isSubscriptionModel,
sessionInfo.contextUsageTokens,
) )
}) })
function isNearBottom(element: HTMLDivElement, offset = SCROLL_OFFSET) { function isNearBottom(element: HTMLDivElement, offset = SCROLL_OFFSET) {
const { scrollTop, scrollHeight, clientHeight } = element const { scrollTop, scrollHeight, clientHeight } = element
const distance = scrollHeight - (scrollTop + clientHeight) 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}`) 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 baseDisplayParts = message.displayParts
const displayParts: MessageDisplayParts = const displayParts: MessageDisplayParts =
baseDisplayParts && baseDisplayParts.showThinking === showThinking baseDisplayParts && baseDisplayParts.showThinking === showThinking
? baseDisplayParts ? baseDisplayParts

View File

@@ -568,20 +568,48 @@ export default function ToolCall(props: ToolCallProps) {
return null 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 ( return (
<div class="tool-call-todos"> <div class="tool-call-todos" role="list">
<For each={todos}> <For each={todos}>
{(todo) => { {(todo) => {
const content = todo.content const content = typeof todo.content === "string" ? todo.content.trim() : ""
if (!content) return null if (!content) return null
const status = typeof todo.status === "string" ? todo.status : "pending"
const label = getStatusLabel(status)
return ( return (
<div class="tool-call-todo-item"> <div
{todo.status === "completed" && "- [x] "} class="tool-call-todo-item"
{todo.status !== "completed" && "- [ ] "} classList={{
{todo.status === "cancelled" && <s>{content}</s>} "tool-call-todo-item-completed": status === "completed",
{todo.status === "in_progress" && <code>{content}</code>} "tool-call-todo-item-cancelled": status === "cancelled",
{todo.status !== "cancelled" && todo.status !== "in_progress" && content} "tool-call-todo-item-active": status === "in_progress",
}}
role="listitem"
>
<span class="tool-call-todo-checkbox" data-status={status} aria-label={label}></span>
<div class="tool-call-todo-body">
<span class="tool-call-todo-text">{content}</span>
<Show when={shouldShowTag(status)}>
<span class="tool-call-todo-tag">{label}</span>
</Show>
</div>
</div> </div>
) )
}} }}
@@ -601,7 +629,7 @@ export default function ToolCall(props: ToolCallProps) {
return ( return (
<div <div
class="message-text tool-call-markdown tool-call-markdown-large tool-call-task-container" class="message-text tool-call-markdown tool-call-task-container"
ref={(element) => initializeScrollContainer(element)} ref={(element) => initializeScrollContainer(element)}
onScroll={(event) => updateScrollState(toolCallId(), event.currentTarget)} onScroll={(event) => updateScrollState(toolCallId(), event.currentTarget)}
> >

View File

@@ -1042,25 +1042,102 @@ button.button-primary {
} }
.tool-call-todos { .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 { .tool-call-todo-item {
font-family: var(--font-family-mono); @apply flex items-start gap-3;
font-size: var(--font-size-xs); border: 1px solid var(--border-base);
line-height: var(--line-height-normal); border-radius: 8px;
padding: 10px 12px;
background-color: var(--surface-secondary);
} }
.tool-call-todo-item code { .tool-call-todo-item-completed {
background-color: rgba(0, 102, 255, 0.12); background-color: var(--surface-code);
padding: 2px 4px;
border-radius: 2px;
font-family: var(--font-family-mono);
color: inherit;
} }
[data-theme="dark"] .tool-call-todo-item code { .tool-call-todo-item-active {
background-color: rgba(0, 128, 255, 0.22); 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 { .tool-call-task-container {