diff --git a/apps/desktop/layer/renderer/package.json b/apps/desktop/layer/renderer/package.json index 5f91fcc9b..d7139ef74 100644 --- a/apps/desktop/layer/renderer/package.json +++ b/apps/desktop/layer/renderer/package.json @@ -119,7 +119,7 @@ "@follow/models": "workspace:*", "@follow/types": "workspace:*", "@follow/utils": "workspace:*", - "@folo-services/ai-tools": "0.2.20", + "@folo-services/ai-tools": "0.2.31", "@types/node": "24.0.10", "@vite-pwa/assets-generator": "1.0.0", "fake-indexeddb": "6.0.1", diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx index 60cb7c1ca..279c007f5 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx @@ -96,7 +96,7 @@ const AIDisplayFeedsPartBase = ({ return ( {/* Statistics Overview */} -
+
{ (message: string | EditorState, editor: LexicalEditor | null) => { resetScrollState() - const blocks = [] as any[] + const blocks = [] as AIChatContextBlock[] for (const block of blockActions.getBlocks()) { if (block.type === "fileAttachment" && block.attachment.serverUrl) { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx index 2912dfeee..c8e84a2dd 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx @@ -7,12 +7,13 @@ import * as React from "react" import { toast } from "sonner" import { copyToClipboard } from "~/lib/clipboard" -import type { BizUIMetadata, BizUITools } from "~/modules/ai-chat/store/types" +import type { BizUIMessage, BizUIMetadata, BizUITools } from "~/modules/ai-chat/store/types" import { MentionPlugin } from "../../editor" import type { RichTextPart } from "../../types/ChatSession" import { convertLexicalToMarkdown } from "../../utils/lexical-markdown" import { AIMessageParts } from "./AIMessageParts" +import { TokenUsagePill } from "./TokenUsagePill" export interface ChatMessage { id: string @@ -22,7 +23,7 @@ export interface ChatMessage { } interface AIChatMessageProps { - message: UIMessage + message: BizUIMessage } // Utility function for converting message to markdown @@ -96,6 +97,16 @@ export const AIChatMessage: React.FC = React.memo(({ message Copy + {message.metadata && ( + + + + )}
diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx index 4ee230941..a9710040f 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx @@ -23,7 +23,7 @@ export const AIDataBlockPart: React.FC = React.memo(({ blo
diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/TokenUsagePill.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/TokenUsagePill.tsx new file mode 100644 index 000000000..9ff9996cd --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/TokenUsagePill.tsx @@ -0,0 +1,114 @@ +import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.js" +import type { BizUIMetadata } from "@folo-services/ai-tools" +import * as React from "react" + +import { formatTokenCountString } from "~/modules/settings/tabs/ai/usage/utils" + +interface TokenUsagePillProps { + metadata: BizUIMetadata | undefined + className?: string + children: React.ReactNode +} + +const formatDuration = (ms: number): string => { + if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s` + return `${ms}ms` +} + +export const TokenUsagePill: React.FC = ({ metadata, children }) => { + if (!metadata) return null + + const hasReasoningTokens = metadata.reasoningTokens != null && metadata.reasoningTokens > 0 + const hasCachedInputTokens = metadata.cachedInputTokens != null && metadata.cachedInputTokens > 0 + const hasBillingMultiplier = + metadata.billingMultiplier != null && metadata.billingMultiplier !== 1 + const hasDuration = metadata.duration != null + + return ( + + {children} + +
+
Model Info
+
+ {metadata.modelUsed ?? "Unknown"} +
+
+
+
Token Usage
+
+ {metadata.totalTokens != null && ( +
+ Total: + + {formatTokenCountString(metadata.totalTokens)} + +
+ )} + {metadata.billedTokens != null && ( +
+ Billed: + + {formatTokenCountString(metadata.billedTokens)} + +
+ )} + {metadata.contextTokens != null && ( +
+ Context: + + {formatTokenCountString(metadata.contextTokens)} + +
+ )} + {metadata.outputTokens != null && ( +
+ Output: + + {formatTokenCountString(metadata.outputTokens)} + +
+ )} + {hasReasoningTokens && ( +
+ Reasoning: + + {formatTokenCountString(metadata.reasoningTokens!)} + +
+ )} + {hasCachedInputTokens && ( +
+ Cached: + + {formatTokenCountString(metadata.cachedInputTokens!)} + +
+ )} +
+ {(hasDuration || hasBillingMultiplier) && ( + <> +
+
+ {hasDuration && ( +
+ Duration: + + {formatDuration(metadata.duration!)} + +
+ )} + {hasBillingMultiplier && ( +
+ Multiplier: + {metadata.billingMultiplier!}ร— +
+ )} +
+ + )} +
+
+
+ ) +} diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx index 216bd1b16..45dd6e6e6 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx @@ -1,13 +1,12 @@ import { Spring } from "@follow/components/constants/spring.js" import { stopPropagation, thenable } from "@follow/utils" -import type { UIDataTypes, UIMessage } from "ai" import type { LexicalEditor, SerializedEditorState } from "lexical" import { AnimatePresence, m } from "motion/react" import * as React from "react" import { useEditingMessageId, useSetEditingMessageId } from "~/modules/ai-chat/atoms/session" import { useChatActions } from "~/modules/ai-chat/store/hooks" -import type { AIChatContextBlock, BizUIMetadata, BizUITools } from "~/modules/ai-chat/store/types" +import type { AIChatContextBlock, BizUIMessage } from "~/modules/ai-chat/store/types" import type { RichTextPart } from "../../types/ChatSession" import { convertLexicalToMarkdown } from "../../utils/lexical-markdown" @@ -16,7 +15,7 @@ import { EditableMessage } from "./EditableMessage" import { UserMessageParts } from "./UserMessageParts" interface UserChatMessageProps { - message: UIMessage + message: BizUIMessage } export const UserChatMessage: React.FC = React.memo(({ message }) => { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts index c7eb2414a..e9e8c68d6 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts @@ -157,8 +157,6 @@ class AIPersistServiceStatic { messages .filter((message) => message.parts.length > 0) .map((message) => { - const convertedParts = message.parts as any[] - return { id: message.id, chatId, @@ -169,7 +167,7 @@ class AIPersistServiceStatic { finishedAt: message.metadata?.finishTime ? new Date(message.metadata.finishTime) : undefined, - messageParts: convertedParts, + messageParts: message.parts, metadata: message.metadata, } as typeof aiChatMessagesTable.$inferInsert }), diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts index c2f67fe79..a7ba5b9c3 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts @@ -12,7 +12,7 @@ import type { ChatSlice } from "./types" export class ChatSliceActions { constructor( private params: Parameters>, - private chatInstance: ZustandChat, + private chatInstance: ZustandChat, ) { return autoBindThis(this) } @@ -182,7 +182,7 @@ export class ChatSliceActions { this.chatInstance.destroy() // Create new chat instance - const newChatInstance = new ZustandChat( + const newChatInstance = new ZustandChat( { id: newChatId, messages: [], @@ -221,7 +221,7 @@ export class ChatSliceActions { this.chatInstance.destroy() // Create new chat instance with loaded messages - const newChatInstance = new ZustandChat( + const newChatInstance = new ZustandChat( { id: chatId, messages, diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-instance.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-instance.ts index 84183105c..6b9d9a086 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-instance.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-instance.ts @@ -6,12 +6,12 @@ import { ZustandChatState } from "./chat-state" import type { ChatSlice } from "./types" // Custom Chat class that uses Zustand-integrated state -export class ZustandChat extends AbstractChat { - override state: ZustandChatState +export class ZustandChat extends AbstractChat { + override state: ZustandChatState #unsubscribeFns: (() => void)[] = [] constructor( - { messages, ...init }: ChatInit, + { messages, ...init }: ChatInit, updateZustandState: (updater: (state: ChatSlice) => ChatSlice) => void, ) { const state = new ZustandChatState(messages, updateZustandState, init.id || "") diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts index 53a69c420..41b980fd2 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts @@ -1,5 +1,7 @@ +/* eslint-disable unicorn/no-for-loop */ import type { ChatState, ChatStatus } from "ai" import { throttle } from "es-toolkit/compat" +import { produce } from "immer" import { AIPersistService } from "../../services" import { ChatStateEventEmitter } from "../event-system/event-emitter" @@ -7,14 +9,14 @@ import type { BizUIMessage } from "../types" import type { ChatSlice } from "./types" // Zustand Chat State that implements AI SDK ChatState interface -export class ZustandChatState implements ChatState { - #messages: UI_MESSAGE[] +export class ZustandChatState implements ChatState { + #messages: BizUIMessage[] #status: ChatStatus = "ready" #error: Error | undefined = undefined - #eventEmitter = new ChatStateEventEmitter() + #eventEmitter = new ChatStateEventEmitter() constructor( - initialMessages: UI_MESSAGE[] = [], + initialMessages: BizUIMessage[] = [], private updateZustandState: (updater: (state: ChatSlice) => ChatSlice) => void, private chatId: string, ) { @@ -25,10 +27,36 @@ export class ZustandChatState implements ChatSt #setupEventHandlers(): void { // Setup event handlers for automatic Zustand synchronization this.#eventEmitter.on("messages", ({ messages }) => { - this.updateZustandState((state) => ({ - ...state, - messages: [...messages], - })) + this.updateZustandState( + produce((state) => { + const stateMessages = state.messages + for (let i = 0; i < messages.length; i++) { + const message = messages[i]! + if (!stateMessages[i]) { + stateMessages[i] = structuredClone(message) as any + } else { + const stateMessage = stateMessages[i]! + stateMessage.id = message.id + + for (let j = 0; j < message.parts.length; j++) { + const statePart = stateMessage.parts[j] || {} + const messagePart = message.parts[j]! + + Object.assign(statePart, messagePart) + stateMessage.parts[j] = statePart as any + } + + stateMessage.parts.length = message.parts.length + + stateMessage.role = message.role + + stateMessage.metadata = stateMessage.metadata ?? {} + Object.assign(stateMessage.metadata, message.metadata) + } + } + stateMessages.length = messages.length + }), + ) }) this.#eventEmitter.on("status", ({ status }) => { @@ -69,19 +97,20 @@ export class ZustandChatState implements ChatSt this.#eventEmitter.emit("error", { error: newError }) } - get messages(): UI_MESSAGE[] { + get messages(): BizUIMessage[] { return this.#messages } - set messages(newMessages: UI_MESSAGE[]) { + set messages(newMessages: BizUIMessage[]) { this.#messages = [...newMessages] + this.#eventEmitter.emit("messages", { messages: this.#messages }) // Auto-persist messages when they change this.#persistMessages() } - pushMessage = (message: UI_MESSAGE) => { + pushMessage = (message: BizUIMessage) => { this.messages = this.#messages.concat(message) } @@ -91,7 +120,7 @@ export class ZustandChatState implements ChatSt this.messages = this.#messages.slice(0, -1) } - replaceMessage = (index: number, message: UI_MESSAGE) => { + replaceMessage = (index: number, message: BizUIMessage) => { if (index < 0 || index >= this.#messages.length) return this.messages = [ @@ -123,7 +152,7 @@ export class ZustandChatState implements ChatSt } // Internal event subscription with payload access - onMessagesChange = (listener: (messages: UI_MESSAGE[]) => void): (() => void) => { + onMessagesChange = (listener: (messages: BizUIMessage[]) => void): (() => void) => { return this.#eventEmitter.on("messages", ({ messages }) => listener(messages)) } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/event-system/event-emitter.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/event-system/event-emitter.ts index c35d76a70..09f105e82 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/event-system/event-emitter.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/event-system/event-emitter.ts @@ -2,12 +2,12 @@ import type { BizUIMessage } from "../types" import type { ChatStateEvents, ChatStateEventType } from "./types" // Event emitter for AI chat state changes with typed payloads -export class ChatStateEventEmitter { +export class ChatStateEventEmitter { #listeners = new Map void>>() on( event: T, - listener: (payload: ChatStateEvents[T]) => void, + listener: (payload: ChatStateEvents[T]) => void, ): () => void { if (!this.#listeners.has(event)) { this.#listeners.set(event, new Set()) @@ -20,7 +20,7 @@ export class ChatStateEventEmitter { } } - emit(event: T, payload: ChatStateEvents[T]): void { + emit(event: T, payload: ChatStateEvents[T]): void { this.#listeners.get(event)?.forEach((listener) => { try { listener(payload) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts index 800b924b5..6bebd46c7 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts @@ -7,14 +7,13 @@ import { ChatSliceActions } from "../chat-core/chat-actions" import { ZustandChat } from "../chat-core/chat-instance" import type { ChatSlice } from "../chat-core/types" import { createChatTransport } from "../transport" -import type { BizUIMessage } from "../types" export const createChatSlice: StateCreator = (...params) => { const [set, get] = params const chatId = nanoid() // Create chat instance with Zustand integration - const chatInstance = new ZustandChat( + const chatInstance = new ZustandChat( { id: chatId, messages: [], diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts index 79567486f..4df1bce26 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts @@ -5,9 +5,9 @@ export interface FileAttachment { name: string type: string size: number - dataUrl: string + dataUrl?: string previewUrl?: string - uploadStatus: "processing" | "uploading" | "completed" | "error" + uploadStatus?: "processing" | "uploading" | "completed" | "error" serverUrl?: string errorMessage?: string /** Upload progress percentage (0-100) */ diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/UsageAnalysisSection.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/UsageAnalysisSection.tsx index e16b3d99a..e5fceeec6 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/UsageAnalysisSection.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/UsageAnalysisSection.tsx @@ -5,7 +5,7 @@ import { useModalStack } from "~/components/ui/modal/stacked/hooks" import { useAIConfiguration } from "~/modules/ai-chat/hooks/useAIConfiguration" import { DetailedUsageModal, UsageProgressRing, UsageWarningBanner } from "./components" -import { formatTimeRemaining, formatTokenCount } from "./utils" +import { formatTimeRemaining, formatTokenCountString } from "./utils" export const UsageAnalysisSection = () => { const { t } = useTranslation("ai") @@ -57,8 +57,7 @@ export const UsageAnalysisSection = () => {
- {formatTokenCount(rateLimit.remainingTokens).value} - {formatTokenCount(rateLimit.remainingTokens).unit} + {formatTokenCountString(rateLimit.remainingTokens)} {t("usage_analysis.tokens_remaining")} @@ -66,9 +65,7 @@ export const UsageAnalysisSection = () => {
- {formatTokenCount(usage.used).value} - {formatTokenCount(usage.used).unit} / {formatTokenCount(usage.total).value} - {formatTokenCount(usage.total).unit} used + {formatTokenCountString(usage.used)} / {formatTokenCountString(usage.total)} used
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/DetailedUsageModal.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/DetailedUsageModal.tsx index b9074d623..95eff41c9 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/DetailedUsageModal.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/DetailedUsageModal.tsx @@ -78,6 +78,11 @@ export const DetailedUsageModal = () => { }) const maxHourCount = Math.max(1, ...hourBuckets) + const formattedUsageTokens = formatTokenCount(usage.used) + const formattedRemainingTokens = formatTokenCount(rateLimit.remainingTokens) + const formattedTotalTokens = formatTokenCount(usage.total) + const formattedAvgDaily = formatTokenCount(avgDaily) + return (

@@ -106,15 +111,15 @@ export const DetailedUsageModal = () => {

@@ -122,8 +127,8 @@ export const DetailedUsageModal = () => {
@@ -139,8 +144,8 @@ export const DetailedUsageModal = () => {
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/EfficiencyTab.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/EfficiencyTab.tsx index 9fdc2b312..7e3456f4b 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/EfficiencyTab.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/EfficiencyTab.tsx @@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@follow/components/ui/ import type { ModelPattern } from "@follow-app/client-sdk" import { useTranslation } from "react-i18next" -import { formatTokenCount } from "../utils" +import { formatTokenCount, formatTokenCountString } from "../utils" import { BarList } from "./charts" interface EfficiencyTabProps { @@ -22,12 +22,15 @@ export const EfficiencyTab = ({ byModel }: EfficiencyTabProps) => { {byModel?.length > 0 ? ( ({ - label: m.model ?? "unknown", - value: m.avgEfficiency || 0, - right: `${formatTokenCount(m.totalTokens ?? 0).value}${formatTokenCount(m.totalTokens ?? 0).unit}`, - }))} - format={(v) => `${formatTokenCount(v).value}${formatTokenCount(v).unit}`} + data={byModel.map((m) => { + const formatted = formatTokenCount(m.totalTokens ?? 0) + return { + label: m.model ?? "unknown", + value: m.avgEfficiency || 0, + right: `${formatted.value}${formatted.unit}`, + } + })} + format={(v) => formatTokenCountString(v)} /> ) : (
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/OverviewTab.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/OverviewTab.tsx index c38078484..549298be3 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/OverviewTab.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/OverviewTab.tsx @@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@follow/components/ui/ import type { DailyPattern } from "@follow-app/client-sdk" import { useTranslation } from "react-i18next" -import { formatTokenCount } from "../utils" +import { formatTokenCountString } from "../utils" import { Sparkline } from "./charts" interface OverviewTabProps { @@ -33,10 +33,7 @@ export const OverviewTab = ({ dailyTotals, peakDay }: OverviewTabProps) => { {peakDay?.date ? ( {t("analytics.peak", { defaultValue: "Peak" })}: - - {formatTokenCount(peakDay.totalTokens).value} - {formatTokenCount(peakDay.totalTokens).unit} - + {formatTokenCountString(peakDay.totalTokens)} {" ยท "} {new Date(peakDay.date).toLocaleDateString()} diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/PatternsTab.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/PatternsTab.tsx index a15678085..a2ac4ef73 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/PatternsTab.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/PatternsTab.tsx @@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@follow/components/ui/ import type { UsagePattern } from "@follow-app/client-sdk" import { useTranslation } from "react-i18next" -import { formatTokenCount } from "../utils" +import { formatTokenCountString } from "../utils" import { BarList, TinyBars } from "./charts" interface PatternsTabProps { @@ -53,7 +53,8 @@ export const PatternsTab = ({ hourBuckets, maxHourCount, byOperation }: Patterns data={byOperation.map((o) => ({ label: o.operationType ?? "unknown", value: o.percentage || 0, - right: `${formatTokenCount(o.totalTokens ?? 0).value}${formatTokenCount(o.totalTokens ?? 0).unit}`, + + right: formatTokenCountString(o.totalTokens ?? 0), }))} suffix="%" /> diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/utils.ts b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/utils.ts index a3ac767df..97bb64c7c 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/utils.ts +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/utils.ts @@ -15,3 +15,8 @@ export const formatTimeRemaining = (ms: number): string => { const rem = minutes % 60 return rem ? `${hours}h ${rem}m` : `${hours}h` } + +export const formatTokenCountString = (count: number): string => { + const formatted = formatTokenCount(count) + return `${formatted.value}${formatted.unit}` +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 312f2d9b1..02c27100a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -768,8 +768,8 @@ importers: specifier: workspace:* version: link:../../../../packages/internal/utils '@folo-services/ai-tools': - specifier: 0.2.20 - version: 0.2.20(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) + specifier: 0.2.31 + version: 0.2.31(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) '@types/node': specifier: 24.0.10 version: 24.0.10 @@ -1967,20 +1967,20 @@ packages: graphql: optional: true + '@ai-sdk/gateway@1.0.15': + resolution: {integrity: sha512-xySXoQ29+KbGuGfmDnABx+O6vc7Gj7qugmj1kGpn0rW0rQNn6UKUuvscKMzWyv1Uv05GyC1vqHq8ZhEOLfXscQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/gateway@1.0.2': resolution: {integrity: sha512-RCE6v6/7JMzSHmjB0R7oWT54y/VYeEv+6NYLtkQKdxN871UawFFHpL7TW7IRnn+/SHdRlr7XoWxxGdHEaGRKiA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4 - '@ai-sdk/openai@2.0.2': - resolution: {integrity: sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4 - - '@ai-sdk/provider-utils@3.0.0': - resolution: {integrity: sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw==} + '@ai-sdk/openai@2.0.22': + resolution: {integrity: sha512-qjSIPL5+LNM9flcBPeR64ZWeAZdYg4XWkAK34H3FaY61dSbuIaeqFPSzmQUrxotVcphAzgfL5tuYRqRYP2ZYyg==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4 @@ -1991,6 +1991,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + '@ai-sdk/provider-utils@3.0.7': + resolution: {integrity: sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/provider@2.0.0': resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} engines: {node: '>=18'} @@ -4048,20 +4054,17 @@ packages: '@follow-app/client-sdk@0.3.48': resolution: {integrity: sha512-nPRnKQwzTTvPaIMOfZierMi9+TUlxc2his1FrGmkXOUbyMe6VtnChMtxHWU03Kdb2mnCJrVmOzJa0CwW6NAvmQ==} - '@folo-services/ai-tools@0.2.20': - resolution: {integrity: sha512-1AJI48H2g64JhuTruSqQKUrn4Tc353hMFdKAbohh65AxTAMN/1pIw9w17Dtjm+g5BVlZUXYKaiIvaN9tHsXLNA==} + '@folo-services/ai-tools@0.2.31': + resolution: {integrity: sha512-oqTVMBc6UCLOt/3M9GX4I+DJ8WT2U8Hlok1MN/Sruw8sQu0ZvtoDvkuiJEtssOQAGDifzHm7z8AVmOgNoGrKUg==} '@folo-services/constants@0.1.27': resolution: {integrity: sha512-ZCTgPvLiNfKiOU3E7m0x6Y6w/8PIkEHVl7C5pH+Tt36DrM81vHZjy+zPnZISgjOd3DYTJFHTOCI2ReFRx8Ir3g==} - '@folo-services/drizzle@0.1.13': - resolution: {integrity: sha512-h6tndGnTLrAmfvxK5XsXGdZaMi2GBmFZBFiBqtabC7Y05A1ZSOaWg/d9f60Ydh586LmVonz5VGrhvzTdn5ukhg==} - '@folo-services/drizzle@0.1.21': resolution: {integrity: sha512-yJAPJz05h2uio5wPOHYbzaeFhJNn5UU9S4HPLQa3L8DNE9IG9DMsNAJqFTEpXQPwEQOf2n3k8FCF8GP/SygA7w==} - '@folo-services/exceptions@0.1.11': - resolution: {integrity: sha512-OMKat8KcIxjw9pBfG3pC1kRcIs0YiuaYmMDA2MRy8AEGcki8F9u5e5QH7OKgP/LlNR7Fs2Csz1XetvcC4nB+RQ==} + '@folo-services/drizzle@0.1.22': + resolution: {integrity: sha512-n4L0bBA5AxqJn5K3k+BTzmtuZh90LAO7G6zALCo3H+BOsnMsQ1OCsQH/xFg7m6ICQ80LJVbHdUN6b4ZKUoQJ/g==} '@folo-services/exceptions@0.1.16': resolution: {integrity: sha512-ThmLLe1Dv2CFsDIgcuY6kJhYJnjiQ7UEsewaqc25MkuImDabhzWnnad+3zRnJGLpveaMeEZ69uqUsTbKC+fR7g==} @@ -7002,6 +7005,12 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ai@5.0.26: + resolution: {integrity: sha512-bGNtG+nYQ2U+5mzuLbxIg9WxGQJ2u5jv2gYgP8C+CJ1YI4qqIjvjOgGEZWzvNet8jiOGIlqstsht9aQefKzmBw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + ai@5.0.4: resolution: {integrity: sha512-nizyqRxFlJShCAh/xPqor9EYpuaiGqCcYqau9A2GaSL+di+Qf6pA+Hos5x1orlodmT+EciZfoC2Sq+I12OmIUg==} engines: {node: '>=18'} @@ -9428,6 +9437,10 @@ packages: resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==} engines: {node: '>=20.0.0'} + eventsource-parser@3.0.5: + resolution: {integrity: sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==} + engines: {node: '>=20.0.0'} + evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} @@ -15845,6 +15858,12 @@ snapshots: optionalDependencies: graphql: 16.8.1 + '@ai-sdk/gateway@1.0.15(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.7(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/gateway@1.0.2(zod@3.25.75)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -15857,20 +15876,12 @@ snapshots: '@ai-sdk/provider-utils': 3.0.1(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/openai@2.0.2(zod@3.25.76)': + '@ai-sdk/openai@2.0.22(zod@3.25.76)': dependencies: '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.0(zod@3.25.76) + '@ai-sdk/provider-utils': 3.0.7(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/provider-utils@3.0.0(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 2.0.0 - '@standard-schema/spec': 1.0.0 - eventsource-parser: 3.0.3 - zod: 3.25.76 - zod-to-json-schema: 3.24.5(zod@3.25.76) - '@ai-sdk/provider-utils@3.0.1(zod@3.25.75)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -15887,6 +15898,13 @@ snapshots: zod: 3.25.76 zod-to-json-schema: 3.24.5(zod@3.25.76) + '@ai-sdk/provider-utils@3.0.7(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.5 + zod: 3.25.76 + '@ai-sdk/provider@2.0.0': dependencies: json-schema: 0.4.0 @@ -18973,11 +18991,11 @@ snapshots: - sql.js - sqlite3 - '@folo-services/ai-tools@0.2.20(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)': + '@folo-services/ai-tools@0.2.31(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)': dependencies: - '@ai-sdk/openai': 2.0.2(zod@3.25.76) - '@folo-services/drizzle': 0.1.13(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2) - ai: 5.0.4(zod@3.25.76) + '@ai-sdk/openai': 2.0.22(zod@3.25.76) + '@folo-services/drizzle': 0.1.22(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2) + ai: 5.0.26(zod@3.25.76) drizzle-orm: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) zod: 3.25.76 transitivePeerDependencies: @@ -19016,45 +19034,6 @@ snapshots: dependencies: zod: 3.25.76 - '@folo-services/drizzle@0.1.13(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)': - dependencies: - '@folo-services/exceptions': 0.1.11 - drizzle-orm: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) - drizzle-zod: 0.7.1(drizzle-orm@0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3))(zod@3.25.76) - nanoid: 5.1.5 - pg: 8.16.3 - zod: 3.25.76 - transitivePeerDependencies: - - '@aws-sdk/client-rds-data' - - '@cloudflare/workers-types' - - '@electric-sql/pglite' - - '@libsql/client' - - '@libsql/client-wasm' - - '@neondatabase/serverless' - - '@op-engineering/op-sqlite' - - '@opentelemetry/api' - - '@planetscale/database' - - '@prisma/client' - - '@tidbcloud/serverless' - - '@types/better-sqlite3' - - '@types/pg' - - '@types/sql.js' - - '@upstash/redis' - - '@vercel/postgres' - - '@xata.io/client' - - better-sqlite3 - - bun-types - - expo-sqlite - - gel - - knex - - kysely - - mysql2 - - pg-native - - postgres - - prisma - - sql.js - - sqlite3 - '@folo-services/drizzle@0.1.21(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)': dependencies: '@folo-services/exceptions': 0.1.16 @@ -19094,7 +19073,44 @@ snapshots: - sql.js - sqlite3 - '@folo-services/exceptions@0.1.11': {} + '@folo-services/drizzle@0.1.22(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)': + dependencies: + '@folo-services/exceptions': 0.1.16 + drizzle-orm: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) + drizzle-zod: 0.7.1(drizzle-orm@0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3))(zod@3.25.76) + nanoid: 5.1.5 + pg: 8.16.3 + zod: 3.25.76 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg-native + - postgres + - prisma + - sql.js + - sqlite3 '@folo-services/exceptions@0.1.16': {} @@ -22653,6 +22669,14 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 + ai@5.0.26(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 1.0.15(zod@3.25.76) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.7(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + ai@5.0.4(zod@3.25.75): dependencies: '@ai-sdk/gateway': 1.0.2(zod@3.25.75) @@ -25633,6 +25657,8 @@ snapshots: eventsource-parser@3.0.3: {} + eventsource-parser@3.0.5: {} + evp_bytestokey@1.0.3: dependencies: md5.js: 1.3.5