add toasts, session usage tracking, and copy controls
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
} from "../stores/sessions"
|
||||
import { setActiveInstanceId } from "../stores/instances"
|
||||
|
||||
const FALLBACK_MODEL_OUTPUT_LIMIT = 32_000
|
||||
const SCROLL_OFFSET = 64
|
||||
|
||||
interface TaskSessionLocation {
|
||||
@@ -53,7 +54,7 @@ function navigateToTaskSession(location: TaskSessionLocation) {
|
||||
// Calculate session tokens and cost from messagesInfo (matches TUI logic)
|
||||
function calculateSessionInfo(messagesInfo?: Map<string, any>, instanceId?: string) {
|
||||
if (!messagesInfo || messagesInfo.size === 0)
|
||||
return { tokens: 0, cost: 0, contextWindow: 0, isSubscriptionModel: false }
|
||||
return { tokens: 0, cost: 0, contextWindow: 0, isSubscriptionModel: false, contextUsageTokens: 0 }
|
||||
|
||||
let tokens = 0
|
||||
let cost = 0
|
||||
@@ -61,6 +62,9 @@ function calculateSessionInfo(messagesInfo?: Map<string, any>, 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()
|
||||
@@ -85,6 +89,9 @@ function calculateSessionInfo(messagesInfo?: Map<string, any>, 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 || ""
|
||||
@@ -108,6 +115,9 @@ function calculateSessionInfo(messagesInfo?: Map<string, any>, 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
|
||||
@@ -115,9 +125,12 @@ function calculateSessionInfo(messagesInfo?: Map<string, any>, instanceId?: stri
|
||||
}
|
||||
}
|
||||
|
||||
return { tokens, cost, contextWindow, isSubscriptionModel }
|
||||
const contextUsageTokens = inputTokensForUsage + cacheReadTokensForUsage + modelOutputLimit
|
||||
|
||||
return { tokens, cost, contextWindow, isSubscriptionModel, contextUsageTokens }
|
||||
}
|
||||
|
||||
|
||||
// Format tokens like TUI (e.g., "110K", "1.2M")
|
||||
function formatTokens(tokens: number): string {
|
||||
if (tokens >= 1000000) {
|
||||
@@ -129,16 +142,23 @@ function formatTokens(tokens: number): string {
|
||||
}
|
||||
|
||||
// Format session info for the session view header
|
||||
function formatSessionInfo(tokens: number, _cost: number, contextWindow: number, _isSubscriptionModel: boolean): string {
|
||||
const tokensStr = formatTokens(tokens)
|
||||
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)
|
||||
|
||||
if (contextWindow > 0) {
|
||||
const windowStr = formatTokens(contextWindow)
|
||||
const percentage = Math.min(100, Math.max(0, Math.round((tokens / contextWindow) * 100)))
|
||||
return `${tokensStr} of ${windowStr} (${percentage}%)`
|
||||
const percentage = Math.min(100, Math.max(0, Math.round((usageTokens / contextWindow) * 100)))
|
||||
return `${usageLabel} of ${windowStr} (${percentage}%)`
|
||||
}
|
||||
|
||||
return tokensStr
|
||||
return usageLabel
|
||||
}
|
||||
|
||||
interface MessageStreamProps {
|
||||
@@ -245,6 +265,7 @@ export default function MessageStream(props: MessageStreamProps) {
|
||||
cost: 0,
|
||||
contextWindow: 0,
|
||||
isSubscriptionModel: false,
|
||||
contextUsageTokens: 0,
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -255,12 +276,14 @@ export default function MessageStream(props: MessageStreamProps) {
|
||||
cost: 0,
|
||||
contextWindow: 0,
|
||||
isSubscriptionModel: false,
|
||||
contextUsageTokens: 0,
|
||||
}
|
||||
return formatSessionInfo(
|
||||
sessionInfo.tokens,
|
||||
sessionInfo.cost,
|
||||
sessionInfo.contextWindow,
|
||||
sessionInfo.isSubscriptionModel,
|
||||
sessionInfo.contextUsageTokens,
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Component, For, Show, createSignal, createEffect, onCleanup, onMount, createMemo, JSX } from "solid-js"
|
||||
import type { Session } from "../types/session"
|
||||
import { MessageSquare, Info, X } from "lucide-solid"
|
||||
import { MessageSquare, Info, X, Copy } from "lucide-solid"
|
||||
import KeyboardHint from "./keyboard-hint"
|
||||
import Kbd from "./kbd"
|
||||
import { keyboardRegistry } from "../lib/keyboard-registry"
|
||||
import { formatShortcut } from "../lib/keyboard-utils"
|
||||
import { showToastNotification } from "../lib/notifications"
|
||||
|
||||
interface SessionListProps {
|
||||
instanceId: string
|
||||
@@ -76,9 +77,26 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
createEffect(() => {
|
||||
props.onWidthChange?.(sidebarWidth())
|
||||
})
|
||||
|
||||
|
||||
const copySessionId = async (event: MouseEvent, sessionId: string) => {
|
||||
event.stopPropagation()
|
||||
|
||||
try {
|
||||
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
||||
throw new Error("Clipboard API unavailable")
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(sessionId)
|
||||
showToastNotification({ message: "Session ID copied", variant: "success" })
|
||||
} catch (error) {
|
||||
console.error(`Failed to copy session ID ${sessionId}:`, error)
|
||||
showToastNotification({ message: "Unable to copy session ID", variant: "error" })
|
||||
}
|
||||
}
|
||||
|
||||
const clampWidth = (width: number) => Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, width))
|
||||
|
||||
|
||||
const removeMouseListeners = () => {
|
||||
if (mouseMoveHandler) {
|
||||
document.removeEventListener("mousemove", mouseMoveHandler)
|
||||
@@ -269,18 +287,30 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
>
|
||||
<MessageSquare class="w-4 h-4 flex-shrink-0" />
|
||||
<span class="session-item-title truncate">{title()}</span>
|
||||
<span
|
||||
class="session-item-close opacity-0 group-hover:opacity-100 hover:bg-status-error hover:text-white rounded p-0.5 transition-all"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
props.onClose(id)
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Close session"
|
||||
>
|
||||
<X class="w-3 h-3" />
|
||||
</span>
|
||||
<div class="flex items-center gap-1 ml-auto">
|
||||
<span
|
||||
class={`session-item-close opacity-80 hover:opacity-100 ${isActive() ? "hover:bg-white/20" : "hover:bg-surface-hover"}`}
|
||||
onClick={(event) => copySessionId(event, id)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Copy session ID"
|
||||
title="Copy session ID"
|
||||
>
|
||||
<Copy class="w-3 h-3" />
|
||||
</span>
|
||||
<span
|
||||
class="session-item-close opacity-0 group-hover:opacity-100 hover:bg-status-error hover:text-white rounded p-0.5 transition-all"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
props.onClose(id)
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Close session"
|
||||
>
|
||||
<X class="w-3 h-3" />
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@@ -315,6 +345,18 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
>
|
||||
<MessageSquare class="w-4 h-4 flex-shrink-0" />
|
||||
<span class="session-item-title truncate">{title()}</span>
|
||||
<div class="flex items-center gap-1 ml-auto">
|
||||
<span
|
||||
class={`session-item-close opacity-80 hover:opacity-100 ${isActive() ? "hover:bg-white/20" : "hover:bg-surface-hover"}`}
|
||||
onClick={(event) => copySessionId(event, id)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Copy session ID"
|
||||
title="Copy session ID"
|
||||
>
|
||||
<Copy class="w-3 h-3" />
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user