add toasts, session usage tracking, and copy controls
This commit is contained in:
12
package-lock.json
generated
12
package-lock.json
generated
@@ -16,7 +16,8 @@
|
||||
"lucide-solid": "^0.300.0",
|
||||
"marked": "^12.0.0",
|
||||
"shiki": "^3.13.0",
|
||||
"solid-js": "^1.8.0"
|
||||
"solid-js": "^1.8.0",
|
||||
"solid-toast": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "^1.0.9",
|
||||
@@ -6761,6 +6762,15 @@
|
||||
"solid-js": "^1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/solid-toast": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/solid-toast/-/solid-toast-0.5.0.tgz",
|
||||
"integrity": "sha512-t770JakjyS2P9b8Qa1zMLOD51KYKWXbTAyJePVUoYex5c5FH5S/HtUBUbZAWFcqRCKmAE8KhyIiCvDZA8bOnxQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
"lucide-solid": "^0.300.0",
|
||||
"marked": "^12.0.0",
|
||||
"shiki": "^3.13.0",
|
||||
"solid-js": "^1.8.0"
|
||||
"solid-js": "^1.8.0",
|
||||
"solid-toast": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "^1.0.9",
|
||||
|
||||
19
src/App.tsx
19
src/App.tsx
@@ -1,4 +1,5 @@
|
||||
import { Component, onMount, onCleanup, Show, createMemo, createEffect, createSignal } from "solid-js"
|
||||
import { Toaster } from "solid-toast"
|
||||
import type { Session } from "./types/session"
|
||||
import type { Attachment } from "./types/attachment"
|
||||
import FolderSelectionView from "./components/folder-selection-view"
|
||||
@@ -219,15 +220,18 @@ const ContextUsagePanel: Component<{ instanceId: string; sessionId: string }> =
|
||||
cost: 0,
|
||||
contextWindow: 0,
|
||||
isSubscriptionModel: false,
|
||||
contextUsageTokens: 0,
|
||||
},
|
||||
)
|
||||
|
||||
const tokens = createMemo(() => info().tokens)
|
||||
const contextUsageTokens = createMemo(() => info().contextUsageTokens ?? 0)
|
||||
const contextWindow = createMemo(() => info().contextWindow)
|
||||
const percentage = createMemo(() => {
|
||||
const windowSize = contextWindow()
|
||||
if (!windowSize || windowSize <= 0) return null
|
||||
const percent = Math.round((tokens() / windowSize) * 100)
|
||||
const usage = contextUsageTokens()
|
||||
const percent = Math.round((usage / windowSize) * 100)
|
||||
return Math.min(100, Math.max(0, percent))
|
||||
})
|
||||
|
||||
@@ -252,7 +256,7 @@ const ContextUsagePanel: Component<{ instanceId: string; sessionId: string }> =
|
||||
</div>
|
||||
<div class="text-sm text-primary/90">
|
||||
{contextWindow()
|
||||
? `${formatTokenTotal(tokens())} of ${formatTokenTotal(contextWindow())}`
|
||||
? `${formatTokenTotal(contextUsageTokens())} of ${formatTokenTotal(contextWindow())}`
|
||||
: "Window size unavailable"}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1081,6 +1085,17 @@ const App: Component = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Toaster
|
||||
position="top-right"
|
||||
gutter={12}
|
||||
toastOptions={{
|
||||
duration: 5000,
|
||||
className: `text-sm shadow-lg border border-base ${
|
||||
isDark() ? "bg-surface-secondary text-primary" : "bg-white text-gray-900"
|
||||
}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
59
src/lib/notifications.tsx
Normal file
59
src/lib/notifications.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import toast from "solid-toast"
|
||||
|
||||
export type ToastVariant = "info" | "success" | "warning" | "error"
|
||||
|
||||
export type ToastPayload = {
|
||||
title?: string
|
||||
message: string
|
||||
variant: ToastVariant
|
||||
duration?: number
|
||||
}
|
||||
|
||||
const variantAccent: Record<ToastVariant, { badge: string; border: string; text: string }> = {
|
||||
info: {
|
||||
badge: "bg-blue-500",
|
||||
border: "border-blue-500/40",
|
||||
text: "text-blue-100",
|
||||
},
|
||||
success: {
|
||||
badge: "bg-emerald-500",
|
||||
border: "border-emerald-500/40",
|
||||
text: "text-emerald-100",
|
||||
},
|
||||
warning: {
|
||||
badge: "bg-amber-500",
|
||||
border: "border-amber-500/40",
|
||||
text: "text-amber-100",
|
||||
},
|
||||
error: {
|
||||
badge: "bg-rose-500",
|
||||
border: "border-rose-500/40",
|
||||
text: "text-rose-100",
|
||||
},
|
||||
}
|
||||
|
||||
export function showToastNotification(payload: ToastPayload) {
|
||||
const accent = variantAccent[payload.variant]
|
||||
const duration = payload.duration ?? 5000
|
||||
|
||||
toast.custom(
|
||||
() => (
|
||||
<div class={`min-w-[280px] max-w-[360px] rounded-xl border px-4 py-3 shadow-xl bg-surface-secondary ${accent.border}`}>
|
||||
<div class="flex gap-3">
|
||||
<span class={`mt-1 inline-block h-2.5 w-2.5 rounded-full ${accent.badge}`} />
|
||||
<div class="flex-1 text-sm leading-snug">
|
||||
{payload.title && <p class="font-semibold text-primary">{payload.title}</p>}
|
||||
<p class={`text-primary/90 ${payload.title ? "mt-1" : ""}`}>{payload.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
duration,
|
||||
ariaProps: {
|
||||
role: "status",
|
||||
"aria-live": "polite",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -20,6 +20,16 @@ interface SessionUpdateEvent {
|
||||
session: any
|
||||
}
|
||||
|
||||
interface TuiToastEvent {
|
||||
type: "tui.toast.show"
|
||||
properties: {
|
||||
title?: string
|
||||
message: string
|
||||
variant: "info" | "success" | "warning" | "error"
|
||||
duration?: number
|
||||
}
|
||||
}
|
||||
|
||||
const [connectionStatus, setConnectionStatus] = createSignal<
|
||||
Map<string, "connecting" | "connected" | "disconnected" | "error">
|
||||
>(new Map())
|
||||
@@ -104,6 +114,9 @@ class SSEManager {
|
||||
case "session.error":
|
||||
this.onSessionError?.(instanceId, event)
|
||||
break
|
||||
case "tui.toast.show":
|
||||
this.onTuiToast?.(instanceId, event as TuiToastEvent)
|
||||
break
|
||||
case "session.idle":
|
||||
console.log("[SSE] Session idle")
|
||||
break
|
||||
@@ -147,6 +160,7 @@ class SSEManager {
|
||||
onSessionUpdate?: (instanceId: string, event: SessionUpdateEvent) => void
|
||||
onSessionCompacted?: (instanceId: string, event: any) => void
|
||||
onSessionError?: (instanceId: string, event: any) => void
|
||||
onTuiToast?: (instanceId: string, event: TuiToastEvent) => void
|
||||
|
||||
getStatus(instanceId: string): "connecting" | "connected" | "disconnected" | "error" | null {
|
||||
return connectionStatus().get(instanceId) ?? null
|
||||
|
||||
@@ -40,12 +40,39 @@ export class FileStorage {
|
||||
}
|
||||
}
|
||||
|
||||
private parseConfig(content: string): ConfigData {
|
||||
const trimmed = content.trim()
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed)
|
||||
} catch (error) {
|
||||
const firstBrace = trimmed.indexOf("{")
|
||||
const lastBrace = trimmed.lastIndexOf("}")
|
||||
|
||||
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
||||
const sanitized = trimmed.slice(firstBrace, lastBrace + 1)
|
||||
|
||||
if (sanitized.length !== trimmed.length) {
|
||||
console.warn("Config file contained trailing data; attempting recovery")
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(sanitized)
|
||||
} catch {
|
||||
// Fall through to rethrow original error below
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Config operations
|
||||
async loadConfig(): Promise<ConfigData> {
|
||||
await this.ensureInitialized()
|
||||
try {
|
||||
const content = await window.electronAPI.readConfigFile()
|
||||
return JSON.parse(content)
|
||||
return this.parseConfig(content)
|
||||
} catch (error) {
|
||||
console.warn("Failed to load config, using defaults:", error)
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { instances } from "./instances"
|
||||
|
||||
import { sseManager } from "../lib/sse-manager"
|
||||
import { decodeHtmlEntities } from "../lib/markdown"
|
||||
import { showToastNotification, ToastVariant } from "../lib/notifications"
|
||||
import { preferences, addRecentModelPreference, getAgentModelPreference, setAgentModelPreference } from "./preferences"
|
||||
|
||||
interface SessionInfo {
|
||||
@@ -13,8 +14,12 @@ interface SessionInfo {
|
||||
cost: number
|
||||
contextWindow: number
|
||||
isSubscriptionModel: boolean
|
||||
contextUsageTokens: number
|
||||
}
|
||||
|
||||
const DEFAULT_MODEL_OUTPUT_LIMIT = 32_000
|
||||
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
|
||||
|
||||
const [sessions, setSessions] = createSignal<Map<string, Map<string, Session>>>(new Map())
|
||||
const [activeSessionId, setActiveSessionId] = createSignal<Map<string, string>>(new Map())
|
||||
const [activeParentSessionId, setActiveParentSessionId] = createSignal<Map<string, string>>(new Map())
|
||||
@@ -424,6 +429,8 @@ function updateSessionInfo(instanceId: string, sessionId: string) {
|
||||
let isSubscriptionModel = false
|
||||
let modelID = ""
|
||||
let providerID = ""
|
||||
let inputTokensForUsage = 0
|
||||
let cacheReadTokensForUsage = 0
|
||||
|
||||
// Calculate from last assistant message in this session only
|
||||
if (session.messagesInfo.size > 0) {
|
||||
@@ -450,7 +457,10 @@ function updateSessionInfo(instanceId: string, sessionId: string) {
|
||||
cost = info.cost || 0
|
||||
}
|
||||
|
||||
// Get model info for context window and subscription check
|
||||
inputTokensForUsage = usage.input || 0
|
||||
cacheReadTokensForUsage = usage.cache?.read || 0
|
||||
|
||||
// Get model info identifiers for context lookups
|
||||
modelID = info.modelID || ""
|
||||
providerID = info.providerID || ""
|
||||
isSubscriptionModel = cost === 0
|
||||
@@ -461,22 +471,39 @@ function updateSessionInfo(instanceId: string, sessionId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get context window from providers
|
||||
if (modelID && providerID) {
|
||||
const instanceProviders = providers().get(instanceId) || []
|
||||
const instanceProviders = providers().get(instanceId) || []
|
||||
|
||||
const sessionModel = session.model
|
||||
let selectedModel: Provider["models"][number] | undefined
|
||||
|
||||
if (sessionModel?.providerId && sessionModel?.modelId) {
|
||||
const provider = instanceProviders.find((p) => p.id === sessionModel.providerId)
|
||||
selectedModel = provider?.models.find((m) => m.id === sessionModel.modelId)
|
||||
}
|
||||
|
||||
if (!selectedModel && modelID && providerID) {
|
||||
const provider = instanceProviders.find((p) => p.id === providerID)
|
||||
if (provider) {
|
||||
const model = provider.models.find((m) => m.id === modelID)
|
||||
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
|
||||
}
|
||||
selectedModel = provider?.models.find((m) => m.id === modelID)
|
||||
}
|
||||
|
||||
let modelOutputLimit = DEFAULT_MODEL_OUTPUT_LIMIT
|
||||
|
||||
if (selectedModel) {
|
||||
if (selectedModel.limit?.context) {
|
||||
contextWindow = selectedModel.limit.context
|
||||
}
|
||||
|
||||
if (selectedModel.limit?.output && selectedModel.limit.output > 0) {
|
||||
modelOutputLimit = selectedModel.limit.output
|
||||
}
|
||||
|
||||
if (selectedModel.cost?.input === 0 && selectedModel.cost?.output === 0) {
|
||||
isSubscriptionModel = true
|
||||
}
|
||||
}
|
||||
|
||||
const contextUsageTokens = inputTokensForUsage + cacheReadTokensForUsage + modelOutputLimit
|
||||
|
||||
setSessionInfoByInstance((prev) => {
|
||||
const next = new Map(prev)
|
||||
const instanceInfo = new Map(prev.get(instanceId))
|
||||
@@ -485,6 +512,7 @@ function updateSessionInfo(instanceId: string, sessionId: string) {
|
||||
cost,
|
||||
contextWindow,
|
||||
isSubscriptionModel,
|
||||
contextUsageTokens,
|
||||
})
|
||||
next.set(instanceId, instanceInfo)
|
||||
return next
|
||||
@@ -552,14 +580,25 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
|
||||
})
|
||||
|
||||
// Initialize session info with zeros for the new session
|
||||
const instanceProviders = providers().get(instanceId) || []
|
||||
const initialProvider = instanceProviders.find((p) => p.id === session.model.providerId)
|
||||
const initialModel = initialProvider?.models.find((m) => m.id === session.model.modelId)
|
||||
const initialContextWindow = initialModel?.limit?.context ?? 0
|
||||
const initialOutputLimit =
|
||||
initialModel?.limit?.output && initialModel.limit.output > 0
|
||||
? initialModel.limit.output
|
||||
: DEFAULT_MODEL_OUTPUT_LIMIT
|
||||
const initialSubscriptionModel = initialModel?.cost?.input === 0 && initialModel?.cost?.output === 0
|
||||
|
||||
setSessionInfoByInstance((prev) => {
|
||||
const next = new Map(prev)
|
||||
const instanceInfo = new Map(prev.get(instanceId))
|
||||
instanceInfo.set(session.id, {
|
||||
tokens: 0,
|
||||
cost: 0,
|
||||
contextWindow: 0,
|
||||
isSubscriptionModel: false,
|
||||
contextWindow: initialContextWindow,
|
||||
isSubscriptionModel: Boolean(initialSubscriptionModel),
|
||||
contextUsageTokens: initialOutputLimit,
|
||||
})
|
||||
next.set(instanceId, instanceInfo)
|
||||
return next
|
||||
@@ -644,14 +683,23 @@ async function forkSession(
|
||||
return next
|
||||
})
|
||||
|
||||
const instanceProviders = providers().get(instanceId) || []
|
||||
const forkProvider = instanceProviders.find((p) => p.id === forkedSession.model.providerId)
|
||||
const forkModel = forkProvider?.models.find((m) => m.id === forkedSession.model.modelId)
|
||||
const forkContextWindow = forkModel?.limit?.context ?? 0
|
||||
const forkOutputLimit =
|
||||
forkModel?.limit?.output && forkModel.limit.output > 0 ? forkModel.limit.output : DEFAULT_MODEL_OUTPUT_LIMIT
|
||||
const forkSubscriptionModel = forkModel?.cost?.input === 0 && forkModel?.cost?.output === 0
|
||||
|
||||
setSessionInfoByInstance((prev) => {
|
||||
const next = new Map(prev)
|
||||
const instanceInfo = new Map(prev.get(instanceId))
|
||||
instanceInfo.set(forkedSession.id, {
|
||||
tokens: 0,
|
||||
cost: 0,
|
||||
contextWindow: 0,
|
||||
isSubscriptionModel: false,
|
||||
contextWindow: forkContextWindow,
|
||||
isSubscriptionModel: Boolean(forkSubscriptionModel),
|
||||
contextUsageTokens: forkOutputLimit,
|
||||
})
|
||||
next.set(instanceId, instanceInfo)
|
||||
return next
|
||||
@@ -1597,6 +1645,10 @@ async function updateSessionAgent(instanceId: string, sessionId: string, agent:
|
||||
if (agent && shouldApplyModel) {
|
||||
setAgentModelPreference(instanceId, agent, nextModel)
|
||||
}
|
||||
|
||||
if (shouldApplyModel) {
|
||||
updateSessionInfo(instanceId, sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSessionModel(
|
||||
@@ -1632,6 +1684,8 @@ async function updateSessionModel(
|
||||
setAgentModelPreference(instanceId, currentAgent, model)
|
||||
}
|
||||
addRecentModelPreference(model)
|
||||
|
||||
updateSessionInfo(instanceId, sessionId)
|
||||
}
|
||||
|
||||
function handleSessionCompacted(instanceId: string, event: any): void {
|
||||
@@ -1677,12 +1731,30 @@ function handleMessagePartRemoved(instanceId: string, event: any): void {
|
||||
loadMessages(instanceId, sessionID, true).catch(console.error)
|
||||
}
|
||||
|
||||
function handleTuiToast(_instanceId: string, event: any): void {
|
||||
const payload = event?.properties
|
||||
if (!payload || typeof payload.message !== "string" || typeof payload.variant !== "string") return
|
||||
if (!payload.message.trim()) return
|
||||
|
||||
const variant: ToastVariant = ALLOWED_TOAST_VARIANTS.has(payload.variant as ToastVariant)
|
||||
? (payload.variant as ToastVariant)
|
||||
: "info"
|
||||
|
||||
showToastNotification({
|
||||
title: typeof payload.title === "string" ? payload.title : undefined,
|
||||
message: payload.message,
|
||||
variant,
|
||||
duration: typeof payload.duration === "number" ? payload.duration : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
sseManager.onMessageUpdate = handleMessageUpdate
|
||||
sseManager.onMessageRemoved = handleMessageRemoved
|
||||
sseManager.onMessagePartRemoved = handleMessagePartRemoved
|
||||
sseManager.onSessionUpdate = handleSessionUpdate
|
||||
sseManager.onSessionCompacted = handleSessionCompacted
|
||||
sseManager.onSessionError = handleSessionError
|
||||
sseManager.onTuiToast = handleTuiToast
|
||||
|
||||
export {
|
||||
sessions,
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface Model {
|
||||
providerId: string
|
||||
limit?: {
|
||||
context?: number
|
||||
output?: number
|
||||
}
|
||||
cost?: {
|
||||
input?: number
|
||||
|
||||
Reference in New Issue
Block a user