Session info + Todo formatting
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -568,20 +568,48 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const getStatusLabel = (status: string): string => {
|
||||||
<div class="tool-call-todos">
|
switch (status) {
|
||||||
<For each={todos}>
|
case "completed":
|
||||||
{(todo) => {
|
return "Completed"
|
||||||
const content = todo.content
|
case "in_progress":
|
||||||
if (!content) return null
|
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-todo-item">
|
<div class="tool-call-todos" role="list">
|
||||||
{todo.status === "completed" && "- [x] "}
|
<For each={todos}>
|
||||||
{todo.status !== "completed" && "- [ ] "}
|
{(todo) => {
|
||||||
{todo.status === "cancelled" && <s>{content}</s>}
|
const content = typeof todo.content === "string" ? todo.content.trim() : ""
|
||||||
{todo.status === "in_progress" && <code>{content}</code>}
|
if (!content) return null
|
||||||
{todo.status !== "cancelled" && todo.status !== "in_progress" && content}
|
|
||||||
|
const status = typeof todo.status === "string" ? todo.status : "pending"
|
||||||
|
const label = getStatusLabel(status)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="tool-call-todo-item"
|
||||||
|
classList={{
|
||||||
|
"tool-call-todo-item-completed": status === "completed",
|
||||||
|
"tool-call-todo-item-cancelled": status === "cancelled",
|
||||||
|
"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)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user